@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.
Files changed (52) hide show
  1. package/agents/adversarial-reviewer.md +35 -29
  2. package/agents/agentic-architect.md +1 -1
  3. package/agents/api-extractor.md +1 -1
  4. package/agents/architecture-mediator.md +1 -1
  5. package/agents/azure-devops-engineer.md +1 -1
  6. package/agents/backend-architect.md +1 -1
  7. package/agents/business-rule-miner.md +1 -1
  8. package/agents/cicd-platform-engineer.md +1 -1
  9. package/agents/ciclo-engineer.md +1 -1
  10. package/agents/code-archaeologist.md +1 -1
  11. package/agents/contract-architect.md +1 -1
  12. package/agents/datamodel-architect.md +1 -1
  13. package/agents/frontend-architect.md +1 -1
  14. package/agents/frontend-engineer.md +1 -1
  15. package/agents/harness-architect.md +1 -1
  16. package/agents/harness-engineer.md +1 -1
  17. package/agents/implementer.md +1 -1
  18. package/agents/infra-architect.md +1 -1
  19. package/agents/legacy-discoverer.md +1 -1
  20. package/agents/legacy-synthesizer.md +1 -1
  21. package/agents/methodology-researcher.md +2 -1
  22. package/agents/operational-artifact-builder.md +1 -1
  23. package/agents/planner.md +1 -1
  24. package/agents/presentation-engineer.md +1 -1
  25. package/agents/python-data-engineer.md +1 -1
  26. package/agents/researcher.md +1 -1
  27. package/agents/reviewer.md +1 -1
  28. package/agents/schema-extractor.md +1 -1
  29. package/agents/specifier.md +1 -1
  30. package/agents/task-generator.md +1 -1
  31. package/hooks/agent-git-guard.mjs +0 -2
  32. package/hooks/dangling-guard.mjs +18 -3
  33. package/hooks/gate-guardian.mjs +8244 -20
  34. package/hooks/gate-guardian.ts +154 -24
  35. package/hooks/lib/api-client.ts +30 -7
  36. package/hooks/lib/artifact-gate.ts +518 -0
  37. package/hooks/lib/artifact-producers.ts +106 -0
  38. package/hooks/lib/workflow-artifact-client.ts +351 -0
  39. package/hooks/mailbox-listener.mjs +0 -2
  40. package/hooks/playwright-output-guard.mjs +0 -2
  41. package/hooks/post-implement.mjs +18 -3
  42. package/hooks/pre-agent-dispatch.mjs +18 -3
  43. package/hooks/security-gate-honest.mjs +0 -2
  44. package/hooks/session-start.mjs +18 -3
  45. package/hooks/subagent-start.mjs +18 -3
  46. package/hooks/subagent-stop.mjs +18 -3
  47. package/hooks/user-prompt.mjs +18 -3
  48. package/hooks/verification-before-completion.mjs +0 -2
  49. package/hooks/workflow-guard.mjs +504 -29
  50. package/hooks/workflow-guard.ts +370 -27
  51. package/manifest.json +46 -46
  52. package/package.json +1 -1
@@ -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
- /** Maps one classified adversarial result to ALLOW/DENY. Fail-open except a rejected iteration. */
182
- function decideAdversarialResult(
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
- // ── Step 2: Mode check — quick/interactive bypass gate entirely ──────────
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
- output({
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
- // 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).
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
- const result = await getAdversarialVerdict(workflowId, token);
324
- decideAdversarialResult(result, workflowId, targetPhase, output, log);
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
@@ -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 AUTH_EMAIL =
127
- process.env.ORCHESTRATOR_ADMIN_EMAIL ||
128
- process.env.ORCHESTRATOR_AUTH_EMAIL ||
129
- 'system@orchestrator.local';
130
- const AUTH_PASSWORD =
131
- process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || 'system123';
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: AUTH_EMAIL, password: AUTH_PASSWORD }),
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;