@orchestrator-claude/definitions 3.36.0 → 3.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/agents/adversarial-reviewer.md +6 -1
  2. package/agents/api-extractor.md +6 -0
  3. package/agents/azure-devops-engineer.md +1 -2
  4. package/agents/business-rule-miner.md +6 -0
  5. package/agents/cicd-platform-engineer.md +1 -2
  6. package/agents/ciclo-engineer.md +15 -2
  7. package/agents/code-archaeologist.md +6 -0
  8. package/agents/debug-sidecar.md +6 -0
  9. package/agents/doc-sidecar.md +6 -0
  10. package/agents/docs-guardian.md +9 -3
  11. package/agents/frontend-engineer.md +1 -2
  12. package/agents/{ai-architect.md → harness-architect.md} +26 -5
  13. package/agents/implementer.md +6 -1
  14. package/agents/legacy-discoverer.md +6 -0
  15. package/agents/legacy-synthesizer.md +6 -0
  16. package/agents/methodology-researcher.md +6 -1
  17. package/agents/operational-artifact-builder.md +6 -1
  18. package/agents/orchestrator.md +27 -87
  19. package/agents/planner.md +6 -1
  20. package/agents/presentation-engineer.md +5 -4
  21. package/agents/python-data-engineer.md +0 -1
  22. package/agents/researcher.md +6 -0
  23. package/agents/reviewer.md +6 -1
  24. package/agents/schema-extractor.md +6 -0
  25. package/agents/software-architect.md +30 -7
  26. package/agents/specifier.md +6 -1
  27. package/agents/task-generator.md +6 -1
  28. package/agents/test-sidecar.md +6 -0
  29. package/dist/scripts/generate-hooks-matcher.d.ts +106 -0
  30. package/dist/scripts/generate-hooks-matcher.d.ts.map +1 -0
  31. package/dist/scripts/generate-hooks-matcher.js +153 -0
  32. package/dist/scripts/generate-hooks-matcher.js.map +1 -0
  33. package/dist/scripts/generate-manifest.d.ts +22 -0
  34. package/dist/scripts/generate-manifest.d.ts.map +1 -1
  35. package/dist/scripts/generate-manifest.js +67 -2
  36. package/dist/scripts/generate-manifest.js.map +1 -1
  37. package/hooks/agent-git-guard.mjs +63 -11
  38. package/hooks/dangling-guard.mjs +42 -12
  39. package/hooks/gate-guardian.mjs +38 -8
  40. package/hooks/hooks.json +3 -9
  41. package/hooks/lib/api-client.ts +128 -9
  42. package/hooks/lib/capability-scanner.ts +391 -55
  43. package/hooks/lib/dispatch-marker.ts +214 -0
  44. package/hooks/lib/git-checkpoint.ts +34 -37
  45. package/hooks/lib/observations-helper.ts +14 -1
  46. package/hooks/mailbox-listener.mjs +69 -17
  47. package/hooks/playwright-output-guard.mjs +40 -10
  48. package/hooks/post-implement.mjs +38 -8
  49. package/hooks/pre-agent-dispatch.mjs +137 -16
  50. package/hooks/pre-agent-dispatch.ts +82 -15
  51. package/hooks/session-start.mjs +274 -103
  52. package/hooks/session-start.ts +250 -69
  53. package/hooks/subagent-start.mjs +7665 -22
  54. package/hooks/subagent-start.ts +367 -16
  55. package/hooks/subagent-stop.mjs +102 -62
  56. package/hooks/subagent-stop.ts +124 -28
  57. package/hooks/user-prompt.mjs +43 -13
  58. package/hooks/verification-before-completion.mjs +63 -11
  59. package/hooks/workflow-guard.mjs +45 -11
  60. package/hooks-settings-template.json +1 -7
  61. package/manifest.json +62 -62
  62. package/package.json +4 -2
  63. package/skills/checkpoint-protocol/SKILL.md +19 -4
  64. package/skills/ciclo-lane-dispatch/SKILL.md +24 -0
  65. package/skills/fase-1b-projecao/SKILL.md +146 -0
  66. package/skills/orchestrator/SKILL.md +11 -3
  67. package/skills/workflow-coordination/SKILL.md +3 -3
  68. package/hooks/post-compact.mjs +0 -7931
  69. package/hooks/post-compact.ts +0 -170
@@ -1,38 +1,68 @@
1
1
  import { createRequire as __createRequireShim } from 'node:module'; const require = __createRequireShim(import.meta.url);
2
2
 
3
3
  // hooks/pre-agent-dispatch.ts
4
- import { execFileSync } from "node:child_process";
4
+ import { execFileSync as execFileSync2 } from "node:child_process";
5
5
 
6
6
  // hooks/lib/api-client.ts
7
7
  import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
8
- import { join as join2, dirname as dirname2 } from "node:path";
9
- import { fileURLToPath } from "node:url";
8
+ import { join as join2 } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+ import { createHash } from "node:crypto";
10
11
 
11
12
  // ../project-root-resolver/dist/index.js
12
13
  import { existsSync } from "node:fs";
13
14
  import { dirname, join } from "node:path";
15
+ function hasProjectRootMarker(candidate, sentinel) {
16
+ return existsSync(join(candidate, ".git")) || existsSync(join(candidate, "package.json")) || existsSync(join(candidate, sentinel, ".root"));
17
+ }
14
18
  function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
19
+ const candidates = [];
15
20
  let currentDir = cwd;
16
21
  for (; ; ) {
17
22
  if (existsSync(join(currentDir, sentinel))) {
18
- return currentDir;
23
+ candidates.push(currentDir);
19
24
  }
20
25
  const parentDir = dirname(currentDir);
21
26
  if (parentDir === currentDir) {
22
- return null;
27
+ break;
23
28
  }
24
29
  currentDir = parentDir;
25
30
  }
31
+ const [innermost] = candidates;
32
+ if (innermost === void 0) {
33
+ return null;
34
+ }
35
+ for (const candidate of candidates) {
36
+ if (hasProjectRootMarker(candidate, sentinel)) {
37
+ return candidate;
38
+ }
39
+ }
40
+ return innermost;
26
41
  }
27
42
 
28
43
  // hooks/lib/api-client.ts
29
- var __filename = fileURLToPath(import.meta.url);
30
- var __dirname = dirname2(__filename);
31
- var PROJECT_ROOT = join2(__dirname, "..", "..", "..");
32
- var STATE_DIR = join2(PROJECT_ROOT, ".orchestrator", ".state");
44
+ function resolveFallbackStateDir(cwd) {
45
+ const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
46
+ return join2(tmpdir(), "orchestrator-hook-fallback", cwdHash);
47
+ }
48
+ var PROJECT_ROOT = (() => {
49
+ const resolved = resolveHookProjectRoot(process.cwd());
50
+ if (resolved !== null && hasProjectRootMarker(resolved, ".orchestrator")) {
51
+ return resolved;
52
+ }
53
+ return null;
54
+ })();
55
+ var STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
56
+ var STATE_DIR = STATE_DIR_OVERRIDE !== void 0 && STATE_DIR_OVERRIDE !== "" ? STATE_DIR_OVERRIDE : PROJECT_ROOT !== null ? join2(PROJECT_ROOT, ".orchestrator", ".state") : resolveFallbackStateDir(process.cwd());
33
57
  var JWT_CACHE = join2(STATE_DIR, "jwt-token");
34
58
  var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
35
59
  mkdirSync(STATE_DIR, { recursive: true });
60
+ if ((STATE_DIR_OVERRIDE === void 0 || STATE_DIR_OVERRIDE === "") && PROJECT_ROOT === null) {
61
+ log(
62
+ "API-CLIENT",
63
+ `PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} \u2014 falling back to isolated state dir ${STATE_DIR} (RF-003)`
64
+ );
65
+ }
36
66
  var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
37
67
  var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
38
68
  var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
@@ -73,6 +103,10 @@ async function ensureProjectIdResolved(token) {
73
103
  );
74
104
  }
75
105
  }
106
+ function normalizeAgentName(subagentType) {
107
+ const i = subagentType.lastIndexOf(":");
108
+ return i === -1 ? subagentType : subagentType.slice(i + 1);
109
+ }
76
110
  function log(prefix, msg) {
77
111
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
78
112
  try {
@@ -211,6 +245,72 @@ function outputContext(hookEvent, context) {
211
245
  );
212
246
  }
213
247
 
248
+ // hooks/lib/dispatch-marker.ts
249
+ import { execFileSync } from "node:child_process";
250
+ var DISPATCH_MARKER_REF_PREFIX = "refs/orchestrator/dispatch";
251
+ var MAX_DISPATCH_MARKERS_PER_KEY = 20;
252
+ var EMPTY_COMPONENT_PLACEHOLDER = "unknown";
253
+ function sanitizeRefComponent(raw) {
254
+ const cleaned = (raw || "").replace(/[^A-Za-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
255
+ return cleaned || EMPTY_COMPONENT_PLACEHOLDER;
256
+ }
257
+ function formatMarkerStamp(d) {
258
+ return d.toISOString().replace(/[-:.]/g, "");
259
+ }
260
+ function buildMarkerRefName(branch, agent, stamp) {
261
+ const branchPart = sanitizeRefComponent(branch);
262
+ const agentPart = sanitizeRefComponent(agent);
263
+ const stampPart = sanitizeRefComponent(stamp);
264
+ return `${DISPATCH_MARKER_REF_PREFIX}/${branchPart}/${stampPart}-${agentPart}`;
265
+ }
266
+ function runGitDefault(args) {
267
+ return execFileSync("git", args, {
268
+ encoding: "utf-8",
269
+ stdio: ["ignore", "pipe", "ignore"]
270
+ });
271
+ }
272
+ function pruneOldestMarkers(prefix, cap, deps) {
273
+ const listed = deps.runGit(["for-each-ref", "--format=%(refname)", "--sort=refname", `${prefix}/`]).split("\n").map((l) => l.trim()).filter(Boolean);
274
+ const excess = listed.length - cap;
275
+ if (excess <= 0) return [];
276
+ const doomed = listed.slice(0, excess);
277
+ for (const ref of doomed) {
278
+ deps.runGit(["update-ref", "-d", ref]);
279
+ }
280
+ return doomed;
281
+ }
282
+ function writeDispatchBoundaryMarker(agentName, branch, deps) {
283
+ let ref = "";
284
+ try {
285
+ if (sanitizeRefComponent(agentName) === EMPTY_COMPONENT_PLACEHOLDER) {
286
+ return {
287
+ status: "failed",
288
+ reason: `refusing to write an unattributable marker (agent name "${agentName}")`
289
+ };
290
+ }
291
+ const sha = deps.runGit(["rev-parse", "HEAD"]).trim();
292
+ if (!/^[0-9a-f]{40}$/.test(sha)) {
293
+ return { status: "failed", reason: `HEAD did not resolve to a 40-hex sha (got "${sha}")` };
294
+ }
295
+ const stamp = formatMarkerStamp((deps.now ?? (() => /* @__PURE__ */ new Date()))());
296
+ ref = buildMarkerRefName(branch, agentName, stamp);
297
+ deps.runGit(["update-ref", ref, sha]);
298
+ let pruned = [];
299
+ try {
300
+ pruned = pruneOldestMarkers(
301
+ `${DISPATCH_MARKER_REF_PREFIX}/${sanitizeRefComponent(branch)}`,
302
+ MAX_DISPATCH_MARKERS_PER_KEY,
303
+ deps
304
+ );
305
+ } catch {
306
+ pruned = [];
307
+ }
308
+ return { status: "written", ref, sha, pruned };
309
+ } catch (err) {
310
+ return { status: "failed", reason: `${ref ? `ref="${ref}" ` : ""}${String(err)}` };
311
+ }
312
+ }
313
+
214
314
  // hooks/pre-agent-dispatch.ts
215
315
  var HOOK = "PRE-AGENT-DISPATCH";
216
316
  var KNOWN_AGENTS = [
@@ -232,10 +332,6 @@ var COMMITTING_AGENTS = [
232
332
  "frontend-engineer"
233
333
  ];
234
334
  var PROTECTED_BRANCHES = ["main", "master"];
235
- function normalizeAgentName(subagentType) {
236
- const i = subagentType.lastIndexOf(":");
237
- return i === -1 ? subagentType : subagentType.slice(i + 1);
238
- }
239
335
  function isCommittingAgent(subagentType) {
240
336
  if (!subagentType) return false;
241
337
  const t = normalizeAgentName(subagentType).toLowerCase();
@@ -274,7 +370,7 @@ function evaluateDispatch(ctx) {
274
370
  }
275
371
  function readBranchFromGit() {
276
372
  try {
277
- return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
373
+ return execFileSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
278
374
  encoding: "utf-8",
279
375
  stdio: ["ignore", "pipe", "ignore"]
280
376
  }).trim();
@@ -288,7 +384,8 @@ var defaultDeps = {
288
384
  log,
289
385
  readBranch: readBranchFromGit,
290
386
  getActiveWorkflow,
291
- writeDispatchIntent
387
+ writeDispatchIntent,
388
+ writeDispatchMarker: (agentName, branch) => writeDispatchBoundaryMarker(agentName, branch, { runGit: runGitDefault })
292
389
  };
293
390
  var DISPATCH_INTENT_RESOLVE_TIMEOUT_MS = 3e3;
294
391
  async function stampDispatchIntent(deps) {
@@ -310,6 +407,29 @@ async function stampDispatchIntent(deps) {
310
407
  deps.log(HOOK, `[TD-205] stampDispatchIntent failed (fail-open): ${String(err)}`);
311
408
  }
312
409
  }
410
+ function markDispatchBoundary(subagentType, branch, deps) {
411
+ if (!deps.writeDispatchMarker) return [];
412
+ if (!isCommittingAgent(subagentType)) return [];
413
+ try {
414
+ const outcome = deps.writeDispatchMarker(subagentType, branch);
415
+ if (outcome.status === "written") {
416
+ deps.log(HOOK, `[S445] boundary marker ${outcome.ref} -> ${outcome.sha}`);
417
+ if (outcome.pruned.length > 0) {
418
+ deps.log(HOOK, `[S445] pruned ${outcome.pruned.length} marker(s) over retention cap`);
419
+ }
420
+ return [];
421
+ }
422
+ deps.log(HOOK, `[S445] boundary marker FAILED: ${outcome.reason}`);
423
+ return [
424
+ `[BOUNDARY-MARKER][S445] Could not record the pre-dispatch git coordinate for '${subagentType}' on '${branch}': ${outcome.reason}. The dispatch proceeds (this never blocks), but there will be NO ref to 'git reset --hard' back to for this agent's work.`
425
+ ];
426
+ } catch (err) {
427
+ deps.log(HOOK, `[S445] boundary marker threw (fail-open): ${String(err)}`);
428
+ return [
429
+ `[BOUNDARY-MARKER][S445] Boundary-marker step threw for '${subagentType}': ${String(err)}. Dispatch proceeds; no rollback coordinate was recorded.`
430
+ ];
431
+ }
432
+ }
313
433
  async function mainWithDeps(deps) {
314
434
  const stdin = await deps.readStdin();
315
435
  const toolInput = getField(stdin, "tool_input") || "";
@@ -317,6 +437,7 @@ async function mainWithDeps(deps) {
317
437
  const prompt = getField(stdin, "tool_input.prompt") || getField(stdin, "tool_input.description") || (typeof toolInput === "string" ? toolInput : JSON.stringify(toolInput));
318
438
  const branch = deps.readBranch();
319
439
  const warnings = evaluateDispatch({ subagentType, prompt, branch });
440
+ warnings.push(...markDispatchBoundary(subagentType, branch, deps));
320
441
  await stampDispatchIntent(deps);
321
442
  if (warnings.length > 0) {
322
443
  deps.outputContext(HOOK, warnings.join("\n"));
@@ -340,7 +461,7 @@ export {
340
461
  isCommittingAgent,
341
462
  isProtectedBranch,
342
463
  mainWithDeps,
343
- normalizeAgentName,
464
+ markDispatchBoundary,
344
465
  parseBranchFromHead,
345
466
  stampDispatchIntent
346
467
  };
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * pre-agent-dispatch.ts — PreToolUse hook for the Agent tool
3
3
  *
4
- * NON-BLOCKING (always exits 0; only injects advisory context). Two responsibilities:
4
+ * NON-BLOCKING (always exits 0; only injects advisory context). Four responsibilities
5
+ * (the count said "Two" while listing three, from S445 back to whenever the third was
6
+ * added — corrected here rather than left to drift a fourth time):
5
7
  *
6
8
  * 1. (RFC-027 Sprint 4) Warn when a known agent is dispatched WITHOUT subagent_type,
7
9
  * so frontmatter (model/tools) loads correctly.
@@ -29,6 +31,15 @@
29
31
  * Best-effort / fail-open in every dimension (missing deps, no active workflow,
30
32
  * resolution timeout, filesystem error) — never blocks the dispatch.
31
33
  *
34
+ * 4. (S445, RFC-042) Boundary marker per ref. RFC-042 settled that a checkpoint is an
35
+ * AUDIT MARK (D1) and is EXPLICIT (D2) — neither of which yields a coordinate for
36
+ * "the state just before this agent started". This hook fires at the exact instant
37
+ * of dispatch and already talks to git, so it records that coordinate: a ref under
38
+ * `refs/orchestrator/` at HEAD's full 40-hex sha, for COMMITTING agents only.
39
+ * It creates no commit, writes no file, and touches no database — a ref is git
40
+ * metadata, so `git status` is unchanged. Rollback is deliberately NOT implemented:
41
+ * it is plain `git reset --hard <ref>`. See lib/dispatch-marker.ts.
42
+ *
32
43
  * Design note (TD-171): the committing-agent set mirrors the `createCheckpoint` tool
33
44
  * holders in `.claude/agents/*.md`. Keep it in sync with that ground truth and with the
34
45
  * orchestrator SKILL §2c.1 Commit-Policy scope.
@@ -42,7 +53,13 @@ import {
42
53
  outputContext,
43
54
  getActiveWorkflow,
44
55
  writeDispatchIntent,
56
+ normalizeAgentName,
45
57
  } from './lib/api-client.js';
58
+ import {
59
+ writeDispatchBoundaryMarker,
60
+ runGitDefault,
61
+ type DispatchMarkerOutcome,
62
+ } from './lib/dispatch-marker.js';
46
63
 
47
64
  const HOOK = 'PRE-AGENT-DISPATCH';
48
65
 
@@ -80,20 +97,6 @@ const PROTECTED_BRANCHES: readonly string[] = ['main', 'master'];
80
97
  // Pure helpers (exported for testing)
81
98
  // ─────────────────────────────────────────────────────────────
82
99
 
83
- /**
84
- * Strip the plugin namespace from a dispatch name.
85
- *
86
- * Under plugin scope the harness namespaces every agent (`orchestrator:implementer`
87
- * — measured in the S398 sandbox, RFC-040 §18.14); in project scope the same agent
88
- * arrives bare (`implementer`). A guard that compares against the bare name only
89
- * answers `false` for the namespaced form — and since hooks fail open, it disables
90
- * itself in silence. Both forms MUST resolve to the same decision.
91
- */
92
- export function normalizeAgentName(subagentType: string): string {
93
- const i = subagentType.lastIndexOf(':');
94
- return i === -1 ? subagentType : subagentType.slice(i + 1);
95
- }
96
-
97
100
  export function isCommittingAgent(subagentType: string): boolean {
98
101
  if (!subagentType) return false;
99
102
  const t = normalizeAgentName(subagentType).toLowerCase();
@@ -176,6 +179,13 @@ export interface HookDeps {
176
179
  getActiveWorkflow?: () => Promise<{ id: string } | null>;
177
180
  /** TD-205: stamps the dispatch-intent marker for workflowId. See getActiveWorkflow doc. */
178
181
  writeDispatchIntent?: (workflowId: string) => boolean;
182
+ /**
183
+ * S445 (RFC-042): records the git BOUNDARY MARKER for a committing agent —
184
+ * a ref at HEAD's full sha, so there is a coordinate to `git reset --hard`
185
+ * back to. Optional for the same reason as the TD-205 deps above: pre-S445
186
+ * test doubles must keep working, and an absent dep simply skips the marker.
187
+ */
188
+ writeDispatchMarker?: (agentName: string, branch: string) => DispatchMarkerOutcome;
179
189
  }
180
190
 
181
191
  function readBranchFromGit(): string {
@@ -197,6 +207,8 @@ const defaultDeps: HookDeps = {
197
207
  readBranch: readBranchFromGit,
198
208
  getActiveWorkflow,
199
209
  writeDispatchIntent,
210
+ writeDispatchMarker: (agentName, branch) =>
211
+ writeDispatchBoundaryMarker(agentName, branch, { runGit: runGitDefault }),
200
212
  };
201
213
 
202
214
  /**
@@ -240,6 +252,57 @@ export async function stampDispatchIntent(
240
252
  }
241
253
  }
242
254
 
255
+ /**
256
+ * S445 (RFC-042): writes the git boundary marker for a COMMITTING agent, and
257
+ * returns an advisory line if it failed (empty array otherwise).
258
+ *
259
+ * ONLY committing agents get a marker. An agent that cannot commit cannot move
260
+ * HEAD, so a marker for it records a boundary nothing can cross — pure noise
261
+ * consuming the retention cap. This condition is the load-bearing one and is
262
+ * covered by an explicit negative control in the test suite.
263
+ *
264
+ * FAIL-OPEN BUT NOT SILENT. A marker failure must never block or delay the
265
+ * dispatch, so everything here is caught. But "fail-open" is not "invisible":
266
+ * the failure mode this feature must avoid is a GREEN VACUUM — the ref silently
267
+ * never being written (e.g. an invalid ref name) while everything looks fine. So
268
+ * a failure is reported on BOTH existing channels: the debug log AND the in-band
269
+ * advisory context the TD-171 branch warning already uses, which is the one a
270
+ * human or orchestrator actually reads.
271
+ */
272
+ export function markDispatchBoundary(
273
+ subagentType: string,
274
+ branch: string,
275
+ deps: Pick<HookDeps, 'writeDispatchMarker' | 'log'>
276
+ ): string[] {
277
+ if (!deps.writeDispatchMarker) return [];
278
+ if (!isCommittingAgent(subagentType)) return [];
279
+
280
+ try {
281
+ const outcome = deps.writeDispatchMarker(subagentType, branch);
282
+
283
+ if (outcome.status === 'written') {
284
+ deps.log(HOOK, `[S445] boundary marker ${outcome.ref} -> ${outcome.sha}`);
285
+ if (outcome.pruned.length > 0) {
286
+ deps.log(HOOK, `[S445] pruned ${outcome.pruned.length} marker(s) over retention cap`);
287
+ }
288
+ return [];
289
+ }
290
+
291
+ deps.log(HOOK, `[S445] boundary marker FAILED: ${outcome.reason}`);
292
+ return [
293
+ `[BOUNDARY-MARKER][S445] Could not record the pre-dispatch git coordinate for ` +
294
+ `'${subagentType}' on '${branch}': ${outcome.reason}. The dispatch proceeds (this never ` +
295
+ `blocks), but there will be NO ref to 'git reset --hard' back to for this agent's work.`,
296
+ ];
297
+ } catch (err) {
298
+ deps.log(HOOK, `[S445] boundary marker threw (fail-open): ${String(err)}`);
299
+ return [
300
+ `[BOUNDARY-MARKER][S445] Boundary-marker step threw for '${subagentType}': ${String(err)}. ` +
301
+ `Dispatch proceeds; no rollback coordinate was recorded.`,
302
+ ];
303
+ }
304
+ }
305
+
243
306
  // ─────────────────────────────────────────────────────────────
244
307
  // Core logic (exported for testing)
245
308
  // ─────────────────────────────────────────────────────────────
@@ -259,6 +322,10 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
259
322
 
260
323
  const warnings = evaluateDispatch({ subagentType, prompt, branch });
261
324
 
325
+ // S445: record the pre-dispatch git coordinate BEFORE the sub-agent spawns.
326
+ // Committing agents only; never blocks; a failure is surfaced, not swallowed.
327
+ warnings.push(...markDispatchBoundary(subagentType, branch, deps));
328
+
262
329
  // TD-205: stamp the dispatch-intent marker regardless of whether a warning
263
330
  // fires — a dispatch is happening either way, and the marker exists purely
264
331
  // to cover the bg-dispatch race for dangling-guard's Stop hook.