brainclaw 1.7.5 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -11
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +139 -13
- package/dist/commands/add-step.js +1 -1
- package/dist/commands/bootstrap.js +2 -26
- package/dist/commands/check-security-mcp.js +50 -33
- package/dist/commands/check-security.js +86 -43
- package/dist/commands/claim.js +22 -21
- package/dist/commands/confirm.js +26 -0
- package/dist/commands/context-diff.js +1 -1
- package/dist/commands/dispatch-watch.js +142 -0
- package/dist/commands/doctor.js +113 -2
- package/dist/commands/estimation-report.js +115 -16
- package/dist/commands/harvest.js +502 -16
- package/dist/commands/init.js +123 -21
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +198 -29
- package/dist/commands/mcp.js +615 -92
- package/dist/commands/memory.js +21 -17
- package/dist/commands/migrate.js +81 -17
- package/dist/commands/prune.js +78 -4
- package/dist/commands/reflect.js +26 -20
- package/dist/commands/register-agent.js +57 -1
- package/dist/commands/repair.js +20 -0
- package/dist/commands/session-end.js +15 -6
- package/dist/commands/session-start.js +18 -1
- package/dist/commands/setup-security.js +39 -18
- package/dist/commands/setup.js +26 -27
- package/dist/commands/stale.js +16 -2
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +11 -13
- package/dist/core/agent-files.js +844 -547
- package/dist/core/agent-integrations.js +0 -3
- package/dist/core/agent-inventory.js +67 -0
- package/dist/core/agent-registry.js +163 -29
- package/dist/core/agentrun-reconciler.js +33 -2
- package/dist/core/agentruns.js +7 -1
- package/dist/core/ai-agent-detection.js +31 -44
- package/dist/core/archival.js +15 -9
- package/dist/core/assignment-reconciler.js +56 -0
- package/dist/core/assignment-sweeper.js +127 -4
- package/dist/core/assignments.js +69 -11
- package/dist/core/bootstrap.js +233 -67
- package/dist/core/brainclaw-version.js +22 -0
- package/dist/core/candidates.js +21 -1
- package/dist/core/claims.js +313 -150
- package/dist/core/config.js +6 -1
- package/dist/core/context-diff.js +148 -20
- package/dist/core/context.js +129 -8
- package/dist/core/coordination.js +22 -3
- package/dist/core/dispatch-status.js +109 -5
- package/dist/core/dispatcher.js +65 -11
- package/dist/core/entity-operations.js +45 -24
- package/dist/core/entity-registry.js +31 -5
- package/dist/core/event-log.js +138 -21
- package/dist/core/events/checkpoint.js +258 -0
- package/dist/core/events/genesis.js +220 -0
- package/dist/core/events/journal.js +507 -0
- package/dist/core/events/materialize.js +126 -0
- package/dist/core/events/registry-post-image.js +110 -0
- package/dist/core/events/verify.js +109 -0
- package/dist/core/execution-adapters.js +23 -0
- package/dist/core/execution.js +25 -0
- package/dist/core/facade-schema.js +48 -0
- package/dist/core/gc-semantic.js +130 -5
- package/dist/core/handoff-snapshot.js +68 -0
- package/dist/core/ids.js +19 -8
- package/dist/core/instruction-templates.js +34 -115
- package/dist/core/io.js +39 -3
- package/dist/core/json-store.js +10 -1
- package/dist/core/lock.js +153 -28
- package/dist/core/loops/bootstrap-acquire.js +25 -1
- package/dist/core/loops/facade-schema.js +2 -0
- package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/presets/bootstrap.js +7 -0
- package/dist/core/loops/store.js +17 -0
- package/dist/core/loops/verbs.js +24 -1
- package/dist/core/markdown.js +8 -76
- package/dist/core/mcp-command-resolution.js +245 -0
- package/dist/core/memory-compactor.js +5 -3
- package/dist/core/memory-lifecycle.js +282 -0
- package/dist/core/merge-risk.js +150 -0
- package/dist/core/messaging.js +8 -1
- package/dist/core/migration.js +11 -1
- package/dist/core/observer-mode.js +26 -0
- package/dist/core/operations/memory-mutation.js +90 -65
- package/dist/core/operations/plan.js +27 -1
- package/dist/core/protocol-skills.js +210 -0
- package/dist/core/reflection-safety.js +6 -7
- package/dist/core/reputation.js +84 -2
- package/dist/core/runtime-signals.js +71 -9
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +125 -0
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +107 -29
- package/dist/core/security-packages.js +121 -0
- package/dist/core/security-scoring.js +76 -9
- package/dist/core/security.js +34 -2
- package/dist/core/sequence.js +11 -2
- package/dist/core/setup-flow.js +141 -13
- package/dist/core/spawn-check.js +110 -4
- package/dist/core/staleness.js +109 -1
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +19 -5
- package/dist/core/worktree.js +169 -7
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/cli.md +11 -10
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +17 -0
- package/docs/concepts/event-log-store-critique-A.md +333 -0
- package/docs/concepts/event-log-store-critique-B.md +353 -0
- package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
- package/docs/concepts/event-log-store-proposal-A.md +365 -0
- package/docs/concepts/event-log-store-proposal-B.md +404 -0
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/identity-model-proposal.md +371 -0
- package/docs/concepts/memory.md +5 -4
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +43 -0
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/workspace-bootstrapping.md +61 -0
- package/docs/integrations/agents.md +4 -4
- package/docs/integrations/cline.md +10 -11
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +5 -5
- package/docs/integrations/copilot.md +14 -12
- package/docs/integrations/openclaw.md +7 -6
- package/docs/integrations/overview.md +7 -7
- package/docs/integrations/roo.md +3 -3
- package/docs/integrations/windsurf.md +6 -6
- package/docs/mcp-schema-changelog.md +51 -20
- package/docs/quickstart.md +48 -47
- package/docs/security.md +174 -15
- package/docs/storage.md +4 -2
- package/package.json +8 -6
package/dist/core/bootstrap.js
CHANGED
|
@@ -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
|
|
57
|
-
if (!options.refresh && existing && existingPlan && isProfileReusable(existing, target,
|
|
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,
|
|
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),
|
|
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
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
//
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
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)
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
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'];
|
package/dist/core/candidates.js
CHANGED
|
@@ -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)
|
|
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 {
|