atris 3.45.1 → 3.46.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.
package/commands/task.js CHANGED
@@ -21,6 +21,13 @@ const {
21
21
  DENIED_TAGS,
22
22
  } = require('../lib/auto-accept-certified');
23
23
  const { evaluateAcceptVerify } = require('../lib/accept-verify-gate');
24
+ const {
25
+ taskExplanation,
26
+ explanationFieldsFromInput,
27
+ explanationLines,
28
+ taskApprovalControls,
29
+ approvalLines,
30
+ } = require('../lib/task-explanation');
24
31
  const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
25
32
  const escapeRegExp = require('../lib/escape-regexp');
26
33
  const reviewIntegrity = require('../lib/review-integrity');
@@ -145,7 +152,8 @@ atris task - durable local task state (SQLite, gitignored)
145
152
  atris autoland tick # second check runs, task lands
146
153
 
147
154
  atris task Show the task desk
148
- atris task new "<title>" [--verify <cmd>] Create a task; weak or missing checks are marked degraded
155
+ atris task new "<title>" [--what-changes "..."] [--why-it-matters "..."] [--done-looks-like "..."] [--verify <cmd>]
156
+ Create a task with a plain explanation; omitted fields get honest defaults
149
157
  atris task next [--tag <tag>] [--create-next]
150
158
  Claim/show next open task; optionally create the generated Endgame fallback
151
159
  atris task continue-work <id> Create/reuse a certified Review follow-up task
@@ -177,8 +185,8 @@ atris task - durable local task state (SQLite, gitignored)
177
185
  atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
178
186
  atris task revise <id> --note "..." Send reviewed work back to Do
179
187
 
180
- atris task add "<title>" [--tag <tag>] [--goal-id <id>] [--verify <cmd>] Create a task
181
- atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] [--verify <cmd>] Create assigned work
188
+ atris task add "<title>" [--tag <tag>] [--goal-id <id>] [--what-changes "..."] [--why-it-matters "..."] [--done-looks-like "..."] [--verify <cmd>] Create a task
189
+ atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] [--what-changes "..."] [--why-it-matters "..."] [--done-looks-like "..."] [--verify <cmd>] Create assigned work
182
190
  atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
183
191
  Record a task-owned Plan stage
184
192
  atris task do <id> --as <owner> --first-move "..."
@@ -488,6 +496,16 @@ function textFlag(args, names) {
488
496
  return '';
489
497
  }
490
498
 
499
+ // Callers may write the plain explanation themselves. Anything they leave out
500
+ // gets an honest derived default at creation time.
501
+ function explanationFlags(args) {
502
+ return explanationFieldsFromInput({
503
+ what_changes: textFlag(args, ['--what-changes']),
504
+ why_it_matters: textFlag(args, ['--why-it-matters']),
505
+ done_looks_like: textFlag(args, ['--done-looks-like']),
506
+ });
507
+ }
508
+
491
509
  function landingFlags(args) {
492
510
  return {
493
511
  happened: textFlag(args, ['--landing', '--happened', '--what-happened']),
@@ -1532,6 +1550,7 @@ function buildTaskStreams(tasks, goals) {
1532
1550
  stream.tasks.push({
1533
1551
  id: task.id,
1534
1552
  title: task.title,
1553
+ explanation: task.explanation || taskExplanation(task),
1535
1554
  status: task.status,
1536
1555
  tag: task.tag,
1537
1556
  claimed_by: task.claimed_by,
@@ -1559,6 +1578,68 @@ function buildTaskStreams(tasks, goals) {
1559
1578
  .sort((a, b) => (b.active_count - a.active_count) || (b.done_count - a.done_count) || a.objective.localeCompare(b.objective));
1560
1579
  }
1561
1580
 
1581
+ // The approval half of the plain layer: one clear way to approve proposed
1582
+ // work, one clear way to ask for a change. Every enabled/blocked answer comes
1583
+ // from the existing review handoff, so this relabels what the certification
1584
+ // rules already allow and can never widen it.
1585
+ function taskApprovalFor(task, { reviewer = 'codex-review', hasExistingReviewFollowUp = null } = {}) {
1586
+ const current = taskPageCurrentStage(task);
1587
+ const metadata = task && task.metadata || {};
1588
+ const actions = taskPageActions(task, { reviewer, hasExistingReviewFollowUp });
1589
+ const inReview = current === 'review';
1590
+ const handoff = inReview
1591
+ ? reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp })
1592
+ : null;
1593
+ const planned = current === 'plan';
1594
+ const planReady = planned && Boolean(
1595
+ (metadata.task_goal || metadata.goal_objective || task.objective)
1596
+ && metadata.exit_condition
1597
+ && (metadata.verify || metadata.proof_needed)
1598
+ && metadata.first_move
1599
+ );
1600
+ const acceptEnabled = inReview ? handoffAllowsHumanAccept(handoff) : planReady;
1601
+ const planOwner = task && (task.claimed_by || taskAssignee(task)) || DEFAULT_OWNER;
1602
+ const acceptCommand = inReview
1603
+ ? actions.human_accept_command
1604
+ : planReady
1605
+ ? `atris task do ${taskRef(task)} --as ${planOwner} --first-move ${taskCommandQuote(metadata.first_move)}`
1606
+ : null;
1607
+ const requestChangeEnabled = !['done', 'blocked', 'missing'].includes(current);
1608
+ const requestChangeCommand = inReview
1609
+ ? actions.revise_command
1610
+ : planned
1611
+ ? `atris task backlog ${taskRef(task)} --reason "<what needs to change>"`
1612
+ : actions.note_command;
1613
+ const blockedReason = inReview
1614
+ ? handoffIsProofBoundaryBlocked(handoff)
1615
+ ? 'The proof points at unfinished or unsafe work. Ask for a change before approval.'
1616
+ : 'The proof still needs its required checks before a person can approve it.'
1617
+ : planned
1618
+ ? 'The plan still needs a goal, finish line, check, and first move before approval.'
1619
+ : current === 'backlog'
1620
+ ? 'This task still needs a complete plan before approval.'
1621
+ : current === 'do'
1622
+ ? 'Work is underway. Approval opens after the proof clears review.'
1623
+ : 'This task is already closed.';
1624
+ const question = inReview
1625
+ ? 'Approve the completed work, or ask for a change?'
1626
+ : planned
1627
+ ? 'Approve this plan, or ask for a change?'
1628
+ : requestChangeEnabled
1629
+ ? 'Ask for a change before work moves forward.'
1630
+ : 'No action is available on this closed task.';
1631
+ return taskApprovalControls({
1632
+ question,
1633
+ approveLabel: inReview ? 'Approve the completed work' : 'Approve this plan',
1634
+ acceptEnabled,
1635
+ acceptCommand,
1636
+ requestChangeEnabled,
1637
+ requestChangeCommand,
1638
+ blockedReason,
1639
+ waitingOn: acceptEnabled ? 'a person' : inReview ? 'the proof check' : planned ? 'a complete plan' : null,
1640
+ });
1641
+ }
1642
+
1562
1643
  function enrichTaskProjection(projection) {
1563
1644
  const root = projection.workspace_root || process.cwd();
1564
1645
  const goalSource = readGoalSources(root);
@@ -1588,7 +1669,7 @@ function enrichTaskProjection(projection) {
1588
1669
  const parentId = parent ? parent.id : metadata.parent_task_id || null;
1589
1670
  const childTasks = children.get(task.id) || [];
1590
1671
  const review = withReviewReceiptPath(taskReviewSummary(task), root);
1591
- return {
1672
+ const enriched = {
1592
1673
  ...task,
1593
1674
  objective: taskObjective(task, parent, goalSource.goals, { parentLinkType, baseObjectives }),
1594
1675
  review,
@@ -1600,6 +1681,14 @@ function enrichTaskProjection(projection) {
1600
1681
  next_task_suggestion: review ? review.next_task : null,
1601
1682
  },
1602
1683
  };
1684
+ // Recomputed here so the derived reason can use the enriched objective
1685
+ // and lineage that only exist after this pass. Explicit fields written
1686
+ // by the caller still win; nothing below this layer is dropped.
1687
+ return {
1688
+ ...enriched,
1689
+ explanation: taskExplanation(enriched),
1690
+ approval: taskApprovalFor(enriched),
1691
+ };
1603
1692
  });
1604
1693
  return {
1605
1694
  ...projection,
@@ -1709,7 +1798,13 @@ function ownerMemberIdForCloud(task) {
1709
1798
  }
1710
1799
 
1711
1800
  function taskDescriptionForCloud(task) {
1801
+ const explanation = task.explanation || taskExplanation(task);
1802
+ const approval = task.approval || taskApprovalFor(task);
1712
1803
  const lines = [
1804
+ ...explanationLines(explanation),
1805
+ ...approvalLines(approval),
1806
+ '',
1807
+ `Technical details: ${task.title}`,
1713
1808
  `Local task: ${task.id}`,
1714
1809
  `Status: ${task.status}`,
1715
1810
  `Latest event: ${task.latest_event_type || 'none'}`,
@@ -2337,14 +2432,17 @@ function taskReviewChatNote(contract) {
2337
2432
  function compactTaskForStatus(task) {
2338
2433
  if (!task) return null;
2339
2434
  const metadata = task.metadata || {};
2435
+ const title = clipStatusTitle(task.title, 140);
2340
2436
  const out = {
2341
2437
  id: task.id,
2342
2438
  display_id: task.display_id || null,
2343
2439
  legacy_ref: task.legacy_ref || taskRef(task.id),
2344
- title: clipStatusTitle(task.title, 140),
2440
+ title,
2345
2441
  result: clipStatusText(task.result || metadata.result, 180) || null,
2346
2442
  status: task.status,
2347
2443
  updated_at: task.updated_at,
2444
+ explanation: taskExplanation({ ...task, title }),
2445
+ approval: task.approval || taskApprovalFor(task),
2348
2446
  };
2349
2447
  if (task.tag) out.tag = task.tag;
2350
2448
  if (task.claimed_by) out.claimed_by = task.claimed_by;
@@ -5396,6 +5494,8 @@ function reviewQueueItem(task, root = process.cwd(), evidence = undefined) {
5396
5494
  id: task.id,
5397
5495
  display_id: task.display_id || null,
5398
5496
  title: task.title,
5497
+ explanation: task.explanation || taskExplanation(task),
5498
+ approval: task.approval || taskApprovalFor(task),
5399
5499
  tag: task.tag || null,
5400
5500
  updated_at: task.updated_at || null,
5401
5501
  review_pass_count: task.review?.agent_review_pass_count || null,
@@ -5649,7 +5749,9 @@ function cmdReviews(args) {
5649
5749
  const badge = item.evidence?.any_forced
5650
5750
  ? ' [evidence:forced]'
5651
5751
  : item.evidence?.all_passing ? ' [evidence:passing]' : '';
5652
- console.log(`${index + 1}. ${gateForHuman(item.title, { title: item.title }).text} (${ref})${badge}`);
5752
+ console.log(`${index + 1}. ${item.explanation.what_changes} (${ref})${badge}`);
5753
+ console.log(` why it matters: ${item.explanation.why_it_matters}`);
5754
+ console.log(` done looks like: ${item.explanation.done_looks_like}`);
5653
5755
  if (item.landing) {
5654
5756
  taskReviewLandingLines(item).forEach(line => console.log(line));
5655
5757
  if (verbose && item.result?.saved) console.log(` saved: ${item.result.saved}`);
@@ -5666,6 +5768,7 @@ function cmdReviews(args) {
5666
5768
  if (verbose && item.review_chat_command) console.log(` /codex: ${item.review_chat_command}`);
5667
5769
  if (item.accept_command) {
5668
5770
  console.log(` say yes: atris task accept ${item.display_id || taskRef(item.id)}`);
5771
+ console.log(` ask for a change: ${item.revise_command}`);
5669
5772
  } else if (item.blocked_accept_reason) {
5670
5773
  console.log(` approve: blocked; ${plainLandingReason(item.blocked_accept_reason)}`);
5671
5774
  console.log(` rework: ${item.revise_command}`);
@@ -5873,11 +5976,17 @@ function taskIsPlannedOpen(task) {
5873
5976
 
5874
5977
  function formatTaskLine(task) {
5875
5978
  if (!task) return 'none';
5876
- const owner = task.claimed_by ? ` @${task.claimed_by}` : '';
5877
- const assigned = !task.claimed_by && taskAssignee(task) ? ` -> ${taskAssignee(task)}` : '';
5878
- const tag = task.tag ? ` #${task.tag}` : '';
5879
- const decision = decisionMarkerFor(task) ? ` ${decisionMarkerFor(task)}` : '';
5880
- return `${taskRef(task)}${owner}${assigned}${tag}${decision} ${task.title}`;
5979
+ const explanation = task.explanation || taskExplanation(task);
5980
+ return explanation.what_changes.replace(/\.$/, '');
5981
+ }
5982
+
5983
+ function printStatusTask(label, task) {
5984
+ console.log(`${label.padEnd(7)} ${formatTaskLine(task)}`);
5985
+ if (!task) return;
5986
+ for (const line of approvalLines(task.approval || taskApprovalFor(task), { indent: ' ' })) console.log(line);
5987
+ const owner = task.claimed_by || taskAssignee(task);
5988
+ const ownerText = owner ? `; owner: ${owner}` : '';
5989
+ console.log(` Technical details: atris task show ${taskRef(task)}${ownerText}`);
5881
5990
  }
5882
5991
 
5883
5992
  function cmdStatus(args) {
@@ -5910,11 +6019,11 @@ function cmdStatus(args) {
5910
6019
  console.log('TASK STATUS');
5911
6020
  console.log(`workspace ${status.workspace_root || '(all)'}`);
5912
6021
  console.log(`plan ${status.counts.plan} / do ${status.counts.do} / review ${status.counts.review} / backlog ${status.counts.backlog} / done ${status.counts.done}`);
5913
- console.log(`current ${formatTaskLine(status.current)}`);
5914
- console.log(`next ${formatTaskLine(status.next)}`);
6022
+ printStatusTask('current', status.current);
6023
+ printStatusTask('next', status.next);
5915
6024
  if (status.needs_review.length) {
5916
6025
  console.log('review');
5917
- for (const task of status.needs_review.slice(0, 3)) console.log(` ${formatTaskLine(task)}`);
6026
+ for (const task of status.needs_review.slice(0, 3)) printStatusTask('review', task);
5918
6027
  }
5919
6028
  if (history) console.log(`history feed ${status.swarlo.feed.length} event${status.swarlo.feed.length === 1 ? '' : 's'}`);
5920
6029
  }
@@ -5969,12 +6078,17 @@ function renderTaskDesk(rows, refRows = rows) {
5969
6078
  console.log('TASK DESK');
5970
6079
  console.log('');
5971
6080
  for (const r of active.slice(0, 12)) {
6081
+ const explanation = taskExplanation(r);
5972
6082
  const owner = r.claimed_by ? ` @${r.claimed_by}` : '';
5973
6083
  const assigned = !r.claimed_by && taskAssignee(r) ? ` -> ${taskAssignee(r)}` : '';
5974
6084
  const tag = r.tag ? ` #${r.tag}` : '';
5975
6085
  const decision = decisionMarkerFor(r) ? ` ${decisionMarkerFor(r)}` : '';
5976
- console.log(`${r.status.padEnd(7)} ${taskRef(r)}${owner}${assigned}${tag}${decision}`);
5977
- console.log(` ${r.title}`);
6086
+ console.log(`What changes: ${explanation.what_changes}`);
6087
+ console.log(` ${r.status.padEnd(7)} ${taskRef(r)}${owner}${assigned}${tag}${decision}`);
6088
+ console.log(` Why it matters: ${explanation.why_it_matters}`);
6089
+ console.log(` Done looks like: ${explanation.done_looks_like}`);
6090
+ for (const line of approvalLines(taskApprovalFor(r), { indent: ' ' })) console.log(line);
6091
+ console.log(` Technical details: ${compactTechnicalDetails(r)}`);
5978
6092
  }
5979
6093
  if (active.length === 0) console.log('clear no active tasks');
5980
6094
  console.log('');
@@ -5999,6 +6113,7 @@ function cmdAdd(args) {
5999
6113
  metadata.goal_objective = String(goalObjective);
6000
6114
  }
6001
6115
  if (typeof verify === 'string' && verify.trim()) metadata.verify = verify.trim();
6116
+ Object.assign(metadata, explanationFlags(args));
6002
6117
  const taskDb = getTaskDb();
6003
6118
  const db = taskDb.open();
6004
6119
  const ws = taskDb.workspaceRoot();
@@ -6104,6 +6219,7 @@ function delegateTask(args, options = {}) {
6104
6219
  metadata.goal_objective = String(goalObjective);
6105
6220
  }
6106
6221
  if (typeof verify === 'string' && verify.trim()) metadata.verify = verify.trim();
6222
+ Object.assign(metadata, explanationFlags(args));
6107
6223
  const result = taskDb.addTask(db, {
6108
6224
  title,
6109
6225
  tag: typeof tag === 'string' ? tag : null,
@@ -6182,6 +6298,13 @@ function taskDayTitle(title, maxLength = 120) {
6182
6298
  return `${clipped || text.slice(0, maxLength).trim()}...`;
6183
6299
  }
6184
6300
 
6301
+ function compactTechnicalDetails(task, formatTitle = value => value) {
6302
+ const explanation = task.explanation || taskExplanation(task);
6303
+ return explanation.sources && explanation.sources.what_changes === 'derived'
6304
+ ? `atris task show ${taskRef(task)}`
6305
+ : formatTitle(task.title);
6306
+ }
6307
+
6185
6308
  function taskDayGroups(tasks, { now = Date.now() } = {}) {
6186
6309
  const active = tasks.filter(task => task.status !== 'done');
6187
6310
  const staleFailed = [];
@@ -6257,10 +6380,16 @@ function cmdDay(args) {
6257
6380
  for (const group of textView.groups) {
6258
6381
  console.log(`${group.owner}`);
6259
6382
  for (const task of group.tasks) {
6383
+ const explanation = task.explanation || taskExplanation(task);
6260
6384
  const tag = task.tag ? ` #${task.tag}` : '';
6261
6385
  const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
6262
6386
  const decision = decisionMarkerFor(task) ? ` ${decisionMarkerFor(task)}` : '';
6263
- console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag}${decision} ${taskDayTitle(task.title)}`);
6387
+ console.log(` What changes: ${explanation.what_changes}`);
6388
+ console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag}${decision}`);
6389
+ console.log(` Why it matters: ${explanation.why_it_matters}`);
6390
+ console.log(` Done looks like: ${explanation.done_looks_like}`);
6391
+ for (const line of approvalLines(task.approval || taskApprovalFor(task), { indent: ' ' })) console.log(line);
6392
+ console.log(` Technical details: ${compactTechnicalDetails(task, taskDayTitle)}`);
6264
6393
  }
6265
6394
  }
6266
6395
  if (textView.hiddenTasks > 0) {
@@ -6318,7 +6447,12 @@ function cmdList(args) {
6318
6447
  const rows = filterTasksByScope(rawRows, scope);
6319
6448
  const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, { everywhere }));
6320
6449
  if (wantsJson(args)) {
6321
- printJson({ ok: true, action: 'list', scope: normalizeTaskQueueScope(scope), tasks: displayRows });
6450
+ const tasks = displayRows.map(task => ({
6451
+ ...task,
6452
+ explanation: taskExplanation(task),
6453
+ approval: taskApprovalFor(task),
6454
+ }));
6455
+ printJson({ ok: true, action: 'list', scope: normalizeTaskQueueScope(scope), tasks });
6322
6456
  return;
6323
6457
  }
6324
6458
  if (rows.length === 0) {
@@ -6327,10 +6461,16 @@ function cmdList(args) {
6327
6461
  return;
6328
6462
  }
6329
6463
  for (const r of displayRows) {
6464
+ const explanation = taskExplanation(r);
6330
6465
  const claim = r.claimed_by ? ` [${r.claimed_by}]` : '';
6331
6466
  const tag = r.tag ? ` #${r.tag}` : '';
6332
6467
  const decision = decisionMarkerFor(r) ? ` ${decisionMarkerFor(r)}` : '';
6333
- console.log(`${r.status.padEnd(8)} ${taskRef(r)}${claim}${tag}${decision}\t${r.title}`);
6468
+ console.log(`What changes: ${explanation.what_changes}`);
6469
+ console.log(` ${r.status.padEnd(8)} ${taskRef(r)}${claim}${tag}${decision}`);
6470
+ console.log(` Why it matters: ${explanation.why_it_matters}`);
6471
+ console.log(` Done looks like: ${explanation.done_looks_like}`);
6472
+ for (const line of approvalLines(taskApprovalFor(r), { indent: ' ' })) console.log(line);
6473
+ console.log(` Technical details: ${compactTechnicalDetails(r)}`);
6334
6474
  }
6335
6475
  }
6336
6476
 
@@ -7287,6 +7427,11 @@ function cmdShow(args) {
7287
7427
  : task.status === 'done'
7288
7428
  ? 'DONE'
7289
7429
  : task.status.toUpperCase();
7430
+ // Plain layer first. Everything below stays exactly as it was.
7431
+ for (const line of explanationLines(task.explanation || taskExplanation(task))) console.log(line);
7432
+ for (const line of approvalLines(task.approval || taskApprovalFor(task))) console.log(line);
7433
+ console.log('');
7434
+ console.log('Technical details:');
7290
7435
  console.log(`${statusLabel} ${taskRef(task)} v${task.current_version}${owner}${tag}`);
7291
7436
  console.log(task.title);
7292
7437
  if (task.metadata?.verification_status === 'degraded') {
@@ -7350,7 +7495,10 @@ function cmdPage(args) {
7350
7495
  });
7351
7496
  return;
7352
7497
  }
7353
- console.log(`TASK PAGE ${taskRef(task)}`);
7498
+ for (const line of explanationLines(page.explanation)) console.log(line);
7499
+ for (const line of approvalLines(page.approval)) console.log(line);
7500
+ console.log('');
7501
+ console.log(`Technical details: TASK PAGE ${taskRef(task)} - ${task.title}`);
7354
7502
  console.log(`Goal: ${page.goal.text || '(none)'}`);
7355
7503
  console.log(`Stage: ${page.stage.current}`);
7356
7504
  printReviewLanding(page.review);
@@ -8284,6 +8432,10 @@ function taskPageContract(task, { reviewer = 'codex-review', hasExistingReviewFo
8284
8432
  const humanAcceptEnabled = task.status === 'review' && handoffAllowsHumanAccept(reviewHandoff);
8285
8433
  return {
8286
8434
  schema: 'atris.task_page.v1',
8435
+ // First layer: what changes, why it matters, what done looks like, and the
8436
+ // two things a person can do about it. The full contract follows unchanged.
8437
+ explanation: task.explanation || taskExplanation(task),
8438
+ approval: taskApprovalFor(task, { reviewer, hasExistingReviewFollowUp }),
8287
8439
  task: {
8288
8440
  id: task.id,
8289
8441
  ref: taskRef(task),
@@ -11478,6 +11630,8 @@ function taskBoardTemplate(model) {
11478
11630
  .meta { margin-top:6px; color:var(--muted); font-size:11px; display:flex; gap:6px; flex-wrap:wrap; font-family:var(--mono); }
11479
11631
  .pill { border:1px solid var(--line); border-radius:999px; padding:1px 6px; }
11480
11632
  .why { margin-top:7px; color:var(--muted); font-size:11px; line-height:1.25; }
11633
+ .plain { margin-top:6px; font-size:11px; line-height:1.35; color:var(--text); }
11634
+ .plain div + div { color:var(--muted); }
11481
11635
  .fact { margin:10px 0; background:oklch(15% 0.012 160); border:1px solid var(--line); border-radius:7px; padding:8px; font-size:12px; line-height:1.35; }
11482
11636
  .fact b { color:var(--muted); font-size:11px; display:block; margin-bottom:3px; }
11483
11637
  .room { margin-top:14px; border-top:1px solid var(--line); padding-top:12px; }
@@ -11685,7 +11839,8 @@ function taskBoardTemplate(model) {
11685
11839
  $('overview').querySelectorAll('.chainbox .chainitem strong').forEach((el, i) => {
11686
11840
  if (i === 0) return;
11687
11841
  const task = latest[i - 1];
11688
- el.textContent = (task.review && task.review.next_task) ? task.title + ' -> ' + task.review.next_task : task.title;
11842
+ const what = task.explanation && task.explanation.what_changes || task.title;
11843
+ el.textContent = (task.review && task.review.next_task) ? what + ' -> ' + task.review.next_task : what;
11689
11844
  });
11690
11845
  }
11691
11846
 
@@ -11720,7 +11875,9 @@ function taskBoardTemplate(model) {
11720
11875
  root.querySelectorAll('.stream h2').forEach((el, i) => { el.textContent = streams[i].objective; });
11721
11876
  root.querySelectorAll('.stream').forEach((streamEl, i) => {
11722
11877
  const tasks = streams[i].tasks.filter((task) => task.status !== 'done').slice(0, 3);
11723
- streamEl.querySelectorAll('.streamtask strong').forEach((el, idx) => { el.textContent = tasks[idx].title; });
11878
+ streamEl.querySelectorAll('.streamtask strong').forEach((el, idx) => {
11879
+ el.textContent = tasks[idx].explanation && tasks[idx].explanation.what_changes || tasks[idx].title;
11880
+ });
11724
11881
  });
11725
11882
  }
11726
11883
 
@@ -11729,14 +11886,26 @@ function taskBoardTemplate(model) {
11729
11886
  btn.className = 'card' + (selected === task.id ? ' active' : '');
11730
11887
  btn.onclick = () => { selected = task.id; render(); };
11731
11888
  const owner = task.claimed_by ? '@' + task.claimed_by : 'unowned';
11732
- btn.innerHTML = '<div class="title"></div><div class="meta"><span class="pill"></span><span class="pill"></span><span class="pill"></span></div><div class="why"></div>';
11733
- btn.querySelector('.title').textContent = task.title;
11889
+ // Plain layer first: what changes, why it matters, what done looks like.
11890
+ // Ids, owner, and version stay below it as detail.
11891
+ btn.innerHTML = '<div class="title"></div><div class="plain"></div>'
11892
+ + '<div class="meta"><span class="pill"></span><span class="pill"></span><span class="pill"></span></div><div class="why"></div>';
11893
+ const plain = task.explanation || {};
11894
+ btn.querySelector('.title').textContent = plain.what_changes || task.title;
11895
+ const plainRows = [];
11896
+ plainRows.push('Why it matters: ' + (plain.why_it_matters || 'No reason recorded yet.'));
11897
+ plainRows.push('Done looks like: ' + (plain.done_looks_like || 'Proof of the work, then review.'));
11898
+ const plainBox = btn.querySelector('.plain');
11899
+ for (const row of plainRows) {
11900
+ const line = document.createElement('div');
11901
+ line.textContent = row;
11902
+ plainBox.appendChild(line);
11903
+ }
11734
11904
  const pills = btn.querySelectorAll('.pill');
11735
11905
  pills[0].textContent = task.display_id || task.id.slice(0, 8);
11736
11906
  pills[1].textContent = owner;
11737
11907
  pills[2].textContent = 'v' + task.current_version;
11738
- const why = task.objective || (task.lineage && task.lineage.parent_title) || (task.review && task.review.proof) || '';
11739
- btn.querySelector('.why').textContent = why;
11908
+ btn.querySelector('.why').textContent = 'Technical details: ' + task.title;
11740
11909
  return btn;
11741
11910
  }
11742
11911
 
@@ -11753,6 +11922,11 @@ function taskBoardTemplate(model) {
11753
11922
  room.innerHTML = [
11754
11923
  '<h3></h3>',
11755
11924
  '<div class="meta"><span class="pill">' + task.status + '</span><span class="pill">' + (task.claimed_by || 'unowned') + '</span><span class="pill">v' + task.current_version + '</span></div>',
11925
+ '<div class="fact"><b>What changes</b><div id="taskWhatChanges"></div></div>',
11926
+ '<div class="fact"><b>Why it matters</b><div id="taskWhyItMatters"></div></div>',
11927
+ '<div class="fact"><b>Done looks like</b><div id="taskDoneLooksLike"></div></div>',
11928
+ '<div class="fact"><b>Approval</b><div id="taskApproval"></div></div>',
11929
+ '<div class="fact"><b>Technical details</b><div id="taskTechnicalDetails"></div></div>',
11756
11930
  '<div class="fact"><b>Goal</b><div id="taskGoal"></div></div>',
11757
11931
  '<div class="fact"><b>Lineage</b><div id="taskLineage"></div></div>',
11758
11932
  '<div class="fact"><b>Result</b><div id="taskHappened"></div></div>',
@@ -11765,9 +11939,18 @@ function taskBoardTemplate(model) {
11765
11939
  '<label>Proof</label><input id="proof" placeholder="npm test, PR link, screenshot, blocked reason...">',
11766
11940
  '<label>Lesson</label><textarea id="lesson" placeholder="What did this task teach us?"></textarea>',
11767
11941
  '<label>Next task</label><input id="nextTask" placeholder="Optional next sharper task">',
11768
- '<div class="actions"><button id="claim">Claim</button><button id="saveNote">Say</button><button id="finish" class="primary full"></button></div>'
11942
+ '<div class="actions"><button id="claim">Claim</button><button id="saveNote">Say</button><button id="requestChange">Ask for a change</button><button id="finish" class="primary full"></button></div>'
11769
11943
  ].join('');
11770
- room.querySelector('h3').textContent = task.title;
11944
+ const plain = task.explanation || {};
11945
+ const approval = task.approval || {};
11946
+ room.querySelector('h3').textContent = plain.what_changes || task.title;
11947
+ $('taskWhatChanges').textContent = plain.what_changes || task.title;
11948
+ $('taskWhyItMatters').textContent = plain.why_it_matters || 'No reason recorded yet.';
11949
+ $('taskDoneLooksLike').textContent = plain.done_looks_like || 'Proof of the work, then review.';
11950
+ $('taskApproval').textContent = approval.approve && approval.approve.enabled
11951
+ ? approval.question
11952
+ : (approval.approve && approval.approve.blocked_reason) || 'Nothing to approve yet.';
11953
+ $('taskTechnicalDetails').textContent = task.title;
11771
11954
  $('taskGoal').textContent = task.objective || 'No matching goal yet.';
11772
11955
  $('taskLineage').textContent = 'parent: ' + parent + ' / next: ' + children;
11773
11956
  const result = task.review && task.review.result || {};
@@ -11785,8 +11968,23 @@ function taskBoardTemplate(model) {
11785
11968
  ? ((task.review.proof || 'no proof') + ' / ' + (task.review.lesson || 'no lesson'))
11786
11969
  : 'No proof yet.';
11787
11970
  room.querySelectorAll('.msg div:last-child').forEach((el, i) => { el.textContent = task.messages[i].content; });
11788
- $('finish').textContent = task.status === 'review' ? 'Accept proof' : 'Move to Review';
11971
+ const canApprove = Boolean(approval.approve && approval.approve.enabled);
11972
+ const planned = task.metadata && task.metadata.stage === 'plan';
11973
+ $('finish').textContent = canApprove
11974
+ ? approval.approve.label
11975
+ : task.status === 'claimed' ? 'Move to Review' : 'Approval not ready';
11976
+ $('finish').disabled = !canApprove && task.status !== 'claimed';
11789
11977
  $('claim').onclick = () => mutate('/api/tasks/' + task.id + '/claim', { owner: 'operator' });
11978
+ // Ask for a change runs the existing revise gate; it never lands work.
11979
+ $('requestChange').disabled = !(approval.request_change && approval.request_change.enabled);
11980
+ $('requestChange').textContent = approval.request_change && approval.request_change.label || 'Ask for a change';
11981
+ $('requestChange').onclick = () => {
11982
+ const note = $('note').value.trim();
11983
+ if (!note) { $('note').focus(); return; }
11984
+ const action = task.status === 'review' ? 'revise' : planned ? 'backlog' : 'message';
11985
+ const body = action === 'message' ? { actor: 'operator', content: note } : { actor: 'operator', note: note, reason: note };
11986
+ mutate('/api/tasks/' + task.id + '/' + action, body);
11987
+ };
11790
11988
  $('saveNote').onclick = () => mutate('/api/tasks/' + task.id + '/message', { actor: 'operator', content: $('note').value });
11791
11989
  $('finish').onclick = () => {
11792
11990
  const proof = $('proof').value.trim();
@@ -11796,9 +11994,11 @@ function taskBoardTemplate(model) {
11796
11994
  if (proof) payload.proof = proof;
11797
11995
  if (lesson) payload.lesson = lesson;
11798
11996
  if (nextTask) payload.next = nextTask;
11799
- if (task.status === 'review') {
11997
+ if (task.status === 'review' && canApprove) {
11800
11998
  payload.createNext = Boolean(nextTask || (task.review && task.review.next_task));
11801
11999
  mutate('/api/tasks/' + task.id + '/accept', payload);
12000
+ } else if (planned && canApprove) {
12001
+ mutate('/api/tasks/' + task.id + '/do', { actor: task.metadata.assigned_to || task.claimed_by || 'operator', first_move: task.metadata.first_move });
11802
12002
  } else {
11803
12003
  mutate('/api/tasks/' + task.id + '/ready', payload);
11804
12004
  }
@@ -11999,11 +12199,15 @@ async function postTaskCreate({ req, res, taskDb, db }) {
11999
12199
  const title = String(body.title || '').trim();
12000
12200
  if (!title) return sendJson(res, 400, { ok: false, reason: 'missing_title', detail: 'title required' });
12001
12201
  const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
12202
+ const metadata = {
12203
+ ...(body.verify ? { verify: String(body.verify).trim() } : {}),
12204
+ ...explanationFieldsFromInput(body),
12205
+ };
12002
12206
  const result = taskDb.addTask(db, {
12003
12207
  title,
12004
12208
  tag: body.tag ? String(body.tag) : 'tasks',
12005
12209
  workspaceRoot: taskDb.workspaceRoot(),
12006
- metadata: body.verify ? { verify: String(body.verify).trim() } : null,
12210
+ metadata: Object.keys(metadata).length ? metadata : null,
12007
12211
  });
12008
12212
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
12009
12213
  return sendJson(res, 200, {
@@ -12716,4 +12920,8 @@ module.exports = {
12716
12920
  projectionWishes,
12717
12921
  taskBoardViewModel,
12718
12922
  taskBoardTemplate,
12923
+ enrichTaskProjection,
12924
+ taskApprovalFor,
12925
+ taskPageContract,
12926
+ taskDescriptionForCloud,
12719
12927
  };
package/commands/voice.js CHANGED
@@ -6,6 +6,10 @@ const path = require('node:path');
6
6
  const { spawnSync } = require('node:child_process');
7
7
 
8
8
  const deterministicVoice = require('../scripts/det/voice');
9
+ const {
10
+ buildVoiceCardHookJson,
11
+ voiceCardForRoot,
12
+ } = require('../lib/voice-card');
9
13
 
10
14
  const JUDGE_TIMEOUT_MS = 30_000;
11
15
 
@@ -161,8 +165,9 @@ function showVoiceHelp(stdout = process.stdout) {
161
165
  stdout.write([
162
166
  'usage: atris voice scan [--json]',
163
167
  ' atris voice judge',
168
+ ' atris voice card [--hook]',
164
169
  '',
165
- 'scan checks binary voice tells. judge checks reply shape and plainness.',
170
+ 'scan checks binary voice tells. judge checks reply shape and plainness. card prints the house voice.',
166
171
  '',
167
172
  ].join('\n'));
168
173
  }
@@ -175,8 +180,13 @@ function voiceCommand(args, options = {}) {
175
180
  showVoiceHelp(stdout);
176
181
  return 0;
177
182
  }
183
+ if (subcommand === 'card') {
184
+ const card = voiceCardForRoot(options.repoRoot || process.cwd());
185
+ stdout.write(`${args.includes('--hook') ? JSON.stringify(buildVoiceCardHookJson(card)) : card}\n`);
186
+ return 0;
187
+ }
178
188
  if (!['scan', 'judge'].includes(subcommand)) {
179
- stderr.write('usage: atris voice scan [--json] | atris voice judge\n');
189
+ stderr.write('usage: atris voice scan [--json] | atris voice judge | atris voice card [--hook]\n');
180
190
  return 2;
181
191
  }
182
192