@orchestrator-claude/definitions 3.43.1 → 3.44.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.
@@ -213,18 +213,74 @@ async function getGateCellMode(typeId, variant, gateKey, token) {
213
213
  return null;
214
214
  }
215
215
  }
216
+ async function apiGetWithStatus(path, token) {
217
+ try {
218
+ const resp = await fetch(`${API_URL}${path}`, {
219
+ headers: {
220
+ Authorization: `Bearer ${token}`,
221
+ "X-Project-ID": PROJECT_ID
222
+ },
223
+ signal: AbortSignal.timeout(5e3)
224
+ });
225
+ return { kind: "response", resp };
226
+ } catch (err) {
227
+ const name = typeof err === "object" && err !== null && "name" in err ? err.name : "";
228
+ const timedOut = name === "TimeoutError" || name === "AbortError";
229
+ return { kind: "error", reason: timedOut ? "timeout" : "network" };
230
+ }
231
+ }
232
+ function iterationDetail(raw) {
233
+ const json = JSON.stringify(raw);
234
+ return json === void 0 ? void 0 : json.slice(0, 40);
235
+ }
236
+ function parseIteration(raw) {
237
+ if (!/^[0-9]+$/.test(raw)) return null;
238
+ const value = Number(raw);
239
+ return Number.isSafeInteger(value) ? value : null;
240
+ }
241
+ function classifyIteration(verdict, rawIteration) {
242
+ if (typeof rawIteration !== "string") {
243
+ const detail = iterationDetail(rawIteration);
244
+ return detail === void 0 ? { kind: "malformed", reason: "iteration_invalid" } : { kind: "malformed", reason: "iteration_invalid", detail };
245
+ }
246
+ const iteration = parseIteration(rawIteration);
247
+ if (iteration === null) {
248
+ return { kind: "iteration_rejected", verdict, detail: iterationDetail(rawIteration) ?? "" };
249
+ }
250
+ return { kind: "ok", verdict, iteration };
251
+ }
252
+ function classifyAdversarialBody(body) {
253
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
254
+ return { kind: "malformed", reason: "body_not_object" };
255
+ }
256
+ const metadata = "metadata" in body ? body.metadata : void 0;
257
+ const customMetadata = typeof metadata === "object" && metadata !== null && "customMetadata" in metadata ? metadata.customMetadata : void 0;
258
+ if (typeof customMetadata !== "object" || customMetadata === null) {
259
+ return { kind: "malformed", reason: "custom_metadata_missing" };
260
+ }
261
+ const verdict = "verdict" in customMetadata ? customMetadata.verdict : void 0;
262
+ if (typeof verdict !== "string") return { kind: "malformed", reason: "verdict_invalid" };
263
+ const rawIteration = "iteration" in customMetadata ? customMetadata.iteration : void 0;
264
+ return classifyIteration(verdict, rawIteration);
265
+ }
216
266
  async function getAdversarialVerdict(workflowId, token) {
217
267
  try {
218
- const resp = await apiGet(`/api/v1/artifact-content/${workflowId}/adversarial_review`, token);
219
- if (!resp) return null;
220
- const { verdict, iteration: rawIteration } = resp.metadata?.customMetadata ?? {};
221
- if (typeof verdict !== "string" || typeof rawIteration !== "string") return null;
222
- if (rawIteration.trim() === "") return null;
223
- const iteration = Number(rawIteration);
224
- if (!Number.isFinite(iteration)) return null;
225
- return { verdict, iteration };
268
+ const got = await apiGetWithStatus(
269
+ `/api/v1/artifact-content/${workflowId}/adversarial_review`,
270
+ token
271
+ );
272
+ if (got.kind === "error") return got;
273
+ if (got.resp.status === 404) return { kind: "absent" };
274
+ if (!got.resp.ok) return { kind: "error", reason: "http", status: got.resp.status };
275
+ let body;
276
+ try {
277
+ body = await got.resp.json();
278
+ } catch {
279
+ return { kind: "malformed", reason: "body_not_json" };
280
+ }
281
+ return classifyAdversarialBody(body);
226
282
  } catch {
227
- return null;
283
+ return { kind: "error", reason: "unexpected" };
228
284
  }
229
285
  }
230
286
  async function readStdin() {
@@ -276,8 +332,8 @@ function deriveGateStrategy(ctx) {
276
332
  };
277
333
  }
278
334
  return {
279
- strategy: "AUTO_APPROVE",
280
- reason: `Phase '${ctx.targetPhase}' is low-risk \u2014 auto-approved.`
335
+ strategy: "ALLOW_LOW_RISK",
336
+ reason: `Phase '${ctx.targetPhase}' is low-risk \u2014 allowed without an approval check; no approval observed by this hook.`
281
337
  };
282
338
  }
283
339
 
@@ -295,6 +351,107 @@ var defaultDeps = {
295
351
  output: outputPreToolUse,
296
352
  log
297
353
  };
354
+ var RESULT_KINDS = /* @__PURE__ */ new Set([
355
+ "ok",
356
+ "absent",
357
+ "malformed",
358
+ "iteration_rejected",
359
+ "error"
360
+ ]);
361
+ function normalizeVerdictResult(raw) {
362
+ const known = typeof raw === "object" && raw !== null && "kind" in raw && RESULT_KINDS.has(String(raw.kind));
363
+ return known ? raw : { kind: "error", reason: "unexpected" };
364
+ }
365
+ function decideOkVerdict(result, workflowId, targetPhase, output, log2) {
366
+ if (result.iteration >= PME_HARD_CAP) {
367
+ log2(
368
+ HOOK,
369
+ `PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(result.iteration)}) \u2014 ALLOW wf=${workflowId}`
370
+ );
371
+ output({
372
+ decision: "allow",
373
+ context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(result.iteration)}/${String(PME_HARD_CAP)}), escalating.`
374
+ });
375
+ return;
376
+ }
377
+ if (result.verdict === "PASS") {
378
+ log2(HOOK, `PME Step 5.5: adversarial verdict=PASS \u2014 ALLOW wf=${workflowId}`);
379
+ output({
380
+ decision: "allow",
381
+ context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`
382
+ });
383
+ return;
384
+ }
385
+ log2(
386
+ HOOK,
387
+ `PME Step 5.5: DENY verdict=${result.verdict} iteration=${String(result.iteration)} wf=${workflowId}`
388
+ );
389
+ output({
390
+ decision: "deny",
391
+ reason: `Gate Guardian: adversarial_review required \u2014 verdict=${result.verdict}, iteration=${String(result.iteration)}. Resolve adversarial review before advancing.`
392
+ });
393
+ }
394
+ function decideRejectedIteration(result, workflowId, targetPhase, output, log2) {
395
+ const { verdict, detail } = result;
396
+ if (verdict === "PASS") {
397
+ log2(
398
+ HOOK,
399
+ `PME Step 5.5: adversarial verdict=PASS with iteration_invalid (${detail}) \u2014 ALLOW wf=${workflowId}`
400
+ );
401
+ output({
402
+ decision: "allow",
403
+ context: `[GATE] PME adversarial gate: verdict=PASS, iteration_invalid (${detail}), hard cap not evaluated, proceeding to '${targetPhase}'.`
404
+ });
405
+ return;
406
+ }
407
+ log2(HOOK, `PME Step 5.5: DENY verdict=${verdict} iteration_invalid (${detail}) wf=${workflowId}`);
408
+ output({
409
+ decision: "deny",
410
+ reason: `Gate Guardian: adversarial_review iteration invalid (${detail}) with verdict=${verdict} \u2014 the hard cap cannot apply. Rewrite the adversarial_review artifact with customMetadata.iteration as a non-negative decimal integer (e.g. '1') and retry.`
411
+ });
412
+ }
413
+ function decideAdversarialResult(raw, workflowId, targetPhase, output, log2) {
414
+ const result = normalizeVerdictResult(raw);
415
+ switch (result.kind) {
416
+ case "ok":
417
+ decideOkVerdict(result, workflowId, targetPhase, output, log2);
418
+ return;
419
+ case "iteration_rejected":
420
+ decideRejectedIteration(result, workflowId, targetPhase, output, log2);
421
+ return;
422
+ case "absent":
423
+ log2(HOOK, `PME Step 5.5: no adversarial artifact found \u2014 fail-open ALLOW wf=${workflowId}`);
424
+ output({
425
+ decision: "allow",
426
+ context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`
427
+ });
428
+ return;
429
+ case "malformed": {
430
+ const detail = result.detail === void 0 ? "" : ` detail=${result.detail}`;
431
+ log2(
432
+ HOOK,
433
+ `PME Step 5.5: adversarial artifact present but malformed (${result.reason}${detail}) \u2014 fail-open ALLOW wf=${workflowId}`
434
+ );
435
+ output({
436
+ decision: "allow",
437
+ context: `[GATE] PME adversarial gate: artifact malformed (${result.reason}), fail-open ALLOW for '${targetPhase}'.`
438
+ });
439
+ return;
440
+ }
441
+ case "error": {
442
+ const status = result.status === void 0 ? "" : ` status=${String(result.status)}`;
443
+ log2(
444
+ HOOK,
445
+ `PME Step 5.5: adversarial artifact unreadable (${result.reason}${status}) \u2014 fail-open ALLOW wf=${workflowId}`
446
+ );
447
+ output({
448
+ decision: "allow",
449
+ context: `[GATE] PME adversarial gate: artifact could not be read (${result.reason}), fail-open ALLOW for '${targetPhase}'.`
450
+ });
451
+ return;
452
+ }
453
+ }
454
+ }
298
455
  async function mainWithDeps(deps) {
299
456
  const {
300
457
  readStdin: readStdin2,
@@ -318,7 +475,7 @@ async function mainWithDeps(deps) {
318
475
  if (mode === "quick") {
319
476
  output({
320
477
  decision: "allow",
321
- context: `[GATE] AUTO_APPROVE: quick mode skips gate evaluation for '${targetPhase}'.`
478
+ context: `[GATE] ALLOW_QUICK_MODE: quick mode skips gate evaluation for '${targetPhase}'; no approval observed by this hook.`
322
479
  });
323
480
  log2(HOOK, `ALLOW quick mode phase=${targetPhase}`);
324
481
  return;
@@ -326,7 +483,7 @@ async function mainWithDeps(deps) {
326
483
  if (mode === "interactive") {
327
484
  output({
328
485
  decision: "allow",
329
- context: `[GATE] AUTO_APPROVE: interactive mode skips artifact gate evaluation for '${targetPhase}'.`
486
+ context: `[GATE] ALLOW_INTERACTIVE_MODE: interactive mode skips artifact gate evaluation for '${targetPhase}'; no approval observed by this hook.`
330
487
  });
331
488
  log2(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
332
489
  return;
@@ -361,45 +518,8 @@ async function mainWithDeps(deps) {
361
518
  token
362
519
  );
363
520
  if (gateMode === "REQ") {
364
- const verdict = await getAdversarialVerdict2(workflowId, token);
365
- if (!verdict) {
366
- log2(
367
- HOOK,
368
- `PME Step 5.5: no adversarial artifact found \u2014 fail-open ALLOW wf=${workflowId}`
369
- );
370
- output({
371
- decision: "allow",
372
- context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`
373
- });
374
- return;
375
- }
376
- if (verdict.iteration >= PME_HARD_CAP) {
377
- log2(
378
- HOOK,
379
- `PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(verdict.iteration)}) \u2014 ALLOW wf=${workflowId}`
380
- );
381
- output({
382
- decision: "allow",
383
- context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(verdict.iteration)}/${String(PME_HARD_CAP)}), escalating.`
384
- });
385
- return;
386
- }
387
- if (verdict.verdict === "PASS") {
388
- log2(HOOK, `PME Step 5.5: adversarial verdict=PASS \u2014 ALLOW wf=${workflowId}`);
389
- output({
390
- decision: "allow",
391
- context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`
392
- });
393
- return;
394
- }
395
- log2(
396
- HOOK,
397
- `PME Step 5.5: DENY verdict=${verdict.verdict} iteration=${String(verdict.iteration)} wf=${workflowId}`
398
- );
399
- output({
400
- decision: "deny",
401
- reason: `Gate Guardian: adversarial_review required \u2014 verdict=${verdict.verdict}, iteration=${String(verdict.iteration)}. Resolve adversarial review before advancing.`
402
- });
521
+ const result = await getAdversarialVerdict2(workflowId, token);
522
+ decideAdversarialResult(result, workflowId, targetPhase, output, log2);
403
523
  return;
404
524
  }
405
525
  log2(
@@ -467,10 +587,10 @@ async function mainWithDeps(deps) {
467
587
  JSON.stringify({ strategy: decision.strategy, targetPhase, riskLevel, reason: decision.reason })
468
588
  );
469
589
  switch (decision.strategy) {
470
- case "AUTO_APPROVE":
590
+ case "ALLOW_LOW_RISK":
471
591
  output({
472
592
  decision: "allow",
473
- context: `[GATE] AUTO_APPROVE: ${decision.reason}`
593
+ context: `[GATE] ALLOW_LOW_RISK: ${decision.reason}`
474
594
  });
475
595
  break;
476
596
  case "ASK_HUMAN":
@@ -13,7 +13,7 @@
13
13
  * 6. classifyRisk(targetPhase) → riskLevel
14
14
  * 7. buildGateContext() + deriveGateStrategy() → GateDecision
15
15
  * 8. switch strategy:
16
- * AUTO_APPROVE → allow + telemetry context
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)
@@ -30,6 +30,7 @@ import {
30
30
  getGateCellMode,
31
31
  getAdversarialVerdict,
32
32
  outputPreToolUse,
33
+ type AdversarialVerdictResult,
33
34
  type PendingAction,
34
35
  } from './lib/api-client.js';
35
36
  import { classifyRisk, deriveGateStrategy, type GateContext } from './lib/gate-logic.js';
@@ -40,7 +41,9 @@ const HOOK = 'GATE-GUARDIAN';
40
41
  * Maximum adversarial review iterations before the hard cap triggers.
41
42
  * When iteration >= PME_HARD_CAP the gate ALLOWS (escalation path) and logs
42
43
  * a BLOCKED-escalation warning. This prevents infinite review loops.
43
- * RFC-031 §4.4 documents this mechanic.
44
+ * RFC-031 §4.4 documents this mechanic. The cap only applies to a well-formed
45
+ * `iteration` (kind 'ok'); an `iteration` the helper rejected has no count to
46
+ * compare, so a non-PASS verdict with it is denied instead (TD-375).
44
47
  */
45
48
  const PME_HARD_CAP = 3;
46
49
 
@@ -65,11 +68,8 @@ export interface HookDeps {
65
68
  gateKey: string,
66
69
  token: string
67
70
  ) => 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>;
71
+ /** PME Step 5.5: retrieve the classified adversarial review verdict (TD-374; never rejects) */
72
+ getAdversarialVerdict: (workflowId: string, token: string) => Promise<AdversarialVerdictResult>;
73
73
  output: (opts: { decision: 'allow' | 'deny'; reason?: string; context?: string }) => void;
74
74
  log: (prefix: string, msg: string) => void;
75
75
  }
@@ -86,6 +86,148 @@ const defaultDeps: HookDeps = {
86
86
  log,
87
87
  };
88
88
 
89
+ // ─────────────────────────────────────────────────────────────
90
+ // PME Step 5.5: one decision per AdversarialVerdictResult kind
91
+ // ─────────────────────────────────────────────────────────────
92
+
93
+ type Output = HookDeps['output'];
94
+ type Log = HookDeps['log'];
95
+
96
+ const RESULT_KINDS: ReadonlySet<string> = new Set([
97
+ 'ok',
98
+ 'absent',
99
+ 'malformed',
100
+ 'iteration_rejected',
101
+ 'error',
102
+ ]);
103
+
104
+ /** null / undefined / unknown kind (a stale dep or a bug) is an unexpected error, never a throw. */
105
+ function normalizeVerdictResult(raw: unknown): AdversarialVerdictResult {
106
+ const known =
107
+ typeof raw === 'object' && raw !== null && 'kind' in raw && RESULT_KINDS.has(String(raw.kind));
108
+ return known ? (raw as AdversarialVerdictResult) : { kind: 'error', reason: 'unexpected' };
109
+ }
110
+
111
+ /** Applies the gate for an `ok` verdict: hard cap -> PASS -> DENY (unchanged texts). */
112
+ function decideOkVerdict(
113
+ result: Extract<AdversarialVerdictResult, { kind: 'ok' }>,
114
+ workflowId: string,
115
+ targetPhase: string,
116
+ output: Output,
117
+ log: Log
118
+ ): void {
119
+ if (result.iteration >= PME_HARD_CAP) {
120
+ // Hard cap reached — ALLOW with escalation log (prevents infinite loops)
121
+ log(
122
+ HOOK,
123
+ `PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(result.iteration)}) — ALLOW wf=${workflowId}`
124
+ );
125
+ output({
126
+ decision: 'allow',
127
+ context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(result.iteration)}/${String(PME_HARD_CAP)}), escalating.`,
128
+ });
129
+ return;
130
+ }
131
+ if (result.verdict === 'PASS') {
132
+ log(HOOK, `PME Step 5.5: adversarial verdict=PASS — ALLOW wf=${workflowId}`);
133
+ output({
134
+ decision: 'allow',
135
+ context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`,
136
+ });
137
+ return;
138
+ }
139
+ // Verdict is NEEDS_REVISION (or unknown) and below hard cap — DENY
140
+ log(
141
+ HOOK,
142
+ `PME Step 5.5: DENY verdict=${result.verdict} iteration=${String(result.iteration)} wf=${workflowId}`
143
+ );
144
+ output({
145
+ decision: 'deny',
146
+ reason: `Gate Guardian: adversarial_review required — verdict=${result.verdict}, iteration=${String(result.iteration)}. Resolve adversarial review before advancing.`,
147
+ });
148
+ }
149
+
150
+ /**
151
+ * `iteration` was a string that is not a canonical decimal integer (TD-375).
152
+ * PASS needs no hard cap, so it proceeds; any other verdict is denied with the
153
+ * way out spelled in the reason (the writer must fix the artifact).
154
+ */
155
+ function decideRejectedIteration(
156
+ result: Extract<AdversarialVerdictResult, { kind: 'iteration_rejected' }>,
157
+ workflowId: string,
158
+ targetPhase: string,
159
+ output: Output,
160
+ log: Log
161
+ ): void {
162
+ const { verdict, detail } = result;
163
+ if (verdict === 'PASS') {
164
+ log(
165
+ HOOK,
166
+ `PME Step 5.5: adversarial verdict=PASS with iteration_invalid (${detail}) — ALLOW wf=${workflowId}`
167
+ );
168
+ output({
169
+ decision: 'allow',
170
+ context: `[GATE] PME adversarial gate: verdict=PASS, iteration_invalid (${detail}), hard cap not evaluated, proceeding to '${targetPhase}'.`,
171
+ });
172
+ return;
173
+ }
174
+ log(HOOK, `PME Step 5.5: DENY verdict=${verdict} iteration_invalid (${detail}) wf=${workflowId}`);
175
+ output({
176
+ decision: 'deny',
177
+ 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.`,
178
+ });
179
+ }
180
+
181
+ /** Maps one classified adversarial result to ALLOW/DENY. Fail-open except a rejected iteration. */
182
+ function decideAdversarialResult(
183
+ raw: unknown,
184
+ workflowId: string,
185
+ targetPhase: string,
186
+ output: Output,
187
+ log: Log
188
+ ): void {
189
+ const result = normalizeVerdictResult(raw);
190
+ switch (result.kind) {
191
+ case 'ok':
192
+ decideOkVerdict(result, workflowId, targetPhase, output, log);
193
+ return;
194
+ case 'iteration_rejected':
195
+ decideRejectedIteration(result, workflowId, targetPhase, output, log);
196
+ return;
197
+ case 'absent':
198
+ log(HOOK, `PME Step 5.5: no adversarial artifact found — fail-open ALLOW wf=${workflowId}`);
199
+ output({
200
+ decision: 'allow',
201
+ context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`,
202
+ });
203
+ return;
204
+ case 'malformed': {
205
+ const detail = result.detail === undefined ? '' : ` detail=${result.detail}`;
206
+ log(
207
+ HOOK,
208
+ `PME Step 5.5: adversarial artifact present but malformed (${result.reason}${detail}) — fail-open ALLOW wf=${workflowId}`
209
+ );
210
+ output({
211
+ decision: 'allow',
212
+ context: `[GATE] PME adversarial gate: artifact malformed (${result.reason}), fail-open ALLOW for '${targetPhase}'.`,
213
+ });
214
+ return;
215
+ }
216
+ case 'error': {
217
+ const status = result.status === undefined ? '' : ` status=${String(result.status)}`;
218
+ log(
219
+ HOOK,
220
+ `PME Step 5.5: adversarial artifact unreadable (${result.reason}${status}) — fail-open ALLOW wf=${workflowId}`
221
+ );
222
+ output({
223
+ decision: 'allow',
224
+ context: `[GATE] PME adversarial gate: artifact could not be read (${result.reason}), fail-open ALLOW for '${targetPhase}'.`,
225
+ });
226
+ return;
227
+ }
228
+ }
229
+ }
230
+
89
231
  // ─────────────────────────────────────────────────────────────
90
232
  // Core logic (exported for testing)
91
233
  // ─────────────────────────────────────────────────────────────
@@ -119,7 +261,7 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
119
261
  if (mode === 'quick') {
120
262
  output({
121
263
  decision: 'allow',
122
- context: `[GATE] AUTO_APPROVE: quick mode skips gate evaluation for '${targetPhase}'.`,
264
+ context: `[GATE] ALLOW_QUICK_MODE: quick mode skips gate evaluation for '${targetPhase}'; no approval observed by this hook.`,
123
265
  });
124
266
  log(HOOK, `ALLOW quick mode phase=${targetPhase}`);
125
267
  return;
@@ -128,7 +270,7 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
128
270
  if (mode === 'interactive') {
129
271
  output({
130
272
  decision: 'allow',
131
- context: `[GATE] AUTO_APPROVE: interactive mode skips artifact gate evaluation for '${targetPhase}'.`,
273
+ context: `[GATE] ALLOW_INTERACTIVE_MODE: interactive mode skips artifact gate evaluation for '${targetPhase}'; no approval observed by this hook.`,
132
274
  });
133
275
  log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
134
276
  return;
@@ -164,7 +306,10 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
164
306
  // a) workflow type is 'project_management_enterprise'
165
307
  // b) the variant's 'adversarial' gate cell is 'REQ'
166
308
  // c) adversarial review artifact exists and verdict is not PASS
167
- // All infra failures are fail-open (ALLOW) so hooks never block on downtime.
309
+ // Absent / malformed / unreadable artifacts are fail-open (ALLOW) so hooks never
310
+ // block on downtime; each is logged under its own label (TD-374). The one
311
+ // exception is a non-PASS verdict whose `iteration` is not a canonical decimal
312
+ // integer: the hard cap cannot be evaluated, so it is denied (TD-375).
168
313
  {
169
314
  const typeVariant = await getWorkflowTypeAndVariant(workflowId, token);
170
315
  if (typeVariant?.type === 'project_management_enterprise') {
@@ -175,49 +320,8 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
175
320
  token
176
321
  );
177
322
  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
- });
323
+ const result = await getAdversarialVerdict(workflowId, token);
324
+ decideAdversarialResult(result, workflowId, targetPhase, output, log);
221
325
  return;
222
326
  }
223
327
  // gateMode is OPT, SKIP, or null — fall through to Step 5
@@ -236,7 +340,7 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
236
340
 
237
341
  // ── Step 5: IMPLEMENT short-circuit (RF-008) ─────────────────────────────
238
342
  // If targetPhase contains "implement", check pending-action BEFORE classifyRisk.
239
- // This prevents AUTO_APPROVE from being applied to implement phases.
343
+ // This prevents the low-risk allow path from being applied to implement phases.
240
344
  //
241
345
  // Phase 4: Only 'implement' phases have the approval path (via pending-action).
242
346
  // Other HIGH-risk phases (deploy, migrate, delete) will always ASK_HUMAN → DENY.
@@ -303,10 +407,10 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
303
407
  );
304
408
 
305
409
  switch (decision.strategy) {
306
- case 'AUTO_APPROVE':
410
+ case 'ALLOW_LOW_RISK':
307
411
  output({
308
412
  decision: 'allow',
309
- context: `[GATE] AUTO_APPROVE: ${decision.reason}`,
413
+ context: `[GATE] ALLOW_LOW_RISK: ${decision.reason}`,
310
414
  });
311
415
  break;
312
416
 
@@ -1088,35 +1088,138 @@ export async function getGateCellMode(
1088
1088
  }
1089
1089
  }
1090
1090
 
1091
+ /**
1092
+ * Outcome of `GET /api/v1/artifact-content/:wf/adversarial_review`, classified
1093
+ * so the caller can tell WHY there is no usable verdict (TD-374) and decide.
1094
+ *
1095
+ * - `ok`: verdict string + `iteration` parsed from a canonical decimal string.
1096
+ * - `absent`: HTTP 404 (no artifact yet).
1097
+ * - `malformed`: HTTP 200 whose body carries no decidable verdict. `iteration_invalid`
1098
+ * here means `iteration` is absent or not a string.
1099
+ * - `iteration_rejected`: verdict is a valid string but `iteration` is a string that
1100
+ * fails the grammar (TD-375). Distinct kind on purpose: the caller must decide, a
1101
+ * `malformed` branch handled as fail-open would reopen the hole in silence.
1102
+ * - `error`: no usable response (HTTP != 200/404, timeout, network, unexpected).
1103
+ */
1104
+ export type AdversarialVerdictResult =
1105
+ | { kind: 'ok'; verdict: string; iteration: number }
1106
+ | { kind: 'absent' }
1107
+ | {
1108
+ kind: 'malformed';
1109
+ reason:
1110
+ | 'body_not_json'
1111
+ | 'body_not_object'
1112
+ | 'custom_metadata_missing'
1113
+ | 'verdict_invalid'
1114
+ | 'iteration_invalid';
1115
+ detail?: string;
1116
+ }
1117
+ | { kind: 'iteration_rejected'; verdict: string; detail: string }
1118
+ | { kind: 'error'; reason: 'http' | 'timeout' | 'network' | 'unexpected'; status?: number };
1119
+
1120
+ type StatusFetch =
1121
+ | { readonly kind: 'response'; readonly resp: Response }
1122
+ | { readonly kind: 'error'; readonly reason: 'timeout' | 'network' };
1123
+
1124
+ /**
1125
+ * Same request as `apiGet` (URL, headers, 5s timeout) but keeps the HTTP
1126
+ * status and the failure cause that `apiGet` collapses into `null`.
1127
+ * `apiGet` itself is intentionally left untouched.
1128
+ */
1129
+ async function apiGetWithStatus(path: string, token: string): Promise<StatusFetch> {
1130
+ try {
1131
+ const resp = await fetch(`${API_URL}${path}`, {
1132
+ headers: {
1133
+ Authorization: `Bearer ${token}`,
1134
+ 'X-Project-ID': PROJECT_ID,
1135
+ },
1136
+ signal: AbortSignal.timeout(5000),
1137
+ });
1138
+ return { kind: 'response', resp };
1139
+ } catch (err) {
1140
+ const name: unknown = typeof err === 'object' && err !== null && 'name' in err ? err.name : '';
1141
+ const timedOut = name === 'TimeoutError' || name === 'AbortError';
1142
+ return { kind: 'error', reason: timedOut ? 'timeout' : 'network' };
1143
+ }
1144
+ }
1145
+
1146
+ /** `JSON.stringify(raw)` capped at 40 chars; undefined when raw has no JSON form. */
1147
+ function iterationDetail(raw: unknown): string | undefined {
1148
+ // JSON.stringify yields undefined for undefined/functions/symbols despite its typing.
1149
+ const json = JSON.stringify(raw) as string | undefined;
1150
+ return json === undefined ? undefined : json.slice(0, 40);
1151
+ }
1152
+
1153
+ /** Canonical decimal grammar for `iteration` (TD-375): ASCII digits, safe integer. */
1154
+ function parseIteration(raw: string): number | null {
1155
+ if (!/^[0-9]+$/.test(raw)) return null;
1156
+ const value = Number(raw);
1157
+ return Number.isSafeInteger(value) ? value : null;
1158
+ }
1159
+
1160
+ function classifyIteration(verdict: string, rawIteration: unknown): AdversarialVerdictResult {
1161
+ if (typeof rawIteration !== 'string') {
1162
+ const detail = iterationDetail(rawIteration);
1163
+ return detail === undefined
1164
+ ? { kind: 'malformed', reason: 'iteration_invalid' }
1165
+ : { kind: 'malformed', reason: 'iteration_invalid', detail };
1166
+ }
1167
+ const iteration = parseIteration(rawIteration);
1168
+ if (iteration === null) {
1169
+ return { kind: 'iteration_rejected', verdict, detail: iterationDetail(rawIteration) ?? '' };
1170
+ }
1171
+ return { kind: 'ok', verdict, iteration };
1172
+ }
1173
+
1174
+ function classifyAdversarialBody(body: unknown): AdversarialVerdictResult {
1175
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) {
1176
+ return { kind: 'malformed', reason: 'body_not_object' };
1177
+ }
1178
+ const metadata: unknown = 'metadata' in body ? body.metadata : undefined;
1179
+ const customMetadata: unknown =
1180
+ typeof metadata === 'object' && metadata !== null && 'customMetadata' in metadata
1181
+ ? metadata.customMetadata
1182
+ : undefined;
1183
+ if (typeof customMetadata !== 'object' || customMetadata === null) {
1184
+ return { kind: 'malformed', reason: 'custom_metadata_missing' };
1185
+ }
1186
+ const verdict: unknown = 'verdict' in customMetadata ? customMetadata.verdict : undefined;
1187
+ if (typeof verdict !== 'string') return { kind: 'malformed', reason: 'verdict_invalid' };
1188
+ const rawIteration: unknown =
1189
+ 'iteration' in customMetadata ? customMetadata.iteration : undefined;
1190
+ return classifyIteration(verdict, rawIteration);
1191
+ }
1192
+
1091
1193
  /**
1092
1194
  * Retrieves the adversarial review verdict and iteration for a workflow.
1093
- * Calls GET /api/v1/artifact-content/:workflowId/adversarial_review.
1094
- * The API serves the fields in metadata.customMetadata (strings — S3
1095
- * x-amz-meta-* headers only carry strings), so `iteration` is coerced:
1096
- * accepted only as a non-blank string whose Number() is finite (TD-331).
1097
- * Returns null when artifact is absent (404), on 5xx, timeout, or missing /
1098
- * malformed customMetadata fields (fail-open — gate is not enforced when
1099
- * artifact is absent). The pre-TD-331 flat metadata.{verdict,iteration}
1100
- * shape is NOT read (RF-003).
1195
+ * Calls GET /api/v1/artifact-content/:workflowId/adversarial_review and
1196
+ * classifies the outcome (see AdversarialVerdictResult); it never rejects and
1197
+ * never returns null. The API serves the fields in metadata.customMetadata
1198
+ * (strings: S3 x-amz-meta-* headers only carry strings), so `iteration` is
1199
+ * accepted only as a canonical decimal string (/^[0-9]+$/, safe integer). The
1200
+ * pre-TD-331 flat metadata.{verdict,iteration} shape is NOT read (RF-003).
1101
1201
  */
1102
1202
  export async function getAdversarialVerdict(
1103
1203
  workflowId: string,
1104
1204
  token: string
1105
- ): Promise<{ verdict: string; iteration: number } | null> {
1205
+ ): Promise<AdversarialVerdictResult> {
1106
1206
  try {
1107
- const resp = await apiGet<{
1108
- content?: string;
1109
- metadata?: { customMetadata?: Record<string, unknown> };
1110
- }>(`/api/v1/artifact-content/${workflowId}/adversarial_review`, token);
1111
- if (!resp) return null;
1112
- const { verdict, iteration: rawIteration } = resp.metadata?.customMetadata ?? {};
1113
- if (typeof verdict !== 'string' || typeof rawIteration !== 'string') return null;
1114
- if (rawIteration.trim() === '') return null;
1115
- const iteration = Number(rawIteration);
1116
- if (!Number.isFinite(iteration)) return null;
1117
- return { verdict, iteration };
1207
+ const got = await apiGetWithStatus(
1208
+ `/api/v1/artifact-content/${workflowId}/adversarial_review`,
1209
+ token
1210
+ );
1211
+ if (got.kind === 'error') return got;
1212
+ if (got.resp.status === 404) return { kind: 'absent' };
1213
+ if (!got.resp.ok) return { kind: 'error', reason: 'http', status: got.resp.status };
1214
+ let body: unknown;
1215
+ try {
1216
+ body = await got.resp.json();
1217
+ } catch {
1218
+ return { kind: 'malformed', reason: 'body_not_json' };
1219
+ }
1220
+ return classifyAdversarialBody(body);
1118
1221
  } catch {
1119
- return null;
1222
+ return { kind: 'error', reason: 'unexpected' };
1120
1223
  }
1121
1224
  }
1122
1225
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  export type RiskLevel = 'HIGH' | 'LOW';
8
- export type GateStrategy = 'AUTO_APPROVE' | 'ASK_HUMAN' | 'BLOCK_AND_REQUIRE';
8
+ export type GateStrategy = 'ALLOW_LOW_RISK' | 'ASK_HUMAN' | 'BLOCK_AND_REQUIRE';
9
9
 
10
10
  export interface GateContext {
11
11
  workflowId: string;
@@ -38,7 +38,7 @@ export function classifyRisk(targetPhase: string): RiskLevel {
38
38
  * Derive the gate strategy based on context.
39
39
  * Rules (evaluated in order):
40
40
  * 1. HIGH risk → ASK_HUMAN
41
- * 2. default → AUTO_APPROVE
41
+ * 2. default → ALLOW_LOW_RISK (allowed without an approval check; the hook observes no approval)
42
42
  *
43
43
  * BLOCK_AND_REQUIRE exists as a type but has no trigger rule yet.
44
44
  */
@@ -50,9 +50,10 @@ export function deriveGateStrategy(ctx: GateContext): GateDecision {
50
50
  };
51
51
  }
52
52
 
53
- // Default: AUTO_APPROVE
53
+ // Default: low-risk phases pass without any approval check (TD-373: the label
54
+ // must not claim an approval this hook never observed).
54
55
  return {
55
- strategy: 'AUTO_APPROVE',
56
- reason: `Phase '${ctx.targetPhase}' is low-risk — auto-approved.`,
56
+ strategy: 'ALLOW_LOW_RISK',
57
+ reason: `Phase '${ctx.targetPhase}' is low-risk — allowed without an approval check; no approval observed by this hook.`,
57
58
  };
58
59
  }
package/manifest.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schema": "orchestrator-definitions/v1",
3
3
  "namespace": "orchestrator",
4
- "version": "3.43.1",
5
- "generatedAt": "2026-10-01T17:04:34.229Z",
4
+ "version": "3.44.0",
5
+ "generatedAt": "2026-10-01T23:00:12.417Z",
6
6
  "definitions": {
7
7
  "agents": [
8
8
  {
@@ -613,7 +613,7 @@
613
613
  "slug": "gate-guardian",
614
614
  "file": "hooks/gate-guardian.mjs",
615
615
  "version": "1.0.0",
616
- "checksum": "sha256:2fe79ad9a8621c9708530e627aed839cf145637af47f76a9ddd45e20da5ff70a",
616
+ "checksum": "sha256:2186f91f00fe6ec0cd6b49336fb45d9041b5c1fa91d7874a06b58defe1ca04ff",
617
617
  "supersedes": null,
618
618
  "metadata": {}
619
619
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-claude/definitions",
3
- "version": "3.43.1",
3
+ "version": "3.44.0",
4
4
  "description": "Built-in definitions (agents, skills, hooks, workflows, KB seeds) for the Orchestrator ecosystem",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",