@specforge/canary-cli 0.2.9 → 0.2.11

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=action-planning-double-call.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-planning-double-call.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/action-planning-double-call.test.ts"],"names":[],"mappings":""}
@@ -12,11 +12,14 @@ export interface FileProbeDeps {
12
12
  /** on-disk existence predicate (defaults to non-throwing `fs.existsSync`). */
13
13
  exists?: FileExists;
14
14
  }
15
+ /** Clear the memoized worktree snapshot (tests; forces the next probe to re-shell git). */
16
+ export declare function resetWorktreeFileCache(): void;
15
17
  /**
16
- * Resolve the worktree root via `git rev-parse --show-toplevel`, then report the
17
- * subset of `paths` that exist in the repo: on-disk (`fs.existsSync`, tracked OR
18
- * untracked) ∪ tracked-but-not-on-disk (`git ls-files --error-unmatch`, the rare
19
- * staged-delete case). Returns `[]` when not inside a worktree. Never throws.
18
+ * Report the subset of `paths` that exist in the repo, answering from the
19
+ * in-process worktree snapshot (MB.36.3): tracked (`git ls-files`, covers on-disk
20
+ * tracked AND the rare staged-delete) ∪ on-disk untracked (`fs.existsSync`).
21
+ * Returns `[]` when not inside a worktree. Never throws. The git snapshot is
22
+ * memoized per process — repeated probes in a session do NOT re-shell git.
20
23
  */
21
24
  export declare function probeExistingFiles(paths: string[], deps?: FileProbeDeps): string[];
22
25
  //# sourceMappingURL=file-existence-injection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-existence-injection.d.ts","sourceRoot":"","sources":["../../../src/tools/core/file-existence-injection.ts"],"names":[],"mappings":"AAiCA,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,IAAI,CAAC;AAE1D,8EAA8E;AAC9E,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;AAEtD,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;AAEtD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,EAAE,SAU9B,CAAC;AAWF,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,EAAE,CA4BtF"}
1
+ {"version":3,"file":"file-existence-injection.d.ts","sourceRoot":"","sources":["../../../src/tools/core/file-existence-injection.ts"],"names":[],"mappings":"AAyCA,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,IAAI,CAAC;AAE1D,8EAA8E;AAC9E,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;AAEtD,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;AAEtD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,EAAE,SAU9B,CAAC;AAWF,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAcD,2FAA2F;AAC3F,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAkCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,EAAE,CA2BtF"}
@@ -19,23 +19,50 @@ const defaultFileExists = (absPath) => {
19
19
  return false;
20
20
  }
21
21
  };
22
+ let cachedSnapshot = null;
23
+ function resetWorktreeFileCache() {
24
+ cachedSnapshot = null;
25
+ }
26
+ function loadWorktreeSnapshot(run) {
27
+ if (cachedSnapshot) return cachedSnapshot;
28
+ let snapshot;
29
+ try {
30
+ const root = run(["rev-parse", "--show-toplevel"]);
31
+ if (root === null || root === "") {
32
+ snapshot = { root: null, tracked: /* @__PURE__ */ new Set() };
33
+ } else {
34
+ const listed = run(["ls-files"]);
35
+ const tracked = /* @__PURE__ */ new Set();
36
+ if (listed !== null && listed !== "") {
37
+ for (const line of listed.split("\n")) {
38
+ const t = line.trim();
39
+ if (t !== "") tracked.add(t);
40
+ }
41
+ }
42
+ snapshot = { root, tracked };
43
+ }
44
+ } catch {
45
+ snapshot = { root: null, tracked: /* @__PURE__ */ new Set() };
46
+ }
47
+ cachedSnapshot = snapshot;
48
+ return snapshot;
49
+ }
22
50
  function probeExistingFiles(paths, deps = {}) {
23
51
  try {
24
52
  if (!Array.isArray(paths) || paths.length === 0) return [];
25
53
  const run = deps.run ?? defaultGitRunner;
26
54
  const exists = deps.exists ?? defaultFileExists;
27
- const root = run(["rev-parse", "--show-toplevel"]);
28
- if (root === null || root === "") return [];
55
+ const snapshot = loadWorktreeSnapshot(run);
56
+ if (snapshot.root === null) return [];
29
57
  const present = [];
30
58
  for (const p of paths) {
31
59
  if (typeof p !== "string" || p === "") continue;
32
- const abs = isAbsolute(p) ? p : join(root, p);
33
- if (exists(abs)) {
60
+ if (snapshot.tracked.has(p)) {
34
61
  present.push(p);
35
62
  continue;
36
63
  }
37
- const tracked = run(["ls-files", "--error-unmatch", "--", abs]);
38
- if (tracked !== null && tracked !== "") present.push(p);
64
+ const abs = isAbsolute(p) ? p : join(snapshot.root, p);
65
+ if (exists(abs)) present.push(p);
39
66
  }
40
67
  return present;
41
68
  } catch {
@@ -44,6 +71,7 @@ function probeExistingFiles(paths, deps = {}) {
44
71
  }
45
72
  export {
46
73
  defaultGitRunner,
47
- probeExistingFiles
74
+ probeExistingFiles,
75
+ resetWorktreeFileCache
48
76
  };
49
77
  //# sourceMappingURL=file-existence-injection.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/tools/core/file-existence-injection.ts"],"sourcesContent":["/**\n * MCP-local file-existence injection (MB.10.5).\n *\n * The planning cross_validation gate's file-provenance model needs the real-repo\n * file set `E` (grep evidence) to resolve brownfield files a plan references but\n * no ticket creates, and to catch a `filesToBeCreated` path that ALREADY exists\n * (a modify mislabelled as create). Only the MCP-LOCAL — this CLI, running on the\n * agent's machine — can observe the worktree; the deployed Lambda has none.\n *\n * Mechanic (a REACTIVE double-call, mirroring the assay's `git-injection.ts`):\n * `complete_planning_session` first calls CPS with NO evidence. In\n * cross_validation the server replies `outcome: 'evidence_required'` carrying a\n * `grepRequest: { paths }` — the exact paths it cannot resolve spec-internally.\n * The CLI probes JUST those paths here and re-calls CPS with the existing subset\n * injected as `existingFiles` (pass-2, the committing verdict). From the agent's\n * view it stays ONE tool call.\n *\n * Trust / scope: the injected set is TRUSTED (the planning agent is cooperative,\n * unlike the assay's coherence-integrity concern) and the probe is SCOPED to\n * exactly `grepRequest.paths` — never a repo-wide scan. Paths are repo-relative\n * (as declared in the spec) and resolved against the worktree root.\n *\n * Degradation: outside a git worktree (or if `git` is unavailable) the probe\n * returns `[]`. The CLI then re-calls with `existingFiles: []` — the \"probed\"\n * marker is the PRESENCE of the field, not its contents — so pass-2 runs the\n * strict spec-internal verdict (`E = createdPaths`). A repo-less caller is never\n * blocked from completing; strict is deterministic + correct for greenfield. The\n * probe is NON-THROWING by construction.\n */\nimport { execFileSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { isAbsolute, join } from 'node:path';\n\n/** Runs a `git` sub-command; returns trimmed stdout, or `null` on any failure. */\nexport type GitRunner = (args: string[]) => string | null;\n\n/** On-disk existence predicate (absolute path). Injectable for unit tests. */\nexport type FileExists = (absPath: string) => boolean;\n\n/** Probes a set of repo-relative paths; returns the subset that exist. */\nexport type FileProbe = (paths: string[]) => string[];\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\n/** Default on-disk check: non-throwing `fs.existsSync`. */\nconst defaultFileExists: FileExists = (absPath) => {\n try {\n return existsSync(absPath);\n } catch {\n return false;\n }\n};\n\nexport interface FileProbeDeps {\n /** git sub-command runner (defaults to a real, non-throwing `git` spawn). */\n run?: GitRunner;\n /** on-disk existence predicate (defaults to non-throwing `fs.existsSync`). */\n exists?: FileExists;\n}\n\n/**\n * Resolve the worktree root via `git rev-parse --show-toplevel`, then report the\n * subset of `paths` that exist in the repo: on-disk (`fs.existsSync`, tracked OR\n * untracked) tracked-but-not-on-disk (`git ls-files --error-unmatch`, the rare\n * staged-delete case). Returns `[]` when not inside a worktree. Never throws.\n */\nexport function probeExistingFiles(paths: string[], deps: FileProbeDeps = {}): string[] {\n try {\n if (!Array.isArray(paths) || paths.length === 0) return [];\n const run = deps.run ?? defaultGitRunner;\n const exists = deps.exists ?? defaultFileExists;\n\n const root = run(['rev-parse', '--show-toplevel']);\n if (root === null || root === '') return []; // not inside a git worktree\n\n const present: string[] = [];\n for (const p of paths) {\n if (typeof p !== 'string' || p === '') continue;\n const abs = isAbsolute(p) ? p : join(root, p);\n // Primary signal: on-disk (covers both tracked and brand-new untracked).\n if (exists(abs)) {\n present.push(p);\n continue;\n }\n // Secondary: tracked-but-not-on-disk (a staged delete). `--error-unmatch`\n // makes git exit non-zero (→ `null`) when the path is not tracked.\n const tracked = run(['ls-files', '--error-unmatch', '--', abs]);\n if (tracked !== null && tracked !== '') present.push(p);\n }\n return present;\n } catch {\n // Never throw inside a tool handler — degrade to \"no evidence\" (strict gate).\n return [];\n }\n}\n"],"mappings":"AA6BA,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,YAAY,YAAY;AAY1B,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;AAGA,MAAM,oBAAgC,CAAC,YAAY;AACjD,MAAI;AACF,WAAO,WAAW,OAAO;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeO,SAAS,mBAAmB,OAAiB,OAAsB,CAAC,GAAa;AACtF,MAAI;AACF,QAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAG,QAAO,CAAC;AACzD,UAAM,MAAM,KAAK,OAAO;AACxB,UAAM,SAAS,KAAK,UAAU;AAE9B,UAAM,OAAO,IAAI,CAAC,aAAa,iBAAiB,CAAC;AACjD,QAAI,SAAS,QAAQ,SAAS,GAAI,QAAO,CAAC;AAE1C,UAAM,UAAoB,CAAC;AAC3B,eAAW,KAAK,OAAO;AACrB,UAAI,OAAO,MAAM,YAAY,MAAM,GAAI;AACvC,YAAM,MAAM,WAAW,CAAC,IAAI,IAAI,KAAK,MAAM,CAAC;AAE5C,UAAI,OAAO,GAAG,GAAG;AACf,gBAAQ,KAAK,CAAC;AACd;AAAA,MACF;AAGA,YAAM,UAAU,IAAI,CAAC,YAAY,mBAAmB,MAAM,GAAG,CAAC;AAC9D,UAAI,YAAY,QAAQ,YAAY,GAAI,SAAQ,KAAK,CAAC;AAAA,IACxD;AACA,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/tools/core/file-existence-injection.ts"],"sourcesContent":["/**\n * MCP-local file-existence injection (MB.10.5 · MB.36.3).\n *\n * The planning file-provenance model needs the real-repo file set `E` (grep\n * evidence) to resolve brownfield files a plan references but no ticket creates,\n * and to catch a `filesToBeCreated` path that ALREADY exists (a modify\n * mislabelled as create). Only the MCP-LOCAL — this CLI, running on the agent's\n * machine — can observe the worktree; the deployed Lambda has none.\n *\n * Mechanic (a REACTIVE double-call, mirroring the assay's `git-injection.ts`):\n * `complete_planning_session` AND `action_planning_session` first call with NO\n * evidence. In ticket_expansion/cross_validation the server may reply\n * `outcome: 'evidence_required'` carrying a `grepRequest: { paths }` — the exact\n * paths it cannot resolve spec-internally. The CLI probes JUST those paths here\n * and re-calls with the existing subset injected as `existingFiles` (pass-2, the\n * committing verdict). From the agent's view it stays ONE tool call.\n *\n * In-memory worktree cache (MB.36.3): planning does NOT mutate the repo — the\n * agent PLANS changes, it doesn't apply them — so the worktree file set is stable\n * for the life of the CLI process. We snapshot it ONCE (`git rev-parse\n * --show-toplevel` + a single bulk `git ls-files`) and answer every subsequent\n * probe from that in-process Set. A path first consumed by a later action (e.g. a\n * cross_validation delta) hits the cache, not a fresh `git` — so repeated probes\n * across a session never re-shell git. `resetWorktreeFileCache()` clears it (tests).\n *\n * Trust / scope: the injected set is TRUSTED (the planning agent is cooperative,\n * unlike the assay's coherence-integrity concern) and the ANSWER is SCOPED to\n * exactly `grepRequest.paths` — never a repo-wide scan is returned. Paths are\n * repo-relative (as declared in the spec) and resolved against the worktree root.\n *\n * Degradation: outside a git worktree (or if `git` is unavailable) the probe\n * returns `[]`. The CLI then re-calls with `existingFiles: []` — the \"probed\"\n * marker is the PRESENCE of the field, not its contents — so pass-2 runs the\n * strict spec-internal verdict (`E = createdPaths`). A repo-less caller is never\n * blocked from completing; strict is deterministic + correct for greenfield. The\n * probe is NON-THROWING by construction.\n */\nimport { execFileSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { isAbsolute, join } from 'node:path';\n\n/** Runs a `git` sub-command; returns trimmed stdout, or `null` on any failure. */\nexport type GitRunner = (args: string[]) => string | null;\n\n/** On-disk existence predicate (absolute path). Injectable for unit tests. */\nexport type FileExists = (absPath: string) => boolean;\n\n/** Probes a set of repo-relative paths; returns the subset that exist. */\nexport type FileProbe = (paths: string[]) => string[];\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\n/** Default on-disk check: non-throwing `fs.existsSync`. */\nconst defaultFileExists: FileExists = (absPath) => {\n try {\n return existsSync(absPath);\n } catch {\n return false;\n }\n};\n\nexport interface FileProbeDeps {\n /** git sub-command runner (defaults to a real, non-throwing `git` spawn). */\n run?: GitRunner;\n /** on-disk existence predicate (defaults to non-throwing `fs.existsSync`). */\n exists?: FileExists;\n}\n\n/**\n * In-process snapshot of the worktree (MB.36.3). `root` is `null` when not inside\n * a git worktree; `tracked` is the full set of repo-relative tracked paths.\n */\ninterface WorktreeSnapshot {\n root: string | null;\n tracked: Set<string>;\n}\n\n/** Memoized worktree snapshot — computed once per CLI process (planning is read-only). */\nlet cachedSnapshot: WorktreeSnapshot | null = null;\n\n/** Clear the memoized worktree snapshot (tests; forces the next probe to re-shell git). */\nexport function resetWorktreeFileCache(): void {\n cachedSnapshot = null;\n}\n\n/**\n * Build (or reuse) the worktree snapshot: `git rev-parse --show-toplevel` for the\n * root + a single bulk `git ls-files` for the tracked set. Computed ONCE per\n * process and cached — the worktree is stable during planning (the agent plans,\n * it doesn't mutate the repo), so every later probe answers from the Set without\n * re-shelling git. Never throws (degrades to a not-in-a-worktree snapshot).\n */\nfunction loadWorktreeSnapshot(run: GitRunner): WorktreeSnapshot {\n if (cachedSnapshot) return cachedSnapshot;\n let snapshot: WorktreeSnapshot;\n try {\n const root = run(['rev-parse', '--show-toplevel']);\n if (root === null || root === '') {\n snapshot = { root: null, tracked: new Set() };\n } else {\n const listed = run(['ls-files']);\n const tracked = new Set<string>();\n if (listed !== null && listed !== '') {\n for (const line of listed.split('\\n')) {\n const t = line.trim();\n if (t !== '') tracked.add(t);\n }\n }\n snapshot = { root, tracked };\n }\n } catch {\n snapshot = { root: null, tracked: new Set() };\n }\n cachedSnapshot = snapshot;\n return snapshot;\n}\n\n/**\n * Report the subset of `paths` that exist in the repo, answering from the\n * in-process worktree snapshot (MB.36.3): tracked (`git ls-files`, covers on-disk\n * tracked AND the rare staged-delete) ∪ on-disk untracked (`fs.existsSync`).\n * Returns `[]` when not inside a worktree. Never throws. The git snapshot is\n * memoized per process — repeated probes in a session do NOT re-shell git.\n */\nexport function probeExistingFiles(paths: string[], deps: FileProbeDeps = {}): string[] {\n try {\n if (!Array.isArray(paths) || paths.length === 0) return [];\n const run = deps.run ?? defaultGitRunner;\n const exists = deps.exists ?? defaultFileExists;\n\n const snapshot = loadWorktreeSnapshot(run);\n if (snapshot.root === null) return []; // not inside a git worktree\n\n const present: string[] = [];\n for (const p of paths) {\n if (typeof p !== 'string' || p === '') continue;\n // Primary signal: the cached tracked set (repo-relative), no git re-shell.\n if (snapshot.tracked.has(p)) {\n present.push(p);\n continue;\n }\n // Secondary: on-disk untracked (a brand-new file the agent already created,\n // not yet `git add`ed). `fs.existsSync` is a syscall, not a git spawn.\n const abs = isAbsolute(p) ? p : join(snapshot.root, p);\n if (exists(abs)) present.push(p);\n }\n return present;\n } catch {\n // Never throw inside a tool handler — degrade to \"no evidence\" (strict gate).\n return [];\n }\n}\n"],"mappings":"AAqCA,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,YAAY,YAAY;AAY1B,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;AAGA,MAAM,oBAAgC,CAAC,YAAY;AACjD,MAAI;AACF,WAAO,WAAW,OAAO;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAmBA,IAAI,iBAA0C;AAGvC,SAAS,yBAA+B;AAC7C,mBAAiB;AACnB;AASA,SAAS,qBAAqB,KAAkC;AAC9D,MAAI,eAAgB,QAAO;AAC3B,MAAI;AACJ,MAAI;AACF,UAAM,OAAO,IAAI,CAAC,aAAa,iBAAiB,CAAC;AACjD,QAAI,SAAS,QAAQ,SAAS,IAAI;AAChC,iBAAW,EAAE,MAAM,MAAM,SAAS,oBAAI,IAAI,EAAE;AAAA,IAC9C,OAAO;AACL,YAAM,SAAS,IAAI,CAAC,UAAU,CAAC;AAC/B,YAAM,UAAU,oBAAI,IAAY;AAChC,UAAI,WAAW,QAAQ,WAAW,IAAI;AACpC,mBAAW,QAAQ,OAAO,MAAM,IAAI,GAAG;AACrC,gBAAM,IAAI,KAAK,KAAK;AACpB,cAAI,MAAM,GAAI,SAAQ,IAAI,CAAC;AAAA,QAC7B;AAAA,MACF;AACA,iBAAW,EAAE,MAAM,QAAQ;AAAA,IAC7B;AAAA,EACF,QAAQ;AACN,eAAW,EAAE,MAAM,MAAM,SAAS,oBAAI,IAAI,EAAE;AAAA,EAC9C;AACA,mBAAiB;AACjB,SAAO;AACT;AASO,SAAS,mBAAmB,OAAiB,OAAsB,CAAC,GAAa;AACtF,MAAI;AACF,QAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAG,QAAO,CAAC;AACzD,UAAM,MAAM,KAAK,OAAO;AACxB,UAAM,SAAS,KAAK,UAAU;AAE9B,UAAM,WAAW,qBAAqB,GAAG;AACzC,QAAI,SAAS,SAAS,KAAM,QAAO,CAAC;AAEpC,UAAM,UAAoB,CAAC;AAC3B,eAAW,KAAK,OAAO;AACrB,UAAI,OAAO,MAAM,YAAY,MAAM,GAAI;AAEvC,UAAI,SAAS,QAAQ,IAAI,CAAC,GAAG;AAC3B,gBAAQ,KAAK,CAAC;AACd;AAAA,MACF;AAGA,YAAM,MAAM,WAAW,CAAC,IAAI,IAAI,KAAK,SAAS,MAAM,CAAC;AACrD,UAAI,OAAO,GAAG,EAAG,SAAQ,KAAK,CAAC;AAAA,IACjC;AACA,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO,CAAC;AAAA,EACV;AACF;","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,CAshCjC;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,CA8S7B;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;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,CA4hCjC;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"}
@@ -372,7 +372,7 @@ Format options:
372
372
  // create_ticket — SHELL (ticket_decomposition): epicId/title/description
373
373
  // + ticketType (the impl/verification decision, set HERE). The remaining body
374
374
  // fields are authored by the ticket node verbs in ticket_expansion;
375
- // dependencies via create_dependencies in cross_validation.
375
+ // dependencies via create_dependencies, native from ticket_expansion onward (MB.36).
376
376
  {
377
377
  properties: {
378
378
  type: { const: "create_ticket" },
@@ -409,12 +409,12 @@ Format options:
409
409
  },
410
410
  // ticket_step_actions — batch add/edit/remove/reorder of implementation steps.
411
411
  {
412
- description: "Batch-edit a ticket's implementation steps (add/edit/remove/reorder in one call). A step is a unit of FUNCTIONAL WORK \u2014 describe the functions/components/adjustments it makes AND what each does, never a bare filename. Declare the file(s) each step touches INLINE via `files: [{path, role}]`, role one of creates|modifies|deletes|imports|reads (imports couples to a build/run dependency; reads is a context-only glance). Fields that do not apply are declared with the `justify` op, never by writing empty values here.",
412
+ description: "Batch-edit a ticket's implementation steps (add/edit/remove/reorder in one call). A step is a unit of FUNCTIONAL WORK \u2014 describe the functions/components/adjustments it makes AND what each does, never a bare filename. Declare the file(s) each step touches INLINE via `files: [{path, role}]`, role one of creates|modifies|deletes|imports|reads (imports couples to a build/run dependency; reads is a context-only glance). Author code/type snippets INLINE via `codeSnippets`/`typeSnippets: [{content, language}]` \u2014 each becomes a ticket-level snippet row linked to this step (no separate snippet verb). NOTE: a call that declares snippets sets the ticket's COMPLETE snippet set, so re-declare all when editing. Fields that do not apply are declared with the `justify` op, never by writing empty values here.",
413
413
  properties: {
414
414
  type: { const: "ticket_step_actions" },
415
415
  ticketId: { type: "string", description: "Ticket id (use list_tickets / lookup_ticket to find)." },
416
- add: { type: "array", description: "New steps to append.", items: { type: "object", properties: { text: { type: "string" }, files: { type: "array", description: "Files this step touches, by role \u2014 derives the step\u2194file link + the TicketFileChange row.", items: { type: "object", properties: { path: { type: "string" }, role: { type: "string", enum: ["creates", "modifies", "deletes", "imports", "reads"] }, symbol: { type: "string" }, why: { type: "string" } }, required: ["path", "role"] } } }, required: ["text"] } },
417
- edit: { type: "array", description: "Existing steps to edit (by stepId).", items: { type: "object", properties: { stepId: { type: "string" }, text: { type: "string" }, files: { type: "array", items: { type: "object", properties: { path: { type: "string" }, role: { type: "string", enum: ["creates", "modifies", "deletes", "imports", "reads"] }, symbol: { type: "string" }, why: { type: "string" } }, required: ["path", "role"] } } }, required: ["stepId"] } },
416
+ add: { type: "array", description: "New steps to append.", items: { type: "object", properties: { text: { type: "string" }, files: { type: "array", description: "Files this step touches, by role \u2014 derives the step\u2194file link + the TicketFileChange row.", items: { type: "object", properties: { path: { type: "string" }, role: { type: "string", enum: ["creates", "modifies", "deletes", "imports", "reads"] }, symbol: { type: "string" }, why: { type: "string" } }, required: ["path", "role"] } }, codeSnippets: { type: "array", description: "Code snippets authored on this step \u2192 TicketCodeSnippet rows + step\u2194snippet links.", items: { type: "object", properties: { content: { type: "string" }, language: { type: "string" }, description: { type: "string" } }, required: ["content"] } }, typeSnippets: { type: "array", description: "Type snippets authored on this step \u2192 TicketTypeSnippet rows + step\u2194snippet links.", items: { type: "object", properties: { content: { type: "string" }, language: { type: "string" }, description: { type: "string" } }, required: ["content"] } } }, required: ["text"] } },
417
+ edit: { type: "array", description: "Existing steps to edit (by stepId).", items: { type: "object", properties: { stepId: { type: "string" }, text: { type: "string" }, files: { type: "array", items: { type: "object", properties: { path: { type: "string" }, role: { type: "string", enum: ["creates", "modifies", "deletes", "imports", "reads"] }, symbol: { type: "string" }, why: { type: "string" } }, required: ["path", "role"] } }, codeSnippets: { type: "array", description: "Code snippets authored on this step \u2192 TicketCodeSnippet rows + step\u2194snippet links.", items: { type: "object", properties: { content: { type: "string" }, language: { type: "string" }, description: { type: "string" } }, required: ["content"] } }, typeSnippets: { type: "array", description: "Type snippets authored on this step \u2192 TicketTypeSnippet rows + step\u2194snippet links.", items: { type: "object", properties: { content: { type: "string" }, language: { type: "string" }, description: { type: "string" } }, required: ["content"] } } }, required: ["stepId"] } },
418
418
  remove: { type: "array", items: { type: "string" }, description: "Step ids to remove." },
419
419
  reorder: { type: "array", items: { type: "string" }, description: "Step ids in the desired order." }
420
420
  },
@@ -459,7 +459,10 @@ Format options:
459
459
  },
460
460
  required: ["type", "id"]
461
461
  },
462
+ // create_dependencies — MB.36: native in BOTH ticket_expansion AND
463
+ // cross_validation (band, no rollback).
462
464
  {
465
+ description: "Declare ticket\u2192ticket `requires` edges in bulk (fromTicketId depends on toTicketId), validated atomically before any write. Native in BOTH ticket_expansion AND cross_validation (band \u2014 no rollback): the consumer\u2192creator ordering is declared in ticket_expansion where the files are authored, and refined in cross_validation to satisfy the dependency-graph integrity checks and widen parallelism. The file-provenance findings' remedy points at this op in ticket_expansion.",
463
466
  properties: {
464
467
  type: { const: "create_dependencies" },
465
468
  dependencies: {
@@ -479,7 +482,10 @@ Format options:
479
482
  },
480
483
  required: ["type", "dependencies"]
481
484
  },
485
+ // delete_dependencies — MB.36: native in BOTH ticket_expansion AND
486
+ // cross_validation (band, no rollback).
482
487
  {
488
+ description: "Remove dependency edges by id (`<fromTicketId>--requires--<toTicketId>`). Native in BOTH ticket_expansion AND cross_validation (band \u2014 no rollback): cut a scaffold or over-declared edge to widen the parallel layers, provided the dependency-graph integrity + file-provenance checks still hold (a protected edge is denied).",
483
489
  properties: {
484
490
  type: { const: "delete_dependencies" },
485
491
  dependencyIds: { type: "array", minItems: 1, items: { type: "string" }, description: "TicketDependency ids to remove (use lookup/list to find)." }
@@ -1218,10 +1224,19 @@ function createToolHandlers(apiClient) {
1218
1224
  if (!sessionId) {
1219
1225
  throw new Error("No active planning session. Call start_planning_session first.");
1220
1226
  }
1221
- const agentResponse = await callLocal(
1222
- "action_planning_session",
1223
- { sessionId, operation: args.operation }
1224
- );
1227
+ const first = await callLocal("action_planning_session", { sessionId, operation: args.operation });
1228
+ let agentResponse;
1229
+ if (first?.outcome === "evidence_required") {
1230
+ const requested = Array.isArray(first.grepRequest?.paths) ? first.grepRequest.paths : [];
1231
+ const existingFiles = probeExistingFiles(requested);
1232
+ agentResponse = await callLocal("action_planning_session", {
1233
+ sessionId,
1234
+ operation: args.operation,
1235
+ existingFiles
1236
+ });
1237
+ } else {
1238
+ agentResponse = first;
1239
+ }
1225
1240
  if (agentResponse?.planningStatus === "closed" && cfg?.specificationId) {
1226
1241
  saveProjectConfig({ planningSessionId: void 0 });
1227
1242
  markPlanningSessionRegistryCompleted({