@orchestrator-claude/definitions 3.43.1 → 3.44.1
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 +35 -29
- package/agents/agentic-architect.md +1 -1
- package/agents/api-extractor.md +1 -1
- package/agents/architecture-mediator.md +1 -1
- package/agents/azure-devops-engineer.md +1 -1
- package/agents/backend-architect.md +1 -1
- package/agents/business-rule-miner.md +1 -1
- package/agents/cicd-platform-engineer.md +1 -1
- package/agents/ciclo-engineer.md +1 -1
- package/agents/code-archaeologist.md +1 -1
- package/agents/contract-architect.md +1 -1
- package/agents/datamodel-architect.md +1 -1
- package/agents/frontend-architect.md +1 -1
- package/agents/frontend-engineer.md +1 -1
- package/agents/harness-architect.md +1 -1
- package/agents/harness-engineer.md +1 -1
- package/agents/implementer.md +1 -1
- package/agents/infra-architect.md +1 -1
- package/agents/legacy-discoverer.md +1 -1
- package/agents/legacy-synthesizer.md +1 -1
- package/agents/methodology-researcher.md +2 -1
- package/agents/operational-artifact-builder.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/presentation-engineer.md +1 -1
- package/agents/python-data-engineer.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/schema-extractor.md +1 -1
- package/agents/specifier.md +1 -1
- package/agents/task-generator.md +1 -1
- package/hooks/agent-git-guard.mjs +0 -2
- package/hooks/dangling-guard.mjs +18 -3
- package/hooks/gate-guardian.mjs +8617 -273
- package/hooks/gate-guardian.ts +296 -62
- package/hooks/lib/api-client.ts +154 -28
- package/hooks/lib/artifact-gate.ts +518 -0
- package/hooks/lib/artifact-producers.ts +106 -0
- package/hooks/lib/gate-logic.ts +6 -5
- package/hooks/lib/workflow-artifact-client.ts +351 -0
- package/hooks/mailbox-listener.mjs +0 -2
- package/hooks/playwright-output-guard.mjs +0 -2
- package/hooks/post-implement.mjs +18 -3
- package/hooks/pre-agent-dispatch.mjs +18 -3
- package/hooks/security-gate-honest.mjs +0 -2
- package/hooks/session-start.mjs +18 -3
- package/hooks/subagent-start.mjs +18 -3
- package/hooks/subagent-stop.mjs +18 -3
- package/hooks/user-prompt.mjs +18 -3
- package/hooks/verification-before-completion.mjs +0 -2
- package/hooks/workflow-guard.mjs +504 -29
- package/hooks/workflow-guard.ts +370 -27
- package/manifest.json +46 -46
- package/package.json +1 -1
package/hooks/gate-guardian.ts
CHANGED
|
@@ -13,10 +13,17 @@
|
|
|
13
13
|
* 6. classifyRisk(targetPhase) → riskLevel
|
|
14
14
|
* 7. buildGateContext() + deriveGateStrategy() → GateDecision
|
|
15
15
|
* 8. switch strategy:
|
|
16
|
-
*
|
|
16
|
+
* ALLOW_LOW_RISK → allow + context (no approval observed by this hook)
|
|
17
17
|
* ASK_HUMAN → deny + approval instructions
|
|
18
18
|
* BLOCK_AND_REQUIRE → deny + requiredAction
|
|
19
19
|
* 9. log(HOOK, JSON summary)
|
|
20
|
+
*
|
|
21
|
+
* RFC-044 F3, S5 (ADR-031): every ALLOW except quick goes through ONE exit, allowAfterLedger(), which first runs the
|
|
22
|
+
* ledger step over the files on disk (lib/artifact-gate.ts): verify the sealed phases exhaustively (D-469), validate
|
|
23
|
+
* and register the files of the phase being left (with advancedBySession, without agentType: TD-390, D-485 (1)).
|
|
24
|
+
* A failure there is a DENY. No DENY registers or verifies. In PME REQ the adversarial verdict is read from
|
|
25
|
+
* workflows/<wf>/<P>/adversarial_review.md first, and from the content store only when there is no file (RF-020).
|
|
26
|
+
* quick mode never registers (Q-PL-4); what stays open on purpose is in TD-391.
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
29
|
import {
|
|
@@ -30,9 +37,21 @@ import {
|
|
|
30
37
|
getGateCellMode,
|
|
31
38
|
getAdversarialVerdict,
|
|
32
39
|
outputPreToolUse,
|
|
40
|
+
type AdversarialVerdictResult,
|
|
33
41
|
type PendingAction,
|
|
34
42
|
} from './lib/api-client.js';
|
|
35
43
|
import { classifyRisk, deriveGateStrategy, type GateContext } from './lib/gate-logic.js';
|
|
44
|
+
import {
|
|
45
|
+
ADVERSARIAL_REVIEW_TYPE,
|
|
46
|
+
defaultArtifactGateDeps,
|
|
47
|
+
fileAdversarialVerdict,
|
|
48
|
+
observeWorkflow,
|
|
49
|
+
registerBeforeAllow,
|
|
50
|
+
sessionOf,
|
|
51
|
+
type ArtifactGateDeps,
|
|
52
|
+
type LedgerContext,
|
|
53
|
+
type Observation,
|
|
54
|
+
} from './lib/artifact-gate.js';
|
|
36
55
|
|
|
37
56
|
const HOOK = 'GATE-GUARDIAN';
|
|
38
57
|
|
|
@@ -40,7 +59,9 @@ const HOOK = 'GATE-GUARDIAN';
|
|
|
40
59
|
* Maximum adversarial review iterations before the hard cap triggers.
|
|
41
60
|
* When iteration >= PME_HARD_CAP the gate ALLOWS (escalation path) and logs
|
|
42
61
|
* a BLOCKED-escalation warning. This prevents infinite review loops.
|
|
43
|
-
* RFC-031 §4.4 documents this mechanic.
|
|
62
|
+
* RFC-031 §4.4 documents this mechanic. The cap only applies to a well-formed
|
|
63
|
+
* `iteration` (kind 'ok'); an `iteration` the helper rejected has no count to
|
|
64
|
+
* compare, so a non-PASS verdict with it is denied instead (TD-375).
|
|
44
65
|
*/
|
|
45
66
|
const PME_HARD_CAP = 3;
|
|
46
67
|
|
|
@@ -65,16 +86,15 @@ export interface HookDeps {
|
|
|
65
86
|
gateKey: string,
|
|
66
87
|
token: string
|
|
67
88
|
) => Promise<'REQ' | 'OPT' | 'SKIP' | null>;
|
|
68
|
-
/** PME Step 5.5: retrieve adversarial review verdict
|
|
69
|
-
getAdversarialVerdict: (
|
|
70
|
-
workflowId: string,
|
|
71
|
-
token: string
|
|
72
|
-
) => Promise<{ verdict: string; iteration: number } | null>;
|
|
89
|
+
/** PME Step 5.5: retrieve the classified adversarial review verdict (TD-374; never rejects) */
|
|
90
|
+
getAdversarialVerdict: (workflowId: string, token: string) => Promise<AdversarialVerdictResult>;
|
|
73
91
|
output: (opts: { decision: 'allow' | 'deny'; reason?: string; context?: string }) => void;
|
|
74
92
|
log: (prefix: string, msg: string) => void;
|
|
93
|
+
/** RFC-044 F3, S5: the disk + ledger step before every ALLOW (lib/artifact-gate.ts). */
|
|
94
|
+
artifacts: ArtifactGateDeps;
|
|
75
95
|
}
|
|
76
96
|
|
|
77
|
-
const defaultDeps: HookDeps = {
|
|
97
|
+
export const defaultDeps: HookDeps = {
|
|
78
98
|
readStdin,
|
|
79
99
|
getWorkflowMode,
|
|
80
100
|
getToken,
|
|
@@ -84,24 +104,180 @@ const defaultDeps: HookDeps = {
|
|
|
84
104
|
getAdversarialVerdict,
|
|
85
105
|
output: outputPreToolUse,
|
|
86
106
|
log,
|
|
107
|
+
artifacts: defaultArtifactGateDeps(),
|
|
87
108
|
};
|
|
88
109
|
|
|
110
|
+
// ─────────────────────────────────────────────────────────────
|
|
111
|
+
// PME Step 5.5: one decision per AdversarialVerdictResult kind
|
|
112
|
+
// ─────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
type Output = HookDeps['output'];
|
|
115
|
+
type Log = HookDeps['log'];
|
|
116
|
+
type GateOutput = Parameters<Output>[0];
|
|
117
|
+
|
|
118
|
+
const RESULT_KINDS: ReadonlySet<string> = new Set([
|
|
119
|
+
'ok',
|
|
120
|
+
'absent',
|
|
121
|
+
'malformed',
|
|
122
|
+
'iteration_rejected',
|
|
123
|
+
'error',
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
/** null / undefined / unknown kind (a stale dep or a bug) is an unexpected error, never a throw. */
|
|
127
|
+
function normalizeVerdictResult(raw: unknown): AdversarialVerdictResult {
|
|
128
|
+
const known =
|
|
129
|
+
typeof raw === 'object' && raw !== null && 'kind' in raw && RESULT_KINDS.has(String(raw.kind));
|
|
130
|
+
return known ? (raw as AdversarialVerdictResult) : { kind: 'error', reason: 'unexpected' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Applies the gate for an `ok` verdict: hard cap -> PASS -> DENY (unchanged texts). */
|
|
134
|
+
function decideOkVerdict(
|
|
135
|
+
result: Extract<AdversarialVerdictResult, { kind: 'ok' }>,
|
|
136
|
+
workflowId: string,
|
|
137
|
+
targetPhase: string,
|
|
138
|
+
output: Output,
|
|
139
|
+
log: Log
|
|
140
|
+
): void {
|
|
141
|
+
if (result.iteration >= PME_HARD_CAP) {
|
|
142
|
+
// Hard cap reached — ALLOW with escalation log (prevents infinite loops)
|
|
143
|
+
log(
|
|
144
|
+
HOOK,
|
|
145
|
+
`PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(result.iteration)}) — ALLOW wf=${workflowId}`
|
|
146
|
+
);
|
|
147
|
+
output({
|
|
148
|
+
decision: 'allow',
|
|
149
|
+
context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(result.iteration)}/${String(PME_HARD_CAP)}), escalating.`,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (result.verdict === 'PASS') {
|
|
154
|
+
log(HOOK, `PME Step 5.5: adversarial verdict=PASS — ALLOW wf=${workflowId}`);
|
|
155
|
+
output({
|
|
156
|
+
decision: 'allow',
|
|
157
|
+
context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`,
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Verdict is NEEDS_REVISION (or unknown) and below hard cap — DENY
|
|
162
|
+
log(
|
|
163
|
+
HOOK,
|
|
164
|
+
`PME Step 5.5: DENY verdict=${result.verdict} iteration=${String(result.iteration)} wf=${workflowId}`
|
|
165
|
+
);
|
|
166
|
+
output({
|
|
167
|
+
decision: 'deny',
|
|
168
|
+
reason: `Gate Guardian: adversarial_review required — verdict=${result.verdict}, iteration=${String(result.iteration)}. Resolve adversarial review before advancing.`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* `iteration` was a string that is not a canonical decimal integer (TD-375).
|
|
174
|
+
* PASS needs no hard cap, so it proceeds; any other verdict is denied with the
|
|
175
|
+
* way out spelled in the reason (the writer must fix the artifact).
|
|
176
|
+
*/
|
|
177
|
+
function decideRejectedIteration(
|
|
178
|
+
result: Extract<AdversarialVerdictResult, { kind: 'iteration_rejected' }>,
|
|
179
|
+
workflowId: string,
|
|
180
|
+
targetPhase: string,
|
|
181
|
+
output: Output,
|
|
182
|
+
log: Log
|
|
183
|
+
): void {
|
|
184
|
+
const { verdict, detail } = result;
|
|
185
|
+
if (verdict === 'PASS') {
|
|
186
|
+
log(
|
|
187
|
+
HOOK,
|
|
188
|
+
`PME Step 5.5: adversarial verdict=PASS with iteration_invalid (${detail}) — ALLOW wf=${workflowId}`
|
|
189
|
+
);
|
|
190
|
+
output({
|
|
191
|
+
decision: 'allow',
|
|
192
|
+
context: `[GATE] PME adversarial gate: verdict=PASS, iteration_invalid (${detail}), hard cap not evaluated, proceeding to '${targetPhase}'.`,
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
log(HOOK, `PME Step 5.5: DENY verdict=${verdict} iteration_invalid (${detail}) wf=${workflowId}`);
|
|
197
|
+
output({
|
|
198
|
+
decision: 'deny',
|
|
199
|
+
reason: `Gate Guardian: adversarial_review iteration invalid (${detail}) with verdict=${verdict} — the hard cap cannot apply. Rewrite the adversarial_review artifact with customMetadata.iteration as a non-negative decimal integer (e.g. '1') and retry.`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Maps one classified adversarial result to ALLOW/DENY. Fail-open except a rejected iteration and, under
|
|
205
|
+
* absentPolicy 'deny', an absent verdict.
|
|
206
|
+
* `absentPolicy` is the seam of Q-PL-3 (T-PM-09): 'allow' (the default) or 'deny', which the PME REQ call site
|
|
207
|
+
* passes since S6c — the reviewer can no longer write to the content store, so its file is the only channel.
|
|
208
|
+
* `expectedFile` names that file in the DENY reason (the actionable part: who did not write what).
|
|
209
|
+
*/
|
|
210
|
+
export function decideAdversarialResult(
|
|
211
|
+
raw: unknown,
|
|
212
|
+
workflowId: string,
|
|
213
|
+
targetPhase: string,
|
|
214
|
+
output: Output,
|
|
215
|
+
log: Log,
|
|
216
|
+
absentPolicy: 'allow' | 'deny' = 'allow',
|
|
217
|
+
expectedFile?: string
|
|
218
|
+
): void {
|
|
219
|
+
const result = normalizeVerdictResult(raw);
|
|
220
|
+
switch (result.kind) {
|
|
221
|
+
case 'ok':
|
|
222
|
+
decideOkVerdict(result, workflowId, targetPhase, output, log);
|
|
223
|
+
return;
|
|
224
|
+
case 'iteration_rejected':
|
|
225
|
+
decideRejectedIteration(result, workflowId, targetPhase, output, log);
|
|
226
|
+
return;
|
|
227
|
+
case 'absent':
|
|
228
|
+
if (absentPolicy === 'deny') {
|
|
229
|
+
log(
|
|
230
|
+
HOOK,
|
|
231
|
+
`PME Step 5.5: no adversarial artifact found — DENY (absentPolicy=deny) wf=${workflowId}`
|
|
232
|
+
);
|
|
233
|
+
const file =
|
|
234
|
+
expectedFile ??
|
|
235
|
+
`project-guidelines/workflows/${workflowId}/<phase>/${ADVERSARIAL_REVIEW_TYPE}.md`;
|
|
236
|
+
output({
|
|
237
|
+
decision: 'deny',
|
|
238
|
+
reason: `Gate Guardian: adversarial_review required — the adversarial-reviewer did not write ${file} and the content store has no verdict, so '${targetPhase}' cannot be entered. Dispatch the adversarial-reviewer for this phase (it writes that file) and retry.`,
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
log(HOOK, `PME Step 5.5: no adversarial artifact found — fail-open ALLOW wf=${workflowId}`);
|
|
243
|
+
output({
|
|
244
|
+
decision: 'allow',
|
|
245
|
+
context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`,
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
case 'malformed': {
|
|
249
|
+
const detail = result.detail === undefined ? '' : ` detail=${result.detail}`;
|
|
250
|
+
log(
|
|
251
|
+
HOOK,
|
|
252
|
+
`PME Step 5.5: adversarial artifact present but malformed (${result.reason}${detail}) — fail-open ALLOW wf=${workflowId}`
|
|
253
|
+
);
|
|
254
|
+
output({
|
|
255
|
+
decision: 'allow',
|
|
256
|
+
context: `[GATE] PME adversarial gate: artifact malformed (${result.reason}), fail-open ALLOW for '${targetPhase}'.`,
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
case 'error': {
|
|
261
|
+
const status = result.status === undefined ? '' : ` status=${String(result.status)}`;
|
|
262
|
+
log(
|
|
263
|
+
HOOK,
|
|
264
|
+
`PME Step 5.5: adversarial artifact unreadable (${result.reason}${status}) — fail-open ALLOW wf=${workflowId}`
|
|
265
|
+
);
|
|
266
|
+
output({
|
|
267
|
+
decision: 'allow',
|
|
268
|
+
context: `[GATE] PME adversarial gate: artifact could not be read (${result.reason}), fail-open ALLOW for '${targetPhase}'.`,
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
89
275
|
// ─────────────────────────────────────────────────────────────
|
|
90
276
|
// Core logic (exported for testing)
|
|
91
277
|
// ─────────────────────────────────────────────────────────────
|
|
92
278
|
|
|
93
279
|
export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
94
|
-
const {
|
|
95
|
-
readStdin,
|
|
96
|
-
getWorkflowMode,
|
|
97
|
-
getToken,
|
|
98
|
-
getNextAction,
|
|
99
|
-
getWorkflowTypeAndVariant,
|
|
100
|
-
getGateCellMode,
|
|
101
|
-
getAdversarialVerdict,
|
|
102
|
-
output,
|
|
103
|
-
log,
|
|
104
|
-
} = deps;
|
|
280
|
+
const { readStdin, getWorkflowMode, getToken, output, log } = deps;
|
|
105
281
|
|
|
106
282
|
const stdin = await readStdin();
|
|
107
283
|
log(HOOK, 'PreToolUse advancePhase triggered');
|
|
@@ -111,7 +287,39 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
111
287
|
|
|
112
288
|
log(HOOK, `workflow=${workflowId} targetPhase=${targetPhase}`);
|
|
113
289
|
|
|
114
|
-
// ──
|
|
290
|
+
// ── RFC-044 F3, S5: the ledger step, shared by every ALLOW (one exit point) ─────────
|
|
291
|
+
const session = sessionOf(stdin.session_id);
|
|
292
|
+
const ledgerCtx: LedgerContext = {
|
|
293
|
+
workflowId,
|
|
294
|
+
...(session.kind === 'ok' && { advancedBySession: session.advancedBySession }),
|
|
295
|
+
};
|
|
296
|
+
let observation: Promise<Observation> | undefined;
|
|
297
|
+
const observe = (): Promise<Observation> =>
|
|
298
|
+
(observation ??= observeWorkflow(workflowId, deps.artifacts));
|
|
299
|
+
|
|
300
|
+
/** The ONE exit of every ALLOW (but quick): verify + register first; a failure there is a DENY. */
|
|
301
|
+
const allowAfterLedger = async (allow: GateOutput): Promise<boolean> => {
|
|
302
|
+
if (session.kind === 'omitted') {
|
|
303
|
+
log(HOOK, `advancedBySession omitted (session_id ${session.why}) wf=${workflowId}`);
|
|
304
|
+
}
|
|
305
|
+
const observed = await observe();
|
|
306
|
+
const outcome =
|
|
307
|
+
observed.kind === 'deny'
|
|
308
|
+
? ({ ok: false, reason: observed.reason } as const)
|
|
309
|
+
: await registerBeforeAllow(observed, ledgerCtx, deps.artifacts, (m) => {
|
|
310
|
+
log(HOOK, m);
|
|
311
|
+
});
|
|
312
|
+
if (!outcome.ok) {
|
|
313
|
+
log(HOOK, `DENY (ledger step) wf=${workflowId}: ${outcome.reason}`);
|
|
314
|
+
output({ decision: 'deny', reason: outcome.reason });
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
log(HOOK, `${outcome.summary} wf=${workflowId}`);
|
|
318
|
+
output(allow);
|
|
319
|
+
return true;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// ── Step 2: Mode check — quick bypasses the gate; interactive skips its evaluation ──
|
|
115
323
|
if (workflowId) {
|
|
116
324
|
const mode = await getWorkflowMode(workflowId);
|
|
117
325
|
log(HOOK, `workflow=${workflowId} mode=${mode ?? 'legacy'}`);
|
|
@@ -119,18 +327,18 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
119
327
|
if (mode === 'quick') {
|
|
120
328
|
output({
|
|
121
329
|
decision: 'allow',
|
|
122
|
-
context: `[GATE]
|
|
330
|
+
context: `[GATE] ALLOW_QUICK_MODE: quick mode skips gate evaluation for '${targetPhase}'; no approval observed by this hook.`,
|
|
123
331
|
});
|
|
124
332
|
log(HOOK, `ALLOW quick mode phase=${targetPhase}`);
|
|
125
333
|
return;
|
|
126
334
|
}
|
|
127
335
|
|
|
128
336
|
if (mode === 'interactive') {
|
|
129
|
-
|
|
337
|
+
const allowed = await allowAfterLedger({
|
|
130
338
|
decision: 'allow',
|
|
131
|
-
context: `[GATE]
|
|
339
|
+
context: `[GATE] ALLOW_INTERACTIVE_MODE: interactive mode skips artifact gate evaluation for '${targetPhase}'; no approval observed by this hook.`,
|
|
132
340
|
});
|
|
133
|
-
log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
|
|
341
|
+
if (allowed) log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
|
|
134
342
|
return;
|
|
135
343
|
}
|
|
136
344
|
}
|
|
@@ -159,12 +367,49 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
159
367
|
return;
|
|
160
368
|
}
|
|
161
369
|
|
|
370
|
+
let decided: GateOutput | undefined;
|
|
371
|
+
const capture: Output = (o) => {
|
|
372
|
+
decided ??= o;
|
|
373
|
+
};
|
|
374
|
+
await evaluateGate(deps, { workflowId, targetPhase, token, observe, output: capture });
|
|
375
|
+
if (decided === undefined) {
|
|
376
|
+
output({ decision: 'deny', reason: 'Gate Guardian: no decision was produced (bug); denying.' });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (decided.decision === 'deny') {
|
|
380
|
+
output(decided);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
await allowAfterLedger(decided);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
interface GateInput {
|
|
387
|
+
workflowId: string;
|
|
388
|
+
targetPhase: string;
|
|
389
|
+
token: string;
|
|
390
|
+
observe: () => Promise<Observation>;
|
|
391
|
+
/** Captures the decision; the caller routes an ALLOW through the ledger step. */
|
|
392
|
+
output: Output;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Steps 5.5, 5 and 6–8: the decision itself, written to `input.output` (a capture), never to stdout. */
|
|
396
|
+
async function evaluateGate(deps: HookDeps, input: GateInput): Promise<void> {
|
|
397
|
+
const { getNextAction, getWorkflowTypeAndVariant, getGateCellMode, getAdversarialVerdict, log } =
|
|
398
|
+
deps;
|
|
399
|
+
const { workflowId, targetPhase, token, observe, output } = input;
|
|
400
|
+
|
|
162
401
|
// ── Step 5.5: PME adversarial gate enforcement (RFC-031 §4.4) ───────────
|
|
163
402
|
// Only enforced when:
|
|
164
403
|
// a) workflow type is 'project_management_enterprise'
|
|
165
404
|
// b) the variant's 'adversarial' gate cell is 'REQ'
|
|
166
405
|
// c) adversarial review artifact exists and verdict is not PASS
|
|
167
|
-
//
|
|
406
|
+
// Malformed / unreadable artifacts are fail-open (ALLOW) so hooks never block
|
|
407
|
+
// on downtime; each is logged under its own label (TD-374). Exceptions: a
|
|
408
|
+
// non-PASS verdict whose `iteration` is not a canonical decimal integer is
|
|
409
|
+
// denied (TD-375), and since S6c (T-S6c-03, T-PM-09) an ABSENT verdict is
|
|
410
|
+
// denied too — the reviewer's vault file is its only channel, so no file and no
|
|
411
|
+
// content-store verdict means the reviewer did not write (R-12). PME non-REQ,
|
|
412
|
+
// quick and interactive never reach this branch.
|
|
168
413
|
{
|
|
169
414
|
const typeVariant = await getWorkflowTypeAndVariant(workflowId, token);
|
|
170
415
|
if (typeVariant?.type === 'project_management_enterprise') {
|
|
@@ -175,49 +420,38 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
175
420
|
token
|
|
176
421
|
);
|
|
177
422
|
if (gateMode === 'REQ') {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
});
|
|
423
|
+
// RF-020: the file of the phase being left comes first; the content store only when there is none.
|
|
424
|
+
const observed = await observe();
|
|
425
|
+
if (observed.kind === 'deny') {
|
|
426
|
+
output({ decision: 'deny', reason: observed.reason });
|
|
201
427
|
return;
|
|
202
428
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
log(HOOK, `PME Step 5.5:
|
|
429
|
+
const fromFile = fileAdversarialVerdict(observed);
|
|
430
|
+
if (fromFile.kind === 'invalid') {
|
|
431
|
+
log(HOOK, `PME Step 5.5: DENY adversarial_review file invalid wf=${workflowId}`);
|
|
206
432
|
output({
|
|
207
|
-
decision: '
|
|
208
|
-
|
|
433
|
+
decision: 'deny',
|
|
434
|
+
reason: `Gate Guardian: adversarial_review file invalid — ${fromFile.reason} Rewrite it with a string verdict and an integer iteration (ADR-031 D3).`,
|
|
209
435
|
});
|
|
210
436
|
return;
|
|
211
437
|
}
|
|
212
|
-
|
|
438
|
+
const result =
|
|
439
|
+
fromFile.kind === 'verdict'
|
|
440
|
+
? fromFile.result
|
|
441
|
+
: await getAdversarialVerdict(workflowId, token);
|
|
213
442
|
log(
|
|
214
443
|
HOOK,
|
|
215
|
-
`PME Step 5.5:
|
|
444
|
+
`PME Step 5.5: verdict source=${fromFile.kind === 'verdict' ? 'file' : 'content-store'} wf=${workflowId}`
|
|
445
|
+
);
|
|
446
|
+
decideAdversarialResult(
|
|
447
|
+
result,
|
|
448
|
+
workflowId,
|
|
449
|
+
targetPhase,
|
|
450
|
+
output,
|
|
451
|
+
log,
|
|
452
|
+
'deny',
|
|
453
|
+
`project-guidelines/workflows/${workflowId}/${observed.phase}/${ADVERSARIAL_REVIEW_TYPE}.md`
|
|
216
454
|
);
|
|
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
455
|
return;
|
|
222
456
|
}
|
|
223
457
|
// gateMode is OPT, SKIP, or null — fall through to Step 5
|
|
@@ -236,7 +470,7 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
236
470
|
|
|
237
471
|
// ── Step 5: IMPLEMENT short-circuit (RF-008) ─────────────────────────────
|
|
238
472
|
// If targetPhase contains "implement", check pending-action BEFORE classifyRisk.
|
|
239
|
-
// This prevents
|
|
473
|
+
// This prevents the low-risk allow path from being applied to implement phases.
|
|
240
474
|
//
|
|
241
475
|
// Phase 4: Only 'implement' phases have the approval path (via pending-action).
|
|
242
476
|
// Other HIGH-risk phases (deploy, migrate, delete) will always ASK_HUMAN → DENY.
|
|
@@ -303,10 +537,10 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
303
537
|
);
|
|
304
538
|
|
|
305
539
|
switch (decision.strategy) {
|
|
306
|
-
case '
|
|
540
|
+
case 'ALLOW_LOW_RISK':
|
|
307
541
|
output({
|
|
308
542
|
decision: 'allow',
|
|
309
|
-
context: `[GATE]
|
|
543
|
+
context: `[GATE] ALLOW_LOW_RISK: ${decision.reason}`,
|
|
310
544
|
});
|
|
311
545
|
break;
|
|
312
546
|
|