@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.
- package/agents/adversarial-reviewer.md +6 -1
- package/agents/api-extractor.md +6 -0
- package/agents/azure-devops-engineer.md +1 -2
- package/agents/business-rule-miner.md +6 -0
- package/agents/cicd-platform-engineer.md +1 -2
- package/agents/ciclo-engineer.md +15 -2
- package/agents/code-archaeologist.md +6 -0
- package/agents/debug-sidecar.md +6 -0
- package/agents/doc-sidecar.md +6 -0
- package/agents/docs-guardian.md +9 -3
- package/agents/frontend-engineer.md +1 -2
- package/agents/{ai-architect.md → harness-architect.md} +26 -5
- package/agents/implementer.md +6 -1
- package/agents/legacy-discoverer.md +6 -0
- package/agents/legacy-synthesizer.md +6 -0
- package/agents/methodology-researcher.md +6 -1
- package/agents/operational-artifact-builder.md +6 -1
- package/agents/orchestrator.md +27 -87
- package/agents/planner.md +6 -1
- package/agents/presentation-engineer.md +5 -4
- package/agents/python-data-engineer.md +0 -1
- package/agents/researcher.md +6 -0
- package/agents/reviewer.md +6 -1
- package/agents/schema-extractor.md +6 -0
- package/agents/software-architect.md +30 -7
- package/agents/specifier.md +6 -1
- package/agents/task-generator.md +6 -1
- package/agents/test-sidecar.md +6 -0
- package/dist/scripts/generate-hooks-matcher.d.ts +106 -0
- package/dist/scripts/generate-hooks-matcher.d.ts.map +1 -0
- package/dist/scripts/generate-hooks-matcher.js +153 -0
- package/dist/scripts/generate-hooks-matcher.js.map +1 -0
- package/dist/scripts/generate-manifest.d.ts +22 -0
- package/dist/scripts/generate-manifest.d.ts.map +1 -1
- package/dist/scripts/generate-manifest.js +67 -2
- package/dist/scripts/generate-manifest.js.map +1 -1
- package/hooks/agent-git-guard.mjs +63 -11
- package/hooks/dangling-guard.mjs +42 -12
- package/hooks/gate-guardian.mjs +38 -8
- package/hooks/hooks.json +3 -9
- package/hooks/lib/api-client.ts +128 -9
- package/hooks/lib/capability-scanner.ts +391 -55
- package/hooks/lib/dispatch-marker.ts +214 -0
- package/hooks/lib/git-checkpoint.ts +34 -37
- package/hooks/lib/observations-helper.ts +14 -1
- package/hooks/mailbox-listener.mjs +69 -17
- package/hooks/playwright-output-guard.mjs +40 -10
- package/hooks/post-implement.mjs +38 -8
- package/hooks/pre-agent-dispatch.mjs +137 -16
- package/hooks/pre-agent-dispatch.ts +82 -15
- package/hooks/session-start.mjs +274 -103
- package/hooks/session-start.ts +250 -69
- package/hooks/subagent-start.mjs +7665 -22
- package/hooks/subagent-start.ts +367 -16
- package/hooks/subagent-stop.mjs +102 -62
- package/hooks/subagent-stop.ts +124 -28
- package/hooks/user-prompt.mjs +43 -13
- package/hooks/verification-before-completion.mjs +63 -11
- package/hooks/workflow-guard.mjs +45 -11
- package/hooks-settings-template.json +1 -7
- package/manifest.json +62 -62
- package/package.json +4 -2
- package/skills/checkpoint-protocol/SKILL.md +19 -4
- package/skills/ciclo-lane-dispatch/SKILL.md +24 -0
- package/skills/fase-1b-projecao/SKILL.md +146 -0
- package/skills/orchestrator/SKILL.md +11 -3
- package/skills/workflow-coordination/SKILL.md +3 -3
- package/hooks/post-compact.mjs +0 -7931
- 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
|
|
9
|
-
import {
|
|
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
|
-
|
|
23
|
+
candidates.push(currentDir);
|
|
19
24
|
}
|
|
20
25
|
const parentDir = dirname(currentDir);
|
|
21
26
|
if (parentDir === currentDir) {
|
|
22
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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).
|
|
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.
|