brainclaw 1.8.0 → 1.9.1

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 (178) hide show
  1. package/README.md +592 -505
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +138 -13
  4. package/dist/commands/add-step.js +1 -1
  5. package/dist/commands/bootstrap.js +2 -26
  6. package/dist/commands/check-security-mcp.js +50 -33
  7. package/dist/commands/check-security.js +86 -43
  8. package/dist/commands/claim.js +22 -21
  9. package/dist/commands/confirm.js +26 -0
  10. package/dist/commands/context-diff.js +1 -1
  11. package/dist/commands/dispatch-watch.js +142 -0
  12. package/dist/commands/doctor.js +113 -2
  13. package/dist/commands/estimation-report.js +115 -16
  14. package/dist/commands/harvest.js +286 -23
  15. package/dist/commands/hooks.js +73 -73
  16. package/dist/commands/init.js +124 -22
  17. package/dist/commands/install-hooks.js +78 -78
  18. package/dist/commands/loops-handlers.js +4 -0
  19. package/dist/commands/mcp-read-handlers.js +253 -41
  20. package/dist/commands/mcp.js +664 -102
  21. package/dist/commands/memory.js +21 -17
  22. package/dist/commands/migrate.js +81 -17
  23. package/dist/commands/prune.js +78 -4
  24. package/dist/commands/reflect.js +26 -20
  25. package/dist/commands/register-agent.js +57 -1
  26. package/dist/commands/repair.js +20 -0
  27. package/dist/commands/session-end.js +15 -6
  28. package/dist/commands/session-start.js +18 -1
  29. package/dist/commands/setup-security.js +39 -18
  30. package/dist/commands/setup.js +26 -27
  31. package/dist/commands/stale.js +16 -2
  32. package/dist/commands/switch.js +26 -5
  33. package/dist/commands/uninstall.js +126 -34
  34. package/dist/commands/update-step.js +6 -0
  35. package/dist/commands/version.js +1 -1
  36. package/dist/commands/worktree.js +60 -0
  37. package/dist/core/actions.js +12 -3
  38. package/dist/core/agent-capability.js +30 -17
  39. package/dist/core/agent-files.js +963 -666
  40. package/dist/core/agent-integrations.js +0 -3
  41. package/dist/core/agent-inventory.js +67 -0
  42. package/dist/core/agent-registry.js +163 -29
  43. package/dist/core/agentrun-reconciler.js +33 -2
  44. package/dist/core/agentruns.js +7 -1
  45. package/dist/core/ai-agent-detection.js +31 -44
  46. package/dist/core/archival.js +15 -9
  47. package/dist/core/assignment-reconciler.js +56 -0
  48. package/dist/core/assignment-sweeper.js +127 -4
  49. package/dist/core/assignments.js +69 -11
  50. package/dist/core/bootstrap.js +233 -67
  51. package/dist/core/brainclaw-version.js +22 -0
  52. package/dist/core/candidates.js +21 -1
  53. package/dist/core/claims.js +313 -150
  54. package/dist/core/codev-prompts.js +38 -38
  55. package/dist/core/config.js +6 -1
  56. package/dist/core/context-diff.js +148 -20
  57. package/dist/core/context.js +129 -8
  58. package/dist/core/coordination.js +22 -3
  59. package/dist/core/default-profiles/doctor.yaml +11 -11
  60. package/dist/core/default-profiles/janitor.yaml +11 -11
  61. package/dist/core/default-profiles/onboarder.yaml +11 -11
  62. package/dist/core/default-profiles/reviewer.yaml +13 -13
  63. package/dist/core/dispatch-status.js +79 -5
  64. package/dist/core/dispatcher.js +65 -12
  65. package/dist/core/entity-operations.js +74 -27
  66. package/dist/core/entity-registry.js +31 -5
  67. package/dist/core/event-log.js +138 -21
  68. package/dist/core/events/checkpoint.js +258 -0
  69. package/dist/core/events/genesis.js +220 -0
  70. package/dist/core/events/journal.js +507 -0
  71. package/dist/core/events/materialize.js +126 -0
  72. package/dist/core/events/registry-post-image.js +110 -0
  73. package/dist/core/events/verify.js +109 -0
  74. package/dist/core/execution-adapters.js +23 -0
  75. package/dist/core/execution.js +1 -1
  76. package/dist/core/facade-schema.js +38 -0
  77. package/dist/core/gc-semantic.js +130 -5
  78. package/dist/core/handoff-snapshot.js +68 -0
  79. package/dist/core/ids.js +19 -8
  80. package/dist/core/instruction-templates.js +34 -115
  81. package/dist/core/io.js +39 -3
  82. package/dist/core/json-store.js +10 -1
  83. package/dist/core/lock.js +153 -28
  84. package/dist/core/loops/bootstrap-acquire.js +25 -1
  85. package/dist/core/loops/facade-schema.js +2 -0
  86. package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
  87. package/dist/core/loops/index.js +1 -0
  88. package/dist/core/loops/presets/bootstrap.js +7 -0
  89. package/dist/core/loops/store.js +17 -0
  90. package/dist/core/loops/verbs.js +24 -2
  91. package/dist/core/markdown.js +8 -76
  92. package/dist/core/mcp-command-resolution.js +245 -0
  93. package/dist/core/memory-compactor.js +5 -3
  94. package/dist/core/memory-lifecycle.js +282 -0
  95. package/dist/core/merge-risk.js +150 -0
  96. package/dist/core/messaging.js +10 -3
  97. package/dist/core/migration.js +11 -1
  98. package/dist/core/observer-mode.js +26 -0
  99. package/dist/core/operations/memory-mutation.js +90 -65
  100. package/dist/core/operations/plan.js +27 -1
  101. package/dist/core/protocol-skills.js +210 -0
  102. package/dist/core/reflection-safety.js +6 -7
  103. package/dist/core/reputation.js +84 -2
  104. package/dist/core/runtime-signals.js +72 -10
  105. package/dist/core/runtime.js +84 -1
  106. package/dist/core/schema.js +114 -0
  107. package/dist/core/search.js +19 -2
  108. package/dist/core/security-detectors.js +125 -0
  109. package/dist/core/security-extract.js +189 -0
  110. package/dist/core/security-guard.js +217 -139
  111. package/dist/core/security-packages.js +121 -0
  112. package/dist/core/security-scoring.js +76 -9
  113. package/dist/core/security.js +34 -2
  114. package/dist/core/sequence.js +11 -2
  115. package/dist/core/setup-flow.js +141 -13
  116. package/dist/core/spawn-check.js +16 -2
  117. package/dist/core/staleness.js +73 -2
  118. package/dist/core/state.js +250 -54
  119. package/dist/core/store-resolution.js +45 -12
  120. package/dist/core/worktree.js +90 -26
  121. package/dist/facts.js +8 -8
  122. package/dist/facts.json +7 -7
  123. package/docs/PROTOCOL.md +223 -0
  124. package/docs/adapters/openclaw.md +43 -43
  125. package/docs/architecture/project-refs.md +328 -328
  126. package/docs/cli.md +2097 -2096
  127. package/docs/concepts/coordination.md +52 -52
  128. package/docs/concepts/coordinator-runbook.md +129 -0
  129. package/docs/concepts/dispatch-lifecycle.md +245 -245
  130. package/docs/concepts/event-log-store.md +928 -0
  131. package/docs/concepts/ideation-loop.md +317 -317
  132. package/docs/concepts/loop-engine.md +520 -511
  133. package/docs/concepts/mcp-governance.md +268 -268
  134. package/docs/concepts/memory.md +89 -88
  135. package/docs/concepts/multi-agent-workflows.md +167 -167
  136. package/docs/concepts/observer-protocol.md +361 -0
  137. package/docs/concepts/parallel-merge-protocol.md +71 -0
  138. package/docs/concepts/plans-and-claims.md +217 -174
  139. package/docs/concepts/project-md-convention.md +35 -35
  140. package/docs/concepts/runtime-notes.md +38 -38
  141. package/docs/concepts/skills.md +78 -0
  142. package/docs/concepts/troubleshooting.md +254 -254
  143. package/docs/concepts/workspace-bootstrapping.md +142 -81
  144. package/docs/context-format-changelog.md +35 -35
  145. package/docs/context-format.md +48 -48
  146. package/docs/index.md +65 -65
  147. package/docs/integrations/agents.md +162 -162
  148. package/docs/integrations/claude-code.md +23 -23
  149. package/docs/integrations/cline.md +87 -88
  150. package/docs/integrations/codex.md +2 -2
  151. package/docs/integrations/continue.md +60 -60
  152. package/docs/integrations/copilot.md +82 -80
  153. package/docs/integrations/cursor.md +23 -23
  154. package/docs/integrations/kilocode.md +72 -72
  155. package/docs/integrations/mcp.md +377 -377
  156. package/docs/integrations/mistral-vibe.md +122 -122
  157. package/docs/integrations/openclaw.md +99 -98
  158. package/docs/integrations/opencode.md +84 -84
  159. package/docs/integrations/overview.md +122 -122
  160. package/docs/integrations/roo.md +74 -74
  161. package/docs/integrations/windsurf.md +83 -83
  162. package/docs/mcp-schema-changelog.md +360 -329
  163. package/docs/playbooks/integration/index.md +121 -121
  164. package/docs/playbooks/orchestration.md +37 -0
  165. package/docs/playbooks/productivity/index.md +99 -99
  166. package/docs/playbooks/team/index.md +117 -117
  167. package/docs/product/agent-first-model.md +184 -184
  168. package/docs/product/entity-model-audit.md +462 -462
  169. package/docs/product/positioning.md +86 -86
  170. package/docs/quickstart-existing-project.md +107 -107
  171. package/docs/quickstart.md +148 -147
  172. package/docs/release-maintenance.md +79 -79
  173. package/docs/reputation.md +52 -52
  174. package/docs/review.md +45 -45
  175. package/docs/security.md +212 -53
  176. package/docs/server-operations.md +118 -118
  177. package/docs/storage.md +110 -108
  178. package/package.json +86 -69
@@ -53,8 +53,8 @@ export function runBootstrapProfile(options = {}) {
53
53
  const existing = loadBootstrapProfile(cwd);
54
54
  const existingPlan = loadBootstrapImportPlan(cwd);
55
55
  const lastApplication = loadBootstrapApplication(cwd);
56
- const existingFingerprint = currentRepoFingerprint(cwd);
57
- if (!options.refresh && existing && existingPlan && isProfileReusable(existing, target, existingFingerprint)) {
56
+ const sourceFingerprint = currentSourceFingerprint(cwd, target);
57
+ if (!options.refresh && existing && existingPlan && isProfileReusable(existing, target, sourceFingerprint)) {
58
58
  const seeds = listBootstrapSeeds(cwd);
59
59
  const importPlan = interviewAnswers.length > 0
60
60
  ? buildBootstrapImportPlan({
@@ -81,7 +81,7 @@ export function runBootstrapProfile(options = {}) {
81
81
  reusedProfile: true,
82
82
  };
83
83
  }
84
- const artifacts = buildBootstrapArtifacts({ cwd, target, repoFingerprint: existingFingerprint });
84
+ const artifacts = buildBootstrapArtifacts({ cwd, target, sourceFingerprint });
85
85
  persistBootstrapArtifacts(artifacts, cwd);
86
86
  const importPlan = interviewAnswers.length > 0
87
87
  ? buildBootstrapImportPlan({
@@ -124,7 +124,7 @@ export function hasReusableBootstrapProfile(target, cwd) {
124
124
  if (!profile) {
125
125
  return false;
126
126
  }
127
- return isProfileReusable(profile, normalizeTarget(target), currentRepoFingerprint(cwd ?? process.cwd()));
127
+ return isProfileReusable(profile, normalizeTarget(target), currentSourceFingerprint(cwd ?? process.cwd(), target));
128
128
  }
129
129
  export function selectDerivedSignals(target, maxSignals, cwd) {
130
130
  const normalizedTarget = normalizeTarget(target);
@@ -342,7 +342,8 @@ function buildBootstrapArtifacts(input) {
342
342
  profile: BootstrapProfileDocumentSchema.parse({
343
343
  schema_version: DERIVED_SCHEMA_VERSION,
344
344
  derived_at: nowISO(),
345
- repo_fingerprint: gitProbe.repoFingerprint ?? input.repoFingerprint,
345
+ repo_fingerprint: gitProbe.repoFingerprint,
346
+ source_fingerprint: input.sourceFingerprint ?? currentSourceFingerprint(input.cwd, input.target),
346
347
  summary,
347
348
  sources_scanned: [...new Set(sourcesScanned)],
348
349
  git_available: gitProbe.available,
@@ -627,28 +628,9 @@ function extractRepoAnalysisSeeds(result, target) {
627
628
  }
628
629
  function extractExecutionContextSeeds(snapshot, target) {
629
630
  const seeds = [];
630
- if (snapshot.branch) {
631
- seeds.push(createSeed({
632
- text: `Current branch: ${snapshot.branch}`,
633
- seedKind: 'environment',
634
- sourceKind: 'machine',
635
- sourceRef: 'git:branch',
636
- confidence: 'high',
637
- tags: ['bootstrap', 'execution', 'git'],
638
- relatedPaths: target ? [target] : undefined,
639
- }));
640
- }
641
- if (snapshot.git_status === 'dirty') {
642
- seeds.push(createSeed({
643
- text: 'Repository has uncommitted changes.',
644
- seedKind: 'warning',
645
- sourceKind: 'machine',
646
- sourceRef: 'git:status',
647
- confidence: 'high',
648
- tags: ['bootstrap', 'execution', 'git'],
649
- relatedPaths: target ? [target] : undefined,
650
- }));
651
- }
631
+ // Branch + dirty-status are execution-context-volatile (change on every
632
+ // checkout/edit). They stay on the live snapshot for display but must NOT
633
+ // be persisted as seeds — otherwise every dispatch invalidates the cache.
652
634
  for (const tool of snapshot.toolchains.slice(0, 3)) {
653
635
  seeds.push(createSeed({
654
636
  text: `Toolchain available: ${tool.name}${tool.version ? ` ${tool.version}` : ''}`,
@@ -735,25 +717,9 @@ function probeGit(cwd, target) {
735
717
  }));
736
718
  }
737
719
  }
738
- // Step 13: Active branches
739
- const branchResult = spawnSync('git', ['branch', '--no-merged', 'HEAD', '--format=%(refname:short)'], {
740
- cwd,
741
- encoding: 'utf-8',
742
- timeout: 5000,
743
- });
744
- if (branchResult.status === 0) {
745
- const branches = branchResult.stdout.split(/\r?\n/).map((b) => b.trim()).filter(Boolean).slice(0, 5);
746
- for (const branch of branches) {
747
- hotspotSeeds.push(createSeed({
748
- text: `Active branch: ${branch}`,
749
- seedKind: 'hotspot',
750
- sourceKind: 'git',
751
- sourceRef: `branch:${branch}`,
752
- confidence: 'low',
753
- tags: ['bootstrap', 'git', 'branch'],
754
- }));
755
- }
756
- }
720
+ // Active branch names are execution-context-volatile (branches come and go
721
+ // every dispatch); we deliberately do not persist them as seeds — see the
722
+ // transient-seed policy in extractExecutionContextSeeds.
757
723
  // Step 13: Recent tags
758
724
  const tagResult = spawnSync('git', ['tag', '--sort=-creatordate', '-l'], {
759
725
  cwd,
@@ -827,18 +793,59 @@ function isProfileReusable(profile, target, currentFingerprint) {
827
793
  if ((profile.target ?? undefined) !== target) {
828
794
  return false;
829
795
  }
830
- if (profile.repo_fingerprint && currentFingerprint) {
831
- return profile.repo_fingerprint === currentFingerprint;
796
+ // Content fingerprint of the harvested sources, not git HEAD: a commit
797
+ // that touches no harvested doc/manifest must not trigger a full re-scan
798
+ // (which previously ran spawnSync git ×N + fs walks on every commit for
799
+ // low-density stores). Profiles persisted before source_fingerprint
800
+ // existed re-scan once and migrate.
801
+ if (profile.source_fingerprint !== currentFingerprint) {
802
+ return false;
803
+ }
804
+ // TTL backstop: git-history-derived seeds (hotspots) drift without any
805
+ // harvested file changing, so cap profile reuse in time.
806
+ const derivedAt = Date.parse(profile.derived_at);
807
+ if (!Number.isFinite(derivedAt) || Date.now() - derivedAt > BOOTSTRAP_PROFILE_TTL_MS) {
808
+ return false;
832
809
  }
833
810
  return true;
834
811
  }
835
- function currentRepoFingerprint(cwd) {
836
- const result = spawnSync('git', ['rev-parse', 'HEAD'], {
837
- cwd,
838
- encoding: 'utf-8',
839
- timeout: 5000,
840
- });
841
- return result.status === 0 ? result.stdout.trim() : undefined;
812
+ const BOOTSTRAP_PROFILE_TTL_MS = 24 * 60 * 60 * 1000;
813
+ const SOURCE_FINGERPRINT_EXTRA_FILES = [
814
+ 'package.json',
815
+ MAKEFILE_NAME,
816
+ ];
817
+ function currentSourceFingerprint(cwd, target) {
818
+ const scanRoot = resolveBootstrapScanRoot(cwd, normalizeTarget(target));
819
+ const candidates = new Set();
820
+ const readmePath = findFirstExisting(scanRoot, README_CANDIDATES);
821
+ if (readmePath) {
822
+ candidates.add(readmePath);
823
+ }
824
+ for (const relativePath of discoverNativeInstructionFiles(scanRoot)) {
825
+ candidates.add(path.join(scanRoot, relativePath));
826
+ }
827
+ for (const relativePath of [
828
+ ...SOURCE_FINGERPRINT_EXTRA_FILES,
829
+ ...CI_FILES,
830
+ ...CONTRIBUTING_FILES,
831
+ ...CHANGELOG_FILES,
832
+ ...DOCKER_FILES,
833
+ ...ENV_EXAMPLE_FILES,
834
+ ]) {
835
+ candidates.add(path.join(scanRoot, relativePath));
836
+ }
837
+ const entries = [];
838
+ for (const filepath of candidates) {
839
+ try {
840
+ const stat = fs.statSync(filepath);
841
+ if (stat.isFile()) {
842
+ entries.push(`${path.relative(scanRoot, filepath).replace(/\\/g, '/')}|${stat.size}|${Math.trunc(stat.mtimeMs)}`);
843
+ }
844
+ }
845
+ catch { /* missing file — not part of the fingerprint */ }
846
+ }
847
+ entries.sort();
848
+ return `src1:${crypto.createHash('sha1').update(entries.join('\n')).digest('hex')}`;
842
849
  }
843
850
  function createSeed(input) {
844
851
  return MemorySeedDocumentSchema.parse({
@@ -1167,6 +1174,9 @@ function renderBootstrapInterviewRationale(question) {
1167
1174
  return `Confirmed via bootstrap interview answer to ${question.id}: ${question.prompt}`;
1168
1175
  }
1169
1176
  function seedToBootstrapSuggestion(seed, allowSummaryFallback) {
1177
+ if (seed.seed_kind === 'decision' || seed.seed_kind === 'constraint' || seed.seed_kind === 'trap') {
1178
+ return seedToTypedSuggestion(seed);
1179
+ }
1170
1180
  if (seed.seed_kind !== 'agent_rule' && seed.seed_kind !== 'command') {
1171
1181
  return undefined;
1172
1182
  }
@@ -1192,6 +1202,30 @@ function seedToBootstrapSuggestion(seed, allowSummaryFallback) {
1192
1202
  reversible: true,
1193
1203
  };
1194
1204
  }
1205
+ function seedToTypedSuggestion(seed) {
1206
+ const target = seed.seed_kind === 'decision'
1207
+ ? 'decision'
1208
+ : seed.seed_kind === 'constraint' ? 'constraint' : 'trap';
1209
+ const base = {
1210
+ id: generateId('bootstrap_suggestions'),
1211
+ target,
1212
+ text: seed.text,
1213
+ rationale: renderBootstrapSuggestionRationale(seed),
1214
+ confidence: seed.confidence,
1215
+ source_seed_ids: [seed.id],
1216
+ source_refs: [seed.source_ref],
1217
+ tags: normalizeBootstrapSuggestionTags(seed.tags),
1218
+ related_paths: seed.related_paths,
1219
+ reversible: true,
1220
+ };
1221
+ if (target === 'constraint') {
1222
+ return { ...base, category: 'process' };
1223
+ }
1224
+ if (target === 'trap') {
1225
+ return { ...base, severity: 'medium' };
1226
+ }
1227
+ return { ...base, outcome: 'pending' };
1228
+ }
1195
1229
  function inferBootstrapInstructionTarget(seed) {
1196
1230
  if (seed.source_kind === 'agents_md') {
1197
1231
  return { layer: 'global' };
@@ -1231,6 +1265,8 @@ function renderBootstrapSuggestionRationale(seed) {
1231
1265
  return `Derived from ${seed.source_ref}`;
1232
1266
  case 'manifest':
1233
1267
  return `Derived from ${seed.source_ref}`;
1268
+ case 'adr':
1269
+ return `Derived from architecture decision record ${seed.source_ref}`;
1234
1270
  default:
1235
1271
  return `Derived from ${seed.source_ref}`;
1236
1272
  }
@@ -1485,7 +1521,9 @@ export function uninstallBootstrapImport(cwd) {
1485
1521
  deletedCount++;
1486
1522
  }
1487
1523
  if (stateChanged) {
1488
- persistState(state, resolvedCwd, { writeProjectMarkdown: false });
1524
+ // deleteMissing: uninstall removes managed artifacts — their files must be
1525
+ // unlinked. Safe: loadState above runs under this same mutate() lock.
1526
+ persistState(state, resolvedCwd, { writeProjectMarkdown: false, deleteMissing: true });
1489
1527
  }
1490
1528
  if (deactivatedCount > 0 || deletedCount > 0) {
1491
1529
  rebuildProjectMd(loadState(resolvedCwd), resolvedCwd);
@@ -1592,8 +1630,14 @@ function scoreSeed(seed, target) {
1592
1630
  }
1593
1631
  function seedKindWeight(kind) {
1594
1632
  switch (kind) {
1633
+ case 'decision':
1634
+ return 13;
1595
1635
  case 'agent_rule':
1596
1636
  return 12;
1637
+ case 'constraint':
1638
+ return 11;
1639
+ case 'trap':
1640
+ return 11;
1597
1641
  case 'warning':
1598
1642
  return 10;
1599
1643
  case 'tooling':
@@ -1804,17 +1848,24 @@ function extractAdditionalBrownfieldSeeds(cwd, target) {
1804
1848
  if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
1805
1849
  sources.push('adr');
1806
1850
  try {
1807
- const files = fs.readdirSync(fullPath).filter((f) => f.endsWith('.md'));
1808
- if (files.length > 0) {
1809
- seeds.push(createSeed({
1810
- text: `Architecture Decision Records: ${files.length} ADR(s) in ${dir}/`,
1811
- seedKind: 'convention',
1812
- sourceKind: 'adr',
1813
- sourceRef: dir,
1814
- confidence: 'high',
1815
- tags: ['bootstrap', 'adr', 'architecture'],
1816
- relatedPaths: target ? [target] : undefined,
1817
- }));
1851
+ const files = fs.readdirSync(fullPath)
1852
+ .filter((f) => f.endsWith('.md'))
1853
+ .map((name) => {
1854
+ const filePath = path.join(fullPath, name);
1855
+ let mtimeMs = 0;
1856
+ try {
1857
+ mtimeMs = fs.statSync(filePath).mtimeMs;
1858
+ }
1859
+ catch { /* ignore */ }
1860
+ return { name, filePath, mtimeMs };
1861
+ })
1862
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
1863
+ .slice(0, ADR_READ_LIMIT);
1864
+ for (const entry of files) {
1865
+ const adrSeed = extractAdrSeed(entry.filePath, dir, target);
1866
+ if (adrSeed) {
1867
+ seeds.push(adrSeed);
1868
+ }
1818
1869
  }
1819
1870
  }
1820
1871
  catch { /* skip unreadable */ }
@@ -1823,4 +1874,119 @@ function extractAdditionalBrownfieldSeeds(cwd, target) {
1823
1874
  }
1824
1875
  return { seeds, sources: [...new Set(sources)] };
1825
1876
  }
1877
+ const ADR_READ_LIMIT = 20;
1878
+ const ADR_DECISION_EXCERPT_CAP = 600;
1879
+ function extractAdrSeed(filePath, dirRef, target) {
1880
+ let content;
1881
+ try {
1882
+ content = fs.readFileSync(filePath, 'utf-8');
1883
+ }
1884
+ catch {
1885
+ return undefined;
1886
+ }
1887
+ if (!content.trim()) {
1888
+ return undefined;
1889
+ }
1890
+ const parsed = parseAdrMarkdown(content);
1891
+ const baseName = path.basename(filePath);
1892
+ const sourceRef = path.posix.join(dirRef.replace(/\\/g, '/'), baseName);
1893
+ const relatedPath = sourceRef;
1894
+ const titleSegment = parsed.title ? parsed.title : baseName.replace(/\.md$/i, '');
1895
+ const statusSegment = parsed.status ? ` [${parsed.status}]` : '';
1896
+ const decisionSegment = parsed.decision
1897
+ ? ` — ${parsed.decision}`
1898
+ : '';
1899
+ const fullText = `ADR ${titleSegment}${statusSegment}${decisionSegment}`.trim();
1900
+ const text = fullText.length > ADR_DECISION_EXCERPT_CAP
1901
+ ? `${fullText.slice(0, ADR_DECISION_EXCERPT_CAP - 1).trimEnd()}…`
1902
+ : fullText;
1903
+ return createSeed({
1904
+ text,
1905
+ seedKind: 'decision',
1906
+ sourceKind: 'adr',
1907
+ sourceRef,
1908
+ confidence: 'high',
1909
+ tags: ['bootstrap', 'adr', 'architecture'],
1910
+ relatedPaths: target ? [relatedPath, target] : [relatedPath],
1911
+ promotionHint: 'decision',
1912
+ });
1913
+ }
1914
+ function parseAdrMarkdown(content) {
1915
+ const lines = content.replace(/\r\n/g, '\n').split('\n');
1916
+ const titleLine = lines.find((line) => /^#\s+/.test(line));
1917
+ const title = titleLine ? titleLine.replace(/^#\s+/, '').trim() : undefined;
1918
+ const sections = splitAdrSections(lines);
1919
+ const statusSection = sections.find((s) => /^(status|statut)$/i.test(s.heading));
1920
+ const status = statusSection ? firstNonEmptyLine(statusSection.body) : undefined;
1921
+ const decisionSection = sections.find((s) => /^(decision|décision)$/i.test(s.heading));
1922
+ let decision;
1923
+ if (decisionSection) {
1924
+ decision = firstParagraph(decisionSection.body);
1925
+ }
1926
+ if (!decision) {
1927
+ const fallbackSection = sections.find((s) => !/^(status|statut|title|titre)$/i.test(s.heading));
1928
+ if (fallbackSection) {
1929
+ decision = firstParagraph(fallbackSection.body);
1930
+ }
1931
+ }
1932
+ if (!decision) {
1933
+ decision = firstNonTitleParagraph(lines);
1934
+ }
1935
+ if (decision && decision.length > ADR_DECISION_EXCERPT_CAP) {
1936
+ decision = `${decision.slice(0, ADR_DECISION_EXCERPT_CAP - 1).trimEnd()}…`;
1937
+ }
1938
+ return { title, status, decision };
1939
+ }
1940
+ function splitAdrSections(lines) {
1941
+ const sections = [];
1942
+ let current;
1943
+ for (const line of lines) {
1944
+ const match = line.match(/^#{2,6}\s+(.+?)\s*$/);
1945
+ if (match) {
1946
+ if (current)
1947
+ sections.push(current);
1948
+ current = { heading: match[1].trim(), body: [] };
1949
+ }
1950
+ else if (current) {
1951
+ current.body.push(line);
1952
+ }
1953
+ }
1954
+ if (current)
1955
+ sections.push(current);
1956
+ return sections;
1957
+ }
1958
+ function firstNonEmptyLine(body) {
1959
+ for (const line of body) {
1960
+ const trimmed = line.trim();
1961
+ if (trimmed.length > 0)
1962
+ return trimmed;
1963
+ }
1964
+ return undefined;
1965
+ }
1966
+ function firstParagraph(body) {
1967
+ const paragraph = [];
1968
+ for (const line of body) {
1969
+ if (line.trim().length === 0) {
1970
+ if (paragraph.length > 0)
1971
+ break;
1972
+ continue;
1973
+ }
1974
+ paragraph.push(line.trim());
1975
+ }
1976
+ return paragraph.length > 0 ? paragraph.join(' ') : undefined;
1977
+ }
1978
+ function firstNonTitleParagraph(lines) {
1979
+ const paragraph = [];
1980
+ for (const line of lines) {
1981
+ if (/^#{1,6}\s+/.test(line))
1982
+ continue;
1983
+ if (line.trim().length === 0) {
1984
+ if (paragraph.length > 0)
1985
+ break;
1986
+ continue;
1987
+ }
1988
+ paragraph.push(line.trim());
1989
+ }
1990
+ return paragraph.length > 0 ? paragraph.join(' ') : undefined;
1991
+ }
1826
1992
  //# sourceMappingURL=bootstrap.js.map
@@ -221,6 +221,8 @@ export function publishLocalBrainclawRelease(cwd, options = {}) {
221
221
  const manifestPath = path.resolve(cwd, options.manifestPath ?? DEFAULT_LOCAL_RELEASE_MANIFEST_PATH);
222
222
  fs.mkdirSync(outputDir, { recursive: true });
223
223
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
224
+ runNpmScript(cwd, 'build:release');
225
+ runNpmScript(cwd, 'pack:check');
224
226
  const packResult = spawnSync(resolveNpmCommand(), resolveNpmPackArgs(outputDir), {
225
227
  cwd,
226
228
  encoding: 'utf-8',
@@ -488,6 +490,26 @@ function resolveNpmPackArgs(outputDir) {
488
490
  }
489
491
  return ['pack', '--json', '--pack-destination', outputDir];
490
492
  }
493
+ function resolveNpmRunArgs(scriptName) {
494
+ if (process.platform === 'win32') {
495
+ return ['/d', '/s', '/c', 'npm', 'run', scriptName];
496
+ }
497
+ return ['run', scriptName];
498
+ }
499
+ function runNpmScript(cwd, scriptName) {
500
+ const result = spawnSync(resolveNpmCommand(), resolveNpmRunArgs(scriptName), {
501
+ cwd,
502
+ encoding: 'utf-8',
503
+ timeout: 300000,
504
+ });
505
+ if (result.error) {
506
+ throw new Error(`Failed to run npm run ${scriptName}: ${result.error.message}`);
507
+ }
508
+ if (result.status !== 0) {
509
+ const message = firstNonEmptyLine(result.stderr) ?? firstNonEmptyLine(result.stdout) ?? `npm run ${scriptName} failed`;
510
+ throw new Error(message);
511
+ }
512
+ }
491
513
  function resolveNpmViewArgs(packageName) {
492
514
  if (process.platform === 'win32') {
493
515
  return ['/d', '/s', '/c', 'npm', 'view', packageName, 'dist-tags', '--json'];
@@ -7,6 +7,7 @@ import { mutate } from './mutation-pipeline.js';
7
7
  import { nowISO, getNextShortLabel } from './ids.js';
8
8
  import { JsonStore } from './json-store.js';
9
9
  import { refreshLiveCompanions } from '../commands/export.js';
10
+ import { emitRegistryPostImage, emitRegistryTombstone, registryFaultPoint } from './events/registry-post-image.js';
10
11
  /**
11
12
  * Return the effective source for a candidate.
12
13
  *
@@ -71,7 +72,13 @@ function candidateStore(dest = 'pending', cwd) {
71
72
  export function saveCandidate(candidate, cwd) {
72
73
  mutate({ cwd }, () => {
73
74
  ensureInboxDirs(cwd);
74
- candidateStore('pending', cwd).save(CandidateSchema.parse(candidate));
75
+ const store = candidateStore('pending', cwd);
76
+ const parsed = CandidateSchema.parse(candidate);
77
+ // pln#568 (I2): journal the pending post-image BEFORE the projection write.
78
+ const created = !store.exists(parsed.id);
79
+ emitRegistryPostImage('candidate', parsed, { created, agent: parsed.author, agent_id: parsed.author_id, session_id: parsed.session_id, cwd });
80
+ registryFaultPoint('after_registry_journal');
81
+ store.save(parsed);
75
82
  // Auto-refresh live companions after candidate changes (non-fatal)
76
83
  try {
77
84
  refreshLiveCompanions(cwd);
@@ -108,6 +115,12 @@ function applySourceFilter(candidates, filter) {
108
115
  export function archiveCandidate(candidate, dest, cwd) {
109
116
  mutate({ cwd }, () => {
110
117
  ensureInboxDirs(cwd);
118
+ // pln#568 (I2): the candidate leaves the pending inbox — tombstone it in the
119
+ // journal BEFORE the projection delete, so the journal-materialized live set
120
+ // (and the observer's pending view) drops it. The accepted/rejected archive
121
+ // dirs are not journaled (the observer only tracks pending candidates).
122
+ emitRegistryTombstone('candidate', candidate.id, { agent: candidate.author, agent_id: candidate.author_id, session_id: candidate.session_id, cwd });
123
+ registryFaultPoint('after_registry_journal');
111
124
  candidateStore(dest, cwd).save(CandidateSchema.parse(candidate));
112
125
  candidateStore('pending', cwd).delete(candidate.id);
113
126
  // Auto-refresh live companions after candidate archive (non-fatal)
@@ -148,6 +161,13 @@ export function cleanupStaleCandidates(options = {}) {
148
161
  mutate({ cwd: options.cwd }, () => {
149
162
  const store = candidateStore('pending', options.cwd);
150
163
  for (const candidate of candidates) {
164
+ emitRegistryTombstone('candidate', candidate.id, {
165
+ agent: candidate.author,
166
+ agent_id: candidate.author_id,
167
+ session_id: candidate.session_id,
168
+ cwd: options.cwd,
169
+ });
170
+ registryFaultPoint('after_registry_journal');
151
171
  store.delete(candidate.id);
152
172
  }
153
173
  try {