brainclaw 0.28.0 → 1.5.3

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 (198) hide show
  1. package/README.md +193 -170
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +683 -23
  4. package/dist/commands/accept.js +3 -0
  5. package/dist/commands/add-step.js +11 -26
  6. package/dist/commands/agent-board.js +70 -3
  7. package/dist/commands/audit.js +19 -0
  8. package/dist/commands/check-policy.js +54 -0
  9. package/dist/commands/check-security-mcp.js +145 -0
  10. package/dist/commands/check-security.js +106 -0
  11. package/dist/commands/claim-resource.js +1 -0
  12. package/dist/commands/codev.js +672 -0
  13. package/dist/commands/compact.js +74 -0
  14. package/dist/commands/complete-step.js +16 -26
  15. package/dist/commands/constraint.js +8 -20
  16. package/dist/commands/decision.js +9 -20
  17. package/dist/commands/delete-plan.js +10 -12
  18. package/dist/commands/delete-step.js +16 -0
  19. package/dist/commands/dispatch.js +163 -0
  20. package/dist/commands/doctor.js +1122 -49
  21. package/dist/commands/enable-agent.js +1 -0
  22. package/dist/commands/export.js +280 -22
  23. package/dist/commands/handoff.js +33 -0
  24. package/dist/commands/harvest.js +189 -0
  25. package/dist/commands/hooks.js +82 -25
  26. package/dist/commands/inbox.js +169 -0
  27. package/dist/commands/init.js +38 -31
  28. package/dist/commands/install-hooks.js +71 -44
  29. package/dist/commands/link.js +89 -0
  30. package/dist/commands/list-claims.js +48 -3
  31. package/dist/commands/list-plans.js +129 -25
  32. package/dist/commands/loops-handlers.js +409 -0
  33. package/dist/commands/mcp-read-handlers.js +1628 -0
  34. package/dist/commands/mcp-schemas.generated.js +74 -0
  35. package/dist/commands/mcp.js +4244 -1475
  36. package/dist/commands/plan-resource.js +64 -0
  37. package/dist/commands/plan.js +12 -26
  38. package/dist/commands/prune.js +37 -2
  39. package/dist/commands/reflect.js +20 -7
  40. package/dist/commands/release-claim.js +11 -6
  41. package/dist/commands/release-notes.js +170 -0
  42. package/dist/commands/repair.js +210 -0
  43. package/dist/commands/run-profile.js +57 -0
  44. package/dist/commands/sequence.js +113 -0
  45. package/dist/commands/session-end.js +423 -14
  46. package/dist/commands/session-start.js +214 -41
  47. package/dist/commands/setup-security.js +103 -0
  48. package/dist/commands/setup.js +42 -4
  49. package/dist/commands/stale.js +109 -0
  50. package/dist/commands/switch.js +131 -10
  51. package/dist/commands/trap.js +14 -31
  52. package/dist/commands/update-handoff.js +63 -4
  53. package/dist/commands/update-plan.js +21 -28
  54. package/dist/commands/update-step.js +37 -0
  55. package/dist/commands/upgrade.js +313 -6
  56. package/dist/commands/usage.js +102 -0
  57. package/dist/commands/version.js +20 -0
  58. package/dist/commands/who.js +124 -0
  59. package/dist/commands/worktree.js +105 -0
  60. package/dist/core/actions.js +315 -0
  61. package/dist/core/agent-capability.js +610 -17
  62. package/dist/core/agent-context.js +7 -1
  63. package/dist/core/agent-files.js +1169 -85
  64. package/dist/core/agent-integrations.js +160 -5
  65. package/dist/core/agent-inventory.js +2 -0
  66. package/dist/core/agent-profiles.js +93 -0
  67. package/dist/core/agent-registry.js +162 -30
  68. package/dist/core/agentrun-reconciler.js +345 -0
  69. package/dist/core/agentruns.js +424 -0
  70. package/dist/core/ai-agent-detection.js +31 -10
  71. package/dist/core/archival.js +77 -0
  72. package/dist/core/assignment-sweeper.js +82 -0
  73. package/dist/core/assignments.js +367 -0
  74. package/dist/core/audit.js +30 -0
  75. package/dist/core/bootstrap.js +61 -10
  76. package/dist/core/brainclaw-version.js +94 -2
  77. package/dist/core/candidates.js +93 -2
  78. package/dist/core/claims.js +419 -0
  79. package/dist/core/codev-metrics.js +77 -0
  80. package/dist/core/codev-personas.js +31 -0
  81. package/dist/core/codev-plan-gen.js +35 -0
  82. package/dist/core/codev-prompts.js +74 -0
  83. package/dist/core/codev-responses.js +62 -0
  84. package/dist/core/codev-rounds.js +218 -0
  85. package/dist/core/config.js +4 -0
  86. package/dist/core/context.js +454 -34
  87. package/dist/core/coordination.js +201 -6
  88. package/dist/core/cross-project.js +230 -16
  89. package/dist/core/default-profiles/doctor.yaml +11 -0
  90. package/dist/core/default-profiles/janitor.yaml +11 -0
  91. package/dist/core/default-profiles/onboarder.yaml +11 -0
  92. package/dist/core/default-profiles/reviewer.yaml +13 -0
  93. package/dist/core/dispatcher.js +1189 -0
  94. package/dist/core/duplicates.js +2 -2
  95. package/dist/core/entity-operations.js +450 -0
  96. package/dist/core/entity-registry.js +344 -0
  97. package/dist/core/event-log.js +1 -0
  98. package/dist/core/events.js +106 -2
  99. package/dist/core/execution-adapters.js +154 -0
  100. package/dist/core/execution-context.js +63 -0
  101. package/dist/core/execution-profile.js +270 -0
  102. package/dist/core/execution.js +255 -0
  103. package/dist/core/facade-schema.js +81 -0
  104. package/dist/core/federation-cloud.js +99 -0
  105. package/dist/core/federation-message.js +52 -0
  106. package/dist/core/federation-transport.js +65 -0
  107. package/dist/core/gc-semantic.js +482 -0
  108. package/dist/core/governance.js +247 -0
  109. package/dist/core/guards.js +19 -0
  110. package/dist/core/ideation.js +72 -0
  111. package/dist/core/identity.js +252 -28
  112. package/dist/core/ids.js +6 -0
  113. package/dist/core/input-validation.js +2 -2
  114. package/dist/core/instruction-templates.js +344 -136
  115. package/dist/core/io.js +90 -11
  116. package/dist/core/lock.js +6 -2
  117. package/dist/core/loops/brief-assembly.js +213 -0
  118. package/dist/core/loops/facade-schema.js +148 -0
  119. package/dist/core/loops/index.js +7 -0
  120. package/dist/core/loops/iteration-engine.js +139 -0
  121. package/dist/core/loops/lock.js +385 -0
  122. package/dist/core/loops/store.js +201 -0
  123. package/dist/core/loops/types.js +403 -0
  124. package/dist/core/loops/verbs.js +534 -0
  125. package/dist/core/markdown.js +15 -3
  126. package/dist/core/memory-compactor.js +432 -0
  127. package/dist/core/memory-git.js +152 -8
  128. package/dist/core/messaging.js +278 -0
  129. package/dist/core/migration.js +32 -1
  130. package/dist/core/mutation-pipeline.js +4 -2
  131. package/dist/core/operations/memory-mutation.js +129 -0
  132. package/dist/core/operations/memory-write.js +78 -0
  133. package/dist/core/operations/plan.js +190 -0
  134. package/dist/core/policy.js +169 -0
  135. package/dist/core/repo-analysis.js +67 -0
  136. package/dist/core/reputation.js +9 -3
  137. package/dist/core/schema.js +546 -21
  138. package/dist/core/search.js +21 -2
  139. package/dist/core/security-cache.js +71 -0
  140. package/dist/core/security-guard.js +152 -0
  141. package/dist/core/security-scoring.js +86 -0
  142. package/dist/core/sequence.js +130 -0
  143. package/dist/core/socket-client.js +113 -0
  144. package/dist/core/staleness.js +246 -0
  145. package/dist/core/state.js +98 -22
  146. package/dist/core/store-resolution.js +54 -12
  147. package/dist/core/toml-writer.js +76 -0
  148. package/dist/core/upgrades/backup.js +232 -0
  149. package/dist/core/upgrades/health-check.js +169 -0
  150. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  151. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  152. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  153. package/dist/core/upgrades/schema-version.js +97 -0
  154. package/dist/core/worktree.js +606 -0
  155. package/dist/facts.js +114 -0
  156. package/dist/facts.json +111 -0
  157. package/docs/architecture/project-refs.md +5 -1
  158. package/docs/cli.md +690 -43
  159. package/docs/concepts/ideation-loop.md +317 -0
  160. package/docs/concepts/loop-engine.md +456 -0
  161. package/docs/concepts/mcp-governance.md +268 -0
  162. package/docs/concepts/memory-staleness.md +122 -0
  163. package/docs/concepts/multi-agent-workflows.md +166 -0
  164. package/docs/concepts/plans-and-claims.md +31 -6
  165. package/docs/concepts/project-md-convention.md +35 -0
  166. package/docs/concepts/troubleshooting.md +220 -0
  167. package/docs/concepts/upgrade-cli.md +202 -0
  168. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  169. package/docs/context-format-changelog.md +2 -2
  170. package/docs/context-format.md +2 -2
  171. package/docs/index.md +68 -0
  172. package/docs/integrations/agents.md +15 -16
  173. package/docs/integrations/cline.md +88 -0
  174. package/docs/integrations/codex.md +75 -23
  175. package/docs/integrations/continue.md +60 -0
  176. package/docs/integrations/copilot.md +67 -9
  177. package/docs/integrations/kilocode.md +72 -0
  178. package/docs/integrations/mcp.md +304 -21
  179. package/docs/integrations/mistral-vibe.md +122 -0
  180. package/docs/integrations/opencode.md +84 -0
  181. package/docs/integrations/overview.md +23 -8
  182. package/docs/integrations/roo.md +74 -0
  183. package/docs/integrations/windsurf.md +83 -0
  184. package/docs/mcp-schema-changelog.md +191 -1
  185. package/docs/playbooks/integration/index.md +121 -0
  186. package/docs/playbooks/productivity/index.md +102 -0
  187. package/docs/playbooks/team/index.md +122 -0
  188. package/docs/product/agent-first-model.md +184 -0
  189. package/docs/product/entity-model-audit.md +462 -0
  190. package/docs/quickstart-existing-project.md +135 -0
  191. package/docs/quickstart.md +124 -37
  192. package/docs/release-maintenance.md +79 -0
  193. package/docs/review.md +2 -0
  194. package/docs/server-operations.md +118 -0
  195. package/package.json +20 -12
  196. package/dist/commands/claude-desktop-extension.js +0 -18
  197. package/dist/commands/diff.js +0 -99
  198. package/dist/core/claude-desktop-extension.js +0 -224
@@ -1,5 +1,11 @@
1
+ import { execSync } from 'node:child_process';
2
+ import fs from 'node:fs';
1
3
  import path from 'node:path';
4
+ import { readProjectVision } from './io.js';
5
+ import { loadActiveProject } from './active-project.js';
6
+ import { checkBrainclawInstallableUpdate, renderBrainclawInstallableUpdateNotice } from './brainclaw-version.js';
2
7
  import { loadConfig } from './config.js';
8
+ import { loadCurrentSession, loadAllSessions } from './identity.js';
3
9
  import { resolveCrossProjectLinks, loadCrossProjectState } from './cross-project.js';
4
10
  import { buildContextDiff } from './context-diff.js';
5
11
  import { resolveContextStoreCwd, resolveStoreChain } from './store-resolution.js';
@@ -12,11 +18,14 @@ import { resolveCurrentHostId } from './host.js';
12
18
  import { inferProjectFromTarget, loadInstructions, resolveInstructions } from './instructions.js';
13
19
  import { buildCurrentAgentResumeSummary, buildReputationRankingLookup } from './reputation.js';
14
20
  import { loadState } from './state.js';
21
+ import { readAuditLog } from './audit.js';
15
22
  import { listCandidates } from './candidates.js';
16
- import { listClaims, isClaimExpired } from './claims.js';
23
+ import { listClaims, isClaimExpired, assessClaimLiveness } from './claims.js';
24
+ import { listAssignments } from './assignments.js';
17
25
  import { listRuntimeNotes } from './runtime.js';
18
26
  import { isTrapActive, listOperationalTraps } from './traps.js';
19
27
  import { buildEstimationReport } from '../commands/estimation-report.js';
28
+ import { detectStaleness } from './staleness.js';
20
29
  export const CONTEXT_SCHEMA_VERSION = '1.2';
21
30
  export function buildContext(options = {}) {
22
31
  const requestedCwd = options.cwd ?? process.cwd();
@@ -25,18 +34,21 @@ export function buildContext(options = {}) {
25
34
  const config = loadConfig(contextCwd);
26
35
  // Resolve parent stores for multi-store merge (walk-up from cwd)
27
36
  const storeChain = resolveStoreChain(contextCwd);
28
- const profile = options.profile ?? config.profile ?? 'dev';
29
37
  const projectMode = config.project_mode ?? 'auto';
30
38
  const projectStrategy = config.projects?.strategy ?? 'manual';
31
39
  const currentHost = resolveCurrentHostId();
32
40
  const memoryVersion = getVisibleMemoryVersion({ cwd: contextCwd, hostId: options.host, allHosts: options.allHosts });
33
41
  const target = normalizeContextTarget(options.target, requestedCwd, contextCwd);
34
42
  const project = options.project?.trim() || inferProjectFromTarget(target, config);
35
- const agent = options.agent?.trim() || config.current_agent?.trim();
36
- const currentAgentIdentity = agent
37
- ? (options.agent?.trim() ? findAgentIdentityByName(agent, contextCwd) : resolveCurrentAgentIdentity(contextCwd))
38
- : undefined;
39
- const profileMaxItems = { compact: 6, copilot: 5, quick: 3 };
43
+ // Agent resolution: explicit param > resolveCurrentAgentIdentity (env + detection).
44
+ // config.current_agent is NOT used — it's a singleton global that cross-contaminates in multi-agent.
45
+ const currentAgentIdentity = options.agent?.trim()
46
+ ? findAgentIdentityByName(options.agent.trim(), contextCwd)
47
+ : resolveCurrentAgentIdentity(contextCwd);
48
+ const agent = options.agent?.trim() || currentAgentIdentity?.agent_name;
49
+ // Profile resolution: explicit param > agent default > config default > 'dev'
50
+ const profile = options.profile ?? currentAgentIdentity?.context_profile ?? config.profile ?? 'dev';
51
+ const profileMaxItems = { dense: 20, compact: 6, copilot: 5, quick: 3, briefing: 5, 'claude-desktop': 8 };
40
52
  const maxItems = options.maxItems ?? profileMaxItems[profile] ?? 8;
41
53
  const maxChars = options.maxChars && options.maxChars > 0 ? options.maxChars : undefined;
42
54
  // Instructions will be resolved after parent-store merge below (line ~460)
@@ -45,6 +57,8 @@ export function buildContext(options = {}) {
45
57
  compact: ['plan', 'constraint'],
46
58
  copilot: ['constraint', 'trap'],
47
59
  quick: ['constraint', 'plan'],
60
+ briefing: ['trap', 'constraint', 'decision'],
61
+ 'claude-desktop': ['plan', 'handoff', 'candidate'],
48
62
  };
49
63
  const allowedSections = profileSections[profile];
50
64
  const items = [];
@@ -80,6 +94,7 @@ export function buildContext(options = {}) {
80
94
  score: 0,
81
95
  reasons: [],
82
96
  extra: c.status,
97
+ plan_id: c.plan_id,
83
98
  provenance: {
84
99
  actor: c.author,
85
100
  actor_id: c.author_id,
@@ -99,6 +114,7 @@ export function buildContext(options = {}) {
99
114
  score: 0,
100
115
  reasons: [],
101
116
  extra: d.related_paths?.join(', '),
117
+ plan_id: d.plan_id,
102
118
  provenance: {
103
119
  actor: d.author,
104
120
  actor_id: d.author_id,
@@ -118,6 +134,7 @@ export function buildContext(options = {}) {
118
134
  score: 0,
119
135
  reasons: [],
120
136
  extra: `${t.severity}, visibility:${t.visibility ?? 'shared'}`,
137
+ plan_id: t.plan_id,
121
138
  provenance: {
122
139
  actor: t.author,
123
140
  actor_id: t.author_id,
@@ -341,9 +358,9 @@ export function buildContext(options = {}) {
341
358
  item.score += 5;
342
359
  item.reasons = uniqueReasons([...item.reasons, 'agent-layer: my assigned plan']);
343
360
  }
344
- // Layer 2: boost items authored by me (+2)
361
+ // Layer 2: boost items authored by me (+0.5)
345
362
  if (item.score >= 0 && item.provenance?.actor === agentName) {
346
- item.score += 2;
363
+ item.score += 0.5;
347
364
  item.reasons = uniqueReasons([...item.reasons, 'agent-layer: my authored item']);
348
365
  }
349
366
  // Reputation signal
@@ -378,28 +395,32 @@ export function buildContext(options = {}) {
378
395
  });
379
396
  const memoryDensity = classifyMemoryDensity(selected.length);
380
397
  const bootstrapEnabled = options.bootstrap !== false;
381
- let bootstrapAvailable = hasReusableBootstrapProfile(target, contextCwd);
398
+ const testMode = process.env.BRAINCLAW_TEST_MODE === '1';
399
+ let bootstrapAvailable = false;
382
400
  let derivedSignals;
383
- if (bootstrapEnabled && (options.refreshBootstrap || memoryDensity === 'low')) {
384
- const bootstrap = runBootstrapProfile({
385
- target,
386
- refresh: options.refreshBootstrap,
387
- cwd: contextCwd,
388
- });
389
- bootstrapAvailable = bootstrap.profile.seed_count > 0;
390
- if (memoryDensity === 'low') {
401
+ if (!testMode) {
402
+ bootstrapAvailable = hasReusableBootstrapProfile(target, contextCwd);
403
+ if (bootstrapEnabled && (options.refreshBootstrap || memoryDensity === 'low')) {
404
+ const bootstrap = runBootstrapProfile({
405
+ target,
406
+ refresh: options.refreshBootstrap,
407
+ cwd: contextCwd,
408
+ });
409
+ bootstrapAvailable = bootstrap.profile.seed_count > 0;
410
+ if (memoryDensity === 'low') {
411
+ const signals = selectDerivedSignals(target, 5, contextCwd);
412
+ if (signals.length > 0) {
413
+ derivedSignals = signals;
414
+ }
415
+ }
416
+ }
417
+ else if (bootstrapEnabled && bootstrapAvailable && memoryDensity === 'low') {
391
418
  const signals = selectDerivedSignals(target, 5, contextCwd);
392
419
  if (signals.length > 0) {
393
420
  derivedSignals = signals;
394
421
  }
395
422
  }
396
423
  }
397
- else if (bootstrapEnabled && bootstrapAvailable && memoryDensity === 'low') {
398
- const signals = selectDerivedSignals(target, 5, contextCwd);
399
- if (signals.length > 0) {
400
- derivedSignals = signals;
401
- }
402
- }
403
424
  const executionSensitive = isExecutionSensitiveTarget(target);
404
425
  const derivedUsesExecution = derivedSignals?.some((signal) => signal.source_kind === 'machine') ?? false;
405
426
  const derivedUsesTooling = derivedSignals?.some((signal) => signal.source_kind === 'skill' || signal.source_kind === 'mcp') ?? false;
@@ -421,17 +442,44 @@ export function buildContext(options = {}) {
421
442
  // Build open_work: active claims and in_progress plans owned by the current agent
422
443
  // Reuses myClaims computed in agent-layer scoring above
423
444
  let openWork;
445
+ const currentSession = loadCurrentSession(contextCwd);
424
446
  if (currentAgentIdentity || agent) {
425
447
  const claimPlanIds = new Set(myClaims.map((c) => c.plan_id).filter(Boolean));
448
+ const activeAssignments = listAssignments(contextCwd, { agent: agentName }).filter((assignment) => !['completed', 'failed', 'expired', 'rerouted'].includes(assignment.status));
426
449
  const inProgressPlans = state.plan_items.filter((p) => p.status === 'in_progress' &&
427
450
  (p.assignee === agentName || claimPlanIds.has(p.id)));
428
- if (myClaims.length > 0 || inProgressPlans.length > 0) {
451
+ if (myClaims.length > 0 || activeAssignments.length > 0 || inProgressPlans.length > 0) {
429
452
  openWork = {
430
- active_claims: myClaims.map(({ id, scope, description, created_at, plan_id, expires_at }) => ({ id, scope, description, created_at, plan_id, expires_at })),
453
+ active_claims: myClaims.map((c) => ({
454
+ id: c.id,
455
+ scope: c.scope,
456
+ description: c.description,
457
+ created_at: c.created_at,
458
+ plan_id: c.plan_id,
459
+ expires_at: c.expires_at,
460
+ liveness: assessClaimLiveness(c, { cwd: contextCwd }).status,
461
+ })),
462
+ active_assignments: activeAssignments.map(({ id, status, scope, description, plan_id, last_heartbeat_at }) => ({
463
+ id,
464
+ status,
465
+ scope,
466
+ description,
467
+ plan_id,
468
+ last_heartbeat_at,
469
+ })),
431
470
  in_progress_plans: inProgressPlans.map(({ id, text, assignee }) => ({ id, text, assignee })),
432
471
  };
433
472
  }
434
473
  }
474
+ const sessionMetrics = buildSessionMetrics({
475
+ cwd: contextCwd,
476
+ sessionId: currentSession?.session_id,
477
+ sessionStartedAt: currentSession?.started_at,
478
+ agentName: agentName,
479
+ agentId: currentAgentIdentity?.agent_id,
480
+ activeClaimsCount: openWork?.active_claims.length ?? 0,
481
+ runtimeNotes,
482
+ });
435
483
  // Cross-project items (subscriber links — read-only, always injected, bypass scoring)
436
484
  const crossProjectItems = [];
437
485
  for (const link of resolveCrossProjectLinks(contextCwd)) {
@@ -439,27 +487,50 @@ export function buildContext(options = {}) {
439
487
  continue;
440
488
  try {
441
489
  const linkedState = loadCrossProjectState(link.absolutePath);
490
+ for (const p of linkedState.plan_items.filter((plan) => plan.status !== 'done' && plan.status !== 'dropped')) {
491
+ crossProjectItems.push({
492
+ id: p.id, section: 'cross_project', text: `[plan] ${p.text}`,
493
+ tags: p.tags, score: 0, reasons: [], from_project: link.projectName,
494
+ extra: `${p.status}, ${p.priority}`,
495
+ });
496
+ }
442
497
  for (const d of linkedState.recent_decisions) {
443
498
  crossProjectItems.push({
444
499
  id: d.id, section: 'cross_project', text: d.text,
445
500
  tags: d.tags, score: 0, reasons: [], from_project: link.projectName,
501
+ plan_id: d.plan_id,
446
502
  });
447
503
  }
448
504
  for (const c of linkedState.active_constraints) {
449
505
  crossProjectItems.push({
450
506
  id: c.id, section: 'cross_project', text: c.text,
451
507
  tags: c.tags, score: 0, reasons: [], from_project: link.projectName,
508
+ plan_id: c.plan_id,
452
509
  });
453
510
  }
454
511
  for (const t of linkedState.known_traps.filter((trap) => isTrapActive(trap))) {
455
512
  crossProjectItems.push({
456
513
  id: t.id, section: 'cross_project', text: t.text,
457
514
  tags: t.tags, score: 0, reasons: [], from_project: link.projectName,
515
+ plan_id: t.plan_id,
458
516
  });
459
517
  }
460
518
  }
461
519
  catch { /* skip unavailable linked project */ }
462
520
  }
521
+ // Staleness detection: non-blocking, capped at 5 warnings to keep context lean.
522
+ // Phase 4 Sprint 1 Lane A step 3 (pln#390): runtime_note staleness now
523
+ // flows through the same surface.
524
+ let staleWarnings;
525
+ try {
526
+ const pendingCandidatesForStaleness = listCandidates('pending', contextCwd);
527
+ const runtimeNotesForStaleness = listRuntimeNotes(undefined, contextCwd);
528
+ const staleReport = detectStaleness(state.plan_items, state.known_traps, state.open_handoffs, pendingCandidatesForStaleness, Date.now(), runtimeNotesForStaleness);
529
+ if (staleReport.warnings.length > 0) {
530
+ staleWarnings = staleReport.warnings.slice(0, 5);
531
+ }
532
+ }
533
+ catch { /* non-fatal */ }
463
534
  const result = {
464
535
  context_schema: CONTEXT_SCHEMA_VERSION,
465
536
  profile,
@@ -468,6 +539,7 @@ export function buildContext(options = {}) {
468
539
  project_mode: projectMode,
469
540
  project_strategy: projectStrategy,
470
541
  current_host: currentHost,
542
+ project_vision: readProjectVision(contextCwd),
471
543
  host_filter: options.host,
472
544
  all_hosts: options.allHosts ?? false,
473
545
  memory_version: memoryVersion,
@@ -490,6 +562,7 @@ export function buildContext(options = {}) {
490
562
  resolved_instructions: resolvedInstructions,
491
563
  resume_summary: resumeSummary,
492
564
  open_work: openWork,
565
+ session_metrics: sessionMetrics,
493
566
  stores: storeChain.length > 1
494
567
  ? storeChain.map(({ cwd, depth, role }) => ({ cwd, depth, role }))
495
568
  : undefined,
@@ -502,9 +575,11 @@ export function buildContext(options = {}) {
502
575
  return undefined;
503
576
  }
504
577
  })(),
578
+ active_project: findActiveProjectInChain(contextCwd, storeChain),
505
579
  cross_project_items: crossProjectItems.length > 0 ? crossProjectItems : undefined,
506
580
  claim_conflicts: detectClaimConflicts(myClaims, otherActiveClaims),
507
581
  workflow_hints: buildWorkflowHints(myClaims, openWork, state.plan_items),
582
+ stale_warnings: staleWarnings,
508
583
  selected,
509
584
  };
510
585
  if (options.digest) {
@@ -516,7 +591,7 @@ export function renderContextMarkdown(result, explain = false) {
516
591
  const lines = [];
517
592
  lines.push(`# Agent Context (${result.profile})`);
518
593
  lines.push('');
519
- if (result.open_work && (result.open_work.active_claims.length > 0 || result.open_work.in_progress_plans.length > 0)) {
594
+ if (result.open_work && (result.open_work.active_claims.length > 0 || result.open_work.active_assignments.length > 0 || result.open_work.in_progress_plans.length > 0)) {
520
595
  lines.push('## ⚠ Your open work');
521
596
  lines.push('');
522
597
  if (result.open_work.active_claims.length > 0) {
@@ -526,10 +601,22 @@ export function renderContextMarkdown(result, explain = false) {
526
601
  const planRef = claim.plan_id ? ` [plan: ${claim.plan_id}]` : '';
527
602
  const expired = claim.expires_at && claim.expires_at < now ? ' ⚠ EXPIRED — run brainclaw prune' : '';
528
603
  const ttlInfo = claim.expires_at && !expired ? ` (expires ${claim.expires_at.slice(0, 16).replace('T', ' ')})` : '';
529
- lines.push(`- [${claim.id}] ${claim.description}${planRef}${ttlInfo}${expired}`);
604
+ const livenessTag = claim.liveness && claim.liveness !== 'live' && claim.liveness !== 'young'
605
+ ? ` [${claim.liveness.toUpperCase()}]`
606
+ : '';
607
+ lines.push(`- [${claim.id}] ${claim.description}${planRef}${ttlInfo}${livenessTag}${expired}`);
530
608
  lines.push(` scope: ${claim.scope}`);
531
609
  }
532
610
  }
611
+ if (result.open_work.active_assignments.length > 0) {
612
+ lines.push('Active assignments (runtime state):');
613
+ for (const assignment of result.open_work.active_assignments) {
614
+ const planRef = assignment.plan_id ? ` [plan: ${assignment.plan_id}]` : '';
615
+ const heartbeat = assignment.last_heartbeat_at ? ` [heartbeat: ${assignment.last_heartbeat_at.slice(0, 16).replace('T', ' ')}]` : '';
616
+ lines.push(`- [${assignment.id}] ${assignment.description}${planRef} (${assignment.status})${heartbeat}`);
617
+ lines.push(` scope: ${assignment.scope}`);
618
+ }
619
+ }
533
620
  if (result.open_work.in_progress_plans.length > 0) {
534
621
  lines.push('In-progress plan items (update status when done):');
535
622
  for (const plan of result.open_work.in_progress_plans) {
@@ -538,6 +625,17 @@ export function renderContextMarkdown(result, explain = false) {
538
625
  }
539
626
  lines.push('');
540
627
  }
628
+ if (result.session_metrics) {
629
+ lines.push('## Session metrics');
630
+ lines.push('');
631
+ lines.push(`- Active claims: ${result.session_metrics.active_claims}`);
632
+ lines.push(`- Edits since last Brainclaw write: ${result.session_metrics.edits_since_last_memory}`);
633
+ lines.push(`- Session duration: ${result.session_metrics.session_duration_minutes} min`);
634
+ if (result.session_metrics.last_brainclaw_write) {
635
+ lines.push(`- Last Brainclaw write: ${result.session_metrics.last_brainclaw_write}`);
636
+ }
637
+ lines.push('');
638
+ }
541
639
  if (result.estimation_calibration) {
542
640
  lines.push(`Estimation calibration: ${result.estimation_calibration}`);
543
641
  lines.push('');
@@ -549,8 +647,47 @@ export function renderContextMarkdown(result, explain = false) {
549
647
  if (result.agent_id && result.agent) {
550
648
  lines.push(`Agent ID: ${result.agent_id}`);
551
649
  }
650
+ if (result.project_vision) {
651
+ lines.push(`Project vision: ${result.project_vision.split('\n')[0]}`);
652
+ }
552
653
  lines.push(`Project mode: ${result.project_mode} (${result.project_strategy})`);
654
+ if (result.active_project) {
655
+ const ap = result.active_project;
656
+ const age = Math.floor((Date.now() - Date.parse(ap.switched_at)) / 3_600_000);
657
+ const sourceHint = ap.source === 'session' ? ', session-scoped' : ', global';
658
+ lines.push(`Active project: ${ap.name ?? ap.path} (switched ${age}h ago by ${ap.switched_by ?? 'unknown'}${sourceHint})`);
659
+ if (ap.source === 'global') {
660
+ lines.push(` ⚠ This is a global switch — all agents on this host see the same project. Use \`brainclaw switch <project>\` during a session for agent-scoped switching.`);
661
+ }
662
+ lines.push(` All commands target this project. Use \`brainclaw switch --clear\` to return to workspace root or \`brainclaw switch <project>\` to change.`);
663
+ }
553
664
  lines.push(`Current host: ${result.current_host}`);
665
+ // Show other active sessions
666
+ try {
667
+ const allSessions = loadAllSessions();
668
+ const ttlMs = 4 * 60 * 60 * 1000;
669
+ const now = Date.now();
670
+ const otherSessions = allSessions.filter(s => s.agent_id !== result.agent_id
671
+ && (now - Date.parse(s.last_seen_at)) <= ttlMs);
672
+ if (otherSessions.length > 0) {
673
+ const summaries = otherSessions.map(s => {
674
+ const proj = s.active_project?.name ?? s.active_project?.path;
675
+ return `${s.user ?? 'unknown'}/${s.agent}${proj ? ` on ${proj}` : ''}`;
676
+ });
677
+ lines.push(`Other active agents: ${summaries.join(', ')}`);
678
+ }
679
+ }
680
+ catch { /* ignore — sessions dir may not exist yet */ }
681
+ // Check for brainclaw update (lightweight local manifest read only)
682
+ try {
683
+ const config = loadConfig();
684
+ const updateCheck = checkBrainclawInstallableUpdate(config, process.cwd());
685
+ const notice = renderBrainclawInstallableUpdateNotice(updateCheck);
686
+ if (notice) {
687
+ lines.push(`⚠ ${notice}`);
688
+ }
689
+ }
690
+ catch { /* ignore — update check is best-effort */ }
554
691
  lines.push(`Memory version: ${result.memory_version}`);
555
692
  lines.push(`Memory density: ${result.memory_density}`);
556
693
  lines.push(`Bootstrap available: ${result.bootstrap_available ? 'yes' : 'no'}`);
@@ -661,8 +798,9 @@ export function renderContextMarkdown(result, explain = false) {
661
798
  for (const item of result.selected) {
662
799
  const tags = item.tags.length ? ` [${item.tags.join(', ')}]` : '';
663
800
  const extra = item.extra ? ` (${item.extra})` : '';
801
+ const planRef = item.plan_id ? ` [plan: ${item.plan_id}]` : '';
664
802
  const why = explain && item.reasons.length ? ` {why: ${item.reasons.join(', ')}}` : '';
665
- lines.push(`- [${item.id}] <${item.section}> ${item.text}${extra}${tags}${why}`);
803
+ lines.push(`- [${item.id}] <${item.section}> ${item.text}${extra}${planRef}${tags}${why}`);
666
804
  }
667
805
  if (result.derived_signals && result.derived_signals.length > 0) {
668
806
  lines.push('');
@@ -730,6 +868,12 @@ export function renderContextPromptTemplate(result, compact = false) {
730
868
  lines.push(`sd=${result.context_diff.since_session ?? ''}`);
731
869
  lines.push(`dc=${result.context_diff.counts.total}`);
732
870
  }
871
+ if (result.stale_warnings && result.stale_warnings.length > 0) {
872
+ lines.push(`sw=${result.stale_warnings.length}`);
873
+ for (const w of result.stale_warnings) {
874
+ lines.push(` - en=${w.entity} id=${w.id} ag=${w.age_days} tx="${w.reason}"`);
875
+ }
876
+ }
733
877
  if (result.resume_summary) {
734
878
  lines.push(`rt=${result.resume_summary.internal_trust}`);
735
879
  lines.push('rs:');
@@ -758,6 +902,14 @@ export function renderContextPromptTemplate(result, compact = false) {
758
902
  }
759
903
  lines.push(`project_mode: ${result.project_mode}`);
760
904
  lines.push(`project_strategy: ${result.project_strategy}`);
905
+ if (result.project_vision) {
906
+ lines.push(`project_vision: "${result.project_vision.split('\n')[0]}"`);
907
+ }
908
+ if (result.active_project) {
909
+ lines.push(`active_project: ${result.active_project.name ?? result.active_project.path}`);
910
+ lines.push(`active_project_switched: ${result.active_project.switched_at}`);
911
+ lines.push(`active_project_source: ${result.active_project.source ?? 'global'}`);
912
+ }
761
913
  lines.push(`current_host: ${result.current_host}`);
762
914
  lines.push(`memory_version: ${result.memory_version}`);
763
915
  lines.push(`memory_density: ${result.memory_density}`);
@@ -838,22 +990,42 @@ export function renderContextPromptTemplate(result, compact = false) {
838
990
  lines.push(` - ${item}`);
839
991
  }
840
992
  }
993
+ if (result.stale_warnings && result.stale_warnings.length > 0) {
994
+ lines.push('stale_warnings:');
995
+ for (const w of result.stale_warnings) {
996
+ lines.push(` - entity=${w.entity} id=${w.id} age_days=${w.age_days} reason="${w.reason}" action="${w.suggested_action}"`);
997
+ }
998
+ }
841
999
  if (result.target) {
842
1000
  lines.push(`target: ${result.target}`);
843
1001
  }
844
1002
  }
845
- if (result.open_work && (result.open_work.active_claims.length > 0 || result.open_work.in_progress_plans.length > 0)) {
1003
+ if (result.open_work && (result.open_work.active_claims.length > 0 || result.open_work.active_assignments.length > 0 || result.open_work.in_progress_plans.length > 0)) {
846
1004
  lines.push(compact ? 'ow:' : 'open_work:');
847
1005
  if (result.open_work.active_claims.length > 0) {
848
1006
  lines.push(compact ? ' claims:' : ' active_claims:');
849
1007
  for (const claim of result.open_work.active_claims) {
1008
+ const lv = claim.liveness ? (compact ? ` lv=${claim.liveness}` : ` liveness=${claim.liveness}`) : '';
850
1009
  if (compact) {
851
1010
  const planRef = claim.plan_id ? ` pl=${claim.plan_id}` : '';
852
- lines.push(` - id=${claim.id}${planRef} sc="${claim.scope}" tx="${claim.description}"`);
1011
+ lines.push(` - id=${claim.id}${planRef} sc="${claim.scope}" tx="${claim.description}"${lv}`);
853
1012
  }
854
1013
  else {
855
1014
  const planRef = claim.plan_id ? ` plan_id=${claim.plan_id}` : '';
856
- lines.push(` - id=${claim.id}${planRef} scope="${claim.scope}" description="${claim.description}"`);
1015
+ lines.push(` - id=${claim.id}${planRef} scope="${claim.scope}" description="${claim.description}"${lv}`);
1016
+ }
1017
+ }
1018
+ }
1019
+ if (result.open_work.active_assignments.length > 0) {
1020
+ lines.push(compact ? ' assignments:' : ' active_assignments:');
1021
+ for (const assignment of result.open_work.active_assignments) {
1022
+ if (compact) {
1023
+ const planRef = assignment.plan_id ? ` pl=${assignment.plan_id}` : '';
1024
+ lines.push(` - id=${assignment.id}${planRef} st=${assignment.status} sc="${assignment.scope}" tx="${assignment.description}"`);
1025
+ }
1026
+ else {
1027
+ const planRef = assignment.plan_id ? ` plan_id=${assignment.plan_id}` : '';
1028
+ lines.push(` - id=${assignment.id}${planRef} status=${assignment.status} scope="${assignment.scope}" description="${assignment.description}"`);
857
1029
  }
858
1030
  }
859
1031
  }
@@ -869,6 +1041,25 @@ export function renderContextPromptTemplate(result, compact = false) {
869
1041
  }
870
1042
  }
871
1043
  }
1044
+ if (result.session_metrics) {
1045
+ lines.push(compact ? 'sm:' : 'session_metrics:');
1046
+ if (compact) {
1047
+ lines.push(` ac=${result.session_metrics.active_claims}`);
1048
+ lines.push(` ed=${result.session_metrics.edits_since_last_memory}`);
1049
+ lines.push(` dur=${result.session_metrics.session_duration_minutes}`);
1050
+ if (result.session_metrics.last_brainclaw_write) {
1051
+ lines.push(` lbw=${result.session_metrics.last_brainclaw_write}`);
1052
+ }
1053
+ }
1054
+ else {
1055
+ lines.push(` active_claims: ${result.session_metrics.active_claims}`);
1056
+ lines.push(` edits_since_last_memory: ${result.session_metrics.edits_since_last_memory}`);
1057
+ lines.push(` session_duration_minutes: ${result.session_metrics.session_duration_minutes}`);
1058
+ if (result.session_metrics.last_brainclaw_write) {
1059
+ lines.push(` last_brainclaw_write: ${result.session_metrics.last_brainclaw_write}`);
1060
+ }
1061
+ }
1062
+ }
872
1063
  lines.push(compact ? 'ins:' : 'instructions:');
873
1064
  if (result.resolved_instructions.length === 0) {
874
1065
  lines.push(compact ? ' - n' : ' - none');
@@ -896,14 +1087,16 @@ export function renderContextPromptTemplate(result, compact = false) {
896
1087
  if (compact) {
897
1088
  const tags = item.tags.length ? ` tg=[${item.tags.join(',')}]` : '';
898
1089
  const extra = item.extra ? ` ex="${item.extra}"` : '';
1090
+ const planRef = item.plan_id ? ` pl=${item.plan_id}` : '';
899
1091
  const why = item.reasons.length ? ` why=[${item.reasons.join('|')}]` : '';
900
- lines.push(` - id=${item.id} tp=${item.section}${tags}${extra}${why} tx="${item.text}"`);
1092
+ lines.push(` - id=${item.id} tp=${item.section}${tags}${extra}${planRef}${why} tx="${item.text}"`);
901
1093
  }
902
1094
  else {
903
1095
  const tags = item.tags.length ? ` tags=[${item.tags.join(',')}]` : '';
904
1096
  const extra = item.extra ? ` extra="${item.extra}"` : '';
1097
+ const planRef = item.plan_id ? ` plan=${item.plan_id}` : '';
905
1098
  const why = item.reasons.length ? ` why=[${item.reasons.join(', ')}]` : '';
906
- lines.push(` - id=${item.id} type=${item.section}${tags}${extra}${why} text="${item.text}"`);
1099
+ lines.push(` - id=${item.id} type=${item.section}${tags}${extra}${planRef}${why} text="${item.text}"`);
907
1100
  }
908
1101
  }
909
1102
  }
@@ -934,6 +1127,119 @@ export function renderContextPromptTemplate(result, compact = false) {
934
1127
  lines.push('```');
935
1128
  return lines.join('\n');
936
1129
  }
1130
+ /**
1131
+ * Render context as an ultra-compact scope briefing (< 500 chars).
1132
+ * Designed for pre-action injection: only what matters for THIS scope, NOW.
1133
+ *
1134
+ * Output example:
1135
+ * scope: src/core/agent-files.ts
1136
+ * traps: [high] MCP server hérite env vars VS Code
1137
+ * claims: no conflict
1138
+ * decisions: brainclawMcpEntry preserves existing command
1139
+ * confidence: high (3 items, freshest 2d ago)
1140
+ */
1141
+ export function renderContextBriefing(result) {
1142
+ const lines = [];
1143
+ if (result.target) {
1144
+ lines.push(`scope: ${result.target}`);
1145
+ }
1146
+ // Traps (high severity first, max 2)
1147
+ const traps = result.selected
1148
+ .filter(i => i.section === 'trap')
1149
+ .slice(0, 2);
1150
+ if (traps.length > 0) {
1151
+ for (const t of traps) {
1152
+ const severity = t.extra?.match(/\b(high|medium|low)\b/)?.[0] ?? '';
1153
+ const text = t.text.length > 80 ? t.text.slice(0, 77) + '...' : t.text;
1154
+ lines.push(`trap: [${severity}] ${text}`);
1155
+ }
1156
+ }
1157
+ // Claim conflicts
1158
+ if (result.claim_conflicts && result.claim_conflicts.length > 0) {
1159
+ for (const c of result.claim_conflicts.slice(0, 2)) {
1160
+ lines.push(`conflict: ${c.other_agent} claims ${c.other_scope}`);
1161
+ }
1162
+ }
1163
+ else {
1164
+ lines.push('claims: no conflict');
1165
+ }
1166
+ // Recent decisions (max 1, truncated)
1167
+ const decisions = result.selected
1168
+ .filter(i => i.section === 'decision')
1169
+ .slice(0, 1);
1170
+ if (decisions.length > 0) {
1171
+ const text = decisions[0].text.length > 80 ? decisions[0].text.slice(0, 77) + '...' : decisions[0].text;
1172
+ lines.push(`decision: ${text}`);
1173
+ }
1174
+ // Confidence score based on item count and scope activity
1175
+ const totalItems = result.selected.length;
1176
+ const hasActivity = result.scoped_activity != null;
1177
+ let confidence = 'low';
1178
+ if (totalItems >= 3 && hasActivity) {
1179
+ confidence = 'high';
1180
+ }
1181
+ else if (totalItems >= 1) {
1182
+ confidence = 'medium';
1183
+ }
1184
+ lines.push(`confidence: ${confidence} (${totalItems} items${hasActivity ? ', scope has recent activity' : ''})`);
1185
+ // Sequence position placeholder (populated when Sequences land)
1186
+ // lines.push(`sequence: ...`);
1187
+ return lines.join('\n');
1188
+ }
1189
+ /**
1190
+ * Render context for Claude Desktop: surface tasks, inbox items, and active plans.
1191
+ * Excludes traps, constraints, and decisions — Claude Desktop sessions are task-oriented.
1192
+ * Target output: < 1500 chars, actionable items only.
1193
+ */
1194
+ export function renderContextClaudeDesktop(result) {
1195
+ const lines = [];
1196
+ lines.push('# Brainclaw — Claude Desktop Session');
1197
+ lines.push('');
1198
+ // Active plans (task queue)
1199
+ const plans = result.selected.filter(i => i.section === 'plan');
1200
+ if (plans.length > 0) {
1201
+ lines.push('## Tasks');
1202
+ for (const p of plans.slice(0, 5)) {
1203
+ const status = p.extra?.match(/\b(todo|in_progress|blocked)\b/)?.[0] ?? 'todo';
1204
+ const text = p.text.length > 100 ? p.text.slice(0, 97) + '...' : p.text;
1205
+ lines.push(`- [${status}] ${text} (${p.id})`);
1206
+ }
1207
+ lines.push('');
1208
+ }
1209
+ // Open handoffs (pending work from other agents)
1210
+ const handoffs = result.selected.filter(i => i.section === 'handoff');
1211
+ if (handoffs.length > 0) {
1212
+ lines.push('## Inbox');
1213
+ for (const h of handoffs.slice(0, 3)) {
1214
+ const from = h.provenance?.actor ?? 'unknown';
1215
+ const text = h.text.length > 100 ? h.text.slice(0, 97) + '...' : h.text;
1216
+ lines.push(`- from:${from} — ${text}`);
1217
+ }
1218
+ lines.push('');
1219
+ }
1220
+ // Candidates (review items)
1221
+ const candidates = result.selected.filter(i => i.section === 'candidate');
1222
+ if (candidates.length > 0) {
1223
+ lines.push('## Review');
1224
+ for (const c of candidates.slice(0, 3)) {
1225
+ const text = c.text.length > 100 ? c.text.slice(0, 97) + '...' : c.text;
1226
+ lines.push(`- ${text} (${c.id})`);
1227
+ }
1228
+ lines.push('');
1229
+ }
1230
+ if (plans.length === 0 && handoffs.length === 0 && candidates.length === 0) {
1231
+ lines.push('No pending tasks. Use `bclaw_quick_capture` to queue work for Claude Desktop.');
1232
+ }
1233
+ // Claim conflicts (always show if present)
1234
+ if (result.claim_conflicts && result.claim_conflicts.length > 0) {
1235
+ lines.push('## Conflicts');
1236
+ for (const c of result.claim_conflicts.slice(0, 2)) {
1237
+ lines.push(`- ${c.other_agent} claims ${c.other_scope}`);
1238
+ }
1239
+ lines.push('');
1240
+ }
1241
+ return lines.join('\n');
1242
+ }
937
1243
  export function buildScopedActivity(input) {
938
1244
  const target = input.target?.trim();
939
1245
  if (!target) {
@@ -1032,6 +1338,85 @@ function classifyMemoryDensity(selectedCount) {
1032
1338
  return 'medium';
1033
1339
  return 'high';
1034
1340
  }
1341
+ const BRAINCLAW_WRITE_ACTIONS = ['create', 'update', 'delete', 'accept', 'reject', 'trust_change', 'promote_direct', 'rollback'];
1342
+ function buildSessionMetrics(input) {
1343
+ if (!input.sessionId || !input.sessionStartedAt || !input.agentName) {
1344
+ return undefined;
1345
+ }
1346
+ const sessionId = input.sessionId;
1347
+ const startedAtMs = Date.parse(input.sessionStartedAt);
1348
+ if (!Number.isFinite(startedAtMs)) {
1349
+ return undefined;
1350
+ }
1351
+ const runtimeWrites = input.runtimeNotes
1352
+ .filter((note) => note.agent === input.agentName
1353
+ && note.session_id === sessionId
1354
+ && ((note.note_type ?? 'observation') === 'observation'))
1355
+ .map((note) => note.created_at);
1356
+ const auditWrites = readAuditLog({ since: input.sessionStartedAt, actor: input.agentId ?? input.agentName }, input.cwd)
1357
+ .filter((entry) => belongsToSession(entry, sessionId))
1358
+ .filter((entry) => BRAINCLAW_WRITE_ACTIONS.includes(entry.action))
1359
+ .map((entry) => entry.timestamp);
1360
+ const lastBrainclawWrite = [...runtimeWrites, ...auditWrites].sort().at(-1);
1361
+ const editsSinceLastMemory = countChangedFilesSince(input.cwd, lastBrainclawWrite ?? input.sessionStartedAt);
1362
+ return {
1363
+ active_claims: input.activeClaimsCount,
1364
+ edits_since_last_memory: editsSinceLastMemory,
1365
+ session_duration_minutes: Math.max(0, Math.floor((Date.now() - startedAtMs) / 60_000)),
1366
+ last_brainclaw_write: lastBrainclawWrite,
1367
+ };
1368
+ }
1369
+ function belongsToSession(entry, sessionId) {
1370
+ return !entry.session_id || entry.session_id === sessionId;
1371
+ }
1372
+ function countChangedFilesSince(cwd, since) {
1373
+ const sinceMs = Date.parse(since);
1374
+ if (!Number.isFinite(sinceMs)) {
1375
+ return 0;
1376
+ }
1377
+ try {
1378
+ const output = execSync('git status --porcelain --untracked-files=normal', {
1379
+ cwd,
1380
+ encoding: 'utf-8',
1381
+ stdio: ['ignore', 'pipe', 'ignore'],
1382
+ }).trim();
1383
+ if (!output) {
1384
+ return 0;
1385
+ }
1386
+ const changedPaths = new Set();
1387
+ for (const line of output.split(/\r?\n/)) {
1388
+ const rawPath = line.slice(3).trim();
1389
+ const resolvedPath = rawPath.includes(' -> ') ? rawPath.split(' -> ').pop() ?? rawPath : rawPath;
1390
+ if (resolvedPath && shouldCountEditedPath(resolvedPath)) {
1391
+ changedPaths.add(resolvedPath);
1392
+ }
1393
+ }
1394
+ let count = 0;
1395
+ for (const relativePath of changedPaths) {
1396
+ const absolutePath = path.join(cwd, relativePath);
1397
+ try {
1398
+ if (!fs.existsSync(absolutePath)) {
1399
+ count++;
1400
+ continue;
1401
+ }
1402
+ if (fs.statSync(absolutePath).mtimeMs >= sinceMs) {
1403
+ count++;
1404
+ }
1405
+ }
1406
+ catch {
1407
+ count++;
1408
+ }
1409
+ }
1410
+ return count;
1411
+ }
1412
+ catch {
1413
+ return 0;
1414
+ }
1415
+ }
1416
+ function shouldCountEditedPath(relativePath) {
1417
+ const normalized = relativePath.replace(/\\/g, '/');
1418
+ return !normalized.startsWith('.brainclaw/') && !normalized.startsWith('.git/');
1419
+ }
1035
1420
  function summariseAgentTooling(snapshot) {
1036
1421
  return {
1037
1422
  agents_md_present: snapshot.agents_md_present,
@@ -1181,6 +1566,13 @@ function computeRelevance(item, terms, profile, target) {
1181
1566
  score += 1;
1182
1567
  reasons.push('runtime execution signal');
1183
1568
  }
1569
+ if (profile === 'dense') {
1570
+ // Dense profile: boost all actionable sections equally for maximum coverage
1571
+ if (item.section === 'decision' || item.section === 'trap' || item.section === 'candidate' || item.section === 'runtime') {
1572
+ score += 2;
1573
+ reasons.push('profile boost: dense');
1574
+ }
1575
+ }
1184
1576
  if (profile === 'dev' && (item.section === 'decision' || item.section === 'trap')) {
1185
1577
  score += 2;
1186
1578
  reasons.push('profile boost: dev');
@@ -1303,6 +1695,34 @@ function scopesOverlap(a, b) {
1303
1695
  }
1304
1696
  return null;
1305
1697
  }
1698
+ // --- Active project resolution ---
1699
+ function findActiveProjectInChain(contextCwd, _storeChain) {
1700
+ // 1. Session-scoped active project (per-agent, highest priority)
1701
+ const session = loadCurrentSession(contextCwd);
1702
+ if (session?.active_project) {
1703
+ return {
1704
+ path: session.active_project.path,
1705
+ name: session.active_project.name,
1706
+ switched_at: session.active_project.switched_at,
1707
+ switched_by: session.agent,
1708
+ source: 'session',
1709
+ };
1710
+ }
1711
+ // 2. Global active-project.json (walk up from contextCwd)
1712
+ let dir = path.resolve(contextCwd);
1713
+ const root = path.parse(dir).root;
1714
+ const home = process.env.HOME || process.env.USERPROFILE || root;
1715
+ while (dir !== root && dir !== home) {
1716
+ const ap = loadActiveProject(dir);
1717
+ if (ap)
1718
+ return { ...ap, source: 'global' };
1719
+ const parent = path.dirname(dir);
1720
+ if (parent === dir)
1721
+ break;
1722
+ dir = parent;
1723
+ }
1724
+ return undefined;
1725
+ }
1306
1726
  // --- Workflow hints ---
1307
1727
  function buildWorkflowHints(myClaims, openWork, plans) {
1308
1728
  const hints = [];