@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
|
@@ -54,7 +54,7 @@ MUST follow these steps in order. Do not skip steps.
|
|
|
54
54
|
|
|
55
55
|
7. **Store critique artifact** — call `artifactStore` with:
|
|
56
56
|
- `artifact_kind: adversarial_review`
|
|
57
|
-
- `phase
|
|
57
|
+
- `phase: "adversarial_review"` — **always use this literal string, regardless of the phase of the artifact under review** (S188 fix: OQ-1 Option B; S185 artifacts stored at `phase='research'` are NOT migrated — applies from S186 onward)
|
|
58
58
|
- `content`: the full structured critique (see Output Format)
|
|
59
59
|
- `metadata.persona_id`: the persona used
|
|
60
60
|
- `metadata.iteration`: iteration number
|
package/agents/reviewer.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Agente Revisor que valida artefatos e codigo produzidos por outros agentes. Use para code review, validacao de specs/plans/tasks, e aprovacao de gates.
|
|
4
|
-
tools: Read, Grep, Glob, Bash, Write, Edit, mcp__orchestrator-extended__artifactRetrieve
|
|
4
|
+
tools: Read, Grep, Glob, Bash, Write, Edit, mcp__orchestrator-extended__artifactRetrieve, mcp__orchestrator-extended__artifactStore
|
|
5
5
|
model: sonnet
|
|
6
6
|
color: yellow
|
|
7
7
|
permissionMode: default
|
|
@@ -29,6 +29,8 @@ Sua funcao e revisar e validar artefatos e codigo produzidos por outros agentes.
|
|
|
29
29
|
## Ferramentas Disponiveis
|
|
30
30
|
|
|
31
31
|
### MCP Tools
|
|
32
|
+
- `artifactRetrieve(artifactId)`: Retrieve artifact under review from MinIO
|
|
33
|
+
- `artifactStore(workflowId, phase, content, ...)`: **Store review artifact directly** — your own review output (review.md, findings, verdict). Do NOT rely on the orchestrator to store it on your behalf (S188 gap resolved by TD-S188-001). Use `phase="review"` for friendly reviews; `phase="adversarial_review"` is reserved for `adversarial-reviewer` agent.
|
|
32
34
|
- `validateArtifact(path, type)`: Validacao automatizada
|
|
33
35
|
- `lookupKnowledgeBase(topic)`: Busca padroes e convencoes
|
|
34
36
|
|
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
|
}
|