entroly-wasm 1.0.86 → 1.0.87
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/js/auto_index.js +2 -1
- package/js/cli.js +30 -13
- package/js/server.js +96 -2
- package/js/work_graph_continuity.js +50 -2
- package/js/work_graph_inbox.js +81 -0
- package/js/work_graph_recovery_ack.js +81 -0
- package/js/work_graph_review.js +222 -0
- package/package.json +11 -4
- package/pkg/entroly_wasm_bg.wasm +0 -0
package/js/auto_index.js
CHANGED
|
@@ -47,6 +47,7 @@ const SUPPORTED_EXTENSIONS = new Set([
|
|
|
47
47
|
const SKIP_PATTERNS = new Set([
|
|
48
48
|
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Cargo.lock',
|
|
49
49
|
'poetry.lock', 'Pipfile.lock', 'composer.lock', 'Gemfile.lock',
|
|
50
|
+
'bun.lock', 'bun.lockb',
|
|
50
51
|
'.DS_Store', 'thumbs.db',
|
|
51
52
|
]);
|
|
52
53
|
|
|
@@ -59,7 +60,7 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
59
60
|
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
60
61
|
'.ttf', '.otf', '.woff', '.woff2', '.eot',
|
|
61
62
|
'.db', '.sqlite', '.sqlite3',
|
|
62
|
-
'.dat', '.pak', '.map',
|
|
63
|
+
'.dat', '.pak', '.map', '.lockb',
|
|
63
64
|
]);
|
|
64
65
|
|
|
65
66
|
const MAX_FILE_BYTES = 50 * 1024;
|
package/js/cli.js
CHANGED
|
@@ -46,6 +46,16 @@ function banner() {
|
|
|
46
46
|
return `${C.CYAN}${C.BOLD} Entroly${C.RESET} v${VERSION} — information-theoretic context optimization`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function detectRunner() {
|
|
50
|
+
const isBun = Boolean(process.versions && process.versions.bun);
|
|
51
|
+
return {
|
|
52
|
+
isBun,
|
|
53
|
+
runner: isBun ? 'bunx' : 'npx',
|
|
54
|
+
runnerPrefix: isBun ? 'bunx --bun' : 'npx',
|
|
55
|
+
runnerArgs: isBun ? ['--bun', 'entroly-wasm', '--serve'] : ['-y', 'entroly-wasm', '--serve'],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
// ── Commands ──
|
|
50
60
|
|
|
51
61
|
function cmdServe() {
|
|
@@ -151,18 +161,20 @@ function cmdInit() {
|
|
|
151
161
|
console.log(banner());
|
|
152
162
|
console.log();
|
|
153
163
|
|
|
164
|
+
const { isBun, runner, runnerPrefix, runnerArgs } = detectRunner();
|
|
165
|
+
|
|
154
166
|
// Detect IDE
|
|
155
167
|
const cwd = process.cwd();
|
|
156
168
|
const cursorDir = path.join(cwd, '.cursor');
|
|
157
169
|
const vscodeDir = path.join(cwd, '.vscode');
|
|
158
170
|
|
|
159
171
|
if (fs.existsSync(cursorDir) || fs.existsSync(path.join(os.homedir(), '.cursor'))) {
|
|
160
|
-
console.log(`${C.GREEN}✓${C.RESET} Detected: ${C.BOLD}Cursor${C.RESET}`);
|
|
172
|
+
console.log(`${C.GREEN}✓${C.RESET} Detected: ${C.BOLD}Cursor${C.RESET}${isBun ? ` ${C.CYAN}(using bunx)${C.RESET}` : ''}`);
|
|
161
173
|
const mcpConfig = {
|
|
162
174
|
mcpServers: {
|
|
163
175
|
entroly: {
|
|
164
|
-
command:
|
|
165
|
-
args:
|
|
176
|
+
command: runner,
|
|
177
|
+
args: runnerArgs,
|
|
166
178
|
},
|
|
167
179
|
},
|
|
168
180
|
};
|
|
@@ -186,19 +198,19 @@ function cmdInit() {
|
|
|
186
198
|
}
|
|
187
199
|
console.log(`${C.GREEN}✓${C.RESET} Written MCP config to ${C.CYAN}${configPath}${C.RESET}`);
|
|
188
200
|
} else if (fs.existsSync(vscodeDir)) {
|
|
189
|
-
console.log(`${C.GREEN}✓${C.RESET} Detected: ${C.BOLD}VS Code${C.RESET}`);
|
|
201
|
+
console.log(`${C.GREEN}✓${C.RESET} Detected: ${C.BOLD}VS Code${C.RESET}${isBun ? ` ${C.CYAN}(using bunx)${C.RESET}` : ''}`);
|
|
190
202
|
console.log(` Add to your MCP settings:`);
|
|
191
|
-
console.log(` ${C.CYAN}"entroly": { "command": "
|
|
203
|
+
console.log(` ${C.CYAN}"entroly": { "command": "${runner}", "args": ${JSON.stringify(runnerArgs)} }${C.RESET}`);
|
|
192
204
|
} else {
|
|
193
205
|
console.log(` No IDE detected. To use Entroly as an MCP server, run:`);
|
|
194
|
-
console.log(` ${C.CYAN}
|
|
206
|
+
console.log(` ${C.CYAN}${runner} ${runnerArgs.join(' ')}${C.RESET}`);
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
console.log();
|
|
198
210
|
console.log(` ${C.BOLD}Quick start:${C.RESET}`);
|
|
199
|
-
console.log(` ${C.CYAN}
|
|
200
|
-
console.log(` ${C.CYAN}
|
|
201
|
-
console.log(` ${C.CYAN}
|
|
211
|
+
console.log(` ${C.CYAN}${runnerPrefix} entroly-wasm optimize 8000 "fix the auth bug"${C.RESET}`);
|
|
212
|
+
console.log(` ${C.CYAN}${runnerPrefix} entroly-wasm health${C.RESET}`);
|
|
213
|
+
console.log(` ${C.CYAN}${runnerPrefix} entroly-wasm demo${C.RESET}`);
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
function cmdDemo() {
|
|
@@ -288,8 +300,12 @@ function cmdStatus() {
|
|
|
288
300
|
const config = new EntrolyConfig();
|
|
289
301
|
const indexPath = path.join(config.checkpointDir, 'index.json.gz');
|
|
290
302
|
const hasIndex = fs.existsSync(indexPath);
|
|
303
|
+
const runtime = (process.versions && process.versions.bun)
|
|
304
|
+
? `Bun v${process.versions.bun}`
|
|
305
|
+
: `Node.js ${process.version}`;
|
|
291
306
|
console.log(`${C.BOLD}Entroly Status${C.RESET}`);
|
|
292
307
|
console.log(` Version: ${VERSION}`);
|
|
308
|
+
console.log(` Runtime: ${runtime}`);
|
|
293
309
|
console.log(` Engine: Wasm (standalone)`);
|
|
294
310
|
console.log(` Index: ${hasIndex ? C.GREEN + '✓ persistent index found' + C.RESET : C.YELLOW + '⚠ no index (run optimize first)' + C.RESET}`);
|
|
295
311
|
console.log(` Checkpoint: ${config.checkpointDir}`);
|
|
@@ -354,6 +370,7 @@ function cmdActivation(args) {
|
|
|
354
370
|
}
|
|
355
371
|
|
|
356
372
|
function cmdHelp() {
|
|
373
|
+
const { runnerPrefix } = detectRunner();
|
|
357
374
|
console.log(banner());
|
|
358
375
|
console.log();
|
|
359
376
|
console.log(` ${C.BOLD}Usage:${C.RESET} entroly-wasm [command] [options]`);
|
|
@@ -374,10 +391,10 @@ function cmdHelp() {
|
|
|
374
391
|
console.log(` ${C.CYAN}clean${C.RESET} Clear cached state`);
|
|
375
392
|
console.log();
|
|
376
393
|
console.log(` ${C.BOLD}Examples:${C.RESET}`);
|
|
377
|
-
console.log(` ${C.GRAY}
|
|
378
|
-
console.log(` ${C.GRAY}
|
|
379
|
-
console.log(` ${C.GRAY}
|
|
380
|
-
console.log(` ${C.GRAY}
|
|
394
|
+
console.log(` ${C.GRAY}${runnerPrefix} entroly-wasm${C.RESET} ${C.GRAY}# auto-detect + start${C.RESET}`);
|
|
395
|
+
console.log(` ${C.GRAY}${runnerPrefix} entroly-wasm optimize 8000 "fix the auth bug"${C.RESET}`);
|
|
396
|
+
console.log(` ${C.GRAY}${runnerPrefix} entroly-wasm health${C.RESET}`);
|
|
397
|
+
console.log(` ${C.GRAY}${runnerPrefix} entroly-wasm gateways${C.RESET}`);
|
|
381
398
|
}
|
|
382
399
|
|
|
383
400
|
// ── Main ──
|
package/js/server.js
CHANGED
|
@@ -16,9 +16,23 @@ const { WorkspaceChangeListener } = require('./workspace');
|
|
|
16
16
|
const { SkillEngine } = require('./skills');
|
|
17
17
|
const { buildRepoMap, renderRepoMapMarkdown } = require('./repo_map');
|
|
18
18
|
const { ingestDiff, ingestDiagram, ingestVoice } = require('./multimodal');
|
|
19
|
+
const { discoverRepositoryIdentity } = require('./work_graph_repo');
|
|
20
|
+
const { WorkGraphStore } = require('./work_graph_store');
|
|
21
|
+
const { handoffRepositoryWithProof, resumeRepository, currentWorkspaceCommitment,
|
|
22
|
+
continuationOutstandingRefs } = require('./work_graph_continuity');
|
|
23
|
+
const { recordReview, reviews } = require('./work_graph_review');
|
|
24
|
+
const { pendingHandoffs, saveHandoff } = require('./work_graph_inbox');
|
|
25
|
+
const { acknowledge: acknowledgeRecovery, arm: armRecovery, requireAcknowledged } = require('./work_graph_recovery_ack');
|
|
19
26
|
const path = require('path');
|
|
20
27
|
const fs = require('fs');
|
|
21
28
|
|
|
29
|
+
const PUBLIC_TOOLS = new Set([
|
|
30
|
+
'checkpoint_state', 'get_stats', 'optimize_context', 'recall_relevant',
|
|
31
|
+
'remember_fragment', 'resume_state', 'verify_response',
|
|
32
|
+
'work_capabilities', 'work_state', 'work_claim', 'work_resume', 'work_acknowledge_recovery',
|
|
33
|
+
'work_handoff', 'work_pending_handoffs', 'work_record_review', 'work_reviews',
|
|
34
|
+
]);
|
|
35
|
+
|
|
22
36
|
function resolveProjectDirectory(root, candidate = '.') {
|
|
23
37
|
try {
|
|
24
38
|
const realRoot = fs.realpathSync(root);
|
|
@@ -161,9 +175,19 @@ class EntrolyMCPServer {
|
|
|
161
175
|
return resolved;
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
// ── MCP Tool Definitions
|
|
178
|
+
// ── MCP Tool Definitions ──
|
|
165
179
|
get tools() {
|
|
166
|
-
|
|
180
|
+
const all = [
|
|
181
|
+
// Work Graph uses the existing Rust-backed store; no second graph.
|
|
182
|
+
{ name: 'work_capabilities', description: 'Check the active repository and local Work Graph review capability.', inputSchema: { type: 'object', properties: { project: { type: 'string', default: '' } } } },
|
|
183
|
+
{ name: 'work_state', description: 'Inspect persisted work state without a model-generated summary.', inputSchema: { type: 'object', properties: { project: { type: 'string', default: '' } } } },
|
|
184
|
+
{ name: 'work_claim', description: 'Claim an explicit task and bounded scope.', inputSchema: { type: 'object', properties: { agent_id: { type: 'string' }, task_title: { type: 'string' }, task_id: { type: 'string', default: '' }, session_id: { type: 'string', default: '' }, scope_paths: { type: 'array', items: { type: 'string' }, default: [] }, project: { type: 'string', default: '' } }, required: ['agent_id', 'task_title'] } },
|
|
185
|
+
{ name: 'work_resume', description: 'Recover durable repository work and exact changed-file identities, optionally with a no-handoff proof.', inputSchema: { type: 'object', properties: { project: { type: 'string', default: '' }, workstream_id: { type: 'string', default: '' }, max_evidence: { type: 'integer', default: 128 }, to_agent: { type: 'string', default: '' } } } },
|
|
186
|
+
{ name: 'work_acknowledge_recovery', description: 'Accept responsibility for one recovered work state before claiming work.', inputSchema: { type: 'object', properties: { token: { type: 'string' }, project: { type: 'string', default: '' } }, required: ['token'] } },
|
|
187
|
+
{ name: 'work_handoff', description: 'Seal a graph-bound cross-agent handoff and continuation proof.', inputSchema: { type: 'object', properties: { from_agent: { type: 'string' }, to_agent: { type: 'string' }, workstream_id: { type: 'string' }, project: { type: 'string', default: '' } }, required: ['from_agent', 'to_agent', 'workstream_id'] } },
|
|
188
|
+
{ name: 'work_pending_handoffs', description: 'Find compact local handoffs addressed to one agent and check source freshness.', inputSchema: { type: 'object', properties: { to_agent: { type: 'string' }, project: { type: 'string', default: '' } }, required: ['to_agent'] } },
|
|
189
|
+
{ name: 'work_record_review', description: 'Record a reviewer verdict, findings, and caller-observed checks tied to the exact handoff and source state. Verdict remains an agent statement.', inputSchema: { type: 'object', properties: { handoff: { type: 'object' }, reviewer_id: { type: 'string' }, verdict: { type: 'string', enum: ['approve', 'changes_requested', 'comment'] }, findings: { type: 'array', items: { type: 'object' } }, summary: { type: 'string' }, checks: { type: 'array', items: { type: 'object' }, default: [] }, project: { type: 'string', default: '' } }, required: ['handoff', 'reviewer_id', 'verdict', 'findings', 'summary'] } },
|
|
190
|
+
{ name: 'work_reviews', description: 'List recorded reviews and whether their source state is still current.', inputSchema: { type: 'object', properties: { project: { type: 'string', default: '' }, workstream_id: { type: 'string', default: '' } } } },
|
|
167
191
|
// ── Core Engine Tools (12) ──
|
|
168
192
|
{ name: 'remember_fragment', description: 'Store a context fragment with automatic dedup and entropy scoring.', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Text content to store' }, source: { type: 'string', description: 'Origin label (e.g. file:utils.py)', default: '' }, token_count: { type: 'integer', description: 'Token count (auto if 0)', default: 0 }, is_pinned: { type: 'boolean', description: 'Always include in optimized context', default: false } }, required: ['content'] } },
|
|
169
193
|
{ name: 'optimize_context', description: 'Select the optimal context subset for a token budget. Uses IOS + PRISM RL + Channel Coding.', inputSchema: { type: 'object', properties: { token_budget: { type: 'integer', description: 'Max tokens (default 128K)', default: 128000 }, query: { type: 'string', description: 'Current task/query for semantic scoring', default: '' } } } },
|
|
@@ -210,10 +234,16 @@ class EntrolyMCPServer {
|
|
|
210
234
|
// ── Hallucination Detection Tool ──
|
|
211
235
|
{ name: 'verify_response', description: 'Verify an AI-generated response for hallucination. Computes entity coverage gap, hedging curvature, and entropy consistency. Returns fused_risk [0.0=safe, 1.0=hallucinated], verdict (pass/warn/flag), and flagged claims. All local — zero LLM calls.', inputSchema: { type: 'object', properties: { response: { type: 'string', description: 'The AI-generated text to verify' }, context: { type: 'string', description: 'Source context provided to the AI', default: '' }, prompt: { type: 'string', description: 'Original user prompt', default: '' } }, required: ['response'] } },
|
|
212
236
|
];
|
|
237
|
+
return process.env.ENTROLY_MCP_PROFILE === 'public'
|
|
238
|
+
? all.filter((tool) => PUBLIC_TOOLS.has(tool.name)) : all;
|
|
213
239
|
}
|
|
214
240
|
|
|
215
241
|
// ── Tool Handlers (36 tools) ──
|
|
216
242
|
handleTool(name, args) {
|
|
243
|
+
if (!this.tools.some((tool) => tool.name === name)) {
|
|
244
|
+
throw new Error(`Tool unavailable in this MCP profile: ${name}`);
|
|
245
|
+
}
|
|
246
|
+
if (name.startsWith('work_')) return this._handleWorkTool(name, args || {});
|
|
217
247
|
this._ensureWarm(); // lazy one-time index load; instant no-op once warm
|
|
218
248
|
switch (name) {
|
|
219
249
|
// ── Core Engine Tools ──
|
|
@@ -555,6 +585,70 @@ class EntrolyMCPServer {
|
|
|
555
585
|
}
|
|
556
586
|
}
|
|
557
587
|
|
|
588
|
+
_handleWorkTool(name, args) {
|
|
589
|
+
const repo = this._projectDir(args.project || '');
|
|
590
|
+
switch (name) {
|
|
591
|
+
case 'work_capabilities': {
|
|
592
|
+
const identity = discoverRepositoryIdentity(repo);
|
|
593
|
+
return { status: 'ok', kind: name, runtime: 'wasm', project_root: identity.root,
|
|
594
|
+
repo_id: identity.repo_id, review_schema: 1, storage: 'local_work_graph' };
|
|
595
|
+
}
|
|
596
|
+
case 'work_state': {
|
|
597
|
+
const store = WorkGraphStore.forRepository(repo);
|
|
598
|
+
const graph = store.load();
|
|
599
|
+
return { status: 'ok', kind: name, trust: 'untrusted_recovered_work_state',
|
|
600
|
+
summary: graph.summary(), unfinished: graph.unfinished(), coordination: graph.coordination(Date.now()) };
|
|
601
|
+
}
|
|
602
|
+
case 'work_claim': {
|
|
603
|
+
const store = WorkGraphStore.forRepository(repo);
|
|
604
|
+
requireAcknowledged(store);
|
|
605
|
+
const { graph, leaseId } = store.claimWork(repo, {
|
|
606
|
+
agentId: args.agent_id, taskTitle: args.task_title, taskId: args.task_id,
|
|
607
|
+
sessionId: args.session_id, scopePaths: args.scope_paths,
|
|
608
|
+
});
|
|
609
|
+
return { status: 'ok', kind: name, lease_id: leaseId, summary: graph.summary() };
|
|
610
|
+
}
|
|
611
|
+
case 'work_resume': {
|
|
612
|
+
const target = args.to_agent == null ? '' : String(args.to_agent).trim();
|
|
613
|
+
if (target.length > 512 || target.includes('\0')) throw new Error('invalid to_agent');
|
|
614
|
+
const view = resumeRepository(repo, {
|
|
615
|
+
workstreamId: args.workstream_id, maxEvidence: args.max_evidence,
|
|
616
|
+
});
|
|
617
|
+
const store = WorkGraphStore.forRepository(repo);
|
|
618
|
+
const acknowledgement = armRecovery(store, view);
|
|
619
|
+
const continuation_proof = target ? store.reconstructedContinuationProof(
|
|
620
|
+
view.selected_workstream.node_id, target, {
|
|
621
|
+
outstanding_work_refs: continuationOutstandingRefs(view),
|
|
622
|
+
created_at_ms: Date.now(),
|
|
623
|
+
}) : undefined;
|
|
624
|
+
return { status: 'ok', kind: name, trust: 'untrusted_recovered_work_state',
|
|
625
|
+
resume: view, acknowledgement, ...(continuation_proof ? { continuation_proof } : {}) };
|
|
626
|
+
}
|
|
627
|
+
case 'work_acknowledge_recovery':
|
|
628
|
+
return { status: 'ok', kind: name,
|
|
629
|
+
...acknowledgeRecovery(WorkGraphStore.forRepository(repo), args.token,
|
|
630
|
+
currentWorkspaceCommitment(repo)) };
|
|
631
|
+
case 'work_handoff': {
|
|
632
|
+
const bundle = handoffRepositoryWithProof(repo, {
|
|
633
|
+
fromAgent: args.from_agent, toAgent: args.to_agent, workstreamId: args.workstream_id,
|
|
634
|
+
});
|
|
635
|
+
const ref = saveHandoff(WorkGraphStore.forRepository(repo), bundle.handoff, bundle.continuation_proof);
|
|
636
|
+
return { status: 'ok', kind: name, ...bundle, handoff_ref: ref };
|
|
637
|
+
}
|
|
638
|
+
case 'work_pending_handoffs':
|
|
639
|
+
return pendingHandoffs(repo, WorkGraphStore.forRepository(repo), args.to_agent);
|
|
640
|
+
case 'work_record_review':
|
|
641
|
+
return recordReview(repo, {
|
|
642
|
+
handoff: args.handoff, reviewerId: args.reviewer_id, verdict: args.verdict,
|
|
643
|
+
findings: args.findings, summary: args.summary, checks: args.checks,
|
|
644
|
+
});
|
|
645
|
+
case 'work_reviews':
|
|
646
|
+
return reviews(repo, { workstreamId: args.workstream_id });
|
|
647
|
+
default:
|
|
648
|
+
throw new Error(`Unknown Work Graph tool: ${name}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
558
652
|
// ── JSON-RPC stdio transport ──
|
|
559
653
|
async run() {
|
|
560
654
|
this._log(`Starting Entroly MCP server v${this.config.serverVersion} (Wasm engine)`);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Repository observation and persistence happen here; all work-state meaning
|
|
5
5
|
// remains in shared Rust.
|
|
6
6
|
const { enrichWorktreeContentDigests } = require('./work_graph_content_digest');
|
|
7
|
+
const crypto = require('crypto');
|
|
7
8
|
const { discoverRepositoryObservation } = require('./work_graph_repo');
|
|
8
9
|
const { WorkGraphStateError, WorkGraphStore } = require('./work_graph_store');
|
|
9
10
|
|
|
@@ -47,6 +48,47 @@ function passiveObservation(repoPath, repositoryOptions) {
|
|
|
47
48
|
return enrichWorktreeContentDigests(repoPath, observation);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function canonical(value) {
|
|
52
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
53
|
+
if (value && typeof value === 'object') {
|
|
54
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
55
|
+
}
|
|
56
|
+
return JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function workspaceCommitment(observation) {
|
|
60
|
+
const stable = {
|
|
61
|
+
repo_id: observation.repo_id ?? null,
|
|
62
|
+
branch: observation.branch ?? null,
|
|
63
|
+
changes: observation.changes ?? null,
|
|
64
|
+
path_disclosure: observation.path_disclosure ?? null,
|
|
65
|
+
};
|
|
66
|
+
return `workspace:${crypto.createHash('sha256').update(canonical(stable), 'utf8').digest('hex')}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function currentWorkspaceCommitment(repoPath) {
|
|
70
|
+
return workspaceCommitment(passiveObservation(repoPath, {}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resumableWorkstreamId(store) {
|
|
74
|
+
const unfinished = store.load().unfinished();
|
|
75
|
+
const content = ['remaining_work', 'decision_ids', 'verification_ids', 'failure_ids', 'agent_ids'];
|
|
76
|
+
const selected = unfinished.find((item) => content.some((field) => Array.isArray(item[field]) && item[field].length))
|
|
77
|
+
|| unfinished[0];
|
|
78
|
+
return selected ? selected.node_id : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function attachCurrentWorktreeChanges(view, observation, maxPaths) {
|
|
82
|
+
const paths = [...new Set((observation.changes || []).map((item) => item.path).filter(Boolean))].sort();
|
|
83
|
+
return {
|
|
84
|
+
...view,
|
|
85
|
+
current_worktree_changed_paths: paths.slice(0, maxPaths),
|
|
86
|
+
current_worktree_change_count: paths.length,
|
|
87
|
+
current_worktree_changes_truncated: paths.length > maxPaths,
|
|
88
|
+
current_workspace_commitment: workspaceCommitment(observation),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
50
92
|
function resumeRepository(repoPath = '.', options = {}) {
|
|
51
93
|
// Validate before constructing the store or observing Git. Invalid recovery
|
|
52
94
|
// requests must not mutate shared state.
|
|
@@ -56,8 +98,10 @@ function resumeRepository(repoPath = '.', options = {}) {
|
|
|
56
98
|
const repositoryOptions = options.repositoryOptions || {};
|
|
57
99
|
|
|
58
100
|
const store = WorkGraphStore.forRepository(repoPath, storeOptions);
|
|
59
|
-
|
|
60
|
-
|
|
101
|
+
const observation = passiveObservation(repoPath, repositoryOptions);
|
|
102
|
+
store.submitObservation(observation);
|
|
103
|
+
const selected = workstreamId || resumableWorkstreamId(store);
|
|
104
|
+
return attachCurrentWorktreeChanges(store.resume(selected, maxEvidence), observation, maxEvidence);
|
|
61
105
|
}
|
|
62
106
|
|
|
63
107
|
function handoffRepository(repoPath = '.', options = {}) {
|
|
@@ -85,6 +129,8 @@ function continuationOutstandingRefs(view) {
|
|
|
85
129
|
? safeView.selected_workstream
|
|
86
130
|
: {};
|
|
87
131
|
const values = [
|
|
132
|
+
...(Array.isArray(safeView.outstanding_work) ? safeView.outstanding_work : []),
|
|
133
|
+
...(Array.isArray(safeView.current_worktree_changed_paths) ? safeView.current_worktree_changed_paths : []),
|
|
88
134
|
...(Array.isArray(safeView.changed_paths) ? safeView.changed_paths : []),
|
|
89
135
|
...(Array.isArray(safeView.failures) ? safeView.failures : []),
|
|
90
136
|
...(Array.isArray(selected.task_ids) ? selected.task_ids : []),
|
|
@@ -125,4 +171,6 @@ module.exports = {
|
|
|
125
171
|
handoffRepository,
|
|
126
172
|
handoffRepositoryWithProof,
|
|
127
173
|
resumeRepository,
|
|
174
|
+
currentWorkspaceCommitment,
|
|
175
|
+
continuationOutstandingRefs,
|
|
128
176
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Process-local host handoff transport. Work semantics stay in the Rust graph.
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { enrichWorktreeContentDigests } = require('./work_graph_content_digest');
|
|
8
|
+
const { discoverRepositoryObservation } = require('./work_graph_repo');
|
|
9
|
+
const { graphSourceState, handoffGraph, sourceState } = require('./work_graph_review');
|
|
10
|
+
const { WorkGraphStateError } = require('./work_graph_store');
|
|
11
|
+
|
|
12
|
+
const MAX_HANDOFF_BYTES = 256 * 1024;
|
|
13
|
+
const MAX_INBOX_ITEMS = 128;
|
|
14
|
+
|
|
15
|
+
function inbox(store) {
|
|
16
|
+
const folder = path.join(store.repoDir, 'handoffs');
|
|
17
|
+
fs.mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
18
|
+
const stat = fs.lstatSync(folder);
|
|
19
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
20
|
+
throw new WorkGraphStateError('unsafe Work Graph handoff inbox');
|
|
21
|
+
}
|
|
22
|
+
if (process.platform !== 'win32') fs.chmodSync(folder, 0o700);
|
|
23
|
+
return folder;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveHandoff(store, handoff, continuationProof) {
|
|
27
|
+
if (!store.load().verifyHandoff(handoff)) {
|
|
28
|
+
throw new WorkGraphStateError('cannot save a handoff that does not match the current graph');
|
|
29
|
+
}
|
|
30
|
+
const payload = { schema_version: 1, handoff, continuation_proof: continuationProof };
|
|
31
|
+
const encoded = Buffer.from(JSON.stringify(payload));
|
|
32
|
+
if (encoded.length > MAX_HANDOFF_BYTES) {
|
|
33
|
+
throw new WorkGraphStateError('handoff exceeds local inbox size limit');
|
|
34
|
+
}
|
|
35
|
+
const name = crypto.createHash('sha256').update(String(handoff.payload_commitment)).digest('hex');
|
|
36
|
+
const folder = inbox(store);
|
|
37
|
+
const target = path.join(folder, `${name}.json`);
|
|
38
|
+
const temporary = path.join(folder, `.${name}.${crypto.randomUUID().replace(/-/g, '')}.tmp`);
|
|
39
|
+
try {
|
|
40
|
+
const fd = fs.openSync(temporary, 'wx', 0o600);
|
|
41
|
+
try { fs.writeSync(fd, encoded); fs.fsyncSync(fd); }
|
|
42
|
+
finally { fs.closeSync(fd); }
|
|
43
|
+
fs.renameSync(temporary, target);
|
|
44
|
+
} finally {
|
|
45
|
+
try { fs.unlinkSync(temporary); } catch (_) {}
|
|
46
|
+
}
|
|
47
|
+
return `handoff:${name}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pendingHandoffs(root, store, toAgent) {
|
|
51
|
+
if (typeof toAgent !== 'string' || !toAgent.trim() || toAgent.length > 512 || toAgent.includes('\0')) {
|
|
52
|
+
throw new WorkGraphStateError('to_agent must be a bounded agent identifier');
|
|
53
|
+
}
|
|
54
|
+
const folder = inbox(store);
|
|
55
|
+
const files = fs.readdirSync(folder).filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
|
|
56
|
+
.map((name) => ({ name, stat: fs.lstatSync(path.join(folder, name)) }))
|
|
57
|
+
.filter(({ stat }) => stat.isFile() && !stat.isSymbolicLink() && stat.size <= MAX_HANDOFF_BYTES)
|
|
58
|
+
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs).slice(0, MAX_INBOX_ITEMS);
|
|
59
|
+
const graph = store.load();
|
|
60
|
+
const current = sourceState(store.repoId,
|
|
61
|
+
enrichWorktreeContentDigests(root, discoverRepositoryObservation(root)));
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const { name } of files) {
|
|
64
|
+
try {
|
|
65
|
+
const payload = JSON.parse(fs.readFileSync(path.join(folder, name), 'utf8'));
|
|
66
|
+
const handoff = payload.handoff;
|
|
67
|
+
if (payload.schema_version !== 1 || handoff.to_agent !== toAgent) continue;
|
|
68
|
+
let valid = false;
|
|
69
|
+
try { valid = current === graphSourceState(handoffGraph(graph, handoff), store.repoId); }
|
|
70
|
+
catch (_) { /* invalid receipts stay visible as stale */ }
|
|
71
|
+
entries.push({
|
|
72
|
+
ref: `handoff:${name.slice(0, -5)}`,
|
|
73
|
+
current: valid,
|
|
74
|
+
handoff, continuation_proof: payload.continuation_proof,
|
|
75
|
+
});
|
|
76
|
+
} catch (_) { /* unreadable or invalid records are not agent instructions */ }
|
|
77
|
+
}
|
|
78
|
+
return { status: 'ok', kind: 'work_pending_handoffs', to_agent: toAgent, handoffs: entries };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { pendingHandoffs, saveHandoff };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Same local recovery marker contract as entroly/work_graph_recovery_ack.py.
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { WorkGraphStateError } = require('./work_graph_store');
|
|
8
|
+
|
|
9
|
+
const MARKER = 'pending-recovery-ack.json';
|
|
10
|
+
const TRUST = 'untrusted_recovered_work_state';
|
|
11
|
+
|
|
12
|
+
function canonical(value) {
|
|
13
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
14
|
+
if (value && typeof value === 'object') {
|
|
15
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
16
|
+
}
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function token(view) {
|
|
21
|
+
return `recovery:${crypto.createHash('sha256').update(canonical(view), 'utf8').digest('hex').slice(0, 32)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function markerPath(store) { return path.join(store.repoDir, MARKER); }
|
|
25
|
+
|
|
26
|
+
function pending(store) {
|
|
27
|
+
let raw;
|
|
28
|
+
try { raw = fs.readFileSync(markerPath(store), 'utf8'); }
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === 'ENOENT') return null;
|
|
31
|
+
return { token: '', trust: TRUST, unknowns: ['unknown:gate-unreadable'] };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const record = JSON.parse(raw);
|
|
35
|
+
if (record && typeof record === 'object' && typeof record.token === 'string') return record;
|
|
36
|
+
} catch (_) { /* malformed markers remain armed */ }
|
|
37
|
+
return { token: '', trust: TRUST, unknowns: ['unknown:gate-corrupt'] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireAcknowledged(store) {
|
|
41
|
+
if (pending(store) !== null) {
|
|
42
|
+
throw new WorkGraphStateError('recovered work state has not been acknowledged; call work_acknowledge_recovery');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function arm(store, view) {
|
|
47
|
+
const value = token(view);
|
|
48
|
+
const unknowns = [...new Set([
|
|
49
|
+
'unknown:previous-agent-intent',
|
|
50
|
+
...(Array.isArray(view.unknowns) ? view.unknowns : []),
|
|
51
|
+
...(view.current_worktree_changes_truncated ? ['unknown:current-worktree-changes-truncated'] : []),
|
|
52
|
+
])].sort();
|
|
53
|
+
const record = { token: value, trust: TRUST, unknowns,
|
|
54
|
+
workspace_commitment: view.current_workspace_commitment };
|
|
55
|
+
fs.mkdirSync(store.repoDir, { recursive: true, mode: 0o700 });
|
|
56
|
+
const temporary = path.join(store.repoDir, `.ack-${crypto.randomUUID()}.tmp`);
|
|
57
|
+
try {
|
|
58
|
+
const fd = fs.openSync(temporary, 'wx', 0o600);
|
|
59
|
+
try { fs.writeSync(fd, Buffer.from(JSON.stringify(record))); fs.fsyncSync(fd); }
|
|
60
|
+
finally { fs.closeSync(fd); }
|
|
61
|
+
fs.renameSync(temporary, markerPath(store));
|
|
62
|
+
} finally {
|
|
63
|
+
try { fs.unlinkSync(temporary); } catch (_) {}
|
|
64
|
+
}
|
|
65
|
+
return { ...record, required: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function acknowledge(store, supplied, workspaceCommitment) {
|
|
69
|
+
const record = pending(store);
|
|
70
|
+
if (record === null) return { acknowledged: true, already_clear: true };
|
|
71
|
+
if (!record.token || typeof supplied !== 'string' || supplied.trim() !== record.token) {
|
|
72
|
+
throw new WorkGraphStateError('acknowledgement token does not match recovered state; call work_resume again');
|
|
73
|
+
}
|
|
74
|
+
if (record.workspace_commitment && workspaceCommitment !== record.workspace_commitment) {
|
|
75
|
+
throw new WorkGraphStateError('repository changed after recovery; call work_resume again');
|
|
76
|
+
}
|
|
77
|
+
fs.unlinkSync(markerPath(store));
|
|
78
|
+
return { acknowledged: true, token: record.token, trust: TRUST, unknowns: record.unknowns };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { acknowledge, arm, pending, requireAcknowledged };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Compact review records use the same Rust Work Graph event log as Python.
|
|
4
|
+
// Reviewer verdicts remain untrusted agent statements.
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { enrichWorktreeContentDigests } = require('./work_graph_content_digest');
|
|
9
|
+
const { discoverRepositoryIdentity, discoverRepositoryObservation } = require('./work_graph_repo');
|
|
10
|
+
const { WorkGraphStore, WorkGraphStateError } = require('./work_graph_store');
|
|
11
|
+
const { WorkGraph } = require('./work_graph');
|
|
12
|
+
const { workGraphNodeId, workGraphEdgeId } = require('../pkg/entroly_wasm');
|
|
13
|
+
|
|
14
|
+
const MAX_FINDINGS = 32;
|
|
15
|
+
const MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
const VERDICTS = new Set(['approve', 'changes_requested', 'comment']);
|
|
17
|
+
const SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'note']);
|
|
18
|
+
|
|
19
|
+
function bounded(value, name, limit = 1024) {
|
|
20
|
+
if (typeof value !== 'string' || !value.trim() || value.length > limit || value.includes('\0')) {
|
|
21
|
+
throw new WorkGraphStateError(`${name} must be nonempty text of at most ${limit} characters`);
|
|
22
|
+
}
|
|
23
|
+
return value.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function observation(root) {
|
|
27
|
+
return enrichWorktreeContentDigests(root, discoverRepositoryObservation(root));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sourceState(repoId, observed) {
|
|
31
|
+
const branch = observed.branch;
|
|
32
|
+
const changes = new Map((observed.changes || []).map((change) => [
|
|
33
|
+
String(change.path || ''), String(change.content_digest || ''),
|
|
34
|
+
]));
|
|
35
|
+
return sourceStateParts(repoId, branch.name || '', branch.head_sha || '', changes);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sourceStateParts(repoId, branch, head, changes) {
|
|
39
|
+
const lines = [repoId, String(branch), String(head)];
|
|
40
|
+
for (const [file, digest] of [...changes].sort(([a], [b]) => Buffer.compare(Buffer.from(a), Buffer.from(b)))) {
|
|
41
|
+
lines.push(`${file}\t${digest}`);
|
|
42
|
+
}
|
|
43
|
+
return `sha256:${crypto.createHash('sha256').update(lines.join('\n'), 'utf8').digest('hex')}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function graphSourceState(graph, repoId) {
|
|
47
|
+
for (const event of [...graph.exportState().events].reverse()) {
|
|
48
|
+
const operations = event.operations || [];
|
|
49
|
+
const repository = operations.find((op) => op.op === 'upsert_node' && op.node
|
|
50
|
+
&& op.node.kind === 'repository' && Object.hasOwn(op.node.attributes || {}, 'head_sha'));
|
|
51
|
+
if (!repository) continue;
|
|
52
|
+
const changes = new Map();
|
|
53
|
+
for (const op of operations) {
|
|
54
|
+
if (op.op === 'add_evidence' && op.evidence && op.evidence.kind === 'git_status') {
|
|
55
|
+
changes.set(String(op.evidence.locator), String(op.evidence.digest || ''));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return sourceStateParts(repoId, repository.node.attributes.branch || '',
|
|
59
|
+
repository.node.attributes.head_sha || '', changes);
|
|
60
|
+
}
|
|
61
|
+
throw new WorkGraphStateError('handoff graph contains no repository observation');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handoffGraph(graph, handoff) {
|
|
65
|
+
const revision = handoff && handoff.graph_revision;
|
|
66
|
+
if (!Number.isSafeInteger(revision) || revision < 1 || revision > graph.revision) {
|
|
67
|
+
throw new WorkGraphStateError('handoff is stale or has an invalid graph revision');
|
|
68
|
+
}
|
|
69
|
+
const historical = revision === graph.revision ? graph : new WorkGraph(graph.repoId);
|
|
70
|
+
if (historical !== graph) {
|
|
71
|
+
for (const event of graph.exportState().events.slice(0, revision)) historical.applyEvent(event);
|
|
72
|
+
}
|
|
73
|
+
if (!historical.verifyHandoff(handoff)) {
|
|
74
|
+
throw new WorkGraphStateError('handoff is stale or fails graph integrity verification');
|
|
75
|
+
}
|
|
76
|
+
return historical;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function checkedFindings(root, values) {
|
|
80
|
+
if (!Array.isArray(values) || values.length > MAX_FINDINGS) {
|
|
81
|
+
throw new WorkGraphStateError(`findings must be a list of at most ${MAX_FINDINGS} items`);
|
|
82
|
+
}
|
|
83
|
+
return values.map((item, index) => {
|
|
84
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
85
|
+
throw new WorkGraphStateError(`findings[${index}] must be an object`);
|
|
86
|
+
}
|
|
87
|
+
const rel = bounded(item.path, 'finding path', 4096).replace(/\\/g, '/');
|
|
88
|
+
const candidate = path.resolve(root, rel);
|
|
89
|
+
const real = fs.realpathSync(candidate);
|
|
90
|
+
const inside = path.relative(root, real);
|
|
91
|
+
if (!inside || inside === '..' || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside) || fs.lstatSync(candidate).isSymbolicLink()) {
|
|
92
|
+
throw new WorkGraphStateError('finding path must be a regular file inside the repository');
|
|
93
|
+
}
|
|
94
|
+
const stat = fs.statSync(real);
|
|
95
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) {
|
|
96
|
+
throw new WorkGraphStateError('finding path must be a readable file of at most 4 MiB');
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isSafeInteger(item.line) || item.line < 1) {
|
|
99
|
+
throw new WorkGraphStateError('finding line must be a positive integer');
|
|
100
|
+
}
|
|
101
|
+
const content = fs.readFileSync(real);
|
|
102
|
+
const text = content.toString('utf8');
|
|
103
|
+
const lineCount = text.length ? text.split(/\r\n|\n|\r/).length - (/[\r\n]$/.test(text) ? 1 : 0) : 0;
|
|
104
|
+
if (item.line > lineCount) throw new WorkGraphStateError('finding line is outside the current file');
|
|
105
|
+
const severity = bounded(item.severity, 'finding severity', 16);
|
|
106
|
+
if (!SEVERITIES.has(severity)) throw new WorkGraphStateError('finding severity is invalid');
|
|
107
|
+
return {
|
|
108
|
+
path: inside.split(path.sep).join('/'),
|
|
109
|
+
line: item.line,
|
|
110
|
+
severity,
|
|
111
|
+
message: bounded(item.message, 'finding message'),
|
|
112
|
+
source_digest: `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function checkedChecks(values) {
|
|
118
|
+
if (values == null) return [];
|
|
119
|
+
if (!Array.isArray(values) || values.length > 16) {
|
|
120
|
+
throw new WorkGraphStateError('checks must be a list of at most 16 items');
|
|
121
|
+
}
|
|
122
|
+
return values.map((item, index) => {
|
|
123
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
124
|
+
throw new WorkGraphStateError(`checks[${index}] must be an object`);
|
|
125
|
+
}
|
|
126
|
+
if (!Number.isSafeInteger(item.exit_code) || item.exit_code < -255 || item.exit_code > 255) {
|
|
127
|
+
throw new WorkGraphStateError('check exit_code must be an integer between -255 and 255');
|
|
128
|
+
}
|
|
129
|
+
return { name: bounded(item.name, 'check name', 256), exit_code: item.exit_code };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function canonical(value) {
|
|
134
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
135
|
+
if (value && typeof value === 'object') {
|
|
136
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
137
|
+
}
|
|
138
|
+
return JSON.stringify(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function recordReview(repoPath, { handoff, reviewerId, verdict, findings, summary, checks, storeOptions = {} }) {
|
|
142
|
+
const reviewer = bounded(reviewerId, 'reviewer_id', 512);
|
|
143
|
+
const choice = bounded(verdict, 'verdict', 32);
|
|
144
|
+
if (!VERDICTS.has(choice)) throw new WorkGraphStateError('invalid review verdict');
|
|
145
|
+
if (!handoff || typeof handoff !== 'object' || handoff.to_agent !== reviewer) {
|
|
146
|
+
throw new WorkGraphStateError('handoff must target the reviewer');
|
|
147
|
+
}
|
|
148
|
+
if (handoff.from_agent === reviewer) throw new WorkGraphStateError('writer and reviewer must differ');
|
|
149
|
+
const root = discoverRepositoryIdentity(repoPath).root;
|
|
150
|
+
const store = WorkGraphStore.forRepository(root, storeOptions);
|
|
151
|
+
const summaryText = bounded(summary, 'summary');
|
|
152
|
+
const validatedChecks = checkedChecks(checks);
|
|
153
|
+
const { result } = store.mutate((graph) => {
|
|
154
|
+
const sealed = handoffGraph(graph, handoff);
|
|
155
|
+
const observed = observation(root);
|
|
156
|
+
const state = sourceState(store.repoId, observed);
|
|
157
|
+
if (state !== graphSourceState(sealed, store.repoId)) {
|
|
158
|
+
throw new WorkGraphStateError('handoff is stale against current repository source');
|
|
159
|
+
}
|
|
160
|
+
const checked = checkedFindings(root, findings);
|
|
161
|
+
if (sourceState(store.repoId, observation(root)) !== state) {
|
|
162
|
+
throw new WorkGraphStateError('repository source changed while recording review');
|
|
163
|
+
}
|
|
164
|
+
if (choice === 'approve' && checked.some((finding) => ['critical', 'high'].includes(finding.severity))) {
|
|
165
|
+
throw new WorkGraphStateError('approve conflicts with blocking findings');
|
|
166
|
+
}
|
|
167
|
+
const payload = {
|
|
168
|
+
schema_version: 1,
|
|
169
|
+
record_kind: 'agent_review',
|
|
170
|
+
repo_id: store.repoId,
|
|
171
|
+
workstream_id: handoff.workstream_id,
|
|
172
|
+
handoff_commitment: handoff.payload_commitment,
|
|
173
|
+
writer_id: handoff.from_agent,
|
|
174
|
+
reviewer_id: reviewer,
|
|
175
|
+
source_state: state,
|
|
176
|
+
verdict: choice,
|
|
177
|
+
summary: summaryText,
|
|
178
|
+
findings: checked,
|
|
179
|
+
checks: validatedChecks,
|
|
180
|
+
trust_boundary: 'agent_statement; source digests observed; verdict unverified',
|
|
181
|
+
};
|
|
182
|
+
const reviewKey = `agent-review:${crypto.createHash('sha256').update(canonical(payload), 'utf8').digest('hex')}`;
|
|
183
|
+
const reviewId = workGraphNodeId('decision', store.repoId, reviewKey);
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
graph.applyEvent({
|
|
186
|
+
observed_at_ms: now,
|
|
187
|
+
source_kind: 'agent_statement',
|
|
188
|
+
source_ref: reviewId,
|
|
189
|
+
actor_id: reviewer,
|
|
190
|
+
operations: [{
|
|
191
|
+
op: 'upsert_node',
|
|
192
|
+
node: {
|
|
193
|
+
node_id: reviewId, kind: 'decision', label: 'agent review recorded',
|
|
194
|
+
trust: 'untrusted', status: 'completed', status_trust: 'untrusted',
|
|
195
|
+
attributes: payload, updated_at_ms: now,
|
|
196
|
+
},
|
|
197
|
+
}, {
|
|
198
|
+
op: 'upsert_edge',
|
|
199
|
+
edge: {
|
|
200
|
+
edge_id: workGraphEdgeId(reviewId, 'part_of', handoff.workstream_id),
|
|
201
|
+
from_node: reviewId, to_node: handoff.workstream_id,
|
|
202
|
+
kind: 'part_of', trust: 'untrusted', updated_at_ms: now,
|
|
203
|
+
},
|
|
204
|
+
}],
|
|
205
|
+
});
|
|
206
|
+
return { review_id: reviewId, ...payload };
|
|
207
|
+
});
|
|
208
|
+
return { status: 'ok', kind: 'work_record_review', review: result };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function reviews(repoPath, { workstreamId = '', storeOptions = {} } = {}) {
|
|
212
|
+
const root = discoverRepositoryIdentity(repoPath).root;
|
|
213
|
+
const store = WorkGraphStore.forRepository(root, storeOptions);
|
|
214
|
+
const current = sourceState(store.repoId, observation(root));
|
|
215
|
+
const records = store.load().snapshot().nodes
|
|
216
|
+
.filter((node) => node.attributes && node.attributes.record_kind === 'agent_review')
|
|
217
|
+
.filter((node) => !workstreamId || node.attributes.workstream_id === workstreamId)
|
|
218
|
+
.map((node) => ({ review_id: node.node_id, current: node.attributes.source_state === current, ...node.attributes }));
|
|
219
|
+
return { status: 'ok', kind: 'work_reviews', reviews: records };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { graphSourceState, handoffGraph, recordReview, reviews, sourceState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entroly-wasm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.87",
|
|
4
4
|
"description": "WebAssembly Context Assurance for AI agents: reduce avoidable token usage with local evidence selection, receipts, exact recovery, and verification.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -53,7 +53,10 @@
|
|
|
53
53
|
"js/work_context_snapshot_store.d.ts",
|
|
54
54
|
"js/work_graph_content_digest.js",
|
|
55
55
|
"js/work_graph_continuity.js",
|
|
56
|
-
"js/work_graph_continuity.d.ts"
|
|
56
|
+
"js/work_graph_continuity.d.ts",
|
|
57
|
+
"js/work_graph_review.js",
|
|
58
|
+
"js/work_graph_recovery_ack.js",
|
|
59
|
+
"js/work_graph_inbox.js"
|
|
57
60
|
],
|
|
58
61
|
"scripts": {
|
|
59
62
|
"build": "wasm-pack build --target nodejs --out-dir pkg",
|
|
@@ -64,10 +67,13 @@
|
|
|
64
67
|
"health": "node js/cli.js health",
|
|
65
68
|
"demo": "node js/cli.js demo",
|
|
66
69
|
"autotune": "node js/cli.js autotune",
|
|
67
|
-
"test": "node test_wasm_e2e.js && node test_activation.js && node test_work_graph.js && node test_work_graph_repo.js && node test_work_graph_store.js && node test_work_graph_content_digest.js && node test_work_graph_continuity.js && node test_context_trust_delivery.js && node test_work_graph_performance.js && node test_work_graph_root_exports.js && node test_context_receipt_parity.js"
|
|
70
|
+
"test": "node test_wasm_e2e.js && node test_activation.js && node test_work_graph.js && node test_work_graph_repo.js && node test_work_graph_store.js && node test_work_graph_content_digest.js && node test_work_graph_continuity.js && node test_work_graph_review.js && node test_context_trust_delivery.js && node test_work_graph_performance.js && node test_work_graph_root_exports.js && node test_context_receipt_parity.js && node test_bun_compat.js",
|
|
71
|
+
"test:bun": "bun test_wasm_e2e.js && bun test_activation.js && bun test_work_graph.js && bun test_work_graph_repo.js && bun test_work_graph_store.js && bun test_work_graph_content_digest.js && bun test_work_graph_continuity.js && bun test_work_graph_review.js && bun test_context_trust_delivery.js && bun test_work_graph_performance.js && bun test_work_graph_root_exports.js && bun test_context_receipt_parity.js && bun test_bun_compat.js"
|
|
68
72
|
},
|
|
69
73
|
"keywords": [
|
|
70
74
|
"entroly",
|
|
75
|
+
"bun",
|
|
76
|
+
"bunx",
|
|
71
77
|
"ai-agents",
|
|
72
78
|
"ai-coding-agents",
|
|
73
79
|
"context-assurance",
|
|
@@ -110,6 +116,7 @@
|
|
|
110
116
|
"author": "entroly",
|
|
111
117
|
"license": "Apache-2.0",
|
|
112
118
|
"engines": {
|
|
113
|
-
"node": ">=16.0.0"
|
|
119
|
+
"node": ">=16.0.0",
|
|
120
|
+
"bun": ">=1.4.0"
|
|
114
121
|
}
|
|
115
122
|
}
|
package/pkg/entroly_wasm_bg.wasm
CHANGED
|
Binary file
|