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.
- package/CHANGELOG.md +55 -0
- package/README-en.md +12 -9
- package/README.md +15 -4
- package/config/eslint/.peaks-rules.cjs +39 -26
- package/dist/cli/commands/_register.js +2 -1
- package/dist/cli/commands/audit-commands.d.ts +1 -1
- package/dist/cli/commands/code-gate-command.d.ts +25 -0
- package/dist/cli/commands/code-gate-command.js +86 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -4
- package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
- package/dist/cli/commands/cron-commands.js +16 -4
- package/dist/cli/commands/dashboard-long-run.js +8 -4
- package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
- package/dist/cli/commands/hooks-commands.js +56 -3
- package/dist/cli/commands/primer-command.d.ts +59 -0
- package/dist/cli/commands/primer-command.js +160 -0
- package/dist/cli/commands/qa-commands.js +1 -2
- package/dist/cli/commands/sediment-commands.d.ts +1 -1
- package/dist/cli/commands/slice-commands.js +20 -8
- package/dist/cli/commands/statusline-commands.d.ts +1 -0
- package/dist/cli/commands/statusline-commands.js +17 -3
- package/dist/cli/commands/tech-commands.d.ts +1 -1
- package/dist/cli/program.js +8 -0
- package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
- package/dist/services/artifacts/request-artifact-service.js +10 -1
- package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
- package/dist/services/code/auto-compact-orchestrator.js +9 -1
- package/dist/services/code/batch-heartbeat-poller.js +26 -0
- package/dist/services/code/orchestrator-can-do.d.ts +24 -0
- package/dist/services/code/orchestrator-can-do.js +48 -2
- package/dist/services/code/post-compact-detector.d.ts +1 -1
- package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
- package/dist/services/config/config-safety.d.ts +23 -0
- package/dist/services/config/config-safety.js +62 -0
- package/dist/services/config/config-service.d.ts +1 -1
- package/dist/services/config/config-service.js +2 -4
- package/dist/services/container/container-lease.js +21 -8
- package/dist/services/context/spillover-store.d.ts +1 -1
- package/dist/services/crystallization/crystallization-types.js +20 -8
- package/dist/services/crystallization/evidence-brief-builder.js +22 -11
- package/dist/services/dispatch/dispatch-record-writer.js +231 -141
- package/dist/services/evolution/evolution-types.js +38 -21
- package/dist/services/feedback/feedback-promotion-service.js +9 -1
- package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
- package/dist/services/hooks/pre-tool-code-gate.js +91 -0
- package/dist/services/ide/current-model-detector.js +1 -2
- package/dist/services/lint/detect-eslint.js +8 -5
- package/dist/services/lint/eslint-runner.d.ts +1 -2
- package/dist/services/lint/eslint-runner.js +86 -23
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +47 -0
- package/dist/services/loop/loop-release-types.js +32 -13
- package/dist/services/loop/spec-service.d.ts +7 -1
- package/dist/services/loop/spec-service.js +218 -182
- package/dist/services/rd/rd-service.js +22 -9
- package/dist/services/scan/archetype-service.js +21 -9
- package/dist/services/session/binding-store.js +1 -2
- package/dist/services/share/bundle-reader.d.ts +6 -9
- package/dist/services/share/bundle-reader.js +297 -223
- package/dist/services/skills/hooks-settings-service.d.ts +33 -0
- package/dist/services/skills/hooks-settings-service.js +48 -3
- package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
- package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
- package/dist/services/skills/skill-statusline-renderer.js +96 -8
- package/dist/services/skills/skill-statusline-service.d.ts +41 -0
- package/dist/services/skills/skill-statusline-service.js +56 -6
- package/dist/services/slice/calibration-store.js +21 -8
- package/dist/services/slice/slice-check-service.js +35 -14
- package/dist/services/slice/slice-decompose-service.js +459 -223
- package/dist/services/standards/project-context.js +135 -73
- package/dist/services/verdict/envelopes.d.ts +1 -2
- package/dist/services/verdict/envelopes.js +1 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
- package/dist/services/vm/vm-lease.js +19 -8
- package/dist/services/worktree/worktree-lease.d.ts +0 -10
- package/dist/services/worktree/worktree-lease.js +19 -8
- package/dist/shared/fs-utils.d.ts +1 -1
- package/package.json +7 -4
- 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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
175
|
+
if (existsSync(stageDir))
|
|
114
176
|
rmSync(stageDir, { recursive: true, force: true });
|
|
115
177
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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:
|
|
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:
|
|
251
|
-
linked_bees_json:
|
|
252
|
-
run_history_json:
|
|
253
|
-
crystallization_evidence_json:
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
shareable: srcLoop.shareable
|
|
261
|
-
share_excluded_paths:
|
|
262
|
-
desktop_visible: srcLoop.desktop_visible
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|