@oooooooooo/ch0de 0.0.0-stage → 1.9.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/API-TESTING.md +76 -0
  2. package/DEBTAUR-INTEGRATION.md +71 -0
  3. package/EDITING.md +80 -0
  4. package/EXPERT-PANEL.md +16 -0
  5. package/INTEGRATIONS.md +75 -0
  6. package/LICENSE +21 -0
  7. package/MODELS-DEV-LICENSE +21 -0
  8. package/PROVIDER-CONNECTIONS.md +48 -0
  9. package/PROVIDERS.md +17 -0
  10. package/README.md +523 -2
  11. package/RELIABILITY.md +95 -0
  12. package/UX-1.3.md +83 -0
  13. package/UX-1.4.1.md +13 -0
  14. package/UX-1.4.md +48 -0
  15. package/UX-1.5.md +13 -0
  16. package/UX-1.6.md +7 -0
  17. package/UX-1.7.3.md +13 -0
  18. package/UX-1.7.4.md +13 -0
  19. package/UX-1.7.md +19 -0
  20. package/acceptance-check.js +27 -0
  21. package/agent-budget.js +4 -0
  22. package/api-testing.js +140 -0
  23. package/automatic-re.js +19 -0
  24. package/benchmarks/public-baselines.json +29 -0
  25. package/benchmarks/render-1.4.0.json +8 -0
  26. package/benchmarks/targets.js +11 -0
  27. package/browser-check.js +1062 -0
  28. package/build-progress.js +10 -0
  29. package/build-quality.js +49 -0
  30. package/chode.js +7937 -0
  31. package/debtaur-engine.js +305 -0
  32. package/debtaur-orchestration.js +456 -0
  33. package/debtaur-worker.cjs +76 -0
  34. package/doctor.js +24 -0
  35. package/edit-transactions.js +79 -0
  36. package/elapsed.js +28 -0
  37. package/evidence-ledger.js +26 -0
  38. package/expert-panel.cjs +42 -0
  39. package/helios-ledger.js +405 -0
  40. package/install.cjs +25 -0
  41. package/integrations.js +30 -0
  42. package/json-output.js +8 -0
  43. package/local-memory.js +613 -0
  44. package/local-models.js +52 -0
  45. package/mcp-client.js +50 -0
  46. package/memory.js +511 -0
  47. package/model-catalog.js +46 -0
  48. package/native-tools.js +27 -0
  49. package/nosignups.js +325 -0
  50. package/observation-progress.js +33 -0
  51. package/orchestrator.js +299 -0
  52. package/package.json +160 -6
  53. package/plan-z6.js +490 -0
  54. package/project-memory.js +27 -0
  55. package/provider-capacity.js +13 -0
  56. package/provider-catalog.js +246 -0
  57. package/provider-connect.js +66 -0
  58. package/provider-credentials.js +5 -0
  59. package/provider-directory.js +8 -0
  60. package/provider-directory.json +32300 -0
  61. package/provider-errors.js +11 -0
  62. package/provider-picker.js +188 -0
  63. package/provider-policy.js +4 -0
  64. package/provider-race.js +25 -0
  65. package/provider-request.js +45 -0
  66. package/provider-store.js +13 -0
  67. package/rea-environment.js +5 -0
  68. package/read-window.js +21 -0
  69. package/recovery-context.js +3 -0
  70. package/reliability-memory.js +38 -0
  71. package/reliability-services.js +34 -0
  72. package/reliability-store.js +16 -0
  73. package/request-deadline.js +4 -0
  74. package/serve-frontend.html +172 -0
  75. package/settings-reset.js +11 -0
  76. package/skill-catalog.js +16 -0
  77. package/task-group.js +22 -0
  78. package/terminal-graphics.js +39 -0
  79. package/terminal-logo.js +76 -0
  80. package/terminal-ui.js +79 -0
  81. package/tool-call-validation.js +26 -0
  82. package/tool-health.js +22 -0
  83. package/tool-history.js +5 -0
  84. package/toolbox.js +156 -0
  85. package/tui-provider-picker.js +134 -0
  86. package/tui-worker.js +32 -0
  87. package/web-context.js +232 -0
  88. package/wolfram-math.js +6 -0
  89. package/worker-transport.js +11 -0
  90. package/workspace-commands.js +45 -0
  91. package/workspace-log.js +56 -0
  92. package/workspace-store.js +7 -0
  93. package/workspace-tui.js +113 -0
  94. package/workspace-view.js +109 -0
  95. package/worktrees.js +18 -0
  96. package/z6-integration.js +227 -0
@@ -0,0 +1,456 @@
1
+ "use strict";
2
+ // Host-owned evidence, not model-authored propositions. See DEBTAUR-INTEGRATION.md.
3
+ const fs = require("fs"),
4
+ path = require("path"),
5
+ crypto = require("crypto"),
6
+ engine = require("./debtaur-engine");
7
+ const hash = engine.hash;
8
+ const RULES = Object.freeze({
9
+ dispatch: {
10
+ rules: [
11
+ "if the scope is valid and the owner is current then the worker may start",
12
+ ],
13
+ goal: "the worker may start",
14
+ facts: ["the scope is valid", "the owner is current"],
15
+ },
16
+ handoff: {
17
+ rules: [
18
+ "if the owner is current and the artifact is current then the handoff is checked",
19
+ "if the handoff is checked and the check is passed then the handoff is ready",
20
+ ],
21
+ goal: "the handoff is ready",
22
+ facts: [
23
+ "the owner is current",
24
+ "the artifact is current",
25
+ "the check is passed",
26
+ ],
27
+ },
28
+ verify: {
29
+ rules: [
30
+ "if the artifact is current and the check is passed then the evidence is checked",
31
+ "if the evidence is checked and the workers are settled then the evidence is usable",
32
+ ],
33
+ goal: "the evidence is usable",
34
+ facts: [
35
+ "the artifact is current",
36
+ "the check is passed",
37
+ "the workers are settled",
38
+ ],
39
+ },
40
+ });
41
+ function create(root, options = {}) {
42
+ root = fs.realpathSync(root);
43
+ const client = options.client || engine.create(options),
44
+ run = crypto.randomUUID(),
45
+ taskHash = hash(
46
+ JSON.stringify([
47
+ options.goal || "",
48
+ options.session || "default",
49
+ options.acceptanceDigest || "",
50
+ ]),
51
+ );
52
+ const owners = new Map(),
53
+ leases = new Map(),
54
+ events = [];
55
+ let sequence = 0,
56
+ decisionCount = 0,
57
+ head = "0".repeat(64),
58
+ lastProof = null,
59
+ lastDecision = null,
60
+ storageWarning = null;
61
+ const relative = path.join(".chode", "orchestration", run + ".json");
62
+ function fileName(file) {
63
+ if (typeof file !== "string" || !file || /[\x00-\x1f]/.test(file))
64
+ throw Error("Invalid artifact path");
65
+ const full = path.resolve(root, file),
66
+ rel = path.relative(root, full);
67
+ if (
68
+ !rel ||
69
+ rel === ".." ||
70
+ rel.startsWith(".." + path.sep) ||
71
+ path.isAbsolute(rel) ||
72
+ /(^|[/\\])(?:\.chode|\.git|node_modules|\.env(?:\.[^/\\]*)?|credentials?)(?:[/\\]|$)/i.test(
73
+ rel,
74
+ ) ||
75
+ (options.allowed && !options.allowed(full))
76
+ )
77
+ throw Error("Artifact is outside the permitted source scope");
78
+ let at = root;
79
+ for (const segment of rel.split(path.sep)) {
80
+ at = path.join(at, segment);
81
+ try {
82
+ if (fs.lstatSync(at).isSymbolicLink())
83
+ throw Error("Linked artifact path");
84
+ } catch (error) {
85
+ if (error.code === "ENOENT") break;
86
+ throw error;
87
+ }
88
+ }
89
+ return {
90
+ full,
91
+ rel: rel.split(path.sep).join("/"),
92
+ key: process.platform === "win32" ? full.toLowerCase() : full,
93
+ };
94
+ }
95
+ function snapshot(files, allowMissing = false) {
96
+ const rows = [];
97
+ let total = 0;
98
+ try {
99
+ if (!Array.isArray(files) || files.length > 256)
100
+ throw Error("Artifact set exceeds256 files");
101
+ const names = [...new Set(files)]
102
+ .map(fileName)
103
+ .sort((a, b) => a.rel.localeCompare(b.rel, "en"));
104
+ for (const item of names) {
105
+ let fd;
106
+ try {
107
+ fd = fs.openSync(
108
+ item.full,
109
+ fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0),
110
+ );
111
+ const st = fs.fstatSync(fd);
112
+ if (
113
+ !st.isFile() ||
114
+ st.nlink > 1 ||
115
+ st.size > 16 * 1024 * 1024 ||
116
+ (total += st.size) > 64 * 1024 * 1024
117
+ )
118
+ throw Error("Artifact cannot be bounded safely");
119
+ // Read at most the observed size plus one byte. A concurrently
120
+ // growing artifact must not turn a bounded hash into an unbounded read.
121
+ const buffer = Buffer.alloc(st.size + 1);
122
+ let length = 0;
123
+ while (length < buffer.length) {
124
+ const read = fs.readSync(
125
+ fd,
126
+ buffer,
127
+ length,
128
+ buffer.length - length,
129
+ length,
130
+ );
131
+ if (!read) break;
132
+ length += read;
133
+ }
134
+ const bytes = buffer.subarray(0, length);
135
+ const after = fs.fstatSync(fd);
136
+ if (
137
+ bytes.length !== st.size ||
138
+ after.mtimeMs !== st.mtimeMs ||
139
+ after.size !== st.size
140
+ )
141
+ throw Error("Artifact changed while hashing");
142
+ rows.push({ file: item.rel, hash: hash(bytes) });
143
+ } catch (error) {
144
+ if (error.code === "ENOENT" && allowMissing)
145
+ rows.push({ file: item.rel, hash: null });
146
+ else throw error;
147
+ } finally {
148
+ if (fd !== undefined) fs.closeSync(fd);
149
+ }
150
+ }
151
+ return { ok: true, files: rows, hash: hash(JSON.stringify(rows)) };
152
+ } catch (error) {
153
+ return { ok: false, reason: error.message, files: [] };
154
+ }
155
+ }
156
+ function persist() {
157
+ if (options.noMemory) return;
158
+ try {
159
+ let at = root;
160
+ for (const part of [".chode", "orchestration"]) {
161
+ at = path.join(at, part);
162
+ try {
163
+ const st = fs.lstatSync(at);
164
+ if (st.isSymbolicLink() || !st.isDirectory())
165
+ throw Error("Unsafe orchestration storage");
166
+ } catch (error) {
167
+ if (error.code !== "ENOENT") throw error;
168
+ fs.mkdirSync(at, { mode: 0o700 });
169
+ }
170
+ }
171
+ const file = path.join(root, relative);
172
+ try {
173
+ const st = fs.lstatSync(file);
174
+ if (st.isSymbolicLink() || st.nlink > 1 || !st.isFile())
175
+ throw Error("Unsafe orchestration report");
176
+ } catch (error) {
177
+ if (error.code !== "ENOENT") throw error;
178
+ }
179
+ const temp = file + "." + crypto.randomBytes(6).toString("hex") + ".tmp";
180
+ try {
181
+ fs.writeFileSync(
182
+ temp,
183
+ JSON.stringify(
184
+ {
185
+ schema: 1,
186
+ policy: "chode-evidence-v1",
187
+ run,
188
+ taskHash,
189
+ head,
190
+ sequence,
191
+ events,
192
+ },
193
+ null,
194
+ 2,
195
+ ) + "\n",
196
+ { flag: "wx", mode: 0o600 },
197
+ );
198
+ fs.renameSync(temp, file);
199
+ } finally {
200
+ try {
201
+ fs.unlinkSync(temp);
202
+ } catch {}
203
+ }
204
+ } catch (error) {
205
+ storageWarning = String(error.message).slice(0, 160);
206
+ }
207
+ }
208
+ function record(kind, data) {
209
+ const event = { sequence: ++sequence, kind, ...data, previous: head };
210
+ head = hash(JSON.stringify(event));
211
+ events.push({ ...event, hash: head });
212
+ if (events.length > 128) events.shift();
213
+ persist();
214
+ }
215
+ async function decide(stage, flags, detail = {}) {
216
+ const policy = RULES[stage],
217
+ local = flags.every(Boolean) && !options.signal?.aborted;
218
+ const operations = [
219
+ ...policy.rules.map((clause) => ({ op: "stated", clause })),
220
+ ...policy.facts
221
+ .filter((_, i) => flags[i])
222
+ .map((clause) => ({ op: "stated", clause })),
223
+ { op: "!", clause: policy.goal },
224
+ ];
225
+ let judged;
226
+ try {
227
+ judged = await client.run(operations, options.signal);
228
+ } catch (error) {
229
+ judged = { status: "unavailable", reason: error.message };
230
+ }
231
+ const gate = judged.status === "judged" ? judged.rows.at(-1) : null;
232
+ const result = {
233
+ allowed: local && (!gate || gate.allow === true),
234
+ status: gate ? (gate.allow && local ? "allow" : "deny") : "unavailable",
235
+ stage,
236
+ localAllowed: local,
237
+ missing: policy.facts.filter((_, i) => !flags[i]),
238
+ backend: gate ? "debtaur-wasm" : "unavailable",
239
+ runtimeHash: judged.runtimeHash,
240
+ inputHash: judged.inputHash,
241
+ outputHash: judged.outputHash,
242
+ reason: gate?.why || judged.reason || "Judge unavailable",
243
+ ...detail,
244
+ };
245
+ decisionCount++;
246
+ lastDecision = result;
247
+ record("decision", {
248
+ ...result,
249
+ proof: gate
250
+ ? {
251
+ verdict: gate.verdict,
252
+ provenance: gate.provenance,
253
+ evidence: gate.evidence,
254
+ }
255
+ : null,
256
+ });
257
+ return result;
258
+ }
259
+ function invalidate(decision, reason) {
260
+ decision.allowed = false;
261
+ decision.status = "deny";
262
+ decision.localAllowed = false;
263
+ decision.reason = reason;
264
+ decision.missing = [
265
+ ...new Set([...(decision.missing || []), "the artifact is current"]),
266
+ ];
267
+ record("invalidation", {
268
+ stage: decision.stage,
269
+ inputHash: decision.inputHash,
270
+ reason,
271
+ });
272
+ return decision;
273
+ }
274
+ async function dispatch(unit) {
275
+ const files = Array.isArray(unit?.files) ? unit.files : [],
276
+ before = snapshot(files, true);
277
+ let scope = before.ok && files.length > 0,
278
+ available = true,
279
+ keys = [];
280
+ if (scope) {
281
+ keys = files.map(fileName).map((x) => x.key);
282
+ available = keys.every((key) => !owners.has(key));
283
+ }
284
+ const lease = {
285
+ id: crypto.randomUUID(),
286
+ unit: String(unit?.id || "worker").slice(0, 80),
287
+ files: [...files],
288
+ active: scope && available,
289
+ before,
290
+ };
291
+ // Reserve before awaiting: concurrent dispatches cannot claim the same file.
292
+ if (lease.active) {
293
+ leases.set(lease.id, lease);
294
+ keys.forEach((key) => owners.set(key, lease.id));
295
+ }
296
+ const verdict = await decide("dispatch", [scope, available], {
297
+ unit: lease.unit,
298
+ lease: lease.id,
299
+ });
300
+ if (!verdict.allowed) {
301
+ release(lease, "dispatch-denied");
302
+ return { ok: false, decision: verdict };
303
+ }
304
+ return { ok: true, lease, decision: verdict };
305
+ }
306
+ function release(lease, reason) {
307
+ if (!lease || leases.get(lease.id) !== lease) return;
308
+ lease.active = false;
309
+ for (const [key, id] of owners) if (id === lease.id) owners.delete(key);
310
+ leases.delete(lease.id);
311
+ record("worker-release", { unit: lease.unit, lease: lease.id, reason });
312
+ }
313
+ async function handoff(lease, beforeChecks, verdict) {
314
+ const owns = !!lease && leases.get(lease.id) === lease && lease.active,
315
+ after = lease ? snapshot(lease.files) : { ok: false };
316
+ const stable =
317
+ !!beforeChecks?.ok && after.ok && beforeChecks.hash === after.hash;
318
+ const decision = await decide(
319
+ "handoff",
320
+ [owns, stable, verdict?.ok === true],
321
+ {
322
+ unit: lease?.unit || "unknown",
323
+ lease: lease?.id,
324
+ snapshotHash: after.hash,
325
+ artifacts: after.files,
326
+ },
327
+ );
328
+ if (decision.allowed) {
329
+ const final = snapshot(lease.files);
330
+ if (!final.ok || final.hash !== after.hash)
331
+ invalidate(
332
+ decision,
333
+ "Artifact changed while the handoff verdict was computed",
334
+ );
335
+ }
336
+ release(lease, decision.allowed ? "handoff" : "handoff-rejected");
337
+ return decision;
338
+ }
339
+ async function verify(check, files) {
340
+ const before = snapshot(files);
341
+ lastProof = null;
342
+ const result = await check(),
343
+ after = snapshot(files);
344
+ const observable = before.ok && after.ok,
345
+ stable = observable && before.hash === after.hash;
346
+ // Unsupported/oversized sets do not invent a native approval; baseline checks
347
+ // remain useful. Actual observed mutations always invalidate the check.
348
+ if (!observable) {
349
+ lastDecision = {
350
+ stage: "verify",
351
+ status: "unavailable",
352
+ backend: "unavailable",
353
+ reason: before.reason || after.reason,
354
+ allowed: result.pass === true,
355
+ localAllowed: result.pass === true,
356
+ };
357
+ record("unobservable-check", lastDecision);
358
+ return result;
359
+ }
360
+ const decision = await decide(
361
+ "verify",
362
+ [
363
+ stable,
364
+ result.pass === true,
365
+ leases.size === 0 &&
366
+ (!options.workersSettled || options.workersSettled()),
367
+ ],
368
+ {
369
+ snapshotHash: after.hash,
370
+ artifacts: after.files,
371
+ checkHash: hash(JSON.stringify(result.checks || [])),
372
+ },
373
+ );
374
+ if (decision.allowed) {
375
+ const final = snapshot(files);
376
+ if (!final.ok || final.hash !== after.hash)
377
+ invalidate(
378
+ decision,
379
+ "Artifact changed while the check verdict was computed",
380
+ );
381
+ }
382
+ result.debtaur = decision;
383
+ if (result.pass && !decision.allowed) {
384
+ const failed = {
385
+ check: "current orchestration evidence",
386
+ ok: false,
387
+ detail: decision.missing.join("; ") || decision.reason,
388
+ };
389
+ result.checks.push(failed);
390
+ result.failed.push(failed);
391
+ result.pass = false;
392
+ }
393
+ if (result.pass)
394
+ lastProof = {
395
+ ...after,
396
+ checkHash: hash(JSON.stringify(result.checks || [])),
397
+ };
398
+ return result;
399
+ }
400
+ async function confirm() {
401
+ if (!lastProof)
402
+ return {
403
+ allowed: !options.signal?.aborted,
404
+ status: "unavailable",
405
+ reason: "No bounded artifact snapshot was certified",
406
+ };
407
+ const now = snapshot(lastProof.files.map((x) => x.file));
408
+ const decision = await decide(
409
+ "verify",
410
+ [
411
+ now.ok && now.hash === lastProof.hash,
412
+ true,
413
+ leases.size === 0 &&
414
+ (!options.workersSettled || options.workersSettled()),
415
+ ],
416
+ { snapshotHash: now.hash, checkHash: lastProof.checkHash },
417
+ );
418
+ if (decision.allowed) {
419
+ const final = snapshot(lastProof.files.map((x) => x.file));
420
+ if (!final.ok || final.hash !== lastProof.hash)
421
+ invalidate(
422
+ decision,
423
+ "Artifact changed while final evidence was confirmed",
424
+ );
425
+ }
426
+ return decision;
427
+ }
428
+ function summary() {
429
+ return {
430
+ policy: "chode-evidence-v1",
431
+ status: lastDecision?.status || "not-used",
432
+ backend: lastDecision?.backend || "unavailable",
433
+ runtimeHash: lastDecision?.runtimeHash,
434
+ stage: lastDecision?.stage,
435
+ reason: lastDecision?.reason,
436
+ decisions: decisionCount,
437
+ report: options.noMemory ? null : relative.split(path.sep).join("/"),
438
+ storageWarning,
439
+ };
440
+ }
441
+ function close() {
442
+ client.close();
443
+ }
444
+ return {
445
+ dispatch,
446
+ handoff,
447
+ release,
448
+ snapshot,
449
+ verify,
450
+ confirm,
451
+ summary,
452
+ close,
453
+ events: () => JSON.parse(JSON.stringify(events)),
454
+ };
455
+ }
456
+ module.exports = { create, RULES };
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ const fs = require("fs"),
3
+ path = require("path"),
4
+ crypto = require("crypto"),
5
+ { parentPort, workerData } = require("worker_threads");
6
+ let modulePromise;
7
+ async function load() {
8
+ const bytes = {};
9
+ for (const [name, expected] of Object.entries(workerData.pinned)) {
10
+ const file = path.join(workerData.directory, name),
11
+ st = fs.lstatSync(file);
12
+ if (
13
+ !st.isFile() ||
14
+ st.isSymbolicLink() ||
15
+ st.nlink > 1 ||
16
+ st.size > 1024 * 1024
17
+ )
18
+ throw Error("Unsafe Debtaur runtime");
19
+ bytes[name] = fs.readFileSync(file);
20
+ if (
21
+ crypto.createHash("sha256").update(bytes[name]).digest("hex") !== expected
22
+ )
23
+ throw Error("Debtaur runtime changed before loading");
24
+ }
25
+ const { default: create } = await import(
26
+ "data:text/javascript;base64," + bytes["judge_wasm.js"].toString("base64")
27
+ );
28
+ const wasm = new WebAssembly.Module(bytes["judge_wasm.wasm"]);
29
+ return create({
30
+ print() {},
31
+ printErr() {},
32
+ instantiateWasm(imports, callback) {
33
+ const instance = new WebAssembly.Instance(wasm, imports);
34
+ callback(instance);
35
+ return instance.exports;
36
+ },
37
+ });
38
+ }
39
+ // Serialize calls: the module-persistent native engine resets its high-water
40
+ // state per batch. Concurrent calls into one instance must never interleave.
41
+ let tail = Promise.resolve();
42
+ parentPort.on("message", (message) => {
43
+ tail = tail
44
+ .then(async () => {
45
+ try {
46
+ const mod = await (modulePromise || (modulePromise = load())),
47
+ bytes = new TextEncoder().encode(message.wire),
48
+ cap = 524288;
49
+ let input = 0,
50
+ output = 0;
51
+ try {
52
+ input = mod._malloc(bytes.length + 1);
53
+ output = mod._malloc(cap);
54
+ if (!input || !output) throw Error("Debtaur allocation failed");
55
+ mod.HEAPU8.set(bytes, input);
56
+ mod.HEAPU8[input + bytes.length] = 0;
57
+ const n = mod._judge_run(input, output, cap);
58
+ if (n <= 0 || n >= cap - 1)
59
+ throw Error("Debtaur output limit reached");
60
+ const raw = new TextDecoder("utf-8", { fatal: true }).decode(
61
+ mod.HEAPU8.subarray(output, output + n),
62
+ );
63
+ parentPort.postMessage({ id: message.id, raw });
64
+ } finally {
65
+ if (input) mod._free(input);
66
+ if (output) mod._free(output);
67
+ }
68
+ } catch (error) {
69
+ parentPort.postMessage({
70
+ id: message.id,
71
+ error: String(error.message).slice(0, 160),
72
+ });
73
+ }
74
+ })
75
+ .catch(() => {});
76
+ });
package/doctor.js ADDED
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+ const fs=require('fs');const path=require('path');
3
+ function inspect(root=process.cwd(),runtime=__dirname,options={}){
4
+ const checks=[];const push=(name,ok,detail)=>checks.push({name,ok,detail});
5
+ push('Node.js version',Number(process.versions.node.split('.')[0])>=18,process.version+' (18+ required)');
6
+ for(const file of ['chode.js','terminal-ui.js','workspace-tui.js','workspace-view.js','workspace-store.js','workspace-commands.js','automatic-re.js','agent-budget.js','acceptance-check.js','recovery-context.js','rea-environment.js','request-deadline.js','elapsed.js','tui-worker.js','web-context.js','toolbox.js','evidence-ledger.js','json-output.js'])push('Runtime '+file,fs.existsSync(path.join(runtime,file)),file);
7
+ for(const file of ['config.json','pending-tasks.json']){const p=path.join(root,'.chode',file);if(!fs.existsSync(p))continue;try{const value=JSON.parse(fs.readFileSync(p,'utf8'));if(file==='pending-tasks.json'&&(!Array.isArray(value)||value.some(t=>!t||typeof t.id!=='string'||typeof t.prompt!=='string')))throw Error('Invalid queue');if(file==='config.json'&&(!value||typeof value!=='object'||Array.isArray(value)))throw Error('Invalid config');push('State '+file,true,'valid JSON and expected shape');}catch(_){push('State '+file,false,'Invalid JSON. Back up and repair '+p);}}
8
+ const npmrc=path.join(root,'.npmrc');if(fs.existsSync(npmrc)){const fd=fs.openSync(npmrc,'r');const header=Buffer.alloc(2);try{fs.readSync(fd,header,0,2,0);}finally{fs.closeSync(fd);}if(header.equals(Buffer.from([255,254]))||header.equals(Buffer.from([254,255])))push('npm configuration encoding',false,'.npmrc is UTF-16; npm expects UTF-8. Convert its encoding without changing credentials.');}
9
+ if(options.rea){const nativeEnv=require('./rea-environment').resolve();for(const name of ['GHIDRA_INSTALL_DIR','JAVA_HOME']){const value=nativeEnv[name];push(name,!!value&&fs.existsSync(value),value&&fs.existsSync(value)?'Configured path exists.':'Not available in this process. If already configured in Windows user settings, restart the terminal; otherwise configure the installed Ghidra/JDK path.');}push('Local REA analysis',true,'Run chode tool rea doctor to check the analyzer. Automatic RE only inspects an explicitly named existing local binary and does not run it.');}
10
+ return {ok:checks.every(x=>x.ok),checks};
11
+ }
12
+ function repair(root=process.cwd()){
13
+ const p=path.join(root,'.npmrc');if(!fs.existsSync(p))return [];
14
+ const bytes=fs.readFileSync(p);let body;
15
+ if(bytes[0]===255&&bytes[1]===254)body=bytes.subarray(2);
16
+ else if(bytes[0]===254&&bytes[1]===255)body=Buffer.from(bytes.subarray(2)).swap16();
17
+ else return [];
18
+ const backup=p+'.encoding-backup';if(fs.existsSync(backup))throw new Error('Encoding backup already exists; review it before another repair.');
19
+ fs.writeFileSync(backup,bytes,{flag:'wx',mode:0o600});
20
+ fs.writeFileSync(p,body.toString('utf16le'),{encoding:'utf8',mode:0o600});
21
+ return ['Converted .npmrc from UTF-16 to UTF-8; original preserved in .npmrc.encoding-backup.'];
22
+ }
23
+ function run(argv=[]){let repairs=[],failure=null;try{if(argv.includes('--fix'))repairs=repair();}catch(error){failure={name:'Configuration repair',ok:false,detail:error.message};}let r;try{r={...inspect(process.cwd(),__dirname,{rea:argv.includes('--rea')}),repairs};}catch(error){r={ok:false,checks:[{name:'Local filesystem',ok:false,detail:error.message}],repairs};}if(failure){r.checks.push(failure);r.ok=false;}if(argv.includes('--json'))console.log(JSON.stringify(r));else console.log(repairs.concat(r.checks.map(c=>(c.ok?'✓ ':'! ')+c.name+' — '+c.detail)).join('\n'));if(!r.ok)process.exitCode=1;return r;}
24
+ module.exports={inspect,repair,run};
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+ const fs=require('fs'),path=require('path'),crypto=require('crypto');
3
+ const MAX_FILE=200*1024,MAX_TOTAL=1024*1024,KEEP=50;
4
+ function unified(file,before,after){
5
+ if(before===after)return '';
6
+ if(before===null&&after==='')return 'diff --git '+JSON.stringify('a/'+file)+' '+JSON.stringify('b/'+file)+'\nnew file mode 100600\n';
7
+ const lines=s=>s==null||s===''?[]:s.split('\n').slice(0,s.endsWith('\n')?-1:undefined);
8
+ const a=lines(before),b=lines(after);let pre=0,tail=0;
9
+ while(pre<a.length&&pre<b.length&&a[pre]===b[pre])pre++;
10
+ if(pre===a.length&&pre===b.length&&pre)pre--;
11
+ const newlineChanged=!!before?.endsWith('\n')!==!!after?.endsWith('\n');
12
+ while(!newlineChanged&&tail<a.length-pre&&tail<b.length-pre&&a[a.length-1-tail]===b[b.length-1-tail])tail++;
13
+ const start=Math.max(0,pre-3),aEnd=Math.min(a.length,a.length-tail+3),bEnd=Math.min(b.length,b.length-tail+3),out=[];
14
+ const label=(prefix,exists)=>exists?JSON.stringify(prefix+'/'+file):'/dev/null';
15
+ out.push('--- '+label('a',before!==null),'+++ '+label('b',after!==null),'@@ -'+(aEnd-start?start+1:start)+','+(aEnd-start)+' +'+(bEnd-start?start+1:start)+','+(bEnd-start)+' @@');
16
+ const emit=(prefix,line,index,arr,raw)=>{out.push(prefix+line);if(index===arr.length-1&&raw&&!raw.endsWith('\n'))out.push('\');};
17
+ for(let i=start;i<pre;i++)emit(' ',a[i],i,a,before);
18
+ for(let i=pre;i<a.length-tail;i++)emit('-',a[i],i,a,before);
19
+ for(let i=pre;i<b.length-tail;i++)emit('+',b[i],i,b,after);
20
+ for(let i=0;i<Math.min(3,tail);i++)emit(' ',a[a.length-tail+i],a.length-tail+i,a,before);
21
+ return out.join('\n')+'\n';
22
+ }
23
+ function create(root,options={}){
24
+ const io=options.fs||fs,base=io.realpathSync(root),directory=path.join(base,'.chode','changes');
25
+ function target(file,state=false){
26
+ if(typeof file!=='string'||!file||/[\x00-\x1f]/.test(file)||path.isAbsolute(file))throw Error('Expected a relative workspace file');
27
+ const full=path.resolve(base,file),rel=path.relative(base,full),parts=rel.split(path.sep);
28
+ if(rel==='..'||rel.startsWith('..'+path.sep)||path.isAbsolute(rel)||!rel||parts.some(p=>p.toLowerCase()==='.git'||(!state&&p.toLowerCase()==='.chode')))throw Error('Denied workspace or harness-state path');
29
+ let at=base;for(const part of parts){at=path.join(at,part);try{if(io.lstatSync(at).isSymbolicLink())throw Error('Symbolic links are not editable');}catch(error){if(error.code!=='ENOENT')throw error;}}
30
+ if(!state&&options.allow&&options.allow(rel)===false)throw Error('Denied protected file');
31
+ return {full,file:rel.replace(/\\/g,'/')};
32
+ }
33
+ function read(file){const t=target(file);try{const stat=io.statSync(t.full);if(!stat.isFile()||stat.size>MAX_FILE||stat.nlink>1)throw Error('Expected an unlinked text file <=200KB');const data=io.readFileSync(t.full);if(data.includes(0))throw Error('Binary files are not editable');const content=data.toString('utf8');if(!Buffer.from(content).equals(data))throw Error('Invalid UTF-8 file');return {file:t.file,before:content,mode:stat.mode&0o777};}catch(error){if(error.code==='ENOENT')return {file:t.file,before:null,mode:0o600};throw error;}}
34
+ function atomic(full,content,mode=0o600){
35
+ io.mkdirSync(path.dirname(full),{recursive:true});const temp=full+'.chode-'+crypto.randomBytes(8).toString('hex')+'.tmp';let fd;
36
+ try{fd=io.openSync(temp,'wx',mode);io.fchmodSync(fd,mode);io.writeFileSync(fd,content);io.fsyncSync(fd);io.closeSync(fd);fd=null;io.renameSync(temp,full);}finally{if(fd!=null)io.closeSync(fd);try{io.unlinkSync(temp);}catch{}}
37
+ }
38
+ function save(record){target('.chode/changes',true);atomic(path.join(directory,record.id+'.json'),JSON.stringify(record));}
39
+ function records(){target('.chode/changes',true);let names;try{names=io.readdirSync(directory);}catch(e){if(e.code==='ENOENT')return [];throw e;}return names.filter(n=>/^change_[a-f0-9]{32}\.json$/.test(n)).map(n=>{
40
+ const full=path.join(directory,n);if(io.lstatSync(full).isSymbolicLink()||io.statSync(full).size>5*MAX_TOTAL)throw Error('Invalid edit journal');const r=JSON.parse(io.readFileSync(full,'utf8'));
41
+ if(r.id+'.json'!==n||!Number.isSafeInteger(r.seq)||!Array.isArray(r.changes)||r.changes.length>20)throw Error('Invalid edit journal');
42
+ for(const c of r.changes){target(c.file);if(![c.before,c.after].every(x=>x===null||typeof x==='string'&&Buffer.byteLength(x)<=MAX_FILE))throw Error('Invalid journal content');}
43
+ return r;
44
+ }).sort((a,b)=>b.seq-a.seq);}
45
+ function current(change){return read(change.file).before;}
46
+ function write(change,value){const t=target(change.file);if(value===null){try{io.unlinkSync(t.full);}catch(e){if(e.code!=='ENOENT')throw e;}}else atomic(t.full,value,change.mode);}
47
+ function restore(record){for(const c of record.changes){const now=current(c);if(now!==c.before&&now!==c.after)throw Error('Recovery conflict: '+c.file);}for(const c of record.changes)if(current(c)!==c.before)write(c,c.before);}
48
+ function locked(fn){
49
+ target('.chode/changes',true);io.mkdirSync(directory,{recursive:true});const lock=path.join(directory,'lock');let fd;
50
+ try{fd=io.openSync(lock,'wx',0o600);}catch(error){if(error.code!=='EEXIST')throw error;let owner;try{owner=JSON.parse(io.readFileSync(lock,'utf8'));process.kill(owner.pid,0);}catch(e){if(e.code==='ESRCH'){io.unlinkSync(lock);return locked(fn);}throw Error('Edit history is locked; inspect .chode/changes/lock before recovery');}throw Error('Another edit transaction is active');}
51
+ try{io.writeFileSync(fd,JSON.stringify({pid:process.pid}));io.closeSync(fd);fd=null;
52
+ for(const record of records())if(['applying','undoing'].includes(record.status)){restore(record);record.status=record.status==='undoing'?'undone':'rolled-back';save(record);}
53
+ return fn();
54
+ }finally{if(fd!=null)io.closeSync(fd);io.unlinkSync(lock);}
55
+ }
56
+ function prepare(edits,compile){
57
+ if(!Array.isArray(edits)||!edits.length||edits.length>20)throw Error('Supply 1–20 edits');const staged=new Map();
58
+ for(const edit of edits){if(!edit||typeof edit!=='object')throw Error('Invalid edit');const t=target(edit.file),key=process.platform==='win32'?t.full.toLowerCase():t.full;let c=staged.get(key);if(!c){c={...read(t.file)};c.after=c.before;staged.set(key,c);}
59
+ const result=compile(edit,c.after);if(!result||!result.ok){const error=Error(result?.error||'Edit could not be prepared');if(result?.recovery){error.recovery=result.recovery;error.file=t.file;}throw error;}if(typeof result.after!=='string'||Buffer.byteLength(result.after)>MAX_FILE||result.after.includes('\0'))throw Error('Expected UTF-8 text <=200KB');c.after=result.after;
60
+ }
61
+ const changes=[...staged.values()].filter(c=>c.before!==c.after);if(changes.reduce((n,c)=>n+Buffer.byteLength(c.before||'')+Buffer.byteLength(c.after||''),0)>MAX_TOTAL)throw Error('Batch exceeds 1MB journal limit');return changes;
62
+ }
63
+ function preview(changes){const diff=changes.map(c=>unified(c.file,c.before,c.after)).join('');return {ok:true,files:changes.map(c=>c.file),diff,truncated:false};}
64
+ function apply(changes,label='edit'){
65
+ return locked(()=>{
66
+ if(!changes.length)return {ok:true,files:[],unchanged:true,diff:''};
67
+ for(const c of changes)if(current(c)!==c.before)throw Error('File changed since preview: '+c.file);
68
+ if(typeof options.validate==='function')for(const c of changes)options.validate(c.file,c.before,c.after);
69
+ const all=records(),record={id:'change_'+crypto.randomBytes(16).toString('hex'),seq:(all[0]?.seq||0)+1,at:new Date().toISOString(),label:String(label).slice(0,120),status:'applying',changes};save(record);
70
+ try{for(const c of changes){if(current(c)!==c.before)throw Error('Concurrent file change: '+c.file);write(c,c.after);}record.status='applied';save(record);}
71
+ catch(error){try{restore(record);record.status='rolled-back';save(record);}catch(recovery){throw Error(error.message+'; '+recovery.message+'; recovery required for '+record.id);}throw error;}
72
+ for(const old of records().slice(KEEP))try{io.unlinkSync(path.join(directory,old.id+'.json'));}catch{}
73
+ return {...preview(changes),id:record.id};
74
+ });
75
+ }
76
+ function undo(id){return locked(()=>{const record=records().find(r=>r.status==='applied');if(!record)throw Error('Nothing to undo');if(id&&id!==record.id)throw Error('Undo the latest applied change first');for(const c of record.changes)if(current(c)!==c.after)throw Error('Undo conflict: file changed after edit: '+c.file);record.status='undoing';save(record);restore(record);record.status='undone';save(record);return {ok:true,id:record.id,files:record.changes.map(c=>c.file),diff:record.changes.map(c=>unified(c.file,c.after,c.before)).join('')};});}
77
+ return {prepare,preview,apply,undo,history:()=>records().map(({id,seq,at,label,status,changes})=>({id,seq,at,label,status,files:changes.map(c=>c.file)})),read};
78
+ }
79
+ module.exports={create,unified};
package/elapsed.js ADDED
@@ -0,0 +1,28 @@
1
+ function formatElapsed(milliseconds) {
2
+ if (typeof milliseconds !== 'number' || !isFinite(milliseconds) || milliseconds < 0) {
3
+ return '0s';
4
+ }
5
+
6
+ const totalSeconds = Math.floor(milliseconds / 1000);
7
+
8
+ if (totalSeconds === 0) {
9
+ return '0s';
10
+ }
11
+
12
+ const hours = Math.floor(totalSeconds / 3600);
13
+ const remainderAfterHours = totalSeconds % 3600;
14
+ const minutes = Math.floor(remainderAfterHours / 60);
15
+ const secondsPart = remainderAfterHours % 60;
16
+
17
+ if (hours > 0) {
18
+ return hours + 'h ' + minutes + 'm ' + secondsPart + 's';
19
+ }
20
+
21
+ if (minutes > 0) {
22
+ return minutes + 'm ' + secondsPart + 's';
23
+ }
24
+
25
+ return secondsPart + 's';
26
+ }
27
+
28
+ module.exports = { formatElapsed };