atris 3.42.0 → 3.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/atris.js +1 -1
- package/commands/lesson.js +178 -4
- package/lib/lesson-ledger.js +84 -0
- package/package.json +1 -1
package/bin/atris.js
CHANGED
|
@@ -538,7 +538,7 @@ function showHelp() {
|
|
|
538
538
|
console.log(' meet - onboard a stranger in one sitting and print their /book link');
|
|
539
539
|
console.log(' avail - Booking availability (/book/{username} weekly windows)');
|
|
540
540
|
console.log(' brain - Compile MAP/TODO/wiki/state into a loadable agent brain');
|
|
541
|
-
console.log(' lesson - Append a one-line lesson to atris/lessons.md (mine: distill receipts/episodes/scorecards into policy lessons)');
|
|
541
|
+
console.log(' lesson - Append a one-line lesson to atris/lessons.md (add --detector validates the falsifier at write time; ledger/revert audit every mutation; mine: distill receipts/episodes/scorecards into policy lessons)');
|
|
542
542
|
console.log(' taste - Record the operator\'s keep, kill, and more creative verdicts');
|
|
543
543
|
console.log(' teach - Turn a bad turn into a failing benchmark, then a permanent guard (red gate: no promotion without a proven failure)');
|
|
544
544
|
console.log(' ingest - Local-first wiki ingest into atris/wiki/');
|
package/commands/lesson.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
} = require('./autopilot');
|
|
9
9
|
const { detectLessonContradictions } = require('../lib/lesson-contradiction');
|
|
10
10
|
const taskDb = require('../lib/task-db');
|
|
11
|
+
const { validateDetector, appendLedgerEntry, readLedger } = require('../lib/lesson-ledger');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Tag a lesson's line in atris/lessons.md with `[resolved]` (idempotent).
|
|
@@ -78,6 +79,14 @@ function autoResolveLessons(cwd, options = {}) {
|
|
|
78
79
|
if (!dryRun && resolved.length) {
|
|
79
80
|
const metaPath = path.join(cwd, 'atris', 'lessons.json');
|
|
80
81
|
fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2) + '\n');
|
|
82
|
+
for (const slug of resolved) {
|
|
83
|
+
appendLedgerEntry(cwd, {
|
|
84
|
+
action: 'resolve',
|
|
85
|
+
slug,
|
|
86
|
+
evidence: `detector passed: ${metadata[slug].detector}`,
|
|
87
|
+
outcome: `status resolved, resolved_at ${today}`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
return { checked, resolved, dryRun };
|
|
@@ -213,12 +222,138 @@ function mineLessons(args) {
|
|
|
213
222
|
}
|
|
214
223
|
}
|
|
215
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Strip the `[resolved]` tag from a lesson's line in atris/lessons.md.
|
|
227
|
+
* Inverse of tagLessonResolvedInMd; used by revert.
|
|
228
|
+
* @returns {boolean} true if the file was changed.
|
|
229
|
+
*/
|
|
230
|
+
function untagLessonResolvedInMd(cwd, slug) {
|
|
231
|
+
const lessonsPath = path.join(cwd, 'atris', 'lessons.md');
|
|
232
|
+
if (!fs.existsSync(lessonsPath)) return false;
|
|
233
|
+
const lines = fs.readFileSync(lessonsPath, 'utf8').split('\n');
|
|
234
|
+
let changed = false;
|
|
235
|
+
for (let i = 0; i < lines.length; i++) {
|
|
236
|
+
const m = lines[i].match(/\*\*\[\d{4}-\d{2}-\d{2}\]\s+([\w-]+)\*\*/);
|
|
237
|
+
if (!m || m[1] !== slug) continue;
|
|
238
|
+
if (!/\[resolved\]/i.test(lines[i])) continue;
|
|
239
|
+
lines[i] = lines[i].replace(/\[resolved\]\s*/i, '');
|
|
240
|
+
changed = true;
|
|
241
|
+
}
|
|
242
|
+
if (changed) fs.writeFileSync(lessonsPath, lines.join('\n'));
|
|
243
|
+
return changed;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Add a lesson as a validated contract in one step: prose line in lessons.md,
|
|
248
|
+
* typed entry in the lessons.json sidecar, and a ledger record with evidence.
|
|
249
|
+
*
|
|
250
|
+
* When a detector is given it must actually run (see validateDetector):
|
|
251
|
+
* a lesson whose falsifier is a typo would sit unresolvable forever and rot
|
|
252
|
+
* trust in the whole file. Detector-less adds are still allowed (process
|
|
253
|
+
* notes), they just never self-retire.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} cwd
|
|
256
|
+
* @param {string} slug kebab-case
|
|
257
|
+
* @param {'pass'|'fail'} status
|
|
258
|
+
* @param {string} explanation
|
|
259
|
+
* @param {{ detector?: string, scope?: string }} [opts]
|
|
260
|
+
* @returns {{ ok: boolean, error?: string, ledger?: object }}
|
|
261
|
+
*/
|
|
262
|
+
function addLesson(cwd, slug, status, explanation, opts = {}) {
|
|
263
|
+
let evidence = 'prose-only (no detector)';
|
|
264
|
+
if (opts.detector !== undefined) {
|
|
265
|
+
const check = validateDetector(opts.detector, cwd);
|
|
266
|
+
if (!check.ok) {
|
|
267
|
+
return { ok: false, error: `detector rejected: ${check.reason}` };
|
|
268
|
+
}
|
|
269
|
+
evidence = `detector validated (exit ${check.exitCode}): ${opts.detector}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
writeLesson(cwd, slug, status, explanation);
|
|
273
|
+
|
|
274
|
+
if (opts.detector !== undefined || opts.scope !== undefined) {
|
|
275
|
+
const metadata = loadLessonMetadata(cwd);
|
|
276
|
+
metadata[slug] = {
|
|
277
|
+
...(metadata[slug] || {}),
|
|
278
|
+
...(opts.detector !== undefined ? { detector: opts.detector } : {}),
|
|
279
|
+
...(opts.scope !== undefined ? { scope: opts.scope } : {}),
|
|
280
|
+
status: (metadata[slug] && metadata[slug].status) || 'open',
|
|
281
|
+
};
|
|
282
|
+
fs.writeFileSync(
|
|
283
|
+
path.join(cwd, 'atris', 'lessons.json'),
|
|
284
|
+
JSON.stringify(metadata, null, 2) + '\n'
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const ledger = appendLedgerEntry(cwd, {
|
|
289
|
+
action: 'add',
|
|
290
|
+
slug,
|
|
291
|
+
evidence,
|
|
292
|
+
outcome: `${status} lesson written`,
|
|
293
|
+
});
|
|
294
|
+
return { ok: true, ledger };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Reopen a resolved lesson: sidecar back to open, [resolved] tag stripped,
|
|
299
|
+
* revert recorded in the ledger. The rollback half of auto-resolve: a
|
|
300
|
+
* detector that passed for the wrong reason (deleted call site, gamed check)
|
|
301
|
+
* must be reversible without hand-editing two files.
|
|
302
|
+
* @returns {{ ok: boolean, error?: string, ledger?: object }}
|
|
303
|
+
*/
|
|
304
|
+
function revertLessonResolution(cwd, slug, reason) {
|
|
305
|
+
const metadata = loadLessonMetadata(cwd);
|
|
306
|
+
const meta = metadata[slug];
|
|
307
|
+
if (!meta) return { ok: false, error: `no sidecar entry for "${slug}"` };
|
|
308
|
+
if (meta.status !== 'resolved') {
|
|
309
|
+
return { ok: false, error: `"${slug}" is not resolved (status: ${meta.status || 'open'})` };
|
|
310
|
+
}
|
|
311
|
+
const prevResolvedAt = meta.resolved_at;
|
|
312
|
+
delete meta.resolved_at;
|
|
313
|
+
meta.status = 'open';
|
|
314
|
+
metadata[slug] = meta;
|
|
315
|
+
fs.writeFileSync(
|
|
316
|
+
path.join(cwd, 'atris', 'lessons.json'),
|
|
317
|
+
JSON.stringify(metadata, null, 2) + '\n'
|
|
318
|
+
);
|
|
319
|
+
untagLessonResolvedInMd(cwd, slug);
|
|
320
|
+
const ledger = appendLedgerEntry(cwd, {
|
|
321
|
+
action: 'revert',
|
|
322
|
+
slug,
|
|
323
|
+
evidence: reason || 'manual revert',
|
|
324
|
+
outcome: `reopened (was resolved ${prevResolvedAt || 'unknown'})`,
|
|
325
|
+
});
|
|
326
|
+
return { ok: true, ledger };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function showLedger(args) {
|
|
330
|
+
const cwd = process.cwd();
|
|
331
|
+
const json = args.includes('--json');
|
|
332
|
+
const limitIdx = args.indexOf('--limit');
|
|
333
|
+
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) || 20 : 20;
|
|
334
|
+
const records = readLedger(cwd, { limit });
|
|
335
|
+
|
|
336
|
+
if (json) {
|
|
337
|
+
console.log(JSON.stringify({ ok: true, action: 'lesson_ledger', count: records.length, records }, null, 2));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (!records.length) {
|
|
341
|
+
console.log('ledger is empty (no lesson mutations recorded yet)');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
for (const r of records) {
|
|
345
|
+
console.log(`${(r.ts || '').slice(0, 16)} ${r.action.padEnd(7)} ${r.slug}: ${r.evidence || ''}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
216
349
|
function printLessonUsage() {
|
|
217
350
|
console.log('');
|
|
218
|
-
console.log(' Usage: atris lesson add <slug> <pass|fail> "<text>"');
|
|
351
|
+
console.log(' Usage: atris lesson add <slug> <pass|fail> "<text>" [--detector "<cmd>"] [--scope <scope>]');
|
|
219
352
|
console.log(' atris lesson mine [--json] [--dry-run]');
|
|
220
353
|
console.log(' atris lesson sweep [--json] [--dry-run]');
|
|
221
354
|
console.log(' atris lesson resolve [--json] [--dry-run]');
|
|
355
|
+
console.log(' atris lesson revert <slug> ["<reason>"]');
|
|
356
|
+
console.log(' atris lesson ledger [--json] [--limit N]');
|
|
222
357
|
console.log('');
|
|
223
358
|
}
|
|
224
359
|
|
|
@@ -251,12 +386,44 @@ function lessonAtris(subcommand, ...args) {
|
|
|
251
386
|
return;
|
|
252
387
|
}
|
|
253
388
|
|
|
389
|
+
if (subcommand === 'ledger') {
|
|
390
|
+
showLedger(args);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (subcommand === 'revert') {
|
|
395
|
+
const [slug, ...reasonParts] = args;
|
|
396
|
+
if (!slug) {
|
|
397
|
+
console.error(' ✗ usage: atris lesson revert <slug> ["<reason>"]');
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
const res = revertLessonResolution(process.cwd(), slug, reasonParts.join(' ').trim() || undefined);
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
console.error(` ✗ ${res.error}`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
console.log(`✓ lesson reopened: ${slug} (ledger ${res.ledger.id})`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
254
409
|
if (subcommand !== 'add') {
|
|
255
410
|
printLessonUsage();
|
|
256
411
|
process.exit(subcommand ? 1 : 0);
|
|
257
412
|
}
|
|
258
413
|
|
|
259
|
-
|
|
414
|
+
// Pull --detector/--scope flag pairs out before positional parsing.
|
|
415
|
+
const opts = {};
|
|
416
|
+
const positional = [];
|
|
417
|
+
for (let i = 0; i < args.length; i++) {
|
|
418
|
+
if (args[i] === '--detector') {
|
|
419
|
+
opts.detector = args[++i];
|
|
420
|
+
} else if (args[i] === '--scope') {
|
|
421
|
+
opts.scope = args[++i];
|
|
422
|
+
} else {
|
|
423
|
+
positional.push(args[i]);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const [slug, status, ...messageParts] = positional;
|
|
260
427
|
const explanation = messageParts.join(' ').trim();
|
|
261
428
|
|
|
262
429
|
if (!slug || !/^[a-z0-9-]+$/.test(slug)) {
|
|
@@ -274,9 +441,16 @@ function lessonAtris(subcommand, ...args) {
|
|
|
274
441
|
process.exit(1);
|
|
275
442
|
}
|
|
276
443
|
|
|
277
|
-
|
|
278
|
-
|
|
444
|
+
const res = addLesson(process.cwd(), slug, status, explanation, opts);
|
|
445
|
+
if (!res.ok) {
|
|
446
|
+
console.error(` ✗ ${res.error}`);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
const typed = opts.detector ? ' [detector validated]' : '';
|
|
450
|
+
console.log(`✓ lesson added: ${slug} (${status})${typed}`);
|
|
279
451
|
}
|
|
280
452
|
|
|
281
453
|
module.exports = lessonAtris;
|
|
282
454
|
module.exports.autoResolveLessons = autoResolveLessons;
|
|
455
|
+
module.exports.addLesson = addLesson;
|
|
456
|
+
module.exports.revertLessonResolution = revertLessonResolution;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lesson contract validation + append-only mutation ledger.
|
|
8
|
+
*
|
|
9
|
+
* Every lesson mutation (add / resolve / revert) lands one JSONL record in
|
|
10
|
+
* .atris/state/lesson_ledger.jsonl with evidence and an id, so self-improvement
|
|
11
|
+
* edits are auditable and reversible instead of silent sidecar rewrites. The
|
|
12
|
+
* ledger is append-only: revert writes a new record, it never deletes history.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const LEDGER_REL = path.join('.atris', 'state', 'lesson_ledger.jsonl');
|
|
16
|
+
|
|
17
|
+
function ledgerPath(root) {
|
|
18
|
+
return path.join(root, LEDGER_REL);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate a detector command at write time. A detector is a shell command
|
|
23
|
+
* whose exit code is the lesson's falsifiable state (0 = bug gone). At add
|
|
24
|
+
* time either exit code is fine (the bug usually still exists) but the
|
|
25
|
+
* command itself must run: empty strings, missing binaries (127), and
|
|
26
|
+
* non-executable targets (126) are rejected so a typo can't masquerade as a
|
|
27
|
+
* forever-failing detector.
|
|
28
|
+
* @returns {{ ok: boolean, exitCode?: number, reason?: string }}
|
|
29
|
+
*/
|
|
30
|
+
function validateDetector(cmd, cwd, timeoutMs = 10000) {
|
|
31
|
+
if (!cmd || typeof cmd !== 'string' || !cmd.trim()) {
|
|
32
|
+
return { ok: false, reason: 'detector is empty' };
|
|
33
|
+
}
|
|
34
|
+
const res = spawnSync(cmd, { shell: true, cwd, timeout: timeoutMs, stdio: 'pipe' });
|
|
35
|
+
if (res.error) return { ok: false, reason: res.error.message };
|
|
36
|
+
if (res.signal) return { ok: false, reason: `detector killed by ${res.signal} (timeout?)` };
|
|
37
|
+
if (res.status === 127) return { ok: false, exitCode: 127, reason: 'command not found (exit 127)' };
|
|
38
|
+
if (res.status === 126) return { ok: false, exitCode: 126, reason: 'command not executable (exit 126)' };
|
|
39
|
+
return { ok: true, exitCode: res.status };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Append one mutation record. Adds id + ts; returns the full record.
|
|
44
|
+
* @param {string} root
|
|
45
|
+
* @param {{ action: 'add'|'resolve'|'revert', slug: string, evidence?: string, outcome?: string }} entry
|
|
46
|
+
*/
|
|
47
|
+
function appendLedgerEntry(root, entry) {
|
|
48
|
+
const file = ledgerPath(root);
|
|
49
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
50
|
+
const record = {
|
|
51
|
+
id: `ll-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`,
|
|
52
|
+
ts: new Date().toISOString(),
|
|
53
|
+
...entry,
|
|
54
|
+
};
|
|
55
|
+
fs.appendFileSync(file, JSON.stringify(record) + '\n');
|
|
56
|
+
return record;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read ledger records, oldest first. Malformed lines are skipped, not fatal:
|
|
61
|
+
* a half-written line from a crashed process must not brick the view.
|
|
62
|
+
* @param {string} root
|
|
63
|
+
* @param {{ limit?: number, slug?: string }} [options]
|
|
64
|
+
*/
|
|
65
|
+
function readLedger(root, options = {}) {
|
|
66
|
+
const file = ledgerPath(root);
|
|
67
|
+
if (!fs.existsSync(file)) return [];
|
|
68
|
+
const records = [];
|
|
69
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
70
|
+
if (!line.trim()) continue;
|
|
71
|
+
try {
|
|
72
|
+
records.push(JSON.parse(line));
|
|
73
|
+
} catch {
|
|
74
|
+
// skip torn line
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const filtered = options.slug ? records.filter((r) => r.slug === options.slug) : records;
|
|
78
|
+
if (options.limit && filtered.length > options.limit) {
|
|
79
|
+
return filtered.slice(filtered.length - options.limit);
|
|
80
|
+
}
|
|
81
|
+
return filtered;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { ledgerPath, validateDetector, appendLedgerEntry, readLedger };
|