@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.
- package/dist/cli/config/paths.d.ts +15 -0
- package/dist/cli/config/paths.d.ts.map +1 -1
- package/dist/cli/config/paths.js +5 -0
- package/dist/cli/config/paths.js.map +1 -1
- package/dist/cli/config/work-sessions-base-registry.d.ts +61 -0
- package/dist/cli/config/work-sessions-base-registry.d.ts.map +1 -0
- package/dist/cli/config/work-sessions-base-registry.js +50 -0
- package/dist/cli/config/work-sessions-base-registry.js.map +1 -0
- package/dist/cli/templates/agents/content/core/sfag-orchestrator.d.ts.map +1 -1
- package/dist/cli/templates/agents/content/core/sfag-orchestrator.js +34 -6
- package/dist/cli/templates/agents/content/core/sfag-orchestrator.js.map +1 -1
- package/dist/cli/templates/agents/content/core/sfag-ticket-implementer.d.ts.map +1 -1
- package/dist/cli/templates/agents/content/core/sfag-ticket-implementer.js +37 -6
- package/dist/cli/templates/agents/content/core/sfag-ticket-implementer.js.map +1 -1
- package/dist/tools/core/git-injection.d.ts +45 -26
- package/dist/tools/core/git-injection.d.ts.map +1 -1
- package/dist/tools/core/git-injection.js +35 -37
- package/dist/tools/core/git-injection.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +50 -7
- package/dist/tools/index.js.map +1 -1
- package/node_modules/@specforge/api-types/package.json +1 -1
- package/node_modules/@specforge/report-types/package.json +1 -1
- package/node_modules/@specforge/session-types/CHANGELOG.md +4 -0
- package/node_modules/@specforge/session-types/dist/index.d.ts +1 -1
- package/node_modules/@specforge/session-types/dist/index.d.ts.map +1 -1
- package/node_modules/@specforge/session-types/dist/index.js +1 -1
- package/node_modules/@specforge/session-types/dist/index.js.map +1 -1
- package/node_modules/@specforge/session-types/dist/runtime/implementation-config.d.ts.map +1 -1
- package/node_modules/@specforge/session-types/dist/runtime/implementation-config.js +3 -1
- package/node_modules/@specforge/session-types/dist/runtime/implementation-config.js.map +1 -1
- package/node_modules/@specforge/session-types/dist/runtime/lifecycle-contract.d.ts +27 -1
- package/node_modules/@specforge/session-types/dist/runtime/lifecycle-contract.d.ts.map +1 -1
- package/node_modules/@specforge/session-types/dist/runtime/work-session-file-change.d.ts +7 -1
- package/node_modules/@specforge/session-types/dist/runtime/work-session-file-change.d.ts.map +1 -1
- package/node_modules/@specforge/session-types/dist/schema/index.d.ts +1 -1
- package/node_modules/@specforge/session-types/dist/schema/index.d.ts.map +1 -1
- package/node_modules/@specforge/session-types/dist/schema/index.js +1 -1
- package/node_modules/@specforge/session-types/dist/schema/index.js.map +1 -1
- package/node_modules/@specforge/session-types/dist/schema/work-session-file-change.d.ts +99 -0
- package/node_modules/@specforge/session-types/dist/schema/work-session-file-change.d.ts.map +1 -1
- package/node_modules/@specforge/session-types/dist/schema/work-session-file-change.js +36 -0
- package/node_modules/@specforge/session-types/dist/schema/work-session-file-change.js.map +1 -1
- package/node_modules/@specforge/session-types/dist/schema/work-session.d.ts +17 -0
- package/node_modules/@specforge/session-types/dist/schema/work-session.d.ts.map +1 -1
- package/node_modules/@specforge/session-types/dist/schema/work-session.js +15 -0
- package/node_modules/@specforge/session-types/dist/schema/work-session.js.map +1 -1
- package/node_modules/@specforge/session-types/package.json +1 -1
- package/package.json +6 -6
- package/src/cli/templates/agents/content/core/sfag-orchestrator.ts +34 -6
- package/src/cli/templates/agents/content/core/sfag-ticket-implementer.ts +37 -6
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
46
|
-
* injects
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
|
68
|
+
export declare function deriveCommittedFileChanges(nameStatus: string | null): CommittedFileChange[];
|
|
51
69
|
/**
|
|
52
|
-
* Read the
|
|
53
|
-
* the `
|
|
54
|
-
* 'local
|
|
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
|
|
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":"
|
|
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
|
|
32
|
-
if (!
|
|
33
|
-
const
|
|
34
|
-
for (const raw of
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
44
|
-
if (!
|
|
45
|
-
|
|
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
|
|
56
|
+
return changes;
|
|
56
57
|
}
|
|
57
|
-
function
|
|
58
|
-
return
|
|
58
|
+
function readCommittedFileChanges(base, run = defaultGitRunner) {
|
|
59
|
+
if (!base) return [];
|
|
60
|
+
return deriveCommittedFileChanges(run(["diff", "--name-status", `${base}..HEAD`]));
|
|
59
61
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
if (
|
|
64
|
-
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/tools/index.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
|
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
|
|
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) => {
|