@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.
- package/agents/docs-guardian.md +13 -73
- 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/post-implement.ts +88 -0
- 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 +53 -29
- package/package.json +1 -1
- package/skills/backlog-synthesis/SKILL.md +1 -1
- 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/agents/docs-guardian.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
**
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
432
|
+
**Last Updated**: 2026-06-25
|
package/hooks/dangling-guard.ts
CHANGED
|
@@ -10,25 +10,163 @@ import {
|
|
|
10
10
|
getField,
|
|
11
11
|
getActiveWorkflow,
|
|
12
12
|
getWorkflowStatus,
|
|
13
|
+
getLastCompletedWorkflow,
|
|
14
|
+
consumeLastCompletedWorkflow,
|
|
13
15
|
outputBlock,
|
|
14
|
-
|
|
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
|
-
|
|
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,
|
|
121
|
+
log(HOOK, `Stop hook triggered — stdin keys: [${Object.keys(stdin).join(', ')}]`);
|
|
21
122
|
|
|
22
123
|
// Prevent infinite loops
|
|
23
|
-
const stopActive = getField(stdin,
|
|
24
|
-
if (stopActive ===
|
|
25
|
-
log(HOOK,
|
|
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,
|
|
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 = [
|
|
179
|
+
const activeStatuses = ['in_progress', 'awaiting_agent', 'awaiting_approval'];
|
|
42
180
|
if (activeStatuses.includes(wfStatus)) {
|
|
43
|
-
log(HOOK,
|
|
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,
|
|
188
|
+
log(HOOK, 'Workflow completed or not active, allowing stop');
|
|
51
189
|
}
|
|
52
190
|
|
|
53
191
|
main().catch(() => process.exit(0));
|
package/hooks/gate-guardian.ts
CHANGED
|
@@ -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
|
|
32
|
-
import { classifyRisk, deriveGateStrategy, type GateContext } from
|
|
34
|
+
} from './lib/api-client.js';
|
|
35
|
+
import { classifyRisk, deriveGateStrategy, type GateContext } from './lib/gate-logic.js';
|
|
33
36
|
|
|
34
|
-
const HOOK =
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
107
|
+
log(HOOK, 'PreToolUse advancePhase triggered');
|
|
67
108
|
|
|
68
|
-
const workflowId = getField(stdin,
|
|
69
|
-
const targetPhase = getField(stdin,
|
|
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 ??
|
|
117
|
+
log(HOOK, `workflow=${workflowId} mode=${mode ?? 'legacy'}`);
|
|
77
118
|
|
|
78
|
-
if (mode ===
|
|
119
|
+
if (mode === 'quick') {
|
|
79
120
|
output({
|
|
80
|
-
decision:
|
|
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 ===
|
|
128
|
+
if (mode === 'interactive') {
|
|
88
129
|
output({
|
|
89
|
-
decision:
|
|
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,
|
|
140
|
+
log(HOOK, 'DENY (could not parse input)');
|
|
100
141
|
output({
|
|
101
|
-
decision:
|
|
102
|
-
reason:
|
|
103
|
-
context:
|
|
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 (
|
|
111
|
-
log(HOOK,
|
|
151
|
+
if (token === null || token === '') {
|
|
152
|
+
log(HOOK, 'DENY (auth failed)');
|
|
112
153
|
output({
|
|
113
|
-
decision:
|
|
114
|
-
reason:
|
|
115
|
-
context:
|
|
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,
|
|
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 ===
|
|
249
|
+
if (status === 'approved' || status === 'awaiting_agent') {
|
|
133
250
|
output({
|
|
134
|
-
decision:
|
|
251
|
+
decision: 'allow',
|
|
135
252
|
context: `[GATE] IMPLEMENT approved (status=${status}): proceeding to phase '${targetPhase}'.`,
|
|
136
253
|
});
|
|
137
|
-
log(
|
|
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:
|
|
143
|
-
reason: `Gate Guardian: IMPLEMENT requires human approval. Status: ${status ||
|
|
144
|
-
context:
|
|
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(
|
|
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(
|
|
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
|
|
306
|
+
case 'AUTO_APPROVE':
|
|
170
307
|
output({
|
|
171
|
-
decision:
|
|
308
|
+
decision: 'allow',
|
|
172
309
|
context: `[GATE] AUTO_APPROVE: ${decision.reason}`,
|
|
173
310
|
});
|
|
174
311
|
break;
|
|
175
312
|
|
|
176
|
-
case
|
|
313
|
+
case 'ASK_HUMAN':
|
|
177
314
|
output({
|
|
178
|
-
decision:
|
|
315
|
+
decision: 'deny',
|
|
179
316
|
reason: `Gate Guardian: ${decision.reason} Ask user for approval.`,
|
|
180
317
|
});
|
|
181
318
|
break;
|
|
182
319
|
|
|
183
|
-
case
|
|
320
|
+
case 'BLOCK_AND_REQUIRE':
|
|
184
321
|
output({
|
|
185
|
-
decision:
|
|
186
|
-
reason: `Gate Guardian BLOCKED: ${decision.reason}. Required: ${decision.requiredAction ??
|
|
322
|
+
decision: 'deny',
|
|
323
|
+
reason: `Gate Guardian BLOCKED: ${decision.reason}. Required: ${decision.requiredAction ?? 'unspecified'}.`,
|
|
187
324
|
});
|
|
188
325
|
break;
|
|
189
326
|
}
|