@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.
- 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 +17 -1
- 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 +12 -3
- package/dist/cli/templates/agents/content/core/sfag-ticket-implementer.js.map +1 -1
- package/dist/tools/core/git-injection.d.ts +28 -23
- package/dist/tools/core/git-injection.d.ts.map +1 -1
- package/dist/tools/core/git-injection.js +24 -35
- package/dist/tools/core/git-injection.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +40 -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 +7 -7
- package/src/cli/templates/agents/content/core/sfag-orchestrator.ts +17 -1
- 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
|
|
40
|
-
if (!
|
|
41
|
-
const
|
|
42
|
-
for (const raw of
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
52
|
-
if (!
|
|
53
|
-
|
|
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
|
|
56
|
+
return changes;
|
|
64
57
|
}
|
|
65
|
-
function
|
|
66
|
-
return
|
|
58
|
+
function readCommittedFileChanges(base, run = defaultGitRunner) {
|
|
59
|
+
if (!base) return [];
|
|
60
|
+
return deriveCommittedFileChanges(run(["diff", "--name-status", `${base}..HEAD`]));
|
|
67
61
|
}
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
72
|
-
|
|
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
|
-
|
|
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;
|
|
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"]
|
|
@@ -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:
|
|
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
|
|
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
|
|
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) => {
|