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
|
@@ -2,7 +2,9 @@ import { memoryExists, memoryPath } from '../core/io.js';
|
|
|
2
2
|
import { loadConfig } from '../core/config.js';
|
|
3
3
|
import { SecurityCache } from '../core/security-cache.js';
|
|
4
4
|
import { querySocketScores } from '../core/socket-client.js';
|
|
5
|
-
import { evaluateBatch, worstDecision } from '../core/security-scoring.js';
|
|
5
|
+
import { applyMode, decisionExitCode, evaluateBatch, worstDecision, } from '../core/security-scoring.js';
|
|
6
|
+
import { parsePackageSpec } from '../core/security-packages.js';
|
|
7
|
+
import { collectPackages } from '../core/security-extract.js';
|
|
6
8
|
export async function runCheckSecurity(options) {
|
|
7
9
|
if (!memoryExists(options.cwd)) {
|
|
8
10
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
@@ -15,8 +17,22 @@ export async function runCheckSecurity(options) {
|
|
|
15
17
|
process.exit(1);
|
|
16
18
|
}
|
|
17
19
|
const ecosystem = options.ecosystem ?? 'npm';
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
+
const effectiveMode = options.mode ?? preinstall.mode ?? 'advisory';
|
|
21
|
+
let packageSpecs;
|
|
22
|
+
try {
|
|
23
|
+
packageSpecs = collectPackages({
|
|
24
|
+
packages: options.packages,
|
|
25
|
+
requirements: options.requirements,
|
|
26
|
+
lockfile: options.lockfile,
|
|
27
|
+
defaultEcosystem: ecosystem,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32
|
+
console.error(`Error: ${msg}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (packageSpecs.length === 0) {
|
|
20
36
|
console.error('No packages specified.');
|
|
21
37
|
process.exit(1);
|
|
22
38
|
}
|
|
@@ -25,28 +41,23 @@ export async function runCheckSecurity(options) {
|
|
|
25
41
|
const cache = new SecurityCache(cachePath, preinstall.cache_ttl_hours);
|
|
26
42
|
// Separate cached vs uncached
|
|
27
43
|
const queries = [];
|
|
28
|
-
const
|
|
29
|
-
for (const
|
|
30
|
-
const
|
|
31
|
-
? name.split('@')
|
|
32
|
-
: name.startsWith('@')
|
|
33
|
-
? [name.slice(0, name.lastIndexOf('@')) || name, name.slice(name.lastIndexOf('@') + 1) || 'latest']
|
|
34
|
-
: [name, 'latest'];
|
|
44
|
+
const cachedScores = [];
|
|
45
|
+
for (const spec of packageSpecs) {
|
|
46
|
+
const { depname, version } = parsePackageSpec(spec);
|
|
35
47
|
const cached = cache.get(ecosystem, depname, version);
|
|
36
|
-
const query = { depname, ecosystem, ...(version !== 'latest' ? { version } : {}) };
|
|
37
48
|
if (cached) {
|
|
38
|
-
|
|
49
|
+
cachedScores.push(cached);
|
|
39
50
|
}
|
|
40
51
|
else {
|
|
41
|
-
queries.push(
|
|
52
|
+
queries.push({ depname, ecosystem, ...(version !== 'latest' ? { version } : {}) });
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
55
|
// Fetch uncached from Socket
|
|
45
56
|
let fetchedScores = [];
|
|
57
|
+
let fetchError = null;
|
|
46
58
|
if (queries.length > 0) {
|
|
47
59
|
try {
|
|
48
60
|
fetchedScores = await querySocketScores(queries, { endpoint: preinstall.socket_endpoint });
|
|
49
|
-
// Update cache
|
|
50
61
|
for (const s of fetchedScores) {
|
|
51
62
|
const eco = s.purl.startsWith('pkg:pypi') ? 'pypi' : 'npm';
|
|
52
63
|
const depname = s.purl.replace(/^pkg:\w+\//, '');
|
|
@@ -55,52 +66,84 @@ export async function runCheckSecurity(options) {
|
|
|
55
66
|
cache.flush();
|
|
56
67
|
}
|
|
57
68
|
catch (err) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
fetchError = err instanceof Error ? err.message : String(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Decide what to do on offline / fetch failure.
|
|
73
|
+
// fallback_on_error semantics:
|
|
74
|
+
// block — abort regardless of mode (operator opted into strictness)
|
|
75
|
+
// warn — surface warning, continue with whatever cache we have
|
|
76
|
+
// pass — silent, continue with cache (or treat as pass if no data)
|
|
77
|
+
if (fetchError && cachedScores.length === 0) {
|
|
78
|
+
const fallback = preinstall.fallback_on_error;
|
|
79
|
+
if (options.json) {
|
|
80
|
+
console.log(JSON.stringify({
|
|
81
|
+
verdicts: [],
|
|
82
|
+
decision: fallbackDecision(fallback),
|
|
83
|
+
effective_decision: applyMode(fallbackDecision(fallback), effectiveMode),
|
|
84
|
+
mode: effectiveMode,
|
|
85
|
+
fetch_error: fetchError,
|
|
86
|
+
}, null, 2));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.error(`Socket MCP error: ${fetchError}`);
|
|
90
|
+
console.error(`Fallback policy: ${fallback} — no cached results to fall back to.`);
|
|
91
|
+
}
|
|
92
|
+
process.exit(decisionExitCode(applyMode(fallbackDecision(fallback), effectiveMode)));
|
|
93
|
+
}
|
|
94
|
+
if (fetchError) {
|
|
95
|
+
// Partial failure: we have some cache, surface a warning.
|
|
96
|
+
if (!options.json) {
|
|
97
|
+
console.error(`Warning: Socket MCP error: ${fetchError} (continuing with ${cachedScores.length} cached result(s))`);
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
|
-
|
|
73
|
-
const allScores = [
|
|
74
|
-
...cachedResults.filter(c => c.scores !== null).map(c => c.scores),
|
|
75
|
-
...fetchedScores,
|
|
76
|
-
];
|
|
100
|
+
const allScores = [...cachedScores, ...fetchedScores];
|
|
77
101
|
const verdicts = evaluateBatch(allScores, preinstall);
|
|
78
|
-
const
|
|
102
|
+
const intrinsic = worstDecision(verdicts);
|
|
103
|
+
const effective = applyMode(intrinsic, effectiveMode);
|
|
79
104
|
if (options.json) {
|
|
80
|
-
console.log(JSON.stringify({
|
|
105
|
+
console.log(JSON.stringify({
|
|
106
|
+
verdicts,
|
|
107
|
+
decision: intrinsic,
|
|
108
|
+
effective_decision: effective,
|
|
109
|
+
mode: effectiveMode,
|
|
110
|
+
...(fetchError ? { fetch_error: fetchError } : {}),
|
|
111
|
+
}, null, 2));
|
|
81
112
|
}
|
|
82
113
|
else {
|
|
83
|
-
printVerdicts(verdicts);
|
|
114
|
+
printVerdicts(verdicts, intrinsic, effective, effectiveMode);
|
|
84
115
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
process.exit(decisionExitCode(effective));
|
|
117
|
+
}
|
|
118
|
+
function fallbackDecision(fallback) {
|
|
119
|
+
if (fallback === 'block')
|
|
120
|
+
return 'block';
|
|
121
|
+
if (fallback === 'warn')
|
|
122
|
+
return 'warn';
|
|
123
|
+
return 'pass';
|
|
91
124
|
}
|
|
92
|
-
function printVerdicts(verdicts) {
|
|
125
|
+
function printVerdicts(verdicts, intrinsic, effective, mode) {
|
|
93
126
|
if (verdicts.length === 0) {
|
|
94
127
|
console.log('No packages to check.');
|
|
95
128
|
return;
|
|
96
129
|
}
|
|
97
130
|
for (const v of verdicts) {
|
|
98
|
-
const icon = v.decision === 'pass' ? '
|
|
131
|
+
const icon = v.decision === 'pass' ? '✅' : v.decision === 'warn' ? '⚠️' : '🛑';
|
|
99
132
|
console.log(`${icon} ${v.ecosystem}/${v.package}@${v.version} — composite=${v.composite} [${v.decision.toUpperCase()}]`);
|
|
100
133
|
console.log(` SC=${v.scores.supplyChain} vuln=${v.scores.vulnerability} qual=${v.scores.quality} maint=${v.scores.maintenance} lic=${v.scores.license}`);
|
|
101
134
|
for (const r of v.reasons) {
|
|
102
|
-
console.log(`
|
|
135
|
+
console.log(` → ${r}`);
|
|
103
136
|
}
|
|
104
137
|
}
|
|
138
|
+
// Surface the mode-aware outcome so operators see what would have happened.
|
|
139
|
+
if (intrinsic !== effective) {
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(`Verdict: ${intrinsic.toUpperCase()} — downgraded to ${effective.toUpperCase()} by mode=advisory.`);
|
|
142
|
+
console.log(' Switch to enforced mode (brainclaw setup-security --mode enforced) to block on this verdict.');
|
|
143
|
+
}
|
|
144
|
+
else if (verdicts.length > 0) {
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(`Verdict: ${intrinsic.toUpperCase()} (mode=${mode})`);
|
|
147
|
+
}
|
|
105
148
|
}
|
|
106
149
|
//# sourceMappingURL=check-security.js.map
|
package/dist/commands/claim.js
CHANGED
|
@@ -42,15 +42,6 @@ export function runClaim(description, options) {
|
|
|
42
42
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
43
43
|
process.exit(1);
|
|
44
44
|
}
|
|
45
|
-
// Check for overlapping active claims on the same scope
|
|
46
|
-
const existing = listClaims(options.cwd).filter(c => c.status === 'active' && c.scope === options.scope);
|
|
47
|
-
if (existing.length > 0) {
|
|
48
|
-
console.warn(`⚠ Active claim(s) already exist for scope "${options.scope}":`);
|
|
49
|
-
for (const c of existing) {
|
|
50
|
-
console.warn(` [${c.id}] by ${c.agent}: ${c.description}`);
|
|
51
|
-
}
|
|
52
|
-
console.warn(' Proceeding anyway (advisory only).');
|
|
53
|
-
}
|
|
54
45
|
const state = loadState(options.cwd);
|
|
55
46
|
const plan = options.plan ? state.plan_items.find((item) => item.id === options.plan) : undefined;
|
|
56
47
|
if (options.plan && !plan) {
|
|
@@ -74,20 +65,30 @@ export function runClaim(description, options) {
|
|
|
74
65
|
expires_at: options.ttl ? parseTtl(options.ttl) : undefined,
|
|
75
66
|
model: resolveCurrentModel(options.cwd),
|
|
76
67
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
try {
|
|
69
|
+
mutate({ cwd: options.cwd }, () => {
|
|
70
|
+
const existing = listClaims(options.cwd).find(c => c.status === 'active' && c.scope === options.scope);
|
|
71
|
+
if (existing) {
|
|
72
|
+
throw new Error(`Active claim already exists for scope "${options.scope}": [${existing.id}] by ${existing.agent}: ${existing.description}`);
|
|
81
73
|
}
|
|
82
|
-
if (plan
|
|
83
|
-
plan.
|
|
74
|
+
if (plan) {
|
|
75
|
+
if (!plan.assignee) {
|
|
76
|
+
plan.assignee = actor.agent;
|
|
77
|
+
}
|
|
78
|
+
if (plan.status === 'todo') {
|
|
79
|
+
plan.status = 'in_progress';
|
|
80
|
+
}
|
|
81
|
+
plan.updated_at = nowISO();
|
|
82
|
+
saveState(state, options.cwd);
|
|
84
83
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
saveClaim(claim, options.cwd);
|
|
85
|
+
rebuildProjectMd(plan ? state : loadState(options.cwd), options.cwd);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
91
92
|
const planInfo = claim.plan_id ? ` [plan ${claim.plan_id}]` : '';
|
|
92
93
|
const ttlInfo = claim.expires_at ? ` (expires ${claim.expires_at.slice(0, 16).replace('T', ' ')})` : '';
|
|
93
94
|
const storeLabel = options.store && options.store !== 'local' ? ` [store:${options.store}]` : '';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
export async function confirmAction(question, yes) {
|
|
4
|
+
if (yes)
|
|
5
|
+
return;
|
|
6
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
7
|
+
console.error(`Error: ${question} Re-run with --yes in non-interactive mode.`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const rl = readline.createInterface({ input, output });
|
|
11
|
+
try {
|
|
12
|
+
const answer = await rl.question(`${question} [y/N] `);
|
|
13
|
+
if (answer.trim().toLowerCase() !== 'y') {
|
|
14
|
+
console.error('Cancelled.');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
rl.close();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=confirm.js.map
|
|
@@ -20,7 +20,7 @@ export function runContextDiff(options = {}) {
|
|
|
20
20
|
console.error(`Error: session '${options.session}' not found in session snapshots or audit log.`);
|
|
21
21
|
process.exit(1);
|
|
22
22
|
}
|
|
23
|
-
console.error('Error: provide --since <ISO date> or --session <id
|
|
23
|
+
console.error('Error: provide --since <ISO date> or --session <id>. (The per-agent "what\'s new" diff is surfaced automatically by `brainclaw context` / bclaw_work.)');
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
26
|
const diff = buildContextDiff({ ...options, includeItems: true });
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { getDispatchStatus } from '../core/dispatch-status.js';
|
|
3
|
+
/** Filesystem activity younger than this vetoes a process-gone verdict. */
|
|
4
|
+
export const FS_FRESH_MS = 5 * 60_000;
|
|
5
|
+
/**
|
|
6
|
+
* Pure decision core — one watch poll in, one state out.
|
|
7
|
+
* Evidence priority (strongest first): worker-written results, then git
|
|
8
|
+
* evidence, then process evidence, then administrative status. This is the
|
|
9
|
+
* inverse of trusting `assignment.status`, which expired live workers three
|
|
10
|
+
* times on 2026-06-10 (can_948acfd6).
|
|
11
|
+
*/
|
|
12
|
+
export function evaluateWatchTick(input) {
|
|
13
|
+
if (input.laneResultStatus === 'failed')
|
|
14
|
+
return 'failed';
|
|
15
|
+
if (input.laneResultStatus !== undefined)
|
|
16
|
+
return 'lane-result';
|
|
17
|
+
if (input.runStatus === 'completed')
|
|
18
|
+
return 'completed';
|
|
19
|
+
if (input.runStatus === 'failed' || input.runStatus === 'timed_out')
|
|
20
|
+
return 'failed';
|
|
21
|
+
// Fresh filesystem activity / a live agent child = the worker is still at
|
|
22
|
+
// work. This vetoes committed-clean (incremental commits leave the tree
|
|
23
|
+
// momentarily clean BETWEEN steps — first-run false positive, stp_a1fe2b76)
|
|
24
|
+
// AND the process-gone verdicts below (stale tracked pid after a respawn).
|
|
25
|
+
const fsFresh = input.fsActivityMs !== undefined && input.fsActivityMs < FS_FRESH_MS;
|
|
26
|
+
const workerActive = input.agentChildAlive === true || fsFresh;
|
|
27
|
+
// Git evidence beats process evidence: a worker that committed everything
|
|
28
|
+
// and went QUIESCENT is DONE for the coordinator's purposes.
|
|
29
|
+
if (input.commitsAhead > 0 && input.dirtyTracked === 0 && !workerActive) {
|
|
30
|
+
return 'committed-clean';
|
|
31
|
+
}
|
|
32
|
+
// Wrapper alive but the real agent child is gone: abrupt death — the wrapper
|
|
33
|
+
// waits forever on inherited pipe handles and never emits a sentinel.
|
|
34
|
+
// agentChildAlive === undefined means "could not observe" — never conclude
|
|
35
|
+
// death from a failed observation.
|
|
36
|
+
if (input.agentChildAlive === false && !fsFresh)
|
|
37
|
+
return 'worker-process-gone';
|
|
38
|
+
// Wrapper itself dead with nothing delivered: same recovery path.
|
|
39
|
+
if (input.pidAlive === false && input.agentChildAlive !== true && !fsFresh) {
|
|
40
|
+
return 'worker-process-gone';
|
|
41
|
+
}
|
|
42
|
+
return 'running';
|
|
43
|
+
}
|
|
44
|
+
const AGENT_CHILD_NAMES = ['claude', 'codex', 'copilot', 'node'];
|
|
45
|
+
/**
|
|
46
|
+
* Does a real agent process live under the wrapper pid?
|
|
47
|
+
* Returns undefined when the observation itself fails (never treated as death).
|
|
48
|
+
*/
|
|
49
|
+
export function probeAgentChildAlive(wrapperPid) {
|
|
50
|
+
if (!wrapperPid)
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
if (process.platform === 'win32') {
|
|
54
|
+
const out = execFileSync('powershell', [
|
|
55
|
+
'-NoProfile', '-Command',
|
|
56
|
+
// Single quotes survive Windows argv re-parsing; double quotes do not.
|
|
57
|
+
`(Get-CimInstance Win32_Process -Filter 'ParentProcessId=${Math.floor(wrapperPid)}').Name`,
|
|
58
|
+
], { encoding: 'utf-8', timeout: 15000 });
|
|
59
|
+
const names = out.split(/\r?\n/).map((l) => l.trim().toLowerCase()).filter(Boolean);
|
|
60
|
+
return names.some((n) => AGENT_CHILD_NAMES.some((a) => n.startsWith(a)));
|
|
61
|
+
}
|
|
62
|
+
const out = execFileSync('ps', ['-o', 'comm=', '--ppid', String(wrapperPid)], {
|
|
63
|
+
encoding: 'utf-8', timeout: 15000,
|
|
64
|
+
});
|
|
65
|
+
const names = out.split('\n').map((l) => l.trim().toLowerCase()).filter(Boolean);
|
|
66
|
+
return names.some((n) => AGENT_CHILD_NAMES.some((a) => n.includes(a)));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const EXIT_CODES = {
|
|
73
|
+
'running': 2, // only used when the timeout fires
|
|
74
|
+
'lane-result': 0,
|
|
75
|
+
'completed': 0,
|
|
76
|
+
'committed-clean': 0,
|
|
77
|
+
'failed': 3,
|
|
78
|
+
'worker-process-gone': 4,
|
|
79
|
+
};
|
|
80
|
+
const NEXT_ACTION = {
|
|
81
|
+
'running': 'Watch timeout — worker still running; re-run watch or inspect with dispatch-status.',
|
|
82
|
+
'lane-result': 'Run `brainclaw harvest <assignment_id> --integrate` to ingest and converge the lane.',
|
|
83
|
+
'completed': 'Run `brainclaw harvest <assignment_id> --integrate` (or merge the lane branch).',
|
|
84
|
+
'committed-clean': 'Work is on the branch; harvest/merge it. The worker stalled only on exit formalities.',
|
|
85
|
+
'failed': 'Read the captured stderr log, then fix and re-dispatch.',
|
|
86
|
+
'worker-process-gone': 'Triage the worktree (commits? dirty files?). Recover uncommitted work by evaluate+commit-on-behalf before any re-dispatch.',
|
|
87
|
+
};
|
|
88
|
+
function sleep(ms) {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
export async function runDispatchWatch(targetId, options = {}) {
|
|
92
|
+
const intervalMs = Math.max(5, options.intervalSeconds ?? 60) * 1000;
|
|
93
|
+
const timeoutMs = Math.max(1, options.timeoutMinutes ?? 90) * 60_000;
|
|
94
|
+
const baseRef = options.base ?? 'master';
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
let poll = 0;
|
|
97
|
+
let lastState = 'running';
|
|
98
|
+
let lastStatus;
|
|
99
|
+
for (;;) {
|
|
100
|
+
poll += 1;
|
|
101
|
+
let status;
|
|
102
|
+
try {
|
|
103
|
+
status = getDispatchStatus({ target_id: targetId, cwd: options.cwd, tail_log_lines: 0, base_ref: baseRef });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`Error: could not resolve '${targetId}': ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
process.exitCode = 5;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
lastStatus = status;
|
|
111
|
+
// Git evidence is computed by getDispatchStatus (shared helper, pln#554 step 2).
|
|
112
|
+
const commitsAhead = status.runtime.commits_ahead ?? 0;
|
|
113
|
+
const dirtyTracked = status.runtime.dirty_tracked ?? 0;
|
|
114
|
+
const state = evaluateWatchTick({
|
|
115
|
+
health: status.diagnosis.health,
|
|
116
|
+
runStatus: status.agent_run?.status,
|
|
117
|
+
laneResultStatus: status.runtime.lane_result?.status,
|
|
118
|
+
pidAlive: status.runtime.pid_alive,
|
|
119
|
+
agentChildAlive: probeAgentChildAlive(status.runtime.pid),
|
|
120
|
+
commitsAhead,
|
|
121
|
+
dirtyTracked,
|
|
122
|
+
fsActivityMs: status.runtime.last_fs_activity_ms,
|
|
123
|
+
});
|
|
124
|
+
lastState = state;
|
|
125
|
+
const line = options.json
|
|
126
|
+
? JSON.stringify({ poll, state, commits_ahead: commitsAhead, dirty_tracked: dirtyTracked, health: status.diagnosis.health })
|
|
127
|
+
: `[poll ${poll}] ${state} (commits=${commitsAhead} dirty=${dirtyTracked} health=${status.diagnosis.health})`;
|
|
128
|
+
console.log(line);
|
|
129
|
+
if (state !== 'running')
|
|
130
|
+
break;
|
|
131
|
+
if (Date.now() - startedAt + intervalMs > timeoutMs)
|
|
132
|
+
break;
|
|
133
|
+
await sleep(intervalMs);
|
|
134
|
+
}
|
|
135
|
+
const assignmentId = lastStatus?.entities.assignment_id ?? targetId;
|
|
136
|
+
if (!options.json) {
|
|
137
|
+
console.log(lastState === 'running' ? 'TIMEOUT' : 'TERMINAL');
|
|
138
|
+
console.log(`→ ${NEXT_ACTION[lastState].replace('<assignment_id>', assignmentId)}`);
|
|
139
|
+
}
|
|
140
|
+
process.exitCode = EXIT_CODES[lastState];
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=dispatch-watch.js.map
|
package/dist/commands/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as childProcess from 'node:child_process';
|
|
|
5
5
|
import { reconcileAllOpenRuns } from '../core/agentrun-reconciler.js';
|
|
6
6
|
import { runSpawnCheck, renderSpawnCheckReport } from '../core/spawn-check.js';
|
|
7
7
|
import { loadAgentRun } from '../core/agentruns.js';
|
|
8
|
-
import { listAgentIdentities, resolveCurrentAgentIdentity } from '../core/agent-registry.js';
|
|
8
|
+
import { listAgentIdentities, listDebrisAgentIdentities, resolveCurrentAgentIdentity } from '../core/agent-registry.js';
|
|
9
9
|
import { listCapabilities as listRegistryCapabilities, listTools as listRegistryTools } from '../core/registries.js';
|
|
10
10
|
import { buildReputationSummary } from '../core/reputation.js';
|
|
11
11
|
import { buildCircuitBreakerSnapshot } from '../core/circuit-breaker.js';
|
|
@@ -24,6 +24,8 @@ import { listRuntimeNotes } from '../core/runtime.js';
|
|
|
24
24
|
import { isTrapExpired, listOperationalTraps } from '../core/traps.js';
|
|
25
25
|
import { scanText } from '../core/security.js';
|
|
26
26
|
import { isTaskLifecycleRuntimeEvent, listRuntimeEvents } from '../core/events.js';
|
|
27
|
+
import { verifyProjectionsAgainstJournal, verifyRegistryAgainstJournal } from '../core/events/verify.js';
|
|
28
|
+
import { resolveJournalMode } from '../core/events/journal.js';
|
|
27
29
|
import { resolveEventSessionId } from '../core/identity.js';
|
|
28
30
|
import { detectContradictions } from '../core/contradictions.js';
|
|
29
31
|
import { loadVersionedJsonFile, scanMigrationStatus } from '../core/migration.js';
|
|
@@ -582,7 +584,64 @@ export async function runDoctorSpawnCheck(options = {}) {
|
|
|
582
584
|
if (report.exit_code !== 0)
|
|
583
585
|
process.exit(report.exit_code);
|
|
584
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* pln#565 — `brainclaw doctor --verify-journal`: the single-command Phase-2
|
|
589
|
+
* cutover gate. Rebuilds state from the journal and diffs it against the live
|
|
590
|
+
* projections; zero drift across real multi-agent traffic is the green light
|
|
591
|
+
* to flip `store.journal.mode` to primary (pln#543 step 5). Exit 1 on drift so
|
|
592
|
+
* CI can gate on it.
|
|
593
|
+
*/
|
|
594
|
+
function runJournalVerification(options) {
|
|
595
|
+
if (!memoryExists(options.cwd)) {
|
|
596
|
+
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
const mode = resolveJournalMode(options.cwd);
|
|
600
|
+
const t0 = Date.now();
|
|
601
|
+
// pln#568 — the gate now covers BOTH the memory families and the registry /
|
|
602
|
+
// coordination families (claim/assignment/agent_run/candidate/sequence), so
|
|
603
|
+
// sustained zero drift authorizes trusting the journal for the observer's
|
|
604
|
+
// attention inputs too, not just memory.
|
|
605
|
+
const drift = [...verifyProjectionsAgainstJournal(options.cwd), ...verifyRegistryAgainstJournal(options.cwd)];
|
|
606
|
+
const elapsed_ms = Date.now() - t0;
|
|
607
|
+
if (options.json) {
|
|
608
|
+
console.log(JSON.stringify({
|
|
609
|
+
check: 'verify-journal',
|
|
610
|
+
journal_mode: mode,
|
|
611
|
+
drift_count: drift.length,
|
|
612
|
+
drift: drift.slice(0, 100),
|
|
613
|
+
elapsed_ms,
|
|
614
|
+
gate: drift.length === 0 ? 'green' : 'drift',
|
|
615
|
+
}, null, 2));
|
|
616
|
+
}
|
|
617
|
+
else if (mode === 'off') {
|
|
618
|
+
console.log('⚠ Journal mode is "off" — nothing to verify (no dual-write running). Set store.journal.mode=dual first.');
|
|
619
|
+
}
|
|
620
|
+
else if (drift.length === 0) {
|
|
621
|
+
console.log(`✔ Journal verification GREEN (mode=${mode}, replay ${elapsed_ms}ms): journal reproduces projections exactly — zero drift.`);
|
|
622
|
+
console.log(' → Phase-2 gate criterion met for this store snapshot. Sustained zero drift across multi-agent traffic authorizes the primary cutover (pln#543 step 5).');
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
console.error(`✗ Journal verification found ${drift.length} drift entr${drift.length === 1 ? 'y' : 'ies'} (mode=${mode}):`);
|
|
626
|
+
const byKind = {};
|
|
627
|
+
for (const d of drift)
|
|
628
|
+
byKind[d.kind] = (byKind[d.kind] ?? 0) + 1;
|
|
629
|
+
for (const [kind, n] of Object.entries(byKind))
|
|
630
|
+
console.error(` ${kind}: ${n}`);
|
|
631
|
+
for (const d of drift.slice(0, 20))
|
|
632
|
+
console.error(` - ${d.kind}: ${d.item_type} ${d.item_id}`);
|
|
633
|
+
if (drift.length > 20)
|
|
634
|
+
console.error(` … and ${drift.length - 20} more`);
|
|
635
|
+
console.error(' → Gate NOT met: the journal does not yet reproduce projections. Do NOT cut over.');
|
|
636
|
+
}
|
|
637
|
+
if (drift.length > 0)
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
585
640
|
export function runDoctor(options = {}) {
|
|
641
|
+
if (options.verifyJournal) {
|
|
642
|
+
runJournalVerification(options);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
586
645
|
if (options.dispatch) {
|
|
587
646
|
const report = runDispatchHealthCheck(options);
|
|
588
647
|
if (options.json) {
|
|
@@ -945,6 +1004,26 @@ export function runDoctor(options = {}) {
|
|
|
945
1004
|
}
|
|
946
1005
|
}
|
|
947
1006
|
catch { /* non-fatal */ }
|
|
1007
|
+
// pln#562 step 2 — surface debris identities (test fixtures, alias leftovers).
|
|
1008
|
+
// Read-only: removal goes through the guarded `register-agent <name> --remove`.
|
|
1009
|
+
try {
|
|
1010
|
+
const debris = listDebrisAgentIdentities(options.cwd);
|
|
1011
|
+
if (debris.length > 0) {
|
|
1012
|
+
const names = debris.map((d) => d.identity.agent_name).join(', ');
|
|
1013
|
+
checks.push({
|
|
1014
|
+
name: 'debris_agent_identities',
|
|
1015
|
+
status: 'warn',
|
|
1016
|
+
message: `${debris.length} debris agent identit${debris.length === 1 ? 'y' : 'ies'} registered (${names}). Remove with \`brainclaw register-agent <name> --remove\`.`,
|
|
1017
|
+
});
|
|
1018
|
+
if (!options.json) {
|
|
1019
|
+
console.warn(`⚠ Debris agent identities: ${names} — remove with \`brainclaw register-agent <name> --remove\`.`);
|
|
1020
|
+
for (const d of debris) {
|
|
1021
|
+
console.warn(` - ${d.identity.agent_name} (${d.identity.agent_id}): ${d.reason}`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
catch { /* non-fatal */ }
|
|
948
1027
|
const agentTooling = buildAgentToolingContext({ cwd: options.cwd });
|
|
949
1028
|
if (agentTooling.agents_md_present && agentTooling.agents_rules.length === 0) {
|
|
950
1029
|
checks.push({
|
|
@@ -1533,7 +1612,13 @@ export function runDoctor(options = {}) {
|
|
|
1533
1612
|
try {
|
|
1534
1613
|
const pendingCandidatesForStaleness = listCandidates('pending', options.cwd);
|
|
1535
1614
|
const runtimeNotesForStaleness = listRuntimeNotes(undefined, options.cwd);
|
|
1536
|
-
const staleReport = detectStaleness(state.plan_items, state.known_traps, state.open_handoffs, pendingCandidatesForStaleness, Date.now(), runtimeNotesForStaleness
|
|
1615
|
+
const staleReport = detectStaleness(state.plan_items, state.known_traps, state.open_handoffs, pendingCandidatesForStaleness, Date.now(), runtimeNotesForStaleness,
|
|
1616
|
+
// pln#557 step 2 — flag entities whose related_paths no longer exist.
|
|
1617
|
+
{
|
|
1618
|
+
decisions: state.recent_decisions,
|
|
1619
|
+
constraints: state.active_constraints,
|
|
1620
|
+
projectRoot: options.cwd ?? process.cwd(),
|
|
1621
|
+
});
|
|
1537
1622
|
if (staleReport.warnings.length > 0) {
|
|
1538
1623
|
const summary = staleSummary(staleReport);
|
|
1539
1624
|
checks.push({
|
|
@@ -1736,6 +1821,32 @@ export function runDoctor(options = {}) {
|
|
|
1736
1821
|
console.log(`Runtime notes: ${notes.length} total`);
|
|
1737
1822
|
console.log(`Local traps: ${localTraps.length} visible on this host`);
|
|
1738
1823
|
}
|
|
1824
|
+
// can_b8d53d18 — runtime notes created with the legacy `run_` prefix collide
|
|
1825
|
+
// with agent_run ids in prefix-based routing (dispatch_status). Surface them
|
|
1826
|
+
// and offer the soft id migration via `brainclaw repair`.
|
|
1827
|
+
const legacyPrefixNotes = listRuntimeNotes({ visibility: 'all', includeAllHosts: true }, options.cwd)
|
|
1828
|
+
.filter((n) => n.id.startsWith('run_'));
|
|
1829
|
+
if (legacyPrefixNotes.length > 0) {
|
|
1830
|
+
checks.push({
|
|
1831
|
+
name: 'runtime_note_id_prefix',
|
|
1832
|
+
status: 'warn',
|
|
1833
|
+
message: `${legacyPrefixNotes.length} runtime note(s) carry the legacy run_ id prefix (collides with agent_run ids)`,
|
|
1834
|
+
details: legacyPrefixNotes.map((n) => n.id),
|
|
1835
|
+
});
|
|
1836
|
+
repairCandidates.push({
|
|
1837
|
+
action: 'migrate_runtime_note_ids',
|
|
1838
|
+
target: 'coordination/runtime',
|
|
1839
|
+
description: `Rename ${legacyPrefixNotes.length} runtime note id(s) from run_* to rtn_* (file rename + id rewrite, lossless)`,
|
|
1840
|
+
safe: true,
|
|
1841
|
+
related_check: 'runtime_note_id_prefix',
|
|
1842
|
+
});
|
|
1843
|
+
if (!options.json) {
|
|
1844
|
+
console.warn(`⚠ ${legacyPrefixNotes.length} runtime note(s) use the legacy run_ id prefix — run \`brainclaw repair\` to migrate them to rtn_.`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
else {
|
|
1848
|
+
checks.push({ name: 'runtime_note_id_prefix', status: 'ok', message: 'No runtime notes with legacy run_ id prefix' });
|
|
1849
|
+
}
|
|
1739
1850
|
const marker = readContextMarker(options.cwd);
|
|
1740
1851
|
const visibleMemoryVersion = getVisibleMemoryVersion({ cwd: options.cwd });
|
|
1741
1852
|
if (marker?.memory_version && marker.memory_version !== visibleMemoryVersion) {
|