peaks-loop 4.0.16 → 4.0.18

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 (80) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README-en.md +12 -9
  3. package/README.md +15 -4
  4. package/config/eslint/.peaks-rules.cjs +39 -26
  5. package/dist/cli/commands/_register.js +2 -1
  6. package/dist/cli/commands/audit-commands.d.ts +1 -1
  7. package/dist/cli/commands/code-gate-command.d.ts +25 -0
  8. package/dist/cli/commands/code-gate-command.js +86 -0
  9. package/dist/cli/commands/code-mode-gate-commands.js +3 -4
  10. package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
  11. package/dist/cli/commands/cron-commands.js +16 -4
  12. package/dist/cli/commands/dashboard-long-run.js +8 -4
  13. package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
  14. package/dist/cli/commands/hooks-commands.js +56 -3
  15. package/dist/cli/commands/primer-command.d.ts +59 -0
  16. package/dist/cli/commands/primer-command.js +160 -0
  17. package/dist/cli/commands/qa-commands.js +1 -2
  18. package/dist/cli/commands/sediment-commands.d.ts +1 -1
  19. package/dist/cli/commands/slice-commands.js +20 -8
  20. package/dist/cli/commands/statusline-commands.d.ts +1 -0
  21. package/dist/cli/commands/statusline-commands.js +17 -3
  22. package/dist/cli/commands/tech-commands.d.ts +1 -1
  23. package/dist/cli/program.js +8 -0
  24. package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
  25. package/dist/services/artifacts/request-artifact-service.js +10 -1
  26. package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
  27. package/dist/services/code/auto-compact-orchestrator.js +9 -1
  28. package/dist/services/code/batch-heartbeat-poller.js +26 -0
  29. package/dist/services/code/orchestrator-can-do.d.ts +24 -0
  30. package/dist/services/code/orchestrator-can-do.js +48 -2
  31. package/dist/services/code/post-compact-detector.d.ts +1 -1
  32. package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
  33. package/dist/services/config/config-safety.d.ts +23 -0
  34. package/dist/services/config/config-safety.js +62 -0
  35. package/dist/services/config/config-service.d.ts +1 -1
  36. package/dist/services/config/config-service.js +2 -4
  37. package/dist/services/container/container-lease.js +21 -8
  38. package/dist/services/context/spillover-store.d.ts +1 -1
  39. package/dist/services/crystallization/crystallization-types.js +20 -8
  40. package/dist/services/crystallization/evidence-brief-builder.js +22 -11
  41. package/dist/services/dispatch/dispatch-record-writer.js +231 -141
  42. package/dist/services/evolution/evolution-types.js +38 -21
  43. package/dist/services/feedback/feedback-promotion-service.js +9 -1
  44. package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
  45. package/dist/services/hooks/pre-tool-code-gate.js +91 -0
  46. package/dist/services/ide/current-model-detector.js +1 -2
  47. package/dist/services/lint/detect-eslint.js +8 -5
  48. package/dist/services/lint/eslint-runner.d.ts +1 -2
  49. package/dist/services/lint/eslint-runner.js +86 -23
  50. package/dist/services/lint/npx-resolver.d.ts +6 -0
  51. package/dist/services/lint/npx-resolver.js +47 -0
  52. package/dist/services/loop/loop-release-types.js +32 -13
  53. package/dist/services/loop/spec-service.d.ts +7 -1
  54. package/dist/services/loop/spec-service.js +218 -182
  55. package/dist/services/rd/rd-service.js +22 -9
  56. package/dist/services/scan/archetype-service.js +21 -9
  57. package/dist/services/session/binding-store.js +1 -2
  58. package/dist/services/share/bundle-reader.d.ts +6 -9
  59. package/dist/services/share/bundle-reader.js +297 -223
  60. package/dist/services/skills/hooks-settings-service.d.ts +33 -0
  61. package/dist/services/skills/hooks-settings-service.js +48 -3
  62. package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
  63. package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
  64. package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
  65. package/dist/services/skills/skill-statusline-renderer.js +96 -8
  66. package/dist/services/skills/skill-statusline-service.d.ts +41 -0
  67. package/dist/services/skills/skill-statusline-service.js +56 -6
  68. package/dist/services/slice/calibration-store.js +21 -8
  69. package/dist/services/slice/slice-check-service.js +35 -14
  70. package/dist/services/slice/slice-decompose-service.js +459 -223
  71. package/dist/services/standards/project-context.js +135 -73
  72. package/dist/services/verdict/envelopes.d.ts +1 -2
  73. package/dist/services/verdict/envelopes.js +1 -2
  74. package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
  75. package/dist/services/vm/vm-lease.js +19 -8
  76. package/dist/services/worktree/worktree-lease.d.ts +0 -10
  77. package/dist/services/worktree/worktree-lease.js +19 -8
  78. package/dist/shared/fs-utils.d.ts +1 -1
  79. package/package.json +7 -4
  80. package/skills/peaks-code/SKILL.md +46 -0
@@ -21,15 +21,12 @@
21
21
  * imported release to `stable` (AC-26 — peaks loop promote
22
22
  * reads evolution_evaluation rows).
23
23
  *
24
- * The reader is also the integration point for AC-26 — the loop
25
- * promote path (which lives in a future slice / CLI command) checks
26
- * `evolution_evaluation.independent_scorer_verdict` before allowing
27
- * a candidate → stable transition; this slice does not add a new
28
- * `peaks loop promote` because `peaks asset crystallize` already
29
- * enforces the brief-section + pre-run gates and creates the
30
- * initial `candidate` row. The integration test for AC-26 verifies
31
- * that a freshly imported bundle has no `evolution_evaluation`
32
- * row and that promotion therefore cannot proceed.
24
+ * Slice 4 (PRD-002b): split the four high-cohort functions
25
+ * (readBundle 15, importLoopBundle 20, materialiseRelatedBees 28,
26
+ * importBeeBundle arrow 38) into small helpers with table dispatch
27
+ * for kind routing and early-return for guard paths. Public API
28
+ * (readBundle, ReadBundleArgs, ReadBundleResult, the four error
29
+ * classes) is preserved verbatim.
33
30
  */
34
31
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rmSync, } from "node:fs";
35
32
  import { join } from "node:path";
@@ -93,6 +90,71 @@ export class BundleMalformedError extends Error {
93
90
  }
94
91
  }
95
92
  /* ---------------------------------------------------------------------- */
93
+ /* Stage helpers (slice 4) */
94
+ /* ---------------------------------------------------------------------- */
95
+ /** SHA-256 content-hash filename pattern. */
96
+ const SHA256_RE = /^[0-9a-f]{64}$/;
97
+ /** Coerce a possibly-unknown value to a string lifecycle status, defaulting to "candidate". */
98
+ function readSourceLifecycle(release) {
99
+ return typeof release.lifecycle_status === "string"
100
+ ? release.lifecycle_status
101
+ : "candidate";
102
+ }
103
+ /**
104
+ * Enforce the AC-25 hard rule: bundles always land as `candidate`;
105
+ * any other source status is refused. Layered at every import site
106
+ * for defense in depth.
107
+ */
108
+ function enforceImportAsCandidate(status) {
109
+ if (status !== "candidate") {
110
+ throw new BundleImportToStableForbiddenError(status);
111
+ }
112
+ }
113
+ /** Coerce an unknown value to a boolean-ish 0/1 column value. */
114
+ function bool01(value, defaultValue) {
115
+ if (value === undefined)
116
+ return defaultValue;
117
+ return value ? 1 : 0;
118
+ }
119
+ /** Coerce an unknown value to a string column value (empty string default). */
120
+ function str(value, defaultValue = "") {
121
+ return typeof value === "string" ? value : defaultValue;
122
+ }
123
+ /** Coerce an unknown value to a string-or-null column value. */
124
+ function strOrNull(value) {
125
+ return typeof value === "string" ? value : null;
126
+ }
127
+ /** Coerce an unknown value to a JSON-stringified column value. */
128
+ function jsonArr(value) {
129
+ return JSON.stringify(value ?? []);
130
+ }
131
+ /** Coerce an unknown value to an ISO-8601 string (now() default). */
132
+ function isoOrNow(value) {
133
+ return typeof value === "string" ? value : new Date().toISOString();
134
+ }
135
+ /** Now as an ISO string. */
136
+ function nowIso() {
137
+ return new Date().toISOString();
138
+ }
139
+ /** Safe directory listing (returns [] on error). */
140
+ function readDirEntries(dir) {
141
+ try {
142
+ return readdirSync(dir);
143
+ }
144
+ catch {
145
+ return [];
146
+ }
147
+ }
148
+ /** Read and JSON.parse a file, throwing BundleMalformedError on parse failure. */
149
+ function readJsonOrThrow(path) {
150
+ try {
151
+ return JSON.parse(readFileSync(path, "utf-8"));
152
+ }
153
+ catch (err) {
154
+ throw new BundleMalformedError(`manifest.json could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
155
+ }
156
+ }
157
+ /* ---------------------------------------------------------------------- */
96
158
  /* Main entrypoint */
97
159
  /* ---------------------------------------------------------------------- */
98
160
  export function readBundle(args) {
@@ -101,77 +163,77 @@ export function readBundle(args) {
101
163
  throw new BundleMalformedError(`bundle file not found at '${inPath}'`);
102
164
  }
103
165
  const stageDir = inPath + ".extract";
104
- if (existsSync(stageDir))
105
- rmSync(stageDir, { recursive: true, force: true });
106
- mkdirSync(stageDir, { recursive: true });
107
- let extractOk = false;
166
+ resetStageDir(stageDir);
108
167
  try {
109
168
  runTar(["-xzf", inPath, "-C", stageDir]);
110
- extractOk = true;
169
+ const manifest = loadStageManifest(stageDir);
170
+ materialiseBlobs(stageDir, blobsDir);
171
+ const assetId = KIND_IMPORTERS[manifest.kind](db, manifest, blobsDir, args.asName);
172
+ return buildReadResult(manifest, assetId);
111
173
  }
112
174
  finally {
113
- if (!extractOk)
175
+ if (existsSync(stageDir))
114
176
  rmSync(stageDir, { recursive: true, force: true });
115
177
  }
116
- try {
117
- const manifestPath = join(stageDir, "manifest.json");
118
- if (!existsSync(manifestPath)) {
119
- throw new BundleMalformedError("bundle is missing manifest.json (writer is required to emit it)");
120
- }
121
- let rawManifest;
122
- try {
123
- rawManifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
124
- }
125
- catch (err) {
126
- throw new BundleMalformedError(`manifest.json could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
127
- }
128
- const manifest = parseManifest(rawManifest);
129
- // Re-materialise content-addressed blobs regardless of kind —
130
- // the layout (`blobs/<sha256>`) is identical for both.
131
- const blobsStageDir = join(stageDir, "blobs");
132
- if (existsSync(blobsStageDir)) {
133
- mkdirSync(blobsDir, { recursive: true });
134
- const blobsOut = blobsDir;
135
- for (const fname of readDirEntries(blobsStageDir)) {
136
- const src = join(blobsStageDir, fname);
137
- if (!fname.match(/^[0-9a-f]{64}$/))
138
- continue;
139
- const destDir = join(blobsOut, fname.slice(0, 2));
140
- mkdirSync(destDir, { recursive: true });
141
- const destPath = join(destDir, fname);
142
- if (!existsSync(destPath)) {
143
- writeFileSync(destPath, readFileSync(src));
144
- }
145
- }
146
- }
147
- if (manifest.kind === "loop") {
148
- const assetId = importLoopBundle(db, manifest, args.asName);
149
- return {
150
- assetId,
151
- kind: "loop",
152
- importedAs: "candidate",
153
- warnings: manifest.format_version_minor === 0 ? [] : [
154
- `minor-version=${manifest.format_version_minor}; supported but flagging for awareness`,
155
- ],
156
- evidenceBriefCount: manifest.evidence_briefs.length,
157
- };
158
- }
159
- const beeId = importBeeBundle(db, manifest, blobsDir, args.asName);
160
- return {
161
- assetId: beeId,
162
- kind: "bee",
163
- importedAs: "candidate",
164
- warnings: manifest.format_version_minor === 0 ? [] : [
165
- `minor-version=${manifest.format_version_minor}; supported but flagging for awareness`,
166
- ],
167
- evidenceBriefCount: manifest.evidence_briefs.length,
168
- };
178
+ }
179
+ /**
180
+ * Clean any prior extract dir, then recreate it. Split out of
181
+ * readBundle so the main orchestrator stays branch-light.
182
+ */
183
+ function resetStageDir(stageDir) {
184
+ if (existsSync(stageDir))
185
+ rmSync(stageDir, { recursive: true, force: true });
186
+ mkdirSync(stageDir, { recursive: true });
187
+ }
188
+ /**
189
+ * Read and parse the staged manifest.json, raising a
190
+ * BundleMalformedError if the file is missing.
191
+ */
192
+ function loadStageManifest(stageDir) {
193
+ const manifestPath = join(stageDir, "manifest.json");
194
+ if (!existsSync(manifestPath)) {
195
+ throw new BundleMalformedError("bundle is missing manifest.json (writer is required to emit it)");
169
196
  }
170
- finally {
171
- if (existsSync(stageDir))
172
- rmSync(stageDir, { recursive: true, force: true });
197
+ return parseManifest(readJsonOrThrow(manifestPath));
198
+ }
199
+ /**
200
+ * Re-materialise content-addressed blobs regardless of kind —
201
+ * the layout (`blobs/<sha256>`) is identical for both.
202
+ */
203
+ function materialiseBlobs(stageDir, blobsDir) {
204
+ const blobsStageDir = join(stageDir, "blobs");
205
+ if (!existsSync(blobsStageDir))
206
+ return;
207
+ mkdirSync(blobsDir, { recursive: true });
208
+ for (const fname of readDirEntries(blobsStageDir)) {
209
+ if (!fname.match(SHA256_RE))
210
+ continue;
211
+ const destDir = join(blobsDir, fname.slice(0, 2));
212
+ mkdirSync(destDir, { recursive: true });
213
+ const destPath = join(destDir, fname);
214
+ if (existsSync(destPath))
215
+ continue;
216
+ writeFileSync(destPath, readFileSync(join(blobsStageDir, fname)));
173
217
  }
174
218
  }
219
+ /**
220
+ * Build the read-result envelope, including the minor-version
221
+ * warning if the bundle is not on the default minor.
222
+ */
223
+ function buildReadResult(manifest, assetId) {
224
+ const warnings = manifest.format_version_minor === 0
225
+ ? []
226
+ : [
227
+ `minor-version=${manifest.format_version_minor}; supported but flagging for awareness`,
228
+ ];
229
+ return {
230
+ assetId,
231
+ kind: manifest.kind,
232
+ importedAs: "candidate",
233
+ warnings,
234
+ evidenceBriefCount: manifest.evidence_briefs.length,
235
+ };
236
+ }
175
237
  /* ---------------------------------------------------------------------- */
176
238
  /* Manifest parsing — enforces the major-version hard block */
177
239
  /* ---------------------------------------------------------------------- */
@@ -218,6 +280,10 @@ function parseManifest(raw) {
218
280
  throw err;
219
281
  }
220
282
  }
283
+ const KIND_IMPORTERS = {
284
+ loop: (db, manifest, _blobsDir, asName) => importLoopBundle(db, manifest, asName),
285
+ bee: (db, manifest, blobsDir, asName) => importBeeBundle(db, manifest, blobsDir, asName),
286
+ };
221
287
  /* ---------------------------------------------------------------------- */
222
288
  /* Loop import */
223
289
  /* ---------------------------------------------------------------------- */
@@ -226,87 +292,85 @@ function importLoopBundle(db, manifest, asName) {
226
292
  const srcId = String(srcLoop.id ?? "");
227
293
  if (!srcId)
228
294
  throw new BundleMalformedError("loop_release is missing id");
295
+ const srcStatus = readSourceLifecycle(srcLoop);
296
+ enforceImportAsCandidate(srcStatus);
229
297
  const targetId = asName ?? srcId;
230
- // Hard-block per spec §7A.2 / AC-25: bundles MAY ONLY land as
231
- // candidate. A non-candidate source lifecycle is REFUSED; we
232
- // never silently coerce away a stable / retired source.
233
- const srcStatus = typeof srcLoop.lifecycle_status === "string"
234
- ? srcLoop.lifecycle_status
235
- : "candidate";
236
- if (srcStatus !== "candidate") {
237
- throw new BundleImportToStableForbiddenError(srcStatus);
238
- }
239
- // The reader always lands as candidate — the source's lifecycle
240
- // status is silently overridden (spec §7A.2 hard rule).
241
- const loopRow = {
298
+ const loopRow = buildLoopRow(srcLoop, targetId);
299
+ enforceImportAsCandidate(loopRow.lifecycle_status);
300
+ // First materialise related bee_release rows. The bundle's
301
+ // loop_bee_relations reference these bee ids; re-stamping the
302
+ // relations before the bee rows land would FK-constraint fail
303
+ // on the receiver.
304
+ const beeIdMap = materialiseRelatedBees(db, manifest.related_bee_releases);
305
+ const tx = db.transaction(() => {
306
+ insertLoopReleaseRow(db, loopRow);
307
+ insertLoopBeeRelations(db, manifest, beeIdMap, targetId);
308
+ });
309
+ tx();
310
+ return targetId;
311
+ }
312
+ /**
313
+ * Pure mapping from a raw loop_release row to the column-shape
314
+ * the receiver writes to `loop_release`. Split out so the SQL
315
+ * INSERT can stay isolated.
316
+ */
317
+ function buildLoopRow(srcLoop, targetId) {
318
+ return {
242
319
  id: targetId,
243
320
  name: srcLoop.name,
244
321
  scenario: srcLoop.scenario,
245
322
  trigger_policy: srcLoop.trigger_policy,
246
- success_criteria_json: JSON.stringify(srcLoop.success_criteria ?? []),
323
+ success_criteria_json: jsonArr(srcLoop.success_criteria),
247
324
  interaction_policy: srcLoop.interaction_policy,
248
325
  feedback_policy: srcLoop.feedback_policy,
249
326
  evolution_policy: srcLoop.evolution_policy,
250
- evaluator_policy_json: JSON.stringify(srcLoop.evaluator_policy ?? []),
251
- linked_bees_json: JSON.stringify(srcLoop.linked_bees ?? []),
252
- run_history_json: JSON.stringify(srcLoop.run_history ?? []),
253
- crystallization_evidence_json: JSON.stringify(srcLoop.crystallization_evidence ?? []),
327
+ evaluator_policy_json: jsonArr(srcLoop.evaluator_policy),
328
+ linked_bees_json: jsonArr(srcLoop.linked_bees),
329
+ run_history_json: jsonArr(srcLoop.run_history),
330
+ crystallization_evidence_json: jsonArr(srcLoop.crystallization_evidence),
331
+ // The reader always lands as candidate — the source's lifecycle
332
+ // status is silently overridden (spec §7A.2 hard rule).
254
333
  lifecycle_status: "candidate",
255
334
  version: srcLoop.version,
256
- schema_version: srcLoop.schema_version ?? PEAKS_BUNDLE_SCHEMA_VERSIONS.loop,
257
- archived_at: typeof srcLoop.archived_at === "string"
258
- ? srcLoop.archived_at
259
- : new Date().toISOString(),
260
- shareable: srcLoop.shareable === undefined ? 1 : srcLoop.shareable ? 1 : 0,
261
- share_excluded_paths: JSON.stringify(srcLoop.share_excluded_paths ?? []),
262
- desktop_visible: srcLoop.desktop_visible === undefined
263
- ? 1
264
- : srcLoop.desktop_visible
265
- ? 1
266
- : 0,
335
+ schema_version: typeof srcLoop.schema_version === "string" && srcLoop.schema_version.length > 0
336
+ ? srcLoop.schema_version
337
+ : PEAKS_BUNDLE_SCHEMA_VERSIONS.loop,
338
+ archived_at: isoOrNow(srcLoop.archived_at),
339
+ shareable: bool01(srcLoop.shareable, 1),
340
+ share_excluded_paths: jsonArr(srcLoop.share_excluded_paths),
341
+ desktop_visible: bool01(srcLoop.desktop_visible, 1),
267
342
  export_bundle_format: typeof srcLoop.export_bundle_format === "string"
268
343
  ? srcLoop.export_bundle_format
269
344
  : "peaks.bundle/1",
270
345
  };
271
- // Defense-in-depth: refuse if lifecycle was anything other than
272
- // candidate. (The schema parse above already pinned the constant,
273
- // but we layer this so any future schema relaxation cannot
274
- // bypass the import-to-candidate rule.)
275
- if (loopRow.lifecycle_status !== "candidate") {
276
- throw new BundleImportToStableForbiddenError(loopRow.lifecycle_status);
346
+ }
347
+ const LOOP_RELEASE_COLUMNS = [
348
+ "id", "name", "scenario", "trigger_policy",
349
+ "success_criteria_json", "interaction_policy", "feedback_policy", "evolution_policy",
350
+ "evaluator_policy_json", "linked_bees_json", "run_history_json", "crystallization_evidence_json",
351
+ "lifecycle_status", "version", "schema_version", "archived_at",
352
+ "shareable", "share_excluded_paths", "desktop_visible", "export_bundle_format",
353
+ ];
354
+ /** Insert one loop_release row (or replace on conflict). */
355
+ function insertLoopReleaseRow(db, row) {
356
+ const placeholders = LOOP_RELEASE_COLUMNS.map(() => "?").join(", ");
357
+ db.prepare(`INSERT OR REPLACE INTO loop_release (${LOOP_RELEASE_COLUMNS.join(", ")}) VALUES (${placeholders})`).run(...LOOP_RELEASE_COLUMNS.map((c) => row[c]));
358
+ }
359
+ /**
360
+ * Re-stamp relations (preserving source row content, but the
361
+ * loop_id is rewritten if `asName` was supplied).
362
+ */
363
+ function insertLoopBeeRelations(db, manifest, beeIdMap, loopReleaseId) {
364
+ const stmt = db.prepare(`INSERT OR REPLACE INTO loop_bee_relation (
365
+ loop_release_id, bee_release_id, role, reason, schema_version, created_at
366
+ ) VALUES (?, ?, ?, ?, ?, ?)`);
367
+ for (const rel of manifest.loop_bee_relations) {
368
+ const r = rel;
369
+ const originalBeeId = Number(r.bee_release_id);
370
+ const newBeeId = beeIdMap.get(originalBeeId) ?? originalBeeId;
371
+ stmt.run(loopReleaseId, newBeeId, str(r.role), str(r.reason), str(r.schema_version, PEAKS_BUNDLE_SCHEMA_VERSIONS.loop_bee_relation), isoOrNow(r.created_at));
277
372
  }
278
- // First materialise related bee_release rows. The bundle's
279
- // loop_bee_relations reference these bee ids; re-stamping the
280
- // relations before the bee rows land would FK-constraint fail
281
- // on the receiver.
282
- const beeIdMap = materialiseRelatedBees(db, manifest.related_bee_releases);
283
- const tx = db.transaction(() => {
284
- db.prepare(`INSERT OR REPLACE INTO loop_release (
285
- id, name, scenario, trigger_policy,
286
- success_criteria_json, interaction_policy, feedback_policy, evolution_policy,
287
- evaluator_policy_json, linked_bees_json, run_history_json, crystallization_evidence_json,
288
- lifecycle_status, version, schema_version, archived_at,
289
- shareable, share_excluded_paths, desktop_visible, export_bundle_format
290
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(loopRow.id, loopRow.name, loopRow.scenario, loopRow.trigger_policy, loopRow.success_criteria_json, loopRow.interaction_policy, loopRow.feedback_policy, loopRow.evolution_policy, loopRow.evaluator_policy_json, loopRow.linked_bees_json, loopRow.run_history_json, loopRow.crystallization_evidence_json, loopRow.lifecycle_status, loopRow.version, loopRow.schema_version, loopRow.archived_at, loopRow.shareable, loopRow.share_excluded_paths, loopRow.desktop_visible, loopRow.export_bundle_format);
291
- // Re-stamp relations (preserving source row content, but the
292
- // loop_id is rewritten if `asName` was supplied).
293
- for (const rel of manifest.loop_bee_relations) {
294
- const r = rel;
295
- const originalBeeId = Number(r.bee_release_id);
296
- const newBeeId = beeIdMap.get(originalBeeId) ?? originalBeeId;
297
- db.prepare(`INSERT OR REPLACE INTO loop_bee_relation (
298
- loop_release_id, bee_release_id, role, reason, schema_version, created_at
299
- ) VALUES (?, ?, ?, ?, ?, ?)`).run(asName ?? String(r.loop_release_id), newBeeId, String(r.role), String(r.reason ?? ""), String(r.schema_version ?? PEAKS_BUNDLE_SCHEMA_VERSIONS.loop_bee_relation), typeof r.created_at === "string"
300
- ? r.created_at
301
- : new Date().toISOString());
302
- }
303
- });
304
- tx();
305
- return targetId;
306
373
  }
307
- /* ---------------------------------------------------------------------- */
308
- /* Related-bees materialisation */
309
- /* ---------------------------------------------------------------------- */
310
374
  /**
311
375
  * Materialise the related_bee_releases rows in `db`. Returns a map
312
376
  * from source bee_release.id → new bee_release.id so the loop /
@@ -323,112 +387,122 @@ function materialiseRelatedBees(db, relatedBees) {
323
387
  if (relatedBees.length === 0)
324
388
  return map;
325
389
  for (const raw of relatedBees) {
326
- const obj = raw;
327
- const release = obj.bee_release ?? {};
328
- const srcId = Number(release.id);
329
- if (!Number.isInteger(srcId) || srcId <= 0)
330
- continue;
331
- const beeName = String(release.bee_name ?? "");
332
- if (!beeName)
333
- continue;
334
- // Honour the same non-candidate hard rule at the bee row.
335
- const srcStatus = typeof release.lifecycle_status === "string"
336
- ? release.lifecycle_status
337
- : "candidate";
338
- if (srcStatus !== "candidate") {
339
- throw new BundleImportToStableForbiddenError(srcStatus);
340
- }
341
- // Pre-existing receiver-side bee with the same name → keep its id.
342
- const existing = db
343
- .prepare("SELECT id FROM bee_release WHERE bee_name = ?")
344
- .get(beeName);
345
- let newId;
346
- if (existing) {
347
- newId = existing.id;
348
- }
349
- else {
350
- const ins = db.prepare(`INSERT INTO bee_release (
351
- bee_name, version, source, archived_at, archived_by, user_intent_raw,
352
- description, parent_version, changelog, shareable, desktop_visible
353
- ) VALUES (?, ?, 'user', ?, 'user', ?, ?, ?, ?, ?, ?)`);
354
- const info = ins.run(beeName, String(release.version ?? "0.0.0"), new Date().toISOString(), release.user_intent_raw ?? null, release.description ?? null, release.parent_version ?? null, release.changelog ?? null, release.shareable === false ? 0 : 1, release.desktop_visible === false ? 0 : 1);
355
- newId = Number(info.lastInsertRowid);
356
- if (obj.manifest) {
357
- const m = obj.manifest;
358
- db.prepare(`INSERT INTO bee_manifest (
359
- release_id, schema_version, description, segments_json,
360
- entrypoint_preamble, promotion, min_cycles,
361
- requires_human, requires_smoke, retire_on_misses
362
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newId, String(m.schema_version ?? "peaks.bee/1"), String(m.description ?? ""), JSON.stringify(m.segments_json ?? []), m.entrypoint_preamble ?? null, String(m.promotion ?? "manual"), m.min_cycles ?? null, m.requires_human === undefined ? 1 : Number(m.requires_human), m.requires_smoke === undefined ? 1 : Number(m.requires_smoke), m.retire_on_misses ?? null);
363
- }
364
- }
365
- map.set(srcId, newId);
390
+ const entry = raw;
391
+ const inserted = materialiseSingleRelatedBee(db, entry);
392
+ if (inserted !== null)
393
+ map.set(inserted.srcId, inserted.newId);
366
394
  }
367
395
  return map;
368
396
  }
397
+ /**
398
+ * Materialise one related bee. Returns null if the entry is not
399
+ * importable (missing id or bee_name). Otherwise returns the
400
+ * src→new id pair so the caller can record it in the redirect map.
401
+ */
402
+ function materialiseSingleRelatedBee(db, entry) {
403
+ const release = entry.bee_release ?? {};
404
+ const srcId = Number(release.id);
405
+ if (!Number.isInteger(srcId) || srcId <= 0)
406
+ return null;
407
+ const beeName = String(release.bee_name ?? "");
408
+ if (!beeName)
409
+ return null;
410
+ // Honour the same non-candidate hard rule at the bee row.
411
+ enforceImportAsCandidate(readSourceLifecycle(release));
412
+ // Pre-existing receiver-side bee with the same name → keep its id.
413
+ const existing = db
414
+ .prepare("SELECT id FROM bee_release WHERE bee_name = ?")
415
+ .get(beeName);
416
+ if (existing)
417
+ return { srcId, newId: existing.id };
418
+ return { srcId, newId: insertNewRelatedBee(db, release, entry.manifest) };
419
+ }
420
+ function insertNewRelatedBee(db, release, manifest) {
421
+ const info = db
422
+ .prepare(`INSERT INTO bee_release (
423
+ bee_name, version, source, archived_at, archived_by, user_intent_raw,
424
+ description, parent_version, changelog, shareable, desktop_visible
425
+ ) VALUES (?, ?, 'user', ?, 'user', ?, ?, ?, ?, ?, ?)`)
426
+ .run(String(release.bee_name ?? ""), String(release.version ?? "0.0.0"), nowIso(), release.user_intent_raw ?? null, release.description ?? null, release.parent_version ?? null, release.changelog ?? null, release.shareable === false ? 0 : 1, release.desktop_visible === false ? 0 : 1);
427
+ const newId = Number(info.lastInsertRowid);
428
+ if (manifest)
429
+ insertBeeManifestRow(db, newId, manifest);
430
+ return newId;
431
+ }
369
432
  /* ---------------------------------------------------------------------- */
370
433
  /* Bee import */
371
434
  /* ---------------------------------------------------------------------- */
372
435
  function importBeeBundle(db, manifest, _blobsDir, asName) {
373
- if (!manifest.bee_release) {
436
+ const beeObj = manifest.bee_release;
437
+ if (!beeObj) {
374
438
  throw new BundleMalformedError("bee_release payload missing from bee bundle");
375
439
  }
376
- const beeObj = manifest.bee_release;
377
440
  const release = beeObj.bee_release;
378
441
  const newBeeName = asName ?? String(release.bee_name ?? "");
379
442
  if (!newBeeName)
380
443
  throw new BundleMalformedError("bee_release.bee_name missing");
381
444
  // Same hard rule: any non-candidate import is refused. The reader
382
445
  // does not honor --as-stable switches.
383
- const srcStatus = typeof release.lifecycle_status === "string"
384
- ? release.lifecycle_status
385
- : "candidate";
386
- if (srcStatus !== "candidate") {
387
- throw new BundleImportToStableForbiddenError(srcStatus);
388
- }
389
- let newId = -1;
446
+ enforceImportAsCandidate(readSourceLifecycle(release));
390
447
  const tx = db.transaction(() => {
391
- const ins = db.prepare(`INSERT INTO bee_release (
448
+ const newId = insertAnchorBeeRelease(db, release, newBeeName);
449
+ if (beeObj.manifest)
450
+ insertBeeManifestRow(db, newId, beeObj.manifest);
451
+ insertBeeSegmentRows(db, newId, beeObj.segments ?? []);
452
+ insertBeeFileRows(db, newId, newBeeName, beeObj.files ?? []);
453
+ insertBeeChangeRows(db, newId, beeObj.changes ?? []);
454
+ return newId;
455
+ });
456
+ return tx();
457
+ }
458
+ /** Insert the anchor bee_release row for a `bee` bundle and return its new id. */
459
+ function insertAnchorBeeRelease(db, release, beeName) {
460
+ const info = db
461
+ .prepare(`INSERT INTO bee_release (
392
462
  bee_name, version, source, archived_at, archived_by, user_intent_raw,
393
463
  description, parent_version, changelog, shareable, desktop_visible
394
- ) VALUES (?, ?, 'user', ?, 'user', ?, ?, ?, ?, ?, ?)`);
395
- const info = ins.run(newBeeName, String(release.version ?? "0.0.0"), new Date().toISOString(), release.user_intent_raw ?? null, release.description ?? null, release.parent_version ?? null, release.changelog ?? null, release.shareable === false ? 0 : 1, release.desktop_visible === false ? 0 : 1);
396
- newId = Number(info.lastInsertRowid);
397
- if (beeObj.manifest) {
398
- const m = beeObj.manifest;
399
- db.prepare(`INSERT INTO bee_manifest (
400
- release_id, schema_version, description, segments_json,
401
- entrypoint_preamble, promotion, min_cycles,
402
- requires_human, requires_smoke, retire_on_misses
403
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newId, String(m.schema_version ?? "peaks.bee/1"), String(m.description ?? ""), JSON.stringify(m.segments_json ?? []), m.entrypoint_preamble ?? null, String(m.promotion ?? "manual"), m.min_cycles ?? null, m.requires_human === undefined ? 1 : Number(m.requires_human), m.requires_smoke === undefined ? 1 : Number(m.requires_smoke), m.retire_on_misses ?? null);
404
- }
405
- for (const s of beeObj.segments ?? []) {
406
- db.prepare(`INSERT INTO bee_segment_ref (
407
- release_id, segment_name, inputs_json, outputs_json, side_effects
408
- ) VALUES (?, ?, ?, ?, ?)`).run(newId, String(s.segment_name ?? ""), s.inputs_json ?? null, s.outputs_json ?? null, s.side_effects ?? null);
409
- }
410
- for (const f of beeObj.files ?? []) {
411
- db.prepare(`INSERT INTO bee_file (
412
- release_id, owner_kind, owner_name, path, kind, size_bytes, sha256, blob_path
413
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(newId, String(f.owner_kind ?? "bee"), newBeeName, String(f.path ?? ""), String(f.kind ?? "other"), Number(f.size_bytes ?? 0), String(f.sha256 ?? ""), String(f.blob_path ?? ""));
414
- }
415
- for (const c of beeObj.changes ?? []) {
416
- db.prepare(`INSERT INTO bee_change (
417
- release_id, change_kind, target_kind, target_name, detail
418
- ) VALUES (?, ?, ?, ?, ?)`).run(newId, String(c.change_kind ?? ""), String(c.target_kind ?? ""), String(c.target_name ?? ""), c.detail ?? null);
419
- }
420
- });
421
- tx();
422
- return newId;
464
+ ) VALUES (?, ?, 'user', ?, 'user', ?, ?, ?, ?, ?, ?)`)
465
+ .run(beeName, String(release.version ?? "0.0.0"), nowIso(), release.user_intent_raw ?? null, release.description ?? null, release.parent_version ?? null, release.changelog ?? null, release.shareable === false ? 0 : 1, release.desktop_visible === false ? 0 : 1);
466
+ return Number(info.lastInsertRowid);
423
467
  }
424
- /* ---------------------------------------------------------------------- */
425
- /* Utility */
426
- /* ---------------------------------------------------------------------- */
427
- function readDirEntries(dir) {
428
- try {
429
- return readdirSync(dir);
468
+ /** Insert one bee_manifest row. */
469
+ function insertBeeManifestRow(db, releaseId, manifest) {
470
+ db.prepare(`INSERT INTO bee_manifest (
471
+ release_id, schema_version, description, segments_json,
472
+ entrypoint_preamble, promotion, min_cycles,
473
+ requires_human, requires_smoke, retire_on_misses
474
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(releaseId, String(manifest.schema_version ?? "peaks.bee/1"), String(manifest.description ?? ""), JSON.stringify(manifest.segments_json ?? []), strOrNull(manifest.entrypoint_preamble), String(manifest.promotion ?? "manual"), manifest.min_cycles ?? null, manifest.requires_human === undefined ? 1 : Number(manifest.requires_human), manifest.requires_smoke === undefined ? 1 : Number(manifest.requires_smoke), manifest.retire_on_misses ?? null);
475
+ }
476
+ /** Insert bee_segment_ref rows. */
477
+ function insertBeeSegmentRows(db, releaseId, segments) {
478
+ if (segments.length === 0)
479
+ return;
480
+ const stmt = db.prepare(`INSERT INTO bee_segment_ref (
481
+ release_id, segment_name, inputs_json, outputs_json, side_effects
482
+ ) VALUES (?, ?, ?, ?, ?)`);
483
+ for (const s of segments) {
484
+ stmt.run(releaseId, String(s.segment_name ?? ""), strOrNull(s.inputs_json), strOrNull(s.outputs_json), strOrNull(s.side_effects));
430
485
  }
431
- catch {
432
- return [];
486
+ }
487
+ /** Insert bee_file rows. */
488
+ function insertBeeFileRows(db, releaseId, beeName, files) {
489
+ if (files.length === 0)
490
+ return;
491
+ const stmt = db.prepare(`INSERT INTO bee_file (
492
+ release_id, owner_kind, owner_name, path, kind, size_bytes, sha256, blob_path
493
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
494
+ for (const f of files) {
495
+ stmt.run(releaseId, String(f.owner_kind ?? "bee"), beeName, String(f.path ?? ""), String(f.kind ?? "other"), Number(f.size_bytes ?? 0), String(f.sha256 ?? ""), String(f.blob_path ?? ""));
496
+ }
497
+ }
498
+ /** Insert bee_change rows. */
499
+ function insertBeeChangeRows(db, releaseId, changes) {
500
+ if (changes.length === 0)
501
+ return;
502
+ const stmt = db.prepare(`INSERT INTO bee_change (
503
+ release_id, change_kind, target_kind, target_name, detail
504
+ ) VALUES (?, ?, ?, ?, ?)`);
505
+ for (const c of changes) {
506
+ stmt.run(releaseId, String(c.change_kind ?? ""), String(c.target_kind ?? ""), String(c.target_name ?? ""), strOrNull(c.detail));
433
507
  }
434
508
  }
@@ -129,6 +129,39 @@ export type PeaksHookEntry = {
129
129
  command: string;
130
130
  event: string;
131
131
  };
132
+ /**
133
+ * Compute the per-IDE peaks hook entries to merge into the settings file.
134
+ * Replaces the slice #1 hardcoded `PEAKS_HOOK_ENTRIES` constant; the constant
135
+ * remains exported (computed for claude-code) for backward compat.
136
+ *
137
+ * Slice #013 (`--no-progress` flag): when `skipProgress` is true, the
138
+ * progress-start entry is omitted from the returned list. Install with this
139
+ * flag will (a) NOT emit the progress hook entry, and (b) will idempotently
140
+ * remove any previously-installed progress entry (sentinel-based merge).
141
+ *
142
+ * Slice #014 (refactor — full removal): only the gate-enforce entry is
143
+ * ever emitted. The `skipProgress` parameter is kept for API stability
144
+ * but is a no-op — the returned list is always single-entry. The progress
145
+ * entry's sentinel is included in the legacy sentinel set so uninstall
146
+ * can find + remove any stale progress-start entry that an older
147
+ * `peaks hooks install` may have written before this slice.
148
+ *
149
+ * Slice 2026-08-06-session-outer-cache: a SessionStart outer-cache primer
150
+ * entry is appended for Claude Code only (SessionStart is a Claude-Code-
151
+ * specific event key today).
152
+ */
153
+ /**
154
+ * Slice 2026-08-06-codegate-vendor-neutral — code-gate hook entry.
155
+ * Vendor-neutral command (`peaks code-gate --json`); the CLI adapter
156
+ * lives in `src/cli/commands/code-gate-command.ts` and the decision
157
+ * logic lives in `src/services/hooks/pre-tool-code-gate.ts`. The
158
+ * shell-script sibling (`src/services/hooks/pre-tool-code-gate.sh`)
159
+ * encodes the same logic for non-Node harnesses.
160
+ */
161
+ export declare const HOOK_CODE_GATE_SENTINEL = "peaks code-gate";
162
+ export declare const HOOK_CODE_GATE_MATCHER = "Edit|Write|MultiEdit";
163
+ export declare const HOOK_CODE_GATE_EVENT = "PreToolUse";
164
+ export declare const HOOK_CODE_GATE_COMMAND = "peaks code-gate --json";
132
165
  /**
133
166
  * The set of Skills whose use is denied at the IDE permissions layer for
134
167
  * every `peaks hooks install` call. Each entry is the namespace-qualified