brainclaw 1.8.0 → 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.
Files changed (140) hide show
  1. package/README.md +12 -11
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +138 -13
  4. package/dist/commands/add-step.js +1 -1
  5. package/dist/commands/bootstrap.js +2 -26
  6. package/dist/commands/check-security-mcp.js +50 -33
  7. package/dist/commands/check-security.js +86 -43
  8. package/dist/commands/claim.js +22 -21
  9. package/dist/commands/confirm.js +26 -0
  10. package/dist/commands/context-diff.js +1 -1
  11. package/dist/commands/dispatch-watch.js +142 -0
  12. package/dist/commands/doctor.js +113 -2
  13. package/dist/commands/estimation-report.js +115 -16
  14. package/dist/commands/harvest.js +285 -22
  15. package/dist/commands/init.js +123 -21
  16. package/dist/commands/loops-handlers.js +4 -0
  17. package/dist/commands/mcp-read-handlers.js +198 -29
  18. package/dist/commands/mcp.js +588 -92
  19. package/dist/commands/memory.js +21 -17
  20. package/dist/commands/migrate.js +81 -17
  21. package/dist/commands/prune.js +78 -4
  22. package/dist/commands/reflect.js +26 -20
  23. package/dist/commands/register-agent.js +57 -1
  24. package/dist/commands/repair.js +20 -0
  25. package/dist/commands/session-end.js +15 -6
  26. package/dist/commands/session-start.js +18 -1
  27. package/dist/commands/setup-security.js +39 -18
  28. package/dist/commands/setup.js +26 -27
  29. package/dist/commands/stale.js +16 -2
  30. package/dist/commands/uninstall.js +126 -34
  31. package/dist/commands/update-step.js +6 -0
  32. package/dist/commands/worktree.js +60 -0
  33. package/dist/core/actions.js +12 -3
  34. package/dist/core/agent-capability.js +11 -13
  35. package/dist/core/agent-files.js +844 -547
  36. package/dist/core/agent-integrations.js +0 -3
  37. package/dist/core/agent-inventory.js +67 -0
  38. package/dist/core/agent-registry.js +163 -29
  39. package/dist/core/agentrun-reconciler.js +33 -2
  40. package/dist/core/agentruns.js +7 -1
  41. package/dist/core/ai-agent-detection.js +31 -44
  42. package/dist/core/archival.js +15 -9
  43. package/dist/core/assignment-reconciler.js +56 -0
  44. package/dist/core/assignment-sweeper.js +127 -4
  45. package/dist/core/assignments.js +69 -11
  46. package/dist/core/bootstrap.js +233 -67
  47. package/dist/core/brainclaw-version.js +22 -0
  48. package/dist/core/candidates.js +21 -1
  49. package/dist/core/claims.js +313 -150
  50. package/dist/core/config.js +6 -1
  51. package/dist/core/context-diff.js +148 -20
  52. package/dist/core/context.js +129 -8
  53. package/dist/core/coordination.js +22 -3
  54. package/dist/core/dispatch-status.js +79 -5
  55. package/dist/core/dispatcher.js +64 -11
  56. package/dist/core/entity-operations.js +45 -24
  57. package/dist/core/entity-registry.js +31 -5
  58. package/dist/core/event-log.js +138 -21
  59. package/dist/core/events/checkpoint.js +258 -0
  60. package/dist/core/events/genesis.js +220 -0
  61. package/dist/core/events/journal.js +507 -0
  62. package/dist/core/events/materialize.js +126 -0
  63. package/dist/core/events/registry-post-image.js +110 -0
  64. package/dist/core/events/verify.js +109 -0
  65. package/dist/core/execution-adapters.js +23 -0
  66. package/dist/core/facade-schema.js +38 -0
  67. package/dist/core/gc-semantic.js +130 -5
  68. package/dist/core/handoff-snapshot.js +68 -0
  69. package/dist/core/ids.js +19 -8
  70. package/dist/core/instruction-templates.js +34 -115
  71. package/dist/core/io.js +39 -3
  72. package/dist/core/json-store.js +10 -1
  73. package/dist/core/lock.js +153 -28
  74. package/dist/core/loops/bootstrap-acquire.js +25 -1
  75. package/dist/core/loops/facade-schema.js +2 -0
  76. package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
  77. package/dist/core/loops/index.js +1 -0
  78. package/dist/core/loops/presets/bootstrap.js +7 -0
  79. package/dist/core/loops/store.js +17 -0
  80. package/dist/core/loops/verbs.js +24 -1
  81. package/dist/core/markdown.js +8 -76
  82. package/dist/core/mcp-command-resolution.js +245 -0
  83. package/dist/core/memory-compactor.js +5 -3
  84. package/dist/core/memory-lifecycle.js +282 -0
  85. package/dist/core/merge-risk.js +150 -0
  86. package/dist/core/messaging.js +8 -1
  87. package/dist/core/migration.js +11 -1
  88. package/dist/core/observer-mode.js +26 -0
  89. package/dist/core/operations/memory-mutation.js +90 -65
  90. package/dist/core/operations/plan.js +27 -1
  91. package/dist/core/protocol-skills.js +210 -0
  92. package/dist/core/reflection-safety.js +6 -7
  93. package/dist/core/reputation.js +84 -2
  94. package/dist/core/runtime-signals.js +71 -9
  95. package/dist/core/runtime.js +84 -1
  96. package/dist/core/schema.js +114 -0
  97. package/dist/core/security-detectors.js +125 -0
  98. package/dist/core/security-extract.js +189 -0
  99. package/dist/core/security-guard.js +107 -29
  100. package/dist/core/security-packages.js +121 -0
  101. package/dist/core/security-scoring.js +76 -9
  102. package/dist/core/security.js +34 -2
  103. package/dist/core/sequence.js +11 -2
  104. package/dist/core/setup-flow.js +141 -13
  105. package/dist/core/staleness.js +72 -1
  106. package/dist/core/state.js +250 -54
  107. package/dist/core/store-resolution.js +19 -5
  108. package/dist/core/worktree.js +72 -8
  109. package/dist/facts.js +8 -8
  110. package/dist/facts.json +7 -7
  111. package/docs/PROTOCOL.md +223 -0
  112. package/docs/cli.md +11 -10
  113. package/docs/concepts/coordinator-runbook.md +129 -0
  114. package/docs/concepts/event-log-store-critique-A.md +333 -0
  115. package/docs/concepts/event-log-store-critique-B.md +353 -0
  116. package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
  117. package/docs/concepts/event-log-store-proposal-A.md +365 -0
  118. package/docs/concepts/event-log-store-proposal-B.md +404 -0
  119. package/docs/concepts/event-log-store.md +928 -0
  120. package/docs/concepts/identity-model-proposal.md +371 -0
  121. package/docs/concepts/memory.md +5 -4
  122. package/docs/concepts/observer-protocol.md +361 -0
  123. package/docs/concepts/parallel-merge-protocol.md +71 -0
  124. package/docs/concepts/plans-and-claims.md +43 -0
  125. package/docs/concepts/skills.md +78 -0
  126. package/docs/concepts/workspace-bootstrapping.md +61 -0
  127. package/docs/integrations/agents.md +4 -4
  128. package/docs/integrations/cline.md +10 -11
  129. package/docs/integrations/codex.md +2 -2
  130. package/docs/integrations/continue.md +5 -5
  131. package/docs/integrations/copilot.md +14 -12
  132. package/docs/integrations/openclaw.md +7 -6
  133. package/docs/integrations/overview.md +7 -7
  134. package/docs/integrations/roo.md +3 -3
  135. package/docs/integrations/windsurf.md +6 -6
  136. package/docs/mcp-schema-changelog.md +29 -2
  137. package/docs/quickstart.md +48 -47
  138. package/docs/security.md +174 -15
  139. package/docs/storage.md +4 -2
  140. package/package.json +8 -6
@@ -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 packageNames = options.packages.split(',').map(p => p.trim()).filter(Boolean);
19
- if (packageNames.length === 0) {
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 cachedResults = [];
29
- for (const name of packageNames) {
30
- const [depname, version] = name.includes('@') && !name.startsWith('@')
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
- cachedResults.push({ query, scores: cached });
49
+ cachedScores.push(cached);
39
50
  }
40
51
  else {
41
- queries.push(query);
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
- const msg = err instanceof Error ? err.message : String(err);
59
- if (preinstall.fallback_on_error === 'block') {
60
- console.error(`Socket MCP error: ${msg} — blocking (fallback=block)`);
61
- process.exit(2);
62
- }
63
- if (preinstall.fallback_on_error === 'warn') {
64
- console.error(`Socket MCP error: ${msg} allowing with warning (fallback=warn)`);
65
- }
66
- // fallback=pass: silent continue
67
- if (cachedResults.length === 0) {
68
- process.exit(preinstall.fallback_on_error === 'warn' ? 1 : 0);
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
- // Combine cached + fetched scores
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 worst = worstDecision(verdicts);
102
+ const intrinsic = worstDecision(verdicts);
103
+ const effective = applyMode(intrinsic, effectiveMode);
79
104
  if (options.json) {
80
- console.log(JSON.stringify({ verdicts, decision: worst }, null, 2));
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
- // Exit codes: 0=pass, 1=warn, 2=block
86
- if (worst === 'block')
87
- process.exit(2);
88
- if (worst === 'warn')
89
- process.exit(1);
90
- process.exit(0);
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' ? '\u2705' : v.decision === 'warn' ? '\u26A0\uFE0F' : '\uD83D\uDED1';
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(` \u2192 ${r}`);
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
@@ -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
- mutate({ cwd: options.cwd }, () => {
78
- if (plan) {
79
- if (!plan.assignee) {
80
- plan.assignee = actor.agent;
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.status === 'todo') {
83
- plan.status = 'in_progress';
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
- plan.updated_at = nowISO();
86
- saveState(state, options.cwd);
87
- }
88
- saveClaim(claim, options.cwd);
89
- rebuildProjectMd(plan ? state : loadState(options.cwd), options.cwd);
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>, or run `brainclaw context` first to seed a marker.');
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
@@ -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) {