@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.
- package/agents/adversarial-reviewer.md +1 -1
- package/agents/reviewer.md +3 -1
- package/hooks/dangling-guard.ts +148 -10
- package/hooks/gate-guardian.ts +177 -40
- package/hooks/lib/api-client.ts +419 -38
- package/hooks/lib/capability-scanner.ts +640 -0
- package/hooks/lib/finops-parser.ts +247 -0
- package/hooks/lib/gate-logic.ts +7 -7
- package/hooks/lib/git-checkpoint.ts +20 -20
- package/hooks/lib/observations-helper.ts +104 -0
- package/hooks/lib/sprint-context.ts +102 -0
- package/hooks/mailbox-listener.ts +139 -25
- package/hooks/post-compact.ts +137 -11
- package/hooks/session-start.ts +416 -47
- package/hooks/sprint-registry.ts +6 -5
- package/hooks/subagent-start.ts +27 -15
- package/hooks/subagent-stop.ts +72 -13
- package/hooks/user-prompt.ts +43 -15
- package/hooks/workflow-guard.ts +96 -37
- package/manifest.json +52 -36
- package/package.json +1 -1
- package/skills/backlog-synthesis/SKILL.md +1 -1
- package/skills/orchestrator/SKILL.md +17 -0
- package/skills/release/SKILL.md +41 -0
- package/skills/workflow-status/SKILL.md +1 -1
- package/hooks/pre-agent-dispatch.ts +0 -57
- package/hooks/verification-before-completion.ts +0 -97
package/hooks/subagent-stop.ts
CHANGED
|
@@ -14,44 +14,103 @@ import {
|
|
|
14
14
|
getField,
|
|
15
15
|
getActiveWorkflow,
|
|
16
16
|
outputContext,
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
26
|
+
const HOOK = 'SUBAGENT-STOP';
|
|
21
27
|
|
|
22
28
|
async function main(): Promise<void> {
|
|
23
29
|
const stdin = await readStdin();
|
|
24
|
-
log(HOOK,
|
|
30
|
+
log(HOOK, 'SubagentStop triggered');
|
|
25
31
|
|
|
26
32
|
const agentType =
|
|
27
|
-
getField(stdin,
|
|
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,
|
|
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-
|
|
40
|
-
|
|
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[] = [
|
|
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(
|
|
94
|
+
parts.push('Proceed to gate check via AskUserQuestion before advancing phase.');
|
|
95
|
+
|
|
96
|
+
outputContext('SubagentStop', parts.join(' '));
|
|
53
97
|
|
|
54
|
-
|
|
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));
|
package/hooks/user-prompt.ts
CHANGED
|
@@ -12,27 +12,28 @@ import {
|
|
|
12
12
|
getNextAction,
|
|
13
13
|
detectWorkflow,
|
|
14
14
|
outputContext,
|
|
15
|
-
} from
|
|
15
|
+
} from './lib/api-client.js';
|
|
16
|
+
import { readSprintContext } from './lib/sprint-context.js';
|
|
16
17
|
|
|
17
|
-
const HOOK =
|
|
18
|
+
const HOOK = 'USER-PROMPT';
|
|
18
19
|
|
|
19
20
|
async function main(): Promise<void> {
|
|
20
|
-
log(HOOK,
|
|
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,
|
|
28
|
+
const prompt = getField(stdin, 'prompt');
|
|
28
29
|
if (!prompt) {
|
|
29
|
-
log(HOOK,
|
|
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,
|
|
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 ===
|
|
44
|
+
if (workflowType === 'interactive_analysis') {
|
|
44
45
|
outputContext(
|
|
45
|
-
|
|
46
|
+
'UserPromptSubmit',
|
|
46
47
|
[
|
|
47
|
-
|
|
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(
|
|
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 ||
|
|
60
|
+
const mode = suggestedMode || 'standard';
|
|
60
61
|
outputContext(
|
|
61
|
-
|
|
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
|
-
|
|
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(
|
|
120
|
+
outputContext('UserPromptSubmit', context);
|
|
93
121
|
}
|
|
94
122
|
|
|
95
123
|
main().catch(() => process.exit(0));
|
package/hooks/workflow-guard.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
30
|
+
} from './lib/api-client.js';
|
|
31
|
+
import { appendObservation } from './lib/observations-helper.js';
|
|
16
32
|
|
|
17
|
-
const HOOK =
|
|
33
|
+
const HOOK = 'WORKFLOW-GUARD';
|
|
18
34
|
|
|
19
35
|
async function main(): Promise<void> {
|
|
20
36
|
const stdin = await readStdin();
|
|
21
|
-
const filePath =
|
|
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 ===
|
|
27
|
-
log(HOOK,
|
|
45
|
+
if (process.env.SKIP_WORKFLOW_GUARD === 'true') {
|
|
46
|
+
log(HOOK, 'ALLOW (SKIP_WORKFLOW_GUARD=true)');
|
|
28
47
|
outputPreToolUse({
|
|
29
|
-
decision:
|
|
30
|
-
context:
|
|
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(
|
|
37
|
-
log(HOOK,
|
|
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,
|
|
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 ||
|
|
71
|
+
log(HOOK, `workflow=${workflow.id} mode=${mode || 'legacy'}`);
|
|
53
72
|
|
|
54
|
-
if (mode ===
|
|
73
|
+
if (mode === 'quick') {
|
|
55
74
|
outputPreToolUse({
|
|
56
|
-
decision:
|
|
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 ===
|
|
81
|
+
if (mode === 'interactive') {
|
|
63
82
|
outputPreToolUse({
|
|
64
|
-
decision:
|
|
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,
|
|
100
|
+
const agentId = getField(stdin, 'agent_id');
|
|
72
101
|
if (agentId) {
|
|
73
|
-
const agentType = getField(stdin,
|
|
74
|
-
log(HOOK, `ALLOW (sub-agent: ${agentType ||
|
|
102
|
+
const agentType = getField(stdin, 'agent_type');
|
|
103
|
+
log(HOOK, `ALLOW (sub-agent: ${agentType || 'unknown'}, workflow: ${workflow.id})`);
|
|
75
104
|
outputPreToolUse({
|
|
76
|
-
decision:
|
|
77
|
-
context: `Active workflow: ${workflow.id}. Sub-agent ${agentType ||
|
|
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
|
-
|
|
84
|
-
|
|
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:
|
|
87
|
-
context:
|
|
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 ||
|
|
152
|
+
log(HOOK, `DENY (main agent direct write, mode=${mode || 'legacy'})`);
|
|
94
153
|
outputPreToolUse({
|
|
95
|
-
decision:
|
|
154
|
+
decision: 'deny',
|
|
96
155
|
reason:
|
|
97
|
-
|
|
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
|
-
|
|
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,
|
|
164
|
+
log(HOOK, 'DENY (no active workflow)');
|
|
106
165
|
outputPreToolUse({
|
|
107
|
-
decision:
|
|
108
|
-
reason:
|
|
166
|
+
decision: 'deny',
|
|
167
|
+
reason: 'Workflow Guard: No active workflow. Direct implementation is not allowed.',
|
|
109
168
|
context: [
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
].join(
|
|
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
|
|