brainclaw 1.28.2 → 1.28.4
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 +6 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/harvest.js +67 -25
- package/dist/commands/loops-handlers.js +28 -1
- package/dist/commands/mcp-catalog.js +25 -4
- package/dist/commands/mcp-read-handlers.js +15 -1
- package/dist/commands/mcp-schemas.generated.js +13 -0
- package/dist/commands/mcp-write-coordination.js +97 -26
- package/dist/commands/mcp-write-entities.js +5 -2
- package/dist/commands/mcp-write-memory.js +87 -1
- package/dist/commands/mcp.js +41 -9
- package/dist/core/code-map/aggregate.js +20 -7
- package/dist/core/code-map/backend.js +17 -7
- package/dist/core/code-map/cascade-jobs.js +174 -0
- package/dist/core/code-map/cascade-worker.js +15 -0
- package/dist/core/code-map/cascade.js +63 -26
- package/dist/core/code-map/query.js +6 -3
- package/dist/core/context.js +16 -3
- package/dist/core/dispatch-status.js +36 -14
- package/dist/core/dispatcher.js +28 -20
- package/dist/core/entity-operations.js +80 -8
- package/dist/core/entity-registry.js +3 -3
- package/dist/core/execution-adapters.js +18 -1
- package/dist/core/facade-schema.js +10 -0
- package/dist/core/ideation-loop-close.js +3 -1
- package/dist/core/lane-result-file.js +72 -0
- package/dist/core/loop-turn-dispatch.js +2 -0
- package/dist/core/loops/brief-assembly.js +19 -11
- package/dist/core/loops/next-expected.js +56 -1
- package/dist/core/loops/reconcile-turn.js +8 -0
- package/dist/core/loops/result-reducers.js +14 -12
- package/dist/core/loops/store.js +4 -0
- package/dist/core/loops/types.js +14 -2
- package/dist/core/loops/verbs.js +8 -1
- package/dist/core/loops/worker-reply-contract.js +1 -1
- package/dist/core/protocol-tool-policy.js +1 -0
- package/dist/core/review-loop-turn-dispatch.js +1 -0
- package/dist/core/schema.js +24 -1
- package/dist/core/search.js +3 -2
- package/dist/core/spawn-check.js +9 -1
- package/dist/core/worktree.js +14 -7
- package/dist/facts.js +10 -9
- package/dist/facts.json +9 -8
- package/docs/cli.md +35 -2
- package/docs/code-map.md +20 -9
- package/docs/concepts/ideation-loop.md +35 -14
- package/docs/integrations/mcp.md +15 -5
- package/docs/mcp-schema-changelog.md +72 -6
- package/package.json +1 -1
|
@@ -19,7 +19,8 @@ import { exportSubgraph } from './export.js';
|
|
|
19
19
|
import { fileId } from './ids.js';
|
|
20
20
|
import { resolveTraversal, aggregateFind, aggregateBrief } from './aggregate.js';
|
|
21
21
|
import { defaultMemoryReader } from './memory-reader.js';
|
|
22
|
-
import {
|
|
22
|
+
import { inspectNestedProjects, refreshWorkspaceCascade } from './cascade.js';
|
|
23
|
+
import { latestCascadeRefreshJob, summarizeCascadeRefreshJob } from './cascade-jobs.js';
|
|
23
24
|
import { loadConfig } from '../config.js';
|
|
24
25
|
import { codeMapDir } from './paths.js';
|
|
25
26
|
/** spec §9 caps the brief reading list at 12 files. */
|
|
@@ -112,17 +113,26 @@ function isMultiProjectWorkspace(cwd) {
|
|
|
112
113
|
/** Per-child store recap for `status(cascade)` in a multi-project workspace. */
|
|
113
114
|
function buildCascadeStatus(rootCwd) {
|
|
114
115
|
const root = rootCwd ?? process.cwd();
|
|
115
|
-
const
|
|
116
|
+
const discovery = inspectNestedProjects(root);
|
|
117
|
+
const children = discovery.projects.map((abs) => {
|
|
116
118
|
const m = readManifest(abs);
|
|
117
119
|
return {
|
|
118
120
|
path: path.relative(root, abs).replace(/\\/g, '/') || '.',
|
|
119
121
|
store_exists: m ? true : storeExists(abs),
|
|
120
122
|
freshness: m ? m.freshness.status : 'missing_index',
|
|
121
123
|
files_indexed: m ? m.stats.files_indexed : null,
|
|
124
|
+
...(m && m.stats.files_indexed === 0 ? { reason: 'no_eligible_files' } : {}),
|
|
122
125
|
};
|
|
123
126
|
});
|
|
124
127
|
const indexed = children.filter((c) => c.freshness !== 'missing_index').length;
|
|
125
|
-
|
|
128
|
+
const latestJob = latestCascadeRefreshJob(root);
|
|
129
|
+
return {
|
|
130
|
+
children,
|
|
131
|
+
indexed_children: indexed,
|
|
132
|
+
total_children: children.length,
|
|
133
|
+
discovery_truncated: discovery.truncated,
|
|
134
|
+
...(latestJob ? { refresh_job: summarizeCascadeRefreshJob(latestJob) } : {}),
|
|
135
|
+
};
|
|
126
136
|
}
|
|
127
137
|
/**
|
|
128
138
|
* P0 JSONL-backed query backend. Reads the durable file store (manifest +
|
|
@@ -198,17 +208,17 @@ export class JsonlBackend {
|
|
|
198
208
|
// A cascade is only fully "acquired" when EVERY project got its lock; if a
|
|
199
209
|
// child or the root was skipped under a live writer, surface that instead
|
|
200
210
|
// of reporting a clean lock_acquired=true over a partial cascade (codex review).
|
|
201
|
-
const
|
|
211
|
+
const incomplete = allProjects.filter((p) => p.outcome === 'locked' || p.outcome === 'failed');
|
|
202
212
|
return {
|
|
203
213
|
ran: allProjects.some((p) => p.ran),
|
|
204
214
|
scope,
|
|
205
|
-
lock_acquired:
|
|
215
|
+
lock_acquired: incomplete.length === 0,
|
|
206
216
|
freshness_badge: badge(root.freshness, {
|
|
207
217
|
files_parsed: root.files_parsed,
|
|
208
218
|
children_refreshed: cascade.children_refreshed,
|
|
209
219
|
}),
|
|
210
|
-
...(
|
|
211
|
-
? { lock_status: `${
|
|
220
|
+
...(incomplete.length > 0
|
|
221
|
+
? { lock_status: `${incomplete.length} project(s) incomplete: ${incomplete.map((p) => `${p.path} (${p.outcome})`).join(', ')}` }
|
|
212
222
|
: {}),
|
|
213
223
|
cascade,
|
|
214
224
|
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { writeFileAtomic } from '../io.js';
|
|
8
|
+
import { codeMapDir } from './paths.js';
|
|
9
|
+
import { inspectNestedProjects, refreshWorkspaceCascade } from './cascade.js';
|
|
10
|
+
export function summarizeCascadeRefreshJob(job) {
|
|
11
|
+
const { result, ...summary } = job;
|
|
12
|
+
if (!result)
|
|
13
|
+
return summary;
|
|
14
|
+
const projects = [result.root_result, ...result.children];
|
|
15
|
+
const outcomeCounts = {};
|
|
16
|
+
for (const project of projects)
|
|
17
|
+
outcomeCounts[project.outcome] = (outcomeCounts[project.outcome] ?? 0) + 1;
|
|
18
|
+
const problemProjects = projects
|
|
19
|
+
.filter((project) => project.outcome !== 'indexed')
|
|
20
|
+
.map((project) => ({ path: project.path, outcome: project.outcome, ...(project.reason ? { reason: project.reason } : {}), ...(project.error ? { error: project.error } : {}) }));
|
|
21
|
+
return { ...summary, outcome_counts: outcomeCounts, ...(problemProjects.length ? { problem_projects: problemProjects } : {}) };
|
|
22
|
+
}
|
|
23
|
+
function jobsDir(root) {
|
|
24
|
+
return path.join(codeMapDir(root), 'cascade-jobs');
|
|
25
|
+
}
|
|
26
|
+
function jobPath(root, jobId) {
|
|
27
|
+
return path.join(jobsDir(root), `${jobId}.json`);
|
|
28
|
+
}
|
|
29
|
+
function writeJob(job) {
|
|
30
|
+
const dir = jobsDir(job.root);
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
const target = jobPath(job.root, job.job_id);
|
|
33
|
+
writeFileAtomic(target, `${JSON.stringify(job, null, 2)}\n`);
|
|
34
|
+
}
|
|
35
|
+
export function readCascadeRefreshJob(root, jobId) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(jobPath(path.resolve(root), jobId), 'utf8'));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function latestCascadeRefreshJob(root) {
|
|
44
|
+
const dir = jobsDir(path.resolve(root));
|
|
45
|
+
try {
|
|
46
|
+
const jobs = fs.readdirSync(dir)
|
|
47
|
+
.filter((name) => name.endsWith('.json'))
|
|
48
|
+
.map((name) => {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.filter((job) => job !== null)
|
|
57
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
58
|
+
return jobs[0] ?? null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function processAlive(pid) {
|
|
65
|
+
if (!pid)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, 0);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function startCascadeRefreshJob(root, scope) {
|
|
76
|
+
const resolvedRoot = path.resolve(root);
|
|
77
|
+
try {
|
|
78
|
+
if (loadConfig(resolvedRoot).project_mode !== 'multi-project')
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const previous = latestCascadeRefreshJob(resolvedRoot);
|
|
85
|
+
if (previous && (previous.status === 'queued' || previous.status === 'running') && processAlive(previous.pid)) {
|
|
86
|
+
return previous;
|
|
87
|
+
}
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
const job = {
|
|
90
|
+
job_id: `cmj_${crypto.randomBytes(8).toString('hex')}`,
|
|
91
|
+
root: resolvedRoot,
|
|
92
|
+
scope,
|
|
93
|
+
status: 'queued',
|
|
94
|
+
created_at: now,
|
|
95
|
+
updated_at: now,
|
|
96
|
+
projects_total: inspectNestedProjects(resolvedRoot).projects.length + 1,
|
|
97
|
+
projects_completed: 0,
|
|
98
|
+
current_project: null,
|
|
99
|
+
};
|
|
100
|
+
writeJob(job);
|
|
101
|
+
const worker = fileURLToPath(new URL('./cascade-worker.js', import.meta.url));
|
|
102
|
+
const child = spawn(process.execPath, [worker, resolvedRoot, job.job_id, scope], {
|
|
103
|
+
cwd: resolvedRoot,
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: 'ignore',
|
|
106
|
+
windowsHide: true,
|
|
107
|
+
});
|
|
108
|
+
const markWorkerExit = (detail) => {
|
|
109
|
+
const current = readCascadeRefreshJob(resolvedRoot, job.job_id);
|
|
110
|
+
if (!current || current.status === 'completed' || current.status === 'failed')
|
|
111
|
+
return;
|
|
112
|
+
const completed = new Date().toISOString();
|
|
113
|
+
writeJob({
|
|
114
|
+
...current,
|
|
115
|
+
status: 'failed',
|
|
116
|
+
error: detail,
|
|
117
|
+
current_project: null,
|
|
118
|
+
completed_at: completed,
|
|
119
|
+
updated_at: completed,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
child.once('error', (error) => {
|
|
123
|
+
markWorkerExit(`cascade worker failed to start: ${error.message}`);
|
|
124
|
+
});
|
|
125
|
+
child.once('exit', (code, signal) => {
|
|
126
|
+
// The worker writes its terminal state synchronously before exiting. If no
|
|
127
|
+
// terminal state exists here, the launch/runtime failed outside its guard.
|
|
128
|
+
markWorkerExit(`cascade worker exited before completion (code=${code ?? 'null'}, signal=${signal ?? 'none'})`);
|
|
129
|
+
});
|
|
130
|
+
child.unref();
|
|
131
|
+
const current = readCascadeRefreshJob(resolvedRoot, job.job_id) ?? job;
|
|
132
|
+
if (current.status === 'queued') {
|
|
133
|
+
current.pid = child.pid;
|
|
134
|
+
current.updated_at = new Date().toISOString();
|
|
135
|
+
writeJob(current);
|
|
136
|
+
}
|
|
137
|
+
return current;
|
|
138
|
+
}
|
|
139
|
+
/** Worker entry seam, exported for deterministic tests and the tiny worker file. */
|
|
140
|
+
export async function runCascadeRefreshJob(root, jobId, scope) {
|
|
141
|
+
const job = readCascadeRefreshJob(root, jobId);
|
|
142
|
+
if (!job)
|
|
143
|
+
throw new Error(`cascade job not found: ${jobId}`);
|
|
144
|
+
const started = new Date().toISOString();
|
|
145
|
+
writeJob({ ...job, status: 'running', pid: process.pid, started_at: started, updated_at: started });
|
|
146
|
+
try {
|
|
147
|
+
const result = await refreshWorkspaceCascade({
|
|
148
|
+
rootCwd: root,
|
|
149
|
+
scope,
|
|
150
|
+
onProgress: (progress) => {
|
|
151
|
+
const current = readCascadeRefreshJob(root, jobId) ?? job;
|
|
152
|
+
writeJob({
|
|
153
|
+
...current,
|
|
154
|
+
status: 'running',
|
|
155
|
+
pid: process.pid,
|
|
156
|
+
projects_total: progress.total,
|
|
157
|
+
projects_completed: progress.completed,
|
|
158
|
+
current_project: progress.current_project,
|
|
159
|
+
...(progress.last_result ? { last_result: progress.last_result } : {}),
|
|
160
|
+
updated_at: new Date().toISOString(),
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const current = readCascadeRefreshJob(root, jobId) ?? job;
|
|
165
|
+
const completed = new Date().toISOString();
|
|
166
|
+
writeJob({ ...current, status: 'completed', result, current_project: null, projects_completed: current.projects_total, completed_at: completed, updated_at: completed });
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const current = readCascadeRefreshJob(root, jobId) ?? job;
|
|
170
|
+
const completed = new Date().toISOString();
|
|
171
|
+
writeJob({ ...current, status: 'failed', error: error instanceof Error ? error.message : String(error), current_project: null, completed_at: completed, updated_at: completed });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=cascade-jobs.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { runCascadeRefreshJob } from './cascade-jobs.js';
|
|
2
|
+
const [root, jobId, rawScope] = process.argv.slice(2);
|
|
3
|
+
if (!root || !jobId)
|
|
4
|
+
process.exit(2);
|
|
5
|
+
const scope = rawScope === 'all' ? 'all' : 'changed';
|
|
6
|
+
try {
|
|
7
|
+
await runCascadeRefreshJob(root, jobId, scope);
|
|
8
|
+
// Tree-sitter/native handles can keep Node's event loop alive after the
|
|
9
|
+
// durable terminal record has been flushed. This process owns no other work.
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=cascade-worker.js.map
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* refreshes existing brainclaw projects, it does not initialise new ones.
|
|
21
21
|
*/
|
|
22
22
|
import path from 'node:path';
|
|
23
|
-
import {
|
|
23
|
+
import { scanNestedBrainclawProjectsDetailed } from '../workspace-projects.js';
|
|
24
24
|
import { loadConfig } from '../config.js';
|
|
25
25
|
import { refresh as runRefresh } from './refresh.js';
|
|
26
26
|
import { readManifest } from './store.js';
|
|
@@ -54,10 +54,14 @@ function projectIdFor(cwd, fallbackId) {
|
|
|
54
54
|
* cascade and the `status --cascade` recap so both agree on the project set.
|
|
55
55
|
*/
|
|
56
56
|
export function listNestedProjects(rootCwd) {
|
|
57
|
+
return inspectNestedProjects(rootCwd).projects;
|
|
58
|
+
}
|
|
59
|
+
export function inspectNestedProjects(rootCwd) {
|
|
57
60
|
const root = path.resolve(rootCwd);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const discovered = scanNestedBrainclawProjectsDetailed(root);
|
|
62
|
+
return { projects: Array.from(new Set(discovered.projects
|
|
63
|
+
.map((c) => path.resolve(c.path))
|
|
64
|
+
.filter((abs) => abs !== root && isStrictlyUnder(abs, root)))).sort(), truncated: discovered.truncated };
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Refresh the whole multi-project workspace: every nested brainclaw project +
|
|
@@ -68,7 +72,8 @@ export async function refreshWorkspaceCascade(input) {
|
|
|
68
72
|
const rootCwd = path.resolve(input.rootCwd);
|
|
69
73
|
// Enumerate nested brainclaw projects strictly under the root (FS scan, so it
|
|
70
74
|
// is strategy-agnostic). De-dup + sort by path for deterministic output.
|
|
71
|
-
const
|
|
75
|
+
const discovery = inspectNestedProjects(rootCwd);
|
|
76
|
+
const childAbsPaths = discovery.projects;
|
|
72
77
|
// Every project to refresh, root first.
|
|
73
78
|
const allProjects = [rootCwd, ...childAbsPaths];
|
|
74
79
|
const refreshOne = async (projectCwd, isRoot) => {
|
|
@@ -77,40 +82,72 @@ export async function refreshWorkspaceCascade(input) {
|
|
|
77
82
|
const nestedUnder = allProjects.filter((p) => p !== projectCwd && isStrictlyUnder(p, projectCwd));
|
|
78
83
|
const extraIgnorePatterns = nestedUnder.map((p) => `${toPosix(path.relative(projectCwd, p))}/**`);
|
|
79
84
|
const projectId = projectIdFor(projectCwd);
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
const projectPath = isRoot ? '.' : toPosix(path.relative(rootCwd, projectCwd));
|
|
86
|
+
try {
|
|
87
|
+
const result = await runRefresh({
|
|
88
|
+
projectId,
|
|
89
|
+
projectRoot: projectCwd,
|
|
90
|
+
scope: input.scope,
|
|
91
|
+
cwd: projectCwd,
|
|
92
|
+
extraIgnorePatterns,
|
|
93
|
+
ownerAgent: input.ownerAgent ?? null,
|
|
94
|
+
ownerAgentId: input.ownerAgentId ?? null,
|
|
95
|
+
});
|
|
96
|
+
const filesIndexed = readManifest(projectCwd)?.stats.files_indexed ?? 0;
|
|
97
|
+
const outcome = !result.lock_acquired
|
|
98
|
+
? 'locked'
|
|
99
|
+
: filesIndexed === 0 ? 'no_eligible_files' : 'indexed';
|
|
100
|
+
return {
|
|
101
|
+
path: projectPath,
|
|
102
|
+
project_id: projectId,
|
|
103
|
+
is_root: isRoot,
|
|
104
|
+
ran: result.ran,
|
|
105
|
+
lock_acquired: result.lock_acquired,
|
|
106
|
+
files_parsed: result.files_parsed,
|
|
107
|
+
files_compacted: result.files_compacted,
|
|
108
|
+
files_indexed: filesIndexed,
|
|
109
|
+
freshness: result.freshness.status,
|
|
110
|
+
outcome,
|
|
111
|
+
...(outcome === 'no_eligible_files' ? { reason: 'no eligible source files found' } : {}),
|
|
112
|
+
...(result.lock_status ? { lock_status: result.lock_status, reason: result.lock_status } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
path: projectPath,
|
|
118
|
+
project_id: projectId,
|
|
119
|
+
is_root: isRoot,
|
|
120
|
+
ran: false,
|
|
121
|
+
lock_acquired: false,
|
|
122
|
+
files_parsed: 0,
|
|
123
|
+
files_compacted: 0,
|
|
124
|
+
files_indexed: readManifest(projectCwd)?.stats.files_indexed ?? null,
|
|
125
|
+
freshness: 'partial',
|
|
126
|
+
outcome: 'failed',
|
|
127
|
+
reason: 'refresh_failed',
|
|
128
|
+
error: error instanceof Error ? error.message : String(error),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
100
131
|
};
|
|
101
132
|
// Children first, then the root (sequential — each holds its own project lock
|
|
102
133
|
// briefly; never blocks bclaw_work, rule 8).
|
|
103
134
|
const children = [];
|
|
135
|
+
const total = childAbsPaths.length + 1;
|
|
136
|
+
input.onProgress?.({ completed: 0, total, current_project: childAbsPaths[0] ? toPosix(path.relative(rootCwd, childAbsPaths[0])) : '.' });
|
|
104
137
|
for (const childCwd of childAbsPaths) {
|
|
105
|
-
|
|
138
|
+
const result = await refreshOne(childCwd, false);
|
|
139
|
+
children.push(result);
|
|
140
|
+
input.onProgress?.({ completed: children.length, total, current_project: children.length < childAbsPaths.length ? toPosix(path.relative(rootCwd, childAbsPaths[children.length])) : '.', last_result: result });
|
|
106
141
|
}
|
|
107
142
|
const rootResult = await refreshOne(rootCwd, true);
|
|
143
|
+
input.onProgress?.({ completed: total, total, current_project: null, last_result: rootResult });
|
|
108
144
|
return {
|
|
109
145
|
is_cascade: true,
|
|
110
146
|
root: rootCwd,
|
|
111
147
|
root_result: rootResult,
|
|
112
148
|
children,
|
|
113
149
|
children_refreshed: children.length,
|
|
150
|
+
discovery_truncated: discovery.truncated,
|
|
114
151
|
};
|
|
115
152
|
}
|
|
116
153
|
//# sourceMappingURL=cascade.js.map
|
|
@@ -232,7 +232,7 @@ export function isTestPath(p) {
|
|
|
232
232
|
/**
|
|
233
233
|
* Score a symbol index entry against the query. Matching is separator/case
|
|
234
234
|
* INSENSITIVE (pln#601): an exact NORMALIZED match scores highest, then prefix,
|
|
235
|
-
* then substring
|
|
235
|
+
* then substring; sub-token-only candidates score zero. Exported symbols + components/hooks
|
|
236
236
|
* get a small boost; test-file symbols are biased DOWN so a test helper never
|
|
237
237
|
* outranks the real definition of the same name (the Fable-audit brief-noise
|
|
238
238
|
* companion to the find defect). Exported for focused ranking tests.
|
|
@@ -250,7 +250,7 @@ export function scoreEntry(entry, query) {
|
|
|
250
250
|
else if (name.includes(q))
|
|
251
251
|
score += 3; // substring
|
|
252
252
|
else
|
|
253
|
-
|
|
253
|
+
return 0; // shared-token noise is not a match
|
|
254
254
|
score *= entry.score_hint; // exported (1.0) vs internal (0.8)
|
|
255
255
|
if (entry.subtype === 'component' || entry.subtype === 'hook')
|
|
256
256
|
score += 1;
|
|
@@ -367,6 +367,9 @@ export function findInStore(query, ctx, checker, acc) {
|
|
|
367
367
|
const confident = validateEntry(entry, checker, acc, root, maxBytes, ctx.cwd, ctx.preferredDirName);
|
|
368
368
|
if (!confident)
|
|
369
369
|
continue;
|
|
370
|
+
const score = scoreEntry(entry, query);
|
|
371
|
+
if (score <= 0)
|
|
372
|
+
continue;
|
|
370
373
|
ranked.push({
|
|
371
374
|
match: {
|
|
372
375
|
node_id: entry.node_id,
|
|
@@ -375,7 +378,7 @@ export function findInStore(query, ctx, checker, acc) {
|
|
|
375
378
|
file_id: entry.file_id,
|
|
376
379
|
kind: entry.kind,
|
|
377
380
|
subtype: entry.subtype ?? null,
|
|
378
|
-
score
|
|
381
|
+
score,
|
|
379
382
|
},
|
|
380
383
|
centrality: importCentrality(entry, resolutionIndex),
|
|
381
384
|
});
|
package/dist/core/context.js
CHANGED
|
@@ -29,6 +29,19 @@ import { isTrapActive, listOperationalTraps } from './traps.js';
|
|
|
29
29
|
import { buildEstimationReport } from '../commands/estimation-report.js';
|
|
30
30
|
import { detectStaleness } from './staleness.js';
|
|
31
31
|
export const CONTEXT_SCHEMA_VERSION = '1.2';
|
|
32
|
+
function verificationStatus(item) {
|
|
33
|
+
const verification = item.verification;
|
|
34
|
+
if (!verification)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (verification.outcome === 'fail')
|
|
37
|
+
return 'verification:fail';
|
|
38
|
+
if (verification.max_age_days !== undefined) {
|
|
39
|
+
const ageMs = Date.now() - new Date(verification.verified_at).getTime();
|
|
40
|
+
if (Number.isFinite(ageMs) && ageMs > verification.max_age_days * 86_400_000)
|
|
41
|
+
return 'verification:stale';
|
|
42
|
+
}
|
|
43
|
+
return 'verification:pass';
|
|
44
|
+
}
|
|
32
45
|
export function buildContext(options = {}) {
|
|
33
46
|
const requestedCwd = options.cwd ?? process.cwd();
|
|
34
47
|
const contextCwd = resolveContextStoreCwd(requestedCwd, options.target);
|
|
@@ -95,7 +108,7 @@ export function buildContext(options = {}) {
|
|
|
95
108
|
related_paths: c.related_paths,
|
|
96
109
|
score: 0,
|
|
97
110
|
reasons: [],
|
|
98
|
-
extra: c.status,
|
|
111
|
+
extra: [c.status, verificationStatus(c)].filter(Boolean).join(', '),
|
|
99
112
|
plan_id: c.plan_id,
|
|
100
113
|
provenance: {
|
|
101
114
|
actor: c.author,
|
|
@@ -125,7 +138,7 @@ export function buildContext(options = {}) {
|
|
|
125
138
|
related_paths: d.related_paths,
|
|
126
139
|
score: 0,
|
|
127
140
|
reasons: [],
|
|
128
|
-
extra: d.related_paths?.join(', '),
|
|
141
|
+
extra: [d.related_paths?.join(', '), verificationStatus(d)].filter(Boolean).join(', ') || undefined,
|
|
129
142
|
plan_id: d.plan_id,
|
|
130
143
|
provenance: {
|
|
131
144
|
actor: d.author,
|
|
@@ -155,7 +168,7 @@ export function buildContext(options = {}) {
|
|
|
155
168
|
related_paths: t.related_paths,
|
|
156
169
|
score: 0,
|
|
157
170
|
reasons: [],
|
|
158
|
-
extra: `${t.severity}, visibility:${t.visibility ?? 'shared'}`,
|
|
171
|
+
extra: [`${t.severity}, visibility:${t.visibility ?? 'shared'}, status:${t.status}`, verificationStatus(t)].filter(Boolean).join(', '),
|
|
159
172
|
plan_id: t.plan_id,
|
|
160
173
|
provenance: {
|
|
161
174
|
actor: t.author,
|
|
@@ -28,9 +28,9 @@ import { loadClaim } from './claims.js';
|
|
|
28
28
|
import { getLoop, listLoops } from './loops/store.js';
|
|
29
29
|
import { isProcessAlive } from './agentrun-reconciler.js';
|
|
30
30
|
import { findRuntimeNoteById } from './runtime.js';
|
|
31
|
-
import { latestActivityMs, decodeOemAwareBuffer, getRuntimeLogPath, getRuntimeSignalPath } from './runtime-signals.js';
|
|
31
|
+
import { latestActivityMs, decodeOemAwareBuffer, getRuntimeLogPath, getRuntimeSignalPath, readCompletionSignals } from './runtime-signals.js';
|
|
32
32
|
import { currentAttemptRunIdForAssignment } from './loops/attempt-reservation.js';
|
|
33
|
-
import {
|
|
33
|
+
import { resolveLaneResultFile } from './lane-result-file.js';
|
|
34
34
|
const DEFAULT_TAIL = 20;
|
|
35
35
|
const DEFAULT_STALL_MS = 5 * 60_000;
|
|
36
36
|
const DEFAULT_BASE_REF = 'master';
|
|
@@ -291,6 +291,23 @@ function computeDiagnosis(assignment, agentRun, runtime, options) {
|
|
|
291
291
|
: `Worker reported "${lr.status}". Read the LANE-RESULT summary + stderr; address the blocker or reroute.`,
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
|
+
if (runtime.terminal_signal) {
|
|
295
|
+
const signal = runtime.terminal_signal;
|
|
296
|
+
if (signal.status === 'contradictory') {
|
|
297
|
+
return {
|
|
298
|
+
health: 'unknown',
|
|
299
|
+
summary: 'both completed and failed terminal sentinels exist; outcome is contradictory and no terminal projection was inferred',
|
|
300
|
+
recommended_next_action: 'Inspect LANE-RESULT.json and both log files; preserve the worktree and reconcile the contradiction before retrying.',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
health: 'terminal',
|
|
305
|
+
summary: `worker wrapper emitted the canonical ${signal.status} terminal signal${agentRun && !TERMINAL_RUN_STATUSES.has(agentRun.status) ? ` (agent_run still ${agentRun.status})` : ''}`,
|
|
306
|
+
recommended_next_action: signal.status === 'completed'
|
|
307
|
+
? 'Run bclaw_harvest (or `brainclaw harvest <assignment_id>`) to ingest LANE-RESULT and converge the Assignment/Claim.'
|
|
308
|
+
: 'Read stderr and LANE-RESULT if present, then replay or reroute the failed slot.',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
294
311
|
// pln#554 — git evidence is the #2 signal, ABOVE process sentinels and
|
|
295
312
|
// administrative status: commits ahead of base with a clean tracked tree
|
|
296
313
|
// means the worker delivered everything to the branch, even if its pid is
|
|
@@ -359,18 +376,11 @@ function computeDiagnosis(assignment, agentRun, runtime, options) {
|
|
|
359
376
|
?? 'Read .stderr.log for the exit reason; then trigger reconciliation by calling bclaw_find(entity="agent_run") again, or cancel + reroute.',
|
|
360
377
|
};
|
|
361
378
|
}
|
|
362
|
-
if (runtime.pid_alive === true && stallAge > options.stallMs && fsActive) {
|
|
363
|
-
return {
|
|
364
|
-
health: 'healthy',
|
|
365
|
-
summary: `agent_run alive (pid=${runtime.pid}); last_event_at stale (${Math.round(stallAge / 1000)}s) but filesystem active ${Math.round((fsAge ?? 0) / 1000)}s ago — working through a long op without a heartbeat`,
|
|
366
|
-
recommended_next_action: 'No action — the worker is actively writing to logs/worktree. Re-check periodically until terminal.',
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
379
|
if (runtime.pid_alive === true && stallAge > options.stallMs) {
|
|
370
380
|
return {
|
|
371
381
|
health: 'stalled',
|
|
372
|
-
summary: `agent_run
|
|
373
|
-
recommended_next_action: '
|
|
382
|
+
summary: `agent_run pid=${runtime.pid} is alive but no explicit progress/heartbeat arrived for ${Math.round(stallAge / 1000)}s${fsActive ? `; filesystem activity ${Math.round((fsAge ?? 0) / 1000)}s ago is context, not proof of worker progress` : ''}`,
|
|
383
|
+
recommended_next_action: 'Inspect stdout/stderr and the expected artifact path. If no phase artifact or progress heartbeat is advancing, replay/reroute the slot; do not treat PID or unrelated filesystem activity as health.',
|
|
374
384
|
};
|
|
375
385
|
}
|
|
376
386
|
if (runtime.pid_alive === true) {
|
|
@@ -436,6 +446,9 @@ export function getDispatchStatus(options) {
|
|
|
436
446
|
const stderrPath = assignmentId
|
|
437
447
|
? getRuntimeLogPath(projectRoot, assignmentId, 'stderr', runtimeRunId)
|
|
438
448
|
: undefined;
|
|
449
|
+
const completionSignals = assignmentId
|
|
450
|
+
? readCompletionSignals(projectRoot, assignmentId, runtimeRunId)
|
|
451
|
+
: {};
|
|
439
452
|
// pln#527 — filesystem-activity age: max mtime across the captured logs + the
|
|
440
453
|
// run's worktree files (skipping junctions). The truer liveness signal when
|
|
441
454
|
// the heartbeat / last_event_at is stale during a long single operation.
|
|
@@ -460,8 +473,9 @@ export function getDispatchStatus(options) {
|
|
|
460
473
|
let laneResult;
|
|
461
474
|
let laneResultStale;
|
|
462
475
|
if (worktreeForFs) {
|
|
463
|
-
|
|
464
|
-
|
|
476
|
+
const resolvedLaneResult = resolveLaneResultFile(worktreeForFs, assignmentId);
|
|
477
|
+
if (resolvedLaneResult.kind === 'found') {
|
|
478
|
+
const parsed = resolvedLaneResult.lane;
|
|
465
479
|
if (parsed.assignment_id === assignmentId) {
|
|
466
480
|
laneResult = { status: parsed.status, summary: parsed.summary };
|
|
467
481
|
}
|
|
@@ -469,7 +483,6 @@ export function getDispatchStatus(options) {
|
|
|
469
483
|
laneResultStale = { assignment_id: parsed.assignment_id, status: parsed.status, summary: parsed.summary };
|
|
470
484
|
}
|
|
471
485
|
}
|
|
472
|
-
catch { /* no / invalid LANE-RESULT.json */ }
|
|
473
486
|
}
|
|
474
487
|
// pln#554 — worktree git evidence (commits ahead of base + dirty tracked files).
|
|
475
488
|
const evidence = gitEvidence(worktreeForFs, options.base_ref ?? DEFAULT_BASE_REF);
|
|
@@ -480,6 +493,15 @@ export function getDispatchStatus(options) {
|
|
|
480
493
|
exists: ackPath ? fs.existsSync(ackPath) : false,
|
|
481
494
|
path: ackPath,
|
|
482
495
|
},
|
|
496
|
+
...(assignmentId && (completionSignals.completed || completionSignals.failed) ? {
|
|
497
|
+
terminal_signal: {
|
|
498
|
+
status: completionSignals.completed && completionSignals.failed
|
|
499
|
+
? 'contradictory'
|
|
500
|
+
: completionSignals.completed ? 'completed' : 'failed',
|
|
501
|
+
completed_path: getRuntimeSignalPath(projectRoot, assignmentId, 'completed', runtimeRunId),
|
|
502
|
+
failed_path: getRuntimeSignalPath(projectRoot, assignmentId, 'failed', runtimeRunId),
|
|
503
|
+
},
|
|
504
|
+
} : {}),
|
|
483
505
|
log_files: {
|
|
484
506
|
stdout: stdoutPath ? readLogTail(stdoutPath, tailLines) : undefined,
|
|
485
507
|
stderr: stderrPath ? readLogTail(stderrPath, tailLines) : undefined,
|