brainclaw 0.29.2 → 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.
- package/README.md +193 -170
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +673 -24
- package/dist/commands/accept.js +3 -0
- package/dist/commands/add-step.js +11 -26
- package/dist/commands/agent-board.js +70 -3
- package/dist/commands/audit.js +19 -0
- package/dist/commands/check-policy.js +54 -0
- package/dist/commands/check-security-mcp.js +145 -0
- package/dist/commands/check-security.js +106 -0
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/codev.js +672 -0
- package/dist/commands/compact.js +74 -0
- package/dist/commands/complete-step.js +16 -26
- package/dist/commands/constraint.js +8 -20
- package/dist/commands/decision.js +9 -20
- package/dist/commands/delete-plan.js +10 -12
- package/dist/commands/delete-step.js +16 -0
- package/dist/commands/dispatch.js +163 -0
- package/dist/commands/doctor.js +1122 -49
- package/dist/commands/enable-agent.js +1 -0
- package/dist/commands/export.js +280 -22
- package/dist/commands/handoff.js +33 -0
- package/dist/commands/harvest.js +189 -0
- package/dist/commands/hooks.js +82 -25
- package/dist/commands/inbox.js +169 -0
- package/dist/commands/init.js +38 -31
- package/dist/commands/install-hooks.js +71 -44
- package/dist/commands/link.js +89 -0
- package/dist/commands/list-claims.js +48 -3
- package/dist/commands/list-plans.js +129 -25
- package/dist/commands/loops-handlers.js +409 -0
- package/dist/commands/mcp-read-handlers.js +1628 -0
- package/dist/commands/mcp-schemas.generated.js +74 -0
- package/dist/commands/mcp.js +4221 -1501
- package/dist/commands/plan-resource.js +64 -0
- package/dist/commands/plan.js +12 -26
- package/dist/commands/prune.js +37 -2
- package/dist/commands/reflect.js +20 -7
- package/dist/commands/release-claim.js +11 -6
- package/dist/commands/release-notes.js +170 -0
- package/dist/commands/repair.js +210 -0
- package/dist/commands/run-profile.js +57 -0
- package/dist/commands/sequence.js +113 -0
- package/dist/commands/session-end.js +423 -14
- package/dist/commands/session-start.js +214 -41
- package/dist/commands/setup-security.js +103 -0
- package/dist/commands/setup.js +42 -4
- package/dist/commands/stale.js +109 -0
- package/dist/commands/switch.js +100 -2
- package/dist/commands/trap.js +14 -31
- package/dist/commands/update-handoff.js +63 -4
- package/dist/commands/update-plan.js +21 -28
- package/dist/commands/update-step.js +37 -0
- package/dist/commands/upgrade.js +313 -6
- package/dist/commands/usage.js +102 -0
- package/dist/commands/version.js +20 -0
- package/dist/commands/who.js +33 -5
- package/dist/commands/worktree.js +105 -0
- package/dist/core/actions.js +315 -0
- package/dist/core/agent-capability.js +610 -17
- package/dist/core/agent-context.js +7 -1
- package/dist/core/agent-files.js +1169 -85
- package/dist/core/agent-integrations.js +160 -5
- package/dist/core/agent-inventory.js +2 -0
- package/dist/core/agent-profiles.js +93 -0
- package/dist/core/agent-registry.js +162 -30
- package/dist/core/agentrun-reconciler.js +345 -0
- package/dist/core/agentruns.js +424 -0
- package/dist/core/ai-agent-detection.js +31 -10
- package/dist/core/archival.js +77 -0
- package/dist/core/assignment-sweeper.js +82 -0
- package/dist/core/assignments.js +367 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/brainclaw-version.js +94 -2
- package/dist/core/candidates.js +93 -2
- package/dist/core/claims.js +419 -0
- package/dist/core/codev-metrics.js +77 -0
- package/dist/core/codev-personas.js +31 -0
- package/dist/core/codev-plan-gen.js +35 -0
- package/dist/core/codev-prompts.js +74 -0
- package/dist/core/codev-responses.js +62 -0
- package/dist/core/codev-rounds.js +218 -0
- package/dist/core/config.js +4 -0
- package/dist/core/context.js +381 -34
- package/dist/core/coordination.js +201 -6
- package/dist/core/cross-project.js +230 -16
- package/dist/core/default-profiles/doctor.yaml +11 -0
- package/dist/core/default-profiles/janitor.yaml +11 -0
- package/dist/core/default-profiles/onboarder.yaml +11 -0
- package/dist/core/default-profiles/reviewer.yaml +13 -0
- package/dist/core/dispatcher.js +1189 -0
- package/dist/core/duplicates.js +2 -2
- package/dist/core/entity-operations.js +450 -0
- package/dist/core/entity-registry.js +344 -0
- package/dist/core/events.js +106 -2
- package/dist/core/execution-adapters.js +154 -0
- package/dist/core/execution-context.js +63 -0
- package/dist/core/execution-profile.js +270 -0
- package/dist/core/execution.js +255 -0
- package/dist/core/facade-schema.js +81 -0
- package/dist/core/federation-cloud.js +99 -0
- package/dist/core/federation-message.js +52 -0
- package/dist/core/federation-transport.js +65 -0
- package/dist/core/gc-semantic.js +482 -0
- package/dist/core/governance.js +247 -0
- package/dist/core/guards.js +19 -0
- package/dist/core/ideation.js +72 -0
- package/dist/core/identity.js +110 -25
- package/dist/core/ids.js +6 -0
- package/dist/core/input-validation.js +2 -2
- package/dist/core/instruction-templates.js +344 -136
- package/dist/core/io.js +90 -11
- package/dist/core/lock.js +6 -2
- package/dist/core/loops/brief-assembly.js +213 -0
- package/dist/core/loops/facade-schema.js +148 -0
- package/dist/core/loops/index.js +7 -0
- package/dist/core/loops/iteration-engine.js +139 -0
- package/dist/core/loops/lock.js +385 -0
- package/dist/core/loops/store.js +201 -0
- package/dist/core/loops/types.js +403 -0
- package/dist/core/loops/verbs.js +534 -0
- package/dist/core/markdown.js +15 -3
- package/dist/core/memory-compactor.js +432 -0
- package/dist/core/memory-git.js +152 -8
- package/dist/core/messaging.js +278 -0
- package/dist/core/migration.js +32 -1
- package/dist/core/mutation-pipeline.js +4 -2
- package/dist/core/operations/memory-mutation.js +129 -0
- package/dist/core/operations/memory-write.js +78 -0
- package/dist/core/operations/plan.js +190 -0
- package/dist/core/policy.js +169 -0
- package/dist/core/reputation.js +9 -3
- package/dist/core/schema.js +491 -6
- package/dist/core/search.js +21 -2
- package/dist/core/security-cache.js +71 -0
- package/dist/core/security-guard.js +152 -0
- package/dist/core/security-scoring.js +86 -0
- package/dist/core/sequence.js +130 -0
- package/dist/core/socket-client.js +113 -0
- package/dist/core/staleness.js +246 -0
- package/dist/core/state.js +98 -22
- package/dist/core/store-resolution.js +43 -11
- package/dist/core/toml-writer.js +76 -0
- package/dist/core/upgrades/backup.js +232 -0
- package/dist/core/upgrades/health-check.js +169 -0
- package/dist/core/upgrades/patches/candidate-archive.js +145 -0
- package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
- package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
- package/dist/core/upgrades/schema-version.js +97 -0
- package/dist/core/worktree.js +606 -0
- package/dist/facts.js +114 -0
- package/dist/facts.json +111 -0
- package/docs/architecture/project-refs.md +5 -1
- package/docs/cli.md +690 -43
- package/docs/concepts/ideation-loop.md +317 -0
- package/docs/concepts/loop-engine.md +456 -0
- package/docs/concepts/mcp-governance.md +268 -0
- package/docs/concepts/memory-staleness.md +122 -0
- package/docs/concepts/multi-agent-workflows.md +166 -0
- package/docs/concepts/plans-and-claims.md +31 -6
- package/docs/concepts/project-md-convention.md +35 -0
- package/docs/concepts/troubleshooting.md +220 -0
- package/docs/concepts/upgrade-cli.md +202 -0
- package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
- package/docs/context-format-changelog.md +2 -2
- package/docs/context-format.md +2 -2
- package/docs/index.md +68 -0
- package/docs/integrations/agents.md +15 -16
- package/docs/integrations/cline.md +88 -0
- package/docs/integrations/codex.md +75 -23
- package/docs/integrations/continue.md +60 -0
- package/docs/integrations/copilot.md +67 -9
- package/docs/integrations/kilocode.md +72 -0
- package/docs/integrations/mcp.md +304 -21
- package/docs/integrations/mistral-vibe.md +122 -0
- package/docs/integrations/opencode.md +84 -0
- package/docs/integrations/overview.md +23 -8
- package/docs/integrations/roo.md +74 -0
- package/docs/integrations/windsurf.md +83 -0
- package/docs/mcp-schema-changelog.md +191 -1
- package/docs/playbooks/integration/index.md +121 -0
- package/docs/playbooks/productivity/index.md +102 -0
- package/docs/playbooks/team/index.md +122 -0
- package/docs/product/agent-first-model.md +184 -0
- package/docs/product/entity-model-audit.md +462 -0
- package/docs/quickstart-existing-project.md +135 -0
- package/docs/quickstart.md +124 -37
- package/docs/release-maintenance.md +79 -0
- package/docs/review.md +2 -0
- package/docs/server-operations.md +118 -0
- package/package.json +20 -12
- package/dist/commands/claude-desktop-extension.js +0 -18
- package/dist/commands/diff.js +0 -99
- package/dist/core/claude-desktop-extension.js +0 -224
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { memoryExists } from '../core/io.js';
|
|
3
4
|
import { buildOperationalIdentity, clearCurrentSession } from '../core/identity.js';
|
|
4
5
|
import { buildContextDiff } from '../core/context-diff.js';
|
|
5
6
|
import { listClaims, releaseClaim } from '../core/claims.js';
|
|
6
7
|
import { listRuntimeNotes, saveRuntimeNote, generateRuntimeNoteId } from '../core/runtime.js';
|
|
7
|
-
import { loadState } from '../core/state.js';
|
|
8
|
+
import { loadState, persistState } from '../core/state.js';
|
|
9
|
+
import { listArchivedCandidates, listCandidates } from '../core/candidates.js';
|
|
10
|
+
import { createFederationMessage } from '../core/federation-message.js';
|
|
11
|
+
import { pushSignal } from '../core/federation-transport.js';
|
|
12
|
+
import { loadConfig } from '../core/config.js';
|
|
13
|
+
import { resolveCrossProjectLinks } from '../core/cross-project.js';
|
|
8
14
|
import { createCandidateFromInput } from './reflect.js';
|
|
9
15
|
import { suggestCandidateTypes } from './reflect-runtime-note.js';
|
|
10
|
-
import { nowISO } from '../core/ids.js';
|
|
11
|
-
import { appendAuditEntry } from '../core/audit.js';
|
|
16
|
+
import { generateIdWithLabel, nowISO } from '../core/ids.js';
|
|
17
|
+
import { appendAuditEntry, readAuditLog } from '../core/audit.js';
|
|
12
18
|
import { requireMinimumTrustLevel, requireRegisteredAgentIdentity } from '../core/agent-registry.js';
|
|
13
19
|
import { loadSessionSnapshot } from '../commands/session-start.js';
|
|
20
|
+
import { extractFilesFromDiff } from '../commands/handoff.js';
|
|
21
|
+
import { suggestCompaction } from '../core/memory-compactor.js';
|
|
22
|
+
import { dispatchReview } from '../core/dispatcher.js';
|
|
23
|
+
export const REFLECTION_QUESTIONS = [
|
|
24
|
+
'What was the biggest time waste in this session, and how could it have been avoided?',
|
|
25
|
+
'What should have been done differently (design, process, or approach)?',
|
|
26
|
+
'What should brainclaw itself improve based on this session?',
|
|
27
|
+
];
|
|
14
28
|
export function runSessionEnd(options = {}) {
|
|
15
29
|
try {
|
|
16
30
|
const result = endSession(options);
|
|
@@ -40,9 +54,43 @@ export function runSessionEnd(options = {}) {
|
|
|
40
54
|
if (options.autoReflect) {
|
|
41
55
|
console.log(` Candidates created from auto-reflect: ${result.candidates_created}`);
|
|
42
56
|
}
|
|
57
|
+
if (result.handoff) {
|
|
58
|
+
console.log(` Reflected handoff: ${result.handoff.handoff_id}${result.handoff.plan_id ? ` (${result.handoff.plan_id})` : ''}`);
|
|
59
|
+
if (result.handoff.review_dispatched) {
|
|
60
|
+
console.log(` Review dispatched: ${result.handoff.reviewer}${result.handoff.review_message_id ? ` [${result.handoff.review_message_id}]` : ''}`);
|
|
61
|
+
}
|
|
62
|
+
else if (result.handoff.review_skip_reason) {
|
|
63
|
+
console.log(` Review not dispatched: ${result.handoff.review_skip_reason}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
43
66
|
if (result.context_diff) {
|
|
44
67
|
console.log(` ${result.context_diff}`);
|
|
45
68
|
}
|
|
69
|
+
if (result.session_stats) {
|
|
70
|
+
console.log(' Session stats:');
|
|
71
|
+
console.log(` duration: ${result.session_stats.session_duration_minutes} min`);
|
|
72
|
+
console.log(` file edits: ${result.session_stats.file_edits_count}`);
|
|
73
|
+
console.log(` claims created: ${result.session_stats.claims_created}`);
|
|
74
|
+
console.log(` memory writes: ${result.session_stats.memory_writes}`);
|
|
75
|
+
console.log(` plan updates: ${result.session_stats.plan_updates}`);
|
|
76
|
+
console.log(` candidates created: ${result.session_stats.candidates_created}`);
|
|
77
|
+
if (result.session_stats.last_brainclaw_write) {
|
|
78
|
+
console.log(` last brainclaw write: ${result.session_stats.last_brainclaw_write}`);
|
|
79
|
+
}
|
|
80
|
+
for (const warning of result.session_stats.warnings) {
|
|
81
|
+
console.log(` warning: ${warning}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (result.compaction_hint) {
|
|
85
|
+
console.log(` 💡 ${result.compaction_hint}`);
|
|
86
|
+
}
|
|
87
|
+
if (result.reflection_prompt) {
|
|
88
|
+
console.log('\n📝 Session reflection:');
|
|
89
|
+
for (let i = 0; i < result.reflection_prompt.questions.length; i++) {
|
|
90
|
+
console.log(` ${i + 1}. ${result.reflection_prompt.questions[i]}`);
|
|
91
|
+
}
|
|
92
|
+
console.log(`\n → Answer with: brainclaw note "your reflection" --tag reflection --tag session:${result.session_id}`);
|
|
93
|
+
}
|
|
46
94
|
}
|
|
47
95
|
catch (e) {
|
|
48
96
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -111,9 +159,8 @@ export function endSession(options = {}) {
|
|
|
111
159
|
visibility: 'shared',
|
|
112
160
|
note_type: 'session_end',
|
|
113
161
|
}, options.cwd);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Reflect-handoff: generate a handoff candidate from git commits since session start
|
|
162
|
+
// Reflect-handoff: materialize an open handoff from git commits since session start
|
|
163
|
+
let reflectedHandoff;
|
|
117
164
|
if (options.reflectHandoff) {
|
|
118
165
|
try {
|
|
119
166
|
const snapshot = loadSessionSnapshot(sessionId, options.cwd);
|
|
@@ -123,25 +170,79 @@ export function endSession(options = {}) {
|
|
|
123
170
|
const commits = execSync(`git log --oneline ${ref}..HEAD`, { encoding: 'utf-8', cwd }).trim();
|
|
124
171
|
const diffStat = execSync(`git diff --stat ${ref}..HEAD`, { encoding: 'utf-8', cwd }).trim();
|
|
125
172
|
if (commits) {
|
|
126
|
-
const
|
|
127
|
-
.filter((c) => c.status === 'released' && c.agent === registered.agent_name)
|
|
128
|
-
|
|
129
|
-
|
|
173
|
+
const releasedClaims = listClaims(options.cwd)
|
|
174
|
+
.filter((c) => c.status === 'released' && c.agent === registered.agent_name);
|
|
175
|
+
const releasedScopes = releasedClaims.map((c) => c.scope).join(', ');
|
|
176
|
+
// Extract files touched from the full diff for the contract
|
|
177
|
+
let filesTouched = [];
|
|
178
|
+
let fullDiff;
|
|
179
|
+
try {
|
|
180
|
+
fullDiff = execSync(`git diff ${ref}..HEAD`, { encoding: 'utf-8', cwd, maxBuffer: 10 * 1024 * 1024 }).trim();
|
|
181
|
+
filesTouched = extractFilesFromDiff(fullDiff);
|
|
182
|
+
}
|
|
183
|
+
catch { /* fall back to empty */ }
|
|
184
|
+
// Extract linked plan IDs from released claims
|
|
185
|
+
const linkedPlans = [...new Set(releasedClaims.map((c) => c.plan_id).filter(Boolean))];
|
|
186
|
+
const primaryPlanId = linkedPlans.length === 1 ? linkedPlans[0] : undefined;
|
|
187
|
+
// Build contract metadata for the handoff text
|
|
188
|
+
const contractLines = [];
|
|
189
|
+
if (filesTouched.length > 0)
|
|
190
|
+
contractLines.push(`Files touched: ${filesTouched.join(', ')}`);
|
|
191
|
+
if (linkedPlans.length > 0)
|
|
192
|
+
contractLines.push(`Linked plans: ${linkedPlans.join(', ')}`);
|
|
130
193
|
const handoffText = [
|
|
131
194
|
`Session ${sessionId} — auto-generated handoff`,
|
|
195
|
+
options.narrative ? `\nNarrative: ${options.narrative}` : '',
|
|
132
196
|
'',
|
|
133
197
|
`Commits:\n${commits}`,
|
|
134
198
|
diffStat ? `\nChanged files:\n${diffStat}` : '',
|
|
135
199
|
releasedScopes ? `\nReleased claims: ${releasedScopes}` : '',
|
|
200
|
+
contractLines.length > 0 ? `\nContract:\n${contractLines.join('\n')}` : '',
|
|
136
201
|
summaryText !== `Session ended — ${sessionNotes.length} runtime note(s) created` ? `\nSummary: ${summaryText}` : '',
|
|
137
202
|
].filter(Boolean).join('\n');
|
|
138
|
-
|
|
203
|
+
const narrativeParts = [
|
|
204
|
+
options.narrative?.trim(),
|
|
205
|
+
summaryText !== `Session ended — ${sessionNotes.length} runtime note(s) created` ? summaryText : undefined,
|
|
206
|
+
].filter((value) => Boolean(value && value.trim().length > 0));
|
|
207
|
+
const materialized = materializeSessionHandoff({
|
|
139
208
|
author: actor.agent,
|
|
140
209
|
authorId: actor.agent_id,
|
|
141
210
|
sessionId,
|
|
142
|
-
|
|
211
|
+
text: handoffText,
|
|
212
|
+
narrative: narrativeParts.length > 0 ? narrativeParts.join('\n\n') : undefined,
|
|
213
|
+
planId: primaryPlanId,
|
|
214
|
+
linkedPlans,
|
|
215
|
+
filesTouched,
|
|
216
|
+
fullDiff,
|
|
143
217
|
cwd: options.cwd,
|
|
144
|
-
}
|
|
218
|
+
});
|
|
219
|
+
reflectedHandoff = {
|
|
220
|
+
handoff_id: materialized.handoff_id,
|
|
221
|
+
plan_id: materialized.plan_id,
|
|
222
|
+
review_dispatched: false,
|
|
223
|
+
review_skip_reason: options.dispatchReview ? 'Reflected handoff is not reviewable yet' : undefined,
|
|
224
|
+
};
|
|
225
|
+
if (options.dispatchReview) {
|
|
226
|
+
const reviewResult = dispatchReview({
|
|
227
|
+
handoffId: materialized.handoff_id,
|
|
228
|
+
reviewer: options.reviewer,
|
|
229
|
+
dispatcherAgent: actor.agent,
|
|
230
|
+
dispatcherAgentId: actor.agent_id,
|
|
231
|
+
sessionId,
|
|
232
|
+
}, options.cwd ?? process.cwd());
|
|
233
|
+
const sent = reviewResult.reviews_sent.find((entry) => entry.handoff_id === materialized.handoff_id);
|
|
234
|
+
const skipped = reviewResult.skipped.find((entry) => entry.handoff_id === materialized.handoff_id);
|
|
235
|
+
if (sent) {
|
|
236
|
+
reflectedHandoff.review_dispatched = true;
|
|
237
|
+
reflectedHandoff.reviewer = sent.reviewer;
|
|
238
|
+
reflectedHandoff.review_message_id = sent.message_id;
|
|
239
|
+
reflectedHandoff.review_skip_reason = undefined;
|
|
240
|
+
updateReflectedHandoffRecipient(materialized.handoff_id, sent.reviewer, options.cwd);
|
|
241
|
+
}
|
|
242
|
+
else if (skipped) {
|
|
243
|
+
reflectedHandoff.review_skip_reason = skipped.reason;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
145
246
|
}
|
|
146
247
|
}
|
|
147
248
|
catch { /* non-fatal — no git or no commits */ }
|
|
@@ -162,7 +263,7 @@ export function endSession(options = {}) {
|
|
|
162
263
|
authorId: note.agent_id,
|
|
163
264
|
projectId: note.project_id,
|
|
164
265
|
sessionId: note.session_id,
|
|
165
|
-
source:
|
|
266
|
+
source: 'auto',
|
|
166
267
|
cwd: options.cwd,
|
|
167
268
|
}, false, false, true);
|
|
168
269
|
if (creation.candidateId) {
|
|
@@ -173,6 +274,46 @@ export function endSession(options = {}) {
|
|
|
173
274
|
}
|
|
174
275
|
}
|
|
175
276
|
}
|
|
277
|
+
const sessionStats = buildSessionStats({
|
|
278
|
+
sessionId,
|
|
279
|
+
sessionStartedAt: loadSessionSnapshot(sessionId, options.cwd)?.started_at,
|
|
280
|
+
agent: actor.agent,
|
|
281
|
+
agentId: actor.agent_id,
|
|
282
|
+
notesInSession: sessionNotes,
|
|
283
|
+
cwd: options.cwd,
|
|
284
|
+
});
|
|
285
|
+
// Memory compaction hint (best-effort, non-fatal)
|
|
286
|
+
let compactionHint;
|
|
287
|
+
try {
|
|
288
|
+
compactionHint = suggestCompaction(state);
|
|
289
|
+
}
|
|
290
|
+
catch { /* non-fatal */ }
|
|
291
|
+
let pushedSignals = 0;
|
|
292
|
+
try {
|
|
293
|
+
pushedSignals = pushSessionFederationSignals({
|
|
294
|
+
sessionId,
|
|
295
|
+
actor,
|
|
296
|
+
sessionNotes,
|
|
297
|
+
cwd: options.cwd,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Non-fatal
|
|
302
|
+
}
|
|
303
|
+
if (pushedSignals > 0 && !options.json) {
|
|
304
|
+
console.log(`✔ Pushed ${pushedSignals} signal(s) to linked projects`);
|
|
305
|
+
}
|
|
306
|
+
appendAuditEntry({
|
|
307
|
+
action: 'session_end',
|
|
308
|
+
actor: actor.agent,
|
|
309
|
+
actor_id: actor.agent_id,
|
|
310
|
+
item_id: sessionId,
|
|
311
|
+
item_type: 'session',
|
|
312
|
+
session_id: sessionId,
|
|
313
|
+
host_id: actor.host_id,
|
|
314
|
+
after: sessionStats,
|
|
315
|
+
}, options.cwd);
|
|
316
|
+
clearCurrentSession(options.cwd, sessionId);
|
|
176
317
|
const result = {
|
|
177
318
|
session_id: sessionId,
|
|
178
319
|
agent: actor.agent,
|
|
@@ -181,7 +322,275 @@ export function endSession(options = {}) {
|
|
|
181
322
|
context_diff: contextDiff,
|
|
182
323
|
summary: summaryText,
|
|
183
324
|
open_work_warning: openWorkWarning,
|
|
325
|
+
session_stats: sessionStats,
|
|
326
|
+
compaction_hint: compactionHint,
|
|
327
|
+
...(reflectedHandoff ? { handoff: reflectedHandoff } : {}),
|
|
184
328
|
};
|
|
329
|
+
if (options.reflect) {
|
|
330
|
+
result.reflection_prompt = {
|
|
331
|
+
questions: [...REFLECTION_QUESTIONS],
|
|
332
|
+
instruction: `Please reflect on this session and answer each question. Write your answers using bclaw_write_note with tags ["reflection", "session:${sessionId}"]. One note per question, or a single combined note.`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
185
335
|
return result;
|
|
186
336
|
}
|
|
337
|
+
function materializeSessionHandoff(input) {
|
|
338
|
+
const cwd = input.cwd ?? process.cwd();
|
|
339
|
+
const state = loadState(cwd);
|
|
340
|
+
const { id, short_label } = generateIdWithLabel('open_handoffs', cwd);
|
|
341
|
+
state.open_handoffs.push({
|
|
342
|
+
id,
|
|
343
|
+
short_label,
|
|
344
|
+
from: input.author,
|
|
345
|
+
to: 'reviewer',
|
|
346
|
+
text: input.text,
|
|
347
|
+
created_at: nowISO(),
|
|
348
|
+
author: input.author,
|
|
349
|
+
author_id: input.authorId,
|
|
350
|
+
session_id: input.sessionId,
|
|
351
|
+
status: 'open',
|
|
352
|
+
plan_id: input.planId,
|
|
353
|
+
narrative: input.narrative,
|
|
354
|
+
tags: ['auto-handoff', `session:${input.sessionId}`],
|
|
355
|
+
related_paths: input.filesTouched.length > 0 ? input.filesTouched : undefined,
|
|
356
|
+
contract: input.filesTouched.length > 0 || input.linkedPlans.length > 0
|
|
357
|
+
? {
|
|
358
|
+
files_touched: input.filesTouched.length > 0 ? input.filesTouched : undefined,
|
|
359
|
+
linked_plans: input.linkedPlans.length > 0 ? input.linkedPlans : undefined,
|
|
360
|
+
}
|
|
361
|
+
: undefined,
|
|
362
|
+
snapshot: input.fullDiff ? { diff: input.fullDiff } : undefined,
|
|
363
|
+
});
|
|
364
|
+
persistState(state, cwd);
|
|
365
|
+
return { handoff_id: id, plan_id: input.planId };
|
|
366
|
+
}
|
|
367
|
+
function updateReflectedHandoffRecipient(handoffId, reviewer, cwd) {
|
|
368
|
+
const effectiveCwd = cwd ?? process.cwd();
|
|
369
|
+
const state = loadState(effectiveCwd);
|
|
370
|
+
const handoff = state.open_handoffs.find((entry) => entry.id === handoffId);
|
|
371
|
+
if (!handoff)
|
|
372
|
+
return;
|
|
373
|
+
handoff.to = reviewer;
|
|
374
|
+
persistState(state, effectiveCwd);
|
|
375
|
+
}
|
|
376
|
+
function pushSessionFederationSignals(input) {
|
|
377
|
+
const cwd = input.cwd ?? process.cwd();
|
|
378
|
+
const config = loadConfig(cwd);
|
|
379
|
+
const links = resolveCrossProjectLinks(cwd);
|
|
380
|
+
const publisherLinks = links.filter((link) => link.role === 'publisher' && link.available);
|
|
381
|
+
if (!config.cross_project_links?.length || publisherLinks.length === 0) {
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
const currentState = loadState(cwd);
|
|
385
|
+
const sessionHandoffs = currentState.open_handoffs.filter((handoff) => handoff.session_id === input.sessionId);
|
|
386
|
+
const sessionCandidates = [
|
|
387
|
+
...listCandidates(undefined, cwd),
|
|
388
|
+
...listArchivedCandidates('accepted', cwd),
|
|
389
|
+
...listArchivedCandidates('rejected', cwd),
|
|
390
|
+
].filter((candidate) => candidate.session_id === input.sessionId);
|
|
391
|
+
const sessionRuntimeNotes = input.sessionNotes.filter((note) => note.session_id === input.sessionId);
|
|
392
|
+
const fromProjectName = config.project_name ?? path.basename(cwd);
|
|
393
|
+
const seen = new Set();
|
|
394
|
+
let pushed = 0;
|
|
395
|
+
const pushEntitySignal = (entityType, entity) => {
|
|
396
|
+
const target = extractCrossProjectTarget(entity);
|
|
397
|
+
if (!target)
|
|
398
|
+
return;
|
|
399
|
+
const link = resolvePublisherLink(target, publisherLinks, entityType, cwd);
|
|
400
|
+
if (!link)
|
|
401
|
+
return;
|
|
402
|
+
const dedupeKey = `${entityType}:${entity.id}:${link.absolutePath}`;
|
|
403
|
+
if (seen.has(dedupeKey))
|
|
404
|
+
return;
|
|
405
|
+
seen.add(dedupeKey);
|
|
406
|
+
const message = createFederationMessage({
|
|
407
|
+
version: 1,
|
|
408
|
+
from: {
|
|
409
|
+
project_id: input.actor.project_id ?? config.project_id,
|
|
410
|
+
project_name: fromProjectName,
|
|
411
|
+
project_path: cwd,
|
|
412
|
+
agent_name: input.actor.agent,
|
|
413
|
+
agent_id: input.actor.agent_id,
|
|
414
|
+
host_id: input.actor.host_id,
|
|
415
|
+
},
|
|
416
|
+
to: {
|
|
417
|
+
project_name: link.projectName,
|
|
418
|
+
project_path: link.absolutePath,
|
|
419
|
+
},
|
|
420
|
+
type: entityType,
|
|
421
|
+
payload: entity,
|
|
422
|
+
causal_parent: input.sessionId,
|
|
423
|
+
});
|
|
424
|
+
pushSignal(link.absolutePath, message);
|
|
425
|
+
pushed++;
|
|
426
|
+
};
|
|
427
|
+
for (const handoff of sessionHandoffs) {
|
|
428
|
+
pushEntitySignal('handoff', handoff);
|
|
429
|
+
}
|
|
430
|
+
for (const candidate of sessionCandidates) {
|
|
431
|
+
pushEntitySignal('candidate', candidate);
|
|
432
|
+
}
|
|
433
|
+
for (const note of sessionRuntimeNotes) {
|
|
434
|
+
pushEntitySignal('runtime_note', note);
|
|
435
|
+
}
|
|
436
|
+
return pushed;
|
|
437
|
+
}
|
|
438
|
+
function resolvePublisherLink(target, publisherLinks, entityType, cwd) {
|
|
439
|
+
const normalized = target.trim().toLowerCase();
|
|
440
|
+
if (!normalized)
|
|
441
|
+
return undefined;
|
|
442
|
+
const absoluteTarget = path.resolve(cwd, target).toLowerCase();
|
|
443
|
+
return publisherLinks.find((link) => {
|
|
444
|
+
if (link.channels?.length && !link.channels.includes(entityType)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const matchesByLabel = [link.projectName, link.name, link.path, link.absolutePath, path.basename(link.absolutePath)]
|
|
448
|
+
.filter((entry) => Boolean(entry))
|
|
449
|
+
.some((entry) => entry.toLowerCase() === normalized);
|
|
450
|
+
if (matchesByLabel)
|
|
451
|
+
return true;
|
|
452
|
+
return path.resolve(link.absolutePath).toLowerCase() === absoluteTarget;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function extractCrossProjectTarget(entity) {
|
|
456
|
+
const direct = extractTargetValue(entity.target_project)
|
|
457
|
+
?? extractTargetValue(entity.targetProject)
|
|
458
|
+
?? extractTargetValue(entity.cross_project)
|
|
459
|
+
?? extractTargetValue(entity.crossProject);
|
|
460
|
+
if (direct)
|
|
461
|
+
return direct;
|
|
462
|
+
const metadata = entity.metadata;
|
|
463
|
+
if (isRecord(metadata)) {
|
|
464
|
+
const metadataTarget = extractTargetValue(metadata.target_project)
|
|
465
|
+
?? extractTargetValue(metadata.targetProject)
|
|
466
|
+
?? extractTargetValue(metadata.cross_project)
|
|
467
|
+
?? extractTargetValue(metadata.crossProject);
|
|
468
|
+
if (metadataTarget)
|
|
469
|
+
return metadataTarget;
|
|
470
|
+
}
|
|
471
|
+
const tags = entity.tags;
|
|
472
|
+
if (Array.isArray(tags)) {
|
|
473
|
+
for (const rawTag of tags) {
|
|
474
|
+
if (typeof rawTag !== 'string')
|
|
475
|
+
continue;
|
|
476
|
+
const parsed = parseTargetTag(rawTag);
|
|
477
|
+
if (parsed)
|
|
478
|
+
return parsed;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
function extractTargetValue(value) {
|
|
484
|
+
if (typeof value === 'string') {
|
|
485
|
+
const trimmed = value.trim();
|
|
486
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
487
|
+
}
|
|
488
|
+
if (!isRecord(value))
|
|
489
|
+
return undefined;
|
|
490
|
+
for (const key of ['path', 'project_path', 'name', 'project_name']) {
|
|
491
|
+
const nested = value[key];
|
|
492
|
+
if (typeof nested === 'string' && nested.trim().length > 0) {
|
|
493
|
+
return nested.trim();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
function parseTargetTag(tag) {
|
|
499
|
+
const match = tag.match(/^(?:target_project|target-project|targetProject|cross_project|cross-project)\s*:\s*(.+)$/i);
|
|
500
|
+
if (!match)
|
|
501
|
+
return undefined;
|
|
502
|
+
const target = match[1].trim();
|
|
503
|
+
return target.length > 0 ? target : undefined;
|
|
504
|
+
}
|
|
505
|
+
function isRecord(value) {
|
|
506
|
+
return Boolean(value) && typeof value === 'object';
|
|
507
|
+
}
|
|
508
|
+
const SESSION_MEMORY_WRITE_ACTIONS = ['create', 'update', 'delete', 'accept', 'reject', 'trust_change', 'promote_direct', 'rollback'];
|
|
509
|
+
function buildSessionStats(input) {
|
|
510
|
+
if (!input.sessionStartedAt) {
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
const startedAtMs = Date.parse(input.sessionStartedAt);
|
|
514
|
+
if (!Number.isFinite(startedAtMs)) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
const auditEntries = readAuditLog({ since: input.sessionStartedAt, actor: input.agentId ?? input.agent }, input.cwd)
|
|
518
|
+
.filter((entry) => belongsToSession(entry, input.sessionId));
|
|
519
|
+
const runtimeWrites = input.notesInSession.filter((note) => (note.note_type ?? 'observation') === 'observation');
|
|
520
|
+
const claimsCreated = auditEntries.filter((entry) => entry.action === 'claim').length;
|
|
521
|
+
const planUpdates = auditEntries.filter((entry) => entry.item_type === 'plan' && ['create', 'update', 'delete'].includes(entry.action)).length;
|
|
522
|
+
const memoryWrites = auditEntries.filter((entry) => SESSION_MEMORY_WRITE_ACTIONS.includes(entry.action)).length + runtimeWrites.length;
|
|
523
|
+
const lastBrainclawWrite = [
|
|
524
|
+
...runtimeWrites.map((note) => note.created_at),
|
|
525
|
+
...auditEntries.filter((entry) => SESSION_MEMORY_WRITE_ACTIONS.includes(entry.action)).map((entry) => entry.timestamp),
|
|
526
|
+
].sort().at(-1);
|
|
527
|
+
const candidatesCreated = countSessionCandidates(input.sessionId, input.cwd);
|
|
528
|
+
const fileEditsCount = countSessionEditedFiles(input.sessionId, input.cwd);
|
|
529
|
+
const warnings = [];
|
|
530
|
+
if (fileEditsCount > 0 && claimsCreated === 0) {
|
|
531
|
+
warnings.push(`${fileEditsCount} file edit(s) with 0 claims created`);
|
|
532
|
+
}
|
|
533
|
+
if (fileEditsCount > 0 && memoryWrites === 0) {
|
|
534
|
+
warnings.push(`${fileEditsCount} file edit(s) with 0 memory writes suggests decisions or traps may have been missed`);
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
session_duration_minutes: Math.max(0, Math.floor((Date.now() - startedAtMs) / 60_000)),
|
|
538
|
+
file_edits_count: fileEditsCount,
|
|
539
|
+
claims_created: claimsCreated,
|
|
540
|
+
memory_writes: memoryWrites,
|
|
541
|
+
plan_updates: planUpdates,
|
|
542
|
+
candidates_created: candidatesCreated,
|
|
543
|
+
last_brainclaw_write: lastBrainclawWrite,
|
|
544
|
+
warnings,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function belongsToSession(entry, sessionId) {
|
|
548
|
+
return !entry.session_id || entry.session_id === sessionId;
|
|
549
|
+
}
|
|
550
|
+
function countSessionCandidates(sessionId, cwd) {
|
|
551
|
+
const authored = [
|
|
552
|
+
...listCandidates(undefined, cwd),
|
|
553
|
+
...listArchivedCandidates('accepted', cwd),
|
|
554
|
+
...listArchivedCandidates('rejected', cwd),
|
|
555
|
+
];
|
|
556
|
+
return authored.filter((candidate) => candidate.session_id === sessionId).length;
|
|
557
|
+
}
|
|
558
|
+
function countSessionEditedFiles(sessionId, cwd) {
|
|
559
|
+
const snapshot = loadSessionSnapshot(sessionId, cwd);
|
|
560
|
+
const repoCwd = cwd ?? process.cwd();
|
|
561
|
+
try {
|
|
562
|
+
const touched = new Set();
|
|
563
|
+
if (snapshot?.git_sha) {
|
|
564
|
+
for (const pathEntry of execSync(`git diff --name-only ${snapshot.git_sha}..HEAD`, {
|
|
565
|
+
cwd: repoCwd,
|
|
566
|
+
encoding: 'utf-8',
|
|
567
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
568
|
+
}).split(/\r?\n/).filter((entry) => Boolean(entry) && shouldCountEditedPath(entry))) {
|
|
569
|
+
touched.add(pathEntry);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
for (const pathEntry of execSync('git diff --name-only HEAD', {
|
|
573
|
+
cwd: repoCwd,
|
|
574
|
+
encoding: 'utf-8',
|
|
575
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
576
|
+
}).split(/\r?\n/).filter((entry) => Boolean(entry) && shouldCountEditedPath(entry))) {
|
|
577
|
+
touched.add(pathEntry);
|
|
578
|
+
}
|
|
579
|
+
for (const pathEntry of execSync('git ls-files --others --exclude-standard', {
|
|
580
|
+
cwd: repoCwd,
|
|
581
|
+
encoding: 'utf-8',
|
|
582
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
583
|
+
}).split(/\r?\n/).filter((entry) => Boolean(entry) && shouldCountEditedPath(entry))) {
|
|
584
|
+
touched.add(pathEntry);
|
|
585
|
+
}
|
|
586
|
+
return touched.size;
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function shouldCountEditedPath(relativePath) {
|
|
593
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
594
|
+
return !normalized.startsWith('.brainclaw/') && !normalized.startsWith('.git/');
|
|
595
|
+
}
|
|
187
596
|
//# sourceMappingURL=session-end.js.map
|