atris 3.31.0 → 3.33.3

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.
package/commands/task.js CHANGED
@@ -8,13 +8,22 @@ const http = require('http');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
10
  const { taskProofState, buildVerifiedProof } = require('../lib/task-proof');
11
- const { evaluateAutoAccept, parseVerifyCommand } = require('../lib/auto-accept-certified');
11
+ const { evaluateAutoAccept, parseVerifyCommand, runVerifyCommand, DENIED_TAGS } = require('../lib/auto-accept-certified');
12
12
  const { extractReceiptEvidence } = require('../lib/receipt-evidence');
13
13
  const escapeRegExp = require('../lib/escape-regexp');
14
14
  const {
15
15
  normalizeOwnerSlug,
16
16
  resolveFunctionalOwner: resolveFunctionalTaskOwner,
17
17
  } = require('../lib/functional-owner');
18
+ const { operatorReady, hasAgentJargon } = require('./autoland');
19
+ const {
20
+ TASK_INSPECT_FIELDS,
21
+ readFieldsFlag,
22
+ stripInspectArgs,
23
+ validateFields,
24
+ inspectTextLines,
25
+ buildInspectPayload,
26
+ } = require('../lib/inspect-fields');
18
27
 
19
28
  const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
20
29
  || process.env.USER
@@ -90,13 +99,22 @@ function getTaskDb() {
90
99
  }
91
100
  }
92
101
 
102
+ function warnIfTaskTitleNeedsOperatorWhy(title) {
103
+ const text = String(title || '').trim();
104
+ if (!text || operatorReady(text)) return null;
105
+ const warning = 'Warning: add the why in plain words to this task title: what it buys or costs, who benefits, and no flags or identifiers.';
106
+ console.error(warning);
107
+ return warning;
108
+ }
109
+
93
110
  function taskUsageText() {
94
111
  return `
95
112
  atris task - durable local task state (SQLite, gitignored)
96
113
 
97
114
  atris task Show the task desk
98
115
  atris task new "<title>" Create a task
99
- atris task next [--create-next] Claim/show next open task; optionally create the generated Endgame fallback
116
+ atris task next [--tag <tag>] [--create-next]
117
+ Claim/show next open task; optionally create the generated Endgame fallback
100
118
  atris task continue-work <id> Create/reuse a certified Review follow-up task
101
119
  atris task say <id> "<message>" Add context to a task
102
120
  atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
@@ -113,6 +131,8 @@ atris task - durable local task state (SQLite, gitignored)
113
131
  atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
114
132
  atris task accept <id> [--proof "..."] [--public]
115
133
  Human accepts proof, marks done; --public also publishes AgentXP
134
+ atris task certify-verified [--dry-run] [--limit <n>] [--as <actor>]
135
+ Re-run the runnable check named in each Review proof as a second actor; passing rows certify (denied lanes and check-less rows wait for a human)
116
136
  atris task auto-accept-certified --dry-run [--strict-verify] [--limit <n>]
117
137
  Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
118
138
  atris task revise <id> --note "..." Send reviewed work back to Do
@@ -128,6 +148,7 @@ atris task - durable local task state (SQLite, gitignored)
128
148
  atris task day [--json] Show today's owner-grouped task list
129
149
  atris task list [--all] [--status <s>] List tasks (default: this workspace)
130
150
  atris task claim <id> [--as <owner>] Atomic claim
151
+ atris task release <id> [--as <owner>] Release your own mistaken claim back to open
131
152
  atris task capabilities [--json] Read-only task CLI/API capability contract
132
153
  atris task capabilities-check [--json] Verify task capability contract conformance
133
154
  atris task review-lane-drain [--json] Pick next safe Review-lane agent action
@@ -143,6 +164,8 @@ atris task - durable local task state (SQLite, gitignored)
143
164
  review-state lanes: needs-agent, continue-work, human-accept-waiting, certified
144
165
  atris task note <id> "<message>" Append dialogue/context to a task
145
166
  atris task show <id> [--json] Show a task card + dialogue
167
+ atris task inspect <id> --fields review,status,title [--json]
168
+ Field-selectable task state (review metadata, status, title, owner, tag)
146
169
  atris task page <id> [--json] Show the one-task page contract
147
170
  atris task step <id> [--json] Refine chat, then advance one safe Plan/Do/Review step
148
171
  atris task done <id> --proof "..." Mark complete with proof
@@ -374,13 +397,36 @@ function textFlag(args, names) {
374
397
 
375
398
  function landingFlags(args) {
376
399
  return {
377
- happened: textFlag(args, ['--happened', '--what-happened']),
400
+ happened: textFlag(args, ['--landing', '--happened', '--what-happened']),
378
401
  checked: textFlag(args, ['--checked', '--how-checked']),
379
402
  tested: textFlag(args, ['--tested', '--what-tested']),
380
403
  decision: textFlag(args, ['--decision', '--acceptance']),
381
404
  };
382
405
  }
383
406
 
407
+ function normalizedLandingSentence(value) {
408
+ return String(value || '').replace(/\s+/g, ' ').trim();
409
+ }
410
+
411
+ function defaultLandingSentenceForTitle(title) {
412
+ return `Completed: ${String(title || '').trim()}.`;
413
+ }
414
+
415
+ function landingNeedsDayOnePm(sentence, title) {
416
+ const text = normalizedLandingSentence(sentence);
417
+ if (!text) return true;
418
+ if (text.toLowerCase() === normalizedLandingSentence(defaultLandingSentenceForTitle(title)).toLowerCase()) return true;
419
+ return hasAgentJargon(text) || !operatorReady(text);
420
+ }
421
+
422
+ function warnIfLandingNeedsDayOnePm(landing, title) {
423
+ const sentence = landing && typeof landing === 'object' ? landing.happened : '';
424
+ if (!landingNeedsDayOnePm(sentence, title)) return null;
425
+ const warning = 'Advisory: add --landing with one capability sentence a day-one PM could read, with the result in plain words and no flags or identifiers.';
426
+ console.error(warning);
427
+ return warning;
428
+ }
429
+
384
430
  function numericFlag(args, name) {
385
431
  const value = flag(args, name);
386
432
  if (value === null || value === true || value === undefined) return null;
@@ -597,18 +643,19 @@ function buildPendingReviewChatPredicate(taskDb, db, workspaceRoot) {
597
643
  function createReviewNextTask(taskDb, db, currentTask, title) {
598
644
  const nextTitle = String(title || '').trim();
599
645
  if (!nextTitle) return null;
646
+ const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(nextTitle);
600
647
  const currentMetadata = currentTask && currentTask.metadata && typeof currentTask.metadata === 'object'
601
648
  ? currentTask.metadata
602
649
  : {};
603
650
  const existing = findExistingReviewNextTask(taskDb, db, currentTask, nextTitle);
604
- if (existing) return { id: existing.id, inserted: false };
651
+ if (existing) return { id: existing.id, inserted: false, operator_title_warning: operatorTitleWarning };
605
652
  const goalId = currentMetadata.goal_id || currentMetadata.goalId || null;
606
653
  const parentId = currentTask && currentTask.id || null;
607
654
  const sourceKey = parentId && typeof taskDb.sourceKey === 'function'
608
655
  ? taskDb.sourceKey(`task_review_next:${parentId}`, nextTitle)
609
656
  : null;
610
657
  try {
611
- return taskDb.addTask(db, {
658
+ const created = taskDb.addTask(db, {
612
659
  title: nextTitle,
613
660
  tag: currentTask && currentTask.tag || null,
614
661
  workspaceRoot: taskDb.workspaceRoot(),
@@ -619,10 +666,11 @@ function createReviewNextTask(taskDb, db, currentTask, title) {
619
666
  source: 'task_review_next',
620
667
  },
621
668
  });
669
+ return { ...created, operator_title_warning: operatorTitleWarning };
622
670
  } catch (error) {
623
671
  if (sourceKey && /constraint|unique/i.test(String(error && (error.code || error.message) || error))) {
624
672
  const racedExisting = findExistingReviewNextTask(taskDb, db, currentTask, nextTitle);
625
- if (racedExisting) return { id: racedExisting.id, inserted: false };
673
+ if (racedExisting) return { id: racedExisting.id, inserted: false, operator_title_warning: operatorTitleWarning };
626
674
  }
627
675
  throw error;
628
676
  }
@@ -1069,6 +1117,50 @@ function taskReviewSummary(task) {
1069
1117
  return reviewSummaryWithVerificationChat(task, review);
1070
1118
  }
1071
1119
 
1120
+ function taskReviewInspectMetadata(task) {
1121
+ const review = task.review || taskReviewSummary(task);
1122
+ if (!review) return null;
1123
+ return {
1124
+ approval_status: review.approval_status || null,
1125
+ agent_certified: review.agent_certified === true,
1126
+ agent_review_pass_count: review.agent_review_pass_count || null,
1127
+ agent_certification_policy: review.agent_certification_policy || null,
1128
+ summary: review.summary || null,
1129
+ proof: review.proof || null,
1130
+ lesson: review.lesson || null,
1131
+ next_task: review.next_task || null,
1132
+ human_revision_count: review.human_revision_count || null,
1133
+ human_revision_note: review.human_revision_note || null,
1134
+ reward: review.reward ?? null,
1135
+ };
1136
+ }
1137
+
1138
+ function taskInspectFieldValues(task, fields) {
1139
+ const values = {};
1140
+ for (const field of fields) {
1141
+ switch (field) {
1142
+ case 'status':
1143
+ values.status = task.status || null;
1144
+ break;
1145
+ case 'title':
1146
+ values.title = task.title || null;
1147
+ break;
1148
+ case 'claimed_by':
1149
+ values.claimed_by = task.claimed_by || null;
1150
+ break;
1151
+ case 'tag':
1152
+ values.tag = task.tag || null;
1153
+ break;
1154
+ case 'review':
1155
+ values.review = taskReviewInspectMetadata(task);
1156
+ break;
1157
+ default:
1158
+ break;
1159
+ }
1160
+ }
1161
+ return values;
1162
+ }
1163
+
1072
1164
  function reviewSummaryWithVerificationChat(task, review) {
1073
1165
  if (!review || task.status !== 'review' || review.approval_status !== 'pending') return review;
1074
1166
  const verifierTask = taskWithReviewEvidence(task, {
@@ -3168,6 +3260,11 @@ function formatTaskQueueScope(scope = {}) {
3168
3260
  .join(' ');
3169
3261
  }
3170
3262
 
3263
+ function noOpenTasksMessage(scope = {}) {
3264
+ const scopeText = formatTaskQueueScope(scope);
3265
+ return scopeText ? `No open tasks for ${scopeText}.` : 'No open tasks.';
3266
+ }
3267
+
3171
3268
  function taskQueueContract(projection, { reviewer = 'codex-review', limit = 8, scope = {}, hasExistingReviewFollowUp = null, hasPendingReviewChat = null, excludeTaskIds = null } = {}) {
3172
3269
  const normalizedScope = normalizeTaskQueueScope(scope);
3173
3270
  const tasks = filterTasksByScope(sortTasksNewestFirst(projection.tasks || []), normalizedScope, { hasExistingReviewFollowUp });
@@ -4885,6 +4982,7 @@ function cmdAdd(args) {
4885
4982
  const taskDb = getTaskDb();
4886
4983
  const db = taskDb.open();
4887
4984
  const ws = taskDb.workspaceRoot();
4985
+ const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
4888
4986
  // Generation throttle — the named root cause is generation > human-review rate. An AGENT cannot keep
4889
4987
  // minting tasks while a wall of certified-but-unaccepted work waits; that treadmill is what accept-group
4890
4988
  // only drains. Humans and --force bypass. Closes the tap instead of just enlarging the bucket.
@@ -4910,6 +5008,7 @@ function cmdAdd(args) {
4910
5008
  action: 'created',
4911
5009
  task_id: result.id,
4912
5010
  inserted: result.inserted !== false,
5011
+ operator_title_warning: operatorTitleWarning,
4913
5012
  projection_path: outPath,
4914
5013
  task,
4915
5014
  });
@@ -4952,6 +5051,7 @@ function cmdDelegate(args) {
4952
5051
  const taskDb = getTaskDb();
4953
5052
  const db = taskDb.open();
4954
5053
  const ws = taskDb.workspaceRoot();
5054
+ const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
4955
5055
  const ownerResolution = resolveFunctionalTaskOwner({
4956
5056
  requestedOwner: requestedOwner && requestedOwner !== true ? requestedOwner : null,
4957
5057
  title,
@@ -5007,6 +5107,7 @@ function cmdDelegate(args) {
5007
5107
  executed_by: executedBy || null,
5008
5108
  via,
5009
5109
  handoff,
5110
+ operator_title_warning: operatorTitleWarning,
5010
5111
  projection_path: outPath,
5011
5112
  task,
5012
5113
  });
@@ -5135,20 +5236,24 @@ function cmdHome(args) {
5135
5236
  function cmdList(args) {
5136
5237
  const all = hasFlag(args, '--all');
5137
5238
  const status = flag(args, '--status');
5239
+ const scope = taskQueueScopeFromArgs(args);
5240
+ const scoped = !taskQueueScopeIsEmpty(scope);
5138
5241
  const taskDb = getTaskDb();
5139
5242
  const db = taskDb.open();
5140
- const rows = taskDb.listTasks(db, {
5243
+ const rawRows = taskDb.listTasks(db, {
5141
5244
  workspaceRoot: all ? null : taskDb.workspaceRoot(),
5142
5245
  status: typeof status === 'string' ? status : null,
5143
- limit: 200,
5246
+ limit: scoped ? null : 200,
5144
5247
  });
5248
+ const rows = filterTasksByScope(rawRows, scope);
5145
5249
  const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, all));
5146
5250
  if (wantsJson(args)) {
5147
- printJson({ ok: true, action: 'list', tasks: displayRows });
5251
+ printJson({ ok: true, action: 'list', scope: normalizeTaskQueueScope(scope), tasks: displayRows });
5148
5252
  return;
5149
5253
  }
5150
5254
  if (rows.length === 0) {
5151
- console.log('(no tasks)');
5255
+ const scopeText = formatTaskQueueScope(scope);
5256
+ console.log(scopeText ? `(no tasks for ${scopeText})` : '(no tasks)');
5152
5257
  return;
5153
5258
  }
5154
5259
  for (const r of displayRows) {
@@ -5184,19 +5289,68 @@ function cmdClaim(args) {
5184
5289
  }
5185
5290
  console.log(`claimed ${taskRef(compactTaskFromProjection(projection, taskId))} as ${owner}`);
5186
5291
  } else {
5292
+ const recoveryCommand = result.reason === 'already_claimed' && result.claimed_by
5293
+ ? `atris task release ${id} --as ${result.claimed_by}`
5294
+ : null;
5187
5295
  if (wantsJson(args)) {
5188
5296
  printJson({
5189
5297
  ok: false,
5190
5298
  command: 'atris task claim',
5191
5299
  reason: result.reason,
5192
5300
  claimed_by: result.claimed_by || null,
5301
+ recovery_command: recoveryCommand,
5193
5302
  detail: `claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
5194
5303
  });
5195
5304
  process.exit(1);
5196
5305
  }
5197
5306
  console.error(`claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
5307
+ if (recoveryCommand) console.error(`Recovery: ${recoveryCommand}`);
5308
+ process.exit(1);
5309
+ }
5310
+ }
5311
+
5312
+ function cmdRelease(args) {
5313
+ const pos = positional(args);
5314
+ const id = pos[0];
5315
+ if (!id) {
5316
+ failTask('atris task release', 'missing_id', 'id required');
5317
+ }
5318
+ const owner = flag(args, '--as') || DEFAULT_OWNER;
5319
+ const taskDb = getTaskDb();
5320
+ const db = taskDb.open();
5321
+ const taskId = requireTaskId(taskDb, db, id, 'atris task release');
5322
+ const result = taskDb.releaseTask(db, { id: taskId, actor: String(owner) });
5323
+ if (result.released) {
5324
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
5325
+ const task = compactTaskFromProjection(projection, taskId);
5326
+ if (wantsJson(args)) {
5327
+ printJson({
5328
+ ok: true,
5329
+ action: 'released',
5330
+ task_id: taskId,
5331
+ owner: String(owner),
5332
+ projection_path: outPath,
5333
+ task,
5334
+ event_version: result.event && result.event.version || null,
5335
+ });
5336
+ return;
5337
+ }
5338
+ console.log(`released ${taskRef(task)} from ${owner}`);
5339
+ console.log(`Next: atris task next --as ${owner}`);
5340
+ return;
5341
+ }
5342
+ if (wantsJson(args)) {
5343
+ printJson({
5344
+ ok: false,
5345
+ command: 'atris task release',
5346
+ reason: result.reason,
5347
+ claimed_by: result.claimed_by || null,
5348
+ detail: `release failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
5349
+ });
5198
5350
  process.exit(1);
5199
5351
  }
5352
+ console.error(`release failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
5353
+ process.exit(1);
5200
5354
  }
5201
5355
 
5202
5356
  function liveTaskWithTitle(tasks, title) {
@@ -5271,6 +5425,7 @@ function buildEndgameTaskSeed({ slug, horizon, owner }) {
5271
5425
 
5272
5426
  function createEndgameSeedTask(taskDb, db, seed, owner) {
5273
5427
  const taskOwner = String(owner || DEFAULT_OWNER);
5428
+ const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(seed.title);
5274
5429
  const result = taskDb.addTask(db, {
5275
5430
  title: seed.title,
5276
5431
  tag: seed.tag,
@@ -5294,20 +5449,23 @@ function createEndgameSeedTask(taskDb, db, seed, owner) {
5294
5449
  ok: true,
5295
5450
  task_id: result.id,
5296
5451
  inserted: result.inserted !== false,
5452
+ operator_title_warning: operatorTitleWarning,
5297
5453
  note_version: note.event.version,
5298
5454
  };
5299
5455
  }
5300
5456
 
5301
5457
  function cmdNext(args) {
5302
5458
  const owner = flag(args, '--as') || DEFAULT_OWNER;
5459
+ const scope = taskQueueScopeFromArgs(args);
5460
+ const scoped = !taskQueueScopeIsEmpty(scope);
5303
5461
  const taskDb = getTaskDb();
5304
5462
  const db = taskDb.open();
5305
- const claimed = taskDb.listTasks(db, {
5463
+ const claimed = filterTasksByScope(taskDb.listTasks(db, {
5306
5464
  workspaceRoot: taskDb.workspaceRoot(),
5307
5465
  status: 'claimed',
5308
5466
  claimedBy: String(owner),
5309
- limit: 1,
5310
- });
5467
+ limit: scoped ? null : 1,
5468
+ }), scope);
5311
5469
  if (claimed.length) {
5312
5470
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
5313
5471
  if (wantsJson(args)) {
@@ -5316,6 +5474,7 @@ function cmdNext(args) {
5316
5474
  action: 'current',
5317
5475
  task_id: claimed[0].id,
5318
5476
  owner: String(owner),
5477
+ scope: normalizeTaskQueueScope(scope),
5319
5478
  projection_path: outPath,
5320
5479
  task: compactTaskFromProjection(projection, claimed[0].id),
5321
5480
  });
@@ -5326,14 +5485,14 @@ function cmdNext(args) {
5326
5485
  return;
5327
5486
  }
5328
5487
  const reviewProjection = writeDefaultProjection(taskDb, db);
5329
- const reviewTasks = (reviewProjection.projection.tasks || [])
5488
+ const reviewTasks = filterTasksByScope(reviewProjection.projection.tasks || [], scope)
5330
5489
  .map(compactTaskForStatus)
5331
5490
  .filter(task => task && task.review && task.review.handoff);
5332
- const open = taskDb.listTasks(db, {
5491
+ const open = filterTasksByScope(taskDb.listTasks(db, {
5333
5492
  workspaceRoot: taskDb.workspaceRoot(),
5334
5493
  status: 'open',
5335
- limit: 1,
5336
- });
5494
+ limit: scoped ? null : 1,
5495
+ }), scope);
5337
5496
  if (!open.length) {
5338
5497
  const { projection, outPath } = reviewProjection;
5339
5498
  const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again')
@@ -5363,10 +5522,12 @@ function cmdNext(args) {
5363
5522
  action: 'created_next',
5364
5523
  task_id: created.task_id,
5365
5524
  owner: String(owner),
5525
+ scope: normalizeTaskQueueScope(scope),
5366
5526
  projection_path: createdOutPath,
5367
5527
  handoff,
5368
5528
  next_agent_action: nextAgentAction,
5369
5529
  note_version: created.note_version,
5530
+ operator_title_warning: created.operator_title_warning || null,
5370
5531
  task: createdTask,
5371
5532
  review_task: reviewTask,
5372
5533
  });
@@ -5384,6 +5545,7 @@ function cmdNext(args) {
5384
5545
  action: handoff.next_action,
5385
5546
  task_id: handoff.next_action === 'continue_work' ? null : reviewTask.id,
5386
5547
  owner: String(owner),
5548
+ scope: normalizeTaskQueueScope(scope),
5387
5549
  projection_path: outPath,
5388
5550
  handoff,
5389
5551
  next_agent_action: nextAgentAction,
@@ -5393,7 +5555,7 @@ function cmdNext(args) {
5393
5555
  });
5394
5556
  return;
5395
5557
  }
5396
- console.log('No open tasks.');
5558
+ console.log(noOpenTasksMessage(scope));
5397
5559
  console.log(handoff.next_action === 'agent_review_again'
5398
5560
  ? `${taskRef(reviewTask)} needs one more agent check before approval.`
5399
5561
  : `${taskRef(reviewTask)} is ready for approval.`);
@@ -5418,11 +5580,12 @@ function cmdNext(args) {
5418
5580
  action: 'none',
5419
5581
  task_id: null,
5420
5582
  owner: String(owner),
5583
+ scope: normalizeTaskQueueScope(scope),
5421
5584
  projection_path: outPath,
5422
5585
  });
5423
5586
  return;
5424
5587
  }
5425
- console.log('No open tasks.');
5588
+ console.log(noOpenTasksMessage(scope));
5426
5589
  console.log('Start with: atris task new "..."');
5427
5590
  return;
5428
5591
  }
@@ -5438,6 +5601,7 @@ function cmdNext(args) {
5438
5601
  action: 'next',
5439
5602
  task_id: open[0].id,
5440
5603
  owner: String(owner),
5604
+ scope: normalizeTaskQueueScope(scope),
5441
5605
  projection_path: outPath,
5442
5606
  task: compactTaskFromProjection(projection, open[0].id),
5443
5607
  });
@@ -5445,6 +5609,9 @@ function cmdNext(args) {
5445
5609
  }
5446
5610
  console.log(`next ${taskRef(compactTaskFromProjection(projection, open[0].id))} @${owner}`);
5447
5611
  console.log(open[0].title);
5612
+ const ref = taskRef(compactTaskFromProjection(projection, open[0].id));
5613
+ const { printOperatorNext } = require('../lib/operator-next');
5614
+ printOperatorNext(`atris task step ${ref}`);
5448
5615
  }
5449
5616
 
5450
5617
  function continueWorkForReviewTask(taskDb, db, taskId, { owner = DEFAULT_OWNER } = {}) {
@@ -5488,6 +5655,7 @@ function continueWorkForReviewTask(taskDb, db, taskId, { owner = DEFAULT_OWNER }
5488
5655
  parent_task_id: taskId,
5489
5656
  next_task_id: nextCreated ? nextCreated.id : null,
5490
5657
  created: Boolean(nextCreated && nextCreated.inserted !== false),
5658
+ operator_title_warning: nextCreated ? nextCreated.operator_title_warning || null : null,
5491
5659
  owner: String(owner || DEFAULT_OWNER),
5492
5660
  projection_path: outPath,
5493
5661
  parent,
@@ -5790,6 +5958,40 @@ function cmdClearPlan(args) {
5790
5958
  console.log(`clear-plan moved ${result.cleared.length} task${result.cleared.length === 1 ? '' : 's'} to Backlog`);
5791
5959
  }
5792
5960
 
5961
+ function cmdInspect(args) {
5962
+ const parsed = readFieldsFlag(args, '--fields');
5963
+ if (!parsed || parsed.error) {
5964
+ failTask('atris task inspect', 'missing_fields', parsed?.error || 'Usage: atris task inspect <id> --fields review,status,title [--json]');
5965
+ }
5966
+ const fieldError = validateFields(parsed.fields, TASK_INSPECT_FIELDS, 'task');
5967
+ if (fieldError) failTask('atris task inspect', 'unknown_fields', fieldError);
5968
+ const ref = stripInspectArgs(args)[0] || '';
5969
+ if (!ref) {
5970
+ failTask('atris task inspect', 'missing_id', 'Usage: atris task inspect <id> --fields review,status,title [--json]');
5971
+ }
5972
+ const taskDb = getTaskDb();
5973
+ const db = taskDb.open();
5974
+ const taskId = requireTaskId(taskDb, db, ref, 'atris task inspect');
5975
+ const projection = enrichTaskProjection(taskDb.taskProjection(db, { taskId }));
5976
+ const task = projection.tasks[0];
5977
+ if (!task) {
5978
+ failTask('atris task inspect', 'not_found', `task not found: ${ref}`, 1);
5979
+ }
5980
+ const values = taskInspectFieldValues(task, parsed.fields);
5981
+ const payload = buildInspectPayload({
5982
+ action: 'task_inspect',
5983
+ idKey: 'task_id',
5984
+ idValue: task.id,
5985
+ fields: parsed.fields,
5986
+ values,
5987
+ });
5988
+ if (wantsJson(args)) {
5989
+ printJson(payload);
5990
+ return;
5991
+ }
5992
+ for (const line of inspectTextLines(parsed.fields, values)) console.log(line);
5993
+ }
5994
+
5793
5995
  function cmdShow(args) {
5794
5996
  const pos = positional(args);
5795
5997
  const id = pos[0];
@@ -7549,6 +7751,7 @@ function cmdFinish(args) {
7549
7751
  const currentTask = taskDb.getTask(db, taskId);
7550
7752
  const actor = String(flag(args, '--as') || DEFAULT_OWNER);
7551
7753
  const proof = proofFlagValue(args);
7754
+ const landing = landingFlags(args);
7552
7755
  const failed = hasFlag(args, '--failed');
7553
7756
  const hasReview = hasFlag(args, '--review') || flag(args, '--lesson') || flag(args, '--next') || flag(args, '--proof') || flag(args, '--reward');
7554
7757
  if (agentProofOnlyMode() && !failed) {
@@ -7585,6 +7788,7 @@ function cmdFinish(args) {
7585
7788
  process.exit(1);
7586
7789
  }
7587
7790
  if (hasReview) {
7791
+ const landingAdvisory = !failed ? warnIfLandingNeedsDayOnePm(landing, currentTask && currentTask.title) : null;
7588
7792
  const result = taskDb.reviewTask(db, {
7589
7793
  id: taskId,
7590
7794
  actor,
@@ -7593,6 +7797,7 @@ function cmdFinish(args) {
7593
7797
  nextTask: typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '',
7594
7798
  proof,
7595
7799
  careerXpEligible: false,
7800
+ landing,
7596
7801
  });
7597
7802
  const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
7598
7803
  const xpProjection = refreshCareerXpAfterReview(result);
@@ -7606,6 +7811,7 @@ function cmdFinish(args) {
7606
7811
  reward: result.episode.reward.value,
7607
7812
  episode: result.episode,
7608
7813
  xp_projection: xpProjection,
7814
+ landing_advisory: landingAdvisory,
7609
7815
  next_task_id: nextCreated ? nextCreated.id : null,
7610
7816
  projection_path: outPath,
7611
7817
  task: compactTaskFromProjection(projection, taskId),
@@ -7700,6 +7906,7 @@ function cmdReady(args) {
7700
7906
  console.error(`ready failed: ${result.reason}`);
7701
7907
  process.exit(1);
7702
7908
  }
7909
+ const landingAdvisory = warnIfLandingNeedsDayOnePm(landing, result.row && result.row.title);
7703
7910
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
7704
7911
  const agentCertified = result.event.payload.agent_certified === true;
7705
7912
  const projectionTask = taskFromProjection(projection, taskId)
@@ -7741,6 +7948,7 @@ function cmdReady(args) {
7741
7948
  agent_certified: agentCertified,
7742
7949
  handoff,
7743
7950
  result_trace: resultTrace,
7951
+ landing_advisory: landingAdvisory,
7744
7952
  ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
7745
7953
  projection_path: outPath,
7746
7954
  task: compactTaskFromProjection(projection, taskId),
@@ -7909,6 +8117,151 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
7909
8117
  return { ok: true, reviewed };
7910
8118
  }
7911
8119
 
8120
+ // Second-actor certification for proof-backed Review rows whose proof names a
8121
+ // runnable, allowlisted check. Re-running that check as a distinct actor IS
8122
+ // the independent verification the certification gate asks for — the row
8123
+ // becomes certified and autoland can land it on the same heartbeat. Rows in
8124
+ // denied lanes, without a runnable check, or already certified keep their
8125
+ // existing paths (review chats, human accept).
8126
+ function certifyVerifyCandidate(task) {
8127
+ const metadata = task.metadata || {};
8128
+ const review = task.review || {};
8129
+ const recorded = typeof metadata.verify === 'string' && metadata.verify.trim() ? [metadata.verify.trim()] : [];
8130
+ const proof = String(review.proof || metadata.latest_agent_proof || '');
8131
+ for (const raw of [...recorded, ...taskReviewEvidenceCommands(proof)]) {
8132
+ // The evidence extractor can keep trailing prose ("git diff --check. Evidence
8133
+ // inspected: ..."); try the full clause, then the first sentence of it.
8134
+ for (const candidate of [raw, raw.split(/\.(?:\s|$)/)[0]].map((c) => String(c || '').trim())) {
8135
+ if (candidate && parseVerifyCommand(candidate).ok) return candidate;
8136
+ }
8137
+ }
8138
+ return null;
8139
+ }
8140
+
8141
+ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
8142
+ const row = taskDb.getTask(db, taskId);
8143
+ if (!row) return;
8144
+ const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
8145
+ metadata.verify = metadata.verify || verify;
8146
+ metadata.certified_verified_at = new Date().toISOString();
8147
+ metadata.certified_verified_by = actor;
8148
+ db.prepare(`
8149
+ UPDATE tasks
8150
+ SET metadata = ?,
8151
+ updated_at = ?
8152
+ WHERE id = ?
8153
+ `).run(JSON.stringify(metadata), Date.now(), taskId);
8154
+ }
8155
+
8156
+ function cmdCertifyVerified(args) {
8157
+ const dryRun = hasFlag(args, '--dry-run');
8158
+ const asJson = wantsJson(args);
8159
+ const actor = String(flag(args, '--as') || 'autoland-verifier');
8160
+ const limitRaw = flag(args, '--limit');
8161
+ const max = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 6) : 6;
8162
+
8163
+ const taskDb = getTaskDb();
8164
+ const db = taskDb.open();
8165
+ const { projection } = writeDefaultProjection(taskDb, db);
8166
+ const candidates = (projection.tasks || [])
8167
+ .filter((t) => t && t.status === 'review')
8168
+ .sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
8169
+ .slice(0, max);
8170
+ const results = [];
8171
+ for (const item of candidates) {
8172
+ const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
8173
+ const task = fullProjection.tasks[0] || null;
8174
+ if (!task) {
8175
+ results.push({ ref: item.display_id || item.id, action: 'skipped', reason: 'task_not_found' });
8176
+ continue;
8177
+ }
8178
+ const ref = task.display_id || task.legacy_ref || task.id;
8179
+ const metadata = task.metadata || {};
8180
+ const review = task.review || {};
8181
+ if (task.status !== 'review') {
8182
+ results.push({ ref, action: 'skipped', reason: 'not_in_review' });
8183
+ continue;
8184
+ }
8185
+ const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
8186
+ if (approval !== 'pending') {
8187
+ results.push({ ref, action: 'skipped', reason: `approval_${approval}` });
8188
+ continue;
8189
+ }
8190
+ const tag = String(task.tag || '').toLowerCase();
8191
+ if (DENIED_TAGS.has(tag)) {
8192
+ results.push({ ref, action: 'skipped', reason: `denied_tag_${tag}` });
8193
+ continue;
8194
+ }
8195
+ // Skip only rows the accept lane can already land, or rows blocked by
8196
+ // something an executed second-actor check cannot cure. A row with two
8197
+ // passes from ONE actor is exactly what this command exists to cure —
8198
+ // "certified" alone is not landable.
8199
+ const evaluation = evaluateAutoAccept(task, { strictVerify: false });
8200
+ if (evaluation.eligible) {
8201
+ results.push({ ref, action: 'skipped', reason: 'already_landable' });
8202
+ continue;
8203
+ }
8204
+ const curable = ['not_agent_certified', 'needs_second_reviewer_or_third_pass', 'insufficient_review_passes'];
8205
+ if (!curable.includes(evaluation.reason)) {
8206
+ results.push({ ref, action: 'skipped', reason: evaluation.reason });
8207
+ continue;
8208
+ }
8209
+ const verify = certifyVerifyCandidate(task);
8210
+ if (!verify) {
8211
+ results.push({ ref, action: 'skipped', reason: 'no_runnable_check_in_proof' });
8212
+ continue;
8213
+ }
8214
+ if (dryRun) {
8215
+ results.push({ ref, action: 'would_certify', verify });
8216
+ continue;
8217
+ }
8218
+ const run = runVerifyCommand(verify, task.workspace_root || process.cwd());
8219
+ if (!run.ok) {
8220
+ results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
8221
+ continue;
8222
+ }
8223
+ const builderProof = String(review.proof || metadata.latest_agent_proof || '').slice(0, 200);
8224
+ const readied = taskDb.readyTask(db, {
8225
+ id: task.id,
8226
+ actor,
8227
+ proof: `Second-actor check: \`${verify}\` re-run by ${actor}, exited 0. Builder proof inspected: ${builderProof}`,
8228
+ lesson: '',
8229
+ nextTask: '',
8230
+ });
8231
+ if (!readied.ready) {
8232
+ results.push({ ref, action: 'certify_failed', reason: readied.reason, verify });
8233
+ continue;
8234
+ }
8235
+ stampCertifyVerifyMetadata(taskDb, db, task.id, actor, verify);
8236
+ const certifiedNow = readied.event?.payload?.agent_certified === true;
8237
+ results.push({ ref, action: certifiedNow ? 'certified' : 'pass_recorded', verify });
8238
+ }
8239
+ const { outPath } = writeDefaultProjection(taskDb, db);
8240
+ const certified = results.filter((r) => r.action === 'certified').length;
8241
+ const payload = {
8242
+ ok: true,
8243
+ action: dryRun ? 'certify_verified_dry_run' : 'certify_verified',
8244
+ actor,
8245
+ certified,
8246
+ would_certify: results.filter((r) => r.action === 'would_certify').length,
8247
+ skipped: results.filter((r) => r.action === 'skipped').length,
8248
+ failed: results.filter((r) => r.action === 'verify_failed' || r.action === 'certify_failed').length,
8249
+ results,
8250
+ projection_path: outPath,
8251
+ };
8252
+ if (asJson) {
8253
+ console.log(JSON.stringify(payload, null, 2));
8254
+ } else if (results.length === 0) {
8255
+ console.log('certify-verified: no review rows to consider.');
8256
+ } else {
8257
+ for (const r of results) {
8258
+ console.log(`${r.action.padEnd(14)} ${r.ref}${r.verify ? ` \`${r.verify}\`` : ''}${r.reason ? ` (${r.reason})` : ''}`);
8259
+ }
8260
+ console.log(`certified ${certified}; humans keep denied lanes and rows without a runnable check.`);
8261
+ }
8262
+ return payload;
8263
+ }
8264
+
7912
8265
  function cmdAutoAcceptCertified(args) {
7913
8266
  const dryRun = hasFlag(args, '--dry-run');
7914
8267
  const strictVerify = hasFlag(args, '--strict-verify');
@@ -9256,13 +9609,14 @@ async function handleTaskApi(req, res, taskDb, db) {
9256
9609
  const body = await readJsonBody(req);
9257
9610
  const title = String(body.title || '').trim();
9258
9611
  if (!title) return sendJson(res, 400, { ok: false, reason: 'missing_title', detail: 'title required' });
9612
+ const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
9259
9613
  const result = taskDb.addTask(db, {
9260
9614
  title,
9261
9615
  tag: body.tag ? String(body.tag) : 'tasks',
9262
9616
  workspaceRoot: taskDb.workspaceRoot(),
9263
9617
  });
9264
9618
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
9265
- return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, projection_path: outPath, task: taskFromProjection(projection, result.id) });
9619
+ return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, operator_title_warning: operatorTitleWarning, projection_path: outPath, task: taskFromProjection(projection, result.id) });
9266
9620
  }
9267
9621
  if (req.method === 'POST' && url.pathname === '/api/tasks/clear-plan') {
9268
9622
  const body = await readJsonBody(req);
@@ -9713,6 +10067,9 @@ async function run(args) {
9713
10067
  return cmdClearPlan(rest);
9714
10068
  case 'claim': return cmdClaim(rest);
9715
10069
  case 'start': return cmdClaim(rest);
10070
+ case 'release':
10071
+ case 'unclaim':
10072
+ return cmdRelease(rest);
9716
10073
  case 'current':
9717
10074
  case 'select':
9718
10075
  return cmdCurrent(rest);
@@ -9758,6 +10115,7 @@ async function run(args) {
9758
10115
  case 'note': return cmdNote(rest);
9759
10116
  case 'say': return cmdNote(rest);
9760
10117
  case 'show': return cmdShow(rest);
10118
+ case 'inspect': return cmdInspect(rest);
9761
10119
  case 'page': return cmdPage(rest);
9762
10120
  case 'step': return cmdStep(rest);
9763
10121
  case 'review-chat':
@@ -9769,6 +10127,8 @@ async function run(args) {
9769
10127
  case 'auto-accept-certified':
9770
10128
  case 'auto-accept':
9771
10129
  return cmdAutoAcceptCertified(rest);
10130
+ case 'certify-verified':
10131
+ return cmdCertifyVerified(rest);
9772
10132
  case 'accept-group':
9773
10133
  return cmdAcceptGroup(rest);
9774
10134
  case 'revise': return cmdRevise(rest);