atris 3.69.0 → 3.71.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 (79) hide show
  1. package/README.md +40 -1
  2. package/atris/skills/README.md +1 -0
  3. package/atris/skills/campaigns/SKILL.md +81 -0
  4. package/atris/skills/meta/SKILL.md +52 -8
  5. package/ax +226 -42
  6. package/bin/atris.js +26 -6
  7. package/commands/autoland.js +176 -7
  8. package/commands/autopilot.js +71 -11
  9. package/commands/campaign.js +759 -0
  10. package/commands/compile.js +19 -8
  11. package/commands/doctor.js +58 -0
  12. package/commands/dream.js +18 -3
  13. package/commands/engine.js +147 -5
  14. package/commands/improve.js +119 -9
  15. package/commands/init.js +18 -1
  16. package/commands/interview.js +15 -3
  17. package/commands/land.js +22 -6
  18. package/commands/link.js +1 -0
  19. package/commands/member.js +433 -99
  20. package/commands/mission.js +332 -41
  21. package/commands/report.js +42 -8
  22. package/commands/run.js +9 -2
  23. package/commands/scout.js +9 -1
  24. package/commands/task.js +132 -16
  25. package/commands/team.js +212 -4
  26. package/commands/unknowns.js +15 -1
  27. package/commands/verify.js +46 -0
  28. package/commands/worktree.js +148 -18
  29. package/commands/write.js +12 -1
  30. package/commands/youtube.js +17 -2
  31. package/lib/accept-verify-gate.js +1 -0
  32. package/lib/auto-accept-certified.js +1 -0
  33. package/lib/autoland.js +13 -4
  34. package/lib/campaign-cadence.js +131 -0
  35. package/lib/campaign-core.js +330 -0
  36. package/lib/campaign-holdout.js +65 -0
  37. package/lib/campaign-loop.js +242 -0
  38. package/lib/campaign-missions.js +237 -0
  39. package/lib/campaign-new.js +251 -0
  40. package/lib/campaign-protect.js +75 -0
  41. package/lib/campaign-reading.js +147 -0
  42. package/lib/campaign-schema.js +196 -0
  43. package/lib/campaign-store.js +380 -0
  44. package/lib/campaign-sync.js +302 -0
  45. package/lib/campaign-trace.js +300 -0
  46. package/lib/conductor-artifacts.js +15 -1
  47. package/lib/daily-log.js +11 -1
  48. package/lib/dispatch-scout.js +18 -0
  49. package/lib/engine-ask.js +35 -4
  50. package/lib/engine-registry.js +2 -0
  51. package/lib/engine-resource.js +518 -0
  52. package/lib/engine-validate.js +189 -9
  53. package/lib/fleet.js +125 -16
  54. package/lib/init-commit.js +80 -0
  55. package/lib/intents.js +8 -0
  56. package/lib/known-commands.js +1 -1
  57. package/lib/land-green.js +58 -7
  58. package/lib/landing-proof.js +352 -0
  59. package/lib/member-autoland.js +479 -10
  60. package/lib/member-board.js +86 -10
  61. package/lib/member-map-fold.js +126 -0
  62. package/lib/member-map-inbox.js +80 -0
  63. package/lib/member-progress.js +48 -1
  64. package/lib/member-report.js +433 -0
  65. package/lib/member-scaffold.js +1 -2
  66. package/lib/member-self-next-job.js +97 -0
  67. package/lib/member-switches.js +7 -0
  68. package/lib/member-task-pick.js +41 -14
  69. package/lib/member-up.js +249 -16
  70. package/lib/roster-prep.js +25 -2
  71. package/lib/runner-command.js +5 -0
  72. package/lib/task-db.js +63 -11
  73. package/lib/task-explanation.js +7 -0
  74. package/lib/task-list-keeper.js +8 -1
  75. package/lib/task-receipt.js +6 -1
  76. package/mcp/atris-mcp/index.mjs +212 -10
  77. package/package.json +2 -1
  78. package/scripts/det/engine-reading.js +37 -0
  79. package/scripts/det/ytnotes +29 -5
@@ -88,22 +88,38 @@ function withWorkspaceCwd(root, fn) {
88
88
  }
89
89
  }
90
90
 
91
- function buildLandings(root, cutoff) {
91
+ // A done task is only a landing when its stored git proof says so. Done
92
+ // without that proof is reported separately, never counted as landed.
93
+ function buildDoneTasks(root, cutoff) {
92
94
  const tasks = readProjection(root) || [];
93
95
  return tasks
94
96
  .filter(task => task && task.status === 'done' && Number(timestampMs(task.done_at)) >= cutoff)
95
97
  .sort((a, b) => Number(timestampMs(b.done_at) || 0) - Number(timestampMs(a.done_at) || 0))
96
98
  .map((task) => {
97
99
  const id = inlineText(task.display_id || task.task_ref || task.id, 'task');
100
+ const landing = task.metadata && typeof task.metadata.landing === 'object' ? task.metadata.landing : null;
98
101
  return {
99
102
  id,
100
103
  title: inlineText(task.title, 'untitled task'),
101
104
  done_at: task.done_at || null,
102
105
  source: `task ${id}`,
106
+ landed: landing?.status === 'landed',
107
+ landing_status: landing?.status || 'unchecked',
108
+ landing_reason: landing?.reason || (landing ? '' : 'no landing proof recorded'),
103
109
  };
104
110
  });
105
111
  }
106
112
 
113
+ function buildLandings(root, cutoff) {
114
+ return buildDoneTasks(root, cutoff).filter(task => task.landed);
115
+ }
116
+
117
+ function landingStatusText(task) {
118
+ const status = String(task?.landing_status || 'unchecked').replace(/_/g, ' ');
119
+ const reason = String(task?.landing_reason || '').replace(/_/g, ' ');
120
+ return reason ? `${status}, ${reason}` : status;
121
+ }
122
+
107
123
  function buildJournalCompletions(root, days, now) {
108
124
  return withWorkspaceCwd(root, () => {
109
125
  const completions = [];
@@ -198,13 +214,16 @@ function buildWeekReportData(root = process.cwd(), { days = DEFAULT_DAYS, now =
198
214
  const windowDays = asWindowDays(days);
199
215
  const nowMs = timestampMs(now) || Date.now();
200
216
  const cutoff = nowMs - windowDays * DAY_MS;
201
- const landings = buildLandings(root, cutoff);
217
+ const done = buildDoneTasks(root, cutoff);
218
+ const landings = done.filter(task => task.landed);
219
+ const unproven = done.filter(task => !task.landed);
202
220
  const completions = buildJournalCompletions(root, windowDays, nowMs);
203
221
  const xp = buildXp(root, cutoff);
204
222
  return {
205
- empty: landings.length === 0 && completions.length === 0 && xp.total <= 0,
223
+ empty: done.length === 0 && completions.length === 0 && xp.total <= 0,
206
224
  days: windowDays,
207
225
  landings,
226
+ unproven,
208
227
  completions,
209
228
  xp,
210
229
  };
@@ -212,11 +231,13 @@ function buildWeekReportData(root = process.cwd(), { days = DEFAULT_DAYS, now =
212
231
 
213
232
  function renderWeekReport(data) {
214
233
  const landings = Array.isArray(data?.landings) ? data.landings : [];
234
+ const unproven = Array.isArray(data?.unproven) ? data.unproven : [];
215
235
  const completions = Array.isArray(data?.completions) ? data.completions : [];
216
236
  const xp = data?.xp && typeof data.xp === 'object' ? data.xp : { total: 0, receipts: [] };
217
237
  const receipts = Array.isArray(xp.receipts) ? xp.receipts : [];
238
+ const unprovenHeader = unproven.length ? `, ${unproven.length} done but not proven landed` : '';
218
239
  const lines = [
219
- `week in review: ${landings.length} landed, ${completions.length} completions, ${formatXp(xp.total)} xp`,
240
+ `week in review: ${landings.length} landed${unprovenHeader}, ${completions.length} completions, ${formatXp(xp.total)} xp`,
220
241
  ];
221
242
 
222
243
  if (data?.empty) {
@@ -227,12 +248,20 @@ function renderWeekReport(data) {
227
248
 
228
249
  if (landings.length) {
229
250
  lines.push('');
230
- lines.push(`landings: ${landings.length} tasks, known from task projection`);
251
+ lines.push(`landings: ${landings.length} tasks, proven by a git landing check`);
231
252
  for (const landing of landings) {
232
253
  lines.push(`- landed: ${inlineText(landing.title)} | size: 1 task | know: ${inlineText(landing.source || landing.id, 'task')}`);
233
254
  }
234
255
  }
235
256
 
257
+ if (unproven.length) {
258
+ lines.push('');
259
+ lines.push(`done but not proven landed: ${unproven.length} tasks, git could not confirm the work reached the target`);
260
+ for (const task of unproven) {
261
+ lines.push(`- done: ${inlineText(task.title)} | landing: ${inlineText(landingStatusText(task))} | know: ${inlineText(task.source || task.id, 'task')}`);
262
+ }
263
+ }
264
+
236
265
  if (completions.length) {
237
266
  lines.push('');
238
267
  lines.push(`journal completions: ${completions.length} items, known from daily logs`);
@@ -258,6 +287,7 @@ function panelRows(rows, emptyTitle, emptySub) {
258
287
 
259
288
  function buildWeekReportHtmlSpec(data) {
260
289
  const landings = Array.isArray(data?.landings) ? data.landings : [];
290
+ const unproven = Array.isArray(data?.unproven) ? data.unproven : [];
261
291
  const completions = Array.isArray(data?.completions) ? data.completions : [];
262
292
  const xp = data?.xp && typeof data.xp === 'object' ? data.xp : { total: 0, receipts: [] };
263
293
  const receipts = Array.isArray(xp.receipts) ? xp.receipts : [];
@@ -277,18 +307,22 @@ function buildWeekReportHtmlSpec(data) {
277
307
  {
278
308
  type: 'panel',
279
309
  heading: 'landings',
280
- sub: 'done tasks from the local task projection',
310
+ sub: 'done tasks whose git landing check passed; the rest are marked not proven',
281
311
  panel: {
282
312
  header: {
283
313
  title: 'task landings',
284
- meta: `${landings.length} ${plural(landings.length, 'task')}`,
314
+ meta: `${landings.length} landed${unproven.length ? `, ${unproven.length} not proven` : ''}`,
285
315
  },
286
316
  rows: panelRows(
287
317
  landings.map((landing) => ({
288
318
  title: `${inlineText(landing.id, 'task')}: ${lowerText(landing.title, 'untitled task')}`,
289
319
  sub: lowerText(landing.source || landing.id, 'task'),
290
320
  sev: 0,
291
- })),
321
+ })).concat(unproven.map((task) => ({
322
+ title: `${inlineText(task.id, 'task')}: ${lowerText(task.title, 'untitled task')}`,
323
+ sub: `not proven landed: ${lowerText(landingStatusText(task), 'unchecked')}`,
324
+ sev: 2,
325
+ }))),
292
326
  'no task landings found',
293
327
  'task projection had no done items in this window'
294
328
  ),
package/commands/run.js CHANGED
@@ -223,12 +223,16 @@ function executePhase(phase, context, options = {}) {
223
223
  const prompt = buildRunPrompt(phase, context, priorCycleReview);
224
224
  const tmpFile = path.join(process.cwd(), '.run-prompt.tmp');
225
225
  fs.writeFileSync(tmpFile, prompt);
226
+ let endResource = () => null;
226
227
 
227
228
  try {
228
229
  const cmd = buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Bash,Read,Write,Edit,Glob,Grep' });
229
230
  // Strip CLAUDECODE env var to allow spawning from within a Claude Code session
230
231
  const env = { ...process.env };
231
232
  delete env.CLAUDECODE;
233
+ // Machine memory and load at both ends of the launch, so a phase killed
234
+ // with SIGKILL says whether the machine was short on memory.
235
+ endResource = require('../lib/engine-resource').startEngineRun(process.cwd(), { engine: 'runner', task: `run ${phase}` });
232
236
  const output = execPhaseCommandSync(cmd, {
233
237
  cwd: process.cwd(),
234
238
  encoding: 'utf8',
@@ -241,14 +245,17 @@ function executePhase(phase, context, options = {}) {
241
245
  });
242
246
 
243
247
  try { fs.unlinkSync(tmpFile); } catch {}
248
+ endResource();
244
249
  return output || '';
245
250
  } catch (err) {
246
251
  try { fs.unlinkSync(tmpFile); } catch {}
252
+ const resourceEnd = endResource(err);
247
253
  if (isPhaseTimeoutError(err)) {
248
254
  throw new Error(`${phase} timed out after ${timeout / 1000}s`);
249
255
  }
250
256
  if (isPhaseKillError(err)) {
251
- throw new Error(`${phase} killed by ${err.signal || 'a signal'} before the ${timeout / 1000}s wall`);
257
+ const cause = resourceEnd && resourceEnd.cause ? `, ${resourceEnd.cause}` : '';
258
+ throw new Error(`${phase} killed by ${err.signal || 'a signal'} before the ${timeout / 1000}s wall${cause}`);
252
259
  }
253
260
  // execSync throws on non-zero exit but may still have output
254
261
  if (err.stdout) return err.stdout;
@@ -1068,4 +1075,4 @@ function diffRunLogs(args = []) {
1068
1075
  }
1069
1076
  }
1070
1077
 
1071
- module.exports = { runAtris, hasWork, hasInboxOrBacklogWork, roadmapOpenCount, getRunLogDir, getRunLogPath, writePhaseToRunLog, appendMasterLoopReceipt, listRunLogs, pruneRunLogs, searchRunLogs, statsRunLogs, exportRunLogs, diffRunLogs, buildRunPrompt };
1078
+ module.exports = { runAtris, executePhase, hasWork, hasInboxOrBacklogWork, roadmapOpenCount, getRunLogDir, getRunLogPath, writePhaseToRunLog, appendMasterLoopReceipt, listRunLogs, pruneRunLogs, searchRunLogs, statsRunLogs, exportRunLogs, diffRunLogs, buildRunPrompt };
package/commands/scout.js CHANGED
@@ -65,12 +65,16 @@ function runScout(question, { dir, model, engine, timeoutMs }) {
65
65
  return new Promise((resolve) => {
66
66
  const eng = ENGINES[engine];
67
67
  const [cmd, cmdArgs] = eng.argv(scoutPrompt(question, dir), model);
68
+ // Machine memory and load at both ends of the run, so a SIGKILL says
69
+ // whether it was the time limit, low memory, or an unknown cause.
70
+ const endResource = require('../lib/engine-resource').startEngineRun(process.cwd(), { engine, task: 'scout' });
68
71
  const child = spawn(cmd, cmdArgs, { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
69
72
 
70
73
  let out = '';
71
74
  let err = '';
72
75
  const timer = setTimeout(() => {
73
76
  child.kill('SIGKILL');
77
+ endResource({ code: 'ETIMEDOUT', signal: 'SIGKILL' });
74
78
  resolve({ question, ok: false, text: `timed out after ${timeoutMs / 1000}s` });
75
79
  }, timeoutMs);
76
80
 
@@ -78,11 +82,15 @@ function runScout(question, { dir, model, engine, timeoutMs }) {
78
82
  child.stderr.on('data', (d) => { err += d; });
79
83
  child.on('error', (e) => {
80
84
  clearTimeout(timer);
85
+ endResource(e);
81
86
  resolve({ question, ok: false, text: `failed to spawn ${cmd}: ${e.message}` });
82
87
  });
83
- child.on('close', (code) => {
88
+ child.on('close', (code, signal) => {
84
89
  clearTimeout(timer);
90
+ // The child does not throw, so a failure is passed in the thrown shape.
91
+ const end = endResource(code === 0 ? null : { signal, stderr: err, message: `${cmd} exited ${code}` });
85
92
  if (code === 0 && out.trim()) resolve({ question, ok: true, text: eng.parse(out) });
93
+ else if (signal) resolve({ question, ok: false, text: `stopped by ${signal}${end && end.cause ? `; ${end.cause}` : ''}` });
86
94
  else resolve({ question, ok: false, text: (err.trim() || out.trim() || `exit ${code}`) });
87
95
  });
88
96
  });
package/commands/task.js CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  } = require('../lib/task-proof');
23
23
  const {
24
24
  candidatePolicyGate,
25
+ declaredCandidatePathValues,
25
26
  evaluateAutoAccept,
26
27
  isAgentCertified,
27
28
  isAutoCertifyVerifyCommandAllowed,
@@ -32,8 +33,10 @@ const {
32
33
  DENIED_TAGS,
33
34
  } = require('../lib/auto-accept-certified');
34
35
  const { evaluateAcceptVerify } = require('../lib/accept-verify-gate');
36
+ const { proveTaskLanding } = require('../lib/landing-proof');
35
37
  const {
36
38
  taskExplanation,
39
+ explicitDoneLooksLike,
37
40
  explanationFieldsFromInput,
38
41
  explanationLines,
39
42
  taskApprovalControls,
@@ -253,6 +256,7 @@ atris task - durable local task state (SQLite, gitignored)
253
256
  atris task archive <id> --reason "..." [--from-failed]
254
257
  Sweep off-roadmap/duplicate work as archived (not failed);
255
258
  --from-failed opts in to relabel a fail-closed row (never done)
259
+ atris task reopen <id> [--reason "..."] Send a done, waiting, failed, or archived task back to open
256
260
  atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
257
261
  atris task prune-test-junk [--yes] [--vacuum] [--json]
258
262
  Count rows test runs left from temp folders; --yes backs up the db, then removes them
@@ -1636,6 +1640,11 @@ function buildTaskStreams(tasks, goals) {
1636
1640
  // work, one clear way to ask for a change. Every enabled/blocked answer comes
1637
1641
  // from the existing review handoff, so this relabels what the certification
1638
1642
  // rules already allow and can never widen it.
1643
+ function joinPlainList(items) {
1644
+ if (items.length <= 2) return items.join(' and ');
1645
+ return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`;
1646
+ }
1647
+
1639
1648
  function taskApprovalFor(task, { reviewer = 'codex-review', hasExistingReviewFollowUp = null } = {}) {
1640
1649
  const metadata = task && task.metadata || {};
1641
1650
  const approvalStatus = (task && task.review && task.review.approval_status) || metadata.approval_status || null;
@@ -1658,12 +1667,16 @@ function taskApprovalFor(task, { reviewer = 'codex-review', hasExistingReviewFol
1658
1667
  ? reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp })
1659
1668
  : null;
1660
1669
  const planned = current === 'plan';
1661
- const planReady = planned && Boolean(
1662
- (metadata.task_goal || metadata.goal_objective || task.objective)
1663
- && metadata.exit_condition
1664
- && (metadata.verify || metadata.proof_needed)
1665
- && metadata.first_move
1666
- );
1670
+ // Name only the plan parts that are really missing: a job given a check
1671
+ // should not be told it still needs one, and a job that says what done
1672
+ // looks like already has its finish line.
1673
+ const planMissing = [
1674
+ !(metadata.task_goal || metadata.goal_objective || task.objective) && 'a goal',
1675
+ !(metadata.exit_condition || explicitDoneLooksLike(metadata)) && 'a finish line',
1676
+ !(metadata.verify || metadata.proof_needed) && 'a check',
1677
+ !metadata.first_move && 'a first move',
1678
+ ].filter(Boolean);
1679
+ const planReady = planned && planMissing.length === 0;
1667
1680
  const acceptEnabled = inReview ? handoffAllowsHumanAccept(handoff) : planReady;
1668
1681
  const planOwner = task && (task.claimed_by || taskAssignee(task)) || DEFAULT_OWNER;
1669
1682
  const acceptCommand = inReview
@@ -1682,7 +1695,7 @@ function taskApprovalFor(task, { reviewer = 'codex-review', hasExistingReviewFol
1682
1695
  ? 'The proof points at unfinished or unsafe work. Ask for a change before approval.'
1683
1696
  : 'The proof still needs its required checks before a person can approve it.'
1684
1697
  : planned
1685
- ? 'The plan still needs a goal, finish line, check, and first move before approval.'
1698
+ ? `The plan still needs ${joinPlainList(planMissing)} before approval.`
1686
1699
  : current === 'backlog'
1687
1700
  ? 'This task still needs a complete plan before approval.'
1688
1701
  : current === 'do'
@@ -5454,11 +5467,12 @@ function autoAcceptCertifiedSmallReviews(taskDb, db, projection) {
5454
5467
  results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
5455
5468
  continue;
5456
5469
  }
5457
- stampAutoAcceptMetadata(taskDb, db, task.id, REVIEW_AUTO_ACCEPT_ACTOR, REVIEW_AUTO_ACCEPT_POLICY);
5470
+ const landing = stampAutoAcceptMetadata(taskDb, db, task, REVIEW_AUTO_ACCEPT_ACTOR, REVIEW_AUTO_ACCEPT_POLICY);
5458
5471
  refreshCareerXpAfterReview(accepted.reviewed);
5459
5472
  results.push({
5460
5473
  ...evaluation,
5461
5474
  action: 'accepted',
5475
+ landing,
5462
5476
  task_id: task.id,
5463
5477
  reward: accepted.reviewed.episode.reward.value,
5464
5478
  });
@@ -6243,6 +6257,27 @@ function cmdAdd(args) {
6243
6257
  const taskDb = getTaskDb();
6244
6258
  const db = taskDb.open();
6245
6259
  const ws = taskDb.workspaceRoot();
6260
+ // Filing a job that is already open or claimed hands back that job instead
6261
+ // of making a copy. Work already handed in for review can be filed again.
6262
+ const existing = taskDb.findUnfinishedTaskByTitle(db, { workspaceRoot: ws, title });
6263
+ if (existing) {
6264
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
6265
+ const task = compactTaskFromProjection(projection, existing.id);
6266
+ if (wantsJson(args)) {
6267
+ printJson({
6268
+ ok: true,
6269
+ action: 'reused',
6270
+ task_id: existing.id,
6271
+ inserted: false,
6272
+ operator_title_warning: null,
6273
+ projection_path: outPath,
6274
+ task,
6275
+ });
6276
+ return;
6277
+ }
6278
+ console.log(`${taskRef(task)}\t${existing.title}\t(already on the list, reused)`);
6279
+ return;
6280
+ }
6246
6281
  const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
6247
6282
  // Generation throttle, the named root cause is generation > human-review rate. An AGENT cannot keep
6248
6283
  // minting tasks while a wall of certified-but-unaccepted work waits; that treadmill is what accept-group
@@ -7898,9 +7933,13 @@ function taskDetail(taskDb, db, taskId) {
7898
7933
  };
7899
7934
  }
7900
7935
 
7936
+ // Printed lines get pasted into a shell. Plain words keep readable double
7937
+ // quotes; text a shell would act on ($, backticks, \, !) is single-quoted so
7938
+ // pasting it runs no command and keeps the words as written.
7901
7939
  function taskCommandQuote(value) {
7902
- const text = String(value || '').replace(/\s+/g, ' ').replace(/"/g, '\\"').trim();
7903
- return `"${text || '...'}"`;
7940
+ const text = String(value || '').replace(/\s+/g, ' ').trim() || '...';
7941
+ if (/[$`\\!]/.test(text)) return `'${text.replace(/'/g, `'\\''`)}'`;
7942
+ return `"${text.replace(/"/g, '\\"')}"`;
7904
7943
  }
7905
7944
 
7906
7945
  function cleanPublicText(value, max = 500) {
@@ -9738,6 +9777,45 @@ function cmdArchive(args) {
9738
9777
  }
9739
9778
  }
9740
9779
 
9780
+ // Send a done, failed, or archived task back to open so it can be claimed
9781
+ // again. Review sign-offs are cleared; the old status and reason are kept.
9782
+ function cmdReopen(args) {
9783
+ const pos = positional(args);
9784
+ const id = pos[0];
9785
+ if (!id) failTask('atris task reopen', 'missing_id', 'task id required');
9786
+ const reason = textFlag(args, ['--reason']) || 'reopened';
9787
+ const taskDb = getTaskDb();
9788
+ const db = taskDb.open();
9789
+ const taskId = requireTaskId(taskDb, db, id, 'atris task reopen');
9790
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
9791
+ const result = taskDb.reopenTask(db, { id: taskId, actor, reason, fromReview: true });
9792
+ if (!result.reopened) {
9793
+ const detail = `reopen failed: ${taskId} ${result.reason}`;
9794
+ if (wantsJson(args)) {
9795
+ printJson({ ok: false, command: 'atris task reopen', reason: result.reason, task_id: taskId, detail });
9796
+ process.exit(1);
9797
+ }
9798
+ console.error(detail);
9799
+ process.exit(1);
9800
+ }
9801
+ const reopenedFrom = result.row && result.row.metadata && result.row.metadata.reopened_from || null;
9802
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
9803
+ const task = compactTaskFromProjection(projection, taskId);
9804
+ if (wantsJson(args)) {
9805
+ printJson({
9806
+ ok: true,
9807
+ action: 'reopened',
9808
+ task_id: taskId,
9809
+ reopened_from: reopenedFrom,
9810
+ reason,
9811
+ projection_path: outPath,
9812
+ task,
9813
+ });
9814
+ return;
9815
+ }
9816
+ console.log(`reopened ${taskRef(task)}${reopenedFrom ? ` (was ${reopenedFrom})` : ''}: ${reason}`);
9817
+ }
9818
+
9741
9819
  function taskCompletionTime(row) {
9742
9820
  const doneAt = Number(row && row.done_at);
9743
9821
  if (Number.isFinite(doneAt) && doneAt > 0) return doneAt;
@@ -10151,7 +10229,9 @@ function cmdReady(args) {
10151
10229
  ? `double-check complete; autoland will accept this ${whenLands}.`
10152
10230
  : needsExternalVerifier
10153
10231
  ? 'proof is ready; this verifier needs a second agent review because autoland cannot rerun it.'
10154
- : `proof is ready; autoland runs the second check and lands it ${whenLands}.`)
10232
+ // Autoland accepts; landing is a git fact it proves separately. Do not
10233
+ // promise "lands it" for work that may not be committed or merged.
10234
+ : `proof is ready; autoland runs the second check and accepts it ${whenLands}; it lands once committed and merged.`)
10155
10235
  : (agentCertified
10156
10236
  ? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
10157
10237
  : 'proof is ready; one more agent check before human approval. XP waits for the human.'),
@@ -10403,19 +10483,50 @@ async function cmdAccept(args) {
10403
10483
  if (publicSync && !publicSync.ok) process.exitCode = 1;
10404
10484
  }
10405
10485
 
10406
- function stampAutoAcceptMetadata(taskDb, db, taskId, actor, policy) {
10486
+ // Every auto-accept passes through here, so this is where the record says
10487
+ // whether the work actually landed. Accepted is a review verdict; landed is a
10488
+ // fact about git, proven by lib/landing-proof.js or not claimed at all.
10489
+ // The check behind a landing is the one that actually ran: the verifier's
10490
+ // passing re-run first, then the check the proof names, then the builder's
10491
+ // recorded verify.
10492
+ function landingProofVerify(task) {
10493
+ const metadata = task.metadata || {};
10494
+ const cache = metadata.verify_cache;
10495
+ if (cache && typeof cache === 'object' && cache.ok === true && String(cache.command || '').trim()) {
10496
+ return String(cache.command).trim();
10497
+ }
10498
+ return certifyVerifyCandidate(task) || String(metadata.verify || '').trim();
10499
+ }
10500
+
10501
+ function stampAutoAcceptMetadata(taskDb, db, task, actor, policy) {
10502
+ const taskId = task.id;
10407
10503
  const row = taskDb.getTask(db, taskId);
10408
- if (!row) return;
10504
+ if (!row) return null;
10505
+ // The caller's task object was read before the re-check ran, so it lacks
10506
+ // the verify_cache and certify stamps written since. Prove from the row as
10507
+ // it stands now, keeping the caller's events and review for claimed paths.
10508
+ const current = { ...task, ...row, metadata: row.metadata && typeof row.metadata === 'object' ? row.metadata : {} };
10509
+ let landing;
10510
+ try {
10511
+ landing = proveTaskLanding(current, {
10512
+ files: declaredCandidatePathValues(current),
10513
+ verify: landingProofVerify(current),
10514
+ });
10515
+ } catch (err) {
10516
+ landing = { status: 'unverified', reason: 'landing_proof_failed', detail: String((err && err.message) || err).slice(0, 200) };
10517
+ }
10409
10518
  const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
10410
10519
  metadata.auto_accepted_at = new Date().toISOString();
10411
10520
  metadata.auto_accepted_by = actor;
10412
10521
  metadata.auto_accept_policy = policy;
10522
+ metadata.landing = { ...landing, checked_at: metadata.auto_accepted_at };
10413
10523
  db.prepare(`
10414
10524
  UPDATE tasks
10415
10525
  SET metadata = ?,
10416
10526
  updated_at = ?
10417
10527
  WHERE id = ?
10418
10528
  `).run(JSON.stringify(metadata), Date.now(), taskId);
10529
+ return landing;
10419
10530
  }
10420
10531
 
10421
10532
  function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '', autoAccepted = false }) {
@@ -10924,11 +11035,12 @@ function cmdAutoAcceptCertified(args) {
10924
11035
  results.push({ ...evaluation, action: 'accept_failed', reason: accepted.reason });
10925
11036
  continue;
10926
11037
  }
10927
- stampAutoAcceptMetadata(taskDb, db, task.id, actor, evaluation.policy);
11038
+ const landing = stampAutoAcceptMetadata(taskDb, db, task, actor, evaluation.policy);
10928
11039
  refreshCareerXpAfterReview(accepted.reviewed);
10929
11040
  results.push({
10930
11041
  ...evaluation,
10931
11042
  action: 'accepted',
11043
+ landing,
10932
11044
  reward: accepted.reviewed.episode.reward.value,
10933
11045
  task_id: task.id,
10934
11046
  });
@@ -11185,11 +11297,12 @@ function cmdSweep(args) {
11185
11297
  results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
11186
11298
  continue;
11187
11299
  }
11188
- stampAutoAcceptMetadata(taskDb, db, task.id, actor, evaluation.policy);
11300
+ const landing = stampAutoAcceptMetadata(taskDb, db, task, actor, evaluation.policy);
11189
11301
  refreshCareerXpAfterReview(accepted.reviewed);
11190
11302
  results.push({
11191
11303
  ...evaluation,
11192
11304
  action: 'accepted',
11305
+ landing,
11193
11306
  task_id: task.id,
11194
11307
  reward: accepted.reviewed.episode.reward.value,
11195
11308
  });
@@ -13475,6 +13588,7 @@ async function runTaskCommand(args) {
13475
13588
  case 'finish': return cmdFinish(rest);
13476
13589
  case 'fail': return cmdDone([...rest, '--failed']);
13477
13590
  case 'archive': return cmdArchive(rest);
13591
+ case 'reopen': return cmdReopen(rest);
13478
13592
  case 'clear-done': return cmdClearDone(rest);
13479
13593
  case 'prune-test-junk': return cmdPruneTestJunk(rest);
13480
13594
  case 'keep': return cmdKeep(rest);
@@ -13521,7 +13635,7 @@ const MUTATING_TASK_COMMANDS = new Set([
13521
13635
  'continue-work', 'continue', 'chat', 'note', 'say', 'retitle', 'tag', 'tags', 'step',
13522
13636
  'ready', 'result', 'accept', 'landing', 'land-review', 'auto-accept-certified',
13523
13637
  'auto-accept', 'sweep', 'audit', 'certify-verified', 'accept-group', 'revise',
13524
- 'done', 'finish', 'fail', 'archive', 'clear-done', 'reap-mission-blockers', 'reap-blockers', 'relabel-archived', 'review', 'import',
13638
+ 'done', 'finish', 'fail', 'archive', 'reopen', 'clear-done', 'reap-mission-blockers', 'reap-blockers', 'relabel-archived', 'review', 'import',
13525
13639
  'setup', 'review-lane-act', 'review-act', 'act-review', 'review-lane-loop',
13526
13640
  'review-loop', 'loop-review', 'review-lane-run', 'review-run', 'run-review',
13527
13641
  ]);
@@ -13596,4 +13710,6 @@ module.exports = {
13596
13710
  taskDescriptionForCloud,
13597
13711
  taskReviewCleanEvidenceCommand,
13598
13712
  certifyVerifyCandidate,
13713
+ landingProofVerify,
13714
+ stampAutoAcceptMetadata,
13599
13715
  };