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/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
|
@@ -17,14 +17,33 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
20
21
|
import { buildInvokeCommand, getSpawnableAgents, getCapabilityProfile, } from './agent-capability.js';
|
|
21
22
|
import { defaultExecutionAdapter, resolveBinaryOnPath } from './execution-adapters.js';
|
|
22
23
|
import { signalExists, readLogTail } from './runtime-signals.js';
|
|
24
|
+
import { recognizeStderrSignature } from './dispatch-status.js';
|
|
23
25
|
const DEFAULT_PROBE_TIMEOUT_MS = 15_000;
|
|
24
26
|
const DEFAULT_PROBE_PROMPT = 'Reply with exactly: OK';
|
|
25
27
|
async function sleep(ms) {
|
|
26
28
|
return new Promise((r) => setTimeout(r, ms));
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Make the probe's temp dir a real (empty) git repo so the round-trip is
|
|
32
|
+
* representative of a real dispatch (workers always run inside a git worktree)
|
|
33
|
+
* and so CLIs with a boot-time git-repo / trusted-directory check don't refuse
|
|
34
|
+
* it (pln#533 fix). Best-effort: if git is unavailable the probe still runs.
|
|
35
|
+
*/
|
|
36
|
+
function initProbeGitRepo(root) {
|
|
37
|
+
try {
|
|
38
|
+
const run = (...args) => spawnSync('git', args, { cwd: root, encoding: 'utf-8', timeout: 5000 });
|
|
39
|
+
run('init', '-q');
|
|
40
|
+
run('config', 'user.email', 'spawn-check@brainclaw.local');
|
|
41
|
+
run('config', 'user.name', 'brainclaw spawn-check');
|
|
42
|
+
run('config', 'commit.gpgsign', 'false');
|
|
43
|
+
run('commit', '--allow-empty', '-q', '-m', 'spawn-check probe');
|
|
44
|
+
}
|
|
45
|
+
catch { /* git absent or failed — probe proceeds without it */ }
|
|
46
|
+
}
|
|
28
47
|
/** Check one agent's spawn round-trip. Exposed for focused testing. */
|
|
29
48
|
export async function checkAgentSpawn(agent, options = {}) {
|
|
30
49
|
const start = Date.now();
|
|
@@ -43,6 +62,11 @@ export async function checkAgentSpawn(agent, options = {}) {
|
|
|
43
62
|
}
|
|
44
63
|
// Isolated signals root so the probe never pollutes the project's runtime dir.
|
|
45
64
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), `bclaw-spawncheck-${agent}-`));
|
|
65
|
+
// pln#533 fix: make the probe dir a real git repo. Real workers run inside a
|
|
66
|
+
// git worktree, and some CLIs refuse a non-git / untrusted dir at boot (codex:
|
|
67
|
+
// "Not inside a trusted directory and --skip-git-repo-check was not specified")
|
|
68
|
+
// — a non-git temp dir would otherwise produce a false-negative spawn failure.
|
|
69
|
+
initProbeGitRepo(root);
|
|
46
70
|
const assignmentId = 'spawn_check';
|
|
47
71
|
try {
|
|
48
72
|
defaultExecutionAdapter.start(invoke, { agent, assignmentId, ackRoot: root, worktreePath: root });
|
|
@@ -59,17 +83,21 @@ export async function checkAgentSpawn(agent, options = {}) {
|
|
|
59
83
|
const completed = signalExists(root, assignmentId, 'completed');
|
|
60
84
|
const failed = signalExists(root, assignmentId, 'failed');
|
|
61
85
|
const duration_ms = Date.now() - start;
|
|
86
|
+
// Capture the stderr tail once (used both for the detail string and for
|
|
87
|
+
// pln#533 boot-signature recognition on the preflight path).
|
|
88
|
+
const stderrRaw = readLogTail(root, assignmentId, 'stderr', 800).trim();
|
|
89
|
+
const stderrTail = stderrRaw ? stderrRaw.split(/\r?\n/).filter(Boolean) : undefined;
|
|
62
90
|
if (completed) {
|
|
63
91
|
return { agent, binary, status: 'ok', delivered, completed: true, duration_ms, detail: 'ack + completed round-trip' };
|
|
64
92
|
}
|
|
65
93
|
if (failed) {
|
|
66
|
-
const tail =
|
|
67
|
-
return { agent, binary, status: 'failed', delivered, completed: false, duration_ms, detail: `wrapper reported failure${tail ? ` — ${tail.replace(/\s+/g, ' ').slice(0, 200)}` : ''}
|
|
94
|
+
const tail = stderrRaw || readLogTail(root, assignmentId, 'stdout', 400).trim();
|
|
95
|
+
return { agent, binary, status: 'failed', delivered, completed: false, duration_ms, detail: `wrapper reported failure${tail ? ` — ${tail.replace(/\s+/g, ' ').slice(0, 200)}` : ''}`, stderr_tail: stderrTail };
|
|
68
96
|
}
|
|
69
97
|
if (delivered) {
|
|
70
|
-
return { agent, binary, status: 'delivered_no_completion', delivered: true, completed: false, duration_ms, detail: `spawned + ack but no completion within ${timeout}ms (silent-death symptom)
|
|
98
|
+
return { agent, binary, status: 'delivered_no_completion', delivered: true, completed: false, duration_ms, detail: `spawned + ack but no completion within ${timeout}ms (silent-death symptom)`, stderr_tail: stderrTail };
|
|
71
99
|
}
|
|
72
|
-
return { agent, binary, status: 'failed', delivered: false, completed: false, duration_ms, detail: `no ack within ${timeout}ms — delivery failed
|
|
100
|
+
return { agent, binary, status: 'failed', delivered: false, completed: false, duration_ms, detail: `no ack within ${timeout}ms — delivery failed`, stderr_tail: stderrTail };
|
|
73
101
|
}
|
|
74
102
|
finally {
|
|
75
103
|
try {
|
|
@@ -122,4 +150,82 @@ export function renderSpawnCheckReport(report) {
|
|
|
122
150
|
}
|
|
123
151
|
return lines.join('\n');
|
|
124
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Pre-flight a single target agent. Pass criteria:
|
|
155
|
+
* - `ok` (ack + completed) → pass.
|
|
156
|
+
* - `delivered_no_completion` (ack but the trivial probe didn't finish in the
|
|
157
|
+
* short window) → PASS: the ack proves spawn + wrapper + delivery work; a
|
|
158
|
+
* boot death never acks. We don't want a slow-but-healthy agent to block.
|
|
159
|
+
* - `failed` / no-ack → BLOCK with a reason (enriched by a recognized boot
|
|
160
|
+
* signature when the stderr matches one).
|
|
161
|
+
* - `not_installed` / `no_template` → BLOCK: the agent cannot be spawned here,
|
|
162
|
+
* so opening a loop on it would only time out.
|
|
163
|
+
* When BRAINCLAW_NO_SPAWN is set (tests/CI), pre-flight is skipped (ok:true).
|
|
164
|
+
*/
|
|
165
|
+
/**
|
|
166
|
+
* Pure mapper: SpawnCheckEntry → PreflightResult. No spawning — exposed so the
|
|
167
|
+
* pass/block policy (and the boot-signature enrichment) can be unit-tested with
|
|
168
|
+
* synthetic entries.
|
|
169
|
+
*/
|
|
170
|
+
export function preflightResultFromEntry(entry) {
|
|
171
|
+
const agent = entry.agent;
|
|
172
|
+
if (entry.status === 'ok' || entry.status === 'delivered_no_completion') {
|
|
173
|
+
return { agent, ok: true, status: entry.status, reason: entry.detail };
|
|
174
|
+
}
|
|
175
|
+
if (entry.status === 'not_installed') {
|
|
176
|
+
return {
|
|
177
|
+
agent, ok: false, status: entry.status,
|
|
178
|
+
reason: `${agent} binary not on PATH — cannot spawn it here`,
|
|
179
|
+
recommended_next_action: `Install the ${agent} CLI (or target a different agent), then retry.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (entry.status === 'no_template') {
|
|
183
|
+
return {
|
|
184
|
+
agent, ok: false, status: entry.status,
|
|
185
|
+
reason: `${agent} has no CLI spawn template — it cannot be auto-dispatched (IDE-only?)`,
|
|
186
|
+
recommended_next_action: `Target a CLI-spawnable agent, or hand this work to ${agent} interactively.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// failed (or no-ack) — try to attach a recognized boot signature.
|
|
190
|
+
const sig = recognizeStderrSignature(entry.stderr_tail);
|
|
191
|
+
return {
|
|
192
|
+
agent, ok: false, status: entry.status,
|
|
193
|
+
reason: sig?.summary ?? `${agent} failed its pre-flight spawn — ${entry.detail}`,
|
|
194
|
+
recommended_next_action: sig?.recommended_next_action
|
|
195
|
+
?? `Inspect the ${agent} CLI config/auth (run \`brainclaw doctor --spawn-check\` for detail), fix it, then retry.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export async function preflightAgentSpawn(agent, options = {}) {
|
|
199
|
+
if (process.env.BRAINCLAW_NO_SPAWN === '1') {
|
|
200
|
+
return { agent, ok: true, status: 'skipped', reason: 'pre-flight skipped (BRAINCLAW_NO_SPAWN)' };
|
|
201
|
+
}
|
|
202
|
+
// Pre-flight uses a tighter window than the full doctor round-trip: a boot
|
|
203
|
+
// death fails fast, and an ack is enough to pass, so we don't need to wait
|
|
204
|
+
// out a healthy agent's full probe completion.
|
|
205
|
+
const entry = await checkAgentSpawn(agent, { timeoutMs: 8_000, ...options });
|
|
206
|
+
return preflightResultFromEntry(entry);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Pre-flight a set of target agents (deduped), one trivial probe each. Returns
|
|
210
|
+
* the per-agent results plus `blocked` (the agents that failed). Callers use
|
|
211
|
+
* `blocked` to skip those agents and surface their reasons instead of opening a
|
|
212
|
+
* loop / dispatching work that would only time out.
|
|
213
|
+
*/
|
|
214
|
+
export async function preflightAgents(agents, options = {}) {
|
|
215
|
+
const unique = [...new Set(agents)];
|
|
216
|
+
const results = [];
|
|
217
|
+
for (const agent of unique) {
|
|
218
|
+
try {
|
|
219
|
+
results.push(await preflightAgentSpawn(agent, options));
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
results.push({
|
|
223
|
+
agent, ok: false, status: 'failed',
|
|
224
|
+
reason: `pre-flight threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const blocked = results.filter((r) => !r.ok);
|
|
229
|
+
return { results, blocked, all_ok: blocked.length === 0 };
|
|
230
|
+
}
|
|
125
231
|
//# sourceMappingURL=spawn-check.js.map
|
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 = {
|
|
@@ -100,6 +102,101 @@ export function detectExpiredTraps(traps, nowIso = new Date().toISOString(), now
|
|
|
100
102
|
}
|
|
101
103
|
return warnings;
|
|
102
104
|
}
|
|
105
|
+
/** pln#530 — a perishable fact unverified for longer than this reads as stale. */
|
|
106
|
+
const VERIFIED_STALE_DAYS = 30;
|
|
107
|
+
/**
|
|
108
|
+
* pln#530 — flag perishable memories (traps that opted in by carrying a
|
|
109
|
+
* `verify_cmd` and/or `verified_at`) whose last empirical verification is stale
|
|
110
|
+
* or never happened, so an agent re-probes the live system instead of trusting a
|
|
111
|
+
* value that may have drifted (the LeaseUp `service_tier` trap that the API later
|
|
112
|
+
* rejected is the motivating case). Only traps with these fields are considered —
|
|
113
|
+
* durable facts are untouched.
|
|
114
|
+
*/
|
|
115
|
+
export function detectUnverifiedMemory(traps, nowMs = Date.now()) {
|
|
116
|
+
const warnings = [];
|
|
117
|
+
for (const trap of traps) {
|
|
118
|
+
if (trap.status !== 'active')
|
|
119
|
+
continue;
|
|
120
|
+
if (!trap.verify_cmd && !trap.verified_at)
|
|
121
|
+
continue; // opt-in: only perishable facts
|
|
122
|
+
const age = trap.verified_at ? ageDays(trap.verified_at, nowMs) : Infinity;
|
|
123
|
+
if (trap.verified_at && age < VERIFIED_STALE_DAYS)
|
|
124
|
+
continue; // freshly verified
|
|
125
|
+
warnings.push({
|
|
126
|
+
id: trap.id,
|
|
127
|
+
entity: 'trap',
|
|
128
|
+
text: truncate(trap.text),
|
|
129
|
+
age_days: Number.isFinite(age) ? age : 9999,
|
|
130
|
+
reason: trap.verified_at
|
|
131
|
+
? `Perishable fact last verified ${age} day${age === 1 ? '' : 's'} ago — re-confirm against the live system before trusting`
|
|
132
|
+
: `Perishable fact never empirically verified (verify_cmd set) — confirm before trusting`,
|
|
133
|
+
suggested_action: trap.verify_cmd
|
|
134
|
+
? `Run \`${trap.verify_cmd}\`, then bclaw_update(trap, ${trap.short_label ?? trap.id}, { verified_at: <now> })`
|
|
135
|
+
: `Re-verify against the live system, then set verified_at via bclaw_update`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return warnings;
|
|
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
|
+
}
|
|
103
200
|
/**
|
|
104
201
|
* Detect open handoffs that have not been acted on for a long time.
|
|
105
202
|
*/
|
|
@@ -203,16 +300,24 @@ export function detectStaleRuntimeNotes(notes, nowMs = Date.now(), thresholds =
|
|
|
203
300
|
* @param candidates Pending candidates
|
|
204
301
|
* @param nowMs Optional timestamp override (for testing)
|
|
205
302
|
*/
|
|
206
|
-
export function detectStaleness(plans, traps, handoffs, candidates, nowMs = Date.now(), runtimeNotes = []) {
|
|
303
|
+
export function detectStaleness(plans, traps, handoffs, candidates, nowMs = Date.now(), runtimeNotes = [], deadRefScan) {
|
|
207
304
|
const nowIso = new Date(nowMs).toISOString();
|
|
208
305
|
const planWarnings = detectStalePlans(plans, nowMs);
|
|
209
306
|
const trapWarnings = detectExpiredTraps(traps, nowIso, nowMs);
|
|
307
|
+
const unverifiedWarnings = detectUnverifiedMemory(traps, nowMs); // pln#530
|
|
210
308
|
const handoffWarnings = detectStaleHandoffs(handoffs, nowMs);
|
|
211
309
|
const candidateWarnings = detectStaleCandidates(candidates, nowMs);
|
|
212
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
|
+
: [];
|
|
213
316
|
const warnings = [
|
|
317
|
+
...deadRefWarnings,
|
|
214
318
|
...planWarnings,
|
|
215
319
|
...trapWarnings,
|
|
320
|
+
...unverifiedWarnings,
|
|
216
321
|
...handoffWarnings,
|
|
217
322
|
...candidateWarnings,
|
|
218
323
|
...noteWarnings,
|
|
@@ -224,6 +329,7 @@ export function detectStaleness(plans, traps, handoffs, candidates, nowMs = Date
|
|
|
224
329
|
handoff_count: handoffWarnings.length,
|
|
225
330
|
candidate_count: candidateWarnings.length,
|
|
226
331
|
runtime_note_count: noteWarnings.length,
|
|
332
|
+
...(deadRefScan ? { dead_reference_count: deadRefWarnings.length } : {}),
|
|
227
333
|
};
|
|
228
334
|
}
|
|
229
335
|
/** Total warning count across all entity types. */
|
|
@@ -241,6 +347,8 @@ export function staleSummary(report) {
|
|
|
241
347
|
parts.push(`${report.candidate_count} pending candidate${report.candidate_count > 1 ? 's' : ''}`);
|
|
242
348
|
if (report.runtime_note_count > 0)
|
|
243
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`);
|
|
244
352
|
return parts.join(', ');
|
|
245
353
|
}
|
|
246
354
|
//# sourceMappingURL=staleness.js.map
|