@orchestrator-claude/definitions 3.37.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} +8 -3
  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 +10 -5
  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
@@ -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, dirname as dirname2 } from "node:path";
7326
- import { fileURLToPath } from "node:url";
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
- return currentDir;
7340
+ candidates.push(currentDir);
7336
7341
  }
7337
7342
  const parentDir = dirname(currentDir);
7338
7343
  if (parentDir === currentDir) {
7339
- return null;
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
- var __filename = fileURLToPath(import.meta.url);
7347
- var __dirname = dirname2(__filename);
7348
- var PROJECT_ROOT = join2(__dirname, "..", "..", "..");
7349
- var STATE_DIR = join2(PROJECT_ROOT, ".orchestrator", ".state");
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 dirname3 } from "node:path";
7603
- import { fileURLToPath as fileURLToPath2 } from "node:url";
7604
- var __filename2 = fileURLToPath2(import.meta.url);
7605
- var __dirname2 = dirname3(__filename2);
7606
- var PROJECT_ROOT2 = join3(__dirname2, "..", "..", "..");
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 null;
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 null;
7661
+ return false;
7632
7662
  }
7633
7663
  try {
7634
- exec("git add -A");
7635
- try {
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 commitHash;
7667
+ return true;
7653
7668
  } catch (err) {
7654
7669
  log("CHECKPOINT", `Error: ${err}`);
7655
- return null;
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
- async function main() {
7712
- const stdin = await readStdin();
7713
- log(HOOK, "SubagentStop triggered");
7714
- const agentType = getField(stdin, "agent_type") || getField(stdin, "tool_input.subagent_type") || "unknown";
7715
- const workflow = await getActiveWorkflow();
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 commitHash = await createCheckpointIfNeeded(workflow.id, workflow.currentPhase);
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}${commitHash ? ` (checkpoint: ${commitHash})` : ""}`,
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
+ };
@@ -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
- async function main(): Promise<void> {
28
- const stdin = await readStdin();
29
- log(HOOK, 'SubagentStop triggered');
52
+ // ─────────────────────────────────────────────────────────────
53
+ // Dependency injection (testability — see subagent-start.ts precedent)
54
+ // ─────────────────────────────────────────────────────────────
30
55
 
31
- const agentType =
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. Git checkpoint if phase changed
82
- const commitHash = await createCheckpointIfNeeded(workflow.id, workflow.currentPhase);
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 Skin, MUST #2). The RFC-034 post-implement signal
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, git checkpoint) and the
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
- // (commitHash is still consumed by the observation payload below.)
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}${commitHash ? ` (checkpoint: ${commitHash})` : ''}`,
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));
@@ -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, dirname as dirname2 } from "node:path";
6
- import { fileURLToPath } from "node:url";
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
- return currentDir;
20
+ candidates.push(currentDir);
16
21
  }
17
22
  const parentDir = dirname(currentDir);
18
23
  if (parentDir === currentDir) {
19
- return null;
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
- var __filename = fileURLToPath(import.meta.url);
27
- var __dirname = dirname2(__filename);
28
- var PROJECT_ROOT = join2(__dirname, "..", "..", "..");
29
- var STATE_DIR = join2(PROJECT_ROOT, ".orchestrator", ".state");
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 dirname3 } from "node:path";
228
- import { fileURLToPath as fileURLToPath2 } from "node:url";
229
- var __filename2 = fileURLToPath2(import.meta.url);
230
- var __dirname2 = dirname3(__filename2);
231
- var PROJECT_ROOT2 = join3(__dirname2, "..", "..", "..");
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`);