@orchestrator-claude/definitions 3.37.0 → 3.37.2
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 +50 -21
- 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} +8 -3
- 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 +29 -15
- 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 +10 -5
- 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 +462 -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/lib/security-gate-core.ts +271 -0
- 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/security-gate-honest.mjs +302 -0
- package/hooks/security-gate-honest.ts +148 -0
- package/hooks/session-start.mjs +304 -106
- package/hooks/session-start.ts +317 -71
- package/hooks/subagent-start.mjs +7673 -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 +77 -61
- package/package.json +4 -2
- package/skills/checkpoint-protocol/SKILL.md +19 -4
- package/skills/ciclo-lane-dispatch/SKILL.md +117 -8
- package/skills/fase-1b-projecao/SKILL.md +150 -0
- package/skills/lane-1-entrevista/SKILL.md +173 -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
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -7322,34 +7322,64 @@ var require_dist = __commonJS({
|
|
|
7322
7322
|
|
|
7323
7323
|
// hooks/lib/api-client.ts
|
|
7324
7324
|
import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
|
|
7325
|
-
import { join as join2
|
|
7326
|
-
import {
|
|
7325
|
+
import { join as join2 } from "node:path";
|
|
7326
|
+
import { tmpdir } from "node:os";
|
|
7327
|
+
import { createHash } from "node:crypto";
|
|
7327
7328
|
|
|
7328
7329
|
// ../project-root-resolver/dist/index.js
|
|
7329
7330
|
import { existsSync } from "node:fs";
|
|
7330
7331
|
import { dirname, join } from "node:path";
|
|
7332
|
+
function hasProjectRootMarker(candidate, sentinel) {
|
|
7333
|
+
return existsSync(join(candidate, ".git")) || existsSync(join(candidate, "package.json")) || existsSync(join(candidate, sentinel, ".root"));
|
|
7334
|
+
}
|
|
7331
7335
|
function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
|
|
7336
|
+
const candidates = [];
|
|
7332
7337
|
let currentDir = cwd;
|
|
7333
7338
|
for (; ; ) {
|
|
7334
7339
|
if (existsSync(join(currentDir, sentinel))) {
|
|
7335
|
-
|
|
7340
|
+
candidates.push(currentDir);
|
|
7336
7341
|
}
|
|
7337
7342
|
const parentDir = dirname(currentDir);
|
|
7338
7343
|
if (parentDir === currentDir) {
|
|
7339
|
-
|
|
7344
|
+
break;
|
|
7340
7345
|
}
|
|
7341
7346
|
currentDir = parentDir;
|
|
7342
7347
|
}
|
|
7348
|
+
const [innermost] = candidates;
|
|
7349
|
+
if (innermost === void 0) {
|
|
7350
|
+
return null;
|
|
7351
|
+
}
|
|
7352
|
+
for (const candidate of candidates) {
|
|
7353
|
+
if (hasProjectRootMarker(candidate, sentinel)) {
|
|
7354
|
+
return candidate;
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
return innermost;
|
|
7343
7358
|
}
|
|
7344
7359
|
|
|
7345
7360
|
// hooks/lib/api-client.ts
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7361
|
+
function resolveFallbackStateDir(cwd) {
|
|
7362
|
+
const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
7363
|
+
return join2(tmpdir(), "orchestrator-hook-fallback", cwdHash);
|
|
7364
|
+
}
|
|
7365
|
+
var PROJECT_ROOT = (() => {
|
|
7366
|
+
const resolved = resolveHookProjectRoot(process.cwd());
|
|
7367
|
+
if (resolved !== null && hasProjectRootMarker(resolved, ".orchestrator")) {
|
|
7368
|
+
return resolved;
|
|
7369
|
+
}
|
|
7370
|
+
return null;
|
|
7371
|
+
})();
|
|
7372
|
+
var STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
|
|
7373
|
+
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());
|
|
7350
7374
|
var JWT_CACHE = join2(STATE_DIR, "jwt-token");
|
|
7351
7375
|
var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
|
|
7352
7376
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
7377
|
+
if ((STATE_DIR_OVERRIDE === void 0 || STATE_DIR_OVERRIDE === "") && PROJECT_ROOT === null) {
|
|
7378
|
+
log(
|
|
7379
|
+
"API-CLIENT",
|
|
7380
|
+
`PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} \u2014 falling back to isolated state dir ${STATE_DIR} (RF-003)`
|
|
7381
|
+
);
|
|
7382
|
+
}
|
|
7353
7383
|
var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
|
|
7354
7384
|
var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
|
|
7355
7385
|
var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
|
|
@@ -7393,6 +7423,10 @@ async function ensureProjectIdResolved(token) {
|
|
|
7393
7423
|
);
|
|
7394
7424
|
}
|
|
7395
7425
|
}
|
|
7426
|
+
function normalizeAgentName(subagentType) {
|
|
7427
|
+
const i = subagentType.lastIndexOf(":");
|
|
7428
|
+
return i === -1 ? subagentType : subagentType.slice(i + 1);
|
|
7429
|
+
}
|
|
7396
7430
|
function log(prefix, msg) {
|
|
7397
7431
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
7398
7432
|
try {
|
|
@@ -7597,13 +7631,12 @@ function getField(obj, path) {
|
|
|
7597
7631
|
}
|
|
7598
7632
|
|
|
7599
7633
|
// hooks/lib/git-checkpoint.ts
|
|
7600
|
-
import { execSync } from "node:child_process";
|
|
7601
7634
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7602
|
-
import { join as join3, dirname as
|
|
7603
|
-
import { fileURLToPath
|
|
7604
|
-
var
|
|
7605
|
-
var
|
|
7606
|
-
var PROJECT_ROOT2 = join3(
|
|
7635
|
+
import { join as join3, dirname as dirname2 } from "node:path";
|
|
7636
|
+
import { fileURLToPath } from "node:url";
|
|
7637
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
7638
|
+
var __dirname = dirname2(__filename);
|
|
7639
|
+
var PROJECT_ROOT2 = join3(__dirname, "..", "..", "..");
|
|
7607
7640
|
var STATE_DIR2 = join3(PROJECT_ROOT2, ".orchestrator", ".state");
|
|
7608
7641
|
var LAST_PHASE_FILE = join3(STATE_DIR2, "last-checkpointed-phase");
|
|
7609
7642
|
var PHASE_ORDER = ["research", "specify", "plan", "tasks", "implement", "validate"];
|
|
@@ -7612,14 +7645,11 @@ function getCompletedPhase(currentPhase) {
|
|
|
7612
7645
|
const idx = PHASE_ORDER.indexOf(lower);
|
|
7613
7646
|
return idx > 0 ? PHASE_ORDER[idx - 1] ?? null : null;
|
|
7614
7647
|
}
|
|
7615
|
-
function exec(cmd) {
|
|
7616
|
-
return execSync(cmd, { cwd: PROJECT_ROOT2, encoding: "utf-8" }).trim();
|
|
7617
|
-
}
|
|
7618
7648
|
async function createCheckpointIfNeeded(workflowId, currentPhase) {
|
|
7619
7649
|
const completedPhase = getCompletedPhase(currentPhase);
|
|
7620
7650
|
if (!completedPhase) {
|
|
7621
7651
|
log("CHECKPOINT", `No completed phase from: ${currentPhase}`);
|
|
7622
|
-
return
|
|
7652
|
+
return false;
|
|
7623
7653
|
}
|
|
7624
7654
|
let lastPhase = "";
|
|
7625
7655
|
try {
|
|
@@ -7628,31 +7658,16 @@ async function createCheckpointIfNeeded(workflowId, currentPhase) {
|
|
|
7628
7658
|
}
|
|
7629
7659
|
if (lastPhase === completedPhase) {
|
|
7630
7660
|
log("CHECKPOINT", `Already checkpointed: ${completedPhase}`);
|
|
7631
|
-
return
|
|
7661
|
+
return false;
|
|
7632
7662
|
}
|
|
7633
7663
|
try {
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
exec("git diff --cached --quiet");
|
|
7637
|
-
log("CHECKPOINT", `No changes for phase: ${completedPhase}`);
|
|
7638
|
-
writeFileSync2(LAST_PHASE_FILE, completedPhase);
|
|
7639
|
-
return null;
|
|
7640
|
-
} catch {
|
|
7641
|
-
}
|
|
7642
|
-
const commitMsg = `[orchestrator] ${completedPhase}: Auto-checkpoint after ${completedPhase} phase`;
|
|
7643
|
-
exec(`git commit -m "${commitMsg}" --no-verify`);
|
|
7644
|
-
const commitHash = exec("git rev-parse HEAD");
|
|
7645
|
-
log("CHECKPOINT", `Created: phase=${completedPhase} commit=${commitHash}`);
|
|
7646
|
-
await registerCheckpoint(
|
|
7647
|
-
workflowId,
|
|
7648
|
-
`Auto-checkpoint after ${completedPhase} phase`,
|
|
7649
|
-
commitHash
|
|
7650
|
-
);
|
|
7664
|
+
log("CHECKPOINT", `Registering checkpoint for phase: ${completedPhase}`);
|
|
7665
|
+
await registerCheckpoint(workflowId, `Auto-checkpoint after ${completedPhase} phase`, "");
|
|
7651
7666
|
writeFileSync2(LAST_PHASE_FILE, completedPhase);
|
|
7652
|
-
return
|
|
7667
|
+
return true;
|
|
7653
7668
|
} catch (err) {
|
|
7654
7669
|
log("CHECKPOINT", `Error: ${err}`);
|
|
7655
|
-
return
|
|
7670
|
+
return false;
|
|
7656
7671
|
}
|
|
7657
7672
|
}
|
|
7658
7673
|
|
|
@@ -7664,7 +7679,7 @@ var MAX_ENTRIES = 50;
|
|
|
7664
7679
|
var MAX_SUMMARY_LENGTH = 200;
|
|
7665
7680
|
function appendObservation(memoryDir, agent, entry) {
|
|
7666
7681
|
try {
|
|
7667
|
-
const agentDir = join4(memoryDir, agent);
|
|
7682
|
+
const agentDir = join4(memoryDir, normalizeAgentName(agent));
|
|
7668
7683
|
const filePath = join4(agentDir, "observations.yaml");
|
|
7669
7684
|
if (!existsSync2(agentDir)) {
|
|
7670
7685
|
mkdirSync2(agentDir, { recursive: true });
|
|
@@ -7708,68 +7723,93 @@ function appendObservation(memoryDir, agent, entry) {
|
|
|
7708
7723
|
|
|
7709
7724
|
// hooks/subagent-stop.ts
|
|
7710
7725
|
var HOOK = "SUBAGENT-STOP";
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
log
|
|
7714
|
-
|
|
7715
|
-
|
|
7726
|
+
var defaultDeps = {
|
|
7727
|
+
readStdin,
|
|
7728
|
+
log,
|
|
7729
|
+
getActiveWorkflow,
|
|
7730
|
+
readInvocationState,
|
|
7731
|
+
isInvocationStateStale,
|
|
7732
|
+
consumeInvocationState,
|
|
7733
|
+
completeAgentInvocation,
|
|
7734
|
+
createCheckpointIfNeeded,
|
|
7735
|
+
resolveAgentMemoryBaseDir,
|
|
7736
|
+
appendObservation
|
|
7737
|
+
};
|
|
7738
|
+
async function mainWithDeps(deps) {
|
|
7739
|
+
const stdin = await deps.readStdin();
|
|
7740
|
+
deps.log(HOOK, "SubagentStop triggered");
|
|
7741
|
+
const rawAgentType = getField(stdin, "agent_type") || getField(stdin, "tool_input.subagent_type") || "unknown";
|
|
7742
|
+
const agentType = normalizeAgentName(rawAgentType);
|
|
7743
|
+
if (agentType === "unknown") {
|
|
7744
|
+
deps.log(
|
|
7745
|
+
HOOK,
|
|
7746
|
+
`[ERROR] could not resolve agent identity from stdin (checked agent_type, tool_input.subagent_type \u2014 both empty; raw payload=${JSON.stringify(stdin)})`
|
|
7747
|
+
);
|
|
7748
|
+
}
|
|
7749
|
+
const workflow = await deps.getActiveWorkflow();
|
|
7716
7750
|
if (!workflow) {
|
|
7717
|
-
log(HOOK, "No active workflow, skipping");
|
|
7751
|
+
deps.log(HOOK, "No active workflow, skipping");
|
|
7718
7752
|
return;
|
|
7719
7753
|
}
|
|
7720
|
-
log(HOOK, `agent=${agentType} workflow=${workflow.id} phase=${workflow.currentPhase}`);
|
|
7721
|
-
const invocationState = readInvocationState(workflow.id, agentType);
|
|
7754
|
+
deps.log(HOOK, `agent=${agentType} workflow=${workflow.id} phase=${workflow.currentPhase}`);
|
|
7755
|
+
const invocationState = deps.readInvocationState(workflow.id, agentType);
|
|
7722
7756
|
if (invocationState) {
|
|
7723
|
-
log(
|
|
7757
|
+
deps.log(
|
|
7724
7758
|
HOOK,
|
|
7725
7759
|
`COMPLETE safety-net: found state file for agent=${agentType} invocationId=${invocationState.invocationId}`
|
|
7726
7760
|
);
|
|
7727
|
-
if (isInvocationStateStale(invocationState)) {
|
|
7728
|
-
log(
|
|
7761
|
+
if (deps.isInvocationStateStale(invocationState)) {
|
|
7762
|
+
deps.log(
|
|
7729
7763
|
HOOK,
|
|
7730
7764
|
`COMPLETE safety-net: state file is stale (>4h) for invocationId=${invocationState.invocationId} \u2014 deleting without completing`
|
|
7731
7765
|
);
|
|
7732
|
-
consumeInvocationState(workflow.id, agentType);
|
|
7766
|
+
deps.consumeInvocationState(workflow.id, agentType);
|
|
7733
7767
|
} else {
|
|
7734
|
-
const completed = await completeAgentInvocation(
|
|
7768
|
+
const completed = await deps.completeAgentInvocation(
|
|
7735
7769
|
invocationState.invocationId,
|
|
7736
7770
|
"completed",
|
|
7737
7771
|
`Safety-net completion: agent ${agentType} finished phase ${workflow.currentPhase}`
|
|
7738
7772
|
);
|
|
7739
7773
|
if (completed) {
|
|
7740
|
-
consumeInvocationState(workflow.id, agentType);
|
|
7741
|
-
log(
|
|
7774
|
+
deps.consumeInvocationState(workflow.id, agentType);
|
|
7775
|
+
deps.log(
|
|
7742
7776
|
HOOK,
|
|
7743
7777
|
`COMPLETE safety-net: invocationId=${invocationState.invocationId} marked complete, state file deleted`
|
|
7744
7778
|
);
|
|
7745
7779
|
} else {
|
|
7746
|
-
log(
|
|
7780
|
+
deps.log(
|
|
7747
7781
|
HOOK,
|
|
7748
7782
|
`COMPLETE safety-net: POST failed for invocationId=${invocationState.invocationId} (fail-open, continuing)`
|
|
7749
7783
|
);
|
|
7750
7784
|
}
|
|
7751
7785
|
}
|
|
7752
7786
|
} else {
|
|
7753
|
-
log(
|
|
7787
|
+
deps.log(
|
|
7754
7788
|
HOOK,
|
|
7755
7789
|
`COMPLETE: agent=${agentType} \u2014 no state file (agent self-reported via MCP or start hook skipped)`
|
|
7756
7790
|
);
|
|
7757
7791
|
}
|
|
7758
|
-
const
|
|
7792
|
+
const checkpointed = await deps.createCheckpointIfNeeded(workflow.id, workflow.currentPhase);
|
|
7759
7793
|
try {
|
|
7760
7794
|
const sessionId = getField(stdin, "session_id") || getField(stdin, "claude_session_id") || "";
|
|
7761
|
-
const memoryBaseDir = resolveAgentMemoryBaseDir(process.cwd());
|
|
7762
|
-
appendObservation(memoryBaseDir, agentType, {
|
|
7795
|
+
const memoryBaseDir = deps.resolveAgentMemoryBaseDir(process.cwd());
|
|
7796
|
+
deps.appendObservation(memoryBaseDir, agentType, {
|
|
7763
7797
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7764
7798
|
session_id: sessionId,
|
|
7765
7799
|
phase: workflow.currentPhase,
|
|
7766
7800
|
workflow_id: workflow.id,
|
|
7767
|
-
summary: `Agent ${agentType} completed phase ${workflow.currentPhase}${
|
|
7801
|
+
summary: `Agent ${agentType} completed phase ${workflow.currentPhase}${checkpointed ? " (checkpoint registered)" : ""}`,
|
|
7768
7802
|
tags: [`agent:${agentType}`, `phase:${workflow.currentPhase}`]
|
|
7769
7803
|
});
|
|
7770
|
-
log(HOOK, `Observation written for agent=${agentType}`);
|
|
7804
|
+
deps.log(HOOK, `Observation written for agent=${agentType}`);
|
|
7771
7805
|
} catch {
|
|
7772
|
-
log(HOOK, "Failed to write observation (fail-open, continuing)");
|
|
7806
|
+
deps.log(HOOK, "Failed to write observation (fail-open, continuing)");
|
|
7773
7807
|
}
|
|
7774
7808
|
}
|
|
7809
|
+
async function main() {
|
|
7810
|
+
await mainWithDeps(defaultDeps);
|
|
7811
|
+
}
|
|
7775
7812
|
main().catch(() => process.exit(0));
|
|
7813
|
+
export {
|
|
7814
|
+
mainWithDeps
|
|
7815
|
+
};
|
package/hooks/subagent-stop.ts
CHANGED
|
@@ -6,6 +6,29 @@
|
|
|
6
6
|
* 3. post-phase-checkpoint.sh
|
|
7
7
|
*
|
|
8
8
|
* Single handler, single auth, sequential logic.
|
|
9
|
+
*
|
|
10
|
+
* TD-214/TD-239 TASK-006 (RF-008, identity point #4 of the consolidator —
|
|
11
|
+
* see packages/definitions/tests/IdentityNormalizationConsolidator.test.ts):
|
|
12
|
+
* `agentType` is now normalized via `normalizeAgentName` IMMEDIATELY after
|
|
13
|
+
* being read from stdin — mirroring TASK-004's fix to subagent-start.ts.
|
|
14
|
+
*
|
|
15
|
+
* Root cause (measured S435, project-guidelines/sessions/S435.md, "ENTREGA
|
|
16
|
+
* 3"): subagent-start.ts (TASK-004, subagent-start.ts:276) writes the
|
|
17
|
+
* per-key invocation-state file using the NORMALIZED bare agent name
|
|
18
|
+
* (`writeInvocationState(workflowId, normalizeAgentName(...), ...)`). This
|
|
19
|
+
* file, before this task, was read back using the RAW un-normalized
|
|
20
|
+
* `agentType` — under plugin scope (`orchestrator:reviewer`),
|
|
21
|
+
* `buildInvocationStatePath` built a DIFFERENT file path than the one
|
|
22
|
+
* subagent-start.ts wrote to, so `readInvocationState` always missed,
|
|
23
|
+
* silently skipping the ENTIRE safety-net block below (no
|
|
24
|
+
* `completeAgentInvocation`, no `consumeInvocationState`). The mismatch was
|
|
25
|
+
* introduced by TASK-004 itself — before it, both sides extracted identity
|
|
26
|
+
* without normalizing and agreed (`git show 36a9179f~1`).
|
|
27
|
+
*
|
|
28
|
+
* Refactored to the `HookDeps`/`mainWithDeps` dependency-injection pattern
|
|
29
|
+
* (mirrors subagent-start.ts, agent-git-guard.ts) so this behavior — and
|
|
30
|
+
* its regression — is unit-testable; the file had zero test coverage
|
|
31
|
+
* before this task. See `.claude/hooks/__tests__/subagent-stop.test.ts`.
|
|
9
32
|
*/
|
|
10
33
|
|
|
11
34
|
import {
|
|
@@ -18,68 +41,133 @@ import {
|
|
|
18
41
|
completeAgentInvocation,
|
|
19
42
|
isInvocationStateStale,
|
|
20
43
|
resolveAgentMemoryBaseDir,
|
|
44
|
+
normalizeAgentName,
|
|
45
|
+
type InvocationStatePayload,
|
|
21
46
|
} from './lib/api-client.js';
|
|
22
47
|
import { createCheckpointIfNeeded } from './lib/git-checkpoint.js';
|
|
23
|
-
import { appendObservation } from './lib/observations-helper.js';
|
|
48
|
+
import { appendObservation, type ObservationEntry } from './lib/observations-helper.js';
|
|
24
49
|
|
|
25
50
|
const HOOK = 'SUBAGENT-STOP';
|
|
26
51
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
// ─────────────────────────────────────────────────────────────
|
|
53
|
+
// Dependency injection (testability — see subagent-start.ts precedent)
|
|
54
|
+
// ─────────────────────────────────────────────────────────────
|
|
30
55
|
|
|
31
|
-
|
|
56
|
+
export interface HookDeps {
|
|
57
|
+
readStdin: () => Promise<Record<string, unknown>>;
|
|
58
|
+
log: (prefix: string, msg: string) => void;
|
|
59
|
+
getActiveWorkflow: () => Promise<{ id: string; currentPhase: string } | null>;
|
|
60
|
+
readInvocationState: (workflowId: string, agentType: string) => InvocationStatePayload | null;
|
|
61
|
+
isInvocationStateStale: (payload: InvocationStatePayload) => boolean;
|
|
62
|
+
consumeInvocationState: (workflowId: string, agentType: string) => boolean;
|
|
63
|
+
completeAgentInvocation: (
|
|
64
|
+
invocationId: string,
|
|
65
|
+
status: string,
|
|
66
|
+
summary?: string
|
|
67
|
+
) => Promise<boolean>;
|
|
68
|
+
createCheckpointIfNeeded: (workflowId: string, currentPhase: string) => Promise<boolean>;
|
|
69
|
+
resolveAgentMemoryBaseDir: (cwd: string) => string;
|
|
70
|
+
appendObservation: (
|
|
71
|
+
memoryDir: string,
|
|
72
|
+
agent: string,
|
|
73
|
+
entry: Omit<ObservationEntry, 'promoted'>
|
|
74
|
+
) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const defaultDeps: HookDeps = {
|
|
78
|
+
readStdin,
|
|
79
|
+
log,
|
|
80
|
+
getActiveWorkflow,
|
|
81
|
+
readInvocationState,
|
|
82
|
+
isInvocationStateStale,
|
|
83
|
+
consumeInvocationState,
|
|
84
|
+
completeAgentInvocation,
|
|
85
|
+
createCheckpointIfNeeded,
|
|
86
|
+
resolveAgentMemoryBaseDir,
|
|
87
|
+
appendObservation,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────────────────────────────────────────
|
|
91
|
+
// Core logic (exported for testing)
|
|
92
|
+
// ─────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
95
|
+
const stdin = await deps.readStdin();
|
|
96
|
+
deps.log(HOOK, 'SubagentStop triggered');
|
|
97
|
+
|
|
98
|
+
// TD-214/TD-239 TASK-006: normalize IMMEDIATELY after read — see module
|
|
99
|
+
// doc for why (subagent-start.ts writes the state file keyed by the
|
|
100
|
+
// normalized bare name; reading it back with the raw namespaced value
|
|
101
|
+
// silently misses).
|
|
102
|
+
const rawAgentType =
|
|
32
103
|
getField(stdin, 'agent_type') || getField(stdin, 'tool_input.subagent_type') || 'unknown';
|
|
104
|
+
const agentType = normalizeAgentName(rawAgentType);
|
|
105
|
+
|
|
106
|
+
if (agentType === 'unknown') {
|
|
107
|
+
// Elevated from implicit/untagged to explicit [ERROR] (entregável of
|
|
108
|
+
// this task) — this is a signal the upstream payload shape changed, not
|
|
109
|
+
// an everyday case (same rationale as subagent-start.ts's read-failure
|
|
110
|
+
// branch). Does NOT return early: checkpoint and the invocation
|
|
111
|
+
// safety-net below are keyed by workflow.id/currentPhase, not by
|
|
112
|
+
// identity (RNF-001), and MUST keep running even when identity fails to
|
|
113
|
+
// resolve.
|
|
114
|
+
deps.log(
|
|
115
|
+
HOOK,
|
|
116
|
+
`[ERROR] could not resolve agent identity from stdin (checked agent_type, ` +
|
|
117
|
+
`tool_input.subagent_type — both empty; raw payload=${JSON.stringify(stdin)})`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
33
120
|
|
|
34
|
-
const workflow = await getActiveWorkflow();
|
|
121
|
+
const workflow = await deps.getActiveWorkflow();
|
|
35
122
|
if (!workflow) {
|
|
36
|
-
log(HOOK, 'No active workflow, skipping');
|
|
123
|
+
deps.log(HOOK, 'No active workflow, skipping');
|
|
37
124
|
return;
|
|
38
125
|
}
|
|
39
126
|
|
|
40
|
-
log(HOOK, `agent=${agentType} workflow=${workflow.id} phase=${workflow.currentPhase}`);
|
|
127
|
+
deps.log(HOOK, `agent=${agentType} workflow=${workflow.id} phase=${workflow.currentPhase}`);
|
|
41
128
|
|
|
42
129
|
// 1. Invocation tracking safety net — complete via state file if agent did not self-report via MCP
|
|
43
|
-
const invocationState = readInvocationState(workflow.id, agentType);
|
|
130
|
+
const invocationState = deps.readInvocationState(workflow.id, agentType);
|
|
44
131
|
if (invocationState) {
|
|
45
|
-
log(
|
|
132
|
+
deps.log(
|
|
46
133
|
HOOK,
|
|
47
134
|
`COMPLETE safety-net: found state file for agent=${agentType} invocationId=${invocationState.invocationId}`
|
|
48
135
|
);
|
|
49
|
-
if (isInvocationStateStale(invocationState)) {
|
|
50
|
-
log(
|
|
136
|
+
if (deps.isInvocationStateStale(invocationState)) {
|
|
137
|
+
deps.log(
|
|
51
138
|
HOOK,
|
|
52
139
|
`COMPLETE safety-net: state file is stale (>4h) for invocationId=${invocationState.invocationId} — deleting without completing`
|
|
53
140
|
);
|
|
54
|
-
consumeInvocationState(workflow.id, agentType);
|
|
141
|
+
deps.consumeInvocationState(workflow.id, agentType);
|
|
55
142
|
} else {
|
|
56
|
-
const completed = await completeAgentInvocation(
|
|
143
|
+
const completed = await deps.completeAgentInvocation(
|
|
57
144
|
invocationState.invocationId,
|
|
58
145
|
'completed',
|
|
59
146
|
`Safety-net completion: agent ${agentType} finished phase ${workflow.currentPhase}`
|
|
60
147
|
);
|
|
61
148
|
if (completed) {
|
|
62
|
-
consumeInvocationState(workflow.id, agentType);
|
|
63
|
-
log(
|
|
149
|
+
deps.consumeInvocationState(workflow.id, agentType);
|
|
150
|
+
deps.log(
|
|
64
151
|
HOOK,
|
|
65
152
|
`COMPLETE safety-net: invocationId=${invocationState.invocationId} marked complete, state file deleted`
|
|
66
153
|
);
|
|
67
154
|
} else {
|
|
68
|
-
log(
|
|
155
|
+
deps.log(
|
|
69
156
|
HOOK,
|
|
70
157
|
`COMPLETE safety-net: POST failed for invocationId=${invocationState.invocationId} (fail-open, continuing)`
|
|
71
158
|
);
|
|
72
159
|
}
|
|
73
160
|
}
|
|
74
161
|
} else {
|
|
75
|
-
log(
|
|
162
|
+
deps.log(
|
|
76
163
|
HOOK,
|
|
77
164
|
`COMPLETE: agent=${agentType} — no state file (agent self-reported via MCP or start hook skipped)`
|
|
78
165
|
);
|
|
79
166
|
}
|
|
80
167
|
|
|
81
|
-
// 2.
|
|
82
|
-
|
|
168
|
+
// 2. Checkpoint registration if phase changed (TD-257: no longer a git
|
|
169
|
+
// operation — see git-checkpoint.ts module doc for why).
|
|
170
|
+
const checkpointed = await deps.createCheckpointIfNeeded(workflow.id, workflow.currentPhase);
|
|
83
171
|
|
|
84
172
|
// 3. NO context emission on SubagentStop — see below.
|
|
85
173
|
//
|
|
@@ -103,32 +191,40 @@ async function main(): Promise<void> {
|
|
|
103
191
|
// Do NOT reintroduce an additionalContext emission here. Any payload on this event
|
|
104
192
|
// re-prompts the sub-agent — the defect is the emission itself, not its wording.
|
|
105
193
|
// The orchestrator already receives the sub-agent's result directly and owns the gate
|
|
106
|
-
// check unconditionally (orchestrator
|
|
194
|
+
// check unconditionally (orchestrator identity, MUST #2 guarantee + #3 instrument — renumbered S478). The RFC-034 post-implement signal
|
|
107
195
|
// is unaffected: it lives in post-implement.ts on PostToolUse[advancePhase], which fires
|
|
108
196
|
// in the MAIN conversation's context, where additionalContext is the correct mechanism.
|
|
109
197
|
//
|
|
110
|
-
// The side effects above (invocation-completion safety net,
|
|
198
|
+
// The side effects above (invocation-completion safety net, checkpoint registration) and the
|
|
111
199
|
// observation below are unaffected — they are writes, not context injection.
|
|
112
|
-
// (
|
|
200
|
+
// (checkpointed is still consumed by the observation payload below.)
|
|
113
201
|
|
|
114
202
|
// 4. Write Tier B observation (fail-open)
|
|
115
203
|
try {
|
|
116
204
|
const sessionId = getField(stdin, 'session_id') || getField(stdin, 'claude_session_id') || '';
|
|
117
205
|
// RFC-040 Operator Multi-Project — Slice 5 (C5): resolves under the
|
|
118
206
|
// project root walked up from cwd (falls back to cwd when unregistered).
|
|
119
|
-
const memoryBaseDir = resolveAgentMemoryBaseDir(process.cwd());
|
|
120
|
-
appendObservation(memoryBaseDir, agentType, {
|
|
207
|
+
const memoryBaseDir = deps.resolveAgentMemoryBaseDir(process.cwd());
|
|
208
|
+
deps.appendObservation(memoryBaseDir, agentType, {
|
|
121
209
|
timestamp: new Date().toISOString(),
|
|
122
210
|
session_id: sessionId,
|
|
123
211
|
phase: workflow.currentPhase,
|
|
124
212
|
workflow_id: workflow.id,
|
|
125
|
-
summary: `Agent ${agentType} completed phase ${workflow.currentPhase}${
|
|
213
|
+
summary: `Agent ${agentType} completed phase ${workflow.currentPhase}${checkpointed ? ' (checkpoint registered)' : ''}`,
|
|
126
214
|
tags: [`agent:${agentType}`, `phase:${workflow.currentPhase}`],
|
|
127
215
|
});
|
|
128
|
-
log(HOOK, `Observation written for agent=${agentType}`);
|
|
216
|
+
deps.log(HOOK, `Observation written for agent=${agentType}`);
|
|
129
217
|
} catch {
|
|
130
|
-
log(HOOK, 'Failed to write observation (fail-open, continuing)');
|
|
218
|
+
deps.log(HOOK, 'Failed to write observation (fail-open, continuing)');
|
|
131
219
|
}
|
|
132
220
|
}
|
|
133
221
|
|
|
222
|
+
// ─────────────────────────────────────────────────────────────
|
|
223
|
+
// Entry point (production use)
|
|
224
|
+
// ─────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
async function main(): Promise<void> {
|
|
227
|
+
await mainWithDeps(defaultDeps);
|
|
228
|
+
}
|
|
229
|
+
|
|
134
230
|
main().catch(() => process.exit(0));
|
package/hooks/user-prompt.mjs
CHANGED
|
@@ -2,34 +2,64 @@ import { createRequire as __createRequireShim } from 'node:module'; const requir
|
|
|
2
2
|
|
|
3
3
|
// hooks/lib/api-client.ts
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
|
|
5
|
-
import { join as join2
|
|
6
|
-
import {
|
|
5
|
+
import { join as join2 } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
|
|
8
9
|
// ../project-root-resolver/dist/index.js
|
|
9
10
|
import { existsSync } from "node:fs";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
12
|
+
function hasProjectRootMarker(candidate, sentinel) {
|
|
13
|
+
return existsSync(join(candidate, ".git")) || existsSync(join(candidate, "package.json")) || existsSync(join(candidate, sentinel, ".root"));
|
|
14
|
+
}
|
|
11
15
|
function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
|
|
16
|
+
const candidates = [];
|
|
12
17
|
let currentDir = cwd;
|
|
13
18
|
for (; ; ) {
|
|
14
19
|
if (existsSync(join(currentDir, sentinel))) {
|
|
15
|
-
|
|
20
|
+
candidates.push(currentDir);
|
|
16
21
|
}
|
|
17
22
|
const parentDir = dirname(currentDir);
|
|
18
23
|
if (parentDir === currentDir) {
|
|
19
|
-
|
|
24
|
+
break;
|
|
20
25
|
}
|
|
21
26
|
currentDir = parentDir;
|
|
22
27
|
}
|
|
28
|
+
const [innermost] = candidates;
|
|
29
|
+
if (innermost === void 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
if (hasProjectRootMarker(candidate, sentinel)) {
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return innermost;
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
// hooks/lib/api-client.ts
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
function resolveFallbackStateDir(cwd) {
|
|
42
|
+
const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
43
|
+
return join2(tmpdir(), "orchestrator-hook-fallback", cwdHash);
|
|
44
|
+
}
|
|
45
|
+
var PROJECT_ROOT = (() => {
|
|
46
|
+
const resolved = resolveHookProjectRoot(process.cwd());
|
|
47
|
+
if (resolved !== null && hasProjectRootMarker(resolved, ".orchestrator")) {
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
})();
|
|
52
|
+
var STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
|
|
53
|
+
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());
|
|
30
54
|
var JWT_CACHE = join2(STATE_DIR, "jwt-token");
|
|
31
55
|
var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
|
|
32
56
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
57
|
+
if ((STATE_DIR_OVERRIDE === void 0 || STATE_DIR_OVERRIDE === "") && PROJECT_ROOT === null) {
|
|
58
|
+
log(
|
|
59
|
+
"API-CLIENT",
|
|
60
|
+
`PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} \u2014 falling back to isolated state dir ${STATE_DIR} (RF-003)`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
33
63
|
var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
|
|
34
64
|
var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
|
|
35
65
|
var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
|
|
@@ -224,11 +254,11 @@ function outputContext(hookEvent, context) {
|
|
|
224
254
|
|
|
225
255
|
// hooks/lib/sprint-context.ts
|
|
226
256
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "node:fs";
|
|
227
|
-
import { join as join3, dirname as
|
|
228
|
-
import { fileURLToPath
|
|
229
|
-
var
|
|
230
|
-
var
|
|
231
|
-
var PROJECT_ROOT2 = join3(
|
|
257
|
+
import { join as join3, dirname as dirname2 } from "node:path";
|
|
258
|
+
import { fileURLToPath } from "node:url";
|
|
259
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
260
|
+
var __dirname = dirname2(__filename);
|
|
261
|
+
var PROJECT_ROOT2 = join3(__dirname, "..", "..", "..");
|
|
232
262
|
var SPRINT_CONTEXT_DIR = join3(PROJECT_ROOT2, ".claude", "sprint-context");
|
|
233
263
|
function contextPath(workflowId) {
|
|
234
264
|
return join3(SPRINT_CONTEXT_DIR, `${workflowId}.json`);
|