peaks-loop 4.0.0-beta.14 → 4.0.0-beta.16

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 (204) hide show
  1. package/CHANGELOG.md +230 -148
  2. package/README-en.md +1 -1
  3. package/README.md +32 -1
  4. package/bin/peaks.js +0 -0
  5. package/dist/cli/cli-helpers.d.ts +1 -1
  6. package/dist/cli/cli-helpers.js +1 -1
  7. package/dist/cli/commands/adapter-commands-s2a.js +1 -1
  8. package/dist/cli/commands/asset-commands.js +33 -16
  9. package/dist/cli/commands/audit-commands.js +1 -1
  10. package/dist/cli/commands/bee-commands.js +1 -1
  11. package/dist/cli/commands/capability-commands.js +1 -1
  12. package/dist/cli/commands/classify-classify-commands.js +1 -1
  13. package/dist/cli/commands/code-commands.js +1 -1
  14. package/dist/cli/commands/code-review-commands.js +1 -1
  15. package/dist/cli/commands/codegraph-commands.js +1 -1
  16. package/dist/cli/commands/compact-command.js +1 -1
  17. package/dist/cli/commands/complexity-commands.js +1 -1
  18. package/dist/cli/commands/config-commands.js +1 -1
  19. package/dist/cli/commands/context-commands.js +1 -1
  20. package/dist/cli/commands/contract-commands.js +1 -1
  21. package/dist/cli/commands/core/artifacts-command.js +1 -1
  22. package/dist/cli/commands/core/binding-commands.js +1 -1
  23. package/dist/cli/commands/core/doctor-command.js +63 -3
  24. package/dist/cli/commands/core/memory-command.js +1 -1
  25. package/dist/cli/commands/core/profile-command.js +1 -1
  26. package/dist/cli/commands/core/proxy-command.js +1 -1
  27. package/dist/cli/commands/core/session-command.js +1 -1
  28. package/dist/cli/commands/core/skill-command.js +2 -2
  29. package/dist/cli/commands/core/standards-command.js +1 -1
  30. package/dist/cli/commands/dispatch-commands.js +2 -2
  31. package/dist/cli/commands/dispatch-from-dag.js +1 -1
  32. package/dist/cli/commands/doc-commands.js +1 -1
  33. package/dist/cli/commands/ecc-commands.js +2 -2
  34. package/dist/cli/commands/evolution-commands.js +1 -1
  35. package/dist/cli/commands/feedback-commands.js +1 -1
  36. package/dist/cli/commands/final-review-commands.js +1 -1
  37. package/dist/cli/commands/fixture-commands.js +1 -1
  38. package/dist/cli/commands/fork-commands.js +1 -1
  39. package/dist/cli/commands/gate-commands.js +1 -1
  40. package/dist/cli/commands/heartbeat-commands.js +1 -1
  41. package/dist/cli/commands/hook-handle.js +1 -1
  42. package/dist/cli/commands/hooks-commands.js +1 -1
  43. package/dist/cli/commands/ide-commands.js +1 -1
  44. package/dist/cli/commands/impact-commands.js +1 -1
  45. package/dist/cli/commands/job-commands.js +1 -1
  46. package/dist/cli/commands/legacy-commands.js +1 -1
  47. package/dist/cli/commands/log-commands.js +1 -1
  48. package/dist/cli/commands/loop-commands.js +1 -1
  49. package/dist/cli/commands/loop-eval-commands.js +1 -1
  50. package/dist/cli/commands/memory-commands.js +1 -1
  51. package/dist/cli/commands/migrate-1-4-1-command.js +1 -1
  52. package/dist/cli/commands/migrate-v2-10-to-v2-11-command.js +1 -1
  53. package/dist/cli/commands/mut-commands.d.ts +1 -1
  54. package/dist/cli/commands/mut-commands.js +1 -5
  55. package/dist/cli/commands/observability-commands.js +1 -1
  56. package/dist/cli/commands/openspec-commands.js +2 -2
  57. package/dist/cli/commands/perf-audit-commands.js +2 -2
  58. package/dist/cli/commands/perf-commands.js +1 -1
  59. package/dist/cli/commands/polyrepo-commands.js +1 -1
  60. package/dist/cli/commands/prd-blocks-commands.js +1 -1
  61. package/dist/cli/commands/prd-commands.js +1 -1
  62. package/dist/cli/commands/project-commands.js +1 -1
  63. package/dist/cli/commands/qa-business-review-commands.js +1 -1
  64. package/dist/cli/commands/qa-commands.d.ts +1 -1
  65. package/dist/cli/commands/qa-commands.js +2 -2
  66. package/dist/cli/commands/release-commands.js +1 -1
  67. package/dist/cli/commands/request-commands.js +1 -1
  68. package/dist/cli/commands/retrospective-commands.js +1 -1
  69. package/dist/cli/commands/reviewer-commands.js +1 -1
  70. package/dist/cli/commands/role-commands.js +1 -1
  71. package/dist/cli/commands/runtime-commands.js +1 -2
  72. package/dist/cli/commands/sc-commands.js +1 -1
  73. package/dist/cli/commands/scan-commands.js +1 -1
  74. package/dist/cli/commands/security-audit-commands.js +2 -2
  75. package/dist/cli/commands/share-commands.d.ts +19 -0
  76. package/dist/cli/commands/share-commands.js +121 -2
  77. package/dist/cli/commands/skill-conformance-commands.js +1 -1
  78. package/dist/cli/commands/skill-loop-engineering-readiness-commands.js +1 -1
  79. package/dist/cli/commands/skill-search-commands.js +1 -1
  80. package/dist/cli/commands/slice-commands.js +1 -1
  81. package/dist/cli/commands/slice-integrate-commands.js +1 -1
  82. package/dist/cli/commands/slice-review-commands.js +1 -1
  83. package/dist/cli/commands/smoke-commands.js +1 -1
  84. package/dist/cli/commands/sop-commands.js +1 -1
  85. package/dist/cli/commands/statusline-commands.js +1 -1
  86. package/dist/cli/commands/sub-agent-commands.js +2 -1
  87. package/dist/cli/commands/understand-commands.js +1 -1
  88. package/dist/cli/commands/upgrade-commands.js +1 -1
  89. package/dist/cli/commands/user-touchpoint-commands.js +1 -1
  90. package/dist/cli/commands/verdict-aggregate-command.js +1 -1
  91. package/dist/cli/commands/workflow-commands.js +1 -1
  92. package/dist/cli/commands/workflow-plan-commands.js +1 -1
  93. package/dist/cli/commands/workspace/init-command.js +1 -1
  94. package/dist/cli/commands/workspace/migrate-command.js +1 -1
  95. package/dist/cli/commands/workspace/reconcile-command.js +1 -1
  96. package/dist/cli/index.js +1 -1
  97. package/dist/cli/program.js +2 -2
  98. package/dist/services/artifacts/artifact-prerequisites.js +1 -1
  99. package/dist/services/artifacts/request-artifact-service.js +1 -1
  100. package/dist/services/artifacts/workspace-service.js +1 -1
  101. package/dist/services/code/auto-compact-orchestrator.js +5 -2
  102. package/dist/services/code-review/ocr-service.d.ts +1 -1
  103. package/dist/services/code-review/ocr-service.js +6 -3
  104. package/dist/services/config/config-types.js +1 -1
  105. package/dist/services/dashboard/project-dashboard-service.js +2 -2
  106. package/dist/services/dispatch/batch-counter.js +1 -1
  107. package/dist/services/dispatch/dispatch-record-writer.js +1 -1
  108. package/dist/services/job/job-progress-store.d.ts +2 -2
  109. package/dist/services/job/job-types.d.ts +27 -27
  110. package/dist/services/log/retention.js +1 -1
  111. package/dist/services/openspec/openspec-archive-service.js +1 -1
  112. package/dist/services/openspec/openspec-bridge-service.js +1 -1
  113. package/dist/services/openspec/openspec-init-service.js +1 -1
  114. package/dist/services/openspec/openspec-propose-from-doctor-service.js +1 -1
  115. package/dist/services/openspec/openspec-render-service.js +1 -1
  116. package/dist/services/openspec/openspec-scan-service.js +1 -1
  117. package/dist/services/openspec/openspec-validate-service.js +1 -1
  118. package/dist/services/scan/acceptance-coverage-service.js +1 -1
  119. package/dist/services/scan/archetype-service.js +1 -1
  120. package/dist/services/scan/existing-system-service.js +1 -1
  121. package/dist/services/scan/libraries-service.js +1 -1
  122. package/dist/services/skill/skill-search-service.d.ts +5 -5
  123. package/dist/services/skill/skill-search-service.js +2 -2
  124. package/dist/services/skills/skill-registry.js +2 -2
  125. package/dist/services/skills/skill-runbook-service.js +1 -1
  126. package/dist/services/slice/slice-check-service.js +1 -1
  127. package/dist/services/understand/understand-hybrid-service.js +1 -1
  128. package/dist/services/understand/understand-scan-service.js +2 -2
  129. package/dist/services/upgrade/upgrade-service.js +4 -3
  130. package/dist/services/verdict/envelopes.d.ts +2 -2
  131. package/dist/services/verdict/envelopes.js +2 -2
  132. package/dist/services/verdict/verdict-aggregator.d.ts +3 -3
  133. package/dist/services/workflow/autonomous-resume-writer.js +1 -1
  134. package/dist/services/workflow/plan-reader.d.ts +1 -1
  135. package/dist/services/workflow/plan-reader.js +1 -1
  136. package/dist/services/workflow/plan-refresher.d.ts +1 -1
  137. package/dist/services/workflow/plan-refresher.js +1 -1
  138. package/dist/services/workflow/plan-trigger-detector.d.ts +1 -1
  139. package/dist/services/workflow/plan-trigger-detector.js +1 -1
  140. package/dist/services/workflow/workflow-router-service.js +11 -9
  141. package/dist/services/workspace/migrate-service.js +1 -1
  142. package/dist/services/workspace/workspace-service.js +1 -1
  143. package/package.json +13 -8
  144. package/scripts/sync-version.mjs +8 -1
  145. package/skills/bee/peaks-rd/SKILL.md +14 -1
  146. package/skills/peaks-code/SKILL.md +44 -0
  147. package/skills/peaks-code/references/sub-agent-dispatch.md +30 -0
  148. package/dist/services/agent/ecc-cache-service.d.ts +0 -97
  149. package/dist/services/agent/ecc-cache-service.js +0 -491
  150. package/dist/services/audit-independent/perf-audit-service.d.ts +0 -151
  151. package/dist/services/audit-independent/perf-audit-service.js +0 -316
  152. package/dist/services/audit-independent/security-audit-service.d.ts +0 -155
  153. package/dist/services/audit-independent/security-audit-service.js +0 -316
  154. package/dist/services/context/dispatch-context-guard.d.ts +0 -27
  155. package/dist/services/context/dispatch-context-guard.js +0 -192
  156. package/dist/services/context/shared-channel.d.ts +0 -93
  157. package/dist/services/context/shared-channel.js +0 -308
  158. package/dist/services/crystallization/crystallization-service.d.ts +0 -239
  159. package/dist/services/crystallization/crystallization-service.js +0 -397
  160. package/dist/services/crystallization/crystallization-store.d.ts +0 -75
  161. package/dist/services/crystallization/crystallization-store.js +0 -193
  162. package/dist/services/crystallization/crystallization-types.d.ts +0 -313
  163. package/dist/services/crystallization/crystallization-types.js +0 -299
  164. package/dist/services/crystallization/evidence-brief-builder.d.ts +0 -237
  165. package/dist/services/crystallization/evidence-brief-builder.js +0 -240
  166. package/dist/services/doctor/doctor-service.d.ts +0 -149
  167. package/dist/services/doctor/doctor-service.js +0 -929
  168. package/dist/services/filesystem/file-lock.d.ts +0 -11
  169. package/dist/services/filesystem/file-lock.js +0 -152
  170. package/dist/services/final-review/final-review-service.d.ts +0 -12
  171. package/dist/services/final-review/final-review-service.js +0 -52
  172. package/dist/services/final-review/final-review-types.d.ts +0 -42
  173. package/dist/services/final-review/final-review-types.js +0 -18
  174. package/dist/services/job/job-resource-snapshot.d.ts +0 -7
  175. package/dist/services/job/job-resource-snapshot.js +0 -75
  176. package/dist/services/mut/assert-scanner.d.ts +0 -6
  177. package/dist/services/mut/assert-scanner.js +0 -116
  178. package/dist/services/mut/index.d.ts +0 -8
  179. package/dist/services/mut/index.js +0 -7
  180. package/dist/services/mut/mut-runner.d.ts +0 -39
  181. package/dist/services/mut/mut-runner.js +0 -29
  182. package/dist/services/mut/production-stryker.d.ts +0 -2
  183. package/dist/services/mut/production-stryker.js +0 -75
  184. package/dist/services/mut/report-builder.d.ts +0 -11
  185. package/dist/services/mut/report-builder.js +0 -110
  186. package/dist/services/mut/report-loader.d.ts +0 -31
  187. package/dist/services/mut/report-loader.js +0 -78
  188. package/dist/services/mut/thresholds.d.ts +0 -21
  189. package/dist/services/mut/thresholds.js +0 -22
  190. package/dist/services/mut/types.d.ts +0 -336
  191. package/dist/services/mut/types.js +0 -59
  192. package/dist/shared/fs.d.ts +0 -4
  193. package/dist/shared/fs.js +0 -26
  194. package/dist/shared/paths.d.ts +0 -6
  195. package/dist/shared/paths.js +0 -50
  196. package/dist/shared/result.d.ts +0 -22
  197. package/dist/shared/result.js +0 -46
  198. package/dist/shared/version.d.ts +0 -1
  199. package/dist/shared/version.js +0 -1
  200. package/examples/video-demo/README.md +0 -142
  201. package/examples/video-demo/out/en-closing-960.png +0 -0
  202. package/examples/video-demo/out/zh-closing-960.png +0 -0
  203. package/examples/video-demo/preview/peaks-loop-demo-en.mp4 +0 -0
  204. package/examples/video-demo/preview/peaks-loop-demo.mp4 +0 -0
@@ -1,316 +0,0 @@
1
- /**
2
- * peaks-security-audit service — independent security audit skill driver.
3
- *
4
- * Slice v2.12.0 (Group A, Tier 2). The skill is decoupled from the
5
- * peaks-rd 5-way fan-out (per PRD AC-2.x) and lives as a standalone
6
- * CLI surface: `peaks security-audit run --rid <id> ...`.
7
- *
8
- * The service owns:
9
- * - 5-state detection of the security-audit runtime
10
- * (handoff-missing / template-missing / dispatch-failed / template-malformed / ready)
11
- * - Loading + sha256 verification of the prd/handoff.md
12
- * - Loading the project-level security-template.md
13
- * - Producing the audit envelope (verdict + violations) to write
14
- * to `.peaks/_runtime/<sid>/audit/security-<rid>.md`
15
- *
16
- * The actual LLM-in-loop audit step is NOT in this service — the
17
- * parent LLM (peaks-security-audit skill prompt) reads the template,
18
- * runs the audit, and returns a structured envelope. This service
19
- * is the pure I/O + validation core; the LLM is the judgement core.
20
- *
21
- * Why a service (not inlined in the CLI):
22
- * - Unit-testable in isolation (6 case per PRD AC-2.8)
23
- * - The 5-state detector mirrors the ecc-bridge detectEcc pattern
24
- * (slice 7 Group D), keeping the surface uniform
25
- * - Future migration to peaks sub-agent dispatch (peaks-rd main
26
- * loop can opt to call this service directly without re-implementing
27
- * the handoff verification)
28
- *
29
- * Cross-references:
30
- * - PRD: `.peaks/_runtime/<sid>/prd/handoff.md` (sha256-locked, schemaVersion: 2)
31
- * - Template: `.peaks/project-scan/security-template.md`
32
- * - Output: `.peaks/_runtime/<sid>/audit/security-<rid>.md`
33
- * - Schema: `.peaks/project-scan/audit-output-schema.md` (schemaVersion: 1)
34
- */
35
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
36
- import { join, resolve, isAbsolute } from 'node:path';
37
- import { createHash } from 'node:crypto';
38
- /**
39
- * Validate a raw value as a SecurityAuditEnvelope. Mirrors the
40
- * `isEccEnvelope` strict-shape pattern.
41
- */
42
- export function isSecurityAuditEnvelope(value) {
43
- if (value === null || typeof value !== 'object')
44
- return false;
45
- const obj = value;
46
- if (typeof obj.verdict !== 'string')
47
- return false;
48
- if (obj.verdict !== 'pass' && obj.verdict !== 'warn' && obj.verdict !== 'block') {
49
- return false;
50
- }
51
- if (!Array.isArray(obj.violations))
52
- return false;
53
- if (typeof obj.summary !== 'string')
54
- return false;
55
- for (const v of obj.violations) {
56
- if (v === null || typeof v !== 'object')
57
- return false;
58
- const vo = v;
59
- if (typeof vo.dimension !== 'string')
60
- return false;
61
- if (typeof vo.severity !== 'string')
62
- return false;
63
- if (vo.severity !== 'CRITICAL' && vo.severity !== 'HIGH' && vo.severity !== 'MED' && vo.severity !== 'LOW') {
64
- return false;
65
- }
66
- if (typeof vo.file !== 'string')
67
- return false;
68
- if (typeof vo.line !== 'number' || !Number.isFinite(vo.line))
69
- return false;
70
- if (typeof vo.hint !== 'string')
71
- return false;
72
- }
73
- return true;
74
- }
75
- /**
76
- * Read the peaks-prd handoff frontmatter + body, verify sha256 matches
77
- * the body. Returns null on missing file or sha256 mismatch; otherwise
78
- * returns the parsed frontmatter + body string.
79
- *
80
- * Defensive: we read frontmatter via a 1-line regex match, not via
81
- * a YAML parser. The handoff service (`src/services/prd/handoff-service.ts`)
82
- * is the canonical writer; we only consume its output here.
83
- */
84
- export function readAndVerifyHandoff(handoffPath, projectRoot) {
85
- if (!handoffPath || !isAbsolute(handoffPath)) {
86
- const absolutePath = resolve(projectRoot, handoffPath);
87
- if (!existsSync(absolutePath))
88
- return null;
89
- return readAndVerifyHandoff(absolutePath, projectRoot);
90
- }
91
- if (!existsSync(handoffPath))
92
- return null;
93
- let raw;
94
- try {
95
- raw = readFileSync(handoffPath, 'utf8');
96
- }
97
- catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
98
- return null;
99
- }
100
- // Frontmatter pattern: `---` line, then key: value pairs, then `---` line.
101
- const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
102
- if (match === null)
103
- return null;
104
- const frontmatterRaw = match[1];
105
- const body = match[2];
106
- const sha256Match = frontmatterRaw.match(/^sha256:\s*([a-f0-9]{64})\s*$/m);
107
- const schemaVersionMatch = frontmatterRaw.match(/^schemaVersion:\s*(\d+)\s*$/m);
108
- if (sha256Match === null || schemaVersionMatch === null)
109
- return null;
110
- const sha256Expected = sha256Match[1];
111
- const schemaVersion = parseInt(schemaVersionMatch[1], 10);
112
- const sha256Actual = createHash('sha256').update(body, 'utf8').digest('hex');
113
- if (sha256Expected !== sha256Actual)
114
- return null;
115
- return {
116
- frontmatter: { sha256: sha256Expected, schemaVersion },
117
- body,
118
- };
119
- }
120
- /**
121
- * Read the project-level security-template.md. Returns the body
122
- * string (with frontmatter stripped) or null if the file is absent.
123
- * Does NOT validate the template content; the parent LLM validates
124
- * by reading.
125
- */
126
- export function readSecurityTemplate(projectRoot) {
127
- const templatePath = join(projectRoot, '.peaks', 'project-scan', 'security-template.md');
128
- if (!existsSync(templatePath))
129
- return null;
130
- try {
131
- return readFileSync(templatePath, 'utf8');
132
- }
133
- catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
134
- return null;
135
- }
136
- }
137
- /**
138
- * 5-state detector. The parent skill inspects the state and either
139
- * proceeds (when `ready`) or records a degradation note and falls
140
- * back (when any other state).
141
- *
142
- * Soft-fail policy mirrors `detectEcc` (slice 7 Group D): the CLI
143
- * never throws; it returns a typed detect result.
144
- */
145
- export function detectSecurityAudit(input) {
146
- const warnings = [];
147
- const nextActions = [];
148
- const handoffPath = join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'prd', 'handoff.md');
149
- const handoffPresent = existsSync(handoffPath);
150
- const templatePath = join(input.projectRoot, '.peaks', 'project-scan', 'security-template.md');
151
- const templatePresent = existsSync(templatePath);
152
- if (!handoffPresent) {
153
- return {
154
- state: 'handoff-missing',
155
- handoffPresent: false,
156
- templatePresent,
157
- warnings: [`peaks-prd handoff not found at ${handoffPath}`],
158
- nextActions: [
159
- 'Run peaks-prd handoff init to produce a sha256-locked handoff before running peaks security-audit.',
160
- 'Until the handoff exists, peaks-security-audit cannot start (gate fail).'
161
- ]
162
- };
163
- }
164
- if (!templatePresent) {
165
- return {
166
- state: 'template-missing',
167
- handoffPresent: true,
168
- templatePresent: false,
169
- warnings: [`security-template.md not found at ${templatePath}`],
170
- nextActions: [
171
- 'Run peaks project template init to bootstrap the 3 audit templates (security-template, perf-template, audit-output-schema).',
172
- 'Until the template exists, peaks-security-audit cannot start (gate fail).'
173
- ]
174
- };
175
- }
176
- if (input.dispatchError !== undefined) {
177
- return {
178
- state: 'dispatch-failed',
179
- handoffPresent: true,
180
- templatePresent: true,
181
- warnings: [
182
- `Audit dispatch failed: ${input.dispatchError instanceof Error ? input.dispatchError.message : String(input.dispatchError)}`
183
- ],
184
- nextActions: [
185
- 'Inspect the audit prompt template for the failure cause.',
186
- 'peaks-security-audit falls back to inline LLM review (degradation note: security-audit-dispatch-failed).'
187
- ]
188
- };
189
- }
190
- if (input.envelope !== undefined && !isSecurityAuditEnvelope(input.envelope)) {
191
- return {
192
- state: 'envelope-malformed',
193
- handoffPresent: true,
194
- templatePresent: true,
195
- warnings: [
196
- 'Security-audit returned a value that failed envelope validation (expected { verdict, violations, summary }).'
197
- ],
198
- nextActions: [
199
- 'Verify the security-audit skill prompt matches PRD AC-2.x output shape.',
200
- 'If the envelope shape is intentionally different, file a bug at the peaks-loop repo.'
201
- ]
202
- };
203
- }
204
- return {
205
- state: 'ready',
206
- handoffPresent: true,
207
- templatePresent: true,
208
- warnings,
209
- nextActions
210
- };
211
- }
212
- /**
213
- * Render a SecurityAuditEnvelope to the canonical audit artifact body
214
- * (markdown). Mirrors `adaptEccEnvelopeToRdCodeReview` in ecc-bridge.ts:
215
- * produce a markdown body with all required sections, ready to write
216
- * to `.peaks/_runtime/<sid>/audit/security-<rid>.md`.
217
- */
218
- export function renderSecurityAuditArtifact(env, opts) {
219
- const counts = {};
220
- for (const v of env.violations) {
221
- counts[v.severity] = (counts[v.severity] ?? 0) + 1;
222
- }
223
- const criticalCount = counts['CRITICAL'] ?? 0;
224
- const findingsBullets = env.violations.length === 0
225
- ? '- (none)'
226
- : env.violations.map((v) => `- [${v.severity}] ${v.dimension} @ ${v.file}:${v.line} — ${v.hint}`).join('\n');
227
- const requiredFixes = env.violations.length === 0
228
- ? ''
229
- : [
230
- '## Required fixes',
231
- '',
232
- ...env.violations.map((v) => `- [${v.severity}] ${v.file}:${v.line} — ${v.hint}`),
233
- ''
234
- ].join('\n');
235
- const body = [
236
- '## Summary',
237
- '',
238
- env.summary,
239
- '',
240
- '## Threat model coverage',
241
- '',
242
- `audited at: ${opts.generatedAt}`,
243
- `verdict: ${env.verdict}`,
244
- `handoffHash: ${opts.handoffHash}`,
245
- '',
246
- '## Findings',
247
- '',
248
- findingsBullets,
249
- '',
250
- requiredFixes,
251
- '## Verdict',
252
- '',
253
- `verdict: ${env.verdict}`,
254
- `CRITICAL: ${criticalCount}`,
255
- ''
256
- ].filter((s) => s.length > 0).join('\n');
257
- return {
258
- body,
259
- violationsCount: env.violations.length,
260
- verdict: env.verdict,
261
- };
262
- }
263
- /**
264
- * Write the rendered audit artifact to
265
- * `.peaks/_runtime/<sid>/audit/security-<rid>.md`.
266
- *
267
- * Atomic write (tmp + rename) to avoid partial writes on crash.
268
- * Returns the absolute path on success.
269
- */
270
- export function writeSecurityAuditArtifact(projectRoot, sessionId, rid, body) {
271
- const targetDir = join(projectRoot, '.peaks', '_runtime', sessionId, 'audit');
272
- mkdirSync(targetDir, { recursive: true });
273
- const targetPath = join(targetDir, `security-${rid}.md`);
274
- // tmp + rename for atomicity
275
- const tmpPath = `${targetPath}.tmp`;
276
- writeFileSync(tmpPath, body, 'utf8');
277
- // Sync rename: on Windows, fs.renameSync overwrites if target exists (Node 22+)
278
- const { renameSync } = require('node:fs');
279
- renameSync(tmpPath, targetPath);
280
- return targetPath;
281
- }
282
- /**
283
- * Convenience: run `detectSecurityAudit` + `renderSecurityAuditArtifact`
284
- * + `writeSecurityAuditArtifact` in one call.
285
- *
286
- * Returns `{ detect, artifactPath, violationsCount, verdict }` or
287
- * `{ detect, artifactPath: null, ... }` when not ready.
288
- */
289
- export function runSecurityAudit(input) {
290
- const detect = detectSecurityAudit({
291
- projectRoot: input.projectRoot,
292
- sessionId: input.sessionId,
293
- ...(input.dispatchError !== undefined ? { dispatchError: input.dispatchError } : {}),
294
- ...(input.envelope !== undefined ? { envelope: input.envelope } : {})
295
- });
296
- if (detect.state !== 'ready' || input.envelope === undefined) {
297
- return { detect, artifactPath: null, violationsCount: 0, verdict: null };
298
- }
299
- // detect.state === 'ready' implies input.envelope passed isSecurityAuditEnvelope.
300
- const env = input.envelope;
301
- const handoffPath = join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'prd', 'handoff.md');
302
- const verified = readAndVerifyHandoff(handoffPath, input.projectRoot);
303
- const handoffHash = verified?.frontmatter.sha256 ?? 'unknown';
304
- const rendered = renderSecurityAuditArtifact(env, {
305
- rid: input.rid,
306
- handoffHash,
307
- generatedAt: input.generatedAt
308
- });
309
- const artifactPath = writeSecurityAuditArtifact(input.projectRoot, input.sessionId, input.rid, rendered.body);
310
- return {
311
- detect,
312
- artifactPath,
313
- violationsCount: rendered.violationsCount,
314
- verdict: rendered.verdict
315
- };
316
- }
@@ -1,27 +0,0 @@
1
- /** Build the canonical artifact path for a given session/rid/role/idx/ext. */
2
- export declare function artifactPath(projectRoot: string, sid: string, rid: string, role: string, idx: number, ext?: string): string;
3
- /** Build the canonical shared channel path. */
4
- export declare function sharedChannelPath(projectRoot: string, sid: string, rid: string, batchId: string): string;
5
- /**
6
- * Assert that `artifactPath` lives under
7
- * `projectRoot/.peaks/_sub_agents/<sid>/artifacts/`. Rejects
8
- * symlink/junction escapes and `..` segments.
9
- *
10
- * Throws an Error with `.code = 'INVALID_ARTIFACT_PATH'` on rejection.
11
- */
12
- export declare function assertSafeArtifactPath(artifactPathInput: string, projectRoot: string): string;
13
- /**
14
- * Assert that `channelPath` lives under
15
- * `projectRoot/.peaks/_sub_agents/<sid>/shared/`. Same R-2 logic as
16
- * `assertSafeArtifactPath` but with a different canonical subdir.
17
- *
18
- * Throws an Error with `.code = 'INVALID_SHARED_CHANNEL_PATH'` on rejection.
19
- */
20
- export declare function assertSafeSharedChannelPath(channelPathInput: string, projectRoot: string): string;
21
- /**
22
- * Soft-warn check on the artifact file name pattern. Returns null if
23
- * the name matches `<rid>-<role>-<idx>.<ext>`, otherwise returns a
24
- * warning string. Does NOT reject (the path is still in the canonical
25
- * dir; the warning is for human/audit readability per G7.4.c).
26
- */
27
- export declare function checkArtifactNameConvention(artifactPathInput: string): string | null;
@@ -1,192 +0,0 @@
1
- /**
2
- * G7.4 path safety + G8 path safety — R-2 symlink/junction guard for
3
- * sub-agent artifact + shared channel paths.
4
- *
5
- * Slice #009's `assertSafeDispatchRecordPath` covers the dispatch record
6
- * path. This module covers two adjacent paths under the same canonical
7
- * root (`.peaks/_sub_agents/<sid>/`):
8
- *
9
- * - `artifacts/<rid>-<role>-<idx>.<ext>` (G7 write-artifact)
10
- * - `shared/<batchId>.json` (G8 share / shared-read)
11
- *
12
- * Both reuse the same R-2 logic: reject `..` segments BEFORE the OS
13
- * resolver collapses them (POSIX normalize silently drops `..`), reject
14
- * absolute paths that escape the canonical root, resolve symlinks via
15
- * `realpathSync` and re-check the resolved path.
16
- *
17
- * The canonical-root pattern is intentionally identical to the slice #009
18
- * helper, so future audits can reuse one mental model: "all sub-agent
19
- * state files must live under `.peaks/_sub_agents/<sid>/` and pass the
20
- * same R-2 guard."
21
- */
22
- import { realpathSync } from 'node:fs';
23
- import { dirname, isAbsolute, relative, resolve } from 'node:path';
24
- const SUB_AGENTS_DIR = '_sub_agents';
25
- const ARTIFACTS_SUBDIR = 'artifacts';
26
- const SHARED_SUBDIR = 'shared';
27
- const ARTIFACT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\.[A-Za-z0-9]+$/;
28
- const BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
29
- /** Build the canonical artifact path for a given session/rid/role/idx/ext. */
30
- export function artifactPath(projectRoot, sid, rid, role, idx, ext = 'md') {
31
- if (typeof role !== 'string' || role.length === 0) {
32
- throw new Error('artifactPath: role must be non-empty');
33
- }
34
- if (!Number.isInteger(idx) || idx < 1) {
35
- throw new Error(`artifactPath: idx must be positive integer (got ${idx})`);
36
- }
37
- const safeSid = sanitizeSegment(sid, 'sessionId');
38
- const safeRid = sanitizeSegment(rid, 'requestId');
39
- const safeRole = sanitizeSegment(role, 'role');
40
- const safeExt = ext.replace(/[^A-Za-z0-9]/g, '');
41
- if (safeExt.length === 0) {
42
- throw new Error(`artifactPath: ext must be alphanumeric (got "${ext}")`);
43
- }
44
- return resolve(projectRoot, '.peaks', SUB_AGENTS_DIR, safeSid, ARTIFACTS_SUBDIR, `${safeRid}-${safeRole}-${String(idx).padStart(3, '0')}.${safeExt}`);
45
- }
46
- /** Build the canonical shared channel path. */
47
- export function sharedChannelPath(projectRoot, sid, rid, batchId) {
48
- if (typeof batchId !== 'string' || batchId.length === 0) {
49
- throw new Error('sharedChannelPath: batchId must be non-empty');
50
- }
51
- const safeSid = sanitizeSegment(sid, 'sessionId');
52
- const safeRid = sanitizeSegment(rid, 'requestId');
53
- if (!BATCH_ID_PATTERN.test(batchId)) {
54
- throw new Error(`sharedChannelPath: batchId must match ${BATCH_ID_PATTERN} (got "${batchId}")`);
55
- }
56
- return resolve(projectRoot, '.peaks', SUB_AGENTS_DIR, safeSid, SHARED_SUBDIR, `${safeRid}-${batchId}.json`);
57
- }
58
- /**
59
- * Assert that `artifactPath` lives under
60
- * `projectRoot/.peaks/_sub_agents/<sid>/artifacts/`. Rejects
61
- * symlink/junction escapes and `..` segments.
62
- *
63
- * Throws an Error with `.code = 'INVALID_ARTIFACT_PATH'` on rejection.
64
- */
65
- export function assertSafeArtifactPath(artifactPathInput, projectRoot) {
66
- if (!isAbsolute(artifactPathInput)) {
67
- throw invalidPathError(artifactPathInput, 'must be absolute');
68
- }
69
- const rawSegments = artifactPathInput.split(/[\\/]/);
70
- if (rawSegments.includes('..')) {
71
- throw invalidPathError(artifactPathInput, 'must not contain .. segments');
72
- }
73
- const expected = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
74
- const rel = relative(expected, artifactPathInput);
75
- if (rel.startsWith('..') || isAbsolute(rel)) {
76
- throw invalidPathError(artifactPathInput, 'must be under .peaks/_sub_agents/');
77
- }
78
- let realArtifact;
79
- let realRoot;
80
- try {
81
- const parent = dirname(artifactPathInput);
82
- const realParent = realpathSync(parent);
83
- realArtifact = resolve(realParent, artifactPathInput.slice(parent.length + 1));
84
- realRoot = realpathSync(projectRoot);
85
- }
86
- catch {
87
- const fallback = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
88
- const rel2 = relative(fallback, artifactPathInput);
89
- if (rel2.startsWith('..') || isAbsolute(rel2)) {
90
- throw invalidPathError(artifactPathInput, 'must be under .peaks/_sub_agents/');
91
- }
92
- return artifactPathInput;
93
- }
94
- const realRel = relative(realRoot, realArtifact);
95
- if (realRel.startsWith('..') || isAbsolute(realRel)) {
96
- throw invalidPathError(artifactPathInput, 'symlink/junction escapes project root');
97
- }
98
- const realExpected = resolve(realRoot, '.peaks', SUB_AGENTS_DIR);
99
- const realRelExpected = relative(realExpected, realArtifact);
100
- if (realRelExpected.startsWith('..') || isAbsolute(realRelExpected)) {
101
- throw invalidPathError(artifactPathInput, 'must be under .peaks/_sub_agents/');
102
- }
103
- return realArtifact;
104
- }
105
- /**
106
- * Assert that `channelPath` lives under
107
- * `projectRoot/.peaks/_sub_agents/<sid>/shared/`. Same R-2 logic as
108
- * `assertSafeArtifactPath` but with a different canonical subdir.
109
- *
110
- * Throws an Error with `.code = 'INVALID_SHARED_CHANNEL_PATH'` on rejection.
111
- */
112
- export function assertSafeSharedChannelPath(channelPathInput, projectRoot) {
113
- if (!isAbsolute(channelPathInput)) {
114
- throw invalidPathErrorShared(channelPathInput, 'must be absolute');
115
- }
116
- const rawSegments = channelPathInput.split(/[\\/]/);
117
- if (rawSegments.includes('..')) {
118
- throw invalidPathErrorShared(channelPathInput, 'must not contain .. segments');
119
- }
120
- const expected = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
121
- const rel = relative(expected, channelPathInput);
122
- if (rel.startsWith('..') || isAbsolute(rel)) {
123
- throw invalidPathErrorShared(channelPathInput, 'must be under .peaks/_sub_agents/shared/');
124
- }
125
- let realChannel;
126
- let realRoot;
127
- try {
128
- const parent = dirname(channelPathInput);
129
- const realParent = realpathSync(parent);
130
- realChannel = resolve(realParent, channelPathInput.slice(parent.length + 1));
131
- realRoot = realpathSync(projectRoot);
132
- }
133
- catch {
134
- const fallback = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
135
- const rel2 = relative(fallback, channelPathInput);
136
- if (rel2.startsWith('..') || isAbsolute(rel2)) {
137
- throw invalidPathErrorShared(channelPathInput, 'must be under .peaks/_sub_agents/shared/');
138
- }
139
- return channelPathInput;
140
- }
141
- const realRel = relative(realRoot, realChannel);
142
- if (realRel.startsWith('..') || isAbsolute(realRel)) {
143
- throw invalidPathErrorShared(channelPathInput, 'symlink/junction escapes project root');
144
- }
145
- const realExpected = resolve(realRoot, '.peaks', SUB_AGENTS_DIR);
146
- const realRelExpected = relative(realExpected, realChannel);
147
- if (realRelExpected.startsWith('..') || isAbsolute(realRelExpected)) {
148
- throw invalidPathErrorShared(channelPathInput, 'must be under .peaks/_sub_agents/shared/');
149
- }
150
- return realChannel;
151
- }
152
- /**
153
- * Soft-warn check on the artifact file name pattern. Returns null if
154
- * the name matches `<rid>-<role>-<idx>.<ext>`, otherwise returns a
155
- * warning string. Does NOT reject (the path is still in the canonical
156
- * dir; the warning is for human/audit readability per G7.4.c).
157
- */
158
- export function checkArtifactNameConvention(artifactPathInput) {
159
- const base = artifactPathInput.split(/[\\/]/).pop() ?? '';
160
- if (ARTIFACT_NAME_PATTERN.test(base)) {
161
- return null;
162
- }
163
- return `Artifact file name does not match <rid>-<role>-<idx>.<ext> convention: ${base}`;
164
- }
165
- function sanitizeSegment(value, field) {
166
- if (typeof value !== 'string' || value.length === 0) {
167
- throw new Error(`${field} must be non-empty`);
168
- }
169
- if (value.length > 256) {
170
- throw new Error(`${field} must be ≤ 256 chars (got ${value.length})`);
171
- }
172
- for (let i = 0; i < value.length; i += 1) {
173
- const code = value.charCodeAt(i);
174
- if (code <= 0x20 || code === 0x7F) {
175
- throw new Error(`${field} must not contain whitespace or control characters`);
176
- }
177
- }
178
- if (value.includes('..') || value.includes('/') || value.includes('\\')) {
179
- throw new Error(`${field} must not contain '..' or path separators`);
180
- }
181
- return value;
182
- }
183
- function invalidPathError(path, reason) {
184
- const e = new Error(`Invalid artifact path: ${reason} (path: ${path})`);
185
- e.code = 'INVALID_ARTIFACT_PATH';
186
- return e;
187
- }
188
- function invalidPathErrorShared(path, reason) {
189
- const e = new Error(`Invalid shared channel path: ${reason} (path: ${path})`);
190
- e.code = 'INVALID_SHARED_CHANNEL_PATH';
191
- return e;
192
- }
@@ -1,93 +0,0 @@
1
- export interface SharedChannelEntry {
2
- readonly version: 1;
3
- readonly at: string;
4
- readonly from: string;
5
- readonly key: string;
6
- readonly value: Readonly<Record<string, unknown>>;
7
- readonly valueSize: number;
8
- }
9
- export interface SharedChannel {
10
- readonly batchId: string;
11
- readonly createdAt: string;
12
- readonly updatedAt: string;
13
- readonly entries: Readonly<Record<string, SharedChannelEntry>>;
14
- }
15
- export declare const SHARED_CHANNEL_MAX_VALUE_BYTES: number;
16
- export declare const SHARED_CHANNEL_SOFT_VALUE_WARN = 1024;
17
- export declare const SHARED_CHANNEL_MAX_FILE_BYTES: number;
18
- export declare const SHARED_CHANNEL_TTL_DAYS = 30;
19
- export type WriteSharedEntryResult = {
20
- readonly ok: true;
21
- readonly entry: SharedChannelEntry;
22
- readonly channelSize: number;
23
- readonly lastWriteWins: boolean;
24
- readonly softWarning: boolean;
25
- } | {
26
- readonly ok: false;
27
- readonly code: 'VALUE_TOO_LARGE' | 'INVALID_BATCH_ID' | 'WRITE_ERROR';
28
- readonly message: string;
29
- };
30
- /**
31
- * Write a shared entry to the per-batch channel file. Atomic-write
32
- * (tmp + rename). Returns the new entry + channel size + last-write-wins
33
- * flag (true if the key already existed; the new value overwrites).
34
- *
35
- * RL-25 size limit: `value ≥ 64KB` is hard-rejected with `VALUE_TOO_LARGE`.
36
- * `value > 1KB and < 64KB` is a soft warning (returned in the result).
37
- */
38
- export declare function writeSharedEntry(opts: {
39
- projectRoot: string;
40
- sid: string;
41
- rid: string;
42
- batchId: string;
43
- key: string;
44
- from: string;
45
- value: Record<string, unknown>;
46
- }): WriteSharedEntryResult;
47
- /**
48
- * Read the shared channel for a batch. Returns the channel with all
49
- * matching entries. Filters: `--since` (ISO8601) and `--key` (glob
50
- * pattern; simple `*` wildcard, no regex).
51
- *
52
- * If the channel file does not exist, returns an empty channel
53
- * (this is the dispatcher's "fresh batch" view).
54
- */
55
- export declare function readSharedChannel(opts: {
56
- projectRoot: string;
57
- sid: string;
58
- rid: string;
59
- batchId: string;
60
- since?: string;
61
- keyPattern?: string;
62
- }): SharedChannel;
63
- /**
64
- * Garbage-collect a single channel. Returns true if the file was
65
- * deleted, false if it did not exist.
66
- */
67
- export declare function gcChannel(opts: {
68
- projectRoot: string;
69
- sid: string;
70
- rid: string;
71
- batchId: string;
72
- }): boolean;
73
- /**
74
- * Check if a channel file is older than `SHARED_CHANNEL_TTL_DAYS` days
75
- * and should be GC'd as an orphan. Returns true if file is missing
76
- * (already GC'd) or older than TTL.
77
- */
78
- export declare function isOrphanChannel(opts: {
79
- projectRoot: string;
80
- sid: string;
81
- rid: string;
82
- batchId: string;
83
- now?: Date;
84
- }): boolean;
85
- /**
86
- * Compile a simple key pattern with `*` wildcards to a matcher. Only
87
- * `*` is special; everything else is a literal. `*` matches zero or
88
- * more characters. Examples:
89
- * "rd.*" matches "rd.completed", "rd.found-blocker"
90
- * "*.completed" matches "rd.completed", "qa.completed"
91
- * "*" matches everything
92
- */
93
- export declare function compileKeyPattern(pattern: string): (key: string) => boolean;