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
package/dist/core/io.js CHANGED
@@ -22,9 +22,11 @@ const ENTITY_DIR_MAP = {
22
22
  'instructions': 'memory/instructions',
23
23
  // coordination/ — Agent↔Project: active work state
24
24
  'plans': 'coordination/plans',
25
+ 'sequences': 'coordination/sequences',
25
26
  'claims': 'coordination/claims',
26
27
  'handoffs': 'coordination/handoffs',
27
28
  'sessions': 'coordination/sessions',
29
+ // Shared root: pending candidate JSONs live at inbox/, agent messages at inbox/{agent}/.
28
30
  'inbox': 'coordination/inbox',
29
31
  'inbox/accepted': 'coordination/inbox/accepted',
30
32
  'inbox/rejected': 'coordination/inbox/rejected',
@@ -32,6 +34,9 @@ const ENTITY_DIR_MAP = {
32
34
  'runtime-hosts': 'coordination/runtime-hosts',
33
35
  'runtime-private': 'coordination/runtime-private',
34
36
  'surface-tasks': 'coordination/surface-tasks',
37
+ 'assignments': 'coordination/assignments',
38
+ 'runs': 'coordination/runs',
39
+ 'actions': 'coordination/actions',
35
40
  // discovery/ — Project entity: what's available
36
41
  'bootstrap': 'discovery/bootstrap',
37
42
  'bootstrap/seeds': 'discovery/bootstrap/seeds',
@@ -78,25 +83,97 @@ export function memoryPath(filename, cwd, preferredDirName) {
78
83
  return path.join(memoryDir(cwd, preferredDirName), filename);
79
84
  }
80
85
  export function storeLockPath(cwd, preferredDirName) {
81
- return memoryPath(STORE_LOCK_BASENAME, cwd, preferredDirName);
86
+ const root = cwd ?? process.cwd();
87
+ const dirName = preferredDirName ?? MEMORY_DIR;
88
+ // Keep the store-wide lock alongside the store root so it survives
89
+ // upgrade park/swap renames. Writers and upgrade/rollback all share
90
+ // this stable target.
91
+ return path.join(root, `${dirName}${STORE_LOCK_BASENAME}`);
82
92
  }
83
93
  export function memoryExists(cwd, preferredDirName) {
84
94
  return fs.existsSync(memoryDir(cwd, preferredDirName));
85
95
  }
96
+ /**
97
+ * Read the project vision from the first available source:
98
+ * 1. PROJECT.md at workspace root (human-written, canonical)
99
+ * 2. .brainclaw/project.md first non-header paragraph (legacy project.md export)
100
+ * Returns undefined if no vision is found.
101
+ */
102
+ export function readProjectVision(cwd = process.cwd(), thresholdLines = 20) {
103
+ // 1. PROJECT.md at workspace root — canonical source
104
+ const projectMdPath = path.join(cwd, 'PROJECT.md');
105
+ if (fs.existsSync(projectMdPath)) {
106
+ try {
107
+ const content = fs.readFileSync(projectMdPath, 'utf-8').trim();
108
+ if (content) {
109
+ const lines = content.split('\n');
110
+ if (lines.length <= thresholdLines) {
111
+ return content;
112
+ }
113
+ return `> **Project Domain Rules**\n> This project maintains detailed domain rules and architecture externally to avoid context bloat.\n> You MUST read \`PROJECT.md\` in the workspace root to understand the project constraints, tech stack, and conventions before coding.`;
114
+ }
115
+ }
116
+ catch { /* fall through */ }
117
+ }
118
+ // 2. .brainclaw/project.md — extract description from first paragraph after title
119
+ const legacyPath = path.join(cwd, MEMORY_DIR, 'project.md');
120
+ if (fs.existsSync(legacyPath)) {
121
+ try {
122
+ const content = fs.readFileSync(legacyPath, 'utf-8');
123
+ const vision = extractVisionFromProjectMd(content);
124
+ if (vision)
125
+ return vision;
126
+ }
127
+ catch { /* fall through */ }
128
+ }
129
+ return undefined;
130
+ }
131
+ /**
132
+ * Extract the vision paragraph from the legacy .brainclaw/project.md format.
133
+ * Looks for a description/vision section or the first non-header, non-list paragraph.
134
+ */
135
+ function extractVisionFromProjectMd(content) {
136
+ const lines = content.split('\n');
137
+ // Look for a line that starts with a descriptive phrase, skip headers and list items
138
+ const descriptionLines = [];
139
+ let inSection = false;
140
+ for (const line of lines) {
141
+ const trimmed = line.trim();
142
+ // Skip empty lines, markdown headers, sentinel lines, and list items at start
143
+ if (!trimmed) {
144
+ if (inSection && descriptionLines.length > 0)
145
+ break; // end of paragraph
146
+ continue;
147
+ }
148
+ if (trimmed.startsWith('#') || trimmed.startsWith('>') || trimmed.startsWith('- **['))
149
+ continue;
150
+ if (trimmed.startsWith('- (none)'))
151
+ continue;
152
+ // Found a content line
153
+ inSection = true;
154
+ descriptionLines.push(trimmed);
155
+ }
156
+ return descriptionLines.length > 0 ? descriptionLines.join('\n') : undefined;
157
+ }
158
+ /**
159
+ * Canonical list of entity-aligned subdirectories expected under `.brainclaw/`.
160
+ * Exposed so doctor + repair flows can audit presence without duplicating the
161
+ * list (pln#397 stp_b5337e30).
162
+ */
163
+ export const REQUIRED_ENTITY_SUBDIRS = [
164
+ 'memory/constraints', 'memory/decisions', 'memory/traps', 'memory/instructions',
165
+ 'coordination/plans', 'coordination/sequences', 'coordination/claims', 'coordination/handoffs', 'coordination/sessions',
166
+ 'coordination/inbox',
167
+ 'discovery',
168
+ 'agents',
169
+ ];
86
170
  export function ensureMemoryDir(cwd, preferredDirName) {
87
171
  const dir = memoryDir(cwd, preferredDirName);
88
172
  if (!fs.existsSync(dir)) {
89
173
  fs.mkdirSync(dir, { recursive: true });
90
174
  }
91
175
  // Ensure entity-aligned subdirectories exist
92
- const entityDirs = [
93
- 'memory/constraints', 'memory/decisions', 'memory/traps', 'memory/instructions',
94
- 'coordination/plans', 'coordination/claims', 'coordination/handoffs', 'coordination/sessions',
95
- 'coordination/inbox',
96
- 'discovery',
97
- 'agents',
98
- ];
99
- for (const subdir of entityDirs) {
176
+ for (const subdir of REQUIRED_ENTITY_SUBDIRS) {
100
177
  const p = path.join(dir, subdir);
101
178
  if (!fs.existsSync(p)) {
102
179
  fs.mkdirSync(p, { recursive: true });
@@ -104,8 +181,10 @@ export function ensureMemoryDir(cwd, preferredDirName) {
104
181
  }
105
182
  }
106
183
  export function withStoreLock(cwd = process.cwd(), fn, preferredDirName) {
107
- ensureMemoryDir(cwd, preferredDirName);
108
- return withLock(storeLockPath(cwd, preferredDirName), fn);
184
+ return withLock(storeLockPath(cwd, preferredDirName), () => {
185
+ ensureMemoryDir(cwd, preferredDirName);
186
+ return fn();
187
+ });
109
188
  }
110
189
  /** Check if a path is a file, or a directory with at least one entry. */
111
190
  function hasContent(p) {
package/dist/core/lock.js CHANGED
@@ -37,8 +37,12 @@ function tryCreateLock(lockPath) {
37
37
  return true;
38
38
  }
39
39
  catch (err) {
40
- if (err instanceof Error && 'code' in err && err.code === 'EEXIST')
41
- return false;
40
+ if (err instanceof Error && 'code' in err) {
41
+ const code = err.code;
42
+ if (code === 'EEXIST' || code === 'EPERM' || code === 'EACCES') {
43
+ return false;
44
+ }
45
+ }
42
46
  throw err;
43
47
  }
44
48
  }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * pln#492 phase 2.d.1 — Critic brief assembly (pure function).
3
+ *
4
+ * Builds the message a slot receives when its phase fires. Honours the
5
+ * phase's `context_filter` so the critic sees adversarial memory only
6
+ * (traps + feedback + runtime_notes + critique_history), the proposal
7
+ * sees positive context (decisions + constraints + plans + project_vision),
8
+ * and revision/synthesis see the full bundle via the '*' wildcard.
9
+ *
10
+ * BM25 ranking, top-K, and the underlying memory store are injected via
11
+ * `BriefMemoryProvider` so this module stays testable in isolation. The
12
+ * dispatch wire-up (phase 2.d.2) plugs in a real provider backed by
13
+ * bclaw_search.
14
+ *
15
+ * The bundle is capped at `maxChars` (default 48 000 ≈ 12 000 tokens at
16
+ * ~4 chars/token English) to mitigate trp#179 (oversized payloads on
17
+ * multi-trap projects forcing agents back to CLI fallbacks). Truncation
18
+ * is greedy by category-then-item order, with a "(memory bundle truncated
19
+ * — N items dropped)" tail so the operator can see when content was
20
+ * dropped.
21
+ */
22
+ const DEFAULT_MAX_CHARS = 48_000;
23
+ const DEFAULT_TOP_K_PER_CATEGORY = 8;
24
+ /**
25
+ * Concrete categories the wildcard '*' resolves to. Excludes loop-internal
26
+ * categories (critique_history / revision_history / synthesis_artifact)
27
+ * because those are sourced from the thread itself, not from the memory
28
+ * provider.
29
+ */
30
+ const WILDCARD_USER_FACING_CATEGORIES = [
31
+ 'traps',
32
+ 'feedback',
33
+ 'runtime_notes',
34
+ 'decisions',
35
+ 'constraints',
36
+ 'handoffs',
37
+ 'plans',
38
+ 'candidates',
39
+ 'project_vision',
40
+ ];
41
+ const LOOP_INTERNAL_CATEGORIES = new Set([
42
+ 'critique_history',
43
+ 'revision_history',
44
+ 'synthesis_artifact',
45
+ ]);
46
+ export function buildIdeationBrief(input) {
47
+ const { thread, slotRole, memoryProvider, maxChars = DEFAULT_MAX_CHARS, topKPerCategory = DEFAULT_TOP_K_PER_CATEGORY, } = input;
48
+ const proposal = findProposalArtifact(thread);
49
+ const proposalText = proposal?.body?.trim() ?? '(no proposal seed found)';
50
+ // Resolve which memory categories the current phase wants. If the
51
+ // current phase has no context_filter, fall back to '*' (full bundle).
52
+ const currentPhaseDef = thread.phases.find((p) => p.name === thread.current_phase);
53
+ const requestedCategories = currentPhaseDef?.context_filter ?? ['*'];
54
+ const userFacingCategories = expandUserFacingCategories(requestedCategories);
55
+ const includesLoopInternal = requestedCategories.some((c) => c === '*' || LOOP_INTERNAL_CATEGORIES.has(c));
56
+ const fetchedItemsByCategory = new Map();
57
+ const categoriesUsed = [];
58
+ for (const category of userFacingCategories) {
59
+ const items = memoryProvider.fetch(category, proposalText, topKPerCategory);
60
+ if (items.length > 0) {
61
+ fetchedItemsByCategory.set(category, items);
62
+ categoriesUsed.push(category);
63
+ }
64
+ }
65
+ // Loop-internal categories: pulled from thread.artifacts directly.
66
+ // critique_history → all critique artifacts in iterations < current.
67
+ // revision_history → all revision artifacts in iterations < current.
68
+ // synthesis_artifact → the most recent synthesis output (if any).
69
+ const priorArtifactsBlock = includesLoopInternal
70
+ ? renderPriorArtifactsBlock(thread, requestedCategories)
71
+ : '';
72
+ const header = renderHeader(thread, slotRole, currentPhaseDef?.name ?? thread.current_phase);
73
+ const proposalBlock = renderProposalBlock(proposalText);
74
+ const memoryBlock = renderMemoryBlock(fetchedItemsByCategory);
75
+ const closing = renderClosingInstructions(slotRole, thread.current_phase);
76
+ // Compose with truncation. The proposal seed and header are fixed;
77
+ // memory + prior artifacts share the remaining budget. Memory before
78
+ // prior-artifacts so the critic always sees fresh adversarial pressure.
79
+ const fixedParts = [header, proposalBlock, closing];
80
+ const fixedSize = fixedParts.reduce((n, s) => n + s.length, 0);
81
+ const remainingBudget = Math.max(0, maxChars - fixedSize);
82
+ const { text: truncatedMemory, truncated, droppedItems, includedItems } = truncateToBudget([memoryBlock, priorArtifactsBlock].filter((s) => s.length > 0), fetchedItemsByCategory, remainingBudget);
83
+ const text = [header, proposalBlock, truncatedMemory, closing]
84
+ .filter((s) => s.length > 0)
85
+ .join('\n\n');
86
+ return { text, truncated, includedItems, droppedItems, categoriesUsed };
87
+ }
88
+ /* ─────────────────────── helpers ─────────────────────── */
89
+ function findProposalArtifact(thread) {
90
+ // Prefer the most recent proposal artifact (in case revisions added
91
+ // updated proposal artifacts later).
92
+ for (let i = thread.artifacts.length - 1; i >= 0; i--) {
93
+ const a = thread.artifacts[i];
94
+ if (a.type === 'proposal')
95
+ return a;
96
+ }
97
+ return undefined;
98
+ }
99
+ function expandUserFacingCategories(requested) {
100
+ if (requested.includes('*')) {
101
+ return [...WILDCARD_USER_FACING_CATEGORIES];
102
+ }
103
+ // Drop loop-internal categories — they're handled separately.
104
+ return requested.filter((c) => !LOOP_INTERNAL_CATEGORIES.has(c) && c !== '*');
105
+ }
106
+ function renderHeader(thread, slotRole, phase) {
107
+ const lines = [
108
+ `# ideation_loop brief`,
109
+ `loop: ${thread.id}`,
110
+ `phase: ${phase}`,
111
+ `iteration: ${thread.iteration_count}`,
112
+ `slot: ${slotRole}`,
113
+ `title: ${thread.title}`,
114
+ ];
115
+ if (thread.goal)
116
+ lines.push(`goal: ${thread.goal}`);
117
+ return lines.join('\n');
118
+ }
119
+ function renderProposalBlock(proposalText) {
120
+ return `## proposal\n\n${proposalText}`;
121
+ }
122
+ function renderMemoryBlock(byCategory) {
123
+ if (byCategory.size === 0)
124
+ return '';
125
+ const sections = ['## memory bundle (BM25-ranked, filtered by phase context)'];
126
+ for (const [category, items] of byCategory) {
127
+ sections.push(`### ${category}`);
128
+ for (const item of items) {
129
+ sections.push(`- [${item.id}] ${item.text}`);
130
+ }
131
+ }
132
+ return sections.join('\n');
133
+ }
134
+ function renderPriorArtifactsBlock(thread, requested) {
135
+ // Critique history is the most useful loop-internal feed for the
136
+ // critique phase: round 2+ critics see what was already raised so they
137
+ // do not duplicate. revision_history gives them the proposer's response.
138
+ const wantsCritique = requested.includes('*') || requested.includes('critique_history');
139
+ const wantsRevision = requested.includes('*') || requested.includes('revision_history');
140
+ const wantsSynthesis = requested.includes('*') || requested.includes('synthesis_artifact');
141
+ const sections = [];
142
+ if (wantsCritique) {
143
+ const priorCritique = thread.artifacts.filter((a) => a.type === 'critique' && (a.iteration ?? 0) < thread.iteration_count);
144
+ if (priorCritique.length > 0) {
145
+ const lines = ['### critique_history (prior iterations)'];
146
+ for (const a of priorCritique) {
147
+ lines.push(`- [${a.artifact_id}] (iter ${a.iteration ?? 0}) ${truncateLine(a.body)}`);
148
+ }
149
+ sections.push(lines.join('\n'));
150
+ }
151
+ }
152
+ if (wantsRevision) {
153
+ const priorRevision = thread.artifacts.filter((a) => a.phase === 'revision' && (a.iteration ?? 0) < thread.iteration_count);
154
+ if (priorRevision.length > 0) {
155
+ const lines = ['### revision_history (prior iterations)'];
156
+ for (const a of priorRevision) {
157
+ lines.push(`- [${a.artifact_id}] (iter ${a.iteration ?? 0}) ${truncateLine(a.body)}`);
158
+ }
159
+ sections.push(lines.join('\n'));
160
+ }
161
+ }
162
+ if (wantsSynthesis) {
163
+ const synthesis = [...thread.artifacts]
164
+ .reverse()
165
+ .find((a) => a.phase === 'synthesis' && a.type === 'plan_draft');
166
+ if (synthesis) {
167
+ sections.push(`### synthesis_artifact\n- [${synthesis.artifact_id}] ${truncateLine(synthesis.body)}`);
168
+ }
169
+ }
170
+ if (sections.length === 0)
171
+ return '';
172
+ return ['## prior loop artifacts', ...sections].join('\n\n');
173
+ }
174
+ function renderClosingInstructions(slotRole, phase) {
175
+ return [
176
+ `## what to produce`,
177
+ `- Phase "${phase}" expects you to act in role "${slotRole}".`,
178
+ `- Emit findings as LoopArtifacts via bclaw_loop intent='complete_turn' or 'add_artifact'.`,
179
+ `- Cite the memory ids you relied on so the synthesis can audit coverage.`,
180
+ ].join('\n');
181
+ }
182
+ function truncateLine(s, maxLen = 200) {
183
+ if (!s)
184
+ return '(no body)';
185
+ return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
186
+ }
187
+ /**
188
+ * Greedy truncation of the variable parts (memory bundle + prior artifacts)
189
+ * to fit the remaining budget. We count items so the caller can warn about
190
+ * dropped content.
191
+ */
192
+ function truncateToBudget(blocks, byCategory, budget) {
193
+ const totalItems = [...byCategory.values()].reduce((n, arr) => n + arr.length, 0);
194
+ if (blocks.length === 0) {
195
+ return { text: '', truncated: false, includedItems: 0, droppedItems: 0 };
196
+ }
197
+ const joined = blocks.join('\n\n');
198
+ if (joined.length <= budget) {
199
+ return { text: joined, truncated: false, includedItems: totalItems, droppedItems: 0 };
200
+ }
201
+ // Truncate hard at the budget boundary, append a tail noting the cut.
202
+ const tail = '\n\n_(memory bundle truncated to fit the brief size cap; some items were dropped — re-run with a smaller proposal or a per-category top-K to surface the missing ones)_';
203
+ const headRoom = Math.max(0, budget - tail.length);
204
+ const truncatedHead = joined.slice(0, headRoom);
205
+ const text = truncatedHead + tail;
206
+ // Approximate dropped item count: count "- [id]" markers in the
207
+ // dropped tail vs total.
208
+ const droppedTail = joined.slice(headRoom);
209
+ const droppedItems = (droppedTail.match(/^- \[/gm) ?? []).length;
210
+ const includedItems = Math.max(0, totalItems - droppedItems);
211
+ return { text, truncated: true, includedItems, droppedItems };
212
+ }
213
+ //# sourceMappingURL=brief-assembly.js.map
@@ -0,0 +1,148 @@
1
+ import { z } from 'zod';
2
+ import { LOOP_KINDS, LOOP_STATUSES, LoopLinksSchema, LoopPhaseSchema, LoopRefSchema, LoopSlotSchema, REVIEW_MODES, StopConditionSchema, } from './types.js';
3
+ /**
4
+ * `bclaw_loop(intent)` request schemas — one per intent, unioned into a
5
+ * discriminated schema. Mirrors the BclawLoopInput type from the v8 RFC.
6
+ */
7
+ const CallerEnvelopeFields = {
8
+ agent: z.string().optional(),
9
+ agentId: z.string().optional(),
10
+ client_request_id: z.string().min(1).optional(),
11
+ };
12
+ /**
13
+ * Slot input shape for `bclaw_loop(intent='open')`. Loosens the persisted
14
+ * `LoopSlotSchema` (which requires server-assigned fields like `slot_id`
15
+ * and `status`) so callers only need to supply `role` plus any optional
16
+ * hints. Exported so it can be consumed both by `BclawLoopOpenSchema`
17
+ * below AND by the build-time MCP schema generator (pln#494 phase 2).
18
+ */
19
+ export const LoopSlotInputSchema = LoopSlotSchema.partial().extend({
20
+ role: z.string().min(1),
21
+ });
22
+ export const BclawLoopOpenSchema = z.object({
23
+ intent: z.literal('open'),
24
+ kind: z.enum(LOOP_KINDS),
25
+ title: z.string().min(1),
26
+ goal: z.string().optional(),
27
+ phases: z.array(LoopPhaseSchema).optional(),
28
+ slots: z.array(LoopSlotInputSchema).optional(),
29
+ linked: LoopLinksSchema.optional(),
30
+ stop_condition: StopConditionSchema.optional(),
31
+ mode: z.enum(REVIEW_MODES).optional(),
32
+ // Opt-in acknowledgement that the caller will drive dispatch manually.
33
+ // Absent (or false) → handler rejects with a pointer to bclaw_coordinate,
34
+ // because a loop opened without a follow-up turn/claim/inbox never runs.
35
+ // See pln#461.
36
+ allow_orphan: z.boolean().optional(),
37
+ ...CallerEnvelopeFields,
38
+ });
39
+ export const BclawLoopGetSchema = z.object({
40
+ intent: z.literal('get'),
41
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
42
+ include_events: z.boolean().optional(),
43
+ ...CallerEnvelopeFields,
44
+ });
45
+ export const BclawLoopListSchema = z.object({
46
+ intent: z.literal('list'),
47
+ kind: z.enum(LOOP_KINDS).optional(),
48
+ status: z.enum(LOOP_STATUSES).optional(),
49
+ limit: z.number().int().positive().optional(),
50
+ offset: z.number().int().nonnegative().optional(),
51
+ ...CallerEnvelopeFields,
52
+ });
53
+ export const BclawLoopTurnSchema = z.object({
54
+ intent: z.literal('turn'),
55
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
56
+ slot_id: z.string().optional(),
57
+ role: z.string().optional(),
58
+ input: z.string().optional(),
59
+ assignment_id: z.string().optional(),
60
+ dispatch: z.boolean().optional(),
61
+ expected_version: z.number().int().nonnegative().optional(),
62
+ ...CallerEnvelopeFields,
63
+ });
64
+ export const BclawLoopCompleteTurnSchema = z.object({
65
+ intent: z.literal('complete_turn'),
66
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
67
+ slot_id: z.string().min(1),
68
+ outcome: z.enum(['done', 'failed', 'cancelled']).optional(),
69
+ failure_reason: z.string().optional(),
70
+ artifact: z
71
+ .object({
72
+ phase: z.string().min(1),
73
+ type: z.string().min(1),
74
+ body: z.string().optional(),
75
+ ref: LoopRefSchema.optional(),
76
+ })
77
+ .optional(),
78
+ expected_version: z.number().int().nonnegative().optional(),
79
+ ...CallerEnvelopeFields,
80
+ });
81
+ export const BclawLoopAdvanceSchema = z.object({
82
+ intent: z.literal('advance'),
83
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
84
+ to_phase: z.string().optional(),
85
+ reason: z.string().optional(),
86
+ force: z.boolean().optional(),
87
+ expected_version: z.number().int().nonnegative().optional(),
88
+ ...CallerEnvelopeFields,
89
+ });
90
+ export const BclawLoopAddArtifactSchema = z.object({
91
+ intent: z.literal('add_artifact'),
92
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
93
+ artifact: z.object({
94
+ phase: z.string().min(1),
95
+ type: z.string().min(1),
96
+ body: z.string().optional(),
97
+ produced_by: z.string().optional(),
98
+ ref: LoopRefSchema.optional(),
99
+ }),
100
+ expected_version: z.number().int().nonnegative().optional(),
101
+ ...CallerEnvelopeFields,
102
+ });
103
+ export const BclawLoopPauseSchema = z.object({
104
+ intent: z.literal('pause'),
105
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
106
+ reason: z.string().optional(),
107
+ expected_version: z.number().int().nonnegative().optional(),
108
+ ...CallerEnvelopeFields,
109
+ });
110
+ export const BclawLoopResumeSchema = z.object({
111
+ intent: z.literal('resume'),
112
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
113
+ expected_version: z.number().int().nonnegative().optional(),
114
+ ...CallerEnvelopeFields,
115
+ });
116
+ export const BclawLoopCloseSchema = z.object({
117
+ intent: z.literal('close'),
118
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
119
+ status: z.enum(['completed', 'cancelled', 'blocked']),
120
+ reason: z.string().optional(),
121
+ expected_version: z.number().int().nonnegative().optional(),
122
+ ...CallerEnvelopeFields,
123
+ });
124
+ export const BclawLoopRequestSchema = z.discriminatedUnion('intent', [
125
+ BclawLoopOpenSchema,
126
+ BclawLoopGetSchema,
127
+ BclawLoopListSchema,
128
+ BclawLoopTurnSchema,
129
+ BclawLoopCompleteTurnSchema,
130
+ BclawLoopAdvanceSchema,
131
+ BclawLoopAddArtifactSchema,
132
+ BclawLoopPauseSchema,
133
+ BclawLoopResumeSchema,
134
+ BclawLoopCloseSchema,
135
+ ]);
136
+ export const BCLAW_LOOP_INTENTS = [
137
+ 'open',
138
+ 'get',
139
+ 'list',
140
+ 'turn',
141
+ 'complete_turn',
142
+ 'advance',
143
+ 'add_artifact',
144
+ 'pause',
145
+ 'resume',
146
+ 'close',
147
+ ];
148
+ //# sourceMappingURL=facade-schema.js.map
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+ export { closeLoop, ensureLoopsDir, generateLoopId, generateMutationId, generateSlotId, getLoop, listLoopEvents, listLoops, openLoop, } from './store.js';
3
+ export { add_artifact, advance, complete_turn, evaluatePhaseAdvanceGate, evaluateStopCondition, pause, resume, turn, } from './verbs.js';
4
+ export { decideNextPhase, artifactsInIteration, noNewCritiqueInIteration, hasCriticSignalInIteration, } from './iteration-engine.js';
5
+ export { buildIdeationBrief, } from './brief-assembly.js';
6
+ export { acquireLock, hashRequest, recordConflict, withLoopLock, DEFAULT_MAX_MUTATION_DURATION_MS, IDEMPOTENCY_TTL_MS, LEASE_GRACE_MS, LEASE_WINDOW_MS, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, LockLostError, LockTimeoutError, VersionConflictError, } from './lock.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,139 @@
1
+ /**
2
+ * pln#492 phase 2.b — Finite state machine for ideation_loop iteration.
3
+ *
4
+ * The driver consults this module before mutating phase state on a loop
5
+ * with an `iteration` block. The engine is pure: it inspects a thread +
6
+ * its protocol shape and returns a structured decision. Applying the
7
+ * decision (mutating thread, appending events, persisting) is the
8
+ * advance() verb's job.
9
+ *
10
+ * Naming the engine an FSM in the file structure is a deliberate
11
+ * implementation discipline (stp_af207293 in pln#492): states = phases,
12
+ * transitions = exit_when conditions + cycle membership, guards = phase
13
+ * advance_gate, actions = system event emissions.
14
+ */
15
+ /**
16
+ * Decide the next phase given the current thread state and the protocol.
17
+ *
18
+ * Throws if `current_phase` is not in `protocol.phases`. Throws if there
19
+ * is no successor (last phase + no iteration block). Callers that want
20
+ * to handle "already at end" should check beforehand.
21
+ */
22
+ export function decideNextPhase(thread, protocol) {
23
+ const phaseNames = protocol.phases.map((p) => p.name);
24
+ const currentIndex = phaseNames.indexOf(thread.current_phase);
25
+ if (currentIndex < 0) {
26
+ throw new Error(`decideNextPhase: current_phase "${thread.current_phase}" not in protocol.phases`);
27
+ }
28
+ const cycle = protocol.iteration?.cycle ?? [];
29
+ const inCycle = cycle.includes(thread.current_phase);
30
+ // Path 1 — current phase is not in the iteration cycle. Linear advance.
31
+ if (!inCycle) {
32
+ if (currentIndex + 1 >= phaseNames.length) {
33
+ throw new Error(`advance: already at last phase "${thread.current_phase}"`);
34
+ }
35
+ return {
36
+ kind: 'advance_to',
37
+ target: phaseNames[currentIndex + 1],
38
+ iteration: thread.iteration_count,
39
+ };
40
+ }
41
+ // Path 2 — current phase is inside the cycle. Two sub-cases:
42
+ // 2a. Not at the end of the cycle yet → step within cycle.
43
+ // 2b. At the end of the cycle → either iterate, exit by exit_when, or
44
+ // exit by max_iterations.
45
+ const cycleIndex = cycle.indexOf(thread.current_phase);
46
+ const atCycleEnd = cycleIndex === cycle.length - 1;
47
+ if (!atCycleEnd) {
48
+ return {
49
+ kind: 'advance_to',
50
+ target: cycle[cycleIndex + 1],
51
+ iteration: thread.iteration_count,
52
+ };
53
+ }
54
+ // 2b — end of cycle. Compute the post-cycle target (phase after the
55
+ // last cycle phase in the protocol order). We use the protocol's
56
+ // declared phase order, not the cycle, because the cycle may be a
57
+ // sub-sequence of phases (e.g. [critique, revision] within
58
+ // [proposal, critique, revision, synthesis]).
59
+ const lastCyclePhaseIndex = phaseNames.indexOf(cycle[cycle.length - 1]);
60
+ if (lastCyclePhaseIndex < 0 || lastCyclePhaseIndex + 1 >= phaseNames.length) {
61
+ throw new Error(`decideNextPhase: cycle's last phase "${cycle[cycle.length - 1]}" has no post-cycle successor`);
62
+ }
63
+ const postCycleTarget = phaseNames[lastCyclePhaseIndex + 1];
64
+ const iterationBlock = protocol.iteration;
65
+ if (!iterationBlock) {
66
+ // Defensive: `inCycle` was true so iteration should be set. If we
67
+ // got here despite that, fall through to a linear advance.
68
+ return {
69
+ kind: 'advance_to',
70
+ target: postCycleTarget,
71
+ iteration: thread.iteration_count,
72
+ };
73
+ }
74
+ // Evaluate exit_when on the just-finished iteration. The current
75
+ // iteration_count is the one that just completed (the engine has not
76
+ // yet incremented).
77
+ if (iterationBlock.exit_when === 'critic_signal' &&
78
+ hasCriticSignalInIteration(thread, thread.iteration_count)) {
79
+ return {
80
+ kind: 'exit_cycle',
81
+ target: postCycleTarget,
82
+ iteration: thread.iteration_count,
83
+ reason: 'critic_signal',
84
+ };
85
+ }
86
+ if (iterationBlock.exit_when === 'no_new_critique_artifacts' &&
87
+ noNewCritiqueInIteration(thread, thread.iteration_count)) {
88
+ return {
89
+ kind: 'exit_cycle',
90
+ target: postCycleTarget,
91
+ iteration: thread.iteration_count,
92
+ reason: 'no_new_critique_artifacts',
93
+ };
94
+ }
95
+ // Cap check. If incrementing iteration_count would exceed the cap,
96
+ // exit via max_iterations. (iteration_count is 0-indexed; cap=3 means
97
+ // iterations 0, 1, 2 are allowed; refusing iteration 3 → exit.)
98
+ if (thread.iteration_count + 1 >= iterationBlock.max_iterations) {
99
+ return {
100
+ kind: 'max_iterations',
101
+ target: postCycleTarget,
102
+ iteration: thread.iteration_count + 1,
103
+ max: iterationBlock.max_iterations,
104
+ };
105
+ }
106
+ // Otherwise iterate: cycle back to the first phase of the cycle and
107
+ // bump iteration_count.
108
+ return {
109
+ kind: 'iterate_to',
110
+ target: cycle[0],
111
+ iteration: thread.iteration_count + 1,
112
+ };
113
+ }
114
+ /**
115
+ * Returns the artifacts produced in a specific iteration window. If an
116
+ * artifact has no `iteration` field (legacy, non-iterating loops, or
117
+ * pre-phase-2.b data), it is treated as belonging to iteration 0 so
118
+ * existing review_loop usage is unaffected.
119
+ */
120
+ export function artifactsInIteration(thread, iteration) {
121
+ return thread.artifacts.filter((a) => (a.iteration ?? 0) === iteration);
122
+ }
123
+ /**
124
+ * `exit_when='no_new_critique_artifacts'` predicate: true when the
125
+ * just-completed iteration produced no critique-typed artifacts. Used
126
+ * by `decideNextPhase` at the cycle boundary.
127
+ */
128
+ export function noNewCritiqueInIteration(thread, iteration) {
129
+ return !thread.artifacts.some((a) => (a.iteration ?? 0) === iteration && a.type === 'critique');
130
+ }
131
+ /**
132
+ * `exit_when='critic_signal'` predicate: true when the iteration
133
+ * contains a `type='critic_signal'` artifact (any subtype/body). The
134
+ * critic emits this when it judges the proposal sufficient.
135
+ */
136
+ export function hasCriticSignalInIteration(thread, iteration) {
137
+ return thread.artifacts.some((a) => (a.iteration ?? 0) === iteration && a.type === 'critic_signal');
138
+ }
139
+ //# sourceMappingURL=iteration-engine.js.map