brainclaw 0.29.2 → 1.5.4

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 (197) hide show
  1. package/LICENSE +21 -74
  2. package/README.md +199 -176
  3. package/dist/brainclaw-vscode.vsix +0 -0
  4. package/dist/cli.js +710 -25
  5. package/dist/commands/accept.js +3 -0
  6. package/dist/commands/add-step.js +11 -26
  7. package/dist/commands/agent-board.js +70 -3
  8. package/dist/commands/audit.js +19 -0
  9. package/dist/commands/check-policy.js +54 -0
  10. package/dist/commands/check-security-mcp.js +145 -0
  11. package/dist/commands/check-security.js +106 -0
  12. package/dist/commands/claim-resource.js +1 -0
  13. package/dist/commands/codev.js +672 -0
  14. package/dist/commands/compact.js +74 -0
  15. package/dist/commands/complete-step.js +16 -26
  16. package/dist/commands/constraint.js +8 -20
  17. package/dist/commands/decision.js +9 -20
  18. package/dist/commands/delete-plan.js +10 -12
  19. package/dist/commands/delete-step.js +16 -0
  20. package/dist/commands/dispatch.js +163 -0
  21. package/dist/commands/doctor.js +1122 -49
  22. package/dist/commands/enable-agent.js +1 -0
  23. package/dist/commands/export.js +280 -22
  24. package/dist/commands/handoff.js +33 -0
  25. package/dist/commands/harvest.js +189 -0
  26. package/dist/commands/hooks.js +82 -25
  27. package/dist/commands/inbox.js +169 -0
  28. package/dist/commands/init.js +38 -31
  29. package/dist/commands/install-hooks.js +71 -44
  30. package/dist/commands/link.js +89 -0
  31. package/dist/commands/list-claims.js +48 -3
  32. package/dist/commands/list-plans.js +129 -25
  33. package/dist/commands/loops-handlers.js +409 -0
  34. package/dist/commands/mcp-read-handlers.js +1628 -0
  35. package/dist/commands/mcp-schemas.generated.js +269 -0
  36. package/dist/commands/mcp.js +4224 -1501
  37. package/dist/commands/plan-resource.js +64 -0
  38. package/dist/commands/plan.js +12 -26
  39. package/dist/commands/prune.js +37 -2
  40. package/dist/commands/reflect.js +20 -7
  41. package/dist/commands/release-claim.js +11 -6
  42. package/dist/commands/release-notes.js +170 -0
  43. package/dist/commands/repair.js +210 -0
  44. package/dist/commands/run-profile.js +57 -0
  45. package/dist/commands/sequence.js +113 -0
  46. package/dist/commands/session-end.js +423 -14
  47. package/dist/commands/session-start.js +214 -41
  48. package/dist/commands/setup-security.js +103 -0
  49. package/dist/commands/setup.js +42 -4
  50. package/dist/commands/stale.js +109 -0
  51. package/dist/commands/switch.js +100 -2
  52. package/dist/commands/trap.js +14 -31
  53. package/dist/commands/update-handoff.js +63 -4
  54. package/dist/commands/update-plan.js +21 -28
  55. package/dist/commands/update-step.js +37 -0
  56. package/dist/commands/upgrade.js +313 -6
  57. package/dist/commands/usage.js +102 -0
  58. package/dist/commands/version.js +20 -0
  59. package/dist/commands/who.js +33 -5
  60. package/dist/commands/worktree.js +105 -0
  61. package/dist/core/actions.js +315 -0
  62. package/dist/core/agent-capability.js +610 -17
  63. package/dist/core/agent-context.js +7 -1
  64. package/dist/core/agent-files.js +1169 -85
  65. package/dist/core/agent-integrations.js +160 -5
  66. package/dist/core/agent-inventory.js +2 -0
  67. package/dist/core/agent-profiles.js +93 -0
  68. package/dist/core/agent-registry.js +162 -30
  69. package/dist/core/agentrun-reconciler.js +345 -0
  70. package/dist/core/agentruns.js +424 -0
  71. package/dist/core/ai-agent-detection.js +31 -10
  72. package/dist/core/archival.js +77 -0
  73. package/dist/core/assignment-sweeper.js +82 -0
  74. package/dist/core/assignments.js +367 -0
  75. package/dist/core/audit.js +30 -0
  76. package/dist/core/brainclaw-version.js +94 -2
  77. package/dist/core/candidates.js +93 -2
  78. package/dist/core/claims.js +419 -0
  79. package/dist/core/codev-metrics.js +77 -0
  80. package/dist/core/codev-personas.js +31 -0
  81. package/dist/core/codev-plan-gen.js +35 -0
  82. package/dist/core/codev-prompts.js +74 -0
  83. package/dist/core/codev-responses.js +62 -0
  84. package/dist/core/codev-rounds.js +218 -0
  85. package/dist/core/config.js +4 -0
  86. package/dist/core/context.js +381 -34
  87. package/dist/core/coordination.js +201 -6
  88. package/dist/core/cross-project.js +230 -16
  89. package/dist/core/default-profiles/doctor.yaml +11 -0
  90. package/dist/core/default-profiles/janitor.yaml +11 -0
  91. package/dist/core/default-profiles/onboarder.yaml +11 -0
  92. package/dist/core/default-profiles/reviewer.yaml +13 -0
  93. package/dist/core/dispatcher.js +1189 -0
  94. package/dist/core/duplicates.js +2 -2
  95. package/dist/core/entity-operations.js +450 -0
  96. package/dist/core/entity-registry.js +344 -0
  97. package/dist/core/events.js +106 -2
  98. package/dist/core/execution-adapters.js +154 -0
  99. package/dist/core/execution-context.js +63 -0
  100. package/dist/core/execution-profile.js +270 -0
  101. package/dist/core/execution.js +255 -0
  102. package/dist/core/facade-schema.js +81 -0
  103. package/dist/core/federation-cloud.js +99 -0
  104. package/dist/core/federation-message.js +52 -0
  105. package/dist/core/federation-transport.js +65 -0
  106. package/dist/core/gc-semantic.js +482 -0
  107. package/dist/core/governance.js +247 -0
  108. package/dist/core/guards.js +19 -0
  109. package/dist/core/ideation.js +72 -0
  110. package/dist/core/identity.js +110 -25
  111. package/dist/core/ids.js +6 -0
  112. package/dist/core/input-validation.js +2 -2
  113. package/dist/core/instruction-templates.js +344 -136
  114. package/dist/core/io.js +90 -11
  115. package/dist/core/lock.js +6 -2
  116. package/dist/core/loops/brief-assembly.js +213 -0
  117. package/dist/core/loops/facade-schema.js +148 -0
  118. package/dist/core/loops/index.js +7 -0
  119. package/dist/core/loops/iteration-engine.js +139 -0
  120. package/dist/core/loops/lock.js +385 -0
  121. package/dist/core/loops/store.js +201 -0
  122. package/dist/core/loops/types.js +403 -0
  123. package/dist/core/loops/verbs.js +534 -0
  124. package/dist/core/markdown.js +15 -3
  125. package/dist/core/memory-compactor.js +432 -0
  126. package/dist/core/memory-git.js +152 -8
  127. package/dist/core/messaging.js +278 -0
  128. package/dist/core/migration.js +32 -1
  129. package/dist/core/mutation-pipeline.js +4 -2
  130. package/dist/core/operations/memory-mutation.js +129 -0
  131. package/dist/core/operations/memory-write.js +78 -0
  132. package/dist/core/operations/plan.js +190 -0
  133. package/dist/core/policy.js +169 -0
  134. package/dist/core/reputation.js +9 -3
  135. package/dist/core/schema.js +491 -6
  136. package/dist/core/search.js +21 -2
  137. package/dist/core/security-cache.js +71 -0
  138. package/dist/core/security-guard.js +152 -0
  139. package/dist/core/security-scoring.js +86 -0
  140. package/dist/core/sequence.js +130 -0
  141. package/dist/core/socket-client.js +113 -0
  142. package/dist/core/staleness.js +246 -0
  143. package/dist/core/state.js +98 -22
  144. package/dist/core/store-resolution.js +43 -11
  145. package/dist/core/toml-writer.js +76 -0
  146. package/dist/core/upgrades/backup.js +232 -0
  147. package/dist/core/upgrades/health-check.js +169 -0
  148. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  149. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  150. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  151. package/dist/core/upgrades/schema-version.js +97 -0
  152. package/dist/core/worktree.js +606 -0
  153. package/dist/facts.js +114 -0
  154. package/dist/facts.json +111 -0
  155. package/docs/architecture/project-refs.md +5 -1
  156. package/docs/cli.md +690 -43
  157. package/docs/concepts/ideation-loop.md +317 -0
  158. package/docs/concepts/loop-engine.md +456 -0
  159. package/docs/concepts/mcp-governance.md +268 -0
  160. package/docs/concepts/memory-staleness.md +122 -0
  161. package/docs/concepts/multi-agent-workflows.md +166 -0
  162. package/docs/concepts/plans-and-claims.md +31 -6
  163. package/docs/concepts/project-md-convention.md +35 -0
  164. package/docs/concepts/troubleshooting.md +220 -0
  165. package/docs/concepts/upgrade-cli.md +202 -0
  166. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  167. package/docs/context-format-changelog.md +2 -2
  168. package/docs/context-format.md +2 -2
  169. package/docs/index.md +68 -0
  170. package/docs/integrations/agents.md +15 -16
  171. package/docs/integrations/cline.md +88 -0
  172. package/docs/integrations/codex.md +75 -23
  173. package/docs/integrations/continue.md +60 -0
  174. package/docs/integrations/copilot.md +67 -9
  175. package/docs/integrations/kilocode.md +72 -0
  176. package/docs/integrations/mcp.md +304 -21
  177. package/docs/integrations/mistral-vibe.md +122 -0
  178. package/docs/integrations/opencode.md +84 -0
  179. package/docs/integrations/overview.md +23 -8
  180. package/docs/integrations/roo.md +74 -0
  181. package/docs/integrations/windsurf.md +83 -0
  182. package/docs/mcp-schema-changelog.md +191 -1
  183. package/docs/playbooks/integration/index.md +121 -0
  184. package/docs/playbooks/productivity/index.md +102 -0
  185. package/docs/playbooks/team/index.md +122 -0
  186. package/docs/product/agent-first-model.md +184 -0
  187. package/docs/product/entity-model-audit.md +462 -0
  188. package/docs/product/positioning.md +10 -10
  189. package/docs/quickstart-existing-project.md +135 -0
  190. package/docs/quickstart.md +124 -37
  191. package/docs/release-maintenance.md +79 -0
  192. package/docs/review.md +2 -0
  193. package/docs/server-operations.md +118 -0
  194. package/package.json +21 -13
  195. package/dist/commands/claude-desktop-extension.js +0 -18
  196. package/dist/commands/diff.js +0 -99
  197. package/dist/core/claude-desktop-extension.js +0 -224
@@ -0,0 +1,534 @@
1
+ import crypto from 'node:crypto';
2
+ import { nowISO } from '../ids.js';
3
+ import { appendEvent, generateMutationId, getLoop, listLoopEvents, writeThreadFile, } from './store.js';
4
+ import { LoopArtifactSchema, } from './types.js';
5
+ import { decideNextPhase, } from './iteration-engine.js';
6
+ function nextSeq(loopId, cwd) {
7
+ const events = listLoopEvents(loopId, cwd);
8
+ return (events[events.length - 1]?.seq ?? 0) + 1;
9
+ }
10
+ function assertMutable(thread, intent) {
11
+ if (thread.status === 'completed' || thread.status === 'cancelled' || thread.status === 'blocked') {
12
+ throw new Error(`${intent}: loop ${thread.id} is already ${thread.status}`);
13
+ }
14
+ }
15
+ function loadLoopOrThrow(id, cwd) {
16
+ const loop = getLoop(id, cwd);
17
+ if (!loop)
18
+ throw new Error(`unknown loop_id ${id}`);
19
+ return loop;
20
+ }
21
+ /* ========================= Stop-condition evaluator ======================= */
22
+ function isVerdictAccepted(artifact) {
23
+ if (artifact.type !== 'verdict')
24
+ return false;
25
+ const body = (artifact.body ?? '').trim().toLowerCase();
26
+ return /^accepted(?:\b|[:\s])/.test(body);
27
+ }
28
+ export function evaluateStopCondition(thread, condition) {
29
+ if (!condition)
30
+ return false;
31
+ switch (condition.kind) {
32
+ case 'phase_reached':
33
+ return thread.current_phase === condition.phase;
34
+ case 'reviewer_green':
35
+ return thread.artifacts.some(isVerdictAccepted);
36
+ case 'max_iterations':
37
+ return thread.iteration_count >= condition.n;
38
+ case 'artifact_produced':
39
+ return thread.artifacts.some((artifact) => artifact.phase === condition.phase && artifact.type === condition.type);
40
+ case 'min_artifacts_by_type': {
41
+ // pln#492 — count artifacts of `type` in the requested scope.
42
+ // Phase scope counts artifacts whose phase matches the thread's
43
+ // current_phase. When the thread is iterating (iteration_count > 0
44
+ // OR any artifact carries an iteration field), phase scope is
45
+ // refined to the current iteration window — that's what makes
46
+ // "≥3 critiques in current critique round" work without the
47
+ // previous round leaking in. loop scope counts across all phases
48
+ // and all iterations.
49
+ const matches = thread.artifacts.filter((artifact) => {
50
+ if (artifact.type !== condition.type)
51
+ return false;
52
+ if (condition.scope === 'phase') {
53
+ if (artifact.phase !== thread.current_phase)
54
+ return false;
55
+ // pln#492 phase 2.b — iteration-window awareness. If either the
56
+ // thread or the artifact carries iteration info, only count the
57
+ // artifacts produced in the thread's current iteration. Legacy
58
+ // loops without iteration tracking are unaffected (both fields
59
+ // default to 0).
60
+ if (thread.iteration_count > 0 ||
61
+ thread.artifacts.some((a) => a.iteration !== undefined)) {
62
+ const artifactIteration = artifact.iteration ?? 0;
63
+ if (artifactIteration !== thread.iteration_count)
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ return true;
69
+ });
70
+ return matches.length >= condition.n;
71
+ }
72
+ case 'manual':
73
+ return false;
74
+ case 'any':
75
+ return condition.conditions.some((c) => evaluateStopCondition(thread, c));
76
+ case 'all':
77
+ return condition.conditions.every((c) => evaluateStopCondition(thread, c));
78
+ default: {
79
+ const exhaustive = condition;
80
+ void exhaustive;
81
+ return false;
82
+ }
83
+ }
84
+ }
85
+ export function evaluatePhaseAdvanceGate(thread, gate) {
86
+ if (!gate)
87
+ return { advance: true };
88
+ if (evaluateStopCondition(thread, gate))
89
+ return { advance: true };
90
+ return {
91
+ advance: false,
92
+ gate_reason: describeUnmetGate(thread, gate),
93
+ };
94
+ }
95
+ function describeUnmetGate(thread, gate) {
96
+ switch (gate.kind) {
97
+ case 'min_artifacts_by_type': {
98
+ // Mirror the iteration-aware filter used in evaluateStopCondition so
99
+ // the message's count matches what the evaluator actually saw — the
100
+ // operator should not see "count=4" in an error from a gate that
101
+ // counted only iteration=1 artifacts.
102
+ const iterationAware = thread.iteration_count > 0 ||
103
+ thread.artifacts.some((a) => a.iteration !== undefined);
104
+ const matches = thread.artifacts.filter((artifact) => {
105
+ if (artifact.type !== gate.type)
106
+ return false;
107
+ if (gate.scope === 'phase') {
108
+ if (artifact.phase !== thread.current_phase)
109
+ return false;
110
+ if (iterationAware) {
111
+ const artifactIteration = artifact.iteration ?? 0;
112
+ if (artifactIteration !== thread.iteration_count)
113
+ return false;
114
+ }
115
+ return true;
116
+ }
117
+ return true;
118
+ });
119
+ return `min_artifacts_by_type unmet: ${gate.scope}-scope count of type "${gate.type}" = ${matches.length} < n=${gate.n}`;
120
+ }
121
+ case 'phase_reached':
122
+ return `phase_reached unmet: current_phase="${thread.current_phase}" expected="${gate.phase}"`;
123
+ case 'reviewer_green':
124
+ return 'reviewer_green unmet: no accepted verdict artifact yet';
125
+ case 'max_iterations':
126
+ return `max_iterations unmet: iteration_count=${thread.iteration_count} < n=${gate.n}`;
127
+ case 'artifact_produced':
128
+ return `artifact_produced unmet: no artifact of type "${gate.type}" in phase "${gate.phase}"`;
129
+ case 'manual':
130
+ return 'manual gate: caller did not signal advance';
131
+ case 'any':
132
+ return `any-of unmet: none of ${gate.conditions.length} sub-conditions held`;
133
+ case 'all':
134
+ return `all-of unmet: at least one of ${gate.conditions.length} sub-conditions failed`;
135
+ default: {
136
+ const exhaustive = gate;
137
+ void exhaustive;
138
+ return 'unknown gate kind';
139
+ }
140
+ }
141
+ }
142
+ function stopHitsBlock(condition) {
143
+ if (!condition)
144
+ return false;
145
+ if (condition.kind === 'max_iterations')
146
+ return true;
147
+ if (condition.kind === 'any' || condition.kind === 'all') {
148
+ return condition.conditions.some(stopHitsBlock);
149
+ }
150
+ return false;
151
+ }
152
+ export function advance(input, cwd) {
153
+ const current = loadLoopOrThrow(input.id, cwd);
154
+ assertMutable(current, 'advance');
155
+ if (current.status === 'paused') {
156
+ throw new Error(`advance: loop ${current.id} is paused; resume before advancing`);
157
+ }
158
+ const phaseNames = current.phases.map((p) => p.name);
159
+ const currentIndex = phaseNames.indexOf(current.current_phase);
160
+ if (currentIndex < 0) {
161
+ throw new Error(`advance: current_phase "${current.current_phase}" is not in phases`);
162
+ }
163
+ // pln#492 phase 2.b — phase-advance gate check. Skipped when the caller
164
+ // is forcing an explicit to_phase or passing { force: true }. On block,
165
+ // emit a `phase_advance_blocked` system event into the journal and throw
166
+ // an actionable error (not a silent hang — mitigates trp#160 wiring class).
167
+ if (input.to_phase === undefined && input.force !== true) {
168
+ const currentPhaseDef = current.phases[currentIndex];
169
+ const gate = currentPhaseDef?.advance_gate;
170
+ const gateOutcome = evaluatePhaseAdvanceGate(current, gate);
171
+ if (!gateOutcome.advance) {
172
+ const blockSeq = nextSeq(current.id, cwd);
173
+ const blockMutation = generateMutationId();
174
+ appendEvent(current.id, {
175
+ event_id: crypto.randomUUID(),
176
+ loop_id: current.id,
177
+ seq: blockSeq,
178
+ at: nowISO(),
179
+ by: input.actor,
180
+ mutation_id: blockMutation,
181
+ kind: 'phase_advance_blocked',
182
+ phase: current.current_phase,
183
+ gate_reason: gateOutcome.gate_reason ?? 'gate evaluation returned no reason',
184
+ }, cwd);
185
+ throw new Error(`advance: phase_advance_blocked on "${current.current_phase}" — ${gateOutcome.gate_reason}`);
186
+ }
187
+ }
188
+ // Decide the next state. If the caller specified a to_phase, honour it
189
+ // verbatim (used by `force`-style overrides and explicit jumps). Otherwise
190
+ // consult the iteration engine, which knows about the cycle, exit_when,
191
+ // and the iteration cap.
192
+ let to_phase;
193
+ let iteration_count = current.iteration_count;
194
+ let iterationDecision;
195
+ if (input.to_phase !== undefined) {
196
+ if (!phaseNames.includes(input.to_phase)) {
197
+ throw new Error(`advance: to_phase "${input.to_phase}" is not in phases`);
198
+ }
199
+ to_phase = input.to_phase;
200
+ const toIndex = phaseNames.indexOf(to_phase);
201
+ const iteratingBackward = toIndex <= currentIndex;
202
+ // Going backward via explicit to_phase is allowed (it bumps the
203
+ // iteration counter so callers can hand-roll their own iteration when
204
+ // they don't have an iteration block defined). The forward-direction
205
+ // restriction only applies when no to_phase is given.
206
+ if (iteratingBackward)
207
+ iteration_count = current.iteration_count + 1;
208
+ }
209
+ else {
210
+ const protocol = {
211
+ phases: current.phases,
212
+ iteration: current.protocol?.iteration,
213
+ };
214
+ iterationDecision = decideNextPhase(current, protocol);
215
+ to_phase = iterationDecision.target;
216
+ iteration_count = iterationDecision.iteration;
217
+ }
218
+ if (evaluateStopCondition(current, current.stop_condition)) {
219
+ const finalStatus = stopHitsMaxIterations(current, current.stop_condition)
220
+ ? 'blocked'
221
+ : 'completed';
222
+ const closed = commitClosedTransition(current, finalStatus, input.actor, input.reason, cwd);
223
+ return { loop: closed, auto_closed: true };
224
+ }
225
+ const now = nowISO();
226
+ const mutation_id = generateMutationId();
227
+ const version = current.version + 1;
228
+ let seq = nextSeq(current.id, cwd);
229
+ const next = {
230
+ ...current,
231
+ version,
232
+ mutation_id,
233
+ current_phase: to_phase,
234
+ iteration_count,
235
+ updated_at: now,
236
+ };
237
+ // pln#492 phase 2.b — when the iteration engine forces the cycle out
238
+ // because the cap was hit, emit `max_iterations_reached` BEFORE the
239
+ // phase_advanced event so the journal reads in causal order.
240
+ if (iterationDecision?.kind === 'max_iterations') {
241
+ appendEvent(current.id, {
242
+ event_id: crypto.randomUUID(),
243
+ loop_id: current.id,
244
+ seq,
245
+ at: now,
246
+ by: input.actor,
247
+ mutation_id,
248
+ kind: 'max_iterations_reached',
249
+ phase: current.current_phase,
250
+ iteration: iterationDecision.iteration,
251
+ max_iterations: iterationDecision.max,
252
+ }, cwd);
253
+ seq = nextSeq(current.id, cwd);
254
+ }
255
+ appendEvent(current.id, {
256
+ event_id: crypto.randomUUID(),
257
+ loop_id: current.id,
258
+ seq,
259
+ at: now,
260
+ by: input.actor,
261
+ mutation_id,
262
+ kind: 'phase_advanced',
263
+ from_phase: current.current_phase,
264
+ to_phase,
265
+ iteration: iteration_count,
266
+ reason: input.reason,
267
+ }, cwd);
268
+ writeThreadFile(next, cwd);
269
+ const postAdvance = evaluateStopCondition(next, next.stop_condition);
270
+ if (postAdvance) {
271
+ const finalStatus = stopHitsMaxIterations(next, next.stop_condition)
272
+ ? 'blocked'
273
+ : 'completed';
274
+ const closed = commitClosedTransition(next, finalStatus, input.actor, input.reason, cwd);
275
+ return { loop: closed, auto_closed: true };
276
+ }
277
+ return { loop: next, auto_closed: false };
278
+ }
279
+ function stopHitsMaxIterations(thread, condition) {
280
+ if (!condition)
281
+ return false;
282
+ if (!stopHitsBlock(condition))
283
+ return false;
284
+ if (condition.kind === 'max_iterations') {
285
+ return thread.iteration_count >= condition.n;
286
+ }
287
+ if (condition.kind === 'any' || condition.kind === 'all') {
288
+ return condition.conditions.some((c) => stopHitsMaxIterations(thread, c));
289
+ }
290
+ return false;
291
+ }
292
+ function commitClosedTransition(thread, final_status, actor, reason, cwd) {
293
+ const now = nowISO();
294
+ const mutation_id = generateMutationId();
295
+ const version = thread.version + 1;
296
+ const seq = nextSeq(thread.id, cwd);
297
+ const next = {
298
+ ...thread,
299
+ version,
300
+ mutation_id,
301
+ status: final_status,
302
+ updated_at: now,
303
+ closed_at: now,
304
+ };
305
+ appendEvent(thread.id, {
306
+ event_id: crypto.randomUUID(),
307
+ loop_id: thread.id,
308
+ seq,
309
+ at: now,
310
+ by: actor,
311
+ mutation_id,
312
+ kind: 'closed',
313
+ final_status,
314
+ reason,
315
+ }, cwd);
316
+ writeThreadFile(next, cwd);
317
+ return next;
318
+ }
319
+ function resolveTurnSlot(thread, input) {
320
+ if (input.slot_id) {
321
+ const match = thread.slots.find((s) => s.slot_id === input.slot_id);
322
+ if (!match)
323
+ throw new Error(`turn: slot_id "${input.slot_id}" not in loop`);
324
+ return match;
325
+ }
326
+ if (input.role) {
327
+ const match = thread.slots.find((s) => s.role === input.role && s.status !== 'done');
328
+ if (!match)
329
+ throw new Error(`turn: no active slot with role "${input.role}"`);
330
+ return match;
331
+ }
332
+ throw new Error(`turn: either slot_id or role must be supplied`);
333
+ }
334
+ export function turn(input, cwd) {
335
+ const current = loadLoopOrThrow(input.id, cwd);
336
+ assertMutable(current, 'turn');
337
+ if (current.status === 'paused')
338
+ throw new Error(`turn: loop ${current.id} is paused`);
339
+ const targetSlot = resolveTurnSlot(current, input);
340
+ const now = nowISO();
341
+ const mutation_id = generateMutationId();
342
+ const version = current.version + 1;
343
+ const seq = nextSeq(current.id, cwd);
344
+ const updatedSlots = current.slots.map((slot) => slot.slot_id === targetSlot.slot_id
345
+ ? {
346
+ ...slot,
347
+ status: 'assigned',
348
+ phase: current.current_phase,
349
+ assignment_id: input.assignment_id ?? slot.assignment_id,
350
+ }
351
+ : slot);
352
+ const next = {
353
+ ...current,
354
+ version,
355
+ mutation_id,
356
+ slots: updatedSlots,
357
+ updated_at: now,
358
+ };
359
+ appendEvent(current.id, {
360
+ event_id: crypto.randomUUID(),
361
+ loop_id: current.id,
362
+ seq,
363
+ at: now,
364
+ by: input.actor,
365
+ mutation_id,
366
+ kind: 'turn_assigned',
367
+ slot_id: targetSlot.slot_id,
368
+ phase: current.current_phase,
369
+ assignment_id: input.assignment_id,
370
+ input: input.input,
371
+ }, cwd);
372
+ writeThreadFile(next, cwd);
373
+ return next;
374
+ }
375
+ export function complete_turn(input, cwd) {
376
+ const current = loadLoopOrThrow(input.id, cwd);
377
+ assertMutable(current, 'complete_turn');
378
+ const slot = current.slots.find((s) => s.slot_id === input.slot_id);
379
+ if (!slot)
380
+ throw new Error(`complete_turn: slot_id "${input.slot_id}" not in loop`);
381
+ // Slot-bound auth. Only enforced when caller_agent_id is supplied (MCP entry path).
382
+ if (input.caller_agent_id !== undefined && !input.admin_override) {
383
+ const ownerMatches = slot.agent_id !== undefined && slot.agent_id === input.caller_agent_id;
384
+ const creatorMatches = current.created_by === input.caller_agent_id;
385
+ if (!ownerMatches && !creatorMatches) {
386
+ throw new Error('unauthorized_slot_write');
387
+ }
388
+ }
389
+ const now = nowISO();
390
+ const mutation_id = generateMutationId();
391
+ const version = current.version + 1;
392
+ const outcome = input.outcome ?? 'done';
393
+ let nextArtifacts = current.artifacts;
394
+ let artifactId;
395
+ if (input.artifact) {
396
+ const newArtifact = LoopArtifactSchema.parse({
397
+ ...input.artifact,
398
+ artifact_id: `art_${crypto.randomBytes(6).toString('hex')}`,
399
+ produced_by: slot.slot_id,
400
+ produced_at: now,
401
+ });
402
+ artifactId = newArtifact.artifact_id;
403
+ nextArtifacts = [...nextArtifacts, newArtifact];
404
+ }
405
+ // Map outcome → terminal slot.status so observers reading the thread can
406
+ // distinguish done/failed/cancelled without replaying the event journal.
407
+ const terminalStatus = outcome;
408
+ const updatedSlots = current.slots.map((s) => s.slot_id === slot.slot_id ? { ...s, status: terminalStatus } : s);
409
+ const next = {
410
+ ...current,
411
+ version,
412
+ mutation_id,
413
+ slots: updatedSlots,
414
+ artifacts: nextArtifacts,
415
+ updated_at: now,
416
+ };
417
+ const events = [];
418
+ if (artifactId && input.artifact) {
419
+ events.push({
420
+ event_id: crypto.randomUUID(),
421
+ loop_id: current.id,
422
+ seq: nextSeq(current.id, cwd),
423
+ at: now,
424
+ by: input.actor,
425
+ mutation_id,
426
+ kind: 'artifact_added',
427
+ artifact_id: artifactId,
428
+ phase: input.artifact.phase,
429
+ type: input.artifact.type,
430
+ produced_by: slot.slot_id,
431
+ });
432
+ }
433
+ events.push({
434
+ event_id: crypto.randomUUID(),
435
+ loop_id: current.id,
436
+ seq: nextSeq(current.id, cwd) + events.length,
437
+ at: now,
438
+ by: input.actor,
439
+ mutation_id,
440
+ kind: 'turn_completed',
441
+ slot_id: slot.slot_id,
442
+ phase: slot.phase ?? current.current_phase,
443
+ artifact_id: artifactId,
444
+ outcome,
445
+ failure_reason: input.failure_reason,
446
+ });
447
+ for (const event of events)
448
+ appendEvent(current.id, event, cwd);
449
+ writeThreadFile(next, cwd);
450
+ return next;
451
+ }
452
+ export function add_artifact(input, cwd) {
453
+ const current = loadLoopOrThrow(input.id, cwd);
454
+ assertMutable(current, 'add_artifact');
455
+ const now = nowISO();
456
+ const mutation_id = generateMutationId();
457
+ const version = current.version + 1;
458
+ const seq = nextSeq(current.id, cwd);
459
+ // pln#492 phase 2.b — auto-populate iteration from the thread's current
460
+ // iteration_count when the caller didn't supply one. Iterating loops get
461
+ // accurate per-iteration counts without callers having to track the
462
+ // index; non-iterating loops are unaffected because iteration_count
463
+ // stays at 0.
464
+ const newArtifact = LoopArtifactSchema.parse({
465
+ ...input.artifact,
466
+ artifact_id: `art_${crypto.randomBytes(6).toString('hex')}`,
467
+ produced_at: now,
468
+ iteration: input.artifact.iteration ?? current.iteration_count,
469
+ });
470
+ const next = {
471
+ ...current,
472
+ version,
473
+ mutation_id,
474
+ artifacts: [...current.artifacts, newArtifact],
475
+ updated_at: now,
476
+ };
477
+ appendEvent(current.id, {
478
+ event_id: crypto.randomUUID(),
479
+ loop_id: current.id,
480
+ seq,
481
+ at: now,
482
+ by: input.actor,
483
+ mutation_id,
484
+ kind: 'artifact_added',
485
+ artifact_id: newArtifact.artifact_id,
486
+ phase: newArtifact.phase,
487
+ type: newArtifact.type,
488
+ produced_by: newArtifact.produced_by,
489
+ }, cwd);
490
+ writeThreadFile(next, cwd);
491
+ return next;
492
+ }
493
+ export function pause(input, cwd) {
494
+ const current = loadLoopOrThrow(input.id, cwd);
495
+ if (current.status !== 'open') {
496
+ throw new Error(`pause: loop ${current.id} is ${current.status}, not open`);
497
+ }
498
+ return commitSimpleStatus(current, 'paused', 'paused', input.actor, input.reason, cwd);
499
+ }
500
+ export function resume(input, cwd) {
501
+ const current = loadLoopOrThrow(input.id, cwd);
502
+ if (current.status !== 'paused') {
503
+ throw new Error(`resume: loop ${current.id} is ${current.status}, not paused`);
504
+ }
505
+ return commitSimpleStatus(current, 'open', 'resumed', input.actor, input.reason, cwd);
506
+ }
507
+ function commitSimpleStatus(current, newStatus, eventKind, actor, reason, cwd) {
508
+ const now = nowISO();
509
+ const mutation_id = generateMutationId();
510
+ const version = current.version + 1;
511
+ const seq = nextSeq(current.id, cwd);
512
+ const next = {
513
+ ...current,
514
+ version,
515
+ mutation_id,
516
+ status: newStatus,
517
+ updated_at: now,
518
+ };
519
+ const base = {
520
+ event_id: crypto.randomUUID(),
521
+ loop_id: current.id,
522
+ seq,
523
+ at: now,
524
+ by: actor,
525
+ mutation_id,
526
+ };
527
+ const event = eventKind === 'paused'
528
+ ? { ...base, kind: 'paused', reason }
529
+ : { ...base, kind: 'resumed' };
530
+ appendEvent(current.id, event, cwd);
531
+ writeThreadFile(next, cwd);
532
+ return next;
533
+ }
534
+ //# sourceMappingURL=verbs.js.map
@@ -101,11 +101,18 @@ export function generateMarkdown(state, cwd) {
101
101
  }
102
102
  lines.push('');
103
103
  lines.push('## Open handoffs');
104
- if (state.open_handoffs.length === 0) {
104
+ const MAX_HANDOFFS = 10;
105
+ const MAX_HANDOFF_TEXT = 500;
106
+ const activeHandoffs = state.open_handoffs
107
+ .filter((h) => h.status === 'open')
108
+ .sort((a, b) => b.created_at.localeCompare(a.created_at))
109
+ .slice(0, MAX_HANDOFFS);
110
+ const totalOpen = state.open_handoffs.filter((h) => h.status === 'open').length;
111
+ if (activeHandoffs.length === 0) {
105
112
  lines.push('- (none)');
106
113
  }
107
114
  else {
108
- for (const h of state.open_handoffs) {
115
+ for (const h of activeHandoffs) {
109
116
  const tags = h.tags.length ? ` [${h.tags.join(', ')}]` : '';
110
117
  const paths = h.related_paths?.length ? ` → ${h.related_paths.join(', ')}` : '';
111
118
  const meta = [h.status];
@@ -113,7 +120,12 @@ export function generateMarkdown(state, cwd) {
113
120
  meta.push(`plan: ${h.plan_id}`);
114
121
  if (h.project)
115
122
  meta.push(`project: ${h.project}`);
116
- lines.push(`- **[${h.id}]** ${h.from} ${h.to}: ${h.text} _(${meta.join(', ')})_${paths}${tags}`);
123
+ const text = h.text.length > MAX_HANDOFF_TEXT ? h.text.slice(0, MAX_HANDOFF_TEXT) + '...' : h.text;
124
+ lines.push(`- **[${h.id}]** ${h.from} → ${h.to}: ${text} _(${meta.join(', ')})_${paths}${tags}`);
125
+ }
126
+ const omitted = totalOpen - activeHandoffs.length;
127
+ if (omitted > 0) {
128
+ lines.push(`- _(${omitted} older handoffs omitted — use \`bclaw_read_handoff\` to inspect)_`);
117
129
  }
118
130
  }
119
131
  lines.push('');