atris 3.43.0 → 3.45.1

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 (52) 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 +36 -3
  9. package/commands/aeo.js +5 -2
  10. package/commands/align.js +5 -2
  11. package/commands/autoland.js +15 -1
  12. package/commands/caretaker.js +303 -0
  13. package/commands/clean.js +76 -0
  14. package/commands/computer.js +5 -2
  15. package/commands/engine-watch.js +212 -0
  16. package/commands/engine.js +99 -11
  17. package/commands/founder.js +304 -0
  18. package/commands/human-missions.js +844 -0
  19. package/commands/improve.js +29 -6
  20. package/commands/init.js +16 -7
  21. package/commands/mission.js +124 -69
  22. package/commands/pull.js +5 -2
  23. package/commands/push.js +5 -2
  24. package/commands/slop.js +34 -3
  25. package/commands/task.js +51 -4
  26. package/commands/team.js +329 -13
  27. package/commands/terminal.js +5 -2
  28. package/commands/verify.js +99 -6
  29. package/commands/workflow.js +10 -3
  30. package/commands/worktree.js +119 -4
  31. package/lib/auto-accept-certified.js +302 -0
  32. package/lib/cloud-mission.js +59 -2
  33. package/lib/conductor-artifacts.js +1 -1
  34. package/lib/dispatch-scout.js +386 -0
  35. package/lib/engine-ask.js +645 -0
  36. package/lib/engine-job-lifecycle.js +65 -0
  37. package/lib/engine-receipt-sweep.js +98 -0
  38. package/lib/engine-registry.js +2 -2
  39. package/lib/engine-validate.js +382 -0
  40. package/lib/fleet.js +459 -106
  41. package/lib/known-commands.js +2 -2
  42. package/lib/member-alive.js +2 -2
  43. package/lib/policy-lessons.js +70 -0
  44. package/lib/receipt-evidence.js +56 -1
  45. package/lib/runner-command.js +1 -1
  46. package/lib/secret-gateway.js +588 -0
  47. package/lib/team-presence.js +13 -1
  48. package/lib/voice-gate.js +6 -0
  49. package/lib/wish-audit.js +5 -205
  50. package/lib/wish-delegate.js +5 -2
  51. package/package.json +6 -1
  52. package/utils/auth.js +56 -9
@@ -918,10 +918,15 @@ function formatImproveReport(result = {}) {
918
918
  }
919
919
 
920
920
  // ---------------------------------------------------------------------------
921
- // atris improve revisions — the gauge for the north-star metric:
922
- // operator revisions after landing = 0. an agent landing is a commit carrying
923
- // the atris co-author trailer (atris-builder[bot]); if a human commit touches
924
- // any of the same files within 72 hours, that landing failed the guarantee.
921
+ // atris improve revisions: the gauge for the north-star metric:
922
+ // operator revisions after landing = 0. an agent landing is a commit whose
923
+ // Co-authored-by trailer matches a known agent signature (atris-builder[bot],
924
+ // claude, cursor, codex, chatgpt, openai), case-insensitive, and only on
925
+ // those trailer lines. commits with no trailer still count as human, so the
926
+ // metric overcounts revisions; accepted on purpose.
927
+ //
928
+ // if a human commit touches any of the same files within 72 hours, that
929
+ // landing failed the guarantee.
925
930
  //
926
931
  // renames are NOT followed: `git log --follow` is per-file and would cost one
927
932
  // subprocess per file per landing, so a post-landing rename reads as "no
@@ -930,9 +935,26 @@ function formatImproveReport(result = {}) {
930
935
  const REVISIONS_SCHEMA = 'atris.improve_revisions.v1';
931
936
  const REVISION_WINDOW_HOURS = 72;
932
937
  const REVISION_WINDOW_MS = REVISION_WINDOW_HOURS * 60 * 60 * 1000;
933
- const AGENT_TRAILER_MARKER = 'atris-builder[bot]';
938
+ const AGENT_TRAILER_MARKERS = [
939
+ 'atris-builder[bot]',
940
+ 'claude',
941
+ 'cursor',
942
+ 'codex',
943
+ 'chatgpt',
944
+ 'openai',
945
+ ];
934
946
  const DEFAULT_REVISIONS_DAYS = 14;
935
947
 
948
+ function isAgentCommitBody(body) {
949
+ const markers = AGENT_TRAILER_MARKERS.map((m) => m.toLowerCase());
950
+ for (const line of String(body || '').split(/\r?\n/)) {
951
+ if (!/^\s*co-authored-by\s*:/i.test(line)) continue;
952
+ const lower = line.toLowerCase();
953
+ if (markers.some((m) => lower.includes(m))) return true;
954
+ }
955
+ return false;
956
+ }
957
+
936
958
  function parseRevisionsArgs(argv = []) {
937
959
  const args = Array.isArray(argv) ? argv : [];
938
960
  const opts = { days: DEFAULT_REVISIONS_DAYS, json: false, help: false };
@@ -999,7 +1021,7 @@ function collectRevisionSignals(root, options = {}) {
999
1021
  at: String(at || '').trim(),
1000
1022
  ms: timestampMs(at),
1001
1023
  subject: String(subject || '').trim(),
1002
- isAgent: String(body || '').includes(AGENT_TRAILER_MARKER),
1024
+ isAgent: isAgentCommitBody(body),
1003
1025
  };
1004
1026
  })
1005
1027
  .filter((c) => c.hash && c.ms != null);
@@ -1350,6 +1372,7 @@ module.exports = {
1350
1372
  runLoopDoctor,
1351
1373
  collectRevisionSignals,
1352
1374
  formatRevisionsReport,
1375
+ isAgentCommitBody,
1353
1376
  runLocalFallback,
1354
1377
  summarizeLocalMissionRun,
1355
1378
  LOCAL_FALLBACK_ARGS,
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
 
@@ -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/pull.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { loadCredentials } = require('../utils/auth');
3
+ const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
4
4
  const { apiRequestJson } = require('../utils/api');
5
5
  const { findAllMembers } = require('./member');
6
6
  const { loadConfig } = require('../utils/config');
@@ -358,15 +358,18 @@ async function pullBusiness(slug) {
358
358
  const autoWake = process.argv.includes('--auto-wake');
359
359
  if (autoWake) {
360
360
  const statusResult = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token: creds.token });
361
+ abortOnAuthFailure(statusResult);
361
362
  const computerStatus = statusResult.ok && statusResult.data ? statusResult.data.status : 'unknown';
362
363
  if (computerStatus !== 'running' || !(statusResult.data && statusResult.data.endpoint)) {
363
364
  process.stdout.write(' Waking EC2 computer... ');
364
365
  _coldWake = true;
365
- await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
366
+ const wake = await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
367
+ abortOnAuthFailure(wake, true);
366
368
  const wakeStart = Date.now();
367
369
  while (Date.now() - wakeStart < 90000) {
368
370
  await new Promise((r) => setTimeout(r, 3000));
369
371
  const s = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token: creds.token });
372
+ abortOnAuthFailure(s, true);
370
373
  if (s.ok && s.data && s.data.status === 'running' && s.data.endpoint) {
371
374
  const elapsed = Math.floor((Date.now() - wakeStart) / 1000);
372
375
  console.log(`awake (${elapsed}s)`);
package/commands/push.js CHANGED
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const readline = require('readline');
5
- const { loadCredentials } = require('../utils/auth');
5
+ const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
6
6
  const { apiRequestJson } = require('../utils/api');
7
7
  const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
8
8
  const { loadManifest, saveManifest, buildManifest, computeLocalHashes, isIgnoredSyncPath, filterSyncFiles } = require('../lib/manifest');
@@ -527,15 +527,18 @@ async function pushAtris() {
527
527
  const autoWake = process.argv.includes('--auto-wake');
528
528
  if (autoWake) {
529
529
  const statusResult = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token: creds.token });
530
+ abortOnAuthFailure(statusResult);
530
531
  const computerStatus = statusResult.ok && statusResult.data ? statusResult.data.status : 'unknown';
531
532
  if (computerStatus !== 'running' || !(statusResult.data && statusResult.data.endpoint)) {
532
533
  process.stdout.write(' Waking EC2 computer... ');
533
534
  _coldWake = true;
534
- await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
535
+ const wake = await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
536
+ abortOnAuthFailure(wake, true);
535
537
  const wakeStart = Date.now();
536
538
  while (Date.now() - wakeStart < 90000) {
537
539
  await new Promise((r) => setTimeout(r, 3000));
538
540
  const s = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token: creds.token });
541
+ abortOnAuthFailure(s, true);
539
542
  if (s.ok && s.data && s.data.status === 'running' && s.data.endpoint) {
540
543
  const elapsed = Math.floor((Date.now() - wakeStart) / 1000);
541
544
  console.log(`awake (${elapsed}s)`);
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 };
package/commands/task.js CHANGED
@@ -11,6 +11,7 @@ const os = require('os');
11
11
  const { hasFlag } = require('../lib/arg-parser');
12
12
  const { taskProofState } = require('../lib/task-proof');
13
13
  const {
14
+ candidatePolicyGate,
14
15
  evaluateAutoAccept,
15
16
  isAgentCertified,
16
17
  isAutoCertifyVerifyCommandAllowed,
@@ -443,6 +444,21 @@ function failTask(label, reason, detail, exitCode = 2) {
443
444
  process.exit(exitCode);
444
445
  }
445
446
 
447
+ function refuseCandidatePolicyGate(label, gate) {
448
+ if (jsonModeActive()) {
449
+ printJson({ ok: false, command: label, ...gate });
450
+ } else {
451
+ console.error(`${label}: ${gate.reason}: ${gate.message}`);
452
+ if (Array.isArray(gate.offenders)) {
453
+ gate.offenders.forEach((offender) => console.error(` ${offender}`));
454
+ }
455
+ if (Array.isArray(gate.lesson_ids) && gate.lesson_ids.length) {
456
+ console.error(` lessons: ${gate.lesson_ids.join(', ')}`);
457
+ }
458
+ }
459
+ process.exit(1);
460
+ }
461
+
446
462
  function proofFlagValue(args) {
447
463
  const proof = flag(args, '--proof');
448
464
  return typeof proof === 'string' ? proof.trim() : '';
@@ -5254,6 +5270,12 @@ function autoAcceptCertifiedSmallReviews(taskDb, db, projection) {
5254
5270
  results.push({ ...evaluation, action: 'queued', task_id: task?.id || item.id || null });
5255
5271
  continue;
5256
5272
  }
5273
+ const policyGate = candidatePolicyGate(task, { executeDetectors: true });
5274
+ if (!policyGate.ok) {
5275
+ results.push({ ...policyGate, action: 'queued', task_id: task?.id || item.id || null });
5276
+ continue;
5277
+ }
5278
+ evaluation.candidate_gate = policyGate.gate;
5257
5279
  const accepted = acceptReviewTask(taskDb, db, task.id, {
5258
5280
  actor: REVIEW_AUTO_ACCEPT_ACTOR,
5259
5281
  proof: evaluation.proof,
@@ -9436,6 +9458,16 @@ function cmdReady(args) {
9436
9458
  if (missionXpIssue) {
9437
9459
  failTask('atris task ready', MISSION_XP_END_TO_END_REASON, missionXpIssue);
9438
9460
  }
9461
+ const readyPolicyTask = {
9462
+ ...beforeTask,
9463
+ workspace_root: process.cwd(),
9464
+ metadata: {
9465
+ ...(beforeTask && beforeTask.metadata || {}),
9466
+ ...(resultFields.files ? { changed_files: resultFields.files } : {}),
9467
+ },
9468
+ };
9469
+ const readyPolicyGate = candidatePolicyGate(readyPolicyTask, { executeDetectors: true });
9470
+ if (!readyPolicyGate.ok) refuseCandidatePolicyGate('atris task ready', readyPolicyGate);
9439
9471
  const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
9440
9472
  actor,
9441
9473
  proof: String(proof),
@@ -9497,12 +9529,12 @@ function cmdReady(args) {
9497
9529
  handoff.codex_prompt = reviewChat.codex_prompt;
9498
9530
  handoff.verification_focus = reviewChat.verification_focus;
9499
9531
  }
9500
- // Mined policy lessons (atris lesson mine) coach the proof at submission
9501
- // time: evidence patterns that historically certify get suggested before
9502
- // the task stalls in the review lane. Advisory only — never blocks ready.
9532
+ // Mined proof lessons without runnable detectors remain coaching. Promoted,
9533
+ // path-scoped detector lessons already passed the hard gate above.
9503
9534
  const { readPolicyLessons, policyHintsForProof } = require('../lib/policy-lessons');
9504
9535
  const policyHints = policyHintsForProof(String(proof), readPolicyLessons(taskDb.workspaceRoot()), taskDb.workspaceRoot());
9505
9536
  if (policyHints.length) handoff.policy_hints = policyHints;
9537
+ if (readyPolicyGate.gate.advisories.length) handoff.policy_gate = readyPolicyGate.gate;
9506
9538
  if (wantsJson(args)) {
9507
9539
  printJson({
9508
9540
  ok: true,
@@ -9998,6 +10030,11 @@ function cmdCertifyVerified(args, options = {}) {
9998
10030
  results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
9999
10031
  continue;
10000
10032
  }
10033
+ const policyGate = candidatePolicyGate(task, { verifyCache, executeDetectors: true });
10034
+ if (!policyGate.ok) {
10035
+ results.push({ ...policyGate, ref, action: 'skipped' });
10036
+ continue;
10037
+ }
10001
10038
  const builderProof = String(review.proof || metadata.latest_agent_proof || '').slice(0, 200);
10002
10039
  const readied = taskDb.readyTask(db, {
10003
10040
  id: task.id,
@@ -10036,7 +10073,12 @@ function cmdCertifyVerified(args, options = {}) {
10036
10073
  console.log('certify-verified: no review rows to consider.');
10037
10074
  } else {
10038
10075
  for (const r of results) {
10039
- console.log(`${r.action.padEnd(14)} ${r.ref}${r.verify ? ` \`${r.verify}\`` : ''}${r.reason ? ` (${r.reason})` : ''}`);
10076
+ const gateDetail = r.reason === 'slop_gate' && Array.isArray(r.offenders)
10077
+ ? ` [${r.offenders.join('; ')}]`
10078
+ : (r.reason === 'lesson_gate' && Array.isArray(r.lesson_ids)
10079
+ ? ` [lessons: ${r.lesson_ids.join(', ')}]`
10080
+ : '');
10081
+ console.log(`${r.action.padEnd(14)} ${r.ref}${r.verify ? ` \`${r.verify}\`` : ''}${r.reason ? ` (${r.reason})` : ''}${gateDetail}`);
10040
10082
  }
10041
10083
  console.log(`certified ${certified}; humans keep denied lanes and rows without a runnable check.`);
10042
10084
  }
@@ -10348,6 +10390,8 @@ function evaluateSweepAutoAccept(task, root) {
10348
10390
  if (denied) return { eligible: false, ref, reason: denied };
10349
10391
  const proof = autoAcceptSweepLatestProof(task);
10350
10392
  if (!proof) return { eligible: false, ref, reason: 'no_proof' };
10393
+ const policyGate = candidatePolicyGate(task, { executeDetectors: true });
10394
+ if (!policyGate.ok) return { ...policyGate, ref };
10351
10395
 
10352
10396
  // 1. An explicit, stored verifier (`atris task ready --verify`, or a prior
10353
10397
  // certify-verified stamp) is the strongest signal: re-run it live, right
@@ -10367,6 +10411,7 @@ function evaluateSweepAutoAccept(task, root) {
10367
10411
  policy: 'sweep_auto_accept_verified_command',
10368
10412
  proof,
10369
10413
  verify: storedVerify,
10414
+ candidate_gate: policyGate.gate,
10370
10415
  proved_by: [`${storedVerify} exited 0`],
10371
10416
  happened: autoAcceptSweepHappened(task),
10372
10417
  };
@@ -10382,6 +10427,7 @@ function evaluateSweepAutoAccept(task, root) {
10382
10427
  policy: 'sweep_auto_accept_verified',
10383
10428
  proof,
10384
10429
  evidence: verifier.evidence,
10430
+ candidate_gate: policyGate.gate,
10385
10431
  proved_by: verifier.proved_by,
10386
10432
  happened: autoAcceptSweepHappened(task),
10387
10433
  };
@@ -10405,6 +10451,7 @@ function evaluateSweepAutoAccept(task, root) {
10405
10451
  policy: 'sweep_auto_accept_verified_derived',
10406
10452
  proof,
10407
10453
  verify: derived,
10454
+ candidate_gate: policyGate.gate,
10408
10455
  proved_by: [`${derived} exited 0`],
10409
10456
  happened: autoAcceptSweepHappened(task),
10410
10457
  };