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.
- package/README.md +592 -505
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +138 -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 +286 -23
- package/dist/commands/hooks.js +73 -73
- package/dist/commands/init.js +124 -22
- package/dist/commands/install-hooks.js +78 -78
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +253 -41
- package/dist/commands/mcp.js +664 -102
- 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/switch.js +26 -5
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/version.js +1 -1
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +30 -17
- package/dist/core/agent-files.js +963 -666
- 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/codev-prompts.js +38 -38
- 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/default-profiles/doctor.yaml +11 -11
- package/dist/core/default-profiles/janitor.yaml +11 -11
- package/dist/core/default-profiles/onboarder.yaml +11 -11
- package/dist/core/default-profiles/reviewer.yaml +13 -13
- package/dist/core/dispatch-status.js +79 -5
- package/dist/core/dispatcher.js +65 -12
- package/dist/core/entity-operations.js +74 -27
- 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 +1 -1
- package/dist/core/facade-schema.js +38 -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 -2
- 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 +10 -3
- 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 +72 -10
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +114 -0
- package/dist/core/search.js +19 -2
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +217 -139
- 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 +16 -2
- package/dist/core/staleness.js +73 -2
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +45 -12
- package/dist/core/worktree.js +90 -26
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/adapters/openclaw.md +43 -43
- package/docs/architecture/project-refs.md +328 -328
- package/docs/cli.md +2097 -2096
- package/docs/concepts/coordination.md +52 -52
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +245 -245
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/ideation-loop.md +317 -317
- package/docs/concepts/loop-engine.md +520 -511
- package/docs/concepts/mcp-governance.md +268 -268
- package/docs/concepts/memory.md +89 -88
- package/docs/concepts/multi-agent-workflows.md +167 -167
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +217 -174
- package/docs/concepts/project-md-convention.md +35 -35
- package/docs/concepts/runtime-notes.md +38 -38
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/troubleshooting.md +254 -254
- package/docs/concepts/workspace-bootstrapping.md +142 -81
- package/docs/context-format-changelog.md +35 -35
- package/docs/context-format.md +48 -48
- package/docs/index.md +65 -65
- package/docs/integrations/agents.md +162 -162
- package/docs/integrations/claude-code.md +23 -23
- package/docs/integrations/cline.md +87 -88
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +60 -60
- package/docs/integrations/copilot.md +82 -80
- package/docs/integrations/cursor.md +23 -23
- package/docs/integrations/kilocode.md +72 -72
- package/docs/integrations/mcp.md +377 -377
- package/docs/integrations/mistral-vibe.md +122 -122
- package/docs/integrations/openclaw.md +99 -98
- package/docs/integrations/opencode.md +84 -84
- package/docs/integrations/overview.md +122 -122
- package/docs/integrations/roo.md +74 -74
- package/docs/integrations/windsurf.md +83 -83
- package/docs/mcp-schema-changelog.md +360 -329
- package/docs/playbooks/integration/index.md +121 -121
- package/docs/playbooks/orchestration.md +37 -0
- package/docs/playbooks/productivity/index.md +99 -99
- package/docs/playbooks/team/index.md +117 -117
- package/docs/product/agent-first-model.md +184 -184
- package/docs/product/entity-model-audit.md +462 -462
- package/docs/product/positioning.md +86 -86
- package/docs/quickstart-existing-project.md +107 -107
- package/docs/quickstart.md +148 -147
- package/docs/release-maintenance.md +79 -79
- package/docs/reputation.md +52 -52
- package/docs/review.md +45 -45
- package/docs/security.md +212 -53
- package/docs/server-operations.md +118 -118
- package/docs/storage.md +110 -108
- package/package.json +86 -69
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
import { matchesAnyEntry, parseListEntry } from './security-packages.js';
|
|
2
|
+
/**
|
|
3
|
+
* Map an intrinsic verdict (pass/warn/block) to the effective decision under
|
|
4
|
+
* a given mode. In advisory mode, a block is downgraded to warn so the
|
|
5
|
+
* operator sees the issue but the wrapper does not abort the install.
|
|
6
|
+
*/
|
|
7
|
+
export function applyMode(decision, mode) {
|
|
8
|
+
if (mode === 'enforced')
|
|
9
|
+
return decision;
|
|
10
|
+
return decision === 'block' ? 'warn' : decision;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Map an effective decision to a CLI exit code.
|
|
14
|
+
* pass -> 0
|
|
15
|
+
* warn -> 1 (wrapper continues, but surfaces the warning)
|
|
16
|
+
* block -> 2 (wrapper aborts the install)
|
|
17
|
+
*/
|
|
18
|
+
export function decisionExitCode(decision) {
|
|
19
|
+
if (decision === 'block')
|
|
20
|
+
return 2;
|
|
21
|
+
if (decision === 'warn')
|
|
22
|
+
return 1;
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
1
25
|
const DEFAULT_WEIGHTS = {
|
|
2
26
|
supply_chain: 0.35,
|
|
3
27
|
vulnerability: 0.30,
|
|
@@ -11,8 +35,47 @@ const DEFAULT_THRESHOLDS = {
|
|
|
11
35
|
supply_chain_block: 30,
|
|
12
36
|
vulnerability_block: 20,
|
|
13
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Normalize weights so they sum to 1.0. Without this, custom configs like
|
|
40
|
+
* `{ supply_chain: 1, vulnerability: 1 }` produce a composite that can
|
|
41
|
+
* exceed 100, making thresholds meaningless. If all weights are zero the
|
|
42
|
+
* defaults are used (degenerate config — fail open to the project default).
|
|
43
|
+
*/
|
|
44
|
+
export function normalizeWeights(weights) {
|
|
45
|
+
const merged = { ...DEFAULT_WEIGHTS, ...weights };
|
|
46
|
+
const sum = merged.supply_chain + merged.vulnerability + merged.quality + merged.maintenance + merged.license;
|
|
47
|
+
if (sum <= 0)
|
|
48
|
+
return { ...DEFAULT_WEIGHTS };
|
|
49
|
+
if (Math.abs(sum - 1) < 1e-9)
|
|
50
|
+
return merged;
|
|
51
|
+
return {
|
|
52
|
+
supply_chain: merged.supply_chain / sum,
|
|
53
|
+
vulnerability: merged.vulnerability / sum,
|
|
54
|
+
quality: merged.quality / sum,
|
|
55
|
+
maintenance: merged.maintenance / sum,
|
|
56
|
+
license: merged.license / sum,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Clamp thresholds to the [0,100] band and enforce the invariant
|
|
61
|
+
* `composite_warn <= composite_pass`. The CLI loader can call this so a
|
|
62
|
+
* mis-configured YAML never produces a "composite=60 → block when
|
|
63
|
+
* pass=50, warn=80" non-monotonic verdict.
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeThresholds(thresholds) {
|
|
66
|
+
const t = { ...DEFAULT_THRESHOLDS, ...thresholds };
|
|
67
|
+
const clamp = (n) => Math.max(0, Math.min(100, n));
|
|
68
|
+
t.composite_pass = clamp(t.composite_pass);
|
|
69
|
+
t.composite_warn = clamp(t.composite_warn);
|
|
70
|
+
t.supply_chain_block = clamp(t.supply_chain_block);
|
|
71
|
+
t.vulnerability_block = clamp(t.vulnerability_block);
|
|
72
|
+
if (t.composite_warn > t.composite_pass) {
|
|
73
|
+
t.composite_warn = t.composite_pass;
|
|
74
|
+
}
|
|
75
|
+
return t;
|
|
76
|
+
}
|
|
14
77
|
export function computeComposite(scores, weights) {
|
|
15
|
-
const w =
|
|
78
|
+
const w = normalizeWeights(weights);
|
|
16
79
|
return Math.round((scores.supplyChain * w.supply_chain +
|
|
17
80
|
scores.vulnerability * w.vulnerability +
|
|
18
81
|
scores.quality * w.quality +
|
|
@@ -20,15 +83,18 @@ export function computeComposite(scores, weights) {
|
|
|
20
83
|
scores.license * w.license) * 10) / 10;
|
|
21
84
|
}
|
|
22
85
|
export function evaluatePackage(scores, config) {
|
|
23
|
-
const thresholds =
|
|
24
|
-
const weights =
|
|
86
|
+
const thresholds = normalizeThresholds(config?.thresholds);
|
|
87
|
+
const weights = normalizeWeights(config?.weights);
|
|
25
88
|
const allowlist = config?.allowlist ?? [];
|
|
26
89
|
const denylist = config?.denylist ?? [];
|
|
27
90
|
const pkgName = scores.purl.replace(/^pkg:\w+\//, '');
|
|
28
91
|
const ecosystem = scores.purl.startsWith('pkg:pypi') ? 'pypi' : 'npm';
|
|
29
92
|
const reasons = [];
|
|
30
|
-
|
|
31
|
-
|
|
93
|
+
const parsedDeny = denylist.map(parseListEntry);
|
|
94
|
+
const parsedAllow = allowlist.map(parseListEntry);
|
|
95
|
+
// Denylist check — canonical (ecosystem, name, optional version) match.
|
|
96
|
+
const denyHit = matchesAnyEntry(parsedDeny, ecosystem, pkgName, scores.version);
|
|
97
|
+
if (denyHit) {
|
|
32
98
|
return {
|
|
33
99
|
package: pkgName,
|
|
34
100
|
ecosystem,
|
|
@@ -36,11 +102,12 @@ export function evaluatePackage(scores, config) {
|
|
|
36
102
|
scores,
|
|
37
103
|
composite: 0,
|
|
38
104
|
decision: 'block',
|
|
39
|
-
reasons: [`Package "${pkgName}"
|
|
105
|
+
reasons: [`Package "${pkgName}@${scores.version}" matches denylist entry "${denyHit.raw.trim()}"`],
|
|
40
106
|
};
|
|
41
107
|
}
|
|
42
|
-
// Allowlist check
|
|
43
|
-
|
|
108
|
+
// Allowlist check — canonical match; skips scoring.
|
|
109
|
+
const allowHit = matchesAnyEntry(parsedAllow, ecosystem, pkgName, scores.version);
|
|
110
|
+
if (allowHit) {
|
|
44
111
|
return {
|
|
45
112
|
package: pkgName,
|
|
46
113
|
ecosystem,
|
|
@@ -48,7 +115,7 @@ export function evaluatePackage(scores, config) {
|
|
|
48
115
|
scores,
|
|
49
116
|
composite: 100,
|
|
50
117
|
decision: 'pass',
|
|
51
|
-
reasons: [`Package "${pkgName}"
|
|
118
|
+
reasons: [`Package "${pkgName}@${scores.version}" matches allowlist entry "${allowHit.raw.trim()}"`],
|
|
52
119
|
};
|
|
53
120
|
}
|
|
54
121
|
const composite = computeComposite(scores, weights);
|
package/dist/core/security.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { runEntropyDetector, runStructuralDetectors } from './security-detectors.js';
|
|
1
2
|
/**
|
|
2
|
-
* Scan a text string for sensitive
|
|
3
|
-
*
|
|
3
|
+
* Scan a text string for sensitive content. Three signal layers run:
|
|
4
|
+
* 1. User-configured regex patterns from `config.redaction.patterns`
|
|
5
|
+
* (the legacy MVP behavior).
|
|
6
|
+
* 2. Structural detectors — exact token shapes for GitHub PATs, AWS
|
|
7
|
+
* access keys, JWTs, etc. High precision; on by default.
|
|
8
|
+
* 3. Entropy detector — flags high-entropy token-like substrings near
|
|
9
|
+
* a sensitive keyword. Tunable, on by default.
|
|
10
|
+
*
|
|
11
|
+
* In strict mode all signals escalate to `block`; otherwise `warn`.
|
|
4
12
|
*/
|
|
5
13
|
export function scanText(text, config) {
|
|
6
14
|
const warnings = [];
|
|
@@ -24,6 +32,30 @@ export function scanText(text, config) {
|
|
|
24
32
|
// skip invalid regex patterns
|
|
25
33
|
}
|
|
26
34
|
}
|
|
35
|
+
// Structural detectors — only run when token_detection is enabled.
|
|
36
|
+
const td = config.security?.token_detection;
|
|
37
|
+
if (td?.enabled !== false) {
|
|
38
|
+
const disabled = td?.detectors;
|
|
39
|
+
for (const m of runStructuralDetectors(text, disabled)) {
|
|
40
|
+
warnings.push({
|
|
41
|
+
level,
|
|
42
|
+
message: `${m.label} (id=${m.detectorId}) detected: ${m.excerpt}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (td?.entropy?.enabled !== false) {
|
|
46
|
+
const entropyMatches = runEntropyDetector(text, {
|
|
47
|
+
enabled: td?.entropy?.enabled ?? true,
|
|
48
|
+
minLength: td?.entropy?.min_length,
|
|
49
|
+
minEntropy: td?.entropy?.min_entropy,
|
|
50
|
+
});
|
|
51
|
+
for (const m of entropyMatches) {
|
|
52
|
+
warnings.push({
|
|
53
|
+
level,
|
|
54
|
+
message: `High-entropy token-shaped substring near a secret keyword (entropy=${m.entropy}): ${m.excerpt}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
27
59
|
const blockPaths = config.security?.block_sensitive_paths ?? true;
|
|
28
60
|
if (blockPaths) {
|
|
29
61
|
for (const sp of config.sensitive_paths) {
|
package/dist/core/sequence.js
CHANGED
|
@@ -5,6 +5,7 @@ import { generateIdWithLabel, nowISO } from './ids.js';
|
|
|
5
5
|
import { resolveEntityDir } from './io.js';
|
|
6
6
|
import { SequenceItemSchema, SequenceSchema } from './schema.js';
|
|
7
7
|
import { refreshLiveCompanions } from '../commands/export.js';
|
|
8
|
+
import { emitRegistryPostImage, emitRegistryTombstone, registryFaultPoint } from './events/registry-post-image.js';
|
|
8
9
|
function sequencesDir(cwd, mode = 'read') {
|
|
9
10
|
return resolveEntityDir('sequences', cwd ?? process.cwd(), mode);
|
|
10
11
|
}
|
|
@@ -39,7 +40,13 @@ function validateRanks(items) {
|
|
|
39
40
|
export function saveSequence(sequence, cwd) {
|
|
40
41
|
mutate({ cwd }, () => {
|
|
41
42
|
ensureSequencesDir(cwd);
|
|
42
|
-
sequenceStore(cwd, 'write')
|
|
43
|
+
const store = sequenceStore(cwd, 'write');
|
|
44
|
+
const parsed = SequenceSchema.parse(sequence);
|
|
45
|
+
// pln#568 (I2): journal the post-image BEFORE the projection write.
|
|
46
|
+
const created = !store.exists(parsed.id);
|
|
47
|
+
emitRegistryPostImage('sequence', parsed, { created, agent: parsed.author, agent_id: parsed.author_id, session_id: parsed.session_id, cwd });
|
|
48
|
+
registryFaultPoint('after_registry_journal');
|
|
49
|
+
store.save(parsed);
|
|
43
50
|
// Auto-refresh live companions after sequence changes (non-fatal)
|
|
44
51
|
try {
|
|
45
52
|
refreshLiveCompanions(cwd);
|
|
@@ -99,6 +106,8 @@ export function deleteSequence(id, cwd) {
|
|
|
99
106
|
if (!current) {
|
|
100
107
|
throw new Error(`Sequence not found: ${id}`);
|
|
101
108
|
}
|
|
109
|
+
emitRegistryTombstone('sequence', current.id, { agent: current.author, agent_id: current.author_id, session_id: current.session_id, cwd });
|
|
110
|
+
registryFaultPoint('after_registry_journal');
|
|
102
111
|
store.delete(current.id);
|
|
103
112
|
return { id: current.id, name: current.name };
|
|
104
113
|
});
|
|
@@ -123,7 +132,7 @@ export function updateSequence(input, cwd) {
|
|
|
123
132
|
tags: input.tags ?? current.tags,
|
|
124
133
|
updated_at: nowISO(),
|
|
125
134
|
};
|
|
126
|
-
|
|
135
|
+
saveSequence(SequenceSchema.parse(next), cwd);
|
|
127
136
|
return next;
|
|
128
137
|
});
|
|
129
138
|
}
|
package/dist/core/setup-flow.js
CHANGED
|
@@ -11,13 +11,150 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { memoryDir, memoryExists } from './io.js';
|
|
15
16
|
import { detectAiAgent } from './ai-agent-detection.js';
|
|
16
17
|
import { resolveStoreChain } from './store-resolution.js';
|
|
17
18
|
import { analyzeRepository } from './repo-analysis.js';
|
|
18
19
|
import { getAgentCapabilityProfile, getAllAgentCapabilityProfiles } from './agent-capability.js';
|
|
19
20
|
import { describeAgentSurfaces } from './agent-capability.js';
|
|
20
21
|
import { loadState } from './state.js';
|
|
22
|
+
/** Entries that don't count as "repo content" when deciding the bootstrap route. */
|
|
23
|
+
const CONTENT_IGNORED = new Set(['.git', '.brainclaw', '.gitignore', '.gitattributes', '.DS_Store', 'Thumbs.db']);
|
|
24
|
+
/** True when cwd contains anything beyond git/brainclaw plumbing. */
|
|
25
|
+
export function repoHasContent(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readdirSync(cwd).some((e) => !CONTENT_IGNORED.has(e));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* THE decision rule for "the memory store is empty — what now?", emitted
|
|
35
|
+
* identically by the three onboarding surfaces (bclaw_work hint, quick_init
|
|
36
|
+
* preview, init preflight):
|
|
37
|
+
*
|
|
38
|
+
* - repo with existing content → bclaw_bootstrap (extract context from docs/manifests/history)
|
|
39
|
+
* - greenfield (nothing to extract) → bootstrap loop (ideate the vision first)
|
|
40
|
+
*
|
|
41
|
+
* The two routes are chainable: extraction first, then a bootstrap loop for
|
|
42
|
+
* whatever vision the docs could not provide — or ideation first, then
|
|
43
|
+
* extraction once content exists.
|
|
44
|
+
* Documented in docs/concepts/workspace-bootstrapping.md ("Empty memory: one rule").
|
|
45
|
+
*/
|
|
46
|
+
export function resolveEmptyMemoryRecommendation(cwd = process.cwd()) {
|
|
47
|
+
if (repoHasContent(cwd)) {
|
|
48
|
+
return {
|
|
49
|
+
route: 'extract',
|
|
50
|
+
reason: 'repo has existing content to extract from',
|
|
51
|
+
mcp_next_action: 'bclaw_bootstrap()',
|
|
52
|
+
cli_next_action: 'brainclaw bootstrap',
|
|
53
|
+
chained_mcp_action: "bclaw_coordinate(intent='ideate', preset='bootstrap')",
|
|
54
|
+
text: "Memory is empty and the repo has existing content → run bclaw_bootstrap (CLI: brainclaw bootstrap) to extract initial context. If the project vision is still missing afterwards, chain a bootstrap loop: bclaw_coordinate(intent='ideate', preset='bootstrap').",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
route: 'ideate',
|
|
59
|
+
reason: 'greenfield repo — nothing to extract yet',
|
|
60
|
+
mcp_next_action: "bclaw_coordinate(intent='ideate', preset='bootstrap')",
|
|
61
|
+
cli_next_action: 'brainclaw bootstrap-loop',
|
|
62
|
+
chained_mcp_action: 'bclaw_bootstrap()',
|
|
63
|
+
text: "Memory is empty and the repo is greenfield → open a bootstrap loop to ideate the project vision: bclaw_coordinate(intent='ideate', preset='bootstrap') (CLI: brainclaw bootstrap-loop). Once content exists, chain bclaw_bootstrap to extract it.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Event log this large reads as a mature store, not a from-scratch case. */
|
|
67
|
+
const RICH_EVENT_LOG_BYTES = 64 * 1024;
|
|
68
|
+
/** This many memory items reads as a mature store. */
|
|
69
|
+
const RICH_STATE_ITEMS = 25;
|
|
70
|
+
/** PROJECT.md this much older than the latest repo/store activity is fossil. */
|
|
71
|
+
const FOSSIL_GAP_MS = 30 * 86_400_000;
|
|
72
|
+
function lastCommitEpochMs(cwd) {
|
|
73
|
+
try {
|
|
74
|
+
const probe = spawnSync('git', ['log', '-1', '--format=%ct'], {
|
|
75
|
+
cwd,
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
timeout: 2000,
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
});
|
|
80
|
+
if (probe.status !== 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
const epoch = Number.parseInt(probe.stdout.trim(), 10);
|
|
83
|
+
return Number.isFinite(epoch) ? epoch * 1000 : undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Composite replacement for the one-bit PROJECT.md stat() that backed
|
|
91
|
+
* `bootstrap_recommended` (pln#513). The single bit had two failure modes on
|
|
92
|
+
* a mature ("amorcé") project:
|
|
93
|
+
* - false positive: rich store, missing PROJECT.md → recommended a
|
|
94
|
+
* from-scratch bootstrap over 17k events of accumulated memory;
|
|
95
|
+
* - eternal false negative: PROJECT.md exists but fossilized — never
|
|
96
|
+
* flagged again no matter how far the repo drifted.
|
|
97
|
+
*
|
|
98
|
+
* Signals combined: PROJECT.md presence × its mtime vs recent activity
|
|
99
|
+
* (last commit, last memory write) × store density (event-log size,
|
|
100
|
+
* memory item count). 'refresh' maps to `bclaw_bootstrap(refresh: true)` —
|
|
101
|
+
* coordinate with the pln#514 step 1 force-flag on the bootstrap-loop side.
|
|
102
|
+
*/
|
|
103
|
+
export function assessBootstrapNeed(cwd = process.cwd()) {
|
|
104
|
+
const reasons = [];
|
|
105
|
+
// Signal 1 — PROJECT.md presence.
|
|
106
|
+
const projectMdPath = path.join(cwd, 'PROJECT.md');
|
|
107
|
+
let projectMdPresent = false;
|
|
108
|
+
let projectMdMtimeMs;
|
|
109
|
+
try {
|
|
110
|
+
const stat = fs.statSync(projectMdPath);
|
|
111
|
+
if (stat.isFile() && stat.size > 0) {
|
|
112
|
+
projectMdPresent = true;
|
|
113
|
+
projectMdMtimeMs = stat.mtimeMs;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch { /* absent */ }
|
|
117
|
+
// Signal 2 — store density.
|
|
118
|
+
let eventLogBytes = 0;
|
|
119
|
+
let eventLogMtimeMs;
|
|
120
|
+
try {
|
|
121
|
+
const stat = fs.statSync(path.join(memoryDir(cwd), 'events.jsonl'));
|
|
122
|
+
eventLogBytes = stat.size;
|
|
123
|
+
eventLogMtimeMs = stat.mtimeMs;
|
|
124
|
+
}
|
|
125
|
+
catch { /* no event log */ }
|
|
126
|
+
let stateItems = 0;
|
|
127
|
+
try {
|
|
128
|
+
const state = loadState(cwd);
|
|
129
|
+
stateItems = state.active_constraints.length + state.recent_decisions.length
|
|
130
|
+
+ state.known_traps.length + state.open_handoffs.length + state.plan_items.length;
|
|
131
|
+
}
|
|
132
|
+
catch { /* unreadable state → 0 */ }
|
|
133
|
+
const storeDensity = eventLogBytes >= RICH_EVENT_LOG_BYTES || stateItems >= RICH_STATE_ITEMS
|
|
134
|
+
? 'rich'
|
|
135
|
+
: eventLogBytes === 0 && stateItems === 0
|
|
136
|
+
? 'empty'
|
|
137
|
+
: 'low';
|
|
138
|
+
if (!projectMdPresent) {
|
|
139
|
+
if (storeDensity === 'rich') {
|
|
140
|
+
// The store already knows this project — regenerate PROJECT.md from it
|
|
141
|
+
// (scanner + memory), do NOT restart discovery from scratch.
|
|
142
|
+
reasons.push(`PROJECT.md missing but the store is rich (${stateItems} items, ${Math.round(eventLogBytes / 1024)} KB events) — regenerate from existing memory instead of bootstrapping from scratch`);
|
|
143
|
+
return { verdict: 'refresh', reasons, project_md_present: false, store_density: storeDensity };
|
|
144
|
+
}
|
|
145
|
+
reasons.push('PROJECT.md missing and the store is sparse — initial bootstrap applies');
|
|
146
|
+
return { verdict: 'bootstrap', reasons, project_md_present: false, store_density: storeDensity };
|
|
147
|
+
}
|
|
148
|
+
// Signal 3 — fossil check: PROJECT.md much older than the latest activity.
|
|
149
|
+
const lastActivityMs = Math.max(lastCommitEpochMs(cwd) ?? 0, eventLogMtimeMs ?? 0);
|
|
150
|
+
if (projectMdMtimeMs !== undefined && lastActivityMs > 0 && lastActivityMs - projectMdMtimeMs > FOSSIL_GAP_MS) {
|
|
151
|
+
const gapDays = Math.round((lastActivityMs - projectMdMtimeMs) / 86_400_000);
|
|
152
|
+
reasons.push(`PROJECT.md is ${gapDays} days older than the latest repo/store activity — likely fossil, refresh it`);
|
|
153
|
+
return { verdict: 'refresh', reasons, project_md_present: true, store_density: storeDensity };
|
|
154
|
+
}
|
|
155
|
+
reasons.push('PROJECT.md present and current relative to recent activity');
|
|
156
|
+
return { verdict: 'none', reasons, project_md_present: true, store_density: storeDensity };
|
|
157
|
+
}
|
|
21
158
|
/**
|
|
22
159
|
* Probe the current working directory to understand what we're working with.
|
|
23
160
|
* This is the first step of the quick setup flow — no questions yet, just detection.
|
|
@@ -37,16 +174,7 @@ export function probeForQuickSetup(cwd = process.cwd()) {
|
|
|
37
174
|
: getAllAgentCapabilityProfiles();
|
|
38
175
|
// Scan nearby stores
|
|
39
176
|
const nearbyStores = resolveStoreChain(cwd);
|
|
40
|
-
|
|
41
|
-
const IGNORED = new Set(['.git', '.brainclaw', '.gitignore', '.gitattributes', '.DS_Store', 'Thumbs.db']);
|
|
42
|
-
let hasContent = false;
|
|
43
|
-
try {
|
|
44
|
-
const entries = fs.readdirSync(cwd);
|
|
45
|
-
hasContent = entries.some((e) => !IGNORED.has(e));
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// empty or unreadable
|
|
49
|
-
}
|
|
177
|
+
const hasContent = repoHasContent(cwd);
|
|
50
178
|
// Analyze repo for project type suggestion
|
|
51
179
|
let suggestedProjectType = 'standalone';
|
|
52
180
|
let analysisReasons = [];
|
|
@@ -164,7 +292,7 @@ export function buildOnboardingPreview(cwd) {
|
|
|
164
292
|
const traps = state.known_traps.filter((t) => t.visibility === 'shared' && (!t.status || t.status === 'active'));
|
|
165
293
|
const plans = state.plan_items.filter((p) => p.status === 'in_progress' || p.status === 'todo');
|
|
166
294
|
if (constraints.length === 0 && traps.length === 0 && plans.length === 0) {
|
|
167
|
-
return
|
|
295
|
+
return resolveEmptyMemoryRecommendation(cwd).text;
|
|
168
296
|
}
|
|
169
297
|
const lines = ['Here is what your agent will see:'];
|
|
170
298
|
if (constraints.length > 0) {
|
|
@@ -185,7 +313,7 @@ export function buildOnboardingPreview(cwd) {
|
|
|
185
313
|
return lines.join('\n');
|
|
186
314
|
}
|
|
187
315
|
catch {
|
|
188
|
-
return
|
|
316
|
+
return resolveEmptyMemoryRecommendation(cwd).text;
|
|
189
317
|
}
|
|
190
318
|
}
|
|
191
319
|
//# sourceMappingURL=setup-flow.js.map
|
package/dist/core/spawn-check.js
CHANGED
|
@@ -48,7 +48,14 @@ function initProbeGitRepo(root) {
|
|
|
48
48
|
export async function checkAgentSpawn(agent, options = {}) {
|
|
49
49
|
const start = Date.now();
|
|
50
50
|
const profile = getCapabilityProfile(agent);
|
|
51
|
-
if (!profile
|
|
51
|
+
if (!profile) {
|
|
52
|
+
// Distinct from no_template: the name didn't resolve to any profile at all
|
|
53
|
+
// (resolution is case-insensitive, so this is a genuine typo/unknown agent,
|
|
54
|
+
// not a casing slip). Reported separately so the pre-flight reason points at
|
|
55
|
+
// the spelling instead of the misleading "IDE-only?" template message.
|
|
56
|
+
return { agent, status: 'unknown_agent', delivered: false, completed: false, duration_ms: 0, detail: `unknown agent '${agent}' — not a registered brainclaw profile` };
|
|
57
|
+
}
|
|
58
|
+
if (!profile.invoke_template || !profile.invoke_binary || !profile.runtime.canBeSpawnedCli) {
|
|
52
59
|
return { agent, status: 'no_template', delivered: false, completed: false, duration_ms: 0, detail: 'no CLI invoke template' };
|
|
53
60
|
}
|
|
54
61
|
const binary = resolveBinaryOnPath(profile.invoke_binary);
|
|
@@ -120,7 +127,7 @@ export async function runSpawnCheck(options = {}) {
|
|
|
120
127
|
});
|
|
121
128
|
}
|
|
122
129
|
}
|
|
123
|
-
const installed = entries.filter((e) => e.status !== 'not_installed' && e.status !== 'no_template');
|
|
130
|
+
const installed = entries.filter((e) => e.status !== 'not_installed' && e.status !== 'no_template' && e.status !== 'unknown_agent');
|
|
124
131
|
const ok = installed.filter((e) => e.status === 'ok').length;
|
|
125
132
|
const failures = installed.filter((e) => e.status === 'failed' || e.status === 'delivered_no_completion').length;
|
|
126
133
|
const not_installed = entries.filter((e) => e.status === 'not_installed').length;
|
|
@@ -172,6 +179,13 @@ export function preflightResultFromEntry(entry) {
|
|
|
172
179
|
if (entry.status === 'ok' || entry.status === 'delivered_no_completion') {
|
|
173
180
|
return { agent, ok: true, status: entry.status, reason: entry.detail };
|
|
174
181
|
}
|
|
182
|
+
if (entry.status === 'unknown_agent') {
|
|
183
|
+
return {
|
|
184
|
+
agent, ok: false, status: entry.status,
|
|
185
|
+
reason: `unknown agent '${agent}' — not a registered brainclaw profile (check spelling/case)`,
|
|
186
|
+
recommended_next_action: `Use a registered agent name (e.g. codex, claude-code, github-copilot). Names are case-insensitive — list installed agents with \`brainclaw doctor --spawn-check\`.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
175
189
|
if (entry.status === 'not_installed') {
|
|
176
190
|
return {
|
|
177
191
|
agent, ok: false, status: entry.status,
|
package/dist/core/staleness.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Staleness is a soft signal — items are warned, not auto-archived.
|
|
5
5
|
* Users choose to dismiss, resolve, or archive via explicit commands.
|
|
6
6
|
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
7
9
|
import { resolvedSource } from './candidates.js';
|
|
8
10
|
/** Thresholds in days. Adjust via config in the future. */
|
|
9
11
|
export const STALENESS_THRESHOLDS = {
|
|
@@ -106,7 +108,7 @@ const VERIFIED_STALE_DAYS = 30;
|
|
|
106
108
|
* pln#530 — flag perishable memories (traps that opted in by carrying a
|
|
107
109
|
* `verify_cmd` and/or `verified_at`) whose last empirical verification is stale
|
|
108
110
|
* or never happened, so an agent re-probes the live system instead of trusting a
|
|
109
|
-
* value that may have drifted (the
|
|
111
|
+
* value that may have drifted (the `service_tier` trap that the API later
|
|
110
112
|
* rejected is the motivating case). Only traps with these fields are considered —
|
|
111
113
|
* durable facts are untouched.
|
|
112
114
|
*/
|
|
@@ -135,6 +137,66 @@ export function detectUnverifiedMemory(traps, nowMs = Date.now()) {
|
|
|
135
137
|
}
|
|
136
138
|
return warnings;
|
|
137
139
|
}
|
|
140
|
+
/** Glob chars / URL schemes — entries we cannot meaningfully existsSync. */
|
|
141
|
+
const NON_CHECKABLE_PATH = /[*?[\]]|:\/\//;
|
|
142
|
+
/**
|
|
143
|
+
* Detect memory entities whose `related_paths` point at files that no longer
|
|
144
|
+
* exist — memory that stayed "confident" through a refactor and is now wrong.
|
|
145
|
+
* Staleness today is purely temporal; this is the structural complement
|
|
146
|
+
* (pln#557 step 2, bridge to pln_79a995b6 memory-lifecycle confirm/decay).
|
|
147
|
+
*
|
|
148
|
+
* Only plain paths are probed: glob patterns and URLs are skipped, and a
|
|
149
|
+
* single missing path among several is enough to flag (the warning lists
|
|
150
|
+
* exactly which ones are gone).
|
|
151
|
+
*/
|
|
152
|
+
export function detectDeadReferences(items, projectRoot, nowMs = Date.now()) {
|
|
153
|
+
const warnings = [];
|
|
154
|
+
for (const item of items) {
|
|
155
|
+
if (!item.related_paths || item.related_paths.length === 0)
|
|
156
|
+
continue;
|
|
157
|
+
const missing = [];
|
|
158
|
+
for (const ref of item.related_paths) {
|
|
159
|
+
const trimmed = ref.trim();
|
|
160
|
+
if (!trimmed || NON_CHECKABLE_PATH.test(trimmed))
|
|
161
|
+
continue;
|
|
162
|
+
const resolved = path.isAbsolute(trimmed) ? trimmed : path.join(projectRoot, trimmed);
|
|
163
|
+
try {
|
|
164
|
+
if (!fs.existsSync(resolved))
|
|
165
|
+
missing.push(trimmed);
|
|
166
|
+
}
|
|
167
|
+
catch { /* unreadable path — treat as non-checkable, not dead */ }
|
|
168
|
+
}
|
|
169
|
+
if (missing.length === 0)
|
|
170
|
+
continue;
|
|
171
|
+
warnings.push({
|
|
172
|
+
id: item.id,
|
|
173
|
+
entity: item.entity,
|
|
174
|
+
text: truncate(item.text),
|
|
175
|
+
age_days: ageDays(item.created_at, nowMs),
|
|
176
|
+
reason: `References missing path${missing.length > 1 ? 's' : ''}: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ` (+${missing.length - 3} more)` : ''} — likely stale after a refactor`,
|
|
177
|
+
suggested_action: `bclaw_update(entity: "${item.entity}", id: "${item.short_label ?? item.id}", { related_paths: [<current paths>] }) # or archive if obsolete`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return warnings;
|
|
181
|
+
}
|
|
182
|
+
/** Map active entities into the dead-reference detector's input shape. */
|
|
183
|
+
export function collectDeadReferenceCandidates(scan) {
|
|
184
|
+
const items = [];
|
|
185
|
+
for (const d of scan.decisions ?? []) {
|
|
186
|
+
items.push({ id: d.id, entity: 'decision', text: d.text, created_at: d.created_at, related_paths: d.related_paths, short_label: d.short_label });
|
|
187
|
+
}
|
|
188
|
+
for (const c of scan.constraints ?? []) {
|
|
189
|
+
if ((c.status ?? 'active') !== 'active')
|
|
190
|
+
continue;
|
|
191
|
+
items.push({ id: c.id, entity: 'constraint', text: c.text, created_at: c.created_at, related_paths: c.related_paths, short_label: c.short_label });
|
|
192
|
+
}
|
|
193
|
+
for (const t of scan.traps ?? []) {
|
|
194
|
+
if (t.status !== 'active')
|
|
195
|
+
continue;
|
|
196
|
+
items.push({ id: t.id, entity: 'trap', text: t.text, created_at: t.created_at, related_paths: t.related_paths, short_label: t.short_label });
|
|
197
|
+
}
|
|
198
|
+
return items;
|
|
199
|
+
}
|
|
138
200
|
/**
|
|
139
201
|
* Detect open handoffs that have not been acted on for a long time.
|
|
140
202
|
*/
|
|
@@ -238,7 +300,7 @@ export function detectStaleRuntimeNotes(notes, nowMs = Date.now(), thresholds =
|
|
|
238
300
|
* @param candidates Pending candidates
|
|
239
301
|
* @param nowMs Optional timestamp override (for testing)
|
|
240
302
|
*/
|
|
241
|
-
export function detectStaleness(plans, traps, handoffs, candidates, nowMs = Date.now(), runtimeNotes = []) {
|
|
303
|
+
export function detectStaleness(plans, traps, handoffs, candidates, nowMs = Date.now(), runtimeNotes = [], deadRefScan) {
|
|
242
304
|
const nowIso = new Date(nowMs).toISOString();
|
|
243
305
|
const planWarnings = detectStalePlans(plans, nowMs);
|
|
244
306
|
const trapWarnings = detectExpiredTraps(traps, nowIso, nowMs);
|
|
@@ -246,7 +308,13 @@ export function detectStaleness(plans, traps, handoffs, candidates, nowMs = Date
|
|
|
246
308
|
const handoffWarnings = detectStaleHandoffs(handoffs, nowMs);
|
|
247
309
|
const candidateWarnings = detectStaleCandidates(candidates, nowMs);
|
|
248
310
|
const noteWarnings = detectStaleRuntimeNotes(runtimeNotes, nowMs);
|
|
311
|
+
// pln#557 step 2 — structural staleness: dead related_paths. The trap list
|
|
312
|
+
// for the scan defaults to the traps already passed in.
|
|
313
|
+
const deadRefWarnings = deadRefScan
|
|
314
|
+
? detectDeadReferences(collectDeadReferenceCandidates({ traps, ...deadRefScan }), deadRefScan.projectRoot, nowMs)
|
|
315
|
+
: [];
|
|
249
316
|
const warnings = [
|
|
317
|
+
...deadRefWarnings,
|
|
250
318
|
...planWarnings,
|
|
251
319
|
...trapWarnings,
|
|
252
320
|
...unverifiedWarnings,
|
|
@@ -261,6 +329,7 @@ export function detectStaleness(plans, traps, handoffs, candidates, nowMs = Date
|
|
|
261
329
|
handoff_count: handoffWarnings.length,
|
|
262
330
|
candidate_count: candidateWarnings.length,
|
|
263
331
|
runtime_note_count: noteWarnings.length,
|
|
332
|
+
...(deadRefScan ? { dead_reference_count: deadRefWarnings.length } : {}),
|
|
264
333
|
};
|
|
265
334
|
}
|
|
266
335
|
/** Total warning count across all entity types. */
|
|
@@ -278,6 +347,8 @@ export function staleSummary(report) {
|
|
|
278
347
|
parts.push(`${report.candidate_count} pending candidate${report.candidate_count > 1 ? 's' : ''}`);
|
|
279
348
|
if (report.runtime_note_count > 0)
|
|
280
349
|
parts.push(`${report.runtime_note_count} stale runtime note${report.runtime_note_count > 1 ? 's' : ''}`);
|
|
350
|
+
if ((report.dead_reference_count ?? 0) > 0)
|
|
351
|
+
parts.push(`${report.dead_reference_count} item${report.dead_reference_count > 1 ? 's' : ''} with dead file references`);
|
|
281
352
|
return parts.join(', ');
|
|
282
353
|
}
|
|
283
354
|
//# sourceMappingURL=staleness.js.map
|