atris 3.71.3 → 3.72.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 (46) hide show
  1. package/atris/skills/campaigns/SKILL.md +1 -1
  2. package/bin/atris.js +7 -1
  3. package/commands/auth.js +29 -2
  4. package/commands/autoland.js +102 -2
  5. package/commands/campaign.js +86 -13
  6. package/commands/clean.js +60 -4
  7. package/commands/gm.js +1273 -7
  8. package/commands/member.js +28 -3
  9. package/commands/mission.js +72 -23
  10. package/commands/sync.js +2 -2
  11. package/commands/task.js +49 -13
  12. package/commands/zen.js +183 -0
  13. package/lib/auto-accept-certified.js +9 -4
  14. package/lib/campaign-cadence.js +15 -5
  15. package/lib/campaign-core.js +44 -17
  16. package/lib/campaign-loop.js +45 -14
  17. package/lib/campaign-reading.js +14 -1
  18. package/lib/campaign-rsi.js +28 -11
  19. package/lib/campaign-schedule.js +124 -0
  20. package/lib/campaign-store.js +34 -2
  21. package/lib/campaign-trace.js +40 -14
  22. package/lib/engine-resource.js +5 -1
  23. package/lib/gm-bet.js +2219 -0
  24. package/lib/gm-phone-page.js +1102 -0
  25. package/lib/gm-phone.js +682 -0
  26. package/lib/gm-season.js +1162 -0
  27. package/lib/gm-target.js +292 -0
  28. package/lib/job-code-verify.js +169 -0
  29. package/lib/known-commands.js +1 -1
  30. package/lib/member-autoland.js +105 -14
  31. package/lib/member-progress.js +2 -2
  32. package/lib/member-report.js +11 -1
  33. package/lib/member-self-next-job.js +6 -1
  34. package/lib/member-up.js +17 -8
  35. package/lib/merged-review-close.js +206 -0
  36. package/lib/mission-protected-lane.js +52 -4
  37. package/lib/qr.js +338 -0
  38. package/lib/site-publish.js +16 -2
  39. package/lib/site.js +17 -7
  40. package/lib/yes-lock.js +530 -0
  41. package/lib/zen-apply.js +619 -0
  42. package/lib/zen-workspace.js +121 -0
  43. package/lib/zen.js +409 -0
  44. package/package.json +1 -1
  45. package/utils/api.js +15 -2
  46. package/utils/auth.js +8 -7
@@ -10636,11 +10636,13 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
10636
10636
  const required = require('../lib/member-task-pick').taskCheck(task).command;
10637
10637
  const checked = mission.verifier_result && mission.verifier_result.passed === true
10638
10638
  && (!required || String(mission.verifier_result.command || '').trim() === required);
10639
- if (!thrown && result && result.started && ['complete', 'ready'].includes(mission.status) && checked) {
10639
+ // The commit comes from the member's own work copy, not the main folder.
10640
+ const workRoot = (result && result.worktree && result.worktree.path) || (mission.worktree && mission.worktree.path) || undefined;
10641
+ const passed = !thrown && result && result.started && ['complete', 'ready'].includes(mission.status) && checked;
10642
+ const changedNothing = passed && selfWrittenJob(task) && !runChangedSomething(taskDb, db, task, result, workRoot);
10643
+ if (passed && !changedNothing) {
10640
10644
  const proof = [`[verified] ${owner} ran mission ${result.mission_id} and its check passed.`,
10641
10645
  receipt ? `Receipt: ${receipt}` : ''].filter(Boolean).join('\n');
10642
- // The commit comes from the member's own work copy, not the main folder.
10643
- const workRoot = (result.worktree && result.worktree.path) || (mission.worktree && mission.worktree.path) || undefined;
10644
10646
  const ready = taskDb.readyTask(db, { id: task.id, actor: owner, proof, workRoot });
10645
10647
  if (ready.ready !== false) return { id: task.id, title: task.title, outcome: 'review', receipt: receipt || null };
10646
10648
  // Its change already merged and the merge line closed the job.
@@ -10650,6 +10652,7 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
10650
10652
  const lastReason = String((ticks[ticks.length - 1] || {}).reason || (ticks[ticks.length - 1] || {}).error || '');
10651
10653
  const outOfTime = /timeout|wall-exceeded/.test(lastReason);
10652
10654
  const why = thrown ? thrown.message
10655
+ : changedNothing ? 'the run passed its check but changed nothing: no new commit, no pushed branch, no pull request'
10653
10656
  : outOfTime ? 'ran out of time before the work finished'
10654
10657
  : (result && result.error) || (result && result.started ? 'the check did not pass' : 'the run did not start');
10655
10658
  const reason = String(why || 'the run did not finish').split(/\r?\n/)[0].trim().slice(0, 200);
@@ -10678,6 +10681,28 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
10678
10681
  return { id: task.id, title: task.title, outcome, reason, ...(held && forReview ? { held_for: 'review' } : {}) };
10679
10682
  }
10680
10683
 
10684
+ // A job the member wrote for itself from its mission ("next step toward:").
10685
+ function selfWrittenJob(task) {
10686
+ return String((task && task.source_key) || '').startsWith('self-next:')
10687
+ || (task && task.metadata && task.metadata.source === 'member mission');
10688
+ }
10689
+
10690
+ // Did the run leave something for review? A pushed branch, a pull request,
10691
+ // or a local landing counts, and so does a commit in the work copy that
10692
+ // changes files since the job started. With no start commit on record there
10693
+ // is nothing to compare, so the run is taken at its word.
10694
+ function runChangedSomething(taskDb, db, task, result, workRoot) {
10695
+ const landed = result && result.autoland;
10696
+ if (landed && (landed.pushed || landed.url || landed.merged)) return true;
10697
+ const start = String((taskDb.getTask(db, task.id) || task)?.metadata?.start_commit || '').trim();
10698
+ if (!start) return true;
10699
+ const cwd = workRoot || task.workspace_root || process.cwd();
10700
+ const diff = spawnSync('git', ['diff', '--quiet', start, 'HEAD'], { cwd, encoding: 'utf8' });
10701
+ // Exit 0 means no change. Exit 1 means the code differs; anything else
10702
+ // could not be read, so the run is taken at its word.
10703
+ return diff.status !== 0;
10704
+ }
10705
+
10681
10706
  const MEMBER_GO_STRIKES = 3;
10682
10707
 
10683
10708
  // Failed runs on a task since its last pass, review, reopen, or a person
@@ -1774,6 +1774,14 @@ function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
1774
1774
  return logPath;
1775
1775
  }
1776
1776
 
1777
+ // Shorten at a word boundary so a lesson never ends mid-word.
1778
+ function clipWords(text, max) {
1779
+ if (text.length <= max) return text;
1780
+ const cut = text.slice(0, max + 1);
1781
+ const space = cut.lastIndexOf(' ');
1782
+ return `${(space > max * 0.6 ? cut.slice(0, space) : text.slice(0, max)).replace(/[\s,;:.-]+$/, '')}...`;
1783
+ }
1784
+
1777
1785
  function crystallizeMemberSkillLesson(owner, entry = {}, root = process.cwd()) {
1778
1786
  const dir = memberDir(owner, root);
1779
1787
  if (!dir) return null;
@@ -1782,38 +1790,47 @@ function crystallizeMemberSkillLesson(owner, entry = {}, root = process.cwd()) {
1782
1790
  const skillDir = path.join(dir, 'skills', 'auto-learned');
1783
1791
  fs.mkdirSync(skillDir, { recursive: true });
1784
1792
  const skillPath = path.join(skillDir, 'SKILL.md');
1785
- const existingBullets = [];
1793
+ let lines = null;
1786
1794
  if (fs.existsSync(skillPath)) {
1787
1795
  try {
1788
- const raw = fs.readFileSync(skillPath, 'utf8');
1789
- for (const line of raw.split(/\r?\n/)) {
1790
- if (line.trim().startsWith('- **')) existingBullets.push(line.trim());
1791
- }
1796
+ lines = fs.readFileSync(skillPath, 'utf8').split(/\r?\n/);
1792
1797
  } catch {}
1793
1798
  }
1799
+ const bulletAt = (lines || []).map((line, i) => (line.trim().startsWith('- **') ? i : -1)).filter((i) => i >= 0);
1800
+ const existingBullets = bulletAt.map((i) => lines[i].trim());
1794
1801
  const verifierText = String(entry.verifier || '').replace(/\s+/g, ' ').trim();
1795
1802
  const nextPushText = String(entry.next_push || '').replace(/\s+/g, ' ').trim();
1796
1803
  const layerLabel = String(entry.layer || 'capabilities').trim();
1797
- const parts = [`- **${todayName()} (${layerLabel})**: ${summary.slice(0, 140)}`];
1798
- if (verifierText) parts.push(`Verify: \`${verifierText.slice(0, 80)}\``);
1799
- if (nextPushText) parts.push(`Next push: ${nextPushText.slice(0, 120)}`);
1804
+ const parts = [`- **${todayName()} (${layerLabel})**: ${clipWords(summary, 240)}`];
1805
+ // A cut-off command cannot be run, so a long one is left out, never clipped.
1806
+ if (verifierText && verifierText.length <= 300) parts.push(`Verify: \`${verifierText}\``);
1807
+ if (nextPushText) parts.push(`Next push: ${clipWords(nextPushText, 200)}`);
1800
1808
  const newBullet = parts.join(' -> ');
1801
1809
  const deduped = [
1802
1810
  newBullet,
1803
1811
  ...existingBullets.filter((b) => !b.includes(summary.slice(0, 80))),
1804
1812
  ].slice(0, 5);
1805
1813
 
1806
- const content = [
1807
- '---',
1808
- 'name: auto-learned',
1809
- `description: Verified patterns crystallized from passing runs for ${owner}. Bounded to 5 newest patterns so context never bloats.`,
1810
- '---',
1811
- '',
1812
- '# Auto-Learned Verified Patterns',
1813
- '',
1814
- ...deduped,
1815
- '',
1816
- ].join('\n');
1814
+ let content;
1815
+ if (bulletAt.length) {
1816
+ // Keep whatever a person or reviewer wrote around the lessons; only the
1817
+ // lesson list itself is rewritten.
1818
+ const first = bulletAt[0];
1819
+ const last = bulletAt[bulletAt.length - 1];
1820
+ content = [...lines.slice(0, first), ...deduped, ...lines.slice(last + 1)].join('\n');
1821
+ } else {
1822
+ content = [
1823
+ '---',
1824
+ 'name: auto-learned',
1825
+ `description: Verified patterns crystallized from passing runs for ${owner}. Bounded to 5 newest patterns so context never bloats.`,
1826
+ '---',
1827
+ '',
1828
+ '# Auto-Learned Verified Patterns',
1829
+ '',
1830
+ ...deduped,
1831
+ '',
1832
+ ].join('\n');
1833
+ }
1817
1834
  fs.writeFileSync(skillPath, content, 'utf8');
1818
1835
  return skillPath;
1819
1836
  }
@@ -3199,6 +3216,7 @@ function missionFromArgs(args) {
3199
3216
  '--base',
3200
3217
  '--task',
3201
3218
  '--ask',
3219
+ '--yes-lock',
3202
3220
  '--campaign',
3203
3221
  '--reserve',
3204
3222
  '--mission-id',
@@ -3242,6 +3260,7 @@ function missionFromArgs(args) {
3242
3260
  const taskIds = readRepeatedFlag(args, '--task');
3243
3261
  // MEMBER.md approval-required becomes an open human ask the owner must clear.
3244
3262
  const humanAsks = [...readRepeatedFlag(args, '--ask'), ...memberApprovalAsks(owner, process.cwd(), objective)];
3263
+ const yesLocks = readYesLockSeal(readFlag(args, '--yes-lock', ''), wantsJson(args));
3245
3264
  const drive = missionDrive(readFlag(args, '--destination', ''));
3246
3265
  const alwaysOn = hasFlag(args, '--always-on');
3247
3266
  const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
@@ -3278,6 +3297,8 @@ function missionFromArgs(args) {
3278
3297
  stop_condition: stopCondition,
3279
3298
  task_ids: taskIds,
3280
3299
  human_asks: humanAsks,
3300
+ // the sealed owner steps: the guard holds what each locks until the owner says yes
3301
+ ...(yesLocks.length ? { yes_locks: { steps: yesLocks, sealed_at: stampIso() } } : {}),
3281
3302
  ...(campaignId ? { campaign_id: campaignId } : {}),
3282
3303
  ...(drive ? { drive } : {}),
3283
3304
  next_action: verifier ? 'run verifier with `atris mission tick <id> --verify`' : 'define verifier or run next task',
@@ -3296,6 +3317,22 @@ function missionFromArgs(args) {
3296
3317
  return mission;
3297
3318
  }
3298
3319
 
3320
+ // --yes-lock '[{"step":3,"text":"...","categories":["publish"]}]': the owner
3321
+ // steps a job may not do before the owner says yes. Bad input stops the start.
3322
+ const YES_LOCK_CATEGORIES = new Set(['publish', 'tag', 'post', 'send', 'merge', 'deploy']);
3323
+ function readYesLockSeal(raw, asJson) {
3324
+ if (!raw) return [];
3325
+ let list;
3326
+ try {
3327
+ list = JSON.parse(raw);
3328
+ } catch {
3329
+ exitMissionError('--yes-lock takes a JSON list of {step, text, categories}', 2, asJson);
3330
+ }
3331
+ const ok = Array.isArray(list) && list.every(l => l && Number.isInteger(l.step) && l.step > 0 && Array.isArray(l.categories) && l.categories.length && l.categories.every(c => YES_LOCK_CATEGORIES.has(c)));
3332
+ if (!ok) exitMissionError('--yes-lock takes a JSON list of {step, text, categories}, with categories from publish, tag, post, send, merge, deploy', 2, asJson);
3333
+ return list.map(l => ({ step: l.step, text: String(l.text || '').slice(0, 200), categories: [...new Set(l.categories)] }));
3334
+ }
3335
+
3299
3336
  function applyMissionStartPatch(mission, patch) {
3300
3337
  if (!patch || typeof patch !== 'object') return mission;
3301
3338
  const { metadata, ...rest } = patch;
@@ -9440,7 +9477,7 @@ function spawnGenericRunnerTick(mission, opts) {
9440
9477
  }
9441
9478
  let gitGuard;
9442
9479
  try {
9443
- gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
9480
+ gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd), missionId: mission.id, yesLockSeal: (mission.yes_locks && mission.yes_locks.steps) || [], stateDir: path.join(require('../lib/state-home').atrisHome(), 'state') });
9444
9481
  } catch (error) {
9445
9482
  resolve({
9446
9483
  ok: false,
@@ -9625,7 +9662,7 @@ function spawnClaudeTick(mission, opts) {
9625
9662
  return new Promise((resolve) => {
9626
9663
  let gitGuard;
9627
9664
  try {
9628
- gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
9665
+ gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd), missionId: mission.id, yesLockSeal: (mission.yes_locks && mission.yes_locks.steps) || [], stateDir: path.join(require('../lib/state-home').atrisHome(), 'state') });
9629
9666
  } catch (error) {
9630
9667
  resolve({
9631
9668
  ok: false,
@@ -10453,7 +10490,7 @@ async function executeMissionRunTicksPhase(context) {
10453
10490
  const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pendingMissionPings(mission), cwd);
10454
10491
  let atris2GitGuard = null;
10455
10492
  try {
10456
- atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
10493
+ atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd), missionId: mission.id, yesLockSeal: (mission.yes_locks && mission.yes_locks.steps) || [], stateDir: path.join(require('../lib/state-home').atrisHome(), 'state') });
10457
10494
  } catch (error) {
10458
10495
  result = {
10459
10496
  ...result,
@@ -11281,12 +11318,23 @@ async function runMission(args) {
11281
11318
  context.error = error;
11282
11319
  throw error;
11283
11320
  } finally {
11284
- if (context.lock) completeMissionRunPhase(context);
11321
+ if (context.lock) {
11322
+ completeMissionRunPhase(context);
11323
+ sweepMergedMemberWork(context);
11324
+ }
11285
11325
  }
11286
11326
 
11287
11327
  return context.returnValue;
11288
11328
  }
11289
11329
 
11330
+ // Merged member work closes its jobs even when the team loop is down.
11331
+ function sweepMergedMemberWork(context) {
11332
+ try {
11333
+ const out = require('../lib/member-autoland').sweepAfterMissionRun({ cwd: context.cwd || process.cwd() });
11334
+ if (out && out.closed > 0 && !context.asJson) console.log(out.line);
11335
+ } catch {}
11336
+ }
11337
+
11290
11338
  function tickMission(args) {
11291
11339
  const asJson = wantsJson(args);
11292
11340
  if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
@@ -12724,6 +12772,7 @@ function missionCommand(args) {
12724
12772
  }
12725
12773
 
12726
12774
  module.exports = {
12775
+ crystallizeMemberSkillLesson,
12727
12776
  listSiblingWorkspaceMissionHints,
12728
12777
  missionProtectedLaneHold,
12729
12778
  missionCommand,
package/commands/sync.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
3
+ const { stateHome } = require('../lib/state-home');
4
4
  const { ensureWikiScaffold } = require('../lib/wiki');
5
5
  const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
6
6
  const { ensureRunIgnore } = require('../lib/gitignore');
@@ -809,7 +809,7 @@ function syncSkills({ silent = false } = {}) {
809
809
  }
810
810
 
811
811
  let updated = 0;
812
- const homeDir = os.homedir();
812
+ const homeDir = stateHome();
813
813
 
814
814
  const skillFolders = fs.readdirSync(packageSkillsDir).filter(f =>
815
815
  fs.statSync(path.join(packageSkillsDir, f)).isDirectory()
package/commands/task.js CHANGED
@@ -29,10 +29,10 @@ const {
29
29
  parseVerifyChain,
30
30
  parseVerifyCommand,
31
31
  runVerifyCommand,
32
- runVerifyCommandCached,
33
32
  DENIED_TAGS,
34
33
  } = require('../lib/auto-accept-certified');
35
34
  const { evaluateAcceptVerify } = require('../lib/accept-verify-gate');
35
+ const { checkedCodeSha, runVerifyAtJobCode } = require('../lib/job-code-verify');
36
36
  const { proveTaskLanding } = require('../lib/landing-proof');
37
37
  const {
38
38
  taskExplanation,
@@ -10645,22 +10645,44 @@ function stampVerifyCacheMetadata(taskDb, db, taskId, { verify, result, actor, w
10645
10645
  reason: (result && result.reason) || null,
10646
10646
  ran_at: new Date().toISOString(),
10647
10647
  ran_by: actor,
10648
- code_sha: currentHeadSha(workspaceRoot),
10648
+ // The commit the check ran on: the job's own code when it has some,
10649
+ // else the folder the job lives in.
10650
+ code_sha: (result && result.code_sha) || currentHeadSha(workspaceRoot),
10651
+ code_source: (result && result.code_source) || 'checkout',
10649
10652
  };
10653
+ // A check is not a touch. Bumping updated_at here put every failed job at
10654
+ // the front of the next pass, so the same six failing jobs won every hour.
10650
10655
  db.prepare(`
10651
10656
  UPDATE tasks
10652
- SET metadata = ?,
10653
- updated_at = ?
10657
+ SET metadata = ?
10654
10658
  WHERE id = ?
10655
- `).run(JSON.stringify(metadata), Date.now(), taskId);
10659
+ `).run(JSON.stringify(metadata), taskId);
10660
+ }
10661
+
10662
+ // When a review job was last given a second check; never checked reads 0.
10663
+ function lastCheckedMs(task) {
10664
+ const ranAt = Date.parse(String(task?.metadata?.verify_cache?.ran_at || ''));
10665
+ return Number.isFinite(ranAt) ? ranAt : 0;
10666
+ }
10667
+
10668
+ // The check already failed on exactly this code: running it again proves
10669
+ // nothing until the code changes.
10670
+ function failedOnSameCode(task, verify, codeSha) {
10671
+ const cache = task?.metadata?.verify_cache;
10672
+ return Boolean(cache && typeof cache === 'object' && cache.ok === false && cache.reason === 'verify_failed'
10673
+ && cache.command === verify && codeSha && cache.code_sha === codeSha);
10656
10674
  }
10657
10675
 
10658
10676
  function cachedVerifyVerdict(task, root) {
10659
10677
  const metadata = (task && task.metadata) || {};
10660
10678
  const cache = metadata.verify_cache;
10661
10679
  if (!cache || typeof cache !== 'object') return { fresh: false, command: metadata.verify || null };
10680
+ // A check run on the job's own code stays fresh until that code changes.
10681
+ const currentCode = cache.code_source && cache.code_source !== 'checkout'
10682
+ ? checkedCodeSha(task, root)
10683
+ : currentHeadSha(task.workspace_root || root);
10662
10684
  const fresh = cache.command === metadata.verify
10663
- && (!cache.code_sha || cache.code_sha === currentHeadSha(task.workspace_root || root));
10685
+ && (!cache.code_sha || cache.code_sha === currentCode);
10664
10686
  return { ...cache, fresh: Boolean(fresh) };
10665
10687
  }
10666
10688
 
@@ -10711,7 +10733,7 @@ function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier',
10711
10733
  revise_reason: revised.reason || null,
10712
10734
  };
10713
10735
  }
10714
- const result = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
10736
+ const result = runVerifyAtJobCode(verify, task, { root: process.cwd(), cache: verifyCache });
10715
10737
  stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result, actor, workspaceRoot: task.workspace_root });
10716
10738
  if (result.ok) return { ok: true, verify, result };
10717
10739
  const note = landingVerifyFailureNote(verify, result);
@@ -10731,7 +10753,10 @@ function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier',
10731
10753
  }
10732
10754
 
10733
10755
  function cmdCertifyVerified(args, options = {}) {
10734
- const dryRun = hasFlag(args, '--dry-run');
10756
+ // preview: the hourly pass's dry run. Reads the named workspace's jobs and
10757
+ // writes nothing, not even the task list file.
10758
+ const preview = options.preview === true;
10759
+ const dryRun = preview || hasFlag(args, '--dry-run');
10735
10760
  const asJson = wantsJson(args);
10736
10761
  const silent = options.silent === true;
10737
10762
  const verifyCache = options.verifyCache || null;
@@ -10741,13 +10766,18 @@ function cmdCertifyVerified(args, options = {}) {
10741
10766
 
10742
10767
  const taskDb = getTaskDb();
10743
10768
  const db = taskDb.open();
10744
- const { projection } = writeDefaultProjection(taskDb, db);
10769
+ const projection = preview
10770
+ ? enrichTaskProjection(taskDb.taskProjection(db, { workspaceRoot: options.workspaceRoot || taskDb.workspaceRoot(), limit: 500 }))
10771
+ : writeDefaultProjection(taskDb, db).projection;
10772
+ // Least recently checked first, oldest first among never-checked jobs. The
10773
+ // limit counts checks that actually run, so skipped rows never use a turn.
10745
10774
  const candidates = (projection.tasks || [])
10746
10775
  .filter((t) => t && t.status === 'review')
10747
- .sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
10748
- .slice(0, max);
10776
+ .sort((a, b) => lastCheckedMs(a) - lastCheckedMs(b) || Number(a.updated_at || 0) - Number(b.updated_at || 0));
10749
10777
  const results = [];
10778
+ let checks = 0;
10750
10779
  for (const item of candidates) {
10780
+ if (checks >= max) break;
10751
10781
  const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
10752
10782
  const task = fullProjection.tasks[0] || null;
10753
10783
  if (!task) {
@@ -10809,11 +10839,16 @@ function cmdCertifyVerified(args, options = {}) {
10809
10839
  results.push({ ref, action: 'skipped', reason: 'no_runnable_check_in_proof' });
10810
10840
  continue;
10811
10841
  }
10842
+ if (failedOnSameCode(task, verify, checkedCodeSha(task, process.cwd()))) {
10843
+ results.push({ ref, action: 'skipped', reason: 'failed_on_this_code', verify });
10844
+ continue;
10845
+ }
10846
+ checks += 1;
10812
10847
  if (dryRun) {
10813
10848
  results.push({ ref, action: 'would_certify', verify });
10814
10849
  continue;
10815
10850
  }
10816
- const run = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
10851
+ const run = runVerifyAtJobCode(verify, task, { root: process.cwd(), cache: verifyCache });
10817
10852
  stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result: run, actor, workspaceRoot: task.workspace_root });
10818
10853
  if (!run.ok) {
10819
10854
  results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
@@ -10840,7 +10875,7 @@ function cmdCertifyVerified(args, options = {}) {
10840
10875
  const certifiedNow = readied.event?.payload?.agent_certified === true;
10841
10876
  results.push({ ref, action: certifiedNow ? 'certified' : 'pass_recorded', verify });
10842
10877
  }
10843
- const { outPath } = writeDefaultProjection(taskDb, db);
10878
+ const outPath = preview ? null : writeDefaultProjection(taskDb, db).outPath;
10844
10879
  const certified = results.filter((r) => r.action === 'certified').length;
10845
10880
  const payload = {
10846
10881
  ok: true,
@@ -13698,6 +13733,7 @@ module.exports = {
13698
13733
  delegateTask,
13699
13734
  AGENT_ENV_MARKERS,
13700
13735
  autoRenderTodoFromDb,
13736
+ cmdCertifyVerified,
13701
13737
  refreshKeptTaskList,
13702
13738
  keptCount,
13703
13739
  projectionMissions,
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ const { abandonedDaysFromEnv, checkZen, sideCopyLines } = require('../lib/zen');
4
+ const { applyZen } = require('../lib/zen-apply');
5
+ const { workspaceZen } = require('../lib/zen-workspace');
6
+
7
+ function showZenHelp() {
8
+ console.log('usage: atris zen [--check] [--no-fetch] [--json]');
9
+ console.log('');
10
+ console.log('Brings this repo to zen: code matches origin, nothing left lying around.');
11
+ console.log('Old stashes are archived, idle side copies put away, finished branches');
12
+ console.log('cleared, stray code edits saved to a zen-backup/ branch, and the main');
13
+ console.log('branch caught up. Only branches already on origin are cleared, so nothing');
14
+ console.log('is lost, and everything else can be brought back. Team notes');
15
+ console.log('in atris/ and agent config stay as they are. Anything touched in the last');
16
+ console.log('30 minutes is left alone. A side copy with unsaved work nobody has touched in');
17
+ console.log('3 days is saved to a zen-backup/ branch and put away. A side copy locked with git worktree lock');
18
+ console.log('always stays. Then the workspace is tidied with atris clean, and');
19
+ console.log('anything only you can do is listed with the one command that does it.');
20
+ console.log('');
21
+ console.log(' --check Only look. Report what is out of place and change nothing.');
22
+ console.log(' --no-fetch Use the last fetched origin instead of fetching.');
23
+ console.log(' --json Print machine-readable results.');
24
+ console.log('');
25
+ console.log('Add more team-state paths in .atris/zen.json: {"team_state": ["logs/"]}');
26
+ }
27
+
28
+ function pause(ms) {
29
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
30
+ }
31
+
32
+ function makeOut(calm) {
33
+ const dim = (text) => (calm ? `\x1b[2m${text}\x1b[0m` : text);
34
+ const line = (text = '', ms = 110) => {
35
+ console.log(text);
36
+ if (calm && text) pause(ms);
37
+ };
38
+ // One raked line, drawn left to right.
39
+ const rake = (width = 34) => {
40
+ if (!calm) {
41
+ console.log(` ${'─'.repeat(width)}`);
42
+ return;
43
+ }
44
+ process.stdout.write(' ');
45
+ for (let i = 0; i < width; i += 1) {
46
+ process.stdout.write(dim('─'));
47
+ pause(14);
48
+ }
49
+ process.stdout.write('\n');
50
+ pause(160);
51
+ };
52
+ return { dim, line, rake };
53
+ }
54
+
55
+ function teamLine(n) {
56
+ if (!n) return 'no team notes changed.';
57
+ return n === 1 ? '1 team note kept as it is.' : `${n} team notes kept as they are.`;
58
+ }
59
+
60
+ function printYours(yours, line, dim) {
61
+ const width = Math.max(...yours.map((item) => item.text.length));
62
+ for (const item of yours) line(` ${item.text.padEnd(width)} ${dim(item.cmd)}`);
63
+ }
64
+
65
+ function printCheck(result, workspace) {
66
+ const kept = result.team_files
67
+ ? (result.team_files === 1 ? '1 team file kept as it is' : `${result.team_files} team files kept as they are`)
68
+ : 'no team files changed';
69
+ const stale = result.fetched === false ? ' (could not reach origin, using last fetch)' : '';
70
+ const tidy = workspace?.todo.length || 0;
71
+ if (result.zen && tidy) {
72
+ console.log(`zen: almost, the code is calm and the workspace has ${tidy} ${tidy === 1 ? 'thing' : 'things'} to tidy`);
73
+ console.log(` code matches ${result.upstream}${stale}`);
74
+ console.log(` ${kept}`);
75
+ console.log(' run atris zen to make it zen');
76
+ } else if (result.zen) {
77
+ console.log('zen: yes');
78
+ console.log(` code matches ${result.upstream}${stale}`);
79
+ console.log(` ${kept}`);
80
+ } else {
81
+ const n = result.problems.length;
82
+ console.log(`zen: no, ${n} ${n === 1 ? 'thing is' : 'things are'} out of place${stale}`);
83
+ for (const problem of result.problems) console.log(` - ${problem.text}`);
84
+ console.log(` ${kept}`);
85
+ console.log(' run atris zen to make it zen');
86
+ }
87
+ if (result.worktrees_with_work.length) {
88
+ for (const text of sideCopyLines(result.worktrees_with_work, abandonedDaysFromEnv())) console.log(` ${text}`);
89
+ }
90
+ const held = result.worktrees_locked?.length || 0;
91
+ if (held) {
92
+ console.log(` ${held} side ${held === 1 ? 'copy is locked by its owner' : 'copies are locked by their owners'}, left alone`);
93
+ }
94
+ const live = result.worktrees_in_use?.length || 0;
95
+ if (live) {
96
+ const mins = result.fresh_minutes;
97
+ console.log(` ${live} side ${live === 1 ? 'copy was' : 'copies were'} used in the last ${mins} ${mins === 1 ? 'minute' : 'minutes'}, left alone`);
98
+ }
99
+ if (workspace?.todo.length) {
100
+ console.log('workspace:');
101
+ for (const text of workspace.todo) console.log(` - ${text}`);
102
+ }
103
+ if (workspace?.yours.length) {
104
+ console.log('for you:');
105
+ printYours(workspace.yours, (text) => console.log(text), (text) => text);
106
+ }
107
+ }
108
+
109
+ function printApply(result, out) {
110
+ const { dim, line, rake } = out;
111
+ line('');
112
+ line(' atris zen', 200);
113
+ line('');
114
+ rake();
115
+ line('');
116
+ line(dim(' code'));
117
+ if (!result.done.length && result.zen) line(` matches ${result.upstream || 'origin'}`);
118
+ for (const text of result.done) line(` ${text}`);
119
+ for (const text of [...result.left, ...result.notes]) line(dim(` ${text}`));
120
+ line('');
121
+ const ws = result.workspace || { done: [], yours: [] };
122
+ if (ws.ran) {
123
+ line(dim(' workspace'));
124
+ if (!ws.done.length) line(' tidy');
125
+ for (const text of ws.done) line(` ${text}`);
126
+ line('');
127
+ }
128
+ if (ws.yours?.length) {
129
+ line(dim(' for you'));
130
+ printYours(ws.yours, line, dim);
131
+ line('');
132
+ }
133
+ if (result.zen) {
134
+ line(` ${result.already ? 'already zen.' : 'zen.'}`, 260);
135
+ } else {
136
+ line(' almost zen. still out of place:');
137
+ for (const text of result.remaining) line(dim(` - ${text}`));
138
+ }
139
+ line(dim(` ${teamLine(result.team_files)}`));
140
+ if (result.saved.length) {
141
+ line(dim(' everything is kept: git for-each-ref refs/archive refs/heads/zen-backup'));
142
+ }
143
+ line('');
144
+ }
145
+
146
+ function zenCommand(args = [], cwd = process.cwd()) {
147
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
148
+ showZenHelp();
149
+ return 0;
150
+ }
151
+ const fetch = !args.includes('--no-fetch');
152
+ const json = args.includes('--json');
153
+
154
+ if (args.includes('--check')) {
155
+ const result = checkZen({ cwd, fetch });
156
+ if (json) {
157
+ console.log(JSON.stringify(result, null, 2));
158
+ return result.ok ? (result.zen ? 0 : 1) : 2;
159
+ }
160
+ if (!result.ok) {
161
+ console.log(`zen: can't check, ${result.error}.`);
162
+ return 2;
163
+ }
164
+ const workspace = workspaceZen({ root: result.root, dryRun: true });
165
+ printCheck(result, workspace);
166
+ return result.zen && !workspace.todo.length ? 0 : 1;
167
+ }
168
+
169
+ const result = applyZen({ cwd, fetch });
170
+ if (json) {
171
+ console.log(JSON.stringify(result, null, 2));
172
+ return result.ok ? (result.zen ? 0 : 1) : 2;
173
+ }
174
+ if (!result.ok) {
175
+ console.log(`zen: can't start, ${result.error}.`);
176
+ return 2;
177
+ }
178
+ const calm = Boolean(process.stdout.isTTY) && !process.env.CI;
179
+ printApply(result, makeOut(calm));
180
+ return result.zen ? 0 : 1;
181
+ }
182
+
183
+ module.exports = { zenCommand, showZenHelp };
@@ -1157,8 +1157,13 @@ function agentBranchOnlyResult(ref, resolved) {
1157
1157
  };
1158
1158
  }
1159
1159
 
1160
- function runResolvedVerify(resolved, workspaceRoot, cache) {
1161
- const result = runVerifyCommandCached(resolved.verify, workspaceRoot, cache);
1160
+ // A check recorded from main runs against the job's own code, the same place
1161
+ // the second check ran it (lib/job-code-verify.js), so the landing gate never
1162
+ // judges a branch-only test by the main folder.
1163
+ function runResolvedVerify(resolved, task, workspaceRoot, cache) {
1164
+ const result = resolved.rewritten
1165
+ ? runVerifyCommandCached(resolved.verify, workspaceRoot, cache)
1166
+ : require('./job-code-verify').runVerifyAtJobCode(resolved.verify, task, { root: workspaceRoot, cache });
1162
1167
  if (resolved.rewritten && result.reason === 'verify_failed') {
1163
1168
  return { ...result, reason: 'verify_failed_on_main', recorded_verify: resolved.recorded };
1164
1169
  }
@@ -1258,7 +1263,7 @@ function evaluateAutoAccept(task, options = {}) {
1258
1263
  if (tierRequiresStrictVerify && !verify) return strictVerifyMissingResult(ref);
1259
1264
  if (resolvedVerify.missing) return agentBranchOnlyResult(ref, resolvedVerify);
1260
1265
  if (verify && executeVerify) {
1261
- const verifyResult = runResolvedVerify(resolvedVerify, task.workspace_root || process.cwd(), verifyCache);
1266
+ const verifyResult = runResolvedVerify(resolvedVerify, task, task.workspace_root || process.cwd(), verifyCache);
1262
1267
  // A check that runs and fails blocks. So does a check whose worktree
1263
1268
  // is gone, otherwise the daily reap converts "has a failing check"
1264
1269
  // into "lands unchecked" the morning after it clears the worktree.
@@ -1361,7 +1366,7 @@ function evaluateAutoAccept(task, options = {}) {
1361
1366
  if (resolvedVerify.missing) return agentBranchOnlyResult(ref, resolvedVerify);
1362
1367
  if (executeVerify) {
1363
1368
  const workspaceRoot = task.workspace_root || process.cwd();
1364
- const verifyResult = runResolvedVerify(resolvedVerify, workspaceRoot, verifyCache);
1369
+ const verifyResult = runResolvedVerify(resolvedVerify, task, workspaceRoot, verifyCache);
1365
1370
  if (!verifyResult.ok) {
1366
1371
  return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
1367
1372
  }