@orchestrator-claude/definitions 3.31.1 → 3.32.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.
@@ -137,7 +137,7 @@ The Documentation Guardian is a specialized agent responsible for maintaining do
137
137
  ```
138
138
 
139
139
  **When to Use**:
140
- - As post-IMPLEMENT hook (triggered automatically)
140
+ - As the post-IMPLEMENT step (orchestrator-dispatched on the System-A post-implement signal — RFC-034)
141
141
  - For immediate execution when changes are trusted
142
142
  - In CI/CD pipelines for automatic documentation updates
143
143
 
@@ -248,82 +248,21 @@ The agent uses the `DocsGuardianSkill` which orchestrates:
248
248
  - **MisplacedDocsChecker**: Finds docs in wrong locations
249
249
  - **ArchiveCandidatesChecker**: Identifies docs to archive
250
250
 
251
- ### Post-IMPLEMENT Hook Integration
251
+ ### Post-IMPLEMENT Hook Integration (System A — RFC-034)
252
252
 
253
- The docs-guardian agent is automatically triggered after the IMPLEMENT phase completes:
253
+ docs-guardian is dispatched by the **orchestrator** after the IMPLEMENT phase completes, via the System-A hook-signal mechanism — **not** an application-internal executor.
254
254
 
255
- **Configuration** (in PostgreSQL):
256
- ```json
257
- {
258
- "defaultHooks": {
259
- "postImplement": {
260
- "agent": "docs-guardian",
261
- "mode": "auto",
262
- "blocking": false,
263
- "enabled": true,
264
- "scope": "all"
265
- }
266
- }
267
- }
268
- ```
269
-
270
- **Hook Execution Flow**:
271
- 1. IMPLEMENT phase completes
272
- 2. `WorkflowStateService.advancePhase()` detects transition from "implement"
273
- 3. `HookExecutor.executePostImplementHook()` invoked
274
- 4. Hook configuration loaded from index
275
- 5. Skip conditions evaluated
276
- 6. If enabled and not skipped:
277
- - SDKAgentExecutor invokes docs-guardian in auto mode
278
- - Checkpoint created
279
- - Actions executed
280
- - Execution tracked in index
281
- 7. Workflow continues (non-blocking)
282
-
283
- **Hook Execution Tracking**:
284
- ```json
285
- {
286
- "hookExecutions": [
287
- {
288
- "hookType": "postImplement",
289
- "workflowId": "wf_123",
290
- "timestamp": "2026-01-17T10:00:00Z",
291
- "duration": 3500,
292
- "status": "success",
293
- "result": {
294
- "actionsExecuted": 5,
295
- "actionsSucceeded": 5,
296
- "actionsFailed": 0
297
- }
298
- }
299
- ]
300
- }
301
- ```
255
+ **Mechanism (RFC-034 / TD-102):**
256
+ 1. IMPLEMENT phase completes; the orchestrator calls `advancePhase` and the workflow enters `review`.
257
+ 2. The OS-level hook `.claude/hooks/post-implement.ts` fires on `PostToolUse[mcp__orchestrator-tools__advancePhase]`, reads the live workflow state, and — when the post-advance `currentPhase === 'review'` (reached only by leaving `implement`) — emits the orchestrator signal:
258
+ `[ORCHESTRATOR] IMPLEMENT completed for <workflowId> — dispatch the docs-guardian sub-agent…`
259
+ 3. The **orchestrator** (main-as-orchestrator) reads the signal and dispatches docs-guardian as part of the review phase, before closing the workflow.
302
260
 
303
- **Customizing Hook Behavior**:
261
+ **Why the orchestrator dispatches, not the hook:** a hook (a node/bash script) cannot spawn a Claude sub-agent in the main-as-orchestrator model (RFC-022). `post-implement.ts` is a *deterministic trigger* that emits the signal; the orchestrator owns sub-agent dispatch. The dispatch rule lives in the orchestrator Skin (`.claude/agents/orchestrator.md`, MUST rule #5) and the `/orchestrator` SKILL (§2d.1).
304
262
 
305
- Disable for specific workflow:
306
- ```json
307
- {
308
- "workflows": [{
309
- "id": "wf_123",
310
- "hooks": {
311
- "postImplement": { "enabled": false }
312
- }
313
- }]
314
- }
315
- ```
263
+ **Fail-open:** if the API is unavailable or env is unset, the hook emits nothing and never blocks the advance. The orchestrator dispatches docs-guardian during review regardless, so coverage does not depend on the signal firing.
316
264
 
317
- Skip based on conditions:
318
- ```json
319
- {
320
- "workflows": [{
321
- "id": "wf_123",
322
- "metadata": { "docsImpact": false }
323
- }]
324
- }
325
- ```
326
- (Hook will be skipped if `skipConditions: ["no-docs-impact"]` is configured)
265
+ > **Historical note (S306, RFC-034 Decision 2):** prior versions documented an application-internal auto-invoke flow — `WorkflowStateService.advancePhase()` → `HookExecutor.executePostImplementHook()` → `SDKAgentExecutor invokes docs-guardian`. That "System B" subsystem was dead code (registered-but-never-resolved, stub-backed: `getHookConfig:()=>null` + a mock agent executor) and was removed completely in S306. The live mechanism is System A as described above.
327
266
 
328
267
  ## Error Handling
329
268
 
@@ -484,9 +423,10 @@ Archive specs/plans/RFCs promptly after implementation
484
423
  - **v1.2.1** (RFC-003 Phase 2): Added sync mode with action plan generation
485
424
  - **v1.2.2** (RFC-003 Phase 3): Added auto mode and post-IMPLEMENT hook integration
486
425
  - **v1.3.0**: Added AGENT-PROMPT-STANDARDS compliance (RFC 2119, severity classification)
426
+ - **v1.4.0** (RFC-034 / TD-102, S307): Post-IMPLEMENT integration migrated from the removed System-B auto-invoke (`HookExecutor`/`WorkflowStateService`) to the System-A hook-signal + orchestrator-dispatch mechanism.
487
427
 
488
428
  ---
489
429
 
490
430
  **Generated by**: RFC-003 Phase 3 Implementation
491
431
  **Standards Compliance**: AGENT-PROMPT-STANDARDS v1.0
492
- **Last Updated**: 2026-01-23
432
+ **Last Updated**: 2026-06-25
@@ -10,25 +10,163 @@ import {
10
10
  getField,
11
11
  getActiveWorkflow,
12
12
  getWorkflowStatus,
13
+ getLastCompletedWorkflow,
14
+ consumeLastCompletedWorkflow,
13
15
  outputBlock,
14
- } from "./lib/api-client.js";
16
+ postTelemetry,
17
+ type TelemetryPayload,
18
+ } from './lib/api-client.js';
19
+ import {
20
+ parseJsonlSession,
21
+ findJsonlPath,
22
+ estimateCost,
23
+ formatFinopsOutput,
24
+ type SessionMetrics,
25
+ } from './lib/finops-parser.js';
26
+
27
+ const HOOK = 'DANGLING-GUARD';
28
+
29
+ /**
30
+ * Returns true only when both workflowId and metrics are present.
31
+ * Fail-open guard: missing either means we skip the POST.
32
+ */
33
+ export function shouldPostTelemetry(
34
+ workflowId: string | null | undefined,
35
+ metrics: SessionMetrics | null | undefined
36
+ ): boolean {
37
+ return workflowId != null && workflowId !== '' && metrics != null;
38
+ }
39
+
40
+ /**
41
+ * Builds a TelemetryPayload from session metrics and estimated cost (USD).
42
+ */
43
+ export function buildTelemetryPayload(
44
+ sessionId: string | undefined,
45
+ metrics: SessionMetrics,
46
+ estimatedCostUsd: number,
47
+ model?: string,
48
+ variant?: string
49
+ ): TelemetryPayload {
50
+ const totalTokens = metrics.inputTokens + metrics.outputTokens + metrics.cacheReadTokens;
51
+ const cacheHitRate = totalTokens > 0 ? metrics.cacheReadTokens / totalTokens : 0;
52
+ return {
53
+ ...(sessionId ? { session_id: sessionId } : {}),
54
+ input_tokens: metrics.inputTokens,
55
+ output_tokens: metrics.outputTokens,
56
+ cache_read_tokens: metrics.cacheReadTokens,
57
+ total_tokens: totalTokens,
58
+ total_turns: metrics.toolCalls,
59
+ cache_hit_rate: Math.round(cacheHitRate * 10000) / 10000,
60
+ estimated_cost_usd: Math.round(estimatedCostUsd * 1000000) / 1000000,
61
+ ...(model ? { model_used: model } : {}),
62
+ ...(variant ? { variant } : {}),
63
+ };
64
+ }
65
+
66
+ /**
67
+ * FinOpsAI telemetry block — fail-open, 5s timeout.
68
+ * Parses session JSONL, emits cost/token metrics, and POSTs to API if workflow active.
69
+ */
70
+ async function emitFinopsTelemetry(
71
+ stdin: Record<string, unknown>,
72
+ workflowId?: string,
73
+ variant?: string
74
+ ): Promise<void> {
75
+ const sessionId = getField(stdin, 'session_id');
76
+ if (!sessionId) {
77
+ log(HOOK, '[FINOPS] No session_id in stdin, skipping telemetry');
78
+ return;
79
+ }
80
+
81
+ const jsonlPath = await findJsonlPath(sessionId);
82
+ if (!jsonlPath) {
83
+ log(HOOK, `[FINOPS] JSONL not found for session ${sessionId}, skipping`);
84
+ return;
85
+ }
86
+
87
+ const metrics = await parseJsonlSession(jsonlPath);
88
+ if (!metrics) {
89
+ log(HOOK, '[FINOPS] Failed to parse JSONL, skipping telemetry');
90
+ return;
91
+ }
92
+
93
+ const cost = estimateCost(metrics);
94
+ const output = formatFinopsOutput(metrics, cost);
95
+ console.error(output);
15
96
 
16
- const HOOK = "DANGLING-GUARD";
97
+ // POST to API only when both workflowId and metrics are available — fail-open
98
+ if (shouldPostTelemetry(workflowId, metrics)) {
99
+ try {
100
+ const payload = buildTelemetryPayload(
101
+ sessionId || undefined,
102
+ metrics,
103
+ cost,
104
+ undefined,
105
+ variant
106
+ );
107
+ const ok = await postTelemetry(workflowId!, payload);
108
+ if (ok) {
109
+ log(HOOK, `[FINOPS] Telemetry posted to workflow ${workflowId}`);
110
+ } else {
111
+ log(HOOK, `[FINOPS] postTelemetry returned false for workflow ${workflowId}`);
112
+ }
113
+ } catch (err) {
114
+ log(HOOK, `[FINOPS] postTelemetry threw: ${String(err)}`);
115
+ }
116
+ }
117
+ }
17
118
 
18
119
  async function main(): Promise<void> {
19
120
  const stdin = await readStdin();
20
- log(HOOK, "Stop hook triggered");
121
+ log(HOOK, `Stop hook triggered — stdin keys: [${Object.keys(stdin).join(', ')}]`);
21
122
 
22
123
  // Prevent infinite loops
23
- const stopActive = getField(stdin, "stop_hook_active");
24
- if (stopActive === "true") {
25
- log(HOOK, "stop_hook_active=true, allowing stop to prevent loop");
124
+ const stopActive = getField(stdin, 'stop_hook_active');
125
+ if (stopActive === 'true') {
126
+ log(HOOK, 'stop_hook_active=true, allowing stop to prevent loop');
26
127
  return;
27
128
  }
28
129
 
29
130
  const workflow = await getActiveWorkflow();
131
+
132
+ // B4 FinOps timing race fix: when getActiveWorkflow() returns null (workflow
133
+ // already completed before Stop hook fires), fall back to last-completed-workflow.json
134
+ // written by the completeWorkflow MCP handler.
135
+ let finopsWorkflowId: string | undefined = workflow?.id;
136
+ let usedFallback = false;
137
+ if (!workflow) {
138
+ const lastCompleted = getLastCompletedWorkflow();
139
+ if (lastCompleted) {
140
+ finopsWorkflowId = lastCompleted.workflowId;
141
+ usedFallback = true;
142
+ log(
143
+ HOOK,
144
+ `[FINOPS] Active workflow null — using last-completed fallback: ${finopsWorkflowId}`
145
+ );
146
+ }
147
+ }
148
+
149
+ // FinOpsAI telemetry — fail-open with 5s timeout
150
+ // Pass workflowId so telemetry is POSTed to the API when a workflow is active or recently completed
151
+ try {
152
+ await Promise.race([
153
+ emitFinopsTelemetry(stdin, finopsWorkflowId, workflow?.variant ?? undefined),
154
+ new Promise<void>((resolve) => setTimeout(resolve, 5000)),
155
+ ]);
156
+ } catch {
157
+ log(HOOK, '[FINOPS] Error in telemetry block, continuing');
158
+ }
159
+
160
+ // Idempotency guard: consume the state file after use (regardless of POST success)
161
+ // to prevent double-posting on subsequent Stop invocations (abnormal exits, retries).
162
+ // Fail-open: even if the POST failed, we consume to avoid infinite retry loops.
163
+ if (usedFallback) {
164
+ consumeLastCompletedWorkflow();
165
+ log(HOOK, '[FINOPS] Consumed last-completed state file (idempotency guard)');
166
+ }
167
+
30
168
  if (!workflow) {
31
- log(HOOK, "No active workflow, clean exit");
169
+ log(HOOK, 'No active workflow, clean exit');
32
170
  return;
33
171
  }
34
172
 
@@ -38,16 +176,16 @@ async function main(): Promise<void> {
38
176
 
39
177
  log(HOOK, `workflow=${workflow.id} status=${wfStatus} phase=${phase}`);
40
178
 
41
- const activeStatuses = ["in_progress", "awaiting_agent", "awaiting_approval"];
179
+ const activeStatuses = ['in_progress', 'awaiting_agent', 'awaiting_approval'];
42
180
  if (activeStatuses.includes(wfStatus)) {
43
- log(HOOK, "BLOCK stop (workflow still active)");
181
+ log(HOOK, 'BLOCK stop (workflow still active)');
44
182
  outputBlock(
45
183
  `[DANGLING-GUARD] Workflow ${workflow.id} is still ${wfStatus} (phase: ${phase}). You must call mcp__orchestrator-extended__completeWorkflow({ workflowId: '${workflow.id}' }) before ending the session.`
46
184
  );
47
185
  return;
48
186
  }
49
187
 
50
- log(HOOK, "Workflow completed or not active, allowing stop");
188
+ log(HOOK, 'Workflow completed or not active, allowing stop');
51
189
  }
52
190
 
53
191
  main().catch(() => process.exit(0));
@@ -26,12 +26,23 @@ import {
26
26
  getToken,
27
27
  getWorkflowMode,
28
28
  getNextAction,
29
+ getWorkflowTypeAndVariant,
30
+ getGateCellMode,
31
+ getAdversarialVerdict,
29
32
  outputPreToolUse,
30
33
  type PendingAction,
31
- } from "./lib/api-client.js";
32
- import { classifyRisk, deriveGateStrategy, type GateContext } from "./lib/gate-logic.js";
34
+ } from './lib/api-client.js';
35
+ import { classifyRisk, deriveGateStrategy, type GateContext } from './lib/gate-logic.js';
33
36
 
34
- const HOOK = "GATE-GUARDIAN";
37
+ const HOOK = 'GATE-GUARDIAN';
38
+
39
+ /**
40
+ * Maximum adversarial review iterations before the hard cap triggers.
41
+ * When iteration >= PME_HARD_CAP the gate ALLOWS (escalation path) and logs
42
+ * a BLOCKED-escalation warning. This prevents infinite review loops.
43
+ * RFC-031 §4.4 documents this mechanic.
44
+ */
45
+ const PME_HARD_CAP = 3;
35
46
 
36
47
  // ─────────────────────────────────────────────────────────────
37
48
  // Dependency injection interface (enables testing without IO)
@@ -42,7 +53,24 @@ export interface HookDeps {
42
53
  getWorkflowMode: (id: string) => Promise<string | null>;
43
54
  getToken: () => Promise<string | null>;
44
55
  getNextAction: (id: string) => Promise<PendingAction | null>;
45
- output: (opts: { decision: "allow" | "deny"; reason?: string; context?: string }) => void;
56
+ /** PME Step 5.5: retrieve workflow type + variant (fail-open: null) */
57
+ getWorkflowTypeAndVariant: (
58
+ workflowId: string,
59
+ token: string
60
+ ) => Promise<{ type: string; variant: string } | null>;
61
+ /** PME Step 5.5: retrieve gate cell mode for a type/variant/key combo (fail-open: null) */
62
+ getGateCellMode: (
63
+ typeId: string,
64
+ variant: string,
65
+ gateKey: string,
66
+ token: string
67
+ ) => Promise<'REQ' | 'OPT' | 'SKIP' | null>;
68
+ /** PME Step 5.5: retrieve adversarial review verdict + iteration (fail-open: null) */
69
+ getAdversarialVerdict: (
70
+ workflowId: string,
71
+ token: string
72
+ ) => Promise<{ verdict: string; iteration: number } | null>;
73
+ output: (opts: { decision: 'allow' | 'deny'; reason?: string; context?: string }) => void;
46
74
  log: (prefix: string, msg: string) => void;
47
75
  }
48
76
 
@@ -51,6 +79,9 @@ const defaultDeps: HookDeps = {
51
79
  getWorkflowMode,
52
80
  getToken,
53
81
  getNextAction,
82
+ getWorkflowTypeAndVariant,
83
+ getGateCellMode,
84
+ getAdversarialVerdict,
54
85
  output: outputPreToolUse,
55
86
  log,
56
87
  };
@@ -60,33 +91,43 @@ const defaultDeps: HookDeps = {
60
91
  // ─────────────────────────────────────────────────────────────
61
92
 
62
93
  export async function mainWithDeps(deps: HookDeps): Promise<void> {
63
- const { readStdin, getWorkflowMode, getToken, getNextAction, output, log } = deps;
94
+ const {
95
+ readStdin,
96
+ getWorkflowMode,
97
+ getToken,
98
+ getNextAction,
99
+ getWorkflowTypeAndVariant,
100
+ getGateCellMode,
101
+ getAdversarialVerdict,
102
+ output,
103
+ log,
104
+ } = deps;
64
105
 
65
106
  const stdin = await readStdin();
66
- log(HOOK, "PreToolUse advancePhase triggered");
107
+ log(HOOK, 'PreToolUse advancePhase triggered');
67
108
 
68
- const workflowId = getField(stdin, "tool_input.workflowId") || getField(stdin, "workflowId");
69
- const targetPhase = getField(stdin, "tool_input.targetPhase") || getField(stdin, "targetPhase");
109
+ const workflowId = getField(stdin, 'tool_input.workflowId') || getField(stdin, 'workflowId');
110
+ const targetPhase = getField(stdin, 'tool_input.targetPhase') || getField(stdin, 'targetPhase');
70
111
 
71
112
  log(HOOK, `workflow=${workflowId} targetPhase=${targetPhase}`);
72
113
 
73
114
  // ── Step 2: Mode check — quick/interactive bypass gate entirely ──────────
74
115
  if (workflowId) {
75
116
  const mode = await getWorkflowMode(workflowId);
76
- log(HOOK, `workflow=${workflowId} mode=${mode ?? "legacy"}`);
117
+ log(HOOK, `workflow=${workflowId} mode=${mode ?? 'legacy'}`);
77
118
 
78
- if (mode === "quick") {
119
+ if (mode === 'quick') {
79
120
  output({
80
- decision: "allow",
121
+ decision: 'allow',
81
122
  context: `[GATE] AUTO_APPROVE: quick mode skips gate evaluation for '${targetPhase}'.`,
82
123
  });
83
124
  log(HOOK, `ALLOW quick mode phase=${targetPhase}`);
84
125
  return;
85
126
  }
86
127
 
87
- if (mode === "interactive") {
128
+ if (mode === 'interactive') {
88
129
  output({
89
- decision: "allow",
130
+ decision: 'allow',
90
131
  context: `[GATE] AUTO_APPROVE: interactive mode skips artifact gate evaluation for '${targetPhase}'.`,
91
132
  });
92
133
  log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
@@ -96,27 +137,103 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
96
137
 
97
138
  // ── Step 3: Parse validation — FAIL-CLOSED ───────────────────────────────
98
139
  if (!workflowId || !targetPhase) {
99
- log(HOOK, "DENY (could not parse input)");
140
+ log(HOOK, 'DENY (could not parse input)');
100
141
  output({
101
- decision: "deny",
102
- reason: "Gate Guardian: Could not parse workflowId or targetPhase.",
103
- context: "Ensure you pass both workflowId and targetPhase to advancePhase.",
142
+ decision: 'deny',
143
+ reason: 'Gate Guardian: Could not parse workflowId or targetPhase.',
144
+ context: 'Ensure you pass both workflowId and targetPhase to advancePhase.',
104
145
  });
105
146
  return;
106
147
  }
107
148
 
108
149
  // ── Step 4: Auth check — FAIL-CLOSED ────────────────────────────────────
109
150
  const token = await getToken();
110
- if (!token) {
111
- log(HOOK, "DENY (auth failed)");
151
+ if (token === null || token === '') {
152
+ log(HOOK, 'DENY (auth failed)');
112
153
  output({
113
- decision: "deny",
114
- reason: "Gate Guardian: Authentication failed.",
115
- context: "Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars.",
154
+ decision: 'deny',
155
+ reason: 'Gate Guardian: Authentication failed.',
156
+ context:
157
+ 'Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars.',
116
158
  });
117
159
  return;
118
160
  }
119
161
 
162
+ // ── Step 5.5: PME adversarial gate enforcement (RFC-031 §4.4) ───────────
163
+ // Only enforced when:
164
+ // a) workflow type is 'project_management_enterprise'
165
+ // b) the variant's 'adversarial' gate cell is 'REQ'
166
+ // c) adversarial review artifact exists and verdict is not PASS
167
+ // All infra failures are fail-open (ALLOW) so hooks never block on downtime.
168
+ {
169
+ const typeVariant = await getWorkflowTypeAndVariant(workflowId, token);
170
+ if (typeVariant?.type === 'project_management_enterprise') {
171
+ const gateMode = await getGateCellMode(
172
+ typeVariant.type,
173
+ typeVariant.variant,
174
+ 'adversarial',
175
+ token
176
+ );
177
+ if (gateMode === 'REQ') {
178
+ const verdict = await getAdversarialVerdict(workflowId, token);
179
+ if (!verdict) {
180
+ // No artifact yet — fail-open: ALLOW
181
+ log(
182
+ HOOK,
183
+ `PME Step 5.5: no adversarial artifact found — fail-open ALLOW wf=${workflowId}`
184
+ );
185
+ output({
186
+ decision: 'allow',
187
+ context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`,
188
+ });
189
+ return;
190
+ }
191
+ if (verdict.iteration >= PME_HARD_CAP) {
192
+ // Hard cap reached — ALLOW with escalation log (prevents infinite loops)
193
+ log(
194
+ HOOK,
195
+ `PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(verdict.iteration)}) — ALLOW wf=${workflowId}`
196
+ );
197
+ output({
198
+ decision: 'allow',
199
+ context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(verdict.iteration)}/${String(PME_HARD_CAP)}), escalating.`,
200
+ });
201
+ return;
202
+ }
203
+ if (verdict.verdict === 'PASS') {
204
+ // Adversarial review passed — ALLOW
205
+ log(HOOK, `PME Step 5.5: adversarial verdict=PASS — ALLOW wf=${workflowId}`);
206
+ output({
207
+ decision: 'allow',
208
+ context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`,
209
+ });
210
+ return;
211
+ }
212
+ // Verdict is NEEDS_REVISION (or unknown) and below hard cap — DENY
213
+ log(
214
+ HOOK,
215
+ `PME Step 5.5: DENY verdict=${verdict.verdict} iteration=${String(verdict.iteration)} wf=${workflowId}`
216
+ );
217
+ output({
218
+ decision: 'deny',
219
+ reason: `Gate Guardian: adversarial_review required — verdict=${verdict.verdict}, iteration=${String(verdict.iteration)}. Resolve adversarial review before advancing.`,
220
+ });
221
+ return;
222
+ }
223
+ // gateMode is OPT, SKIP, or null — fall through to Step 5
224
+ log(
225
+ HOOK,
226
+ `PME Step 5.5: gateMode=${gateMode ?? 'null'} — ALLOW (non-REQ gate) wf=${workflowId}`
227
+ );
228
+ output({
229
+ decision: 'allow',
230
+ context: `[GATE] PME adversarial gate: mode=${gateMode ?? 'none'} (non-REQ), proceeding to '${targetPhase}'.`,
231
+ });
232
+ return;
233
+ }
234
+ // typeVariant null or non-PME type — fail-open or bypass, fall through
235
+ }
236
+
120
237
  // ── Step 5: IMPLEMENT short-circuit (RF-008) ─────────────────────────────
121
238
  // If targetPhase contains "implement", check pending-action BEFORE classifyRisk.
122
239
  // This prevents AUTO_APPROVE from being applied to implement phases.
@@ -125,25 +242,42 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
125
242
  // Other HIGH-risk phases (deploy, migrate, delete) will always ASK_HUMAN → DENY.
126
243
  // An approval path for these will be added in Phase 5 (Adaptive Gate V2).
127
244
  if (/implement/i.test(targetPhase)) {
128
- log(HOOK, "Checking approval for IMPLEMENT-type phase");
245
+ log(HOOK, 'Checking approval for IMPLEMENT-type phase');
129
246
  const action = await getNextAction(workflowId);
130
- const status = action?.pendingAction?.status ?? "";
247
+ const status = action?.pendingAction?.status ?? '';
131
248
 
132
- if (status === "approved" || status === "awaiting_agent") {
249
+ if (status === 'approved' || status === 'awaiting_agent') {
133
250
  output({
134
- decision: "allow",
251
+ decision: 'allow',
135
252
  context: `[GATE] IMPLEMENT approved (status=${status}): proceeding to phase '${targetPhase}'.`,
136
253
  });
137
- log(HOOK, JSON.stringify({ hook: "gate-guardian", strategy: "IMPLEMENT_APPROVED", targetPhase, workflowId }));
254
+ log(
255
+ HOOK,
256
+ JSON.stringify({
257
+ hook: 'gate-guardian',
258
+ strategy: 'IMPLEMENT_APPROVED',
259
+ targetPhase,
260
+ workflowId,
261
+ })
262
+ );
138
263
  return;
139
264
  }
140
265
 
141
266
  output({
142
- decision: "deny",
143
- reason: `Gate Guardian: IMPLEMENT requires human approval. Status: ${status || "not-set"}.`,
144
- context: "Ask the user for approval first. Then call mcp__orchestrator-tools__approveAction.",
267
+ decision: 'deny',
268
+ reason: `Gate Guardian: IMPLEMENT requires human approval. Status: ${status || 'not-set'}.`,
269
+ context: 'Ask the user for approval first. Then call mcp__orchestrator-tools__approveAction.',
145
270
  });
146
- log(HOOK, JSON.stringify({ hook: "gate-guardian", strategy: "IMPLEMENT_DENIED", targetPhase, workflowId, reason: "pending-action not approved" }));
271
+ log(
272
+ HOOK,
273
+ JSON.stringify({
274
+ hook: 'gate-guardian',
275
+ strategy: 'IMPLEMENT_DENIED',
276
+ targetPhase,
277
+ workflowId,
278
+ reason: 'pending-action not approved',
279
+ })
280
+ );
147
281
  return;
148
282
  }
149
283
 
@@ -155,7 +289,7 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
155
289
  targetPhase,
156
290
  // Reserved for Phase 5+6 — will be populated when OLAP integration provides
157
291
  // workflow context. Currently unused by deriveGateStrategy().
158
- currentPhase: "",
292
+ currentPhase: '',
159
293
  executionMode: null,
160
294
  workflowMode: null,
161
295
  riskLevel,
@@ -163,27 +297,30 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
163
297
 
164
298
  const decision = deriveGateStrategy(ctx);
165
299
 
166
- log(HOOK, JSON.stringify({ strategy: decision.strategy, targetPhase, riskLevel, reason: decision.reason }));
300
+ log(
301
+ HOOK,
302
+ JSON.stringify({ strategy: decision.strategy, targetPhase, riskLevel, reason: decision.reason })
303
+ );
167
304
 
168
305
  switch (decision.strategy) {
169
- case "AUTO_APPROVE":
306
+ case 'AUTO_APPROVE':
170
307
  output({
171
- decision: "allow",
308
+ decision: 'allow',
172
309
  context: `[GATE] AUTO_APPROVE: ${decision.reason}`,
173
310
  });
174
311
  break;
175
312
 
176
- case "ASK_HUMAN":
313
+ case 'ASK_HUMAN':
177
314
  output({
178
- decision: "deny",
315
+ decision: 'deny',
179
316
  reason: `Gate Guardian: ${decision.reason} Ask user for approval.`,
180
317
  });
181
318
  break;
182
319
 
183
- case "BLOCK_AND_REQUIRE":
320
+ case 'BLOCK_AND_REQUIRE':
184
321
  output({
185
- decision: "deny",
186
- reason: `Gate Guardian BLOCKED: ${decision.reason}. Required: ${decision.requiredAction ?? "unspecified"}.`,
322
+ decision: 'deny',
323
+ reason: `Gate Guardian BLOCKED: ${decision.reason}. Required: ${decision.requiredAction ?? 'unspecified'}.`,
187
324
  });
188
325
  break;
189
326
  }