atris 3.45.1 → 3.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +19 -3
- package/FOR_AGENTS.md +5 -0
- package/README.md +2 -2
- package/atris/AGENTS.md +4 -3
- package/atris/CLAUDE.md +1 -1
- package/atris/atris.md +21 -1
- package/atris/skills/atris/SKILL.md +7 -3
- package/atris/skills/loop/SKILL.md +6 -3
- package/atris.md +18 -1
- package/bin/atris.js +5 -0
- package/commands/autoland.js +25 -3
- package/commands/brain.js +14 -3
- package/commands/ci.js +44 -0
- package/commands/codex-goal.js +21 -119
- package/commands/init.js +33 -10
- package/commands/mission.js +116 -53
- package/commands/pack.js +90 -25
- package/commands/sync.js +34 -6
- package/commands/task.js +239 -31
- package/commands/voice.js +12 -2
- package/lib/ci-runner.js +396 -0
- package/lib/engine-validate.js +141 -1
- package/lib/known-commands.js +1 -1
- package/lib/task-db.js +22 -3
- package/lib/task-explanation.js +229 -0
- package/lib/todo-fallback.js +6 -0
- package/lib/voice-card.js +258 -0
- package/package.json +1 -1
- package/templates/research-canonical/atris.md +6 -0
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>" [--
|
|
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
|
-
|
|
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
|
|
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}. ${
|
|
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
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
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
|
-
|
|
5914
|
-
|
|
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))
|
|
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(
|
|
5977
|
-
console.log(` ${r.
|
|
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(`
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
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
|
-
|
|
11733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|