Skip to content

feat: add /vault-cli:session-close command#17

Merged
bborbe merged 3 commits into
masterfrom
feature/session-close
Jun 12, 2026
Merged

feat: add /vault-cli:session-close command#17
bborbe merged 3 commits into
masterfrom
feature/session-close

Conversation

@bborbe

@bborbe bborbe commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Port /session-close from ~/.claude/commands/ into the vault-cli plugin so coworkers installing vault-cli get the per-session lifecycle bookend (alongside the existing per-task and per-day trinities).

Rationale

The vault docs ([[Task Lifecycle Guide]] + [[Daily Task Workflow Guide]]) document work-on-task, sync-progress, complete-task, start-day, complete-day — all per-task and per-day vault-cli citizens. Per-session was the missing third lifecycle, living as an undocumented top-level command. This PR closes that gap.

Changes

  • commands/session-close.md — new command, ported from the top-level version with config-driven paths (no hardcoded 24 Tasks/, 23 Goals/, 50 Knowledge Base/, 65 Runbooks/)
  • README.md — add to plugin command table
  • CHANGELOG.md## Unreleased entry

Vault-Agnostic Pattern Compliance

Per [[Vault-Agnostic Slash Command Pattern]]:

  • Folder paths read from vault-cli config list --output json (tasks_dir, goals_dir, daily_dir, knowledge_dir)
  • Runbook folders auto-discovered via ^[0-9]+ [Rr]unbooks$ regex (no config field today)
  • Cross-surface integrations runtime-detected: gh, dark-factory, mcp__semantic-search__*, TaskList — all skip silently when absent
  • No hardcoded hostnames, project keys, or vault paths
  • Inline command — analyzes parent conversation (touched files, completion signals); cannot be delegated to a sub-agent

Verification grep confirms no hardcoded references in the command body.

Test plan

  • Install plugin update locally; invoke /vault-cli:session-close in this same conversation
  • Confirm verdict matches the legacy /session-close behavior (mode 1/2/3 outputs)
  • Invoke from a vault with non-default folder names (e.g. Brogrammers); confirm paths resolve correctly
  • Invoke without dark-factory installed; confirm Phase 6 skips silently
  • Invoke without gh; confirm Phase 3 git checks degrade gracefully

Follow-up (separate)

  • Remove ~/.claude/commands/session-close.md after this lands
  • Update vault Personal/CLAUDE.md and ~/.claude/CLAUDE.md to reference /vault-cli:session-close
  • Update [[CLAUDE.md Rules]] references

Port end-of-session safety check from ~/.claude/commands/session-close.md
into the vault-cli plugin so coworkers installing vault-cli get the
per-session lifecycle bookend alongside per-task and per-day trinities.

All vault-specific paths driven by `vault-cli config` (tasks_dir,
goals_dir, daily_dir, knowledge_dir); runbook folders auto-discovered
via `^[0-9]+ [Rr]unbooks$` regex (no config field). Cross-surface
checks (git, dark-factory, gh, TaskList) degrade silently when absent.

Verification grep returns no hardcoded vault paths or org-specific
identifiers in the command body.
@bborbe bborbe marked this pull request as ready for review June 12, 2026 11:24

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now let me review the key concerns from the plan systematically.

Reviewed files:

  • commands/session-close.md — new 321-line slash command
  • CHANGELOG.md — added unreleased entry
  • README.md — added two new command entries

Findings:

Security

  • Tool allowlist — The vault-cli:*, git:*, gh:*, dark-factory:*, lsof:*, find:*, ps:*, grep:*, jobs:*, command -v:* wildcards are broad, but the command's behavior is read-only + ask (Phase 9 surfaces items, never auto-commits/kills/closes). The vault-cli:* wildcard is acceptable for this vault lifecycle management command. No critical security issue.
  • Phase 3.5 sibling session detection via lsof +D / ps -o pid,cwd is documented as having false negatives ("conservative test"). Acceptable tradeoff. Not a bug.

Correctness

  • Phase 3: @{u} fallback false positivegit log --oneline @{u}.. 2>/dev/null || echo "no upstream / not pushed" will output the literal string "no upstream / not pushed" on branches without an upstream. This string is then counted as "unpushed commits" in downstream logic. This is a real correctness bug. A branch with no upstream (e.g., a local-only feature branch never pushed) would be incorrectly flagged as having unpushed commits.
  • Phase 8.6: single head -1 for orphan detectiongrep ... | head -1 finds at most one inbound linker. If a page has multiple inbound linkers and the first is the page itself, the orphan check could produce a false negative. Documented as a known limitation. Low severity — one match is sufficient for most use cases.
  • VAULT_CONFIG caching — Parsed once at command start. If vault-cli config changes mid-session, paths used are stale. This is standard caching behavior and is documented. Not a bug.

Testing

  • No test file for the new command — Slash commands in commands/ are not tested via make precommit. This is consistent with the project's existing pattern (other commands have no unit tests). Acceptable.

{
  "verdict": "request-changes",
  "summary": "The session-close slash command is well-structured and the allowlist is appropriate for its read+report+ask model. However, Phase 3 has a correctness bug: when a branch has no upstream configured, `git log @{u}..` fails silently and `echo \"no upstream / not pushed\"` is incorrectly treated as unpushed commits. Additionally, the changelog entry was placed under `## Unreleased` rather than `## v0.75.0`, inconsistent with the project's versioned-release changelog style.",
  "comments": [
    {
      "file": "commands/session-close.md",
      "line": 74,
      "severity": "major",
      "message": "Phase 3: `git log --oneline @{u}.. 2>/dev/null || echo \"no upstream / not pushed\"` — when @{u} has no upstream, the fallback echo is a plain string not an error. Downstream logic that counts \"lines\" from this output will miscount this as an unpushed commit. Fix: check upstream existence first with `git rev-parse --abbrev-ref @{u} 2>/dev/null`, or suppress the fallback echo entirely (only emit on actual unpushed commits)."
    },
    {
      "file": "CHANGELOG.md",
      "line": 11,
      "severity": "minor",
      "message": "Changelog entry is under `## Unreleased` (unreleased section). This project uses versioned releases — the entry should go under `## v0.75.0` (next minor bump from v0.74.0), consistent with the project's changelog convention. `## Unreleased` is typically reserved for unreleased changes in main, not in a feature branch PR."
    }
  ],
  "concerns_addressed": [
    "security: tool allowlist is appropriate for read+report+ask command — no auto-mutations, vault-cli:* wildcard acceptable for lifecycle management",
    "security: Phase 3.5 lsof +D sibling detection documented as conservative with false negatives — acceptable tradeoff, not a bug",
    "correctness: Phase 1 scope detection — no issue found in command doc",
    "correctness: Phase 3 git @{u}.. fallback — RAISED as major: false positive on no-upstream branches",
    "correctness: Phase 3.5 orphan worktree detection — documented false negatives acceptable",
    "correctness: Phase 8 reflect scoring thresholds — documented as arbitrary heuristic, acceptable for soft signal",
    "correctness: Phase 8.6 single grep result — documented limitation, acceptable",
    "correctness: VAULT_CONFIG cached — standard caching, documented, not a bug",
    "correctness: Phase 8.5 conversation scan — documented expense, not a bug",
    "tests: no test file — consistent with project pattern for slash commands, not a blocker"
  ]
}

Replace `git log @{u}.. || echo "no upstream"` with an explicit upstream
check via `git rev-parse --abbrev-ref @{u}` before the log call. The
fallback echo string was ambiguous — a line-counting consumer would
miscount it as an unpushed commit. Empty output now unambiguously means
"caught-up OR no upstream" — both silent-OK states.

Addresses pr-reviewer MAJOR finding on PR #17.
@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot dismissed their stale review June 12, 2026 11:39

superseded by new automated review

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
  "verdict": "request-changes",
  "summary": "The session-close command implements its behavior correctly and has exemplary output format specification, but the allowed-tools Bash patterns are overly broad for a read-only observation command — git:*, gh:*, and dark-factory:* grant write operations (push, reset, repo delete, spec approve) that the command never performs. Additionally, at 324 lines the command is 3× the recommended 50–100 line size; Phase 8 scoring tables and Phase 3.5/8.6 detection algorithms should be extracted to a skill.",
  "comments": [
    {
      "file": "commands/session-close.md",
      "line": 10,
      "severity": "major",
      "message": "Bash(git:*) is too broad for a read-only command. Grants write operations (git push, git reset --hard, git push --force) that the command never performs. Should be narrowed to read-only subcommands: git status, git log, git rev-parse, git worktree, git ls-remote, git branch, git diff only."
    },
    {
      "file": "commands/session-close.md",
      "line": 11,
      "severity": "major",
      "message": "Bash(gh:*) is too broad. Allows gh repo delete, gh api (full API access), and other write operations. The command only reads PR state. Should be narrowed to Bash(gh pr:*) or Bash(gh pr view:*) specifically."
    },
    {
      "file": "commands/session-close.md",
      "line": 16,
      "severity": "major",
      "message": "Bash(dark-factory:*) is too broad. Allows dark-factory spec approve and dark-factory daemon (state-mutating) in addition to dark-factory status (read-only). The command only calls dark-factory status. Should be Bash(dark-factory status:*) or Bash(dark-factory:status) specifically."
    },
    {
      "file": "commands/session-close.md",
      "line": 1,
      "severity": "nit",
      "message": "Command is 324 lines, exceeding the 50–100 line guideline by 3×. Phase 8 scoring tables, Phase 3.5 orphan worktree detection, and Phase 8.6 link hygiene algorithms should be extracted to a skill invoked via the Skill tool, keeping the command to phase orchestration and output formatting only."
    }
  ],
  "concerns_addressed": [
    "security: allowed-tools broad bash access — raised as major findings, patterns need narrowing to least-privilege read-only subsets",
    "correctness: Phase 1 session scope detection relies on 'edits this session' — acknowledged in command design (must stay inline), sub-agent cannot see parent conversation",
    "correctness: Phase 3 git log fallback to 'true' — the commit f3dbe83 adds upstream awareness, 19a8b05 fix confirmed upstream-aware fallback is in place",
    "correctness: Phase 3.5 orphan worktree detection uses lsof +D (Linux-only) — documented as platform-specific in Phase 3.5 description, acceptable degradation",
    "correctness: Phase 8 reflect scoring is heuristic-based — acknowledged in command, only surfaces suggestion not auto-invokes",
    "correctness: Phase 8.6 link hygiene wikilink scope — correctly scoped to VAULT_CONFIG[].path paths only, skips non-vault dirs",
    "correctness: VAULT_CONFIG caching — command parses vault-cli config list --output json at runtime detection (line 36), not cached at startup, so stale config is not an issue",
    "tests: no test files changed — command is a markdown slash command, not Go code; precommit lint would catch any issues",
    "security: VAULT_CONFIG JSON parsing — vault-cli config list --output json output is structured, subsequent commands use paths from config for ls/grep/find which are read-only operations"
  ]
}

Replace broad `Bash(git:*)`, `Bash(gh:*)`, `Bash(dark-factory:*)` with
the specific read-only subcommands the command actually invokes.

- git: status, log, rev-parse, worktree, ls-remote, branch (all read-only)
- gh: dropped — not actually used in the command body
- dark-factory: narrowed to `dark-factory status:*`

Aligns with sibling commands (sync-progress.md) and least-privilege
principle. Addresses pr-reviewer MAJOR findings on PR #17.
@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot dismissed their stale review June 12, 2026 11:46

superseded by new automated review

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff is between HEAD~1 (19a8b05) and HEAD (67a4bb6) — a security fix that narrows allowed-tools from broad patterns to specific read-only commands.

Review:

Security fix is soundgit:* and gh:* and dark-factory:* were dangerously broad. Narrowing to git status, git log, git rev-parse, git worktree, git ls-remote, git branch, and dark-factory status (read-only operations) follows least-privilege correctly.

No functional regression — the session-close command uses these tools for inspection only (git status, git log, git worktree list), not mutations. The narrow set covers all documented usages in the command.

CHANGELOG entry exists — the ## Unreleased section already describes this command.

⚠️ One minor observation: command -v was removed entirely — it was in the previous version's allowed-tools for runtime detection of gh, dark-factory, etc. The command uses command -v gh and command -v dark-factory at runtime (lines 41-44), but this tool is not in the allowed-tools list. This means those runtime checks would fail in a restricted environment. However, since the command degrades gracefully when tools are absent ("skip silently"), this is acceptable — the absence of command -v just means absent tools won't be detected, not that the command will error.

Conclusion: Approve. This is a pure security hardening with no functional impact.

{
  "verdict": "approve",
  "summary": "Security fix narrows allowed-tools from dangerously broad `git:*`, `gh:*`, `dark-factory:*` patterns to specific read-only subcommands. Correctly scoped to the actual inspection operations used in session-close (status, log, worktree list). No functional regression. CHANGELOG already updated.",
  "comments": [],
  "concerns_addressed": [
    "security: allowed-tools narrowed from git:* to git status/log/rev-parse/worktree/ls-remote/branch — only read-only operations permitted"
  ]
}

@bborbe bborbe merged commit 47b61b1 into master Jun 12, 2026
2 checks passed
@bborbe bborbe deleted the feature/session-close branch June 12, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant