@specforge/canary-cli 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cli/config/paths.d.ts +15 -0
  2. package/dist/cli/config/paths.d.ts.map +1 -1
  3. package/dist/cli/config/paths.js +5 -0
  4. package/dist/cli/config/paths.js.map +1 -1
  5. package/dist/cli/config/work-sessions-base-registry.d.ts +61 -0
  6. package/dist/cli/config/work-sessions-base-registry.d.ts.map +1 -0
  7. package/dist/cli/config/work-sessions-base-registry.js +50 -0
  8. package/dist/cli/config/work-sessions-base-registry.js.map +1 -0
  9. package/dist/cli/templates/agents/content/core/sfag-orchestrator.d.ts.map +1 -1
  10. package/dist/cli/templates/agents/content/core/sfag-orchestrator.js +17 -1
  11. package/dist/cli/templates/agents/content/core/sfag-orchestrator.js.map +1 -1
  12. package/dist/cli/templates/agents/content/core/sfag-ticket-implementer.d.ts.map +1 -1
  13. package/dist/cli/templates/agents/content/core/sfag-ticket-implementer.js +12 -3
  14. package/dist/cli/templates/agents/content/core/sfag-ticket-implementer.js.map +1 -1
  15. package/dist/tools/core/git-injection.d.ts +28 -23
  16. package/dist/tools/core/git-injection.d.ts.map +1 -1
  17. package/dist/tools/core/git-injection.js +24 -35
  18. package/dist/tools/core/git-injection.js.map +1 -1
  19. package/dist/tools/index.d.ts.map +1 -1
  20. package/dist/tools/index.js +40 -7
  21. package/dist/tools/index.js.map +1 -1
  22. package/node_modules/@specforge/api-types/package.json +1 -1
  23. package/node_modules/@specforge/report-types/package.json +1 -1
  24. package/node_modules/@specforge/session-types/CHANGELOG.md +4 -0
  25. package/node_modules/@specforge/session-types/dist/index.d.ts +1 -1
  26. package/node_modules/@specforge/session-types/dist/index.d.ts.map +1 -1
  27. package/node_modules/@specforge/session-types/dist/index.js +1 -1
  28. package/node_modules/@specforge/session-types/dist/index.js.map +1 -1
  29. package/node_modules/@specforge/session-types/dist/runtime/implementation-config.d.ts.map +1 -1
  30. package/node_modules/@specforge/session-types/dist/runtime/implementation-config.js +3 -1
  31. package/node_modules/@specforge/session-types/dist/runtime/implementation-config.js.map +1 -1
  32. package/node_modules/@specforge/session-types/dist/runtime/lifecycle-contract.d.ts +27 -1
  33. package/node_modules/@specforge/session-types/dist/runtime/lifecycle-contract.d.ts.map +1 -1
  34. package/node_modules/@specforge/session-types/dist/runtime/work-session-file-change.d.ts +7 -1
  35. package/node_modules/@specforge/session-types/dist/runtime/work-session-file-change.d.ts.map +1 -1
  36. package/node_modules/@specforge/session-types/dist/schema/index.d.ts +1 -1
  37. package/node_modules/@specforge/session-types/dist/schema/index.d.ts.map +1 -1
  38. package/node_modules/@specforge/session-types/dist/schema/index.js +1 -1
  39. package/node_modules/@specforge/session-types/dist/schema/index.js.map +1 -1
  40. package/node_modules/@specforge/session-types/dist/schema/work-session-file-change.d.ts +99 -0
  41. package/node_modules/@specforge/session-types/dist/schema/work-session-file-change.d.ts.map +1 -1
  42. package/node_modules/@specforge/session-types/dist/schema/work-session-file-change.js +36 -0
  43. package/node_modules/@specforge/session-types/dist/schema/work-session-file-change.js.map +1 -1
  44. package/node_modules/@specforge/session-types/dist/schema/work-session.d.ts +17 -0
  45. package/node_modules/@specforge/session-types/dist/schema/work-session.d.ts.map +1 -1
  46. package/node_modules/@specforge/session-types/dist/schema/work-session.js +15 -0
  47. package/node_modules/@specforge/session-types/dist/schema/work-session.js.map +1 -1
  48. package/node_modules/@specforge/session-types/package.json +1 -1
  49. package/package.json +7 -7
  50. package/src/cli/templates/agents/content/core/sfag-orchestrator.ts +17 -1
  51. package/src/cli/templates/agents/content/core/sfag-ticket-implementer.ts +12 -3
@@ -36,53 +36,42 @@ function readHeadCommitSha(run = defaultGitRunner) {
36
36
  const sha = run(["rev-parse", "HEAD"]);
37
37
  return sha === null || sha === "" ? null : sha;
38
38
  }
39
- function deriveLocalFileChanges(statusPorcelain, ctx) {
40
- if (!statusPorcelain) return [];
41
- const actions = [];
42
- for (const raw of statusPorcelain.split("\n")) {
39
+ function deriveCommittedFileChanges(nameStatus) {
40
+ if (!nameStatus) return [];
41
+ const changes = [];
42
+ for (const raw of nameStatus.split("\n")) {
43
43
  const line = raw.replace(/\r$/, "");
44
44
  if (line.trim() === "") continue;
45
- const xy = line.slice(0, 2);
46
- let path = line.slice(3).trim();
47
- const arrow = path.indexOf(" -> ");
48
- if (arrow >= 0) path = path.slice(arrow + 4).trim();
49
- if (path.startsWith('"') && path.endsWith('"')) path = path.slice(1, -1);
45
+ const cols = line.split(" ");
46
+ const code = cols[0]?.trim() ?? "";
47
+ if (code === "") continue;
48
+ const letter = code[0];
49
+ const isRenameOrCopy = letter === "R" || letter === "C";
50
+ const path = (isRenameOrCopy ? cols[cols.length - 1] : cols[1])?.trim() ?? "";
50
51
  if (path === "") continue;
51
- const kind = classifyPorcelain(xy);
52
- if (!kind) continue;
53
- actions.push({
54
- action: "record_file_change",
55
- workSessionId: ctx.workSessionId,
56
- ticketId: ctx.ticketId,
57
- expectedPath: path,
58
- expectedAction: kind.expected,
59
- actualAction: kind.actual,
60
- status: "matched"
61
- });
52
+ const action = classifyDiffStatus(letter);
53
+ if (!action) continue;
54
+ changes.push({ path, action });
62
55
  }
63
- return actions;
56
+ return changes;
64
57
  }
65
- function readLocalFileChanges(ctx, run = defaultGitRunner) {
66
- return deriveLocalFileChanges(run(["status", "--porcelain"]), ctx);
58
+ function readCommittedFileChanges(base, run = defaultGitRunner) {
59
+ if (!base) return [];
60
+ return deriveCommittedFileChanges(run(["diff", "--name-status", `${base}..HEAD`]));
67
61
  }
68
- function classifyPorcelain(xy) {
69
- const s = xy.trim();
70
- if (s === "") return null;
71
- if (s === "??" || s.includes("A") || s.includes("R") || s.includes("C")) {
72
- return { expected: "create", actual: "created" };
73
- }
74
- if (s.includes("D")) return { expected: "delete", actual: "deleted" };
75
- if (s.includes("M") || s.includes("T") || s.includes("U")) {
76
- return { expected: "modify", actual: "modified" };
77
- }
62
+ function classifyDiffStatus(letter) {
63
+ if (!letter) return null;
64
+ if (letter === "A" || letter === "R" || letter === "C") return "create";
65
+ if (letter === "D") return "delete";
66
+ if (letter === "M" || letter === "T" || letter === "U") return "modify";
78
67
  return null;
79
68
  }
80
69
  export {
81
70
  defaultGitRunner,
82
- deriveLocalFileChanges,
71
+ deriveCommittedFileChanges,
83
72
  makeGitRunner,
73
+ readCommittedFileChanges,
84
74
  readHeadCommitSha,
85
- readLocalFileChanges,
86
75
  readWorktreeGitEvidence
87
76
  };
88
77
  //# sourceMappingURL=git-injection.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/tools/core/git-injection.ts"],"sourcesContent":["/**\n * MCP-local git-evidence injection (M23.2).\n *\n * The work lifecycle's SWS/AWS verbs need worktree git evidence that only the\n * MCP-LOCAL (this CLI, running on the agent's machine — the deployed Lambda has\n * NO worktree) can observe. Per `docs/architecture/work-lifecycle-flow.md` §0 +\n * [[project_coherence_score]]:\n *\n * - **SWS** (`start_work_session`): runs `git status` for the clean-workspace\n * precondition (`gitClean`) and reports the worktree provenance —\n * `worktreePath` via `git rev-parse --show-toplevel` + `branch` via\n * `git rev-parse --abbrev-ref HEAD` — so SWS persists\n * `WorkSession.worktreePath`/`branch` (M16.2). When the agent is NOT inside a\n * git worktree (\"caso esteja em uma\") the provenance is `null` (SWS still\n * records the WorkSession) and `gitClean` defaults to `true` (a non-git caller\n * is never \"dirty\" — it must not be blocked by the precondition).\n * - **AWS** (`action_work_session`, `validateFiles: 'local'`): runs\n * `git status --porcelain` and injects one `record_file_change` action per\n * created/modified/deleted file (the agent only reports the `referenced`\n * files git cannot see).\n *\n * The git commands run through an injectable `GitRunner` (defaults to a real,\n * non-throwing `git` spawn) so the layer is unit-testable without a live repo and\n * never throws inside a tool handler.\n */\nimport { execFileSync } from 'node:child_process';\n\n/**\n * Runs a `git` sub-command; returns trimmed stdout, or `null` on any failure.\n * `cwd` (MB.45) selects the worktree the command resolves against — omitted, git\n * runs in the MCP server's `process.cwd()`. An in-process worker fleet shares ONE\n * server (ONE cwd), so a per-call `cwd` is the only way each worker's git-evidence\n * binds to ITS worktree rather than the launcher's (feedback c1537bdc).\n */\nexport type GitRunner = (args: string[], cwd?: string) => string | null;\n\n/** Default runner: spawn `git` (optionally in `cwd`), trimmed stdout, `null` on any failure. */\nexport const defaultGitRunner: GitRunner = (args, cwd) => {\n try {\n const out = execFileSync('git', args, {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n // A non-existent `cwd` makes execFileSync throw (ENOENT) → caught → null, which\n // the readers treat as \"not in a worktree\". Omit the key entirely when unset so\n // the default process.cwd() behavior is byte-identical to before MB.45.\n ...(cwd !== undefined && cwd !== '' && { cwd }),\n });\n return out.trim();\n } catch {\n return null;\n }\n};\n\n/**\n * Bind a `GitRunner` to a worktree `cwd` (MB.45). The SWS/CWS tool handlers pass the\n * worker's `worktree` arg here so every git read (`readWorktreeGitEvidence`,\n * `readHeadCommitSha`) resolves in THAT worktree, not the shared server cwd. Returns\n * `base` UNCHANGED when `cwd` is absent (the historical process.cwd() behavior), so a\n * non-fleet / repo-less caller is unaffected. `base` is injectable for tests.\n */\nexport function makeGitRunner(cwd?: string, base: GitRunner = defaultGitRunner): GitRunner {\n if (cwd === undefined || cwd === '') return base;\n return (args) => base(args, cwd);\n}\n\nexport interface WorktreeGitEvidence {\n /**\n * `true` when the worktree has no uncommitted changes — OR when the caller is\n * not inside a git repo (a non-git caller is never \"dirty\", so SWS still runs).\n */\n gitClean: boolean;\n /** Worktree root (`git rev-parse --show-toplevel`); `null` when not in a worktree. */\n worktreePath: string | null;\n /** Current branch (`git rev-parse --abbrev-ref HEAD`); `null` when not in a worktree. */\n branch: string | null;\n}\n\n/**\n * SWS git evidence — the clean-status precondition input + the worktree\n * provenance (M23.2). Emits `null` provenance (and `gitClean: true`) when the\n * agent is not inside a git worktree, so SWS still records the WorkSession.\n */\nexport function readWorktreeGitEvidence(run: GitRunner = defaultGitRunner): WorktreeGitEvidence {\n const worktreePath = run(['rev-parse', '--show-toplevel']);\n if (worktreePath === null || worktreePath === '') {\n // Not inside a git worktree — SWS still records the WorkSession.\n return { gitClean: true, worktreePath: null, branch: null };\n }\n const branch = run(['rev-parse', '--abbrev-ref', 'HEAD']);\n const status = run(['status', '--porcelain']);\n return {\n // A failed `git status` (null) must not block SWS → treat as clean.\n gitClean: status === null ? true : status.trim() === '',\n worktreePath,\n branch: branch === null || branch === '' ? null : branch,\n };\n}\n\n/**\n * The worktree HEAD commit hash (`git rev-parse HEAD`), or `null` when not inside a\n * git worktree or the repo has no commit yet. The MCP-local reads this on\n * `complete_work_session` and binds it as `WorkSession.commitSha` — the per-session\n * commit proof the CWS git-evidence gate requires to finalize (M23.2). The deployed\n * Lambda has no worktree, so only this CLI can observe it.\n */\nexport function readHeadCommitSha(run: GitRunner = defaultGitRunner): string | null {\n const sha = run(['rev-parse', 'HEAD']);\n return sha === null || sha === '' ? null : sha;\n}\n\n/** One injected file-change action (a subset of the AWS `record_file_change` payload). */\nexport interface LocalFileChangeAction {\n action: 'record_file_change';\n workSessionId: string;\n ticketId: string;\n expectedPath: string;\n expectedAction: 'create' | 'modify' | 'delete';\n actualAction: 'created' | 'modified' | 'deleted';\n status: 'matched';\n}\n\ninterface DeriveCtx {\n workSessionId: string;\n ticketId: string;\n}\n\n/**\n * Parse `git status --porcelain` into the `record_file_change` set the MCP-local\n * injects in `validateFiles: 'local'` mode — one action per created/modified/\n * deleted path (the agent reports the `referenced` files itself). A rename (`R`)\n * surfaces as a create of the new path.\n */\nexport function deriveLocalFileChanges(\n statusPorcelain: string | null,\n ctx: DeriveCtx,\n): LocalFileChangeAction[] {\n if (!statusPorcelain) return [];\n const actions: LocalFileChangeAction[] = [];\n for (const raw of statusPorcelain.split('\\n')) {\n const line = raw.replace(/\\r$/, '');\n if (line.trim() === '') continue;\n // porcelain v1: `XY<space>path` — XY is the 2-char status, path starts at col 3.\n const xy = line.slice(0, 2);\n let path = line.slice(3).trim();\n // Rename/copy renders as `old -> new`; record the new path.\n const arrow = path.indexOf(' -> ');\n if (arrow >= 0) path = path.slice(arrow + 4).trim();\n // git quotes paths with special chars.\n if (path.startsWith('\"') && path.endsWith('\"')) path = path.slice(1, -1);\n if (path === '') continue;\n const kind = classifyPorcelain(xy);\n if (!kind) continue;\n actions.push({\n action: 'record_file_change',\n workSessionId: ctx.workSessionId,\n ticketId: ctx.ticketId,\n expectedPath: path,\n expectedAction: kind.expected,\n actualAction: kind.actual,\n status: 'matched',\n });\n }\n return actions;\n}\n\n/**\n * Read the worktree's changed-file set (`git status --porcelain`) and map it to\n * the `record_file_change` actions the MCP-local injects (AWS `validateFiles:\n * 'local'`). Empty when not inside a git worktree.\n */\nexport function readLocalFileChanges(ctx: DeriveCtx, run: GitRunner = defaultGitRunner): LocalFileChangeAction[] {\n return deriveLocalFileChanges(run(['status', '--porcelain']), ctx);\n}\n\nfunction classifyPorcelain(\n xy: string,\n): { expected: 'create' | 'modify' | 'delete'; actual: 'created' | 'modified' | 'deleted' } | null {\n const s = xy.trim();\n if (s === '') return null;\n // Untracked / added / renamed / copied → a new (created) file.\n if (s === '??' || s.includes('A') || s.includes('R') || s.includes('C')) {\n return { expected: 'create', actual: 'created' };\n }\n if (s.includes('D')) return { expected: 'delete', actual: 'deleted' };\n if (s.includes('M') || s.includes('T') || s.includes('U')) {\n return { expected: 'modify', actual: 'modified' };\n }\n return null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/tools/core/git-injection.ts"],"sourcesContent":["/**\n * MCP-local git-evidence injection (M23.2 + MB.46).\n *\n * The work lifecycle's SWS/CWS verbs need worktree git evidence that only the\n * MCP-LOCAL (this CLI, running on the agent's machine — the deployed Lambda has\n * NO worktree) can observe. Per `docs/architecture/work-lifecycle-flow.md` §0 +\n * [[project_coherence_score]]:\n *\n * - **SWS** (`start_work_session`): runs `git status` for the clean-workspace\n * precondition (`gitClean`) and reports the worktree provenance —\n * `worktreePath` via `git rev-parse --show-toplevel` + `branch` via\n * `git rev-parse --abbrev-ref HEAD` — so SWS persists\n * `WorkSession.worktreePath`/`branch` (M16.2). When the agent is NOT inside a\n * git worktree (\"caso esteja em uma\") the provenance is `null` (SWS still\n * records the WorkSession) and `gitClean` defaults to `true` (a non-git caller\n * is never \"dirty\" — it must not be blocked by the precondition). SWS also\n * captures the base HEAD (`readHeadCommitSha`) as the two-dot anchor for the\n * CWS commit diff (MB.46.4 / D3).\n * - **CWS** (`complete_work_session`): reads the HEAD commit hash\n * (`readHeadCommitSha`, bound as `WorkSession.commitSha`) AND derives the REAL\n * changed-file set from the commit diff `git diff --name-status <base>..HEAD`\n * (`readCommittedFileChanges`, MB.46.4 / F2 / B-estreito). The commit diff — NOT\n * the working tree — is the correct surface: the SWS forces the tree clean and\n * the worker commits before CWS, so `git status` would capture nothing. The base\n * comes from the CLI's own local per-worktree registry (D3b), stashed at SWS.\n *\n * The git commands run through an injectable `GitRunner` (defaults to a real,\n * non-throwing `git` spawn) so the layer is unit-testable without a live repo and\n * never throws inside a tool handler.\n */\nimport { execFileSync } from 'node:child_process';\nimport type { ExpectedAction } from '@specforge/session-types';\n\n/**\n * Runs a `git` sub-command; returns trimmed stdout, or `null` on any failure.\n * `cwd` (MB.45) selects the worktree the command resolves against — omitted, git\n * runs in the MCP server's `process.cwd()`. An in-process worker fleet shares ONE\n * server (ONE cwd), so a per-call `cwd` is the only way each worker's git-evidence\n * binds to ITS worktree rather than the launcher's (feedback c1537bdc).\n */\nexport type GitRunner = (args: string[], cwd?: string) => string | null;\n\n/** Default runner: spawn `git` (optionally in `cwd`), trimmed stdout, `null` on any failure. */\nexport const defaultGitRunner: GitRunner = (args, cwd) => {\n try {\n const out = execFileSync('git', args, {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n // A non-existent `cwd` makes execFileSync throw (ENOENT) → caught → null, which\n // the readers treat as \"not in a worktree\". Omit the key entirely when unset so\n // the default process.cwd() behavior is byte-identical to before MB.45.\n ...(cwd !== undefined && cwd !== '' && { cwd }),\n });\n return out.trim();\n } catch {\n return null;\n }\n};\n\n/**\n * Bind a `GitRunner` to a worktree `cwd` (MB.45). The SWS/CWS tool handlers pass the\n * worker's `worktree` arg here so every git read (`readWorktreeGitEvidence`,\n * `readHeadCommitSha`) resolves in THAT worktree, not the shared server cwd. Returns\n * `base` UNCHANGED when `cwd` is absent (the historical process.cwd() behavior), so a\n * non-fleet / repo-less caller is unaffected. `base` is injectable for tests.\n */\nexport function makeGitRunner(cwd?: string, base: GitRunner = defaultGitRunner): GitRunner {\n if (cwd === undefined || cwd === '') return base;\n return (args) => base(args, cwd);\n}\n\nexport interface WorktreeGitEvidence {\n /**\n * `true` when the worktree has no uncommitted changes — OR when the caller is\n * not inside a git repo (a non-git caller is never \"dirty\", so SWS still runs).\n */\n gitClean: boolean;\n /** Worktree root (`git rev-parse --show-toplevel`); `null` when not in a worktree. */\n worktreePath: string | null;\n /** Current branch (`git rev-parse --abbrev-ref HEAD`); `null` when not in a worktree. */\n branch: string | null;\n}\n\n/**\n * SWS git evidence — the clean-status precondition input + the worktree\n * provenance (M23.2). Emits `null` provenance (and `gitClean: true`) when the\n * agent is not inside a git worktree, so SWS still records the WorkSession.\n */\nexport function readWorktreeGitEvidence(run: GitRunner = defaultGitRunner): WorktreeGitEvidence {\n const worktreePath = run(['rev-parse', '--show-toplevel']);\n if (worktreePath === null || worktreePath === '') {\n // Not inside a git worktree — SWS still records the WorkSession.\n return { gitClean: true, worktreePath: null, branch: null };\n }\n const branch = run(['rev-parse', '--abbrev-ref', 'HEAD']);\n const status = run(['status', '--porcelain']);\n return {\n // A failed `git status` (null) must not block SWS → treat as clean.\n gitClean: status === null ? true : status.trim() === '',\n worktreePath,\n branch: branch === null || branch === '' ? null : branch,\n };\n}\n\n/**\n * The worktree HEAD commit hash (`git rev-parse HEAD`), or `null` when not inside a\n * git worktree or the repo has no commit yet. The MCP-local reads this on\n * `complete_work_session` and binds it as `WorkSession.commitSha` — the per-session\n * commit proof the CWS git-evidence gate requires to finalize (M23.2). The deployed\n * Lambda has no worktree, so only this CLI can observe it.\n */\nexport function readHeadCommitSha(run: GitRunner = defaultGitRunner): string | null {\n const sha = run(['rev-parse', 'HEAD']);\n return sha === null || sha === '' ? null : sha;\n}\n\n/**\n * One entry of the REAL changed-file set the MCP-local derives from the commit\n * diff and injects as the CWS `localFileChanges` batch (MB.46.4). `action` mirrors\n * the plan's expected-action vocabulary (a strict subset — never `reference`),\n * so the array is assignable to `CompleteWorkSessionPayload.localFileChanges`.\n */\nexport interface CommittedFileChange {\n path: string;\n action: Extract<ExpectedAction, 'create' | 'modify' | 'delete'>;\n}\n\n/**\n * Parse `git diff --name-status <base>..HEAD` output into the changed-file set the\n * MCP-local injects as the CWS `localFileChanges` batch (MB.46.4 / F2). This is the\n * COMMIT diff — the correct surface — not the working tree (`git status`), which the\n * SWS forces clean and the worker empties by committing before CWS. Two-dot\n * `base..HEAD` measures the net set of the worker's commits since the session base.\n *\n * `--name-status` columns are TAB-separated: `A\\tpath` (add → create),\n * `M\\tpath`/`T\\tpath` (modify), `D\\tpath` (delete), and rename/copy carry a\n * similarity suffix + two paths `R100\\told\\tnew` / `C100\\told\\tnew` — surfaced as a\n * create of the NEW path (parity with the retired porcelain reader). Empty when the\n * diff is empty or unavailable (`null`).\n */\nexport function deriveCommittedFileChanges(nameStatus: string | null): CommittedFileChange[] {\n if (!nameStatus) return [];\n const changes: CommittedFileChange[] = [];\n for (const raw of nameStatus.split('\\n')) {\n const line = raw.replace(/\\r$/, '');\n if (line.trim() === '') continue;\n const cols = line.split('\\t');\n const code = cols[0]?.trim() ?? '';\n if (code === '') continue;\n const letter = code[0];\n // Rename (R) / copy (C) carry `old\\tnew` — record the NEW path (last column).\n const isRenameOrCopy = letter === 'R' || letter === 'C';\n const path = (isRenameOrCopy ? cols[cols.length - 1] : cols[1])?.trim() ?? '';\n if (path === '') continue;\n const action = classifyDiffStatus(letter);\n if (!action) continue;\n changes.push({ path, action });\n }\n return changes;\n}\n\n/**\n * Read the commit diff `git diff --name-status <base>..HEAD` through the runner and\n * map it to the CWS `localFileChanges` batch (MB.46.4). `base` is the SWS-captured\n * HEAD recovered from the CLI's local per-worktree registry (D3b). Empty when not\n * inside a git worktree or the diff is unavailable. `run` must be bound to the\n * worker's worktree cwd (MB.45, via `makeGitRunner`).\n */\nexport function readCommittedFileChanges(base: string, run: GitRunner = defaultGitRunner): CommittedFileChange[] {\n if (!base) return [];\n return deriveCommittedFileChanges(run(['diff', '--name-status', `${base}..HEAD`]));\n}\n\nfunction classifyDiffStatus(\n letter: string | undefined,\n): CommittedFileChange['action'] | null {\n if (!letter) return null;\n // Add / rename / copy → a new (created) file (the rename's new path).\n if (letter === 'A' || letter === 'R' || letter === 'C') return 'create';\n if (letter === 'D') return 'delete';\n // Modify / type-change / unmerged → modify.\n if (letter === 'M' || letter === 'T' || letter === 'U') return 'modify';\n return null;\n}\n"],"mappings":"AA8BA,SAAS,oBAAoB;AAatB,MAAM,mBAA8B,CAAC,MAAM,QAAQ;AACxD,MAAI;AACF,UAAM,MAAM,aAAa,OAAO,MAAM;AAAA,MACpC,UAAU;AAAA,MACV,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA,MAIlC,GAAI,QAAQ,UAAa,QAAQ,MAAM,EAAE,IAAI;AAAA,IAC/C,CAAC;AACD,WAAO,IAAI,KAAK;AAAA,EAClB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,SAAS,cAAc,KAAc,OAAkB,kBAA6B;AACzF,MAAI,QAAQ,UAAa,QAAQ,GAAI,QAAO;AAC5C,SAAO,CAAC,SAAS,KAAK,MAAM,GAAG;AACjC;AAmBO,SAAS,wBAAwB,MAAiB,kBAAuC;AAC9F,QAAM,eAAe,IAAI,CAAC,aAAa,iBAAiB,CAAC;AACzD,MAAI,iBAAiB,QAAQ,iBAAiB,IAAI;AAEhD,WAAO,EAAE,UAAU,MAAM,cAAc,MAAM,QAAQ,KAAK;AAAA,EAC5D;AACA,QAAM,SAAS,IAAI,CAAC,aAAa,gBAAgB,MAAM,CAAC;AACxD,QAAM,SAAS,IAAI,CAAC,UAAU,aAAa,CAAC;AAC5C,SAAO;AAAA;AAAA,IAEL,UAAU,WAAW,OAAO,OAAO,OAAO,KAAK,MAAM;AAAA,IACrD;AAAA,IACA,QAAQ,WAAW,QAAQ,WAAW,KAAK,OAAO;AAAA,EACpD;AACF;AASO,SAAS,kBAAkB,MAAiB,kBAAiC;AAClF,QAAM,MAAM,IAAI,CAAC,aAAa,MAAM,CAAC;AACrC,SAAO,QAAQ,QAAQ,QAAQ,KAAK,OAAO;AAC7C;AA0BO,SAAS,2BAA2B,YAAkD;AAC3F,MAAI,CAAC,WAAY,QAAO,CAAC;AACzB,QAAM,UAAiC,CAAC;AACxC,aAAW,OAAO,WAAW,MAAM,IAAI,GAAG;AACxC,UAAM,OAAO,IAAI,QAAQ,OAAO,EAAE;AAClC,QAAI,KAAK,KAAK,MAAM,GAAI;AACxB,UAAM,OAAO,KAAK,MAAM,GAAI;AAC5B,UAAM,OAAO,KAAK,CAAC,GAAG,KAAK,KAAK;AAChC,QAAI,SAAS,GAAI;AACjB,UAAM,SAAS,KAAK,CAAC;AAErB,UAAM,iBAAiB,WAAW,OAAO,WAAW;AACpD,UAAM,QAAQ,iBAAiB,KAAK,KAAK,SAAS,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK;AAC3E,QAAI,SAAS,GAAI;AACjB,UAAM,SAAS,mBAAmB,MAAM;AACxC,QAAI,CAAC,OAAQ;AACb,YAAQ,KAAK,EAAE,MAAM,OAAO,CAAC;AAAA,EAC/B;AACA,SAAO;AACT;AASO,SAAS,yBAAyB,MAAc,MAAiB,kBAAyC;AAC/G,MAAI,CAAC,KAAM,QAAO,CAAC;AACnB,SAAO,2BAA2B,IAAI,CAAC,QAAQ,iBAAiB,GAAG,IAAI,QAAQ,CAAC,CAAC;AACnF;AAEA,SAAS,mBACP,QACsC;AACtC,MAAI,CAAC,OAAQ,QAAO;AAEpB,MAAI,WAAW,OAAO,WAAW,OAAO,WAAW,IAAK,QAAO;AAC/D,MAAI,WAAW,IAAK,QAAO;AAE3B,MAAI,WAAW,OAAO,WAAW,OAAO,WAAW,IAAK,QAAO;AAC/D,SAAO;AACT;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAML,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAUhC;;GAEG;AACH,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,IAAI,IAAI,EAAE,CA2iCjC;AAED;;GAEG;AACH,KAAK,WAAW,GAAG,CACjB,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC1B,OAAO,CAAC,OAAO,CAAC,CAAC;AA6EtB;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,GACnB,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAkV7B;AAiBD;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,GAAE,OAAe,GACrB,OAAO,CAAC,OAAO,CAAC,CA4DlB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,GAAE,OAAe,GACrB,OAAO,CAAC,OAAO,GAAG,gBAAgB,CAAC,CAOrC;AAGD,OAAO,EACL,eAAe,EACf,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAML,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAmBhC;;GAEG;AACH,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,IAAI,IAAI,EAAE,CA2jCjC;AAED;;GAEG;AACH,KAAK,WAAW,GAAG,CACjB,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC1B,OAAO,CAAC,OAAO,CAAC,CAAC;AA6EtB;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,GACnB,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CA4W7B;AAiBD;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,GAAE,OAAe,GACrB,OAAO,CAAC,OAAO,CAAC,CA4DlB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,GAAE,OAAe,GACrB,OAAO,CAAC,OAAO,GAAG,gBAAgB,CAAC,CAOrC;AAGD,OAAO,EACL,eAAe,EACf,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAC"}
@@ -8,12 +8,21 @@ import {
8
8
  } from "../validation/index.js";
9
9
  import { injectContext, injectContextRequired } from "./core/context-helper.js";
10
10
  import { probeExistingFiles } from "./core/file-existence-injection.js";
11
- import { readWorktreeGitEvidence, readHeadCommitSha, makeGitRunner } from "./core/git-injection.js";
11
+ import {
12
+ readWorktreeGitEvidence,
13
+ readHeadCommitSha,
14
+ readCommittedFileChanges,
15
+ makeGitRunner
16
+ } from "./core/git-injection.js";
12
17
  import { tryLoadProjectConfig, saveProjectConfig } from "../cli/config/index.js";
13
18
  import {
14
19
  appendPlanningSessionRegistry,
15
20
  markPlanningSessionRegistryCompleted
16
21
  } from "../cli/config/planning-sessions-registry.js";
22
+ import {
23
+ recordWorkSessionBaseOnce,
24
+ getWorkSessionBase
25
+ } from "../cli/config/work-sessions-base-registry.js";
17
26
  function getTools() {
18
27
  const tools = [
19
28
  // ========================================================================
@@ -33,6 +42,10 @@ function getTools() {
33
42
  id: {
34
43
  type: "string",
35
44
  description: "Entity ID"
45
+ },
46
+ section: {
47
+ type: "string",
48
+ description: "What to return. 'summary' (default): title/status/counts/scores only. Or ONE section name to get just `{ id, title, <section> }` (there is no 'full' \u2014 fetch the sections you need one at a time) \u2014 specification: description, background, scope, goals, requirements, nonFunctionalRequirements, acceptanceCriteria, guardrails, architecture, techStack, folderStructures, sharedPatterns, epicTargets, fieldDeclarations, tags, dependencyTree, statusInfo, epics, patterns; epic: description, objective, scope, goals, architecture, acceptanceCriteria, validationCommands, apiContracts, sharedPatterns, fileStructures, requirementsCovered, nfrsCovered, goalsCovered, category, fieldDeclarations, statusInfo, tickets; ticket: description, filesToBeCreated, filesToBeModified, filesToBeDeleted, filesToBeReferenced, codeSnippetIds, typeSnippetIds, implementationSteps, acceptanceCriteria, testSpecification, codeReferences, typeReferences, guardrails, qualityGates, testCommands, coverageTarget, notes, fieldDeclarations, tags, dependencies. The requirement/NFR ids that epic `requirementsCovered`/`nfrsCovered` need come from section 'requirements' / 'nonFunctionalRequirements' of the specification."
36
49
  }
37
50
  },
38
51
  required: ["type", "id"]
@@ -722,7 +735,7 @@ Set getTicket: true to fetch full ticket details in the response \u2014 useful f
722
735
  },
723
736
  action: {
724
737
  type: "object",
725
- description: "The assay action to record \u2014 the AWS action vocabulary. `action` (the discriminator) is one of: mark_implementation_step_completion, mark_acceptance_check, record_test_result, add_skip_justification, amend_skip_justification, add_failure_justification, amend_failure_justification, record_file_change, add_file_change_justification, amend_file_change_justification, create_discovery (with `blocking`), update_discovery, set_discovery_resolved. Include `workSessionId` and the per-action fields. Forwarded verbatim as the lifecycle inner payload.",
738
+ description: 'The assay action to record \u2014 the AWS action vocabulary. `action` (the discriminator) is one of: mark_implementation_step_completion, mark_acceptance_check, record_test_result, add_skip_justification, amend_skip_justification, add_failure_justification, amend_failure_justification, record_file_change, add_file_change_justification, amend_file_change_justification, create_discovery (with `blocking`), update_discovery, set_discovery_resolved. Include `workSessionId` and the per-action fields. Forwarded verbatim as the lifecycle inner payload. add_file_change_justification / amend_file_change_justification: when the file is an `extra`/`mismatched` deviation, ALSO set `fulfills` to the plan item it fulfils \u2014 `{kind:"step", id:<implementation step id>, fileId:<the declared fileChangeId it replaces>}` unblocks the finalize (the plan under-declared the file); `{kind:"acceptance_criterion"|"test", id}` records why the extra exists (a rebuttal for that AC/test). amend OVERRIDES the prior `fulfills` with the corrected link.',
726
739
  properties: {
727
740
  action: {
728
741
  type: "string",
@@ -743,7 +756,17 @@ Set getTicket: true to fetch full ticket details in the response \u2014 useful f
743
756
  ],
744
757
  description: "The AWS action discriminator."
745
758
  },
746
- workSessionId: { type: "string", description: "The active WorkSession id (from start_work_session)." }
759
+ workSessionId: { type: "string", description: "The active WorkSession id (from start_work_session)." },
760
+ fulfills: {
761
+ type: "object",
762
+ description: 'add_file_change_justification / amend_file_change_justification ONLY \u2014 the link from an `extra`/`mismatched` deviation to the plan item it fulfils. `{kind:"step", id:<implementation step id>, fileId:<the declared fileChangeId this file replaces>}` suppresses the missing declared file and unblocks finalize; `{kind:"acceptance_criterion", id}` or `{kind:"test", id}` (id = the testType) records the honest reason for the extra without suppressing anything.',
763
+ properties: {
764
+ kind: { type: "string", enum: ["step", "acceptance_criterion", "test"], description: "What the deviation fulfils." },
765
+ id: { type: "string", description: "The implementation step id (kind:step), acceptance criterion id (kind:acceptance_criterion), or testType (kind:test)." },
766
+ fileId: { type: "string", description: "kind:step ONLY \u2014 the declared fileChangeId (TicketFileId) of the file this deviation replaces (may be a file declared by another step)." }
767
+ },
768
+ required: ["kind", "id"]
769
+ }
747
770
  },
748
771
  required: ["action"]
749
772
  },
@@ -1274,12 +1297,18 @@ function createToolHandlers(apiClient) {
1274
1297
  start_work_session: async (_client, args) => {
1275
1298
  validateRequired(args, "ticketId");
1276
1299
  const worktree = typeof args.worktree === "string" ? args.worktree : void 0;
1277
- const git = readWorktreeGitEvidence(makeGitRunner(worktree));
1300
+ const gitRun = makeGitRunner(worktree);
1301
+ const git = readWorktreeGitEvidence(gitRun);
1302
+ const baseCommitSha = readHeadCommitSha(gitRun);
1303
+ if (baseCommitSha !== null && typeof args.ticketId === "string") {
1304
+ recordWorkSessionBaseOnce({ ticketId: args.ticketId, worktree, baseCommitSha });
1305
+ }
1278
1306
  return await callLocal("start_work_session", {
1279
1307
  ticketId: args.ticketId,
1280
1308
  gitClean: git.gitClean,
1281
1309
  worktreePath: git.worktreePath,
1282
- branch: git.branch
1310
+ branch: git.branch,
1311
+ ...baseCommitSha !== null && { baseCommitSha }
1283
1312
  });
1284
1313
  },
1285
1314
  action_work_session: async (_client, args) => {
@@ -1292,7 +1321,10 @@ function createToolHandlers(apiClient) {
1292
1321
  complete_work_session: async (_client, args) => {
1293
1322
  validateRequired(args, "ticketId", "summary");
1294
1323
  const worktree = typeof args.worktree === "string" ? args.worktree : void 0;
1295
- const commitSha = readHeadCommitSha(makeGitRunner(worktree));
1324
+ const gitRun = makeGitRunner(worktree);
1325
+ const commitSha = readHeadCommitSha(gitRun);
1326
+ const base = typeof args.ticketId === "string" ? getWorkSessionBase(args.ticketId, worktree) : void 0;
1327
+ const localFileChanges = base ? readCommittedFileChanges(base, gitRun) : [];
1296
1328
  return await callLocal("complete_work_session", {
1297
1329
  ticketId: args.ticketId,
1298
1330
  summary: args.summary,
@@ -1301,7 +1333,8 @@ function createToolHandlers(apiClient) {
1301
1333
  filesDeleted: args.filesDeleted,
1302
1334
  actualHours: args.actualHours,
1303
1335
  validation: args.validation,
1304
- ...commitSha !== null && { commitSha }
1336
+ ...commitSha !== null && { commitSha },
1337
+ ...localFileChanges.length > 0 && { localFileChanges }
1305
1338
  });
1306
1339
  },
1307
1340
  reopen_specification: async (_client, args) => {