@orchestrator-claude/definitions 3.31.0 → 3.31.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.
@@ -14,44 +14,103 @@ import {
14
14
  getField,
15
15
  getActiveWorkflow,
16
16
  outputContext,
17
- } from "./lib/api-client.js";
18
- import { createCheckpointIfNeeded } from "./lib/git-checkpoint.js";
17
+ readInvocationState,
18
+ consumeInvocationState,
19
+ completeAgentInvocation,
20
+ isInvocationStateStale,
21
+ } from './lib/api-client.js';
22
+ import { join } from 'node:path';
23
+ import { createCheckpointIfNeeded } from './lib/git-checkpoint.js';
24
+ import { appendObservation } from './lib/observations-helper.js';
19
25
 
20
- const HOOK = "SUBAGENT-STOP";
26
+ const HOOK = 'SUBAGENT-STOP';
21
27
 
22
28
  async function main(): Promise<void> {
23
29
  const stdin = await readStdin();
24
- log(HOOK, "SubagentStop triggered");
30
+ log(HOOK, 'SubagentStop triggered');
25
31
 
26
32
  const agentType =
27
- getField(stdin, "agent_type") ||
28
- getField(stdin, "tool_input.subagent_type") ||
29
- "unknown";
33
+ getField(stdin, 'agent_type') || getField(stdin, 'tool_input.subagent_type') || 'unknown';
30
34
 
31
35
  const workflow = await getActiveWorkflow();
32
36
  if (!workflow) {
33
- log(HOOK, "No active workflow, skipping");
37
+ log(HOOK, 'No active workflow, skipping');
34
38
  return;
35
39
  }
36
40
 
37
41
  log(HOOK, `agent=${agentType} workflow=${workflow.id} phase=${workflow.currentPhase}`);
38
42
 
39
- // 1. Invocation tracking — agent self-reports via MCP; this is safety net
40
- log(HOOK, `COMPLETE: agent=${agentType} (safety net — agent self-reports via MCP)`);
43
+ // 1. Invocation tracking safety net — complete via state file if agent did not self-report via MCP
44
+ const invocationState = readInvocationState(workflow.id, agentType);
45
+ if (invocationState) {
46
+ log(
47
+ HOOK,
48
+ `COMPLETE safety-net: found state file for agent=${agentType} invocationId=${invocationState.invocationId}`
49
+ );
50
+ if (isInvocationStateStale(invocationState)) {
51
+ log(
52
+ HOOK,
53
+ `COMPLETE safety-net: state file is stale (>4h) for invocationId=${invocationState.invocationId} — deleting without completing`
54
+ );
55
+ consumeInvocationState(workflow.id, agentType);
56
+ } else {
57
+ const completed = await completeAgentInvocation(
58
+ invocationState.invocationId,
59
+ 'completed',
60
+ `Safety-net completion: agent ${agentType} finished phase ${workflow.currentPhase}`
61
+ );
62
+ if (completed) {
63
+ consumeInvocationState(workflow.id, agentType);
64
+ log(
65
+ HOOK,
66
+ `COMPLETE safety-net: invocationId=${invocationState.invocationId} marked complete, state file deleted`
67
+ );
68
+ } else {
69
+ log(
70
+ HOOK,
71
+ `COMPLETE safety-net: POST failed for invocationId=${invocationState.invocationId} (fail-open, continuing)`
72
+ );
73
+ }
74
+ }
75
+ } else {
76
+ log(
77
+ HOOK,
78
+ `COMPLETE: agent=${agentType} — no state file (agent self-reported via MCP or start hook skipped)`
79
+ );
80
+ }
41
81
 
42
82
  // 2. Git checkpoint if phase changed
43
83
  const commitHash = await createCheckpointIfNeeded(workflow.id, workflow.currentPhase);
44
84
 
45
85
  // 3. Guide main conversation — NO ping-pong, main IS the orchestrator
46
- const parts: string[] = [`[ORCHESTRATOR] Agent '${agentType}' completed for workflow ${workflow.id}.`];
86
+ const parts: string[] = [
87
+ `[ORCHESTRATOR] Agent '${agentType}' completed for workflow ${workflow.id}.`,
88
+ ];
47
89
 
48
90
  if (commitHash) {
49
91
  parts.push(`Checkpoint: ${commitHash}.`);
50
92
  }
51
93
 
52
- parts.push("Proceed to gate check via AskUserQuestion before advancing phase.");
94
+ parts.push('Proceed to gate check via AskUserQuestion before advancing phase.');
95
+
96
+ outputContext('SubagentStop', parts.join(' '));
53
97
 
54
- outputContext("SubagentStop", parts.join(" "));
98
+ // 4. Write Tier B observation (fail-open)
99
+ try {
100
+ const sessionId = getField(stdin, 'session_id') || getField(stdin, 'claude_session_id') || '';
101
+ const memoryBaseDir = join(process.cwd(), '.claude', 'agent-memory');
102
+ appendObservation(memoryBaseDir, agentType, {
103
+ timestamp: new Date().toISOString(),
104
+ session_id: sessionId,
105
+ phase: workflow.currentPhase,
106
+ workflow_id: workflow.id,
107
+ summary: `Agent ${agentType} completed phase ${workflow.currentPhase}${commitHash ? ` (checkpoint: ${commitHash})` : ''}`,
108
+ tags: [`agent:${agentType}`, `phase:${workflow.currentPhase}`],
109
+ });
110
+ log(HOOK, `Observation written for agent=${agentType}`);
111
+ } catch {
112
+ log(HOOK, 'Failed to write observation (fail-open, continuing)');
113
+ }
55
114
  }
56
115
 
57
116
  main().catch(() => process.exit(0));
@@ -12,27 +12,28 @@ import {
12
12
  getNextAction,
13
13
  detectWorkflow,
14
14
  outputContext,
15
- } from "./lib/api-client.js";
15
+ } from './lib/api-client.js';
16
+ import { readSprintContext } from './lib/sprint-context.js';
16
17
 
17
- const HOOK = "USER-PROMPT";
18
+ const HOOK = 'USER-PROMPT';
18
19
 
19
20
  async function main(): Promise<void> {
20
- log(HOOK, "UserPromptSubmit triggered");
21
+ log(HOOK, 'UserPromptSubmit triggered');
21
22
 
22
23
  const stdin = await readStdin();
23
24
  const workflow = await getActiveWorkflow();
24
25
 
25
26
  if (!workflow) {
26
27
  // No active workflow — detect and suggest
27
- const prompt = getField(stdin, "prompt");
28
+ const prompt = getField(stdin, 'prompt');
28
29
  if (!prompt) {
29
- log(HOOK, "empty prompt, silence");
30
+ log(HOOK, 'empty prompt, silence');
30
31
  return;
31
32
  }
32
33
 
33
34
  const detection = await detectWorkflow(prompt);
34
35
  if (!detection) {
35
- log(HOOK, "detect API failed/timeout, silence (fail-open)");
36
+ log(HOOK, 'detect API failed/timeout, silence (fail-open)');
36
37
  return;
37
38
  }
38
39
 
@@ -40,25 +41,25 @@ async function main(): Promise<void> {
40
41
  log(HOOK, `detected type=${workflowType} confidence=${confidence} mode=${suggestedMode}`);
41
42
 
42
43
  // Tier 1: interactive_analysis
43
- if (workflowType === "interactive_analysis") {
44
+ if (workflowType === 'interactive_analysis') {
44
45
  outputContext(
45
- "UserPromptSubmit",
46
+ 'UserPromptSubmit',
46
47
  [
47
- "[ORCHESTRATOR] This looks like an analytical/design request. How would you like to proceed?",
48
+ '[ORCHESTRATOR] This looks like an analytical/design request. How would you like to proceed?',
48
49
  '(a) Continue as a free conversation (no workflow)',
49
50
  '(b) Start an interactive_analysis workflow for structured exploration (cyclic phases: research → discuss → analyze)',
50
- "",
51
+ '',
51
52
  'If you choose (b), I will call: mcp__orchestrator-tools__startWorkflow({ type: "interactive_analysis", prompt: "..." })',
52
- ].join("\n")
53
+ ].join('\n')
53
54
  );
54
55
  return;
55
56
  }
56
57
 
57
58
  // Tier 2: high confidence (>= 0.7)
58
59
  if (confidence >= 0.7) {
59
- const mode = suggestedMode || "standard";
60
+ const mode = suggestedMode || 'standard';
60
61
  outputContext(
61
- "UserPromptSubmit",
62
+ 'UserPromptSubmit',
62
63
  `[ORCHESTRATOR] Detected: ${workflowType} workflow (confidence: ${confidence}, suggested mode: ${mode}).\nTo start: mcp__orchestrator-tools__startWorkflow({ type: "${workflowType}", prompt: "...", mode: "${mode}" })\nOr continue without a workflow if this is exploratory.`
63
64
  );
64
65
  return;
@@ -67,7 +68,7 @@ async function main(): Promise<void> {
67
68
  // Tier 3: medium confidence (0.5-0.69)
68
69
  if (confidence >= 0.5) {
69
70
  outputContext(
70
- "UserPromptSubmit",
71
+ 'UserPromptSubmit',
71
72
  `[ORCHESTRATOR] Hint: this might be a ${workflowType} task. Consider starting a workflow if you plan to write code.`
72
73
  );
73
74
  return;
@@ -78,6 +79,33 @@ async function main(): Promise<void> {
78
79
  return;
79
80
  }
80
81
 
82
+ // Active workflow — check for brainstorm context injection (RFC-027)
83
+ const prompt = getField(stdin, 'prompt');
84
+ if (/brainstorm/i.test(prompt)) {
85
+ try {
86
+ const sprintCtx = readSprintContext(workflow.id);
87
+ if (sprintCtx) {
88
+ const previousSummary =
89
+ sprintCtx.previousSprints.length === 0
90
+ ? 'none'
91
+ : sprintCtx.previousSprints.map((s) => `S${s.index}(${s.status})`).join(', ');
92
+
93
+ // Context format: keep in sync with brainstormContextBranch in user-prompt-brainstorm.test.ts
94
+ const brainstormContext = [
95
+ `[PARENT-DOC-CONTEXT] parentDocument=${sprintCtx.parentDocument} | sprint=${sprintCtx.sprintIndex} | scope=${sprintCtx.sprintScope} | previousSprints=${previousSummary}`,
96
+ `[ORCHESTRATOR] Include parent document context in this brainstorm. Read the RFC for background.`,
97
+ ].join('\n');
98
+
99
+ log(HOOK, `brainstorm detected, injecting sprint context for workflow=${workflow.id}`);
100
+ outputContext('UserPromptSubmit', brainstormContext);
101
+ return;
102
+ }
103
+ } catch {
104
+ // fail-open: if readSprintContext throws unexpectedly, continue to standard injection
105
+ log(HOOK, 'brainstorm context read failed, falling through to standard injection');
106
+ }
107
+ }
108
+
81
109
  // Active workflow — inject state
82
110
  const action = await getNextAction(workflow.id);
83
111
  let context: string;
@@ -89,7 +117,7 @@ async function main(): Promise<void> {
89
117
  }
90
118
 
91
119
  log(HOOK, `workflow=${workflow.id} hasAction=${action?.hasAction}`);
92
- outputContext("UserPromptSubmit", context);
120
+ outputContext('UserPromptSubmit', context);
93
121
  }
94
122
 
95
123
  main().catch(() => process.exit(0));
@@ -1,10 +1,25 @@
1
1
  /**
2
2
  * workflow-guard.ts — PreToolUse[Write|Edit] hook (RFC-022 Phase 2)
3
3
  * Replaces workflow-guard.sh
4
+ *
4
5
  * Blocks writes to src/ and tests/ without active workflow.
5
- * Respects SKIP_WORKFLOW_GUARD, quick/interactive modes, sub-agent writes.
6
+ *
7
+ * Allow paths (in evaluation order):
8
+ * 1. SKIP_WORKFLOW_GUARD=true (explicit bypass, all paths)
9
+ * 2. Non-guarded path (file outside src/ or tests/)
10
+ * 3. Config/doc file extension (.json|.yaml|.yml|.md|.env*)
11
+ * 4. Active workflow + mode='quick' (main agent writes allowed by design)
12
+ * 5. Active workflow + mode='interactive' (writes unexpected, but allowed)
13
+ * 6. Active workflow + mode='direct' (ADR-017 Phase 3 — tracked via startDirectExecution)
14
+ * 7. Active workflow + caller is sub-agent (agent_id present in stdin)
15
+ * 8. Active workflow + SKIP_SUBAGENT_GUARD=true (DEPRECATED — ADR-013 §6.5.3, telemetered to observations.yaml)
16
+ * Deny paths:
17
+ * - Active workflow + standard/full/legacy mode + main agent direct write (no agent_id, no bypass)
18
+ * - No active workflow
6
19
  */
7
20
 
21
+ import { join } from 'node:path';
22
+
8
23
  import {
9
24
  log,
10
25
  readStdin,
@@ -12,35 +27,39 @@ import {
12
27
  getActiveWorkflow,
13
28
  getWorkflowMode,
14
29
  outputPreToolUse,
15
- } from "./lib/api-client.js";
30
+ } from './lib/api-client.js';
31
+ import { appendObservation } from './lib/observations-helper.js';
16
32
 
17
- const HOOK = "WORKFLOW-GUARD";
33
+ const HOOK = 'WORKFLOW-GUARD';
18
34
 
19
35
  async function main(): Promise<void> {
20
36
  const stdin = await readStdin();
21
- const filePath = getField(stdin, "tool_input.file_path") || getField(stdin, "file_path") || getField(stdin, "input.file_path");
37
+ const filePath =
38
+ getField(stdin, 'tool_input.file_path') ||
39
+ getField(stdin, 'file_path') ||
40
+ getField(stdin, 'input.file_path');
22
41
 
23
42
  log(HOOK, `file_path=${filePath}`);
24
43
 
25
44
  // Explicit bypass
26
- if (process.env.SKIP_WORKFLOW_GUARD === "true") {
27
- log(HOOK, "ALLOW (SKIP_WORKFLOW_GUARD=true)");
45
+ if (process.env.SKIP_WORKFLOW_GUARD === 'true') {
46
+ log(HOOK, 'ALLOW (SKIP_WORKFLOW_GUARD=true)');
28
47
  outputPreToolUse({
29
- decision: "allow",
30
- context: "Workflow guard bypassed via SKIP_WORKFLOW_GUARD=true.",
48
+ decision: 'allow',
49
+ context: 'Workflow guard bypassed via SKIP_WORKFLOW_GUARD=true.',
31
50
  });
32
51
  return;
33
52
  }
34
53
 
35
54
  // Only guard src/ and tests/ paths
36
- if (!filePath.includes("/src/") && !filePath.includes("/tests/")) {
37
- log(HOOK, "ALLOW (non-guarded path)");
55
+ if (!filePath.includes('/src/') && !filePath.includes('/tests/')) {
56
+ log(HOOK, 'ALLOW (non-guarded path)');
38
57
  return;
39
58
  }
40
59
 
41
60
  // Allow config/doc files
42
61
  if (/\.(json|ya?ml|md|env(\..*)?)$/.test(filePath)) {
43
- log(HOOK, "ALLOW (config/doc file)");
62
+ log(HOOK, 'ALLOW (config/doc file)');
44
63
  return;
45
64
  }
46
65
 
@@ -49,71 +68,111 @@ async function main(): Promise<void> {
49
68
 
50
69
  if (workflow) {
51
70
  const mode = await getWorkflowMode(workflow.id);
52
- log(HOOK, `workflow=${workflow.id} mode=${mode || "legacy"}`);
71
+ log(HOOK, `workflow=${workflow.id} mode=${mode || 'legacy'}`);
53
72
 
54
- if (mode === "quick") {
73
+ if (mode === 'quick') {
55
74
  outputPreToolUse({
56
- decision: "allow",
75
+ decision: 'allow',
57
76
  context: `Active workflow: ${workflow.id} (mode: quick). Direct writes allowed in quick mode.`,
58
77
  });
59
78
  return;
60
79
  }
61
80
 
62
- if (mode === "interactive") {
81
+ if (mode === 'interactive') {
63
82
  outputPreToolUse({
64
- decision: "allow",
83
+ decision: 'allow',
65
84
  context: `Active workflow: ${workflow.id} (mode: interactive). Note: code writes are unexpected in interactive/analysis workflows.`,
66
85
  });
67
86
  return;
68
87
  }
69
88
 
89
+ // ADR-017 Phase 3: direct execution mode — main agent writes without sub-agent required
90
+ if (mode === 'direct') {
91
+ log(HOOK, `ALLOW (executionMode=direct, workflow: ${workflow.id})`);
92
+ outputPreToolUse({
93
+ decision: 'allow',
94
+ context: `Active workflow: ${workflow.id} (executionMode: direct). Direct writes allowed — record will be tracked via startDirectExecution/completeDirectExecution.`,
95
+ });
96
+ return;
97
+ }
98
+
70
99
  // Check if running inside a sub-agent
71
- const agentId = getField(stdin, "agent_id");
100
+ const agentId = getField(stdin, 'agent_id');
72
101
  if (agentId) {
73
- const agentType = getField(stdin, "agent_type");
74
- log(HOOK, `ALLOW (sub-agent: ${agentType || "unknown"}, workflow: ${workflow.id})`);
102
+ const agentType = getField(stdin, 'agent_type');
103
+ log(HOOK, `ALLOW (sub-agent: ${agentType || 'unknown'}, workflow: ${workflow.id})`);
75
104
  outputPreToolUse({
76
- decision: "allow",
77
- context: `Active workflow: ${workflow.id}. Sub-agent ${agentType || "unknown"} write allowed.`,
105
+ decision: 'allow',
106
+ context: `Active workflow: ${workflow.id}. Sub-agent ${agentType || 'unknown'} write allowed.`,
78
107
  });
79
108
  return;
80
109
  }
81
110
 
82
111
  // Main agent writing directly — check SKIP_SUBAGENT_GUARD
83
- if (process.env.SKIP_SUBAGENT_GUARD === "true") {
84
- log(HOOK, `ALLOW (SKIP_SUBAGENT_GUARD=true, workflow: ${workflow.id})`);
112
+ // DEPRECATED per ADR-013 §6.5.3 — preferred replacements are mode='quick'
113
+ // (single-phase, main-agent writes) or mode='direct' (ADR-017 Phase 3,
114
+ // multi-phase main-agent writes with startDirectExecution audit trail).
115
+ // Each bypass is recorded to .claude/agent-memory/main/observations.yaml
116
+ // (RFC-027 Sprint 3) so usage can be measured before full removal.
117
+ if (process.env.SKIP_SUBAGENT_GUARD === 'true') {
118
+ log(HOOK, `ALLOW (SKIP_SUBAGENT_GUARD=true, workflow: ${workflow.id}) [DEPRECATED]`);
119
+
120
+ // Fail-open observation for cross-session telemetry
121
+ try {
122
+ const sessionId =
123
+ getField(stdin, 'session_id') || getField(stdin, 'claude_session_id') || '';
124
+ const memoryBaseDir = join(process.cwd(), '.claude', 'agent-memory');
125
+ appendObservation(memoryBaseDir, 'main', {
126
+ timestamp: new Date().toISOString(),
127
+ session_id: sessionId,
128
+ phase: workflow.currentPhase || 'unknown',
129
+ workflow_id: workflow.id,
130
+ summary: `SKIP_SUBAGENT_GUARD bypass triggered. file=${filePath} mode=${mode || 'legacy'}. Prefer mode:'quick' or mode:'direct' (ADR-013 §6.5.3).`,
131
+ tags: ['skip_subagent_guard_bypass', 'deprecated_bypass', `mode:${mode || 'legacy'}`],
132
+ });
133
+ } catch {
134
+ // fail-open — never block on telemetry
135
+ }
136
+
85
137
  outputPreToolUse({
86
- decision: "allow",
87
- context: `Active workflow: ${workflow.id}. Direct write allowed via SKIP_SUBAGENT_GUARD.`,
138
+ decision: 'allow',
139
+ context: [
140
+ `Active workflow: ${workflow.id}. Direct write allowed via SKIP_SUBAGENT_GUARD.`,
141
+ '',
142
+ '⚠️ DEPRECATED per ADR-013 §6.5.3. Prefer:',
143
+ " • mode:'quick' (startWorkflow({ mode: 'quick' })) — single-phase, main-agent writes allowed by design",
144
+ " • mode:'direct' (ADR-017 Phase 3) — multi-phase, main-agent writes with startDirectExecution audit trail",
145
+ 'Bypass logged to .claude/agent-memory/main/observations.yaml for usage review (S200+ remediation decision).',
146
+ ].join('\n'),
88
147
  });
89
148
  return;
90
149
  }
91
150
 
92
151
  // Main agent direct write — DENY
93
- log(HOOK, `DENY (main agent direct write, mode=${mode || "legacy"})`);
152
+ log(HOOK, `DENY (main agent direct write, mode=${mode || 'legacy'})`);
94
153
  outputPreToolUse({
95
- decision: "deny",
154
+ decision: 'deny',
96
155
  reason:
97
- "Workflow Guard: Direct code writes are blocked. You must invoke a sub-agent (e.g. implementer) to write code. The sub-agent will have write access.",
156
+ 'Workflow Guard: Direct code writes are blocked. You must invoke a sub-agent (e.g. implementer) to write code. The sub-agent will have write access.',
98
157
  context:
99
- "Use the Agent tool to spawn an implementer sub-agent for code changes. The workflow-guard allows writes only from sub-agents (identified by agent_id in hook input).",
158
+ 'Use the Agent tool to spawn an implementer sub-agent for code changes. The workflow-guard allows writes only from sub-agents (identified by agent_id in hook input).',
100
159
  });
101
160
  return;
102
161
  }
103
162
 
104
163
  // No active workflow — DENY
105
- log(HOOK, "DENY (no active workflow)");
164
+ log(HOOK, 'DENY (no active workflow)');
106
165
  outputPreToolUse({
107
- decision: "deny",
108
- reason: "Workflow Guard: No active workflow. Direct implementation is not allowed.",
166
+ decision: 'deny',
167
+ reason: 'Workflow Guard: No active workflow. Direct implementation is not allowed.',
109
168
  context: [
110
- "You MUST start a workflow before writing code:",
169
+ 'You MUST start a workflow before writing code:',
111
170
  '1. mcp__orchestrator-tools__detectWorkflow({ prompt: "..." })',
112
171
  '2. mcp__orchestrator-tools__startWorkflow({ workflowType: "...", prompt: "..." })',
113
- "3. Follow the nextStep returned by startWorkflow",
114
- "",
115
- "The workflow-guard hook blocks all writes to src/ and tests/ without an active workflow.",
116
- ].join("\n"),
172
+ '3. Follow the nextStep returned by startWorkflow',
173
+ '',
174
+ 'The workflow-guard hook blocks all writes to src/ and tests/ without an active workflow.',
175
+ ].join('\n'),
117
176
  });
118
177
  }
119
178