@specforge/canary-cli 0.2.16 → 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 +34 -6
  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 +37 -6
  14. package/dist/cli/templates/agents/content/core/sfag-ticket-implementer.js.map +1 -1
  15. package/dist/tools/core/git-injection.d.ts +45 -26
  16. package/dist/tools/core/git-injection.d.ts.map +1 -1
  17. package/dist/tools/core/git-injection.js +35 -37
  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 +50 -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 +6 -6
  50. package/src/cli/templates/agents/content/core/sfag-orchestrator.ts +34 -6
  51. package/src/cli/templates/agents/content/core/sfag-ticket-implementer.ts +37 -6
@@ -1,7 +1,22 @@
1
- /** Runs a `git` sub-command; returns trimmed stdout, or `null` on any failure. */
2
- export type GitRunner = (args: string[]) => string | null;
3
- /** Default runner: spawn `git`, return trimmed stdout, `null` on any failure. */
1
+ import type { ExpectedAction } from '@specforge/session-types';
2
+ /**
3
+ * Runs a `git` sub-command; returns trimmed stdout, or `null` on any failure.
4
+ * `cwd` (MB.45) selects the worktree the command resolves against — omitted, git
5
+ * runs in the MCP server's `process.cwd()`. An in-process worker fleet shares ONE
6
+ * server (ONE cwd), so a per-call `cwd` is the only way each worker's git-evidence
7
+ * binds to ITS worktree rather than the launcher's (feedback c1537bdc).
8
+ */
9
+ export type GitRunner = (args: string[], cwd?: string) => string | null;
10
+ /** Default runner: spawn `git` (optionally in `cwd`), trimmed stdout, `null` on any failure. */
4
11
  export declare const defaultGitRunner: GitRunner;
12
+ /**
13
+ * Bind a `GitRunner` to a worktree `cwd` (MB.45). The SWS/CWS tool handlers pass the
14
+ * worker's `worktree` arg here so every git read (`readWorktreeGitEvidence`,
15
+ * `readHeadCommitSha`) resolves in THAT worktree, not the shared server cwd. Returns
16
+ * `base` UNCHANGED when `cwd` is absent (the historical process.cwd() behavior), so a
17
+ * non-fleet / repo-less caller is unaffected. `base` is injectable for tests.
18
+ */
19
+ export declare function makeGitRunner(cwd?: string, base?: GitRunner): GitRunner;
5
20
  export interface WorktreeGitEvidence {
6
21
  /**
7
22
  * `true` when the worktree has no uncommitted changes — OR when the caller is
@@ -27,32 +42,36 @@ export declare function readWorktreeGitEvidence(run?: GitRunner): WorktreeGitEvi
27
42
  * Lambda has no worktree, so only this CLI can observe it.
28
43
  */
29
44
  export declare function readHeadCommitSha(run?: GitRunner): string | null;
30
- /** One injected file-change action (a subset of the AWS `record_file_change` payload). */
31
- export interface LocalFileChangeAction {
32
- action: 'record_file_change';
33
- workSessionId: string;
34
- ticketId: string;
35
- expectedPath: string;
36
- expectedAction: 'create' | 'modify' | 'delete';
37
- actualAction: 'created' | 'modified' | 'deleted';
38
- status: 'matched';
39
- }
40
- interface DeriveCtx {
41
- workSessionId: string;
42
- ticketId: string;
45
+ /**
46
+ * One entry of the REAL changed-file set the MCP-local derives from the commit
47
+ * diff and injects as the CWS `localFileChanges` batch (MB.46.4). `action` mirrors
48
+ * the plan's expected-action vocabulary (a strict subset — never `reference`),
49
+ * so the array is assignable to `CompleteWorkSessionPayload.localFileChanges`.
50
+ */
51
+ export interface CommittedFileChange {
52
+ path: string;
53
+ action: Extract<ExpectedAction, 'create' | 'modify' | 'delete'>;
43
54
  }
44
55
  /**
45
- * Parse `git status --porcelain` into the `record_file_change` set the MCP-local
46
- * injects in `validateFiles: 'local'` mode one action per created/modified/
47
- * deleted path (the agent reports the `referenced` files itself). A rename (`R`)
48
- * surfaces as a create of the new path.
56
+ * Parse `git diff --name-status <base>..HEAD` output into the changed-file set the
57
+ * MCP-local injects as the CWS `localFileChanges` batch (MB.46.4 / F2). This is the
58
+ * COMMIT diff the correct surface not the working tree (`git status`), which the
59
+ * SWS forces clean and the worker empties by committing before CWS. Two-dot
60
+ * `base..HEAD` measures the net set of the worker's commits since the session base.
61
+ *
62
+ * `--name-status` columns are TAB-separated: `A\tpath` (add → create),
63
+ * `M\tpath`/`T\tpath` (modify), `D\tpath` (delete), and rename/copy carry a
64
+ * similarity suffix + two paths `R100\told\tnew` / `C100\told\tnew` — surfaced as a
65
+ * create of the NEW path (parity with the retired porcelain reader). Empty when the
66
+ * diff is empty or unavailable (`null`).
49
67
  */
50
- export declare function deriveLocalFileChanges(statusPorcelain: string | null, ctx: DeriveCtx): LocalFileChangeAction[];
68
+ export declare function deriveCommittedFileChanges(nameStatus: string | null): CommittedFileChange[];
51
69
  /**
52
- * Read the worktree's changed-file set (`git status --porcelain`) and map it to
53
- * the `record_file_change` actions the MCP-local injects (AWS `validateFiles:
54
- * 'local'`). Empty when not inside a git worktree.
70
+ * Read the commit diff `git diff --name-status <base>..HEAD` through the runner and
71
+ * map it to the CWS `localFileChanges` batch (MB.46.4). `base` is the SWS-captured
72
+ * HEAD recovered from the CLI's local per-worktree registry (D3b). Empty when not
73
+ * inside a git worktree or the diff is unavailable. `run` must be bound to the
74
+ * worker's worktree cwd (MB.45, via `makeGitRunner`).
55
75
  */
56
- export declare function readLocalFileChanges(ctx: DeriveCtx, run?: GitRunner): LocalFileChangeAction[];
57
- export {};
76
+ export declare function readCommittedFileChanges(base: string, run?: GitRunner): CommittedFileChange[];
58
77
  //# sourceMappingURL=git-injection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"git-injection.d.ts","sourceRoot":"","sources":["../../../src/tools/core/git-injection.ts"],"names":[],"mappings":"AA2BA,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,IAAI,CAAC;AAE1D,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,EAAE,SAU9B,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,sFAAsF;IACtF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,yFAAyF;IACzF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,SAA4B,GAAG,mBAAmB,CAc9F;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,SAA4B,GAAG,MAAM,GAAG,IAAI,CAGlF;AAED,0FAA0F;AAC1F,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC/C,YAAY,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;IACjD,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,UAAU,SAAS;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,GAAG,EAAE,SAAS,GACb,qBAAqB,EAAE,CA4BzB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,GAAE,SAA4B,GAAG,qBAAqB,EAAE,CAE/G"}
1
+ {"version":3,"file":"git-injection.d.ts","sourceRoot":"","sources":["../../../src/tools/core/git-injection.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAExE,gGAAgG;AAChG,eAAO,MAAM,gBAAgB,EAAE,SAc9B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,SAA4B,GAAG,SAAS,CAGzF;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,sFAAsF;IACtF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,yFAAyF;IACzF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,SAA4B,GAAG,mBAAmB,CAc9F;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,SAA4B,GAAG,MAAM,GAAG,IAAI,CAGlF;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC,CAAC;CACjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,mBAAmB,EAAE,CAmB3F;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,SAA4B,GAAG,mBAAmB,EAAE,CAG/G"}
@@ -1,15 +1,23 @@
1
1
  import { execFileSync } from "node:child_process";
2
- const defaultGitRunner = (args) => {
2
+ const defaultGitRunner = (args, cwd) => {
3
3
  try {
4
4
  const out = execFileSync("git", args, {
5
5
  encoding: "utf8",
6
- stdio: ["ignore", "pipe", "ignore"]
6
+ stdio: ["ignore", "pipe", "ignore"],
7
+ // A non-existent `cwd` makes execFileSync throw (ENOENT) → caught → null, which
8
+ // the readers treat as "not in a worktree". Omit the key entirely when unset so
9
+ // the default process.cwd() behavior is byte-identical to before MB.45.
10
+ ...cwd !== void 0 && cwd !== "" && { cwd }
7
11
  });
8
12
  return out.trim();
9
13
  } catch {
10
14
  return null;
11
15
  }
12
16
  };
17
+ function makeGitRunner(cwd, base = defaultGitRunner) {
18
+ if (cwd === void 0 || cwd === "") return base;
19
+ return (args) => base(args, cwd);
20
+ }
13
21
  function readWorktreeGitEvidence(run = defaultGitRunner) {
14
22
  const worktreePath = run(["rev-parse", "--show-toplevel"]);
15
23
  if (worktreePath === null || worktreePath === "") {
@@ -28,52 +36,42 @@ function readHeadCommitSha(run = defaultGitRunner) {
28
36
  const sha = run(["rev-parse", "HEAD"]);
29
37
  return sha === null || sha === "" ? null : sha;
30
38
  }
31
- function deriveLocalFileChanges(statusPorcelain, ctx) {
32
- if (!statusPorcelain) return [];
33
- const actions = [];
34
- 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")) {
35
43
  const line = raw.replace(/\r$/, "");
36
44
  if (line.trim() === "") continue;
37
- const xy = line.slice(0, 2);
38
- let path = line.slice(3).trim();
39
- const arrow = path.indexOf(" -> ");
40
- if (arrow >= 0) path = path.slice(arrow + 4).trim();
41
- 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() ?? "";
42
51
  if (path === "") continue;
43
- const kind = classifyPorcelain(xy);
44
- if (!kind) continue;
45
- actions.push({
46
- action: "record_file_change",
47
- workSessionId: ctx.workSessionId,
48
- ticketId: ctx.ticketId,
49
- expectedPath: path,
50
- expectedAction: kind.expected,
51
- actualAction: kind.actual,
52
- status: "matched"
53
- });
52
+ const action = classifyDiffStatus(letter);
53
+ if (!action) continue;
54
+ changes.push({ path, action });
54
55
  }
55
- return actions;
56
+ return changes;
56
57
  }
57
- function readLocalFileChanges(ctx, run = defaultGitRunner) {
58
- 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`]));
59
61
  }
60
- function classifyPorcelain(xy) {
61
- const s = xy.trim();
62
- if (s === "") return null;
63
- if (s === "??" || s.includes("A") || s.includes("R") || s.includes("C")) {
64
- return { expected: "create", actual: "created" };
65
- }
66
- if (s.includes("D")) return { expected: "delete", actual: "deleted" };
67
- if (s.includes("M") || s.includes("T") || s.includes("U")) {
68
- return { expected: "modify", actual: "modified" };
69
- }
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";
70
67
  return null;
71
68
  }
72
69
  export {
73
70
  defaultGitRunner,
74
- deriveLocalFileChanges,
71
+ deriveCommittedFileChanges,
72
+ makeGitRunner,
73
+ readCommittedFileChanges,
75
74
  readHeadCommitSha,
76
- readLocalFileChanges,
77
75
  readWorktreeGitEvidence
78
76
  };
79
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/** Runs a `git` sub-command; returns trimmed stdout, or `null` on any failure. */\nexport type GitRunner = (args: string[]) => string | null;\n\n/** Default runner: spawn `git`, return trimmed stdout, `null` on any failure. */\nexport const defaultGitRunner: GitRunner = (args) => {\n try {\n const out = execFileSync('git', args, {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n return out.trim();\n } catch {\n return null;\n }\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":"AAyBA,SAAS,oBAAoB;AAMtB,MAAM,mBAA8B,CAAC,SAAS;AACnD,MAAI;AACF,UAAM,MAAM,aAAa,OAAO,MAAM;AAAA,MACpC,UAAU;AAAA,MACV,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,IACpC,CAAC;AACD,WAAO,IAAI,KAAK;AAAA,EAClB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;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;AAwBO,SAAS,uBACd,iBACA,KACyB;AACzB,MAAI,CAAC,gBAAiB,QAAO,CAAC;AAC9B,QAAM,UAAmC,CAAC;AAC1C,aAAW,OAAO,gBAAgB,MAAM,IAAI,GAAG;AAC7C,UAAM,OAAO,IAAI,QAAQ,OAAO,EAAE;AAClC,QAAI,KAAK,KAAK,MAAM,GAAI;AAExB,UAAM,KAAK,KAAK,MAAM,GAAG,CAAC;AAC1B,QAAI,OAAO,KAAK,MAAM,CAAC,EAAE,KAAK;AAE9B,UAAM,QAAQ,KAAK,QAAQ,MAAM;AACjC,QAAI,SAAS,EAAG,QAAO,KAAK,MAAM,QAAQ,CAAC,EAAE,KAAK;AAElD,QAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,EAAG,QAAO,KAAK,MAAM,GAAG,EAAE;AACvE,QAAI,SAAS,GAAI;AACjB,UAAM,OAAO,kBAAkB,EAAE;AACjC,QAAI,CAAC,KAAM;AACX,YAAQ,KAAK;AAAA,MACX,QAAQ;AAAA,MACR,eAAe,IAAI;AAAA,MACnB,UAAU,IAAI;AAAA,MACd,cAAc;AAAA,MACd,gBAAgB,KAAK;AAAA,MACrB,cAAc,KAAK;AAAA,MACnB,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAOO,SAAS,qBAAqB,KAAgB,MAAiB,kBAA2C;AAC/G,SAAO,uBAAuB,IAAI,CAAC,UAAU,aAAa,CAAC,GAAG,GAAG;AACnE;AAEA,SAAS,kBACP,IACiG;AACjG,QAAM,IAAI,GAAG,KAAK;AAClB,MAAI,MAAM,GAAI,QAAO;AAErB,MAAI,MAAM,QAAQ,EAAE,SAAS,GAAG,KAAK,EAAE,SAAS,GAAG,KAAK,EAAE,SAAS,GAAG,GAAG;AACvE,WAAO,EAAE,UAAU,UAAU,QAAQ,UAAU;AAAA,EACjD;AACA,MAAI,EAAE,SAAS,GAAG,EAAG,QAAO,EAAE,UAAU,UAAU,QAAQ,UAAU;AACpE,MAAI,EAAE,SAAS,GAAG,KAAK,EAAE,SAAS,GAAG,KAAK,EAAE,SAAS,GAAG,GAAG;AACzD,WAAO,EAAE,UAAU,UAAU,QAAQ,WAAW;AAAA,EAClD;AACA,SAAO;AACT;","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,CAiiCjC;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,CAqU7B;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 } 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"]
@@ -684,6 +697,10 @@ Format options:
684
697
  ticketId: {
685
698
  type: "string",
686
699
  description: "The ID of the ticket to start"
700
+ },
701
+ worktree: {
702
+ type: "string",
703
+ description: "Absolute path to THIS worker's git worktree. Pass it when running as one of several concurrent workers so the git evidence (clean status, branch, commit) is read from your worktree \u2014 not the shared MCP server directory. Each concurrent worker needs its OWN worktree; two active sessions in the same worktree are denied (concurrent_worktree). Omit when you run a single session in the current directory."
687
704
  }
688
705
  },
689
706
  required: ["ticketId"]
@@ -718,7 +735,7 @@ Set getTicket: true to fetch full ticket details in the response \u2014 useful f
718
735
  },
719
736
  action: {
720
737
  type: "object",
721
- 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.',
722
739
  properties: {
723
740
  action: {
724
741
  type: "string",
@@ -739,7 +756,17 @@ Set getTicket: true to fetch full ticket details in the response \u2014 useful f
739
756
  ],
740
757
  description: "The AWS action discriminator."
741
758
  },
742
- 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
+ }
743
770
  },
744
771
  required: ["action"]
745
772
  },
@@ -810,6 +837,10 @@ Set getTicket: true to fetch full ticket details in the response \u2014 useful f
810
837
  type: "string",
811
838
  description: "The ID of the ticket"
812
839
  },
840
+ worktree: {
841
+ type: "string",
842
+ description: "Absolute path to THIS worker's git worktree \u2014 pass the SAME value you passed to start_work_session so the commit hash is read from your worktree, not the shared MCP server directory. Omit for a single session in the current directory."
843
+ },
813
844
  summary: {
814
845
  type: "string",
815
846
  description: "Summary of work completed"
@@ -1265,12 +1296,19 @@ function createToolHandlers(apiClient) {
1265
1296
  },
1266
1297
  start_work_session: async (_client, args) => {
1267
1298
  validateRequired(args, "ticketId");
1268
- const git = readWorktreeGitEvidence();
1299
+ const worktree = typeof args.worktree === "string" ? args.worktree : void 0;
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
+ }
1269
1306
  return await callLocal("start_work_session", {
1270
1307
  ticketId: args.ticketId,
1271
1308
  gitClean: git.gitClean,
1272
1309
  worktreePath: git.worktreePath,
1273
- branch: git.branch
1310
+ branch: git.branch,
1311
+ ...baseCommitSha !== null && { baseCommitSha }
1274
1312
  });
1275
1313
  },
1276
1314
  action_work_session: async (_client, args) => {
@@ -1282,7 +1320,11 @@ function createToolHandlers(apiClient) {
1282
1320
  },
1283
1321
  complete_work_session: async (_client, args) => {
1284
1322
  validateRequired(args, "ticketId", "summary");
1285
- const commitSha = readHeadCommitSha();
1323
+ const worktree = typeof args.worktree === "string" ? args.worktree : void 0;
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) : [];
1286
1328
  return await callLocal("complete_work_session", {
1287
1329
  ticketId: args.ticketId,
1288
1330
  summary: args.summary,
@@ -1291,7 +1333,8 @@ function createToolHandlers(apiClient) {
1291
1333
  filesDeleted: args.filesDeleted,
1292
1334
  actualHours: args.actualHours,
1293
1335
  validation: args.validation,
1294
- ...commitSha !== null && { commitSha }
1336
+ ...commitSha !== null && { commitSha },
1337
+ ...localFileChanges.length > 0 && { localFileChanges }
1295
1338
  });
1296
1339
  },
1297
1340
  reopen_specification: async (_client, args) => {