peaks-loop 4.0.0-beta → 4.0.0-beta.3

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 (179) hide show
  1. package/CHANGELOG.md +105 -87
  2. package/README-en.md +17 -4
  3. package/README.md +17 -4
  4. package/dist/src/cli/commands/asset-commands.d.ts +45 -0
  5. package/dist/src/cli/commands/asset-commands.js +404 -0
  6. package/dist/src/cli/commands/bee-commands.d.ts +26 -0
  7. package/dist/src/cli/commands/bee-commands.js +169 -0
  8. package/dist/src/cli/commands/{solo-commands.d.ts → code-commands.d.ts} +24 -24
  9. package/dist/src/cli/commands/{solo-commands.js → code-commands.js} +78 -78
  10. package/dist/src/cli/commands/core/skill-command.js +1 -1
  11. package/dist/src/cli/commands/dispatch-from-dag.js +1 -1
  12. package/dist/src/cli/commands/evolution-commands.d.ts +27 -0
  13. package/dist/src/cli/commands/evolution-commands.js +393 -0
  14. package/dist/src/cli/commands/hook-handle.js +7 -7
  15. package/dist/src/cli/commands/job-commands.js +2 -2
  16. package/dist/src/cli/commands/loop-commands.d.ts +19 -13
  17. package/dist/src/cli/commands/loop-commands.js +147 -13
  18. package/dist/src/cli/commands/sediment-commands.js +9 -0
  19. package/dist/src/cli/commands/session-auto-compact-hook-command.js +2 -2
  20. package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.d.ts +67 -0
  21. package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.js +173 -0
  22. package/dist/src/cli/commands/sub-agent-shared.d.ts +1 -1
  23. package/dist/src/cli/commands/user-touchpoint-commands.d.ts +4 -4
  24. package/dist/src/cli/commands/user-touchpoint-commands.js +14 -14
  25. package/dist/src/cli/commands/workflow-commands.js +22 -22
  26. package/dist/src/cli/program.js +23 -6
  27. package/dist/src/services/artifacts/repair-cycle-service.js +1 -1
  28. package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +1 -1
  29. package/dist/src/services/audit/enforcers/active-skill-resolver.js +1 -1
  30. package/dist/src/services/audit/enforcers/{solo-code-ban.d.ts → code-ban.d.ts} +6 -6
  31. package/dist/src/services/audit/enforcers/{solo-code-ban.js → code-ban.js} +6 -6
  32. package/dist/src/services/audit/red-line-catalog.d.ts +2 -2
  33. package/dist/src/services/audit/red-line-catalog.js +5 -5
  34. package/dist/src/services/audit/types.d.ts +2 -2
  35. package/dist/src/services/{solo → code}/auto-compact-orchestrator.d.ts +1 -1
  36. package/dist/src/services/{solo → code}/auto-compact-orchestrator.js +3 -3
  37. package/dist/src/services/{solo → code}/dag-orchestrator.d.ts +1 -1
  38. package/dist/src/services/{solo → code}/dag-orchestrator.js +1 -1
  39. package/dist/src/services/{solo → code}/emit-handoff.d.ts +1 -1
  40. package/dist/src/services/{solo → code}/emit-handoff.js +1 -1
  41. package/dist/src/services/{solo → code}/job-shape-decision.d.ts +1 -1
  42. package/dist/src/services/{solo → code}/job-shape-decision.js +5 -5
  43. package/dist/src/services/{solo → code}/mode-gate.d.ts +9 -9
  44. package/dist/src/services/{solo → code}/mode-gate.js +5 -5
  45. package/dist/src/services/{solo → code}/post-compact-detector.d.ts +3 -3
  46. package/dist/src/services/{solo → code}/post-compact-detector.js +4 -4
  47. package/dist/src/services/{solo → code}/step-08-gate.d.ts +4 -4
  48. package/dist/src/services/{solo → code}/step-08-gate.js +4 -4
  49. package/dist/src/services/{solo → code}/user-touchpoint-classifier.d.ts +1 -1
  50. package/dist/src/services/{solo → code}/user-touchpoint-classifier.js +1 -1
  51. package/dist/src/services/context/auto-compact-dispatcher.d.ts +2 -2
  52. package/dist/src/services/context/auto-compact-dispatcher.js +2 -2
  53. package/dist/src/services/context/auto-compact-types.d.ts +1 -1
  54. package/dist/src/services/crystallization/crystallization-service.d.ts +239 -0
  55. package/dist/src/services/crystallization/crystallization-service.js +397 -0
  56. package/dist/src/services/crystallization/crystallization-store.d.ts +75 -0
  57. package/dist/src/services/crystallization/crystallization-store.js +193 -0
  58. package/dist/src/services/crystallization/crystallization-types.d.ts +313 -0
  59. package/dist/src/services/crystallization/crystallization-types.js +299 -0
  60. package/dist/src/services/crystallization/evidence-brief-builder.d.ts +237 -0
  61. package/dist/src/services/crystallization/evidence-brief-builder.js +240 -0
  62. package/dist/src/services/dispatch/slice-dag.d.ts +2 -2
  63. package/dist/src/services/dispatch/test-tool-detection.d.ts +1 -1
  64. package/dist/src/services/dispatch/test-tool-detection.js +1 -1
  65. package/dist/src/services/evolution/evolution-service.d.ts +137 -0
  66. package/dist/src/services/evolution/evolution-service.js +281 -0
  67. package/dist/src/services/evolution/evolution-store.d.ts +57 -0
  68. package/dist/src/services/evolution/evolution-store.js +232 -0
  69. package/dist/src/services/evolution/evolution-types.d.ts +706 -0
  70. package/dist/src/services/evolution/evolution-types.js +286 -0
  71. package/dist/src/services/evolution/independent-evaluator-runner.d.ts +90 -0
  72. package/dist/src/services/evolution/independent-evaluator-runner.js +106 -0
  73. package/dist/src/services/evolution/regression-skeptic-runner.d.ts +64 -0
  74. package/dist/src/services/evolution/regression-skeptic-runner.js +113 -0
  75. package/dist/src/services/feedback/feedback-promotion-service.js +2 -2
  76. package/dist/src/services/job/job-progress-store.d.ts +1 -1
  77. package/dist/src/services/job/job-progress-store.js +1 -1
  78. package/dist/src/services/loop/loop-bee-relation-service.d.ts +107 -0
  79. package/dist/src/services/loop/loop-bee-relation-service.js +211 -0
  80. package/dist/src/services/loop/loop-bee-relation-store.d.ts +53 -0
  81. package/dist/src/services/loop/loop-bee-relation-store.js +130 -0
  82. package/dist/src/services/loop/loop-bee-relation-types.d.ts +118 -0
  83. package/dist/src/services/loop/loop-bee-relation-types.js +114 -0
  84. package/dist/src/services/loop/loop-release-service.d.ts +46 -0
  85. package/dist/src/services/loop/loop-release-service.js +66 -0
  86. package/dist/src/services/loop/loop-release-store.d.ts +55 -0
  87. package/dist/src/services/loop/loop-release-store.js +134 -0
  88. package/dist/src/services/loop/loop-release-types.d.ts +250 -0
  89. package/dist/src/services/loop/loop-release-types.js +162 -0
  90. package/dist/src/services/preferences/preferences-service.d.ts +1 -1
  91. package/dist/src/services/preferences/preferences-types.d.ts +1 -1
  92. package/dist/src/services/rd/standards-diagnostic.d.ts +1 -1
  93. package/dist/src/services/rd/standards-diagnostic.js +2 -2
  94. package/dist/src/services/recommendations/capability-seed-items.js +1 -1
  95. package/dist/src/services/recommendations/capability-seed-mappings.js +1 -1
  96. package/dist/src/services/recommendations/recommendation-service.js +1 -1
  97. package/dist/src/services/refactor/refactor-service.d.ts +1 -1
  98. package/dist/src/services/share/bundle-reader.d.ts +100 -0
  99. package/dist/src/services/share/bundle-reader.js +434 -0
  100. package/dist/src/services/share/bundle-types.d.ts +392 -0
  101. package/dist/src/services/share/bundle-types.js +318 -0
  102. package/dist/src/services/share/bundle-writer.d.ts +97 -0
  103. package/dist/src/services/share/bundle-writer.js +429 -0
  104. package/dist/src/services/share/run-state-contract.d.ts +133 -0
  105. package/dist/src/services/share/run-state-contract.js +138 -0
  106. package/dist/src/services/signal/cancel-handler.js +3 -3
  107. package/dist/src/services/skill/resume-detector.js +1 -1
  108. package/dist/src/services/skillhub/release-import.js +7 -2
  109. package/dist/src/services/skillhub/release-retain.js +6 -1
  110. package/dist/src/services/skillhub/sqlite-store.js +17 -1
  111. package/dist/src/services/skillhub/types.d.ts +10 -0
  112. package/dist/src/services/skills/skill-presence-service.d.ts +1 -1
  113. package/dist/src/services/standards/loop-engineering-lint.d.ts +38 -0
  114. package/dist/src/services/standards/loop-engineering-lint.js +101 -0
  115. package/dist/src/services/standards/loop-engineering-readiness-lint.d.ts +111 -0
  116. package/dist/src/services/standards/loop-engineering-readiness-lint.js +248 -0
  117. package/dist/src/services/upgrade/1x-detector-service.js +1 -1
  118. package/dist/src/services/workflow/pipeline-verify-service.d.ts +1 -1
  119. package/dist/src/services/workflow/pipeline-verify-service.js +3 -3
  120. package/dist/src/services/workflow/workflow-autonomous-service.d.ts +3 -3
  121. package/dist/src/services/workflow/workflow-autonomous-service.js +2 -2
  122. package/dist/src/services/workflow/workflow-router-service.d.ts +6 -6
  123. package/dist/src/services/workflow/workflow-router-service.js +33 -33
  124. package/dist/src/services/workspace/claude-settings-template.d.ts +1 -1
  125. package/dist/src/services/workspace/claude-settings-template.js +5 -5
  126. package/dist/src/shared/version.d.ts +1 -1
  127. package/dist/src/shared/version.js +1 -1
  128. package/examples/video-demo/README.md +57 -0
  129. package/examples/video-demo/out/en-closing-960.png +0 -0
  130. package/examples/video-demo/out/zh-closing-960.png +0 -0
  131. package/examples/video-demo/preview/peaks-loop-demo-en.mp4 +0 -0
  132. package/examples/video-demo/preview/peaks-loop-demo.mp4 +0 -0
  133. package/output-styles/peaks-skill-swarm.md +3 -3
  134. package/package.json +6 -2
  135. package/skills/bee/peaks-qa/SKILL.md +1 -1
  136. package/skills/bee/peaks-qa/references/artifact-per-request.md +3 -3
  137. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +6 -6
  138. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +1 -1
  139. package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
  140. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +11 -11
  141. package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
  142. package/skills/bee/peaks-sc/SKILL.md +1 -1
  143. package/skills/bee/peaks-txt/SKILL.md +3 -3
  144. package/skills/bee/peaks-ui/SKILL.md +8 -8
  145. package/skills/peaks-code/SKILL.md +34 -21
  146. package/skills/peaks-code/references/anchoring-and-session-info.md +2 -2
  147. package/skills/peaks-code/references/artifact-contracts.md +1 -1
  148. package/skills/peaks-code/references/boundaries.md +3 -3
  149. package/skills/peaks-code/references/codegraph-orchestration.md +1 -1
  150. package/skills/peaks-code/references/dag-orchestrator.md +7 -7
  151. package/skills/peaks-code/references/external-references.md +2 -2
  152. package/skills/peaks-code/references/fanout-mandatory.md +3 -3
  153. package/skills/peaks-code/references/fast-mode.md +2 -2
  154. package/skills/peaks-code/references/frontend-only-mode.md +9 -9
  155. package/skills/peaks-code/references/job-loop.md +1 -1
  156. package/skills/peaks-code/references/local-artifact-workspace.md +2 -2
  157. package/skills/peaks-code/references/micro-cycle.md +14 -14
  158. package/skills/peaks-code/references/mode-selection-with-stale-presence.md +5 -5
  159. package/skills/peaks-code/references/mode-selection.md +3 -3
  160. package/skills/peaks-code/references/openspec-workflow.md +16 -16
  161. package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
  162. package/skills/peaks-code/references/quality-gate-cheatsheet.md +1 -1
  163. package/skills/peaks-code/references/refactor-mode.md +1 -1
  164. package/skills/peaks-code/references/runbook.md +15 -15
  165. package/skills/peaks-code/references/step-0-55-1x-detection.md +1 -1
  166. package/skills/peaks-code/references/step-0-8-gate.md +11 -11
  167. package/skills/peaks-code/references/sub-agent-dispatch.md +8 -8
  168. package/skills/peaks-code/references/swarm-dispatch-contract.md +17 -17
  169. package/skills/peaks-code/references/workflow-gates-and-types.md +2 -2
  170. package/skills/peaks-code/references/workflow.md +5 -5
  171. package/skills/peaks-content/SKILL.md +272 -0
  172. package/skills/peaks-issue-fix-orchestrator/SKILL.md +307 -0
  173. package/skills/peaks-resume/SKILL.md +2 -2
  174. package/skills/peaks-status/SKILL.md +2 -2
  175. package/skills/peaks-test/SKILL.md +2 -2
  176. /package/dist/src/services/{solo → code}/batch-heartbeat-poller.d.ts +0 -0
  177. /package/dist/src/services/{solo → code}/batch-heartbeat-poller.js +0 -0
  178. /package/dist/src/services/{solo → code}/status-line-renderer.d.ts +0 -0
  179. /package/dist/src/services/{solo → code}/status-line-renderer.js +0 -0
@@ -0,0 +1,397 @@
1
+ import { z, ZodError } from "zod";
2
+ import { CrystallizationEventSchema, EvidenceBriefSchema, } from "./crystallization-types.js";
3
+ import { BriefSectionError } from "./evidence-brief-builder.js";
4
+ import { CRYSTALLIZATION_SCHEMA_VERSION, ensureCrystallizationEventTable, getCrystallizationEvent, listCrystallizationEvents, newCrystallizationId, updateCrystallizationEventStatus, } from "./crystallization-store.js";
5
+ import { insertLoopRelease } from "../loop/loop-release-store.js";
6
+ import { LoopReleaseSchema } from "../loop/loop-release-types.js";
7
+ import { insertLoopBeeRelation } from "../loop/loop-bee-relation-store.js";
8
+ import { LoopBeeRelationSchema, } from "../loop/loop-bee-relation-types.js";
9
+ /**
10
+ * CrystallizationService — spec §5 (post-run crystallization)
11
+ * + §4.5 + §4.7 + §10 RL-2 / RL-3 / RL-7.
12
+ *
13
+ * Hard rules enforced at THIS layer (defense in depth; the Zod
14
+ * schemas above and the brief refine guard add the second wall):
15
+ *
16
+ * - AC-4 / RL-2: a crystallization_event is written only when the
17
+ * candidate task has `task_status === 'completed'` AND
18
+ * `gates_passed === true` AND `evidence_collected === true`.
19
+ * Any pre-run attempt (status !== 'completed') is rejected
20
+ * with `CRYSTALLIZATION_PRE_RUN`. This is the pre-run block
21
+ * enforced by AC-4, AC-5, AC-6, AC-15, AC-16, AC-17 of the M5
22
+ * slice.
23
+ * - AC-6 / RL-3: writing a (loop_release, main_bee_release,
24
+ * loop_bee_relation) pair happens in a SINGLE TRANSACTION. The
25
+ * brief persist on crystallization_event also lives inside
26
+ * the same transaction (the brief is the durable evidence;
27
+ * dropping either side would break consistency).
28
+ * - AC-7 / RL-1: this layer never asks the user to hand-author
29
+ * JSON / manifest / CLI verb; all inputs are typed payloads
30
+ * the LLM submits on the user's behalf.
31
+ * - AC-15 / RL-7: the brief is mandatory; the
32
+ * `EvidenceBriefSchema.refine` guard rejects partial briefs
33
+ * at the parse boundary.
34
+ * - AC-16 / RL-7: the brief has all 4 sections
35
+ * (what_happened / why_it_matters / what_learned /
36
+ * what_action). The service throws `BriefSectionError` if the
37
+ * payload fails the guard.
38
+ *
39
+ * Sub-domain error codes:
40
+ *
41
+ * - CRYSTALLIZATION_PRE_RUN: task_status / gates_passed /
42
+ * evidence_collected precondition failed.
43
+ * - CRYSTALLIZATION_INVALID_TASK_STATE: a task input field is
44
+ * malformed (e.g. unknown task_status).
45
+ * - MISSING_BRIEF_SECTION: a brief is missing at least one of
46
+ * its 4 sections (raised from the evidence-brief-builder
47
+ * refine; surfaced as-is by the service).
48
+ * - CRYSTALLIZATION_TX_FAILED: a transaction-side effect failed
49
+ * (FK constraint, duplicate key, …). The service re-raises
50
+ * richer domain errors first; this is the catch-all for
51
+ * unexpected SQL errors.
52
+ */
53
+ /* ---------------------------------------------------------------------- */
54
+ /* Pre-run task gate — AC-4 / RL-2 */
55
+ /* ---------------------------------------------------------------------- */
56
+ export const CRYSTALLIZATION_TASK_STATUSES = [
57
+ "completed",
58
+ "in_progress",
59
+ "scratch",
60
+ "blocked",
61
+ "failed",
62
+ ];
63
+ /**
64
+ * The pre-run task state the service requires. Crystallization is
65
+ * GATED on these three flags. The validator rejects any input that
66
+ * does not match. Adding a new flag means a schema-version bump; for
67
+ * now the three are required.
68
+ */
69
+ export const CrystallizationTaskStateSchema = z.object({
70
+ task_id: z.string().trim().min(1).max(256),
71
+ task_status: z.literal("completed", {
72
+ errorMap: () => ({
73
+ message: "task_status must be 'completed' for crystallization (AC-4 / RL-2)",
74
+ }),
75
+ }),
76
+ gates_passed: z.literal(true, {
77
+ errorMap: () => ({
78
+ message: "gates_passed must be true for crystallization (AC-4 / RL-2)",
79
+ }),
80
+ }),
81
+ evidence_collected: z.literal(true, {
82
+ errorMap: () => ({
83
+ message: "evidence_collected must be true for crystallization (AC-4 / RL-2)",
84
+ }),
85
+ }),
86
+ });
87
+ export class CrystallizationIntegrityError extends Error {
88
+ code;
89
+ findings;
90
+ constructor(code, message, findings = []) {
91
+ super(message);
92
+ this.name = "CrystallizationIntegrityError";
93
+ this.code = code;
94
+ this.findings = findings;
95
+ }
96
+ }
97
+ /**
98
+ * Minimum bee_release payload the service needs to create a main bee
99
+ * alongside a loop. The full bee_release schema (see
100
+ * `src/services/skillhub/release-retain.ts`) carries manifest rows,
101
+ * segment refs, file rows, etc.; M5 only writes the bee_release header
102
+ * + a single bee_manifest row sufficient to keep the 4.x FK
103
+ * constraints green. Full bee retaining lands in a future slice.
104
+ *
105
+ * Schema definition is hoisted above the interface so the typed
106
+ * interface can reference the inferred Zod type without forward-
107
+ * reference gymnastics.
108
+ */
109
+ const BeeNameSchema = z
110
+ .string()
111
+ .trim()
112
+ .min(1, "bee_name is required")
113
+ .max(200)
114
+ .regex(/^[a-z0-9][a-z0-9._-]*$/, {
115
+ message: "bee_name must be kebab-/snake-case starting with a lowercase letter or digit",
116
+ });
117
+ const BeeCrystallizeInputSchema = z.object({
118
+ bee_name: BeeNameSchema,
119
+ version: z
120
+ .string()
121
+ .regex(/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/, {
122
+ message: "version must be a semver string (e.g. 0.1.0)",
123
+ }),
124
+ description: z.string().trim().min(1).max(4000),
125
+ user_intent_raw: z.string().trim().max(4000).optional(),
126
+ parent_version: z.string().trim().max(64).optional(),
127
+ changelog: z.string().trim().max(8000).optional(),
128
+ });
129
+ /**
130
+ * Service interface for the post-run crystallization flow.
131
+ */
132
+ export class CrystallizationService {
133
+ db;
134
+ constructor(db) {
135
+ this.db = db;
136
+ // Idempotent — safe to call on every constructor invocation; the
137
+ // openStateDb pipeline already applied the SQL migration, but a
138
+ // caller-built DB (e.g. tests) needs this.
139
+ ensureCrystallizationEventTable(this.db);
140
+ }
141
+ /**
142
+ * Read a CrystallizationEvent by id.
143
+ */
144
+ read(id) {
145
+ return getCrystallizationEvent(this.db, id);
146
+ }
147
+ /**
148
+ * List events. Filter by lifecycle_status and/or the FK pointer
149
+ * columns. When no filters are supplied, lists ALL events
150
+ * (newest-first).
151
+ */
152
+ list(opts = {}) {
153
+ return listCrystallizationEvents(this.db, opts);
154
+ }
155
+ /**
156
+ * Update a crystallization event's lifecycle_status. Always allowed
157
+ * (no schema-version gate). Returns the updated row or undefined if
158
+ * the id is absent.
159
+ *
160
+ * A `stable` transition is the natural post-crystallization state;
161
+ * the service does NOT enforce user-confirmation here (per spec
162
+ * §5.6 promotion happens on a separate `peaks loop promote` path).
163
+ * `retired` is the dispose path (`peaks asset dispose`).
164
+ */
165
+ updateStatus(id, next) {
166
+ return updateCrystallizationEventStatus(this.db, id, next);
167
+ }
168
+ /**
169
+ * Pre-run gate: throws if any of `task_status`, `gates_passed`,
170
+ * `evidence_collected` is not as required. Called from
171
+ * `crystallize` BEFORE the transaction begins.
172
+ *
173
+ * Reachable as a public method so the CLI / tests can probe the
174
+ * gate in isolation.
175
+ */
176
+ assertReady(task) {
177
+ let parsed;
178
+ try {
179
+ parsed = CrystallizationTaskStateSchema.parse(task);
180
+ }
181
+ catch (err) {
182
+ if (err instanceof ZodError) {
183
+ const findings = err.issues.map((i) => ({
184
+ path: i.path.join("."),
185
+ message: i.message,
186
+ }));
187
+ const first = err.issues[0];
188
+ throw new CrystallizationIntegrityError("CRYSTALLIZATION_PRE_RUN", `pre-run gate failed: ${first?.message ?? "invalid task state"}`, findings);
189
+ }
190
+ throw err;
191
+ }
192
+ // Defense in depth — re-assert at the service boundary.
193
+ if (parsed.task_status !== "completed" ||
194
+ parsed.gates_passed !== true ||
195
+ parsed.evidence_collected !== true) {
196
+ throw new CrystallizationIntegrityError("CRYSTALLIZATION_PRE_RUN", "pre-run gate failed: task must be completed with gates_passed=true AND evidence_collected=true (AC-4 / RL-2)", [
197
+ {
198
+ path: "task",
199
+ message: "task_status must equal 'completed' and gates_passed and evidence_collected must both be true",
200
+ },
201
+ ]);
202
+ }
203
+ return parsed;
204
+ }
205
+ /**
206
+ * Run the post-run crystallization flow.
207
+ *
208
+ * Steps (all in ONE better-sqlite3 transaction, atomic):
209
+ *
210
+ * 1. Pre-run gate (task must be completed / gates_passed /
211
+ * evidence_collected).
212
+ * 2. Re-validate the 4-section brief (EvidenceBriefSchema.parse,
213
+ * EvidenceBriefSchema.refine).
214
+ * 3. Insert loop_release.
215
+ * 4. Insert bee_release header (+ a single bee_manifest row to
216
+ * keep the 4.x FK contract).
217
+ * 5. Insert loop_bee_relation row with role='main'.
218
+ * 6. Insert crystallization_event with the brief persisted.
219
+ *
220
+ * Returns the four ids + the new loop's lifecycle_status. On any
221
+ * failure the transaction is rolled back and the partial rows
222
+ * disappear.
223
+ *
224
+ * Throws:
225
+ * - CrystallizationIntegrityError(CRYSTALLIZATION_PRE_RUN) on
226
+ * the pre-run gate.
227
+ * - BriefSectionError or ZodError if the brief is malformed;
228
+ * surfaced as
229
+ * CrystallizationIntegrityError(MISSING_BRIEF_SECTION).
230
+ * - CrystallizationIntegrityError(CRYSTALLIZATION_TX_FAILED)
231
+ * on any SQL transaction failure.
232
+ */
233
+ crystallize(payload) {
234
+ const task = this.assertReady(payload.task);
235
+ // Re-validate the 4-section brief up-front; the schema refines
236
+ // guard rejects partial briefs. This is the AC-15 / AC-16 / AC-17
237
+ // gate.
238
+ let brief;
239
+ try {
240
+ brief = payload.evidence_brief;
241
+ // Defensive re-parse even though the input is typed; ensures
242
+ // `parse()` runs the .refine() guard.
243
+ EvidenceBriefSchema.parse(brief);
244
+ }
245
+ catch (err) {
246
+ if (err instanceof ZodError || err instanceof BriefSectionError) {
247
+ const findings = err instanceof ZodError
248
+ ? err.issues.map((i) => ({
249
+ path: i.path.join("."),
250
+ message: i.message,
251
+ }))
252
+ : err.findings;
253
+ throw new CrystallizationIntegrityError("MISSING_BRIEF_SECTION", "evidence_brief is missing at least one of the 4 required sections (spec §4.7 / RL-7)", [...findings]);
254
+ }
255
+ throw err;
256
+ }
257
+ // Re-validate loop_input via Zod so a hand-crafted caller input
258
+ // cannot bypass the schema. (M8 dogfood follow-up.)
259
+ const loopRow = LoopReleaseSchema.parse(payload.loop_input);
260
+ // Re-validate bee_input.
261
+ const beeRow = BeeCrystallizeInputSchema.parse(payload.bee_input);
262
+ // Loop-bee relation input (the relation's `reason` is the NL
263
+ // account the LLM authored). We validate the shape WITHOUT
264
+ // bee_release_id (which is filled in by the bee insert below),
265
+ // then validate the final tuple after the SQL writes.
266
+ const relationInputShape = LoopBeeRelationSchema.omit({
267
+ id: true,
268
+ created_at: true,
269
+ bee_release_id: true,
270
+ }).parse({
271
+ loop_release_id: loopRow.id,
272
+ role: "main",
273
+ reason: payload.bee_relation_reason,
274
+ });
275
+ const eventId = newCrystallizationId();
276
+ const eventCreatedAt = new Date().toISOString();
277
+ let loopReleaseId = loopRow.id;
278
+ let beeReleaseId = -1;
279
+ let relationId = -1;
280
+ let eventPersistedId = eventId;
281
+ const tx = this.db.transaction(() => {
282
+ // 1. Insert loop_release.
283
+ insertLoopRelease(this.db, loopRow);
284
+ // 2. Insert bee_release header. The existing
285
+ // `retainRelease` adds segment_ref / file / change rows; the
286
+ // minimal crystallization write here only opens the bee_release
287
+ // header row + a bee_manifest row. Future slices can promote
288
+ // the bee retain path to a richer flow.
289
+ const beeInsert = this.db.prepare(`INSERT INTO bee_release (
290
+ bee_name, version, source, archived_at, archived_by,
291
+ user_intent_raw, description, parent_version, changelog,
292
+ shareable, desktop_visible
293
+ ) VALUES (?, ?, 'user', ?, ?, ?, ?, ?, ?, 1, 1)`);
294
+ const beeInfo = beeInsert.run(beeRow.bee_name, beeRow.version, eventCreatedAt, "llm", beeRow.user_intent_raw ?? null, beeRow.description, beeRow.parent_version ?? null, beeRow.changelog ?? null);
295
+ beeReleaseId = Number(beeInfo.lastInsertRowid);
296
+ // Update the bee_release_pointer row so the latest version is
297
+ // discoverable via list tooling.
298
+ this.db
299
+ .prepare(`INSERT OR REPLACE INTO bee_release_pointer (bee_name, latest_version, released_at) VALUES (?, ?, ?)`)
300
+ .run(beeRow.bee_name, beeRow.version, eventCreatedAt);
301
+ // 3. Insert bee_manifest stub. Mirrors `retainRelease` minimum
302
+ // surface: promotion = 'candidate', requires_human = 0,
303
+ // requires_smoke = 0. Min-cycles left NULL.
304
+ this.db
305
+ .prepare(`INSERT INTO bee_manifest (
306
+ release_id, schema_version, description, segments_json,
307
+ entrypoint_preamble, promotion, min_cycles,
308
+ requires_human, requires_smoke, retire_on_misses
309
+ ) VALUES (?, ?, ?, '[]', '', 'candidate', NULL, 0, 0, NULL)`)
310
+ .run(beeReleaseId, "peaks.bee/1", beeRow.description);
311
+ // 4. Insert the loop_bee_relation row (FKs now valid). Re-parse
312
+ // through the input schema so the bee_release_id is validated
313
+ // too — defense in depth. `id` is autoincrement (not in input).
314
+ const fullRelation = LoopBeeRelationSchema.omit({
315
+ id: true,
316
+ created_at: true,
317
+ }).parse({
318
+ loop_release_id: relationInputShape.loop_release_id,
319
+ bee_release_id: beeReleaseId,
320
+ role: relationInputShape.role,
321
+ reason: relationInputShape.reason,
322
+ schema_version: "peaks.loop-bee-relation/1",
323
+ });
324
+ const relationRow = insertLoopBeeRelation(this.db, fullRelation);
325
+ relationId = relationRow.id;
326
+ // 5. Persist the crystallization_event row with the brief inline
327
+ // (the brief is the durable evidence — AC-17).
328
+ const eventInput = {
329
+ trigger: payload.trigger,
330
+ evidence_brief: brief,
331
+ evidence_bullets: payload.evidence_bullets ?? [],
332
+ source_trace_pointers: payload.source_trace_pointers ?? [],
333
+ evaluator_summary: payload.evaluator_summary ?? "",
334
+ user_decision_summary: payload.user_decision_summary ?? "",
335
+ created_loop_release_id: loopReleaseId,
336
+ created_bee_release_id: beeReleaseId,
337
+ // updated_* are not set on a CREATE crystallization; they will
338
+ // be set on future UPDATE crystallizations.
339
+ lifecycle_status: "candidate",
340
+ };
341
+ const persisted = insertCrystallizationEventRaw(this.db, eventInput, eventId, eventCreatedAt);
342
+ eventPersistedId = persisted.id;
343
+ });
344
+ try {
345
+ tx();
346
+ }
347
+ catch (err) {
348
+ // Map known SQLite constraint errors to friendlier codes; raise
349
+ // CrystallizationIntegrityError so the CLI can render the JSON
350
+ // envelope.
351
+ if (err instanceof CrystallizationIntegrityError ||
352
+ err instanceof BriefSectionError) {
353
+ throw err;
354
+ }
355
+ const message = err instanceof Error ? err.message : String(err);
356
+ throw new CrystallizationIntegrityError("CRYSTALLIZATION_TX_FAILED", `crystallization transaction failed: ${message}`, [
357
+ {
358
+ path: "tx",
359
+ message: "transaction rolled back; verify loop/bee ids are unique and FK targets exist",
360
+ },
361
+ ]);
362
+ }
363
+ // Silence the unused-variable lint; the parsed task is the source
364
+ // of truth at the entry boundary.
365
+ void task;
366
+ return {
367
+ loop_release_id: loopReleaseId,
368
+ bee_release_id: beeReleaseId,
369
+ loop_bee_relation_id: relationId,
370
+ crystallization_event_id: eventPersistedId,
371
+ loop_release_lifecycle_status: "candidate",
372
+ };
373
+ }
374
+ }
375
+ /* ---------------------------------------------------------------------- */
376
+ /* Internal: thin writer that injects the deterministic eventId + */
377
+ /* created_at so the crystallization flow can land all rows in */
378
+ /* one tx before any non-idempotent residue lands. */
379
+ /* ---------------------------------------------------------------------- */
380
+ function insertCrystallizationEventRaw(db, row, id, createdAt) {
381
+ const persisted = CrystallizationEventSchema.parse({
382
+ ...row,
383
+ id,
384
+ schema_version: CRYSTALLIZATION_SCHEMA_VERSION,
385
+ created_at: createdAt,
386
+ });
387
+ const stmt = db.prepare(`INSERT INTO crystallization_event (
388
+ id, trigger, evidence_brief_json, evidence_bullets_json,
389
+ source_trace_pointers_json, evaluator_summary,
390
+ user_decision_summary,
391
+ created_loop_release_id, updated_loop_release_id,
392
+ created_bee_release_id, updated_bee_release_id,
393
+ lifecycle_status, schema_version, created_at
394
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
395
+ stmt.run(persisted.id, persisted.trigger, JSON.stringify(persisted.evidence_brief), JSON.stringify(persisted.evidence_bullets), JSON.stringify(persisted.source_trace_pointers), persisted.evaluator_summary, persisted.user_decision_summary, persisted.created_loop_release_id ?? null, persisted.updated_loop_release_id ?? null, persisted.created_bee_release_id ?? null, persisted.updated_bee_release_id ?? null, persisted.lifecycle_status, persisted.schema_version, persisted.created_at);
396
+ return persisted;
397
+ }
@@ -0,0 +1,75 @@
1
+ import type Database from "better-sqlite3";
2
+ import { type CrystallizationEvent, type CrystallizationEventInput, type CrystallizationEventStatus } from "./crystallization-types.js";
3
+ /**
4
+ * Low-level SQLite access for the `crystallization_event` table.
5
+ * The migration is registered with the existing `openStateDb()`
6
+ * pipeline via
7
+ * `src/services/skillhub/migrations/006-crystallization-event.sql`
8
+ * and is applied in lexicographic order alongside the other
9
+ * skillhub migrations. The function `ensureCrystallizationEventTable`
10
+ * below is a belt-and-suspenders re-applier for callers that pass a
11
+ * database they built themselves (e.g. tests).
12
+ *
13
+ * Defense in depth:
14
+ * - All JSON columns are stored as TEXT and parsed at the
15
+ * boundary. No big JSON BLOB at the SQLite layer — the row
16
+ * stays small and queryable.
17
+ * - The migration is idempotent (CREATE TABLE IF NOT EXISTS,
18
+ * CREATE INDEX IF NOT EXISTS) so re-running it is safe.
19
+ * - The 4-section brief guard and any other business rule is
20
+ * enforced at the SERVICE layer (not the DB); the DB stores
21
+ * the persisted, validated row.
22
+ */
23
+ declare const SCHEMA_VERSION: "peaks.crystallization/1";
24
+ /**
25
+ * Re-apply the crystallization_event table migration against an
26
+ * already-open database. Used by tests that build their own DB
27
+ * without the openStateDb pipeline. Idempotent.
28
+ */
29
+ export declare function ensureCrystallizationEventTable(db: Database.Database): void;
30
+ /**
31
+ * Insert a CrystallizationEvent row. The row's `schema_version` is
32
+ * taken from the input (must equal `peaks.crystallization/1`);
33
+ * `id` and `created_at` are stamped here so callers cannot backdate
34
+ * a row.
35
+ *
36
+ * The Zod schema (parse boundary) re-asserts the 4-section brief
37
+ * guard: a missing-section event is rejected before the SQL INSERT.
38
+ *
39
+ * Returns the persisted row.
40
+ */
41
+ export declare function insertCrystallizationEvent(db: Database.Database, row: CrystallizationEventInput): CrystallizationEvent;
42
+ /** Read a single CrystallizationEvent row by id; returns undefined if absent. */
43
+ export declare function getCrystallizationEvent(db: Database.Database, id: string): CrystallizationEvent | undefined;
44
+ /**
45
+ * List CrystallizationEvent rows. Filter by `lifecycle_status` and/or
46
+ * any of the optional FK pointers. When ALL filters are omitted, lists
47
+ * every event (newest-first by `created_at`).
48
+ */
49
+ export declare function listCrystallizationEvents(db: Database.Database, opts?: {
50
+ lifecycle_status?: CrystallizationEventStatus;
51
+ created_loop_release_id?: string;
52
+ updated_loop_release_id?: string;
53
+ created_bee_release_id?: number;
54
+ updated_bee_release_id?: number;
55
+ }): CrystallizationEvent[];
56
+ /**
57
+ * Update the lifecycle_status of an existing CrystallizationEvent row.
58
+ * Returns the updated row, or undefined if absent.
59
+ *
60
+ * Lifecycle transitions are NOT validated here; the service layer
61
+ * decides whether `stable` requires a user_confirmation_pointer or
62
+ * other evidence (mirroring the evolution-store design).
63
+ */
64
+ export declare function updateCrystallizationEventStatus(db: Database.Database, id: string, next: CrystallizationEventStatus): CrystallizationEvent | undefined;
65
+ /**
66
+ * Test seam: produce a fresh id for a CrystallizationEvent row.
67
+ * Format: `crys-<hex>` (12 hex chars). Matches the Zod
68
+ * `^crys-[0-9a-f]{8,}$` pattern in CrystallizationEventSchema.
69
+ */
70
+ export declare function newCrystallizationId(): string;
71
+ /**
72
+ * Test seam: re-export the schema version so the service layer has a
73
+ * single source of truth.
74
+ */
75
+ export { SCHEMA_VERSION as CRYSTALLIZATION_SCHEMA_VERSION };
@@ -0,0 +1,193 @@
1
+ import { CrystallizationEventSchema, } from "./crystallization-types.js";
2
+ /**
3
+ * Low-level SQLite access for the `crystallization_event` table.
4
+ * The migration is registered with the existing `openStateDb()`
5
+ * pipeline via
6
+ * `src/services/skillhub/migrations/006-crystallization-event.sql`
7
+ * and is applied in lexicographic order alongside the other
8
+ * skillhub migrations. The function `ensureCrystallizationEventTable`
9
+ * below is a belt-and-suspenders re-applier for callers that pass a
10
+ * database they built themselves (e.g. tests).
11
+ *
12
+ * Defense in depth:
13
+ * - All JSON columns are stored as TEXT and parsed at the
14
+ * boundary. No big JSON BLOB at the SQLite layer — the row
15
+ * stays small and queryable.
16
+ * - The migration is idempotent (CREATE TABLE IF NOT EXISTS,
17
+ * CREATE INDEX IF NOT EXISTS) so re-running it is safe.
18
+ * - The 4-section brief guard and any other business rule is
19
+ * enforced at the SERVICE layer (not the DB); the DB stores
20
+ * the persisted, validated row.
21
+ */
22
+ const SCHEMA_VERSION = "peaks.crystallization/1";
23
+ /**
24
+ * Re-apply the crystallization_event table migration against an
25
+ * already-open database. Used by tests that build their own DB
26
+ * without the openStateDb pipeline. Idempotent.
27
+ */
28
+ export function ensureCrystallizationEventTable(db) {
29
+ db.exec(`
30
+ CREATE TABLE IF NOT EXISTS crystallization_event (
31
+ id TEXT PRIMARY KEY,
32
+ trigger TEXT NOT NULL CHECK (trigger IN (
33
+ 'user_explicit',
34
+ 'llm_suggested',
35
+ 'success_default_prompt',
36
+ 'similar_task_recurrence'
37
+ )),
38
+ evidence_brief_json TEXT NOT NULL,
39
+ evidence_bullets_json TEXT NOT NULL DEFAULT '[]',
40
+ source_trace_pointers_json TEXT NOT NULL DEFAULT '[]',
41
+ evaluator_summary TEXT NOT NULL DEFAULT '',
42
+ user_decision_summary TEXT NOT NULL DEFAULT '',
43
+ created_loop_release_id TEXT REFERENCES loop_release(id) ON DELETE SET NULL,
44
+ updated_loop_release_id TEXT REFERENCES loop_release(id) ON DELETE SET NULL,
45
+ created_bee_release_id INTEGER REFERENCES bee_release(id) ON DELETE SET NULL,
46
+ updated_bee_release_id INTEGER REFERENCES bee_release(id) ON DELETE SET NULL,
47
+ lifecycle_status TEXT NOT NULL DEFAULT 'candidate'
48
+ CHECK (lifecycle_status IN ('candidate','stable','retired')),
49
+ schema_version TEXT NOT NULL CHECK (schema_version = 'peaks.crystallization/1'),
50
+ created_at TEXT NOT NULL,
51
+ CHECK (length(id) > 0),
52
+ CHECK (length(evidence_brief_json) >= 2)
53
+ );
54
+ CREATE INDEX IF NOT EXISTS idx_crystallization_event_created_loop
55
+ ON crystallization_event(created_loop_release_id);
56
+ CREATE INDEX IF NOT EXISTS idx_crystallization_event_updated_loop
57
+ ON crystallization_event(updated_loop_release_id);
58
+ CREATE INDEX IF NOT EXISTS idx_crystallization_event_created_bee
59
+ ON crystallization_event(created_bee_release_id);
60
+ CREATE INDEX IF NOT EXISTS idx_crystallization_event_updated_bee
61
+ ON crystallization_event(updated_bee_release_id);
62
+ CREATE INDEX IF NOT EXISTS idx_crystallization_event_lifecycle
63
+ ON crystallization_event(lifecycle_status);
64
+ CREATE INDEX IF NOT EXISTS idx_crystallization_event_created_at
65
+ ON crystallization_event(created_at);
66
+ `);
67
+ }
68
+ function rowToCrystallizationEvent(row) {
69
+ return {
70
+ id: row.id,
71
+ trigger: row.trigger,
72
+ evidence_brief: JSON.parse(row.evidence_brief_json),
73
+ evidence_bullets: JSON.parse(row.evidence_bullets_json),
74
+ source_trace_pointers: JSON.parse(row.source_trace_pointers_json),
75
+ evaluator_summary: row.evaluator_summary,
76
+ user_decision_summary: row.user_decision_summary,
77
+ created_loop_release_id: row.created_loop_release_id ?? undefined,
78
+ updated_loop_release_id: row.updated_loop_release_id ?? undefined,
79
+ created_bee_release_id: row.created_bee_release_id ?? undefined,
80
+ updated_bee_release_id: row.updated_bee_release_id ?? undefined,
81
+ lifecycle_status: row.lifecycle_status,
82
+ schema_version: row.schema_version,
83
+ created_at: row.created_at,
84
+ };
85
+ }
86
+ /**
87
+ * Insert a CrystallizationEvent row. The row's `schema_version` is
88
+ * taken from the input (must equal `peaks.crystallization/1`);
89
+ * `id` and `created_at` are stamped here so callers cannot backdate
90
+ * a row.
91
+ *
92
+ * The Zod schema (parse boundary) re-asserts the 4-section brief
93
+ * guard: a missing-section event is rejected before the SQL INSERT.
94
+ *
95
+ * Returns the persisted row.
96
+ */
97
+ export function insertCrystallizationEvent(db, row) {
98
+ const id = newCrystallizationId();
99
+ const createdAt = new Date().toISOString();
100
+ // Build the persisted row first so the Zod refine guard (brief
101
+ // must have 4 sections) runs before any SQL side effects.
102
+ const persisted = CrystallizationEventSchema.parse({
103
+ ...row,
104
+ id,
105
+ schema_version: SCHEMA_VERSION,
106
+ created_at: createdAt,
107
+ });
108
+ const stmt = db.prepare(`INSERT INTO crystallization_event (
109
+ id, trigger, evidence_brief_json, evidence_bullets_json,
110
+ source_trace_pointers_json, evaluator_summary,
111
+ user_decision_summary,
112
+ created_loop_release_id, updated_loop_release_id,
113
+ created_bee_release_id, updated_bee_release_id,
114
+ lifecycle_status, schema_version, created_at
115
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
116
+ stmt.run(persisted.id, persisted.trigger, JSON.stringify(persisted.evidence_brief), JSON.stringify(persisted.evidence_bullets), JSON.stringify(persisted.source_trace_pointers), persisted.evaluator_summary, persisted.user_decision_summary, persisted.created_loop_release_id ?? null, persisted.updated_loop_release_id ?? null, persisted.created_bee_release_id ?? null, persisted.updated_bee_release_id ?? null, persisted.lifecycle_status, persisted.schema_version, persisted.created_at);
117
+ return persisted;
118
+ }
119
+ /** Read a single CrystallizationEvent row by id; returns undefined if absent. */
120
+ export function getCrystallizationEvent(db, id) {
121
+ const row = db
122
+ .prepare("SELECT * FROM crystallization_event WHERE id = ?")
123
+ .get(id);
124
+ if (!row)
125
+ return undefined;
126
+ return rowToCrystallizationEvent(row);
127
+ }
128
+ /**
129
+ * List CrystallizationEvent rows. Filter by `lifecycle_status` and/or
130
+ * any of the optional FK pointers. When ALL filters are omitted, lists
131
+ * every event (newest-first by `created_at`).
132
+ */
133
+ export function listCrystallizationEvents(db, opts = {}) {
134
+ const wheres = [];
135
+ const params = [];
136
+ if (opts.lifecycle_status !== undefined) {
137
+ wheres.push("lifecycle_status = ?");
138
+ params.push(opts.lifecycle_status);
139
+ }
140
+ if (opts.created_loop_release_id !== undefined) {
141
+ wheres.push("created_loop_release_id = ?");
142
+ params.push(opts.created_loop_release_id);
143
+ }
144
+ if (opts.updated_loop_release_id !== undefined) {
145
+ wheres.push("updated_loop_release_id = ?");
146
+ params.push(opts.updated_loop_release_id);
147
+ }
148
+ if (opts.created_bee_release_id !== undefined) {
149
+ wheres.push("created_bee_release_id = ?");
150
+ params.push(opts.created_bee_release_id);
151
+ }
152
+ if (opts.updated_bee_release_id !== undefined) {
153
+ wheres.push("updated_bee_release_id = ?");
154
+ params.push(opts.updated_bee_release_id);
155
+ }
156
+ let sql = "SELECT * FROM crystallization_event";
157
+ if (wheres.length > 0)
158
+ sql += " WHERE " + wheres.join(" AND ");
159
+ sql += " ORDER BY created_at DESC, id ASC";
160
+ const rows = db.prepare(sql).all(...params);
161
+ return rows.map(rowToCrystallizationEvent);
162
+ }
163
+ /**
164
+ * Update the lifecycle_status of an existing CrystallizationEvent row.
165
+ * Returns the updated row, or undefined if absent.
166
+ *
167
+ * Lifecycle transitions are NOT validated here; the service layer
168
+ * decides whether `stable` requires a user_confirmation_pointer or
169
+ * other evidence (mirroring the evolution-store design).
170
+ */
171
+ export function updateCrystallizationEventStatus(db, id, next) {
172
+ const existing = getCrystallizationEvent(db, id);
173
+ if (!existing)
174
+ return undefined;
175
+ db.prepare("UPDATE crystallization_event SET lifecycle_status = ? WHERE id = ?").run(next, id);
176
+ return getCrystallizationEvent(db, id);
177
+ }
178
+ /**
179
+ * Test seam: produce a fresh id for a CrystallizationEvent row.
180
+ * Format: `crys-<hex>` (12 hex chars). Matches the Zod
181
+ * `^crys-[0-9a-f]{8,}$` pattern in CrystallizationEventSchema.
182
+ */
183
+ export function newCrystallizationId() {
184
+ const hex = Math.floor(Math.random() * 0x1_000_000_000_000)
185
+ .toString(16)
186
+ .padStart(12, "0");
187
+ return `crys-${hex}`;
188
+ }
189
+ /**
190
+ * Test seam: re-export the schema version so the service layer has a
191
+ * single source of truth.
192
+ */
193
+ export { SCHEMA_VERSION as CRYSTALLIZATION_SCHEMA_VERSION };