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.
Files changed (45) hide show
  1. package/atris/skills/design/SKILL.md +7 -1
  2. package/atris/skills/engines/SKILL.md +44 -13
  3. package/atris/team/customer-lead/MEMBER.md +45 -0
  4. package/atris/team/customer-lead/SOUL.md +33 -0
  5. package/atris/team/customer-lead/START_HERE.md +7 -0
  6. package/atris/team/customer-lead/skills/customer-commitments/SKILL.md +45 -0
  7. package/atris/team/improver/MEMBER.md +33 -0
  8. package/bin/atris.js +37 -4
  9. package/commands/autoland.js +15 -1
  10. package/commands/caretaker.js +303 -0
  11. package/commands/clean.js +76 -0
  12. package/commands/engine-watch.js +212 -0
  13. package/commands/engine.js +99 -11
  14. package/commands/founder.js +304 -0
  15. package/commands/human-missions.js +844 -0
  16. package/commands/init.js +16 -7
  17. package/commands/lesson.js +178 -4
  18. package/commands/mission.js +124 -69
  19. package/commands/slop.js +34 -3
  20. package/commands/task.js +51 -4
  21. package/commands/team.js +329 -13
  22. package/commands/verify.js +99 -6
  23. package/commands/worktree.js +119 -4
  24. package/lib/auto-accept-certified.js +302 -0
  25. package/lib/cloud-mission.js +59 -2
  26. package/lib/conductor-artifacts.js +1 -1
  27. package/lib/dispatch-scout.js +383 -0
  28. package/lib/engine-ask.js +645 -0
  29. package/lib/engine-job-lifecycle.js +65 -0
  30. package/lib/engine-receipt-sweep.js +98 -0
  31. package/lib/engine-registry.js +2 -2
  32. package/lib/engine-validate.js +374 -0
  33. package/lib/fleet.js +459 -106
  34. package/lib/known-commands.js +2 -2
  35. package/lib/lesson-ledger.js +84 -0
  36. package/lib/member-alive.js +2 -2
  37. package/lib/policy-lessons.js +70 -0
  38. package/lib/receipt-evidence.js +56 -1
  39. package/lib/runner-command.js +1 -1
  40. package/lib/secret-gateway.js +588 -0
  41. package/lib/team-presence.js +13 -1
  42. package/lib/voice-gate.js +6 -0
  43. package/lib/wish-audit.js +5 -205
  44. package/lib/wish-delegate.js +5 -2
  45. 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 sourceFile = path.join(__dirname, '..', 'atris', 'team', name, 'MEMBER.md');
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
- fs.mkdirSync(path.join(targetMemberDir, 'skills'), { recursive: true });
618
- fs.mkdirSync(path.join(targetMemberDir, 'tools'), { recursive: true });
619
- fs.mkdirSync(path.join(targetMemberDir, 'context'), { recursive: true });
620
- fs.copyFileSync(sourceFile, targetFile);
621
- markReady('team', name, `✓ Created team/${name}/ (MEMBER.md + skills/ + tools/ + context/)`);
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
 
@@ -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
- const [slug, status, ...messageParts] = args;
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
- writeLesson(process.cwd(), slug, status, explanation);
278
- console.log(`✓ lesson added: ${slug} (${status})`);
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;
@@ -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 (engineBackedTick && !tickEngineId) {
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
- await verifyMissionRunTickPhase(context);
9419
- result = context.currentTick.result;
9420
- const verifierResult = context.currentTick.verifierResult;
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 = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
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
- const tickReceiptText = result.atris2?.receipt_text || result.claude?.receipt_text || result.drill?.receipt_text || '';
9438
- const layerInfo = extractLayerFromReceiptText(tickReceiptText, tickWorktree?.new_since_baseline_sample);
9439
- result.layer = layerInfo.layer;
9440
- result.layer_source = layerInfo.source;
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
- receiptPath = writeReceipt(runtimeMission, {
9447
- kind: 'mission_run_tick',
9448
- tick: tickRecord,
9449
- frozen,
9450
- verifier_result: verifierResult,
9451
- rate_limit_info: lastRateLimit,
9452
- worktree: tickWorktree,
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
- const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
9942
- const worktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
9943
- const protectedLaneGuard = inspectMissionTickProtectedDiff(mission, tickWorktreeBefore, cwd);
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
- if (protectedLaneGuard.allowed && verify && verifierCommand) {
9953
- verifierResult = runVerifier(verifierCommand);
9954
- }
9955
- const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: verifierCommand || effectiveVerifier, baseline: worktreeBaseline });
9956
-
9957
- // Same layer classification as the run-tick path; manual ticks carry their
9958
- // receipt text in --summary.
9959
- const layerInfo = extractLayerFromReceiptText(summary || '', tickWorktree?.new_since_baseline_sample);
9960
- const guardPauseReason = protectedLaneGuard.unreadable
9961
- ? 'mission-diff-unreadable'
9962
- : 'protected-lane-review';
9963
- const tickRecord = {
9964
- status: protectedLaneGuard.allowed ? 'ran' : protectedLaneGuard.status,
9965
- reason: protectedLaneGuard.allowed ? 'tick-recorded' : guardPauseReason,
9966
- tick_index: tickIdx,
9967
- ran: protectedLaneGuard.allowed,
9968
- started_at: tickStart,
9969
- claude: { skipped: true, reason: 'orchestrator-is-caller-session' },
9970
- summary: summary || null,
9971
- layer: layerInfo.layer,
9972
- layer_source: layerInfo.source,
9973
- verifier_passed: verifierResult ? !!verifierResult.passed : null,
9974
- protected_lane_guard: protectedLaneGuard,
9975
- finished_at: stampIso(),
9976
- worktree: tickWorktree,
9977
- };
9978
- const receiptPath = writeReceipt(mission, {
9979
- kind: 'mission_tick',
9980
- tick: tickRecord,
9981
- frozen: {
9982
- verifier: verifierCommand || effectiveVerifier || '',
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
- verifier_result: verifierResult,
9987
- protected_lane_guard: protectedLaneGuard,
9988
- rate_limit_info: null,
9989
- worktree: tickWorktree,
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
- ...missionTickResultLines(outputMission, tickIdx, receiptPath, verifierResult, summary),
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 subcommand = args[0] || 'status';
11030
- const rest = args.slice(1);
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 { out = execFileSync('git', ['diff', '--unified=0', ...(staged ? ['--cached'] : [])], { encoding: 'utf8', cwd }); }
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 };