peaks-loop 4.0.47 → 4.0.49

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 (126) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/agents/karpathy-reviewer.md +11 -10
  5. package/dist/cli/cli-helpers.d.ts +34 -0
  6. package/dist/cli/cli-helpers.js +57 -0
  7. package/dist/cli/commands/code-job-shape-commands.js +8 -0
  8. package/dist/cli/commands/code-runtime-commands.js +48 -8
  9. package/dist/cli/commands/compact-command.js +110 -0
  10. package/dist/cli/commands/config-commands.js +15 -9
  11. package/dist/cli/commands/dashboard-long-run.js +6 -0
  12. package/dist/cli/commands/dispatch-commands.js +11 -1
  13. package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
  14. package/dist/cli/commands/feedback-commands.d.ts +11 -7
  15. package/dist/cli/commands/feedback-commands.js +49 -17
  16. package/dist/cli/commands/final-review-commands.js +12 -0
  17. package/dist/cli/commands/hooks-commands.js +4 -4
  18. package/dist/cli/commands/job-commands.js +8 -0
  19. package/dist/cli/commands/loop-eval-commands.js +31 -0
  20. package/dist/cli/commands/perf-audit-commands.js +2 -0
  21. package/dist/cli/commands/playwright-commands.js +12 -0
  22. package/dist/cli/commands/prd-commands.js +1 -1
  23. package/dist/cli/commands/qa-commands.js +22 -0
  24. package/dist/cli/commands/request-commands.js +8 -0
  25. package/dist/cli/commands/scan-commands.js +1 -1
  26. package/dist/cli/commands/security-audit-commands.js +2 -0
  27. package/dist/cli/commands/slice-integrate-commands.js +22 -0
  28. package/dist/cli/commands/statusline-commands.js +44 -4
  29. package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
  30. package/dist/cli/commands/sub-agent/detached.js +47 -22
  31. package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
  32. package/dist/cli/commands/verdict-aggregate-command.js +95 -13
  33. package/dist/cli/commands/workflow-commands.js +1 -1
  34. package/dist/cli/index.js +5 -45
  35. package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
  36. package/dist/services/artifacts/artifact-prerequisites.js +140 -65
  37. package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
  38. package/dist/services/artifacts/request-artifact-service.js +77 -46
  39. package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
  40. package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
  41. package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
  42. package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
  43. package/dist/services/audit-independent/perf-audit-service.js +27 -5
  44. package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
  45. package/dist/services/audit-independent/security-audit-service.js +28 -6
  46. package/dist/services/code/auto-compact-lifecycle.d.ts +194 -0
  47. package/dist/services/code/auto-compact-lifecycle.js +229 -11
  48. package/dist/services/code/auto-compact-orchestrator.js +118 -7
  49. package/dist/services/code/compact-event-settle.d.ts +134 -0
  50. package/dist/services/code/compact-event-settle.js +240 -0
  51. package/dist/services/compact-history/compact-history-service.d.ts +14 -0
  52. package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
  53. package/dist/services/config/config-restore.d.ts +12 -1
  54. package/dist/services/config/config-restore.js +35 -4
  55. package/dist/services/config/config-rollback.js +6 -1
  56. package/dist/services/context/auto-compact-types.d.ts +20 -2
  57. package/dist/services/context/harness-context-witness.d.ts +310 -0
  58. package/dist/services/context/harness-context-witness.js +606 -0
  59. package/dist/services/evidence/evidence-generator.js +86 -49
  60. package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
  61. package/dist/services/feedback/feedback-promotion-service.js +341 -20
  62. package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
  63. package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
  64. package/dist/services/final-review/final-review-service.d.ts +9 -0
  65. package/dist/services/final-review/final-review-service.js +36 -12
  66. package/dist/services/ide/ide-registry.d.ts +19 -0
  67. package/dist/services/ide/ide-registry.js +21 -0
  68. package/dist/services/job/job-progress-store.js +18 -3
  69. package/dist/services/job/job-state-store.js +7 -0
  70. package/dist/services/observability/jsonl-store.d.ts +19 -0
  71. package/dist/services/observability/jsonl-store.js +27 -2
  72. package/dist/services/observability/observability-service.d.ts +10 -3
  73. package/dist/services/observability/observability-service.js +16 -3
  74. package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
  75. package/dist/services/prd/handoff-auto-regen.js +31 -27
  76. package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
  77. package/dist/services/prd/handoff-frontmatter.js +75 -0
  78. package/dist/services/prd/handoff-service.d.ts +41 -2
  79. package/dist/services/prd/handoff-service.js +124 -8
  80. package/dist/services/prd/handoff-types.d.ts +3 -2
  81. package/dist/services/prd/handoff-types.js +3 -2
  82. package/dist/services/qa/qa-business-review-state.js +23 -0
  83. package/dist/services/sc/sc-service.d.ts +8 -0
  84. package/dist/services/sc/sc-service.js +8 -1
  85. package/dist/services/scan/karpathy-service.js +2 -2
  86. package/dist/services/session/getSessionDir.d.ts +33 -0
  87. package/dist/services/session/getSessionDir.js +60 -0
  88. package/dist/services/session/session-checkpoint-service.js +8 -0
  89. package/dist/services/skill/resume-detector.js +29 -11
  90. package/dist/services/skills/hooks-codegate-superpowers.d.ts +6 -0
  91. package/dist/services/skills/hooks-codegate-superpowers.js +61 -2
  92. package/dist/services/skills/hooks-settings-service.js +14 -4
  93. package/dist/services/skills/session-start-hook-constants.d.ts +45 -0
  94. package/dist/services/skills/session-start-hook-constants.js +45 -0
  95. package/dist/services/skills/skill-statusline-service.d.ts +14 -0
  96. package/dist/services/slice/slice-check-service.js +29 -11
  97. package/dist/services/slice/slice-review-state.js +23 -0
  98. package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
  99. package/dist/services/workflow/pipeline-verify-gate-support.js +221 -103
  100. package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
  101. package/dist/services/workflow/pipeline-verify-service.js +47 -33
  102. package/dist/services/workflow/pipeline-verify-types.d.ts +15 -6
  103. package/dist/services/workspace/claude-settings-template.d.ts +56 -8
  104. package/dist/services/workspace/claude-settings-template.js +98 -20
  105. package/dist/services/workspace/workspace-claude-settings-materializer.js +78 -7
  106. package/dist/shared/runtime-root.d.ts +73 -0
  107. package/dist/shared/runtime-root.js +77 -0
  108. package/package.json +6 -6
  109. package/skills/bee/peaks-prd/SKILL.md +7 -5
  110. package/skills/bee/peaks-qa/SKILL.md +5 -5
  111. package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
  112. package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
  113. package/skills/bee/peaks-rd/SKILL.md +8 -6
  114. package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
  115. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
  116. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
  117. package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
  118. package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
  119. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
  120. package/skills/peaks-code/SKILL.md +1 -1
  121. package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
  122. package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
  123. package/skills/peaks-code/references/resume-detection.md +13 -7
  124. package/skills/peaks-code/references/runbook.md +3 -2
  125. package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
  126. package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
@@ -15,14 +15,27 @@ export declare function dispatch(f: DispatchFlags): Promise<{
15
15
  ok: boolean;
16
16
  command: string;
17
17
  data: {
18
+ expectedCompletionSeconds: number;
19
+ orchestratorVisibleHint: string;
18
20
  mode: string;
19
21
  vendor: "claude" | "codex" | "copilot" | undefined;
20
22
  pid: number;
21
23
  dispatchRecordPath: string;
22
24
  maxConcurrent: number;
23
25
  noThrottle: boolean;
24
- orchestratorVisibleHint: string;
26
+ } | {
25
27
  expectedCompletionSeconds: number;
28
+ spawnError: {
29
+ code: string | undefined;
30
+ message: string;
31
+ };
32
+ orchestratorVisibleHint: string;
33
+ mode: string;
34
+ vendor: "claude" | "codex" | "copilot" | undefined;
35
+ pid: number;
36
+ dispatchRecordPath: string;
37
+ maxConcurrent: number;
38
+ noThrottle: boolean;
26
39
  };
27
40
  warnings: string[];
28
41
  nextActions: string[];
@@ -25,15 +25,26 @@ export async function dispatch(f) {
25
25
  if (f.noThrottle) {
26
26
  warnings.push('user-overrode: --no-throttle (peak runtime may exceed performance ceiling)');
27
27
  }
28
- // rid-001 fix (F1 follow-up): the vendor CLI's ChildProcess is owned by
29
- // peaks-loop-internal-runtime/dispatch.dispatchDetached, which now exposes
30
- // it via DispatchResult.child. When the vendor binary is not on PATH the
31
- // spawn fires an async 'error' event after dispatchDetached returns; we
32
- // attach a per-child handler that swallows ENOENT (expected when the user
33
- // hasn't installed claude/codex/copilot) and logs anything else. This is
34
- // the canonical pattern (matches codegraph-process-runner.ts) — no more
35
- // process-level uncaughtException swallow.
28
+ // The vendor CLI's ChildProcess is owned by
29
+ // peaks-loop-internal-runtime/dispatch.dispatchDetached, and this handler
30
+ // deliberately attaches NO 'error' listener to it.
31
+ //
32
+ // The handler that used to live here — an 'error' listener attached to
33
+ // `r.child` after the await — could never fire. `ProcessSupervisor.spawn`
34
+ // attaches its own 'error' + 'spawn' listeners in the SAME synchronous turn
35
+ // the child is created, and `dispatchDetached` awaits the resulting `settled` promise
36
+ // before returning. A missing binary is emitted on the nextTick queue,
37
+ // which drains BEFORE the awaiting caller resumes — so by the time this
38
+ // function holds `r`, the event has already been consumed and turned into
39
+ // the typed `spawnError` value below. Attaching a listener here is not
40
+ // merely late; it is unreachable by construction.
36
41
  const sid = process.env.PEAKS_SESSION_ID ?? 'local';
42
+ // `spawnError` is optional in this annotation, not because the value is
43
+ // uncertain but because the type this resolves against is: the workspace
44
+ // package's `types` entry points at `dist/index.d.ts`, and a `dist` built
45
+ // before the field was added does not declare it. Reading it defensively
46
+ // (`?? null`, below) is correct in both worlds; requiring it here would make
47
+ // the handler fail to compile against a stale build.
37
48
  let r;
38
49
  r = await dispatchDetached({
39
50
  sid,
@@ -46,15 +57,17 @@ export async function dispatch(f) {
46
57
  runtimeDir: `.peaks/_runtime/${sid}/detached`,
47
58
  subAgentsDir: `.peaks/_sub_agents/${sid}`,
48
59
  });
49
- r.child?.on('error', (err) => {
50
- if (err && err.code === 'ENOENT')
51
- return;
52
- // Non-ENOENT child errors are surfaced to stderr; the dispatch envelope
53
- // has already been written and the detached child is fire-and-forget.
54
- console.error('[peaks sub-agent dispatch] detached child error:', err);
55
- });
60
+ // The launch outcome is the envelope's `ok`, not a footnote. A vendor CLI
61
+ // that is not installed is an expected environment, and the dispatch record
62
+ // already says `status: 'failed'`; the envelope used to say `ok: true` with
63
+ // `pid: -1` and a "⏳ Spawning …" hint, so the two surfaces disagreed and the
64
+ // orchestrator read a launch that never happened as a running one.
65
+ //
66
+ // `?? null` covers a DispatchResult-shaped value produced by a test double
67
+ // that omits the field; a real `dispatchDetached` always sets it.
68
+ const spawnError = r.spawnError ?? null;
56
69
  return {
57
- ok: true,
70
+ ok: spawnError === null,
58
71
  command: 'sub-agent.dispatch.detached',
59
72
  data: {
60
73
  mode: 'detached',
@@ -63,14 +76,26 @@ export async function dispatch(f) {
63
76
  dispatchRecordPath: r.dispatchRecordPath,
64
77
  maxConcurrent,
65
78
  noThrottle: f.noThrottle ?? false,
66
- orchestratorVisibleHint: `⏳ Spawning detached sub-agent via ${f.vendor ?? 'claude'}: rid=${f.requestId} (ETA ~60s)`,
79
+ ...(spawnError === null
80
+ ? {
81
+ orchestratorVisibleHint: `⏳ Spawning detached sub-agent via ${f.vendor ?? 'claude'}: rid=${f.requestId} (ETA ~60s)`
82
+ }
83
+ : {
84
+ spawnError: { code: spawnError.code, message: spawnError.message },
85
+ orchestratorVisibleHint: `❌ Could not launch detached sub-agent via ${f.vendor ?? 'claude'}: rid=${f.requestId} (${spawnError.code})`
86
+ }),
67
87
  expectedCompletionSeconds: 60,
68
88
  },
69
89
  warnings,
70
- nextActions: [
71
- 'Sub-agent runs as detached OS process. Status at .peaks/_runtime/<sid>/detached/<rid>/status.json',
72
- 'Use `peaks sub-agent list --mode detached` to monitor.',
73
- 'Run `peaks sub-agent cleanup --orphan` to reap orphan processes (RL-15: user-only decision).',
74
- ],
90
+ nextActions: spawnError === null
91
+ ? [
92
+ 'Sub-agent runs as detached OS process. Status at .peaks/_runtime/<sid>/detached/<rid>/status.json',
93
+ 'Use `peaks sub-agent list --mode detached` to monitor.',
94
+ 'Run `peaks sub-agent cleanup --orphan` to reap orphan processes (RL-15: user-only decision).'
95
+ ]
96
+ : [
97
+ `The ${f.vendor ?? 'claude'} CLI is not runnable from this shell (${spawnError.code}); nothing was launched and no sub-agent is running.`,
98
+ 'Either install the vendor CLI on PATH, or re-dispatch without --mode detached to use the in-process path.'
99
+ ]
75
100
  };
76
101
  }
@@ -2,9 +2,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
4
4
  import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
5
+ import { isUnsafePathInput } from '../../shared/path-safety.js';
5
6
  import { printResult } from '../cli-helpers.js';
6
7
  const REGISTRATIONS_FILE = 'service-registrations.json';
7
8
  function registrationsPath(input) {
9
+ // Both id axes reach this join. `dispatchId` is `--dispatch-id` (falling back
10
+ // to PEAKS_DISPATCH_ID), and `sessionId` is the session binding; neither was
11
+ // checked, and `register` writes through this path. Guarding the whole join
12
+ // rather than only the flag keeps the function's contract single-stated.
13
+ if (isUnsafePathInput(input.dispatchId)) {
14
+ throw new Error(`Invalid dispatch id: ${input.dispatchId} (must be a single path segment)`);
15
+ }
16
+ if (isUnsafePathInput(input.sessionId)) {
17
+ throw new Error(`Invalid session id: ${input.sessionId} (must be a single path segment)`);
18
+ }
8
19
  return join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'dispatch', input.dispatchId, REGISTRATIONS_FILE);
9
20
  }
10
21
  function readAll(file) {
@@ -4,11 +4,51 @@ import { aggregateVerdict } from '../../services/verdict/verdict-aggregator.js';
4
4
  import { parseKarpathyEnvelope, parseMutEnvelope, parseQaEnvelope, parseSecurityEnvelope, parsePerfEnvelope, envelopesToAggregatorInput } from '../../services/verdict/envelopes.js';
5
5
  import { addJsonOption, printResult } from '../cli-helpers.js';
6
6
  import { fail, ok } from 'peaks-loop-shared/result';
7
- const SECURITY_REL = 'audit/security.md';
8
- const PERF_REL = 'audit/perf.md';
9
- const KARPATHY_REL = 'rd/karpathy-review.md';
7
+ import { isUnsafePathInput } from '../../shared/path-safety.js';
8
+ import { DEFAULT_REQUEST_TYPE } from '../../services/artifacts/artifact-prerequisites.js';
9
+ import { REQUEST_ID_PATTERN } from '../../services/artifacts/request-artifact-service.js';
10
+ import { contractEvidencePaths } from '../../services/workflow/pipeline-verify-gate-support.js';
11
+ // Slice `2026-09-14-audit-artifact-rid-scoping`: the audit/review evidence
12
+ // filenames carry the rid. The ridless names are the pre-rid locations and
13
+ // stay readable during the back-compat window. `mut/` is NOT rid-scoped —
14
+ // see `MUT_REPORT` in `artifact-prerequisites.ts`: no producer in the repo
15
+ // can write a rid-scoped mut report, so probing a templated name here would
16
+ // look for a file that no writer can create.
17
+ //
18
+ // The candidate lists are read FROM the contract — the same
19
+ // `contractEvidencePaths()` the verify pipeline resolves with — rather than
20
+ // hand-written here. Hand-written lists held two of the contract's THREE
21
+ // tiers, so a session whose security/perf evidence sits at the oldest tier
22
+ // (`rd/security-review.md`, `rd/perf-baseline.md` — the live shape of three
23
+ // sessions on disk) reported `block` while `peaks request transition`
24
+ // accepted the same tree, and the hint named a legacy location it had never
25
+ // probed. One table, one answer.
10
26
  const MUT_REL = 'mut/mut-report.json';
11
27
  const QA_REL = 'qa/test-reports';
28
+ /**
29
+ * The declared tiers for one artifact name, primary first. Probed by the name
30
+ * this command used before the rid-scoping (the table matches it as either
31
+ * the primary or a legacy tier), so the lookup survives the primary moving.
32
+ * `verdict aggregate` is request-type-agnostic and every fanout-trigger type
33
+ * declares these artifacts identically, so the default type's list is the
34
+ * full one; a name the table does not carry falls back to itself.
35
+ */
36
+ function contractRels(probeName) {
37
+ return contractEvidencePaths('rd', 'qa-handoff', DEFAULT_REQUEST_TYPE, probeName) ?? [probeName];
38
+ }
39
+ const SECURITY_RELS = contractRels('audit/security.md');
40
+ const PERF_RELS = contractRels('audit/perf.md');
41
+ const KARPATHY_RELS = contractRels('rd/karpathy-review.md');
42
+ /**
43
+ * Sources whose absence must not read as `pass`. `mut` is excluded on
44
+ * purpose: `MUT_REPORT` carries `backCompat: true`, so the contract itself
45
+ * treats a missing mut report as a warning rather than a gate failure.
46
+ */
47
+ const REQUIRED_SOURCES = [
48
+ { key: 'security', source: 'security-audit', rels: SECURITY_RELS },
49
+ { key: 'perf', source: 'perf-audit', rels: PERF_RELS },
50
+ { key: 'karpathy', source: 'karpathy-reviewer', rels: KARPATHY_RELS }
51
+ ];
12
52
  export function registerVerdictAggregateCommands(program, io) {
13
53
  const verdict = program.command('verdict').description('Aggregate the 5 envelope sources feeding peaks-code verdict logic');
14
54
  addJsonOption(verdict
@@ -25,6 +65,27 @@ export function registerVerdictAggregateCommands(program, io) {
25
65
  process.exitCode = 1;
26
66
  return;
27
67
  }
68
+ // The rid SELECTS THE FILE, so it has to be one path segment. This is the
69
+ // repo's own request-id guard — the one `request-artifact-service.ts`
70
+ // applies to every request artifact and the reason `peaks workflow
71
+ // verify-pipeline` refuses a hostile rid — and it was not applied to the
72
+ // joins below. Measured before it was: `--from-rid
73
+ // '../../../../../../ONLY-HERE'` read `<project>/ONLY-HERE.md`, a file
74
+ // outside `.peaks/`, as this slice's security AND perf evidence.
75
+ if (!REQUEST_ID_PATTERN.test(rid)) {
76
+ printResult(io, fail('verdict.aggregate', 'RID_INVALID', `Invalid request id: ${rid} (expected letters, digits, dots, underscores, or dashes)`, {}, ['Pass the rid of the slice whose evidence you want aggregated, e.g. 2026-09-14-some-slug']), options.json);
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ // `--sid` names the session DIRECTORY and is the same axis. Reject
81
+ // anything that is not one safe path segment rather than pinning a
82
+ // format: the default here is the deliberately non-format value
83
+ // `default`.
84
+ if (isUnsafePathInput(sid)) {
85
+ printResult(io, fail('verdict.aggregate', 'SID_INVALID', `Invalid session id: ${sid} (must be a single path segment)`, {}, ['Pass the session id, e.g. 2026-09-14-session-abc123']), options.json);
86
+ process.exitCode = 1;
87
+ return;
88
+ }
28
89
  try {
29
90
  const sources = {
30
91
  // v2.13.3 AC-1: use the canonical markdown-aware parser from
@@ -34,9 +95,9 @@ export function registerVerdictAggregateCommands(program, io) {
34
95
  // returned `verdict: warn, violations: []` — the aggregator
35
96
  // then produced `reasons: []` and `verdict: 'pass'` even when
36
97
  // the audit had flagged HIGH violations.
37
- security: readAudit(projectRoot, sid, SECURITY_REL, parseSecurityEnvelope),
38
- perf: readAudit(projectRoot, sid, PERF_REL, parsePerfEnvelope),
39
- karpathy: readAudit(projectRoot, sid, KARPATHY_REL, (m) => parseKarpathyEnvelope(m)),
98
+ security: readAudit(projectRoot, sid, rid, SECURITY_RELS, parseSecurityEnvelope),
99
+ perf: readAudit(projectRoot, sid, rid, PERF_RELS, parsePerfEnvelope),
100
+ karpathy: readAudit(projectRoot, sid, rid, KARPATHY_RELS, (m) => parseKarpathyEnvelope(m)),
40
101
  mut: await readMut(projectRoot, sid),
41
102
  qa: readQa(projectRoot, sid, rid)
42
103
  };
@@ -55,7 +116,26 @@ export function registerVerdictAggregateCommands(program, io) {
55
116
  mut: sources.mut !== null ? 'present' : 'missing',
56
117
  qa: sources.qa !== null ? 'present' : 'missing'
57
118
  };
58
- printResult(io, ok('verdict.aggregate', { verdict: result.verdict, reasons: result.reasons, sources: sourceFlags }, [], []), options.json);
119
+ // Fail closed. `aggregateVerdict()` starts its top-level verdict at
120
+ // `'pass'`, so a source that could not be read simply never enters the
121
+ // input and the run reports green. Before this slice the bare paths
122
+ // were the ones the writers wrote; after it the writers moved and this
123
+ // reader did not — so a missing security or perf audit became a `pass`
124
+ // for the loop evaluator (`services/loop/evaluator-dispatcher.ts`). A
125
+ // guard that cannot read its input must not report success.
126
+ const missingRequired = REQUIRED_SOURCES.filter((entry) => sources[entry.key] === null);
127
+ const missingReasons = missingRequired.map((entry) => ({
128
+ source: entry.source,
129
+ sources: [entry.source],
130
+ signal: 'block',
131
+ kind: 'missing-evidence',
132
+ hint: `${entry.rels.map((rel) => rel.replace('<rid>', rid)).join(' | ')} could not be read under .peaks/_runtime/${sid}/`
133
+ }));
134
+ printResult(io, ok('verdict.aggregate', {
135
+ verdict: missingReasons.length > 0 ? 'block' : result.verdict,
136
+ reasons: [...missingReasons, ...result.reasons],
137
+ sources: sourceFlags
138
+ }, [], []), options.json);
59
139
  }
60
140
  catch (error) {
61
141
  const message = error instanceof Error ? error.message : String(error);
@@ -64,12 +144,14 @@ export function registerVerdictAggregateCommands(program, io) {
64
144
  }
65
145
  });
66
146
  }
67
- function readAudit(projectRoot, sid, rel, parse) {
68
- const path = join(projectRoot, '.peaks', '_runtime', sid, rel);
69
- if (!existsSync(path))
70
- return null;
71
- const md = readFileSync(path, 'utf8');
72
- return parse(md);
147
+ function readAudit(projectRoot, sid, rid, rels, parse) {
148
+ // Canonical rid-scoped location first, then the pre-rid locations.
149
+ for (const rel of rels) {
150
+ const path = join(projectRoot, '.peaks', '_runtime', sid, rel.replace('<rid>', rid));
151
+ if (existsSync(path))
152
+ return parse(readFileSync(path, 'utf8'));
153
+ }
154
+ return null;
73
155
  }
74
156
  async function readMut(projectRoot, sid) {
75
157
  const path = join(projectRoot, '.peaks', '_runtime', sid, MUT_REL);
@@ -420,7 +420,7 @@ export function registerWorkflowCommands(program, io) {
420
420
  // semantics, role-based auth).
421
421
  addJsonOption(workflow
422
422
  .command('skip')
423
- .description('Skip specific gates for a request (RD/QA). Use --dry-run to preview without writing. Allowed gate names: QA / RD (phase shortcuts) or specific gate names (rd-request-exists, tech-doc, code-review, security-review, qa-request-exists, test-cases, test-report, security-findings, performance-findings). Three rules apply: (1) only docs/config/chore slices can skip; (2) skip is one-time per rid; (3) script callers must also pass --i-have-reviewed.')
423
+ .description('Skip specific gates for a request (RD/QA). Use --dry-run to preview without writing. Allowed gate names: QA / RD (phase shortcuts) or specific gate names (rd-request-exists, prd-handoff, bug-analysis, code-review, security-review, perf-baseline, qa-request-exists, test-cases, test-report). Three rules apply: (1) only docs/config/chore slices can skip; (2) skip is one-time per rid; (3) script callers must also pass --i-have-reviewed.')
424
424
  .requiredOption('--rid <rid>', 'request identifier')
425
425
  .requiredOption('--project <path>', 'project root path')
426
426
  .requiredOption('--gates <list>', 'comma-separated gate names (e.g. "QA" or "QA,slice-check" or "code-review,security-review")')
package/dist/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CommanderError } from 'commander';
2
2
  import { createProgram } from './program.js';
3
3
  import { getErrorMessage } from 'peaks-loop-shared/result';
4
- import { printErrorEnvelope } from './cli-helpers.js';
4
+ import { printErrorEnvelope, printMissingRequiredOptionEnvelope, resolveInvokedCommandPath } from './cli-helpers.js';
5
5
  const defaultIo = {
6
6
  stdout: (text) => process.stdout.write(`${text}\n`),
7
7
  stderr: (text) => process.stderr.write(`${text}\n`)
@@ -30,32 +30,6 @@ const argv = process.argv.slice(2);
30
30
  const hasHelp = argv.some((arg) => arg === '--help' || arg === '-h');
31
31
  const firstPositional = argv.find((arg) => !arg.startsWith('-'));
32
32
  const program = createProgram();
33
- /**
34
- * The subcommand path the caller actually invoked, in the same tokens they
35
- * typed (`release canary`). Derived from the registered command tree by
36
- * consuming leading non-`-` argv tokens, so it never disagrees with what
37
- * Commander dispatched on.
38
- *
39
- * Commander's `CommanderError` carries a code and a message but no command
40
- * reference — `missingMandatoryOptionValue` is raised by the leaf command and
41
- * throws out of `parseAsync` with nothing naming it. Without this walk the
42
- * missing-option envelope could only say `command: "cli"`, which is the field
43
- * the caller needs to act on.
44
- */
45
- function resolveInvokedCommandPath() {
46
- let cmd = program;
47
- const parts = [];
48
- for (const token of argv) {
49
- if (token.startsWith('-'))
50
- break;
51
- const next = cmd.commands.find((c) => c.name() === token || c.aliases().includes(token));
52
- if (next === undefined)
53
- break;
54
- parts.push(next.name());
55
- cmd = next;
56
- }
57
- return parts.length > 0 ? parts.join(' ') : program.name();
58
- }
59
33
  if (hasHelp && firstPositional !== undefined) {
60
34
  const registered = new Set(program.commands.map((c) => c.name()));
61
35
  if (!registered.has(firstPositional)) {
@@ -86,24 +60,10 @@ program.parseAsync(process.argv).catch((error) => {
86
60
  return;
87
61
  }
88
62
  if (error.code === 'commander.missingMandatoryOptionValue') {
89
- // A `.requiredOption()` the caller did not supply. Commander raises this
90
- // as a plain `Error`-shaped `CommanderError`, so the `.catch()` below used
91
- // to file it under `UNHANDLED_ERROR` — "you left out an argument" reported
92
- // as a crash, with empty `nextActions` and no way to see WHICH option or
93
- // what values it takes. The flags string Commander formats carries both
94
- // (`--percent <10|50>`), so it is worth extracting rather than
95
- // paraphrasing: it is the option's own declaration.
96
- const message = getErrorMessage(error);
97
- const option = /required option '([^']+)'/.exec(message)?.[1];
98
- const invoked = resolveInvokedCommandPath();
99
- printErrorEnvelope(defaultIo, invoked, 'MISSING_REQUIRED_OPTION', option === undefined
100
- ? message
101
- : `Missing required option '${option}' for \`peaks ${invoked}\`.`, { option: option ?? null }, [
102
- option === undefined
103
- ? `Run \`peaks ${invoked} --help\` to see the options this command requires.`
104
- : `Supply ${option} — it is required, so the command has no default for it.`,
105
- `Run \`peaks ${invoked} --help\` for the option's accepted values and its siblings.`
106
- ]);
63
+ // See `printMissingRequiredOptionEnvelope` — the same builder backs the
64
+ // in-process test runner, so the envelope a test asserts is the envelope
65
+ // a user gets.
66
+ printMissingRequiredOptionEnvelope(defaultIo, resolveInvokedCommandPath(program, argv), getErrorMessage(error));
107
67
  return;
108
68
  }
109
69
  if (error.code === 'commander.missingArgument' || error.code === 'commander.unknownCommand' || error.code === 'commander.unknownOption') {
@@ -29,18 +29,36 @@ export type ArtifactPrerequisite = {
29
29
  */
30
30
  headingMustContain?: ReadonlyArray<string>;
31
31
  /**
32
- * Slice v2.12.0 Group B Tier 5: optional legacy path that satisfies
33
- * the same gate. When `relativePath` does not resolve on disk, the
34
- * resolver tries this fallback path before reporting the prereq as
35
- * missing. Use this for 1-minor-release back-compat windows where
36
- * an old artifact location is still accepted alongside the new one.
37
- * v2.13.0 should remove all `legacyRelativePath` entries.
32
+ * The immediately previous location of this artifact. When
33
+ * `relativePath` does not resolve on disk, the resolver tries this
34
+ * path before reporting the prereq as missing.
35
+ *
36
+ * This field is a published surface, not just a private hint:
37
+ * `verify-pipeline`'s `contractEvidencePaths()`
38
+ * (`src/services/workflow/pipeline-verify-gate-support.ts`) reads it
39
+ * to build its probe list, so it must keep meaning "the one path a
40
+ * previous release wrote". Do not repurpose it as a general list —
41
+ * add older tiers to `legacyRelativePaths` instead.
38
42
  *
39
43
  * Body checks (`mustContain` / `mustContainAny` /
40
44
  * `headingMustContain`) apply to whichever path resolved — the
41
- * gate does not distinguish which path served the file.
45
+ * gate does not distinguish which path served the file. That is
46
+ * deliberate: a legacy hit must keep the gate open, or existing
47
+ * sessions would fail the transition on upgrade.
42
48
  */
43
49
  legacyRelativePath?: string;
50
+ /**
51
+ * Further, older locations, tried **in declared order** after
52
+ * `legacyRelativePath`. Slice `2026-09-14-audit-artifact-rid-scoping`
53
+ * added this because the two audit artifacts have a two-release
54
+ * history behind their rid-scoped path: the pre-rid-scoping
55
+ * `audit/security.md` (v2.12.0) and, behind it, the v2.11.x
56
+ * `rd/security-review.md`. Three sessions on disk
57
+ * (`2026-09-06-session-a87ca4`, `2026-09-10-session-528a63`,
58
+ * `2026-09-12-session-e37ef0`) hold their security evidence only at
59
+ * the last of those, so the oldest tier still has to resolve.
60
+ */
61
+ legacyRelativePaths?: ReadonlyArray<string>;
44
62
  };
45
63
  export type PrerequisiteCheckResult = {
46
64
  ok: boolean;
@@ -75,4 +93,17 @@ export type CheckPrerequisitesOptions = {
75
93
  requestType?: RequestType;
76
94
  };
77
95
  export declare function getPrerequisitesFor(role: RequestArtifactRole, newState: RequestArtifactState, requestType?: RequestType): ReadonlyArray<ArtifactPrerequisite>;
96
+ /**
97
+ * The contract's BODY checks for one prerequisite, applied to `body`. Returns
98
+ * one human-readable message per failed check, empty when the body satisfies
99
+ * the contract.
100
+ *
101
+ * Exported because this contract has a SECOND enforcer: `peaks workflow
102
+ * verify-pipeline` probes the same table for the paths it checks
103
+ * (`pipeline-verify-gate-support.ts`), and a resolver that probed only
104
+ * `existsSync` passed files this function rejects — a guard laxer than the
105
+ * contract it claims to read. Both callers now share this one implementation,
106
+ * so the checker cannot drift from the table again.
107
+ */
108
+ export declare function prerequisiteBodyViolations(prerequisite: ArtifactPrerequisite, body: string): string[];
78
109
  export declare function checkPrerequisites(options: CheckPrerequisitesOptions): Promise<PrerequisiteCheckResult>;