@orchestrator-claude/definitions 3.32.2 → 3.33.0

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 (87) hide show
  1. package/README.md +113 -0
  2. package/agents/debug-sidecar.md +1 -1
  3. package/agents/doc-sidecar.md +1 -1
  4. package/agents/test-sidecar.md +1 -1
  5. package/dist/cli.js +78 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/DiffCommand.js +1 -1
  8. package/dist/commands/DiffCommand.js.map +1 -1
  9. package/dist/commands/InstallCommand.d.ts +41 -0
  10. package/dist/commands/InstallCommand.d.ts.map +1 -0
  11. package/dist/commands/InstallCommand.js +253 -0
  12. package/dist/commands/InstallCommand.js.map +1 -0
  13. package/dist/http/ApiClient.d.ts +5 -0
  14. package/dist/http/ApiClient.d.ts.map +1 -1
  15. package/dist/http/ApiClient.js.map +1 -1
  16. package/dist/install/DevCheckoutDetector.d.ts +66 -0
  17. package/dist/install/DevCheckoutDetector.d.ts.map +1 -0
  18. package/dist/install/DevCheckoutDetector.js +150 -0
  19. package/dist/install/DevCheckoutDetector.js.map +1 -0
  20. package/dist/install/HooksWirer.d.ts +29 -0
  21. package/dist/install/HooksWirer.d.ts.map +1 -0
  22. package/dist/install/HooksWirer.js +80 -0
  23. package/dist/install/HooksWirer.js.map +1 -0
  24. package/dist/install/LocalDiffReporter.d.ts +25 -0
  25. package/dist/install/LocalDiffReporter.d.ts.map +1 -0
  26. package/dist/install/LocalDiffReporter.js +50 -0
  27. package/dist/install/LocalDiffReporter.js.map +1 -0
  28. package/dist/install/Materializer.d.ts +67 -0
  29. package/dist/install/Materializer.d.ts.map +1 -0
  30. package/dist/install/Materializer.js +196 -0
  31. package/dist/install/Materializer.js.map +1 -0
  32. package/dist/install/McpRegistrar.d.ts +63 -0
  33. package/dist/install/McpRegistrar.d.ts.map +1 -0
  34. package/dist/install/McpRegistrar.js +114 -0
  35. package/dist/install/McpRegistrar.js.map +1 -0
  36. package/dist/install/ProcessRunner.d.ts +25 -0
  37. package/dist/install/ProcessRunner.d.ts.map +1 -0
  38. package/dist/install/ProcessRunner.js +41 -0
  39. package/dist/install/ProcessRunner.js.map +1 -0
  40. package/dist/install/ProjectSettingsNeutralizer.d.ts +39 -0
  41. package/dist/install/ProjectSettingsNeutralizer.d.ts.map +1 -0
  42. package/dist/install/ProjectSettingsNeutralizer.js +100 -0
  43. package/dist/install/ProjectSettingsNeutralizer.js.map +1 -0
  44. package/dist/install/types.d.ts +52 -0
  45. package/dist/install/types.d.ts.map +1 -0
  46. package/dist/install/types.js +8 -0
  47. package/dist/install/types.js.map +1 -0
  48. package/dist/manifest/ManifestLoader.d.ts.map +1 -1
  49. package/dist/manifest/ManifestLoader.js +3 -2
  50. package/dist/manifest/ManifestLoader.js.map +1 -1
  51. package/dist/manifest/types.d.ts +3 -0
  52. package/dist/manifest/types.d.ts.map +1 -1
  53. package/dist/scripts/bundle-hooks.d.ts +39 -0
  54. package/dist/scripts/bundle-hooks.d.ts.map +1 -0
  55. package/dist/scripts/bundle-hooks.js +165 -0
  56. package/dist/scripts/bundle-hooks.js.map +1 -0
  57. package/dist/scripts/generate-manifest.d.ts +22 -3
  58. package/dist/scripts/generate-manifest.d.ts.map +1 -1
  59. package/dist/scripts/generate-manifest.js +78 -35
  60. package/dist/scripts/generate-manifest.js.map +1 -1
  61. package/hooks/dangling-guard.mjs +7426 -0
  62. package/hooks/dangling-guard.ts +223 -6
  63. package/hooks/gate-guardian.mjs +463 -0
  64. package/hooks/lib/additional-directories-sync.ts +295 -0
  65. package/hooks/lib/api-client.ts +388 -7
  66. package/hooks/lib/session-selection.ts +81 -0
  67. package/hooks/mailbox-listener.mjs +259 -0
  68. package/hooks/post-compact.mjs +7931 -0
  69. package/hooks/post-implement.mjs +238 -0
  70. package/hooks/session-start.mjs +8234 -0
  71. package/hooks/session-start.ts +264 -23
  72. package/hooks/sprint-registry.mjs +59 -0
  73. package/hooks/subagent-start.mjs +289 -0
  74. package/hooks/subagent-stop.mjs +7793 -0
  75. package/hooks/subagent-stop.ts +4 -2
  76. package/hooks/user-prompt.mjs +329 -0
  77. package/hooks/workflow-guard.mjs +7805 -0
  78. package/hooks/workflow-guard.ts +360 -119
  79. package/hooks-settings-template.json +63 -0
  80. package/manifest.json +54 -68
  81. package/mcp-servers.json +48 -0
  82. package/package.json +10 -4
  83. package/personas/auditor-compliance.md +104 -0
  84. package/personas/cfo-conservador.md +88 -0
  85. package/personas/designer-perfeccionista.md +104 -0
  86. package/personas/diretor-cetico-fmcg.md +80 -0
  87. package/personas/engenheiro-cetico-construcao.md +107 -0
@@ -4,22 +4,39 @@
4
4
  *
5
5
  * Blocks writes to src/ and tests/ without active workflow.
6
6
  *
7
- * Allow paths (in evaluation order):
7
+ * RFC-040 TD-204 §⑤-A (S366): Classe-A asset-safety gate, made
8
+ * "cwd-scoped-safe" — resolves the OWNING project of the write TARGET
9
+ * (file_path), not the session's cwd (per TD-204 §"cwd não some, rebaixa":
10
+ * "o guard Classe-A ainda resolve file_path -> projeto pra autorizar cada
11
+ * Write contra o projeto selecionado"). Today, in this repo (a registered
12
+ * project with an `.orchestrator` sentinel), targetProjectRoot always
13
+ * resolves non-null, so every write still falls through to the pre-existing
14
+ * mode/sub-agent logic (rules 6/7 below) — byte-equivalent to pre-⑤-A
15
+ * behavior. The two NEW branches (4, 5) are dormant-but-tested until this
16
+ * hook is installed globally (future installer) against an unregistered or
17
+ * foreign (trc/red-bull) cwd, where they become load-bearing: a Classe-A
18
+ * gate NEVER no-ops silently on an unrecognized project path.
19
+ *
20
+ * Allow paths (in evaluation order — see decideWorkflowGuardAction):
8
21
  * 1. SKIP_WORKFLOW_GUARD=true (explicit bypass, all paths)
9
22
  * 2. Non-guarded path (file outside src/ or tests/)
10
23
  * 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)
24
+ * 4. [TD-204 ⑤-A] `.orchestrator/.unmanaged` sentinel present at the
25
+ * target's resolved project root — explicit, auditable opt-out.
26
+ * 5. [TD-204 ⑤-A, deny] target path resolves to NO registered project
27
+ * (targetProjectRoot === null) — instructs registration or the
28
+ * `.unmanaged` opt-out. NEVER a silent no-op.
29
+ * 6. Active workflow + mode='quick'|'interactive'|'direct', or caller is
30
+ * a sub-agent (agent_id present), or SKIP_SUBAGENT_GUARD=true
31
+ * (DEPRECATED — ADR-013 §6.5.3, telemetered to observations.yaml)
16
32
  * Deny paths:
17
- * - Active workflow + standard/full/legacy mode + main agent direct write (no agent_id, no bypass)
18
- * - No active workflow
33
+ * - Active workflow + standard/full/legacy mode + main agent direct write
34
+ * (no agent_id, no bypass)
35
+ * - No active workflow (target project IS registered)
19
36
  */
20
37
 
21
- import { join } from 'node:path';
22
-
38
+ import { dirname, join } from 'node:path';
39
+ import { existsSync } from 'node:fs';
23
40
  import {
24
41
  log,
25
42
  readStdin,
@@ -27,153 +44,377 @@ import {
27
44
  getActiveWorkflow,
28
45
  getWorkflowMode,
29
46
  outputPreToolUse,
47
+ resolveAgentMemoryBaseDir,
48
+ resolveHookProjectRoot,
30
49
  } from './lib/api-client.js';
31
50
  import { appendObservation } from './lib/observations-helper.js';
32
51
 
33
52
  const HOOK = 'WORKFLOW-GUARD';
34
53
 
35
- async function main(): Promise<void> {
36
- const stdin = await readStdin();
37
- const filePath =
38
- getField(stdin, 'tool_input.file_path') ||
39
- getField(stdin, 'file_path') ||
40
- getField(stdin, 'input.file_path');
54
+ // ---------------------------------------------------------------------------
55
+ // TD-204 ⑤-A: cwd-scoped-safe block-decision (pure, unit-testable)
56
+ // Mirrors the TD-203 dangling-guard.ts pattern: decideWorkflowGuardAction()
57
+ // takes ONLY injected inputs (zero fs/API), main() gathers signals and
58
+ // delegates.
59
+ // ---------------------------------------------------------------------------
41
60
 
42
- log(HOOK, `file_path=${filePath}`);
61
+ export type WorkflowGuardAction = 'allow' | 'deny';
62
+
63
+ export interface WorkflowGuardOutput {
64
+ decision: WorkflowGuardAction;
65
+ reason?: string;
66
+ context?: string;
67
+ }
68
+
69
+ /** Mirrors ObservationEntry minus timestamp/session_id (added by main()). */
70
+ export interface WorkflowGuardObservation {
71
+ phase: string;
72
+ workflow_id: string;
73
+ summary: string;
74
+ tags: string[];
75
+ }
76
+
77
+ export interface WorkflowGuardDecision {
78
+ /**
79
+ * 1-7, matches the numbered precedence in TD-204 ⑤-A. Rule 6 covers every
80
+ * "active workflow" sub-branch (quick/interactive/direct/sub-agent/
81
+ * SKIP_SUBAGENT_GUARD/main-agent-deny) — they all require a REAL fetched
82
+ * workflow to distinguish, unlike rule 7 (no active workflow) which a
83
+ * `workflow: null` trial call can already prove correctly. main() uses
84
+ * `matchedRule === 7` as the sole signal for whether the (network) active
85
+ * workflow must actually be fetched to confirm the decision — see main().
86
+ */
87
+ matchedRule: 1 | 2 | 3 | 4 | 5 | 6 | 7;
88
+ /** Whether outputPreToolUse must be called. Rules 2/3 are silent pass-throughs (pre-existing behavior — no hookSpecificOutput emitted). */
89
+ emitOutput: boolean;
90
+ output?: WorkflowGuardOutput;
91
+ logMessages: string[];
92
+ observation?: WorkflowGuardObservation;
93
+ }
94
+
95
+ export interface WorkflowGuardWorkflowInput {
96
+ id: string;
97
+ /** getWorkflowMode() result: executionMode ?? mode ?? null. */
98
+ mode: string | null;
99
+ /** WorkflowStatus.currentPhase — required upstream, defensively `|| 'unknown'` at use-site (mirrors pre-⑤-A code). */
100
+ currentPhase: string;
101
+ }
102
+
103
+ export interface WorkflowGuardDecisionInput {
104
+ filePath: string;
105
+ skipWorkflowGuard: boolean;
106
+ skipSubagentGuard: boolean;
107
+ /**
108
+ * TD-204 ⑤-A: resolved via resolveHookProjectRoot(dirname(filePath)) — the
109
+ * file_path's OWNING project root, NOT the session's cwd. `null` means the
110
+ * write target does not belong to any project with an `.orchestrator`
111
+ * sentinel in its ancestry.
112
+ */
113
+ targetProjectRoot: string | null;
114
+ /**
115
+ * True iff `<targetProjectRoot>/.orchestrator/.unmanaged` exists (explicit,
116
+ * auditable opt-out sentinel, TD-204 ⑤-A). Always false when
117
+ * targetProjectRoot is null (the marker itself lives inside `.orchestrator`,
118
+ * so a null root implies no `.orchestrator` dir was found at all).
119
+ */
120
+ isUnmanaged: boolean;
121
+ /**
122
+ * `null` covers BOTH "genuinely no active workflow" and "not yet fetched"
123
+ * (see WorkflowGuardDecision.matchedRule doc — main() distinguishes the two
124
+ * via a cheap trial call before paying for the network fetch).
125
+ */
126
+ workflow: WorkflowGuardWorkflowInput | null;
127
+ /** getField(stdin, 'agent_id') — empty string when absent (never fetched over network). */
128
+ agentId: string;
129
+ /** getField(stdin, 'agent_type') — empty string when absent. */
130
+ agentType: string;
131
+ }
132
+
133
+ const CONFIG_DOC_RE = /\.(json|ya?ml|md|env(\..*)?)$/;
134
+
135
+ /**
136
+ * Pure decision function for the workflow-guard PreToolUse[Write|Edit] hook
137
+ * (TD-204 ⑤-A). Evaluates precedence rules 1-7 top-down, first match wins.
138
+ * Takes only injected inputs — no fs/API calls — so every branch is
139
+ * independently testable and deterministic.
140
+ */
141
+ export function decideWorkflowGuardAction(
142
+ input: WorkflowGuardDecisionInput
143
+ ): WorkflowGuardDecision {
144
+ const {
145
+ filePath,
146
+ skipWorkflowGuard,
147
+ skipSubagentGuard,
148
+ targetProjectRoot,
149
+ isUnmanaged,
150
+ workflow,
151
+ agentId,
152
+ agentType,
153
+ } = input;
43
154
 
44
- // Explicit bypass
45
- if (process.env.SKIP_WORKFLOW_GUARD === 'true') {
46
- log(HOOK, 'ALLOW (SKIP_WORKFLOW_GUARD=true)');
47
- outputPreToolUse({
48
- decision: 'allow',
49
- context: 'Workflow guard bypassed via SKIP_WORKFLOW_GUARD=true.',
50
- });
51
- return;
155
+ // Rule 1: explicit global bypass
156
+ if (skipWorkflowGuard) {
157
+ return {
158
+ matchedRule: 1,
159
+ emitOutput: true,
160
+ output: {
161
+ decision: 'allow',
162
+ context: 'Workflow guard bypassed via SKIP_WORKFLOW_GUARD=true.',
163
+ },
164
+ logMessages: ['ALLOW (SKIP_WORKFLOW_GUARD=true)'],
165
+ };
52
166
  }
53
167
 
54
- // Only guard src/ and tests/ paths
168
+ // Rule 2: non-guarded path (outside src/ or tests/) — silent pass-through
55
169
  if (!filePath.includes('/src/') && !filePath.includes('/tests/')) {
56
- log(HOOK, 'ALLOW (non-guarded path)');
57
- return;
170
+ return { matchedRule: 2, emitOutput: false, logMessages: ['ALLOW (non-guarded path)'] };
171
+ }
172
+
173
+ // Rule 3: config/doc file extension — silent pass-through
174
+ if (CONFIG_DOC_RE.test(filePath)) {
175
+ return { matchedRule: 3, emitOutput: false, logMessages: ['ALLOW (config/doc file)'] };
58
176
  }
59
177
 
60
- // Allow config/doc files
61
- if (/\.(json|ya?ml|md|env(\..*)?)$/.test(filePath)) {
62
- log(HOOK, 'ALLOW (config/doc file)');
63
- return;
178
+ // Rule 4 [TD-204 ⑤-A]: explicit, auditable unmanaged opt-out sentinel
179
+ if (isUnmanaged) {
180
+ return {
181
+ matchedRule: 4,
182
+ emitOutput: true,
183
+ output: {
184
+ decision: 'allow',
185
+ context: `Workflow guard bypassed via .orchestrator/.unmanaged sentinel at ${String(targetProjectRoot)}. Explicit, auditable opt-out (RFC-040 TD-204 Classe-A asset-safety gate).`,
186
+ },
187
+ logMessages: [`ALLOW (.orchestrator/.unmanaged sentinel at ${String(targetProjectRoot)})`],
188
+ };
64
189
  }
65
190
 
66
- // Check for active workflow
67
- const workflow = await getActiveWorkflow();
191
+ // Rule 5 [TD-204 ⑤-A, deny]: write target does not belong to ANY registered
192
+ // project. Branches the block message ("não-registrado") — NEVER a silent
193
+ // no-op, per TD-204's Classe-A contract.
194
+ if (targetProjectRoot === null) {
195
+ return {
196
+ matchedRule: 5,
197
+ emitOutput: true,
198
+ output: {
199
+ decision: 'deny',
200
+ reason:
201
+ 'Workflow Guard: target path does not belong to any registered project (no .orchestrator sentinel found).',
202
+ context: [
203
+ `The file ${filePath} is outside any project registered with the Orchestrator — no .orchestrator sentinel was found walking up from its directory.`,
204
+ '',
205
+ 'You MUST either:',
206
+ " 1. Register this project: mcp__orchestrator-tools__registerProject({ rootPath: '<project-root>', ... }) — see the registration runbook, or",
207
+ " 2. Mark it explicitly unmanaged (opt out of this guard): create an empty file at '<project-root>/.orchestrator/.unmanaged'.",
208
+ '',
209
+ 'The workflow-guard hook NEVER no-ops silently on an unrecognized project path (RFC-040 TD-204 Classe-A asset-safety gate).',
210
+ ].join('\n'),
211
+ },
212
+ logMessages: [`DENY (unregistered target path, file=${filePath})`],
213
+ };
214
+ }
68
215
 
216
+ // Rules 6/7: from here on, targetProjectRoot is a REGISTERED project —
217
+ // behavior is byte-equivalent to pre-⑤-A workflow-guard.ts.
69
218
  if (workflow) {
70
- const mode = await getWorkflowMode(workflow.id);
71
- log(HOOK, `workflow=${workflow.id} mode=${mode || 'legacy'}`);
219
+ const mode = workflow.mode;
220
+ const modeLogLine = `workflow=${workflow.id} mode=${mode || 'legacy'}`;
72
221
 
222
+ // Rule 6a: mode='quick' — main agent writes allowed by design
73
223
  if (mode === 'quick') {
74
- outputPreToolUse({
75
- decision: 'allow',
76
- context: `Active workflow: ${workflow.id} (mode: quick). Direct writes allowed in quick mode.`,
77
- });
78
- return;
224
+ return {
225
+ matchedRule: 6,
226
+ emitOutput: true,
227
+ output: {
228
+ decision: 'allow',
229
+ context: `Active workflow: ${workflow.id} (mode: quick). Direct writes allowed in quick mode.`,
230
+ },
231
+ logMessages: [modeLogLine],
232
+ };
79
233
  }
80
234
 
235
+ // Rule 6b: mode='interactive' — writes unexpected, but allowed
81
236
  if (mode === 'interactive') {
82
- outputPreToolUse({
83
- decision: 'allow',
84
- context: `Active workflow: ${workflow.id} (mode: interactive). Note: code writes are unexpected in interactive/analysis workflows.`,
85
- });
86
- return;
237
+ return {
238
+ matchedRule: 6,
239
+ emitOutput: true,
240
+ output: {
241
+ decision: 'allow',
242
+ context: `Active workflow: ${workflow.id} (mode: interactive). Note: code writes are unexpected in interactive/analysis workflows.`,
243
+ },
244
+ logMessages: [modeLogLine],
245
+ };
87
246
  }
88
247
 
89
- // ADR-017 Phase 3: direct execution mode — main agent writes without sub-agent required
248
+ // Rule 6c: mode='direct' — ADR-017 Phase 3
90
249
  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;
250
+ return {
251
+ matchedRule: 6,
252
+ emitOutput: true,
253
+ output: {
254
+ decision: 'allow',
255
+ context: `Active workflow: ${workflow.id} (executionMode: direct). Direct writes allowed — record will be tracked via startDirectExecution/completeDirectExecution.`,
256
+ },
257
+ logMessages: [modeLogLine, `ALLOW (executionMode=direct, workflow: ${workflow.id})`],
258
+ };
97
259
  }
98
260
 
99
- // Check if running inside a sub-agent
100
- const agentId = getField(stdin, 'agent_id');
261
+ // Rule 6d: caller is a sub-agent (agent_id present in stdin)
101
262
  if (agentId) {
102
- const agentType = getField(stdin, 'agent_type');
103
- log(HOOK, `ALLOW (sub-agent: ${agentType || 'unknown'}, workflow: ${workflow.id})`);
104
- outputPreToolUse({
105
- decision: 'allow',
106
- context: `Active workflow: ${workflow.id}. Sub-agent ${agentType || 'unknown'} write allowed.`,
107
- });
108
- return;
263
+ return {
264
+ matchedRule: 6,
265
+ emitOutput: true,
266
+ output: {
267
+ decision: 'allow',
268
+ context: `Active workflow: ${workflow.id}. Sub-agent ${agentType || 'unknown'} write allowed.`,
269
+ },
270
+ logMessages: [
271
+ modeLogLine,
272
+ `ALLOW (sub-agent: ${agentType || 'unknown'}, workflow: ${workflow.id})`,
273
+ ],
274
+ };
109
275
  }
110
276
 
111
- // Main agent writing directly — check SKIP_SUBAGENT_GUARD
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,
277
+ // Rule 6e: main agent direct write, SKIP_SUBAGENT_GUARD=true (DEPRECATED,
278
+ // ADR-013 §6.5.3). Telemetered via an observation for cross-session usage
279
+ // review.
280
+ if (skipSubagentGuard) {
281
+ return {
282
+ matchedRule: 6,
283
+ emitOutput: true,
284
+ output: {
285
+ decision: 'allow',
286
+ context: [
287
+ `Active workflow: ${workflow.id}. Direct write allowed via SKIP_SUBAGENT_GUARD.`,
288
+ '',
289
+ '⚠️ DEPRECATED per ADR-013 §6.5.3. Prefer:',
290
+ " • mode:'quick' (startWorkflow({ mode: 'quick' })) — single-phase, main-agent writes allowed by design",
291
+ " • mode:'direct' (ADR-017 Phase 3) — multi-phase, main-agent writes with startDirectExecution audit trail",
292
+ 'Bypass logged to .claude/agent-memory/main/observations.yaml for usage review (S200+ remediation decision).',
293
+ ].join('\n'),
294
+ },
295
+ logMessages: [
296
+ modeLogLine,
297
+ `ALLOW (SKIP_SUBAGENT_GUARD=true, workflow: ${workflow.id}) [DEPRECATED]`,
298
+ ],
299
+ observation: {
128
300
  phase: workflow.currentPhase || 'unknown',
129
301
  workflow_id: workflow.id,
130
302
  summary: `SKIP_SUBAGENT_GUARD bypass triggered. file=${filePath} mode=${mode || 'legacy'}. Prefer mode:'quick' or mode:'direct' (ADR-013 §6.5.3).`,
131
303
  tags: ['skip_subagent_guard_bypass', 'deprecated_bypass', `mode:${mode || 'legacy'}`],
132
- });
133
- } catch {
134
- // fail-open — never block on telemetry
135
- }
136
-
137
- outputPreToolUse({
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'),
147
- });
148
- return;
304
+ },
305
+ };
149
306
  }
150
307
 
151
- // Main agent direct write — DENY
152
- log(HOOK, `DENY (main agent direct write, mode=${mode || 'legacy'})`);
153
- outputPreToolUse({
308
+ // Rule 6f: main agent direct write — DENY
309
+ return {
310
+ matchedRule: 6,
311
+ emitOutput: true,
312
+ output: {
313
+ decision: 'deny',
314
+ reason:
315
+ '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.',
316
+ context:
317
+ '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).',
318
+ },
319
+ logMessages: [modeLogLine, `DENY (main agent direct write, mode=${mode || 'legacy'})`],
320
+ };
321
+ }
322
+
323
+ // Rule 7: no active workflow (target project IS registered) — DENY
324
+ return {
325
+ matchedRule: 7,
326
+ emitOutput: true,
327
+ output: {
154
328
  decision: 'deny',
155
- reason:
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.',
157
- context:
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).',
159
- });
160
- return;
329
+ reason: 'Workflow Guard: No active workflow. Direct implementation is not allowed.',
330
+ context: [
331
+ 'You MUST start a workflow before writing code:',
332
+ '1. mcp__orchestrator-tools__detectWorkflow({ prompt: "..." })',
333
+ '2. mcp__orchestrator-tools__startWorkflow({ workflowType: "...", prompt: "..." })',
334
+ '3. Follow the nextStep returned by startWorkflow',
335
+ '',
336
+ 'The workflow-guard hook blocks all writes to src/ and tests/ without an active workflow.',
337
+ ].join('\n'),
338
+ },
339
+ logMessages: ['DENY (no active workflow)'],
340
+ };
341
+ }
342
+
343
+ async function main(): Promise<void> {
344
+ const stdin = await readStdin();
345
+ const filePath =
346
+ getField(stdin, 'tool_input.file_path') ||
347
+ getField(stdin, 'file_path') ||
348
+ getField(stdin, 'input.file_path');
349
+
350
+ log(HOOK, `file_path=${filePath}`);
351
+
352
+ const skipWorkflowGuard = process.env.SKIP_WORKFLOW_GUARD === 'true';
353
+ const skipSubagentGuard = process.env.SKIP_SUBAGENT_GUARD === 'true';
354
+ const agentId = getField(stdin, 'agent_id');
355
+ const agentType = getField(stdin, 'agent_type');
356
+
357
+ // RFC-040 TD-204 ⑤-A: resolve the OWNING project of the write TARGET
358
+ // (file_path), not the session's cwd. Cheap fs walk-up (no network) — safe
359
+ // to compute unconditionally for every Write/Edit.
360
+ const targetDir = filePath.length > 0 ? dirname(filePath) : process.cwd();
361
+ const targetProjectRoot = resolveHookProjectRoot(targetDir);
362
+ const isUnmanaged =
363
+ targetProjectRoot !== null &&
364
+ existsSync(join(targetProjectRoot, '.orchestrator', '.unmanaged'));
365
+
366
+ const baseInput: Omit<WorkflowGuardDecisionInput, 'workflow'> = {
367
+ filePath,
368
+ skipWorkflowGuard,
369
+ skipSubagentGuard,
370
+ targetProjectRoot,
371
+ isUnmanaged,
372
+ agentId,
373
+ agentType,
374
+ };
375
+
376
+ // Two-pass evaluation: try with workflow=null first. Rules 1-5 never
377
+ // inspect `workflow`, so a `null` trial resolves them correctly WITHOUT
378
+ // any network I/O — preserving the pre-⑤-A short-circuit that skips
379
+ // getActiveWorkflow()/getWorkflowMode() for non-guarded, config-doc,
380
+ // unmanaged, and unregistered paths. Only when the trial itself lands on
381
+ // rule 7 ("no active workflow") do we actually need the network fetch to
382
+ // confirm — at most one real fetch, matching original behavior exactly.
383
+ let decision = decideWorkflowGuardAction({ ...baseInput, workflow: null });
384
+
385
+ if (decision.matchedRule === 7) {
386
+ const activeWorkflow = await getActiveWorkflow();
387
+ let workflow: WorkflowGuardWorkflowInput | null = null;
388
+ if (activeWorkflow) {
389
+ const mode = await getWorkflowMode(activeWorkflow.id);
390
+ workflow = { id: activeWorkflow.id, mode, currentPhase: activeWorkflow.currentPhase };
391
+ }
392
+ decision = decideWorkflowGuardAction({ ...baseInput, workflow });
393
+ }
394
+
395
+ for (const line of decision.logMessages) {
396
+ log(HOOK, line);
397
+ }
398
+
399
+ if (decision.observation) {
400
+ try {
401
+ const sessionId = getField(stdin, 'session_id') || getField(stdin, 'claude_session_id') || '';
402
+ // RFC-040 Operator Multi-Project — Slice 5 (C5): resolves under the
403
+ // project root walked up from cwd (falls back to cwd when unregistered).
404
+ const memoryBaseDir = resolveAgentMemoryBaseDir(process.cwd());
405
+ appendObservation(memoryBaseDir, 'main', {
406
+ timestamp: new Date().toISOString(),
407
+ session_id: sessionId,
408
+ ...decision.observation,
409
+ });
410
+ } catch {
411
+ // fail-open — never block on telemetry
412
+ }
161
413
  }
162
414
 
163
- // No active workflow — DENY
164
- log(HOOK, 'DENY (no active workflow)');
165
- outputPreToolUse({
166
- decision: 'deny',
167
- reason: 'Workflow Guard: No active workflow. Direct implementation is not allowed.',
168
- context: [
169
- 'You MUST start a workflow before writing code:',
170
- '1. mcp__orchestrator-tools__detectWorkflow({ prompt: "..." })',
171
- '2. mcp__orchestrator-tools__startWorkflow({ workflowType: "...", prompt: "..." })',
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'),
176
- });
415
+ if (decision.emitOutput && decision.output) {
416
+ outputPreToolUse(decision.output);
417
+ }
177
418
  }
178
419
 
179
420
  main().catch(() => process.exit(0));
@@ -0,0 +1,63 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup|resume",
6
+ "hooks": [
7
+ { "type": "command", "command": "hooks/session-start.mjs", "timeout": 10000 },
8
+ { "type": "command", "command": "hooks/sprint-registry.mjs", "timeout": 5000 }
9
+ ]
10
+ }
11
+ ],
12
+ "UserPromptSubmit": [
13
+ {
14
+ "matcher": "",
15
+ "hooks": [{ "type": "command", "command": "hooks/user-prompt.mjs", "timeout": 10000 }]
16
+ }
17
+ ],
18
+ "PreToolUse": [
19
+ {
20
+ "matcher": "mcp__orchestrator-tools__advancePhase",
21
+ "hooks": [{ "type": "command", "command": "hooks/gate-guardian.mjs", "timeout": 15000 }]
22
+ },
23
+ {
24
+ "matcher": "Write|Edit",
25
+ "hooks": [{ "type": "command", "command": "hooks/workflow-guard.mjs", "timeout": 10000 }]
26
+ }
27
+ ],
28
+ "SubagentStart": [
29
+ {
30
+ "matcher": "implementer|specifier|planner|task-generator|reviewer|researcher",
31
+ "hooks": [{ "type": "command", "command": "hooks/subagent-start.mjs", "timeout": 10000 }]
32
+ }
33
+ ],
34
+ "SubagentStop": [
35
+ {
36
+ "matcher": "implementer|specifier|planner|task-generator|reviewer|researcher",
37
+ "hooks": [{ "type": "command", "command": "hooks/subagent-stop.mjs", "timeout": 30000 }]
38
+ }
39
+ ],
40
+ "Stop": [
41
+ {
42
+ "matcher": "",
43
+ "hooks": [{ "type": "command", "command": "hooks/dangling-guard.mjs", "timeout": 15000 }]
44
+ }
45
+ ],
46
+ "PostToolUse": [
47
+ {
48
+ "matcher": "",
49
+ "hooks": [{ "type": "command", "command": "hooks/mailbox-listener.mjs", "timeout": 5000 }]
50
+ },
51
+ {
52
+ "matcher": "mcp__orchestrator-tools__advancePhase",
53
+ "hooks": [{ "type": "command", "command": "hooks/post-implement.mjs", "timeout": 15000 }]
54
+ }
55
+ ],
56
+ "PostCompact": [
57
+ {
58
+ "matcher": "",
59
+ "hooks": [{ "type": "command", "command": "hooks/post-compact.mjs", "timeout": 5000 }]
60
+ }
61
+ ]
62
+ }
63
+ }