@orchestrator-claude/definitions 3.44.0 → 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 +8244 -20
- package/hooks/gate-guardian.ts +154 -24
- package/hooks/lib/api-client.ts +30 -7
- package/hooks/lib/artifact-gate.ts +518 -0
- package/hooks/lib/artifact-producers.ts +106 -0
- 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
|
@@ -17,6 +17,13 @@
|
|
|
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 {
|
|
@@ -34,6 +41,17 @@ import {
|
|
|
34
41
|
type PendingAction,
|
|
35
42
|
} from './lib/api-client.js';
|
|
36
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';
|
|
37
55
|
|
|
38
56
|
const HOOK = 'GATE-GUARDIAN';
|
|
39
57
|
|
|
@@ -72,9 +90,11 @@ export interface HookDeps {
|
|
|
72
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,6 +104,7 @@ const defaultDeps: HookDeps = {
|
|
|
84
104
|
getAdversarialVerdict,
|
|
85
105
|
output: outputPreToolUse,
|
|
86
106
|
log,
|
|
107
|
+
artifacts: defaultArtifactGateDeps(),
|
|
87
108
|
};
|
|
88
109
|
|
|
89
110
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -92,6 +113,7 @@ const defaultDeps: HookDeps = {
|
|
|
92
113
|
|
|
93
114
|
type Output = HookDeps['output'];
|
|
94
115
|
type Log = HookDeps['log'];
|
|
116
|
+
type GateOutput = Parameters<Output>[0];
|
|
95
117
|
|
|
96
118
|
const RESULT_KINDS: ReadonlySet<string> = new Set([
|
|
97
119
|
'ok',
|
|
@@ -178,13 +200,21 @@ function decideRejectedIteration(
|
|
|
178
200
|
});
|
|
179
201
|
}
|
|
180
202
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
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(
|
|
183
211
|
raw: unknown,
|
|
184
212
|
workflowId: string,
|
|
185
213
|
targetPhase: string,
|
|
186
214
|
output: Output,
|
|
187
|
-
log: Log
|
|
215
|
+
log: Log,
|
|
216
|
+
absentPolicy: 'allow' | 'deny' = 'allow',
|
|
217
|
+
expectedFile?: string
|
|
188
218
|
): void {
|
|
189
219
|
const result = normalizeVerdictResult(raw);
|
|
190
220
|
switch (result.kind) {
|
|
@@ -195,6 +225,20 @@ function decideAdversarialResult(
|
|
|
195
225
|
decideRejectedIteration(result, workflowId, targetPhase, output, log);
|
|
196
226
|
return;
|
|
197
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
|
+
}
|
|
198
242
|
log(HOOK, `PME Step 5.5: no adversarial artifact found — fail-open ALLOW wf=${workflowId}`);
|
|
199
243
|
output({
|
|
200
244
|
decision: 'allow',
|
|
@@ -233,17 +277,7 @@ function decideAdversarialResult(
|
|
|
233
277
|
// ─────────────────────────────────────────────────────────────
|
|
234
278
|
|
|
235
279
|
export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
236
|
-
const {
|
|
237
|
-
readStdin,
|
|
238
|
-
getWorkflowMode,
|
|
239
|
-
getToken,
|
|
240
|
-
getNextAction,
|
|
241
|
-
getWorkflowTypeAndVariant,
|
|
242
|
-
getGateCellMode,
|
|
243
|
-
getAdversarialVerdict,
|
|
244
|
-
output,
|
|
245
|
-
log,
|
|
246
|
-
} = deps;
|
|
280
|
+
const { readStdin, getWorkflowMode, getToken, output, log } = deps;
|
|
247
281
|
|
|
248
282
|
const stdin = await readStdin();
|
|
249
283
|
log(HOOK, 'PreToolUse advancePhase triggered');
|
|
@@ -253,7 +287,39 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
253
287
|
|
|
254
288
|
log(HOOK, `workflow=${workflowId} targetPhase=${targetPhase}`);
|
|
255
289
|
|
|
256
|
-
// ──
|
|
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 ──
|
|
257
323
|
if (workflowId) {
|
|
258
324
|
const mode = await getWorkflowMode(workflowId);
|
|
259
325
|
log(HOOK, `workflow=${workflowId} mode=${mode ?? 'legacy'}`);
|
|
@@ -268,11 +334,11 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
268
334
|
}
|
|
269
335
|
|
|
270
336
|
if (mode === 'interactive') {
|
|
271
|
-
|
|
337
|
+
const allowed = await allowAfterLedger({
|
|
272
338
|
decision: 'allow',
|
|
273
339
|
context: `[GATE] ALLOW_INTERACTIVE_MODE: interactive mode skips artifact gate evaluation for '${targetPhase}'; no approval observed by this hook.`,
|
|
274
340
|
});
|
|
275
|
-
log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
|
|
341
|
+
if (allowed) log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
|
|
276
342
|
return;
|
|
277
343
|
}
|
|
278
344
|
}
|
|
@@ -301,15 +367,49 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
301
367
|
return;
|
|
302
368
|
}
|
|
303
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
|
+
|
|
304
401
|
// ── Step 5.5: PME adversarial gate enforcement (RFC-031 §4.4) ───────────
|
|
305
402
|
// Only enforced when:
|
|
306
403
|
// a) workflow type is 'project_management_enterprise'
|
|
307
404
|
// b) the variant's 'adversarial' gate cell is 'REQ'
|
|
308
405
|
// c) adversarial review artifact exists and verdict is not PASS
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
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.
|
|
313
413
|
{
|
|
314
414
|
const typeVariant = await getWorkflowTypeAndVariant(workflowId, token);
|
|
315
415
|
if (typeVariant?.type === 'project_management_enterprise') {
|
|
@@ -320,8 +420,38 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
320
420
|
token
|
|
321
421
|
);
|
|
322
422
|
if (gateMode === 'REQ') {
|
|
323
|
-
|
|
324
|
-
|
|
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 });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const fromFile = fileAdversarialVerdict(observed);
|
|
430
|
+
if (fromFile.kind === 'invalid') {
|
|
431
|
+
log(HOOK, `PME Step 5.5: DENY adversarial_review file invalid wf=${workflowId}`);
|
|
432
|
+
output({
|
|
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).`,
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const result =
|
|
439
|
+
fromFile.kind === 'verdict'
|
|
440
|
+
? fromFile.result
|
|
441
|
+
: await getAdversarialVerdict(workflowId, token);
|
|
442
|
+
log(
|
|
443
|
+
HOOK,
|
|
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`
|
|
454
|
+
);
|
|
325
455
|
return;
|
|
326
456
|
}
|
|
327
457
|
// gateMode is OPT, SKIP, or null — fall through to Step 5
|
package/hooks/lib/api-client.ts
CHANGED
|
@@ -123,12 +123,27 @@ const API_URL = process.env.ORCHESTRATOR_API_URL || 'http://localhost:3001';
|
|
|
123
123
|
// `system` tier account (level 4, ABOVE admin) rather than admin — the
|
|
124
124
|
// env-override chain is preserved verbatim so an explicit
|
|
125
125
|
// ORCHESTRATOR_ADMIN_EMAIL/ORCHESTRATOR_AUTH_EMAIL still wins.
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
const DEFAULT_AUTH_EMAIL = 'system@orchestrator.local';
|
|
127
|
+
// TD-267 A1 (S572): there is NO default password. The credential is read
|
|
128
|
+
// lazily, at login time, and a password that is unset, empty or
|
|
129
|
+
// whitespace-only counts as absent — in which case login() makes no network
|
|
130
|
+
// call at all and getToken() returns null (each hook keeps the posture it
|
|
131
|
+
// already has for "no token"). The client never invents a credential.
|
|
132
|
+
const MISSING_CREDENTIAL_MESSAGE =
|
|
133
|
+
'no credential configured (ORCHESTRATOR_ADMIN_PASSWORD / ORCHESTRATOR_AUTH_PASSWORD unset or empty) - login not attempted';
|
|
134
|
+
let missingCredentialLogged = false;
|
|
135
|
+
|
|
136
|
+
function resolveAuthCredentials(): { email: string; password: string } | null {
|
|
137
|
+
const password =
|
|
138
|
+
(process.env.ORCHESTRATOR_ADMIN_PASSWORD ?? '').trim() ||
|
|
139
|
+
(process.env.ORCHESTRATOR_AUTH_PASSWORD ?? '').trim();
|
|
140
|
+
if (!password) return null;
|
|
141
|
+
const email =
|
|
142
|
+
process.env.ORCHESTRATOR_ADMIN_EMAIL ||
|
|
143
|
+
process.env.ORCHESTRATOR_AUTH_EMAIL ||
|
|
144
|
+
DEFAULT_AUTH_EMAIL;
|
|
145
|
+
return { email, password };
|
|
146
|
+
}
|
|
132
147
|
// Mutable (not const): RFC-040 Operator Multi-Project, Slice 5 (C5) lazily
|
|
133
148
|
// resolves this once per process when the env var is absent — see
|
|
134
149
|
// ensureProjectIdResolved() below.
|
|
@@ -338,6 +353,14 @@ async function login(): Promise<string | null> {
|
|
|
338
353
|
// the server ignores X-Project-ID on this route entirely, so login can
|
|
339
354
|
// always proceed and PROJECT_ID is resolved afterwards, once a Bearer
|
|
340
355
|
// token is available (see ensureProjectIdResolved, called from getToken()).
|
|
356
|
+
const credentials = resolveAuthCredentials();
|
|
357
|
+
if (credentials === null) {
|
|
358
|
+
if (!missingCredentialLogged) {
|
|
359
|
+
missingCredentialLogged = true;
|
|
360
|
+
log('API-CLIENT', MISSING_CREDENTIAL_MESSAGE);
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
341
364
|
try {
|
|
342
365
|
const resp = await fetch(`${API_URL}/api/v1/auth/login`, {
|
|
343
366
|
method: 'POST',
|
|
@@ -345,7 +368,7 @@ async function login(): Promise<string | null> {
|
|
|
345
368
|
'Content-Type': 'application/json',
|
|
346
369
|
'X-Project-ID': PROJECT_ID,
|
|
347
370
|
},
|
|
348
|
-
body: JSON.stringify({ email:
|
|
371
|
+
body: JSON.stringify({ email: credentials.email, password: credentials.password }),
|
|
349
372
|
signal: AbortSignal.timeout(5000),
|
|
350
373
|
});
|
|
351
374
|
if (!resp.ok) return null;
|