atris 3.70.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 (76) hide show
  1. package/README.md +18 -0
  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 +359 -94
  20. package/commands/mission.js +249 -36
  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 +71 -17
  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 +108 -15
  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 +33 -3
  50. package/lib/engine-registry.js +2 -0
  51. package/lib/engine-resource.js +518 -0
  52. package/lib/fleet.js +125 -16
  53. package/lib/init-commit.js +80 -0
  54. package/lib/intents.js +8 -0
  55. package/lib/known-commands.js +1 -1
  56. package/lib/land-green.js +58 -7
  57. package/lib/landing-proof.js +352 -0
  58. package/lib/member-autoland.js +395 -18
  59. package/lib/member-board.js +7 -1
  60. package/lib/member-map-fold.js +126 -0
  61. package/lib/member-map-inbox.js +80 -0
  62. package/lib/member-progress.js +48 -1
  63. package/lib/member-report.js +127 -8
  64. package/lib/member-self-next-job.js +97 -0
  65. package/lib/member-task-pick.js +41 -14
  66. package/lib/member-up.js +249 -16
  67. package/lib/roster-prep.js +25 -2
  68. package/lib/runner-command.js +5 -0
  69. package/lib/task-db.js +47 -11
  70. package/lib/task-explanation.js +7 -0
  71. package/lib/task-list-keeper.js +8 -1
  72. package/lib/task-receipt.js +6 -1
  73. package/mcp/atris-mcp/index.mjs +212 -10
  74. package/package.json +2 -1
  75. package/scripts/det/engine-reading.js +37 -0
  76. 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,7 +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)
256
- atris task reopen <id> [--reason "..."] Send a done, failed, or archived task back to open
259
+ atris task reopen <id> [--reason "..."] Send a done, waiting, failed, or archived task back to open
257
260
  atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
258
261
  atris task prune-test-junk [--yes] [--vacuum] [--json]
259
262
  Count rows test runs left from temp folders; --yes backs up the db, then removes them
@@ -1637,6 +1640,11 @@ function buildTaskStreams(tasks, goals) {
1637
1640
  // work, one clear way to ask for a change. Every enabled/blocked answer comes
1638
1641
  // from the existing review handoff, so this relabels what the certification
1639
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
+
1640
1648
  function taskApprovalFor(task, { reviewer = 'codex-review', hasExistingReviewFollowUp = null } = {}) {
1641
1649
  const metadata = task && task.metadata || {};
1642
1650
  const approvalStatus = (task && task.review && task.review.approval_status) || metadata.approval_status || null;
@@ -1659,12 +1667,16 @@ function taskApprovalFor(task, { reviewer = 'codex-review', hasExistingReviewFol
1659
1667
  ? reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp })
1660
1668
  : null;
1661
1669
  const planned = current === 'plan';
1662
- const planReady = planned && Boolean(
1663
- (metadata.task_goal || metadata.goal_objective || task.objective)
1664
- && metadata.exit_condition
1665
- && (metadata.verify || metadata.proof_needed)
1666
- && metadata.first_move
1667
- );
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;
1668
1680
  const acceptEnabled = inReview ? handoffAllowsHumanAccept(handoff) : planReady;
1669
1681
  const planOwner = task && (task.claimed_by || taskAssignee(task)) || DEFAULT_OWNER;
1670
1682
  const acceptCommand = inReview
@@ -1683,7 +1695,7 @@ function taskApprovalFor(task, { reviewer = 'codex-review', hasExistingReviewFol
1683
1695
  ? 'The proof points at unfinished or unsafe work. Ask for a change before approval.'
1684
1696
  : 'The proof still needs its required checks before a person can approve it.'
1685
1697
  : planned
1686
- ? 'The plan still needs a goal, finish line, check, and first move before approval.'
1698
+ ? `The plan still needs ${joinPlainList(planMissing)} before approval.`
1687
1699
  : current === 'backlog'
1688
1700
  ? 'This task still needs a complete plan before approval.'
1689
1701
  : current === 'do'
@@ -5455,11 +5467,12 @@ function autoAcceptCertifiedSmallReviews(taskDb, db, projection) {
5455
5467
  results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
5456
5468
  continue;
5457
5469
  }
5458
- 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);
5459
5471
  refreshCareerXpAfterReview(accepted.reviewed);
5460
5472
  results.push({
5461
5473
  ...evaluation,
5462
5474
  action: 'accepted',
5475
+ landing,
5463
5476
  task_id: task.id,
5464
5477
  reward: accepted.reviewed.episode.reward.value,
5465
5478
  });
@@ -7920,9 +7933,13 @@ function taskDetail(taskDb, db, taskId) {
7920
7933
  };
7921
7934
  }
7922
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.
7923
7939
  function taskCommandQuote(value) {
7924
- const text = String(value || '').replace(/\s+/g, ' ').replace(/"/g, '\\"').trim();
7925
- return `"${text || '...'}"`;
7940
+ const text = String(value || '').replace(/\s+/g, ' ').trim() || '...';
7941
+ if (/[$`\\!]/.test(text)) return `'${text.replace(/'/g, `'\\''`)}'`;
7942
+ return `"${text.replace(/"/g, '\\"')}"`;
7926
7943
  }
7927
7944
 
7928
7945
  function cleanPublicText(value, max = 500) {
@@ -9771,7 +9788,7 @@ function cmdReopen(args) {
9771
9788
  const db = taskDb.open();
9772
9789
  const taskId = requireTaskId(taskDb, db, id, 'atris task reopen');
9773
9790
  const actor = String(flag(args, '--as') || DEFAULT_OWNER);
9774
- const result = taskDb.reopenTask(db, { id: taskId, actor, reason });
9791
+ const result = taskDb.reopenTask(db, { id: taskId, actor, reason, fromReview: true });
9775
9792
  if (!result.reopened) {
9776
9793
  const detail = `reopen failed: ${taskId} ${result.reason}`;
9777
9794
  if (wantsJson(args)) {
@@ -10212,7 +10229,9 @@ function cmdReady(args) {
10212
10229
  ? `double-check complete; autoland will accept this ${whenLands}.`
10213
10230
  : needsExternalVerifier
10214
10231
  ? 'proof is ready; this verifier needs a second agent review because autoland cannot rerun it.'
10215
- : `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.`)
10216
10235
  : (agentCertified
10217
10236
  ? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
10218
10237
  : 'proof is ready; one more agent check before human approval. XP waits for the human.'),
@@ -10464,19 +10483,50 @@ async function cmdAccept(args) {
10464
10483
  if (publicSync && !publicSync.ok) process.exitCode = 1;
10465
10484
  }
10466
10485
 
10467
- 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;
10468
10503
  const row = taskDb.getTask(db, taskId);
10469
- 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
+ }
10470
10518
  const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
10471
10519
  metadata.auto_accepted_at = new Date().toISOString();
10472
10520
  metadata.auto_accepted_by = actor;
10473
10521
  metadata.auto_accept_policy = policy;
10522
+ metadata.landing = { ...landing, checked_at: metadata.auto_accepted_at };
10474
10523
  db.prepare(`
10475
10524
  UPDATE tasks
10476
10525
  SET metadata = ?,
10477
10526
  updated_at = ?
10478
10527
  WHERE id = ?
10479
10528
  `).run(JSON.stringify(metadata), Date.now(), taskId);
10529
+ return landing;
10480
10530
  }
10481
10531
 
10482
10532
  function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '', autoAccepted = false }) {
@@ -10985,11 +11035,12 @@ function cmdAutoAcceptCertified(args) {
10985
11035
  results.push({ ...evaluation, action: 'accept_failed', reason: accepted.reason });
10986
11036
  continue;
10987
11037
  }
10988
- stampAutoAcceptMetadata(taskDb, db, task.id, actor, evaluation.policy);
11038
+ const landing = stampAutoAcceptMetadata(taskDb, db, task, actor, evaluation.policy);
10989
11039
  refreshCareerXpAfterReview(accepted.reviewed);
10990
11040
  results.push({
10991
11041
  ...evaluation,
10992
11042
  action: 'accepted',
11043
+ landing,
10993
11044
  reward: accepted.reviewed.episode.reward.value,
10994
11045
  task_id: task.id,
10995
11046
  });
@@ -11246,11 +11297,12 @@ function cmdSweep(args) {
11246
11297
  results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
11247
11298
  continue;
11248
11299
  }
11249
- stampAutoAcceptMetadata(taskDb, db, task.id, actor, evaluation.policy);
11300
+ const landing = stampAutoAcceptMetadata(taskDb, db, task, actor, evaluation.policy);
11250
11301
  refreshCareerXpAfterReview(accepted.reviewed);
11251
11302
  results.push({
11252
11303
  ...evaluation,
11253
11304
  action: 'accepted',
11305
+ landing,
11254
11306
  task_id: task.id,
11255
11307
  reward: accepted.reviewed.episode.reward.value,
11256
11308
  });
@@ -13658,4 +13710,6 @@ module.exports = {
13658
13710
  taskDescriptionForCloud,
13659
13711
  taskReviewCleanEvidenceCommand,
13660
13712
  certifyVerifyCandidate,
13713
+ landingProofVerify,
13714
+ stampAutoAcceptMetadata,
13661
13715
  };