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
@@ -6,8 +6,53 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import { loadState, persistState } from '../state.js';
9
+ import path from 'node:path';
10
+ import { loadState, mutateState } from '../state.js';
10
11
  import { resolveStoreChain, resolveTargetStore } from '../store-resolution.js';
12
+ function bucketFor(state, itemType) {
13
+ if (itemType === 'constraint')
14
+ return state.active_constraints;
15
+ if (itemType === 'decision')
16
+ return state.recent_decisions;
17
+ return state.known_traps;
18
+ }
19
+ function findInState(state, itemId, itemType) {
20
+ return bucketFor(state, itemType).find((item) => item.id === itemId || item.short_label === itemId);
21
+ }
22
+ function replaceInState(state, itemId, itemType, item) {
23
+ const bucket = bucketFor(state, itemType);
24
+ const idx = bucket.findIndex((entry) => entry.id === itemId || entry.short_label === itemId);
25
+ if (idx < 0)
26
+ return false;
27
+ bucket[idx] = item;
28
+ return true;
29
+ }
30
+ function removeFromState(state, itemId, itemType) {
31
+ const bucket = bucketFor(state, itemType);
32
+ const before = bucket.length;
33
+ const filtered = bucket.filter((item) => item.id !== itemId && item.short_label !== itemId);
34
+ if (itemType === 'constraint')
35
+ state.active_constraints = filtered;
36
+ if (itemType === 'decision')
37
+ state.recent_decisions = filtered;
38
+ if (itemType === 'trap')
39
+ state.known_traps = filtered;
40
+ return filtered.length !== before;
41
+ }
42
+ function applyMemoryPatch(item, input) {
43
+ const next = { ...item };
44
+ if (input.text)
45
+ next.text = input.text;
46
+ if (input.tags)
47
+ next.tags = input.tags;
48
+ if (input.status && input.type === 'trap') {
49
+ next.status = input.status;
50
+ }
51
+ if (input.patch) {
52
+ Object.assign(next, input.patch);
53
+ }
54
+ return next;
55
+ }
11
56
  /**
12
57
  * Walk the store chain to find a memory item by type and id.
13
58
  * Searches by both id and short_label.
@@ -37,17 +82,11 @@ export function deleteMemoryItem(itemId, itemType, cwd) {
37
82
  if (!found) {
38
83
  throw new Error(`${itemType} with id '${itemId}' not found in any store`);
39
84
  }
40
- const state = loadState(found.store.cwd);
41
- if (itemType === 'constraint') {
42
- state.active_constraints = state.active_constraints.filter((c) => c.id !== itemId && c.short_label !== itemId);
43
- }
44
- else if (itemType === 'decision') {
45
- state.recent_decisions = state.recent_decisions.filter((d) => d.id !== itemId && d.short_label !== itemId);
46
- }
47
- else if (itemType === 'trap') {
48
- state.known_traps = state.known_traps.filter((t) => t.id !== itemId && t.short_label !== itemId);
49
- }
50
- persistState(state, found.store.cwd);
85
+ mutateState((state) => {
86
+ if (!removeFromState(state, itemId, itemType)) {
87
+ throw new Error(`${itemType} with id '${itemId}' not found in ${found.store.role} store`);
88
+ }
89
+ }, found.store.cwd);
51
90
  return {
52
91
  deletedId: itemId,
53
92
  itemType,
@@ -59,65 +98,51 @@ export function updateMemoryItem(input, cwd) {
59
98
  if (!found) {
60
99
  throw new Error(`${input.type} with id '${input.id}' not found in any store`);
61
100
  }
62
- const { item, store: sourceStore } = found;
101
+ const { store: sourceStore } = found;
63
102
  const previousStore = sourceStore.role;
64
- // Apply field updates
65
- if (input.text)
66
- item.text = input.text;
67
- if (input.tags)
68
- item.tags = input.tags;
69
- if (input.status && input.type === 'trap') {
70
- item.status = input.status;
71
- }
72
- if (input.patch) {
73
- Object.assign(item, input.patch);
74
- }
75
103
  if (input.moveToStore) {
76
104
  const targetCwd = resolveTargetStore(cwd, input.moveToStore);
77
- // Delete from source
78
- const sourceState = loadState(sourceStore.cwd);
79
- if (input.type === 'constraint') {
80
- sourceState.active_constraints = sourceState.active_constraints.filter((c) => c.id !== input.id);
81
- }
82
- else if (input.type === 'decision') {
83
- sourceState.recent_decisions = sourceState.recent_decisions.filter((d) => d.id !== input.id);
105
+ if (path.resolve(targetCwd) === path.resolve(sourceStore.cwd)) {
106
+ mutateState((state) => {
107
+ const current = findInState(state, input.id, input.type);
108
+ if (!current) {
109
+ throw new Error(`${input.type} with id '${input.id}' not found in ${sourceStore.role} store`);
110
+ }
111
+ replaceInState(state, input.id, input.type, applyMemoryPatch(current, input));
112
+ }, sourceStore.cwd);
113
+ return {
114
+ updatedId: input.id,
115
+ itemType: input.type,
116
+ previousStore,
117
+ newStore: input.moveToStore,
118
+ };
84
119
  }
85
- else if (input.type === 'trap') {
86
- sourceState.known_traps = sourceState.known_traps.filter((t) => t.id !== input.id);
87
- }
88
- persistState(sourceState, sourceStore.cwd);
89
- // Add to target
90
- const targetState = loadState(targetCwd);
91
- if (input.type === 'constraint') {
92
- targetState.active_constraints.push(item);
93
- }
94
- else if (input.type === 'decision') {
95
- targetState.recent_decisions.push(item);
96
- }
97
- else if (input.type === 'trap') {
98
- targetState.known_traps.push(item);
99
- }
100
- persistState(targetState, targetCwd);
120
+ const movedItem = mutateState((state) => {
121
+ const current = findInState(state, input.id, input.type);
122
+ if (!current) {
123
+ throw new Error(`${input.type} with id '${input.id}' not found in ${sourceStore.role} store`);
124
+ }
125
+ return applyMemoryPatch(current, input);
126
+ }, sourceStore.cwd);
127
+ // Write target before deleting source. A failure can leave a duplicate, but
128
+ // not silent data loss.
129
+ mutateState((state) => {
130
+ if (!replaceInState(state, input.id, input.type, movedItem)) {
131
+ bucketFor(state, input.type).push(movedItem);
132
+ }
133
+ }, targetCwd);
134
+ mutateState((state) => {
135
+ removeFromState(state, input.id, input.type);
136
+ }, sourceStore.cwd);
101
137
  }
102
138
  else {
103
- // Update in place
104
- const state = loadState(sourceStore.cwd);
105
- if (input.type === 'constraint') {
106
- const idx = state.active_constraints.findIndex((c) => c.id === input.id);
107
- if (idx >= 0)
108
- state.active_constraints[idx] = item;
109
- }
110
- else if (input.type === 'decision') {
111
- const idx = state.recent_decisions.findIndex((d) => d.id === input.id);
112
- if (idx >= 0)
113
- state.recent_decisions[idx] = item;
114
- }
115
- else if (input.type === 'trap') {
116
- const idx = state.known_traps.findIndex((t) => t.id === input.id);
117
- if (idx >= 0)
118
- state.known_traps[idx] = item;
119
- }
120
- persistState(state, sourceStore.cwd);
139
+ mutateState((state) => {
140
+ const current = findInState(state, input.id, input.type);
141
+ if (!current) {
142
+ throw new Error(`${input.type} with id '${input.id}' not found in ${sourceStore.role} store`);
143
+ }
144
+ replaceInState(state, input.id, input.type, applyMemoryPatch(current, input));
145
+ }, sourceStore.cwd);
121
146
  }
122
147
  return {
123
148
  updatedId: input.id,
@@ -52,6 +52,10 @@ export function addStep(input, cwd) {
52
52
  assignee: input.assignee,
53
53
  created_at: nowISO(),
54
54
  updated_at: nowISO(),
55
+ // PlanStepSchema preprocesses estimated_effort (string→minutes); the cast
56
+ // defers coercion to the schema parse on persist, matching plan-level.
57
+ estimated_effort: input.estimatedEffort,
58
+ actual_effort: input.actualEffort,
55
59
  };
56
60
  plan.steps = [...(plan.steps ?? []), step];
57
61
  plan.updated_at = nowISO();
@@ -106,7 +110,12 @@ export function completeStep(input, cwd) {
106
110
  throw new Error(`Step '${input.stepId}' not found in plan '${input.planId}'.`);
107
111
  }
108
112
  const timestamp = nowISO();
113
+ const previousStatus = step.status;
109
114
  step.status = 'done';
115
+ if (!step.started_at)
116
+ step.started_at = timestamp;
117
+ if (previousStatus !== 'done' || !step.completed_at)
118
+ step.completed_at = timestamp; // pln#495: stamp completion for per-step duration
110
119
  step.updated_at = timestamp;
111
120
  plan.updated_at = timestamp;
112
121
  const totalSteps = plan.steps.length;
@@ -135,12 +144,29 @@ export function updateStep(input, cwd) {
135
144
  throw new Error(`Step '${input.stepId}' not found in plan '${input.planId}'.`);
136
145
  }
137
146
  const timestamp = nowISO();
138
- if (input.status)
147
+ if (input.status) {
148
+ const previousStatus = step.status;
139
149
  step.status = input.status;
150
+ // pln#495: stamp the step lifecycle so the report can sum per-step
151
+ // durations (started→completed) and exclude inter-step idle gaps.
152
+ if (input.status !== 'todo' && (!step.started_at || previousStatus === 'done'))
153
+ step.started_at = timestamp;
154
+ if (input.status === 'done') {
155
+ if (previousStatus !== 'done' || !step.completed_at)
156
+ step.completed_at = timestamp;
157
+ }
158
+ else if (previousStatus === 'done') {
159
+ step.completed_at = undefined;
160
+ }
161
+ }
140
162
  if (input.text !== undefined)
141
163
  step.text = input.text;
142
164
  if (input.assignee !== undefined)
143
165
  step.assignee = input.assignee || undefined;
166
+ if (input.estimatedEffort !== undefined)
167
+ step.estimated_effort = input.estimatedEffort;
168
+ if (input.actualEffort !== undefined)
169
+ step.actual_effort = input.actualEffort;
144
170
  step.updated_at = timestamp;
145
171
  plan.updated_at = timestamp;
146
172
  const totalSteps = plan.steps.length;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Protocol-skills pack (pln#519) — workflow-decomposed agent skills.
3
+ *
4
+ * Three SKILL.md files that package brainclaw's critical workflows so an agent
5
+ * loads THE right protocol at the right moment, instead of skimming a
6
+ * monolithic AGENTS.md. Orthogonal to the agent-PROFILE skills
7
+ * (openclaw/nano-/nemo-/pico-/zeroclaw): the same agent can load both. Marked
8
+ * `metadata.protocol: true` so skill-loader UIs can list protocols separately.
9
+ *
10
+ * Design: .brainclaw/coordination/briefs/pln519-protocol-skills-design.md.
11
+ * Content lives here as the single source of truth and is EMBEDDED (not read
12
+ * from a repo file) so it ships and installs identically whether brainclaw runs
13
+ * from source or an npm install — matching ensureUniversalBrainclawSkill.
14
+ *
15
+ * Guard-rails (design §E): skills carry NO dynamic state (claim/loop/plan ids
16
+ * are always live lookups, never literals), reference facade verbs by name only
17
+ * (never re-implement mcp.ts logic), show BOTH the MCP call and the CLI
18
+ * fallback (cold-start / worktree-without-.brainclaw cases), and stay capped at
19
+ * 3 for this version.
20
+ */
21
+ const SESSION_BODY = `# brainclaw-session
22
+
23
+ Open / resume / close a working session on a brainclaw project. Prefer the MCP
24
+ facade; the \`brainclaw …\` CLI is the fallback when the MCP server is not
25
+ reachable (cold start, or a worktree without \`.brainclaw/\`).
26
+
27
+ ## When to use
28
+
29
+ - Starting a NEW session on a brainclaw project — any first call in the session, or after a context compaction.
30
+ - RESUMING outstanding work — \`BRAINCLAW_CLAIM_ID\` is set, or the operator says "continue X".
31
+ - About to EDIT a specific scope (file / dir / feature) that should be reserved against other agents.
32
+
33
+ ## Workflow
34
+
35
+ 1. \`bclaw_work(intent='consult')\` — loads project memory and reports active claims. Read \`bootstrap_recommended\`: if true the project has no usable PROJECT.md (see \`brainclaw-multi-agent\` → bootstrap loop).
36
+ 2. If you will edit a scope, claim it: \`bclaw_work(intent='execute', scope='<path-or-feature>')\`. The response's \`claim_id\` is yours; \`claim_status='created'\` = new, \`'existing'\` = resumed.
37
+ 3. Do the work. Honor the \`warnings\` array (claim conflicts, sensitive paths, high-severity traps on your scope).
38
+ 4. When done (committed, tested), \`bclaw_session_end(autoRelease: true)\` — closes the session record and releases your remaining claims. \`autoRelease\` defaults to false; pass it explicitly or claims survive the session.
39
+
40
+ CLI fallback: \`brainclaw context --json\` · \`brainclaw claim create "<desc>" --scope <path>\` · \`brainclaw session-end --auto-release\`.
41
+
42
+ ## Anti-rationalizations
43
+
44
+ - **"I'm just exploring, I'll skip session-end."** → A live claim outlives a crash. The next agent sees your stale claim and is blocked. Calling \`bclaw_session_end(autoRelease: true)\` is the zero-cost guarantee — without the flag the claim survives.
45
+ - **"I know the project, I don't need to consult."** → State changes between sessions (commits, new traps, new constraints). Consult is cheap and surfaces what you'd miss.
46
+ - **"I'll claim later once I know the exact scope."** → Claim-before-edit IS the contract; it is exactly what prevents races with parallel agents.
47
+
48
+ ## Red flags
49
+
50
+ - \`claim_conflicts\` reports another agent's claim on your target scope → STOP. Route through coordination (\`bclaw_coordinate intent='reroute'\` or ask the operator); never override silently.
51
+ - High-severity \`[trap]\`/\`[constraint]\` warning on your scope → read it before editing.
52
+ - \`bootstrap_recommended=true\` while you're about to make architectural calls → the project has no PROJECT.md; consider a bootstrap loop first.
53
+
54
+ ## Verification
55
+
56
+ - The \`claim_id\` returned stays stable across subsequent calls (session continuity).
57
+ - After session-end, \`bclaw_find(entity='claim', filter={status:'active'})\` shows no active claim of yours on the scope.
58
+
59
+ ## See also
60
+
61
+ - \`brainclaw-memory-capture\` — for decisions/traps captured DURING the session.
62
+ - \`brainclaw-multi-agent\` — for delegating part of the work.
63
+ `;
64
+ const MEMORY_CAPTURE_BODY = `# brainclaw-memory-capture
65
+
66
+ Capture project memory at the RIGHT granularity and type, so it is retrievable
67
+ later. The entity type is not cosmetic — it drives retrieval and surfacing.
68
+
69
+ ## When to use
70
+
71
+ - You made a **design call** future agents must respect → **decision**.
72
+ - You hit an **externally-imposed rule** you cannot relax alone → **constraint**.
73
+ - You found an **environment / process pitfall** another agent would also hit → **trap**.
74
+ - You finished a chunk of work to be consumed by another agent → **handoff**.
75
+ - A keep-worthy observation that is not a hard decision/constraint/trap → **runtime_note** (lowest signal).
76
+ - You are genuinely unsure of the type → **candidate** (carries a proposed \`type\`; a reviewer reclassifies).
77
+
78
+ ## Workflow
79
+
80
+ 1. Pick the type:
81
+
82
+ \`\`\`
83
+ Negotiated + recorded as the way forward? → decision
84
+ Imposed externally, cannot be relaxed by you? → constraint
85
+ About HOW the system/env/tools behave (not WHAT)? → trap
86
+ A "danger ahead" pointer for a future agent? → trap
87
+ Output of work to be consumed by another agent? → handoff
88
+ Unsure between decision/constraint/trap? → candidate (with type)
89
+ Else → runtime_note
90
+ \`\`\`
91
+
92
+ 2. Write via the canonical grammar: \`bclaw_create(entity='<type>', data={ text, author, ...optional })\`. Declare the classifying field yourself (caller assertion wins over keyword heuristics): for a \`decision\` set \`outcome\` (one of \`approved | rejected | deferred | pending\` — \`pending\` until ratified); for a \`trap\` set \`severity\` (\`low | medium | high\`, defaults to medium); for a \`constraint\` set \`category\`; for a \`candidate\` its proposed \`type\` (\`constraint | decision | trap | handoff\` — required). \`handoff\` is NOT created via bclaw_create — use \`brainclaw handoff "<text>"\` (CLI) or let \`bclaw_session_end(reflectHandoff: true)\` materialize one from commits. For free-form capture restricted to \`decision | trap | constraint | note\`, \`bclaw_quick_capture(text, type)\` is the shortcut.
93
+ 3. Verify it is re-readable: \`bclaw_get(entity='<type>', id='<id>')\` returns your content. If \`bclaw_create\` returned \`validation_error\` (e.g. \`outcome\` not in enum), fix the field and retry — the error message lists the accepted values.
94
+
95
+ CLI fallback: \`brainclaw memory create <type> "<text>"\` (decision | constraint | trap | handoff via top-level \`brainclaw handoff "<text>"\`).
96
+
97
+ ## Anti-rationalizations
98
+
99
+ - **"I'll write a runtime_note, I'm unsure of the type."** → Notes are the lowest-signal type (aggregated, not surfaced). If it's actionable, pick decision/constraint/trap — being wrong is fine, that's what candidates are for.
100
+ - **"I'll add it as a decision AND a runtime_note to be safe."** → Duplicates pollute search and retrieval. Pick ONE.
101
+ - **"I'll write the decision without an outcome."** → \`outcome\` is optional but enum-validated when set; an invalid value (e.g. \`proposed\`, \`accepted\`) is rejected with the accepted list (\`approved | rejected | deferred | pending\`). Set \`pending\` early if not yet ratified — it makes the lifecycle explicit and lets reviewers transition it cleanly.
102
+ - **"A 5-line trap for a 1-line problem."** → Traps are read under pressure. Severity + symptom + mitigation, scannable.
103
+
104
+ ## Red flags
105
+
106
+ - The content already exists under a different id → \`bclaw_find\` first; \`bclaw_update\` the existing one instead of duplicating.
107
+ - You're about to write 10+ items at once → pause; you're dumping context, not capturing signal. Consolidate.
108
+ - The "decision" was proposed by someone else and not yet accepted → it's a \`candidate\`, not a \`decision\`.
109
+
110
+ ## Verification
111
+
112
+ - \`bclaw_create\` returns an id (e.g. \`dec_…\`).
113
+ - \`bclaw_get(entity='<type>', id)\` returns identical content; \`bclaw_search\`/\`bclaw_find\` index it.
114
+ - Traps surface in \`bclaw_work\` warnings on the relevant scope.
115
+
116
+ ## See also
117
+
118
+ - \`brainclaw-session\` — capture happens DURING a session.
119
+ - \`brainclaw-multi-agent\` — capture review findings as decisions/traps.
120
+ `;
121
+ const MULTI_AGENT_BODY = `# brainclaw-multi-agent
122
+
123
+ Coordinate work across agents — delegate, request review, drive parallel
124
+ dispatch, run multi-turn loops. Use when the work exceeds one agent's
125
+ competence or when parallelism gains outweigh orchestration cost.
126
+
127
+ ## When to use
128
+
129
+ - A second pair of eyes before merging → **review loop**.
130
+ - An open-ended design needing multiple perspectives → **ideation loop** (or **bootstrap loop** for PROJECT.md genesis).
131
+ - N independent sub-tasks runnable in parallel → **dispatch**.
132
+ - You're inside a loop someone else opened and must drive your turn → **loop verbs**.
133
+
134
+ ## Workflow — pick the verb first
135
+
136
+ \`\`\`
137
+ Delegate a scope (with a claim)? → bclaw_coordinate(intent='assign')
138
+ Ask an agent for input, no claim? → bclaw_coordinate(intent='consult')
139
+ Review a commit / branch / candidate? → bclaw_coordinate(intent='review', open_loop=true)
140
+ Brainstorm with multiple agents? → bclaw_coordinate(intent='ideate')
141
+ Parallelize a sequence's lanes? → bclaw_dispatch(intent='execute')
142
+ Drive YOUR turn in an open loop? → bclaw_loop(intent='turn|complete_turn|advance|close')
143
+ \`\`\`
144
+
145
+ ### Review loop
146
+ 1. Commit your changes first (or pass \`allow_dirty=true\` only if the worker doesn't need the dirty files — it spawns from HEAD).
147
+ 2. \`bclaw_coordinate(intent='review', open_loop=true, review_mode='symmetric', targetAgents=['<agent>'], scope='<commit/branch/feature>', task='<what to review + acceptance bar>')\`.
148
+ 3. Verify the worker is ALIVE — \`bclaw_dispatch_status(target_id='<asgn_…>')\` (health verdict + recommended action), not the bare \`delivered_and_started\` return.
149
+ 4. When findings land, apply fixes / push back via another turn.
150
+ 5. \`bclaw_loop(intent='close', loop_id='<id>', status='completed', reason='<verdict>')\`; release the worker's claim.
151
+
152
+ ### Parallel dispatch
153
+ 1. A sequence with lane declarations (or a plan with lanes).
154
+ 2. \`bclaw_dispatch(intent='execute')\` fans the items across agents per their lanes. Before merging the lanes back, run \`brainclaw worktree check\` (pre-merge conflict detection).
155
+
156
+ ## Anti-rationalizations
157
+
158
+ - **"I'll call \`bclaw_loop(intent='open')\` to start the review."** → STOP. \`open\` creates the loop WITHOUT dispatching a turn; the reviewer never gets the work. Use \`bclaw_coordinate(intent='review', open_loop=true)\` — it opens AND dispatches.
159
+ - **"It returned \`delivered_and_started\`, so the worker is running."** → That only means the brief-ack sentinel was touched. Verify with \`bclaw_dispatch_status\`; spawns die silently.
160
+ - **"I'll dispatch with uncommitted changes, the worker will see them."** → It spawns from HEAD in a worktree; your dirty files are invisible. Commit, or pass \`allow_dirty=true\` consciously.
161
+
162
+ ## Red flags
163
+
164
+ - \`agent_run.status='running'\` but the worker pid is dead → silent spawn death; check the captured stderr, retry or reassign. (Do NOT trust a stale \`LANE-RESULT.json\` inherited by the worktree — verify its \`assignment_id\` matches.)
165
+ - Cross-project dispatch (\`project='<other>'\`) → auto-spawn is disabled by design; the target picks the brief up async via its own \`bclaw_work\`. Don't block waiting.
166
+ - A dispatched worker operates in a worktree that has no \`.brainclaw/\` → its MCP/CLI may be limited (trp#336); it falls back to file-based output. Expect a file deliverable, harvest it.
167
+
168
+ ## Verification
169
+
170
+ - Dispatch: \`bclaw_dispatch_status(target_id)\` returns \`healthy\` (pid alive, recent fs activity), or a terminal verdict with a recommended next action.
171
+ - Review loop: \`bclaw_loop(intent='get', loop_id)\` shows the reviewer slot \`done\` and the findings artifact attached.
172
+ - Bootstrap/ideate: at converge the loop is \`completed\` (and for bootstrap, PROJECT.md is materialized at the project root).
173
+
174
+ ## See also
175
+
176
+ - \`brainclaw-session\` — start a session BEFORE dispatching.
177
+ - \`brainclaw-memory-capture\` — capture review findings as typed memory.
178
+ - Docs: \`docs/concepts/loop-engine.md\`, \`docs/concepts/dispatch-lifecycle.md\`, \`docs/concepts/parallel-merge-protocol.md\`.
179
+ `;
180
+ /** The single source of truth for what ships in the protocol-skills pack. */
181
+ export const PROTOCOL_SKILLS = [
182
+ {
183
+ id: 'brainclaw-session',
184
+ description: 'Open / resume / close a working session on a brainclaw project. Use at session start, when resuming after a compaction, or before editing a scope that needs a claim.',
185
+ body: SESSION_BODY,
186
+ },
187
+ {
188
+ id: 'brainclaw-memory-capture',
189
+ description: 'Capture project memory (decision, constraint, trap, runtime_note, handoff, candidate) at the right type and granularity. Use after a design call, hitting a constraint, discovering a trap, or producing a handoff.',
190
+ body: MEMORY_CAPTURE_BODY,
191
+ },
192
+ {
193
+ id: 'brainclaw-multi-agent',
194
+ description: 'Coordinate across agents — delegate, request review, drive parallel dispatch, run multi-turn loops. Use when work exceeds one agent or parallelism beats orchestration cost.',
195
+ body: MULTI_AGENT_BODY,
196
+ },
197
+ ];
198
+ /** Render the full SKILL.md (frontmatter + body) for a protocol skill. */
199
+ export function renderProtocolSkill(skill, brainclawVersion) {
200
+ return `---
201
+ name: ${skill.id}
202
+ description: '${skill.description.replace(/'/g, "''")}'
203
+ metadata:
204
+ protocol: true
205
+ brainclaw_version: ${brainclawVersion}
206
+ ---
207
+
208
+ ${skill.body}`;
209
+ }
210
+ //# sourceMappingURL=protocol-skills.js.map
@@ -1,5 +1,5 @@
1
1
  import { loadState } from './state.js';
2
- import { detectNewItemContradictions, hasBlockingContradictions, summarizeContradictions } from './contradictions.js';
2
+ import { detectNewItemContradictions, summarizeContradictions } from './contradictions.js';
3
3
  export function evaluateReflectionSafety(input) {
4
4
  if (input.type === 'handoff') {
5
5
  return {};
@@ -8,14 +8,13 @@ export function evaluateReflectionSafety(input) {
8
8
  if (contradictions.length === 0) {
9
9
  return {};
10
10
  }
11
- const contradictionSummary = summarizeContradictions(contradictions);
12
- const promotionBlockedReason = input.automation && hasBlockingContradictions(contradictions)
13
- ? 'contradiction_detected'
14
- : undefined;
11
+ // Advisory only (pln#542, cnd_abe61d68 incident: 18 keyword false positives
12
+ // on a review summary blocked promotion). Contradictions ride along as
13
+ // metadata on the candidate for the human/curator to weigh — they never
14
+ // set promotion_blocked_reason anymore.
15
15
  return {
16
16
  contradictions_detected: contradictions,
17
- contradiction_summary: contradictionSummary,
18
- promotion_blocked_reason: promotionBlockedReason,
17
+ contradiction_summary: summarizeContradictions(contradictions),
19
18
  };
20
19
  }
21
20
  //# sourceMappingURL=reflection-safety.js.map
@@ -4,6 +4,7 @@ import { listClaims } from './claims.js';
4
4
  import { loadConfig } from './config.js';
5
5
  import { nowISO } from './ids.js';
6
6
  import { listRuntimeNotes } from './runtime.js';
7
+ import { loadState } from './state.js';
7
8
  function clampScore(value) {
8
9
  if (!Number.isFinite(value)) {
9
10
  return 0;
@@ -30,6 +31,12 @@ function createAccumulator(identity) {
30
31
  claims_created: 0,
31
32
  released_claims: 0,
32
33
  orphan_runtime_noise: 0,
34
+ memory_confirmations_authored: 0,
35
+ memory_infirmations_authored: 0,
36
+ memory_saved_me_reports: 0,
37
+ memory_misled_me_reports: 0,
38
+ memory_items_reinforced: 0,
39
+ memory_items_misled_others: 0,
33
40
  },
34
41
  };
35
42
  }
@@ -151,6 +158,55 @@ function trackRuntimeSignals(note, store, sinceMs, resolveIdentity) {
151
158
  stats.signals.plan_linked_activity += 1;
152
159
  }
153
160
  }
161
+ /**
162
+ * pln#544 — memory lifecycle reinforcement signals.
163
+ *
164
+ * For each decision / constraint / trap, walk the bounded confirmations[]
165
+ * log and attribute:
166
+ * - the event itself to the attesting agent (`by`/`by_id`), increasing
167
+ * their confirmations/infirmations/saved-me/misled-me counters.
168
+ * - reinforcement back to the item author (saved_me / misled_me) so that
169
+ * "memory that actually helped" rewards whoever wrote it.
170
+ *
171
+ * The 30-day reputation window applies — older confirmation events do not
172
+ * count, mirroring how stale candidate signals are excluded above.
173
+ */
174
+ function trackMemoryLifecycleSignals(item, store, sinceMs, resolveIdentity) {
175
+ const events = item.confirmations ?? [];
176
+ if (events.length === 0)
177
+ return;
178
+ const author = resolveIdentity(item.author_id ?? item.author);
179
+ for (const event of events) {
180
+ if (!withinWindow(event.at, sinceMs))
181
+ continue;
182
+ const attester = resolveIdentity(event.by_id ?? event.by);
183
+ if (attester) {
184
+ const stats = getAccumulator(store, attester);
185
+ if (event.kind === 'confirm')
186
+ stats.signals.memory_confirmations_authored += 1;
187
+ else if (event.kind === 'infirm')
188
+ stats.signals.memory_infirmations_authored += 1;
189
+ else if (event.kind === 'saved_me') {
190
+ stats.signals.memory_confirmations_authored += 1;
191
+ stats.signals.memory_saved_me_reports += 1;
192
+ }
193
+ else if (event.kind === 'misled_me') {
194
+ stats.signals.memory_infirmations_authored += 1;
195
+ stats.signals.memory_misled_me_reports += 1;
196
+ }
197
+ }
198
+ // Reinforcement back to the item author — only "explicitly reinforced" /
199
+ // "explicitly debunked" events flow there. Passive confirm/infirm are
200
+ // peer-review signals, not author signals.
201
+ if (author && attester && attester.key !== author.key) {
202
+ const ownerStats = getAccumulator(store, author);
203
+ if (event.kind === 'saved_me')
204
+ ownerStats.signals.memory_items_reinforced += 1;
205
+ else if (event.kind === 'misled_me')
206
+ ownerStats.signals.memory_items_misled_others += 1;
207
+ }
208
+ }
209
+ }
154
210
  function trackClaimSignals(claim, store, sinceMs, resolveIdentity) {
155
211
  if (!withinWindow(claim.created_at, sinceMs) && !withinWindow(claim.released_at, sinceMs)) {
156
212
  return;
@@ -177,14 +233,24 @@ function finalizeSnapshot(accumulator) {
177
233
  const boundedPromotions = Math.min(accumulator.signals.promoted_runtime_candidates, 4);
178
234
  const boundedPlanActivity = Math.min(accumulator.signals.plan_linked_activity, 4);
179
235
  const boundedNoise = Math.min(accumulator.signals.orphan_runtime_noise, 4);
236
+ // pln#544 — memory-lifecycle reinforcement caps so a single noisy attester
237
+ // can't dominate the score. Saved-me on items I authored is the strongest
238
+ // positive signal ("my memory actually saved another agent"); misled-me on
239
+ // my items is the symmetric penalty.
240
+ const boundedSavedMeAuthored = Math.min(accumulator.signals.memory_items_reinforced, 5);
241
+ const boundedMisledOthers = Math.min(accumulator.signals.memory_items_misled_others, 5);
242
+ const boundedMemoryReviewVolume = Math.min(accumulator.signals.memory_confirmations_authored + accumulator.signals.memory_infirmations_authored, 10);
180
243
  const contributionQuality = clampScore(35 * accumulator.signals.accepted_candidates
181
244
  + 20 * accumulator.signals.promoted_runtime_accepted
182
245
  + 10 * boundedUses
183
246
  + 4 * boundedStars
184
- - 12 * accumulator.signals.rejected_candidates_authored);
247
+ + 15 * boundedSavedMeAuthored
248
+ - 12 * accumulator.signals.rejected_candidates_authored
249
+ - 18 * boundedMisledOthers);
185
250
  const reviewReliability = clampScore(30 * accumulator.signals.accepted_reviews
186
251
  + 18 * accumulator.signals.rejected_reviews
187
- + 4 * accumulator.signals.reasoned_rejections);
252
+ + 4 * accumulator.signals.reasoned_rejections
253
+ + 6 * boundedMemoryReviewVolume);
188
254
  const continuityHygiene = clampScore(12 * boundedRuntimeNotes
189
255
  + 12 * boundedPromotions
190
256
  + 8 * boundedPlanActivity
@@ -243,6 +309,22 @@ export function buildReputationSnapshot(cwd) {
243
309
  for (const claim of listClaims(cwd)) {
244
310
  trackClaimSignals(claim, store, sinceMs, resolvers.resolve);
245
311
  }
312
+ // pln#544 — feed memory-lifecycle confirmations back into reputation:
313
+ // attesting agents earn confirmation_authored; items reinforced by
314
+ // 'saved_me' reward their author. Best-effort: never let a lifecycle
315
+ // signal failure block reputation rebuild.
316
+ try {
317
+ const state = loadState(cwd);
318
+ const memoryItems = [
319
+ ...state.recent_decisions,
320
+ ...state.active_constraints,
321
+ ...state.known_traps,
322
+ ];
323
+ for (const item of memoryItems) {
324
+ trackMemoryLifecycleSignals(item, store, sinceMs, resolvers.resolve);
325
+ }
326
+ }
327
+ catch { /* state may be unreadable in cold-start scenarios */ }
246
328
  const agents = [...store.values()]
247
329
  .map((entry) => finalizeSnapshot(entry))
248
330
  .sort((a, b) => {