atris 3.42.0 → 3.44.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/atris/skills/design/SKILL.md +7 -1
- package/atris/skills/engines/SKILL.md +44 -13
- package/atris/team/customer-lead/MEMBER.md +45 -0
- package/atris/team/customer-lead/SOUL.md +33 -0
- package/atris/team/customer-lead/START_HERE.md +7 -0
- package/atris/team/customer-lead/skills/customer-commitments/SKILL.md +45 -0
- package/atris/team/improver/MEMBER.md +33 -0
- package/bin/atris.js +37 -4
- package/commands/autoland.js +15 -1
- package/commands/caretaker.js +303 -0
- package/commands/clean.js +76 -0
- package/commands/engine-watch.js +212 -0
- package/commands/engine.js +99 -11
- package/commands/founder.js +304 -0
- package/commands/human-missions.js +844 -0
- package/commands/init.js +16 -7
- package/commands/lesson.js +178 -4
- package/commands/mission.js +124 -69
- package/commands/slop.js +34 -3
- package/commands/task.js +51 -4
- package/commands/team.js +329 -13
- package/commands/verify.js +99 -6
- package/commands/worktree.js +119 -4
- package/lib/auto-accept-certified.js +302 -0
- package/lib/cloud-mission.js +59 -2
- package/lib/conductor-artifacts.js +1 -1
- package/lib/dispatch-scout.js +383 -0
- package/lib/engine-ask.js +645 -0
- package/lib/engine-job-lifecycle.js +65 -0
- package/lib/engine-receipt-sweep.js +98 -0
- package/lib/engine-registry.js +2 -2
- package/lib/engine-validate.js +374 -0
- package/lib/fleet.js +459 -106
- package/lib/known-commands.js +2 -2
- package/lib/lesson-ledger.js +84 -0
- package/lib/member-alive.js +2 -2
- package/lib/policy-lessons.js +70 -0
- package/lib/receipt-evidence.js +56 -1
- package/lib/runner-command.js +1 -1
- package/lib/secret-gateway.js +588 -0
- package/lib/team-presence.js +13 -1
- package/lib/voice-gate.js +6 -0
- package/lib/wish-audit.js +5 -205
- package/lib/wish-delegate.js +5 -2
- package/package.json +6 -1
package/commands/init.js
CHANGED
|
@@ -602,9 +602,12 @@ function initAtris() {
|
|
|
602
602
|
|
|
603
603
|
|
|
604
604
|
// Copy team members (MEMBER.md format — directory per member with skills/tools/context)
|
|
605
|
-
const starterMembers = ['navigator', 'executor', 'validator', 'mission-lead', 'improver'];
|
|
605
|
+
const starterMembers = ['navigator', 'executor', 'validator', 'mission-lead', 'improver', 'customer-lead'];
|
|
606
|
+
const starterMemberFiles = ['MEMBER.md', 'SOUL.md', 'START_HERE.md'];
|
|
607
|
+
const starterMemberDirs = ['skills', 'tools', 'context'];
|
|
606
608
|
starterMembers.forEach(name => {
|
|
607
|
-
const
|
|
609
|
+
const sourceMemberDir = path.join(__dirname, '..', 'atris', 'team', name);
|
|
610
|
+
const sourceFile = path.join(sourceMemberDir, 'MEMBER.md');
|
|
608
611
|
const targetMemberDir = path.join(teamDir, name);
|
|
609
612
|
const targetFile = path.join(targetMemberDir, 'MEMBER.md');
|
|
610
613
|
const legacyFile = path.join(teamDir, `${name}.md`);
|
|
@@ -614,11 +617,17 @@ function initAtris() {
|
|
|
614
617
|
|
|
615
618
|
if (fs.existsSync(sourceFile)) {
|
|
616
619
|
fs.mkdirSync(targetMemberDir, { recursive: true });
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
620
|
+
starterMemberDirs.forEach(dirName => {
|
|
621
|
+
const sourceDir = path.join(sourceMemberDir, dirName);
|
|
622
|
+
const targetDir = path.join(targetMemberDir, dirName);
|
|
623
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
624
|
+
if (fs.existsSync(sourceDir)) fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
625
|
+
});
|
|
626
|
+
starterMemberFiles.forEach(fileName => {
|
|
627
|
+
const source = path.join(sourceMemberDir, fileName);
|
|
628
|
+
if (fs.existsSync(source)) fs.copyFileSync(source, path.join(targetMemberDir, fileName));
|
|
629
|
+
});
|
|
630
|
+
markReady('team', name, `✓ Created team/${name}/ (identity + skills/ + tools/ + context/)`);
|
|
622
631
|
}
|
|
623
632
|
});
|
|
624
633
|
|
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;
|
package/commands/mission.js
CHANGED
|
@@ -46,6 +46,7 @@ const {
|
|
|
46
46
|
renderEmailLine,
|
|
47
47
|
renderMorningCardRow,
|
|
48
48
|
} = require('../lib/receipt-block');
|
|
49
|
+
const { findCachedMissionStepReceipt } = require('../lib/receipt-evidence');
|
|
49
50
|
const {
|
|
50
51
|
pruneRuns,
|
|
51
52
|
runsPruneLines,
|
|
@@ -79,7 +80,7 @@ const {
|
|
|
79
80
|
missionVerifierTimeoutMs,
|
|
80
81
|
resolveDefaultVerifier,
|
|
81
82
|
} = require('../lib/default-verifier');
|
|
82
|
-
const { redirectToWorkspaceRoot } = require('../lib/mission-root');
|
|
83
|
+
const { resolveWorkspaceRoot, redirectToWorkspaceRoot } = require('../lib/mission-root');
|
|
83
84
|
const { readJson, writeJson } = require('../lib/json-file');
|
|
84
85
|
const {
|
|
85
86
|
normalizeHumanAsks,
|
|
@@ -908,6 +909,11 @@ function loadMissionMap(root = process.cwd()) {
|
|
|
908
909
|
return map;
|
|
909
910
|
}
|
|
910
911
|
|
|
912
|
+
function hasLocalMissionState(root = process.cwd()) {
|
|
913
|
+
return readJsonLines(statePaths(root).missionsJsonl)
|
|
914
|
+
.some((mission) => mission && mission.id && mission.cloud !== true);
|
|
915
|
+
}
|
|
916
|
+
|
|
911
917
|
function terminalNextAction(status) {
|
|
912
918
|
if (status === 'complete') return 'mission complete';
|
|
913
919
|
if (status === 'stopped') return 'mission stopped';
|
|
@@ -9151,6 +9157,8 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9151
9157
|
const tickStart = stampIso();
|
|
9152
9158
|
const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
9153
9159
|
let result = { status: 'skipped', reason: 'unknown', tick_index: tickIdx, ran: false, started_at: tickStart };
|
|
9160
|
+
let cachedStep = findCachedMissionStepReceipt(cwd, { missionId: mission.id, tickIndex: tickIdx });
|
|
9161
|
+
let cachedVerifierResult = cachedStep ? cachedStep.verifier_result : null;
|
|
9154
9162
|
const tickSelection = resolveMissionTickRunner(runtimeMission, cwd);
|
|
9155
9163
|
const tickRuntimeMission = tickSelection.mission;
|
|
9156
9164
|
const tickRunnerName = String(tickRuntimeMission.runner || '').trim().toLowerCase();
|
|
@@ -9182,7 +9190,17 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9182
9190
|
}
|
|
9183
9191
|
|
|
9184
9192
|
// Active-hours gate
|
|
9185
|
-
if (
|
|
9193
|
+
if (cachedStep) {
|
|
9194
|
+
result = {
|
|
9195
|
+
...result,
|
|
9196
|
+
...cachedStep.tick,
|
|
9197
|
+
tick_index: tickIdx,
|
|
9198
|
+
cached: true,
|
|
9199
|
+
reason: 'receipt-cache-hit',
|
|
9200
|
+
ran: cachedStep.tick.ran !== false,
|
|
9201
|
+
status: cachedStep.tick.status || 'ran',
|
|
9202
|
+
};
|
|
9203
|
+
} else if (engineBackedTick && !tickEngineId) {
|
|
9186
9204
|
result = { ...result, status: 'errored', reason: 'no-ready-engine' };
|
|
9187
9205
|
} else if (!isWithinActiveHours(mission.active_hours)) {
|
|
9188
9206
|
result = { ...result, status: 'skipped', reason: 'quiet-hours' };
|
|
@@ -9399,7 +9417,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9399
9417
|
}
|
|
9400
9418
|
}
|
|
9401
9419
|
|
|
9402
|
-
if (tickEngineId) {
|
|
9420
|
+
if (tickEngineId && !cachedStep) {
|
|
9403
9421
|
result.rate_limit_info = lastRateLimit;
|
|
9404
9422
|
const engineHealth = recordMissionEngineTickOutcome(tickEngineId, result, cwd);
|
|
9405
9423
|
if (engineHealth) result.engine_health = engineHealth.health;
|
|
@@ -9415,12 +9433,18 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9415
9433
|
tickIdx,
|
|
9416
9434
|
frozen,
|
|
9417
9435
|
};
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9436
|
+
let verifierResult = null;
|
|
9437
|
+
if (cachedStep) {
|
|
9438
|
+
verifierResult = cachedVerifierResult;
|
|
9439
|
+
if (verifierResult) result.verifier_passed = verifierResult.passed;
|
|
9440
|
+
} else {
|
|
9441
|
+
await verifyMissionRunTickPhase(context);
|
|
9442
|
+
result = context.currentTick.result;
|
|
9443
|
+
verifierResult = context.currentTick.verifierResult;
|
|
9444
|
+
}
|
|
9421
9445
|
pauseReason = context.pauseReason;
|
|
9422
9446
|
context.currentTick = null;
|
|
9423
|
-
let receiptPath = null;
|
|
9447
|
+
let receiptPath = cachedStep ? cachedStep.receipt_path : null;
|
|
9424
9448
|
|
|
9425
9449
|
// Review-lane drain: always-on loops sweep the agent-safe review actions
|
|
9426
9450
|
// each tick so proof-backed work reaches certified on cadence with zero
|
|
@@ -9430,27 +9454,32 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9430
9454
|
? { skipped: true, reason: 'no-drain-flag' }
|
|
9431
9455
|
: runReviewLaneDrain(cwd);
|
|
9432
9456
|
}
|
|
9433
|
-
const tickWorktree =
|
|
9457
|
+
const tickWorktree = cachedStep?.tick?.worktree
|
|
9458
|
+
|| worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
|
|
9434
9459
|
|
|
9435
9460
|
// Layer classification needs the receipt text AND the worktree receipt, so it
|
|
9436
9461
|
// runs here — after both exist — covering the claude and atris2 branches alike.
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9462
|
+
if (!(cachedStep && cachedStep.tick && cachedStep.tick.layer)) {
|
|
9463
|
+
const tickReceiptText = result.atris2?.receipt_text || result.claude?.receipt_text || result.drill?.receipt_text || '';
|
|
9464
|
+
const layerInfo = extractLayerFromReceiptText(tickReceiptText, tickWorktree?.new_since_baseline_sample);
|
|
9465
|
+
result.layer = layerInfo.layer;
|
|
9466
|
+
result.layer_source = layerInfo.source;
|
|
9467
|
+
}
|
|
9441
9468
|
|
|
9442
9469
|
// Persist tick to mission state + write structured receipt
|
|
9443
9470
|
const finishedAt = stampIso();
|
|
9444
|
-
const tickRecord = { ...result, started_at: tickStart, finished_at: finishedAt, worktree: tickWorktree };
|
|
9471
|
+
const tickRecord = { ...result, started_at: result.started_at || tickStart, finished_at: result.finished_at || finishedAt, worktree: tickWorktree };
|
|
9445
9472
|
ticks.push(tickRecord);
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9473
|
+
if (!receiptPath) {
|
|
9474
|
+
receiptPath = writeReceipt(runtimeMission, {
|
|
9475
|
+
kind: 'mission_run_tick',
|
|
9476
|
+
tick: tickRecord,
|
|
9477
|
+
frozen,
|
|
9478
|
+
verifier_result: verifierResult,
|
|
9479
|
+
rate_limit_info: lastRateLimit,
|
|
9480
|
+
worktree: tickWorktree,
|
|
9481
|
+
});
|
|
9482
|
+
}
|
|
9454
9483
|
|
|
9455
9484
|
const xpReadyAction = missionXpReadyAction(mission, receiptPath);
|
|
9456
9485
|
const budgetRemainingSeconds = missionFullBudgetRemainingSeconds(mission);
|
|
@@ -9938,56 +9967,72 @@ function tickMission(args) {
|
|
|
9938
9967
|
const tickStart = stampIso();
|
|
9939
9968
|
const lastTickIndex = Number(mission.last_tick_index || 0);
|
|
9940
9969
|
const tickIdx = lastTickIndex + 1;
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9970
|
+
// Interrupt between writeReceipt and saveMission leaves a completed receipt
|
|
9971
|
+
// for this tick_index while last_tick_index stays behind. Reuse it on resume
|
|
9972
|
+
// instead of re-deriving the step from scratch.
|
|
9973
|
+
const cachedStep = findCachedMissionStepReceipt(cwd, { missionId: mission.id, tickIndex: tickIdx });
|
|
9945
9974
|
const effectiveVerifier = effectiveMissionVerifier(mission);
|
|
9946
9975
|
const verifierCommand = verify
|
|
9947
9976
|
? String(verifyOverride || effectiveVerifier || '').trim()
|
|
9948
9977
|
: '';
|
|
9949
|
-
if (verifierCommand) assertMissionVerifier(verifierCommand, asJson);
|
|
9978
|
+
if (!cachedStep && verifierCommand) assertMissionVerifier(verifierCommand, asJson);
|
|
9950
9979
|
|
|
9951
9980
|
let verifierResult = null;
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
lane: mission.lane || 'workspace',
|
|
9981
|
+
let tickRecord = null;
|
|
9982
|
+
let receiptPath = null;
|
|
9983
|
+
if (cachedStep) {
|
|
9984
|
+
tickRecord = {
|
|
9985
|
+
...cachedStep.tick,
|
|
9986
|
+
cached: true,
|
|
9987
|
+
reason: 'receipt-cache-hit',
|
|
9988
|
+
};
|
|
9989
|
+
verifierResult = cachedStep.verifier_result;
|
|
9990
|
+
receiptPath = cachedStep.receipt_path;
|
|
9991
|
+
} else {
|
|
9992
|
+
const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
9993
|
+
const worktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
|
|
9994
|
+
const protectedLaneGuard = inspectMissionTickProtectedDiff(mission, tickWorktreeBefore, cwd);
|
|
9995
|
+
|
|
9996
|
+
if (protectedLaneGuard.allowed && verify && verifierCommand) {
|
|
9997
|
+
verifierResult = runVerifier(verifierCommand);
|
|
9998
|
+
}
|
|
9999
|
+
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: verifierCommand || effectiveVerifier, baseline: worktreeBaseline });
|
|
10000
|
+
|
|
10001
|
+
// Same layer classification as the run-tick path; manual ticks carry their
|
|
10002
|
+
// receipt text in --summary.
|
|
10003
|
+
const layerInfo = extractLayerFromReceiptText(summary || '', tickWorktree?.new_since_baseline_sample);
|
|
10004
|
+
const guardPauseReason = protectedLaneGuard.unreadable
|
|
10005
|
+
? 'mission-diff-unreadable'
|
|
10006
|
+
: 'protected-lane-review';
|
|
10007
|
+
tickRecord = {
|
|
10008
|
+
status: protectedLaneGuard.allowed ? 'ran' : protectedLaneGuard.status,
|
|
10009
|
+
reason: protectedLaneGuard.allowed ? 'tick-recorded' : guardPauseReason,
|
|
10010
|
+
tick_index: tickIdx,
|
|
10011
|
+
ran: protectedLaneGuard.allowed,
|
|
9984
10012
|
started_at: tickStart,
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
10013
|
+
claude: { skipped: true, reason: 'orchestrator-is-caller-session' },
|
|
10014
|
+
summary: summary || null,
|
|
10015
|
+
layer: layerInfo.layer,
|
|
10016
|
+
layer_source: layerInfo.source,
|
|
10017
|
+
verifier_passed: verifierResult ? !!verifierResult.passed : null,
|
|
10018
|
+
protected_lane_guard: protectedLaneGuard,
|
|
10019
|
+
finished_at: stampIso(),
|
|
10020
|
+
worktree: tickWorktree,
|
|
10021
|
+
};
|
|
10022
|
+
receiptPath = writeReceipt(mission, {
|
|
10023
|
+
kind: 'mission_tick',
|
|
10024
|
+
tick: tickRecord,
|
|
10025
|
+
frozen: {
|
|
10026
|
+
verifier: verifierCommand || effectiveVerifier || '',
|
|
10027
|
+
lane: mission.lane || 'workspace',
|
|
10028
|
+
started_at: tickStart,
|
|
10029
|
+
},
|
|
10030
|
+
verifier_result: verifierResult,
|
|
10031
|
+
protected_lane_guard: protectedLaneGuard,
|
|
10032
|
+
rate_limit_info: null,
|
|
10033
|
+
worktree: tickWorktree,
|
|
10034
|
+
});
|
|
10035
|
+
}
|
|
9991
10036
|
|
|
9992
10037
|
let status = 'running';
|
|
9993
10038
|
let nextAction = (verifierCommand || effectiveVerifier)
|
|
@@ -9995,7 +10040,8 @@ function tickMission(args) {
|
|
|
9995
10040
|
: (mission.always_on && missionTaskSpine(mission)?.has_task
|
|
9996
10041
|
? nextCandidateTickAction(mission)
|
|
9997
10042
|
: 'attach task, verifier, or proof');
|
|
9998
|
-
const nextGoalChain = advanceMissionGoalChain(mission.goal_chain, summary, verifierResult);
|
|
10043
|
+
const nextGoalChain = advanceMissionGoalChain(mission.goal_chain, summary || tickRecord.summary, verifierResult);
|
|
10044
|
+
const protectedLaneGuard = tickRecord.protected_lane_guard || { allowed: true };
|
|
9999
10045
|
if (!protectedLaneGuard.allowed) {
|
|
10000
10046
|
status = 'paused';
|
|
10001
10047
|
nextAction = protectedLaneGuard.unreadable
|
|
@@ -10072,9 +10118,10 @@ function tickMission(args) {
|
|
|
10072
10118
|
const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: outputMission.id });
|
|
10073
10119
|
const codexGoalState = refreshCodexGoalController(process.cwd());
|
|
10074
10120
|
printJsonOrText(
|
|
10075
|
-
{ ok: true, action: 'mission_tick', mission: outputMission, tick: tickRecord, verifier_result: verifierResult, blocker, receipt_path: receiptPath, log_path: logPath, atris_goal_state: atrisGoalState, codex_goal_state: codexGoalState, continuation_goal: continuationGoal, operator_summary_warning: operatorSummaryWarning },
|
|
10121
|
+
{ ok: true, action: 'mission_tick', mission: outputMission, tick: tickRecord, verifier_result: verifierResult, blocker, receipt_path: receiptPath, log_path: logPath, atris_goal_state: atrisGoalState, codex_goal_state: codexGoalState, continuation_goal: continuationGoal, operator_summary_warning: operatorSummaryWarning, cached: Boolean(tickRecord.cached) },
|
|
10076
10122
|
[
|
|
10077
|
-
...
|
|
10123
|
+
...(tickRecord.cached ? [`Reused receipt for tick ${tickIdx}: ${receiptPath}`] : []),
|
|
10124
|
+
...missionTickResultLines(outputMission, tickIdx, receiptPath, verifierResult, summary || tickRecord.summary),
|
|
10078
10125
|
...(missionBlockerReceiptLine(blocker) ? [missionBlockerReceiptLine(blocker)] : []),
|
|
10079
10126
|
...(continuationGoal?.mission ? [`Next goal: ${continuationGoal.mission.objective}`] : []),
|
|
10080
10127
|
],
|
|
@@ -11026,8 +11073,16 @@ function answerMissionHumanAsk(ref, askIndex, answer, note = '') {
|
|
|
11026
11073
|
}
|
|
11027
11074
|
|
|
11028
11075
|
function missionCommand(args) {
|
|
11029
|
-
const
|
|
11030
|
-
const
|
|
11076
|
+
const simpleCardArgs = args.filter((value) => value !== '--json');
|
|
11077
|
+
const isBareMission = simpleCardArgs.length === 0;
|
|
11078
|
+
if (args[0] === 'answer') {
|
|
11079
|
+
return require('./human-missions').answerCommand(args.slice(1));
|
|
11080
|
+
}
|
|
11081
|
+
if (isBareMission && !hasLocalMissionState(resolveWorkspaceRoot())) {
|
|
11082
|
+
return require('./human-missions').currentMissionCommand(args);
|
|
11083
|
+
}
|
|
11084
|
+
const subcommand = isBareMission ? 'status' : (args[0] || 'status');
|
|
11085
|
+
const rest = isBareMission ? args : args.slice(1);
|
|
11031
11086
|
// Every mission verb resolves its state store from process.cwd(). Running one
|
|
11032
11087
|
// from a subdirectory used to create a nested .atris store the fleet never
|
|
11033
11088
|
// reads (proven footgun: a nested .atris appeared under
|
package/commands/slop.js
CHANGED
|
@@ -134,10 +134,12 @@ function addProjectRule(rule, root = process.cwd()) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// Map<absFile, Set<changedLineNumber>> from the working-tree (or --cached) git diff.
|
|
137
|
-
function gitChangedLines(staged, cwd = process.cwd()) {
|
|
137
|
+
function gitChangedLines(staged, cwd = process.cwd(), range = null) {
|
|
138
138
|
const map = new Map();
|
|
139
139
|
let out;
|
|
140
|
-
try {
|
|
140
|
+
try {
|
|
141
|
+
out = execFileSync('git', ['diff', '--unified=0', ...(staged ? ['--cached'] : []), ...(range ? [range] : [])], { encoding: 'utf8', cwd });
|
|
142
|
+
}
|
|
141
143
|
catch { return map; }
|
|
142
144
|
let cur = null;
|
|
143
145
|
for (const line of out.split('\n')) {
|
|
@@ -239,6 +241,35 @@ function scanFile(file, rules = RULES, pairRules = PAIR_RULES) {
|
|
|
239
241
|
return findings;
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
// Programmatic scanner for landing gates. Callers provide the exact candidate
|
|
245
|
+
// paths, so this never widens into a repo scan. An optional changed-lines map
|
|
246
|
+
// keeps branch-diff gates on the lines the candidate actually added or edited.
|
|
247
|
+
function scanPaths(targets, options = {}) {
|
|
248
|
+
const root = path.resolve(options.root || process.cwd());
|
|
249
|
+
const rules = options.rules || RULES.concat(loadProjectRules(root));
|
|
250
|
+
const pairRules = options.pairRules || PAIR_RULES;
|
|
251
|
+
const changedLines = options.changedLines instanceof Map ? options.changedLines : null;
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
const files = [];
|
|
254
|
+
for (const target of Array.isArray(targets) ? targets : [targets]) {
|
|
255
|
+
if (!target) continue;
|
|
256
|
+
for (const file of walk(path.resolve(root, target), [])) {
|
|
257
|
+
const absolute = path.resolve(file);
|
|
258
|
+
if (seen.has(absolute)) continue;
|
|
259
|
+
seen.add(absolute);
|
|
260
|
+
files.push(absolute);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
let findings = files.flatMap((file) => scanFile(file, rules, pairRules));
|
|
264
|
+
if (changedLines) {
|
|
265
|
+
findings = findings.filter((finding) => {
|
|
266
|
+
const lines = changedLines.get(path.resolve(finding.file));
|
|
267
|
+
return lines && lines.has(finding.line);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return { root, files, findings };
|
|
271
|
+
}
|
|
272
|
+
|
|
242
273
|
function detect(argv) {
|
|
243
274
|
const json = argv.includes('--json');
|
|
244
275
|
const quiet = argv.includes('--quiet');
|
|
@@ -583,4 +614,4 @@ function slopCommand(argv) {
|
|
|
583
614
|
return 0;
|
|
584
615
|
}
|
|
585
616
|
|
|
586
|
-
module.exports = { slopCommand, detect, scanFile, RULES, PAIR_RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook, findDeadCode, findOrphanedExports, listJsFiles };
|
|
617
|
+
module.exports = { slopCommand, detect, scanFile, scanPaths, RULES, PAIR_RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook, findDeadCode, findOrphanedExports, listJsFiles };
|