atris 3.34.0 → 3.35.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/AGENTS.md +0 -2
- package/FOR_AGENTS.md +5 -3
- package/atris/skills/engines/SKILL.md +16 -7
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris.md +4 -2
- package/ax +475 -17
- package/bin/atris.js +197 -44
- package/commands/aeo.js +52 -0
- package/commands/autoland.js +313 -19
- package/commands/business.js +91 -8
- package/commands/codex-goal.js +26 -2
- package/commands/drive.js +187 -0
- package/commands/engine.js +73 -2
- package/commands/feed.js +202 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +262 -3
- package/commands/init.js +13 -1
- package/commands/integrations.js +39 -11
- package/commands/interview.js +143 -0
- package/commands/land.js +114 -13
- package/commands/lesson.js +112 -1
- package/commands/linear.js +38 -0
- package/commands/member.js +398 -42
- package/commands/mission.js +554 -64
- package/commands/now.js +25 -1
- package/commands/radar.js +259 -14
- package/commands/serve.js +54 -0
- package/commands/status.js +50 -5
- package/commands/stripe.js +38 -0
- package/commands/supabase.js +39 -0
- package/commands/task.js +935 -71
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/worktree.js +68 -10
- package/commands/write.js +399 -0
- package/lib/auto-accept-certified.js +70 -19
- package/lib/autoland.js +39 -3
- package/lib/fleet.js +250 -9
- package/lib/memory-view.js +14 -5
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/official-cli-integration.js +174 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/review-integrity.js +147 -0
- package/lib/runner-command.js +23 -0
- package/lib/task-db.js +220 -7
- package/lib/task-proof.js +20 -0
- package/lib/task-receipt.js +93 -0
- package/package.json +1 -1
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/commands/task.js
CHANGED
|
@@ -7,10 +7,18 @@ const fs = require('fs');
|
|
|
7
7
|
const http = require('http');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
-
const { taskProofState
|
|
11
|
-
const {
|
|
10
|
+
const { taskProofState } = require('../lib/task-proof');
|
|
11
|
+
const {
|
|
12
|
+
evaluateAutoAccept,
|
|
13
|
+
isAgentCertified,
|
|
14
|
+
isAutoCertifyVerifyCommandAllowed,
|
|
15
|
+
parseVerifyCommand,
|
|
16
|
+
runVerifyCommand,
|
|
17
|
+
DENIED_TAGS,
|
|
18
|
+
} = require('../lib/auto-accept-certified');
|
|
12
19
|
const { extractReceiptEvidence } = require('../lib/receipt-evidence');
|
|
13
20
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
21
|
+
const reviewIntegrity = require('../lib/review-integrity');
|
|
14
22
|
const {
|
|
15
23
|
normalizeOwnerSlug,
|
|
16
24
|
resolveFunctionalOwner: resolveFunctionalTaskOwner,
|
|
@@ -111,6 +119,13 @@ function taskUsageText() {
|
|
|
111
119
|
return `
|
|
112
120
|
atris task - durable local task state (SQLite, gitignored)
|
|
113
121
|
|
|
122
|
+
golden path (zero human turns):
|
|
123
|
+
atris task delegate "fix the login bug" --to <member>
|
|
124
|
+
atris task claim <id> --as <member>
|
|
125
|
+
... build ...
|
|
126
|
+
atris task ready <id> --verify
|
|
127
|
+
atris autoland tick # second check runs, task lands
|
|
128
|
+
|
|
114
129
|
atris task Show the task desk
|
|
115
130
|
atris task new "<title>" Create a task
|
|
116
131
|
atris task next [--tag <tag>] [--create-next]
|
|
@@ -120,6 +135,8 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
120
135
|
atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
|
|
121
136
|
atris task ready <id> --proof "..." Agent proof ready; native goal can complete
|
|
122
137
|
atris task ready <id> --verify "<cmd>" Run <cmd>; only ready if it exits 0 (executed proof)
|
|
138
|
+
Writes atris/runs/ receipt (pass or fail), folds path into proof
|
|
139
|
+
atris task receipt <id> --verify "<cmd>" Run <cmd> and write an atris/runs/ receipt without going to ready
|
|
123
140
|
atris task plan-preview "<purpose>" [--tag <tag>] [--owner <member>] [--task <id>]
|
|
124
141
|
Show the plain Plan before work starts
|
|
125
142
|
atris task ready <id> --proof "..." [--changed "..." --checked "..." --saved "..." --try "..."]
|
|
@@ -133,20 +150,23 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
133
150
|
Human accepts proof, marks done; --public also publishes AgentXP
|
|
134
151
|
atris task certify-verified [--dry-run] [--limit <n>] [--as <actor>]
|
|
135
152
|
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)
|
|
136
|
-
atris task auto-accept-certified --dry-run [--strict-verify] [--limit <n>]
|
|
153
|
+
atris task auto-accept-certified --dry-run [--strict-verify] [--all] [--limit <n>]
|
|
137
154
|
Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
|
|
155
|
+
atris task sweep --auto-accept [--json] Auto-accept verified Review rows; protected lanes wait for human
|
|
138
156
|
atris task revise <id> --note "..." Send reviewed work back to Do
|
|
139
157
|
|
|
140
158
|
atris task add "<title>" [--tag <tag>] [--goal-id <id>] Create a task
|
|
141
|
-
atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] Create assigned work
|
|
159
|
+
atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] Create assigned work
|
|
142
160
|
atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
|
|
143
161
|
Record a task-owned Plan stage
|
|
144
162
|
atris task do <id> --as <owner> --first-move "..."
|
|
145
163
|
Start task-owned Do work from the plan
|
|
146
164
|
atris task backlog <id> [--reason "..."] Move a planned open task back to Backlog
|
|
147
165
|
atris task clear-plan --yes Move all planned open tasks back to Backlog
|
|
148
|
-
atris task day [--json]
|
|
149
|
-
|
|
166
|
+
atris task day [--all] [--everywhere] [--json] show today's owner-grouped task list
|
|
167
|
+
--all stays in this workspace; --everywhere spans workspaces
|
|
168
|
+
atris task list [--all] [--everywhere] [--status <s>]
|
|
169
|
+
list tasks in this workspace; --everywhere spans workspaces
|
|
150
170
|
atris task claim <id> [--as <owner>] Atomic claim
|
|
151
171
|
atris task release <id> [--as <owner>] Release your own mistaken claim back to open
|
|
152
172
|
atris task capabilities [--json] Read-only task CLI/API capability contract
|
|
@@ -163,6 +183,9 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
163
183
|
Advance the scoped current task one safe step
|
|
164
184
|
review-state lanes: needs-agent, continue-work, human-accept-waiting, certified
|
|
165
185
|
atris task note <id> "<message>" Append dialogue/context to a task
|
|
186
|
+
atris task tag <id> --add <tag> [--remove <tag>]
|
|
187
|
+
Update tags on an existing task (e.g. --add needs-human to hold it
|
|
188
|
+
from sweep + fleet staffing); logs a task_tags_updated event
|
|
166
189
|
atris task show <id> [--json] Show a task card + dialogue
|
|
167
190
|
atris task inspect <id> --fields review,status,title [--json]
|
|
168
191
|
Field-selectable task state (review metadata, status, title, owner, tag)
|
|
@@ -170,6 +193,11 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
170
193
|
atris task step <id> [--json] Refine chat, then advance one safe Plan/Do/Review step
|
|
171
194
|
atris task done <id> --proof "..." Mark complete with proof
|
|
172
195
|
atris task done <id> --failed [--proof "..."] Mark failed, optionally reviewed
|
|
196
|
+
atris task archive <id> --reason "..." [--from-failed]
|
|
197
|
+
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
198
|
+
--from-failed opts in to relabel a fail-closed row (never done)
|
|
199
|
+
atris task relabel-archived [--dry-run|--apply]
|
|
200
|
+
One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
|
|
173
201
|
atris task finish <id> --proof "..." Legacy alias for done with proof
|
|
174
202
|
atris task review <id> --reward <n> [--verify "<cmd>"]
|
|
175
203
|
Write review event + RSI episode
|
|
@@ -183,10 +211,13 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
183
211
|
atris task sync --dry-run Plan cloud/Swarlo task sync writes
|
|
184
212
|
atris task import <file> One-shot import from TODO.md
|
|
185
213
|
atris task lineage <id> [--json] Show endgame -> tasks -> commits chain
|
|
186
|
-
atris task events [id] [--limit <n>]
|
|
187
|
-
atris task events --all
|
|
188
|
-
atris task
|
|
189
|
-
atris task
|
|
214
|
+
atris task events [id] [--limit <n>] print recent task events
|
|
215
|
+
atris task events --all print the full current-workspace ledger
|
|
216
|
+
atris task events --everywhere print the full ledger across workspaces
|
|
217
|
+
atris task export [--all] [--everywhere] [--out <file>]
|
|
218
|
+
write web/desktop JSON projection
|
|
219
|
+
atris task render [--all] [--everywhere] [--out <file>]
|
|
220
|
+
regenerate compact TODO.md view from state
|
|
190
221
|
atris task where Print db path + workspace scope
|
|
191
222
|
atris task help This help
|
|
192
223
|
|
|
@@ -226,6 +257,15 @@ function hasFlag(args, name) {
|
|
|
226
257
|
return args.indexOf(name) !== -1;
|
|
227
258
|
}
|
|
228
259
|
|
|
260
|
+
function taskScopeEverywhere(args = [], options = {}) {
|
|
261
|
+
if (options.everywhere !== undefined) return Boolean(options.everywhere);
|
|
262
|
+
return hasFlag(args, '--everywhere');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function scopedWorkspaceRoot(taskDb, args = [], options = {}) {
|
|
266
|
+
return taskScopeEverywhere(args, options) ? null : taskDb.workspaceRoot();
|
|
267
|
+
}
|
|
268
|
+
|
|
229
269
|
function hasEmptyFlagValue(args, name) {
|
|
230
270
|
const i = args.indexOf(name);
|
|
231
271
|
return i !== -1 && args[i + 1] === '';
|
|
@@ -462,10 +502,11 @@ function positional(args) {
|
|
|
462
502
|
});
|
|
463
503
|
}
|
|
464
504
|
|
|
465
|
-
function writeDefaultProjection(taskDb, db,
|
|
505
|
+
function writeDefaultProjection(taskDb, db, options = {}) {
|
|
506
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, [], options);
|
|
466
507
|
const projection = enrichTaskProjection(taskDb.taskProjection(db, {
|
|
467
|
-
workspaceRoot
|
|
468
|
-
limit: 500,
|
|
508
|
+
workspaceRoot,
|
|
509
|
+
limit: options.all ? null : 500,
|
|
469
510
|
}));
|
|
470
511
|
const outPath = path.resolve(path.join('.atris', 'state', 'tasks.projection.json'));
|
|
471
512
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
@@ -692,6 +733,9 @@ function certifiedReviewNextAction(nextTaskTitle) {
|
|
|
692
733
|
}
|
|
693
734
|
|
|
694
735
|
function proofBoundaryBlockedEvaluation(task) {
|
|
736
|
+
// strictVerify stays off here: this is a render-path probe for the boundary
|
|
737
|
+
// reason only, and the default-true strict mode would spawn the verify
|
|
738
|
+
// subprocess for every certified row just to draw the desk.
|
|
695
739
|
const evaluation = evaluateAutoAccept(task, { strictVerify: false, minPasses: 0 });
|
|
696
740
|
return evaluation && evaluation.reason === 'proof_unmerged_or_draft_pr_boundary'
|
|
697
741
|
? evaluation
|
|
@@ -1057,11 +1101,65 @@ function taskReviewSavedText(task, review = {}, ref = taskRef(task)) {
|
|
|
1057
1101
|
return `Work record saved as ${ref}.`;
|
|
1058
1102
|
}
|
|
1059
1103
|
|
|
1104
|
+
function cleanReviewProofText(value) {
|
|
1105
|
+
const text = String(value || '').trim();
|
|
1106
|
+
return text || null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function reviewMessageLooksLikeProof(value) {
|
|
1110
|
+
const text = cleanReviewProofText(value);
|
|
1111
|
+
return Boolean(text && /\b(?:proof|verified|verifier|passed|receipt|git diff --check|node --test|npm test|pnpm test|yarn test|pytest|typecheck)\b/i.test(text));
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function taskReviewEventProof(task) {
|
|
1115
|
+
const events = Array.isArray(task.events) ? task.events : [];
|
|
1116
|
+
for (const event of events.slice().reverse()) {
|
|
1117
|
+
const payload = event && event.payload && typeof event.payload === 'object' ? event.payload : {};
|
|
1118
|
+
const explicit = cleanReviewProofText(payload.proof || payload.verify);
|
|
1119
|
+
if (explicit) return explicit;
|
|
1120
|
+
const message = cleanReviewProofText(payload.content || payload.chat_packet || payload.stage_packet);
|
|
1121
|
+
if (message && reviewMessageLooksLikeProof(message)) return message;
|
|
1122
|
+
}
|
|
1123
|
+
const messages = Array.isArray(task.messages) ? task.messages : [];
|
|
1124
|
+
for (const message of messages.slice().reverse()) {
|
|
1125
|
+
const content = cleanReviewProofText(message && message.content);
|
|
1126
|
+
if (content && reviewMessageLooksLikeProof(content)) return content;
|
|
1127
|
+
}
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function taskReviewProofFallback(task, payload = {}) {
|
|
1132
|
+
if (!task || task.status !== 'review') return null;
|
|
1133
|
+
const metadata = task.metadata || {};
|
|
1134
|
+
return cleanReviewProofText(metadata.latest_agent_proof)
|
|
1135
|
+
|| cleanReviewProofText(payload.proof)
|
|
1136
|
+
|| cleanReviewProofText(metadata.proof)
|
|
1137
|
+
|| cleanReviewProofText(metadata.verify)
|
|
1138
|
+
|| cleanReviewProofText(metadata.latest_agent_verify)
|
|
1139
|
+
|| cleanReviewProofText(payload.verify)
|
|
1140
|
+
|| taskReviewEventProof(task);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function reviewReceiptPath(proofText, root) {
|
|
1144
|
+
const evidence = extractReceiptEvidence(proofText, root);
|
|
1145
|
+
if (!evidence) return null;
|
|
1146
|
+
return evidence.receipts?.[0]?.path || evidence.missing?.[0] || null;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function withReviewReceiptPath(review, root) {
|
|
1150
|
+
if (!review) return null;
|
|
1151
|
+
return {
|
|
1152
|
+
...review,
|
|
1153
|
+
receipt_path: reviewReceiptPath(review.proof, root),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1060
1157
|
function taskReviewSummary(task) {
|
|
1061
1158
|
const reviewed = (task.events || []).slice().reverse().find(e => e.event_type === 'reviewed' || e.event_type === 'proof_ready' || e.event_type === 'revision_requested');
|
|
1062
1159
|
const payload = reviewed && reviewed.payload || {};
|
|
1063
1160
|
const metadata = task.metadata || {};
|
|
1064
|
-
|
|
1161
|
+
const fallbackProof = taskReviewProofFallback(task, payload);
|
|
1162
|
+
if (!reviewed && !metadata.approval_status && !metadata.agent_review_pass_count && !metadata.human_revision_count && !metadata.agent_certified && !fallbackProof) return null;
|
|
1065
1163
|
if (reviewed && reviewed.event_type === 'revision_requested') {
|
|
1066
1164
|
const review = {
|
|
1067
1165
|
summary: reviewSummary(task, payload),
|
|
@@ -1101,7 +1199,7 @@ function taskReviewSummary(task) {
|
|
|
1101
1199
|
const review = {
|
|
1102
1200
|
summary: reviewSummary(task, payload),
|
|
1103
1201
|
reward: reviewed && reviewed.event_type === 'reviewed' && payload.reward !== undefined ? payload.reward : null,
|
|
1104
|
-
proof: readyField('proof', 'latest_agent_proof'),
|
|
1202
|
+
proof: readyField('proof', 'latest_agent_proof') || fallbackProof,
|
|
1105
1203
|
lesson: readyField('lesson', 'latest_agent_lesson'),
|
|
1106
1204
|
next_task: readyField('next_task', 'latest_agent_next_task'),
|
|
1107
1205
|
approval_status: metadata.approval_status || (task.status === 'review' ? 'pending' : null),
|
|
@@ -1132,6 +1230,7 @@ function taskReviewInspectMetadata(task) {
|
|
|
1132
1230
|
human_revision_count: review.human_revision_count || null,
|
|
1133
1231
|
human_revision_note: review.human_revision_note || null,
|
|
1134
1232
|
reward: review.reward ?? null,
|
|
1233
|
+
receipt_path: review.receipt_path || null,
|
|
1135
1234
|
};
|
|
1136
1235
|
}
|
|
1137
1236
|
|
|
@@ -1317,7 +1416,7 @@ function enrichTaskProjection(projection) {
|
|
|
1317
1416
|
const parentLinkType = parentFromParentId ? 'parent_task_id' : parentFromGoalId ? 'goal_id' : null;
|
|
1318
1417
|
const parentId = parent ? parent.id : metadata.parent_task_id || null;
|
|
1319
1418
|
const childTasks = children.get(task.id) || [];
|
|
1320
|
-
const review = taskReviewSummary(task);
|
|
1419
|
+
const review = withReviewReceiptPath(taskReviewSummary(task), root);
|
|
1321
1420
|
return {
|
|
1322
1421
|
...task,
|
|
1323
1422
|
objective: taskObjective(task, parent, goalSource.goals, { parentLinkType, baseObjectives }),
|
|
@@ -1502,6 +1601,44 @@ function latestTaskEvent(task) {
|
|
|
1502
1601
|
return events.length ? events[events.length - 1] : null;
|
|
1503
1602
|
}
|
|
1504
1603
|
|
|
1604
|
+
function verifiedProofCommand(proof) {
|
|
1605
|
+
const match = String(proof || '').match(/\[verified\]\s+`([^`]+)`\s+passed\s+\(exit 0\)/i);
|
|
1606
|
+
return match ? String(match[1] || '').trim() : '';
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function autoCertifyCommandCandidatesForTask(task) {
|
|
1610
|
+
const metadata = task && task.metadata || {};
|
|
1611
|
+
const review = task && task.review || {};
|
|
1612
|
+
const proof = String(review.proof || metadata.latest_agent_proof || '');
|
|
1613
|
+
const candidates = [
|
|
1614
|
+
metadata.verify,
|
|
1615
|
+
metadata.latest_agent_verify,
|
|
1616
|
+
verifiedProofCommand(review.proof),
|
|
1617
|
+
verifiedProofCommand(metadata.latest_agent_proof),
|
|
1618
|
+
...taskReviewEvidenceCommands(proof),
|
|
1619
|
+
].map(value => String(value || '').trim()).filter(Boolean);
|
|
1620
|
+
const seen = new Set();
|
|
1621
|
+
return candidates.filter((candidate) => {
|
|
1622
|
+
const key = candidate.toLowerCase();
|
|
1623
|
+
if (seen.has(key)) return false;
|
|
1624
|
+
seen.add(key);
|
|
1625
|
+
return true;
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function reviewBlockerForTask(task) {
|
|
1630
|
+
const ref = taskRef(task);
|
|
1631
|
+
const candidates = autoCertifyCommandCandidatesForTask(task);
|
|
1632
|
+
const safe = candidates.find(command => isAutoCertifyVerifyCommandAllowed(command));
|
|
1633
|
+
const unsafe = candidates.find(command => !isAutoCertifyVerifyCommandAllowed(command));
|
|
1634
|
+
const reason = !safe && unsafe ? 'verify_command_not_allowed' : 'needs_second_actor_review';
|
|
1635
|
+
return {
|
|
1636
|
+
reason,
|
|
1637
|
+
verify_command: (!safe && unsafe) ? unsafe : (safe || candidates[0] || null),
|
|
1638
|
+
next_command: `atris task review-chat ${ref} --as codex-review`,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1505
1642
|
function reviewHandoffForTask(task, { suppressExistingFollowUp = false, hasExistingReviewFollowUp = null } = {}) {
|
|
1506
1643
|
const review = task && task.review || {};
|
|
1507
1644
|
if (task && task.status !== 'review') return null;
|
|
@@ -1522,6 +1659,11 @@ function reviewHandoffForTask(task, { suppressExistingFollowUp = false, hasExist
|
|
|
1522
1659
|
handoff.reason = proofBoundary.reason;
|
|
1523
1660
|
handoff.next_action_detail = proofBoundary.next_action || null;
|
|
1524
1661
|
handoff.revise_command = `atris task revise ${taskRef(task)} --note "<replace stale PR proof with merged proof or move back to Do>"`;
|
|
1662
|
+
} else if (!agentCertified) {
|
|
1663
|
+
const blocker = reviewBlockerForTask(task);
|
|
1664
|
+
handoff.reason = blocker.reason;
|
|
1665
|
+
handoff.next_action_detail = blocker.verify_command || null;
|
|
1666
|
+
handoff.review_chat_command = blocker.next_command;
|
|
1525
1667
|
} else if (agentCertified && nextTask && !hasExistingFollowUp) {
|
|
1526
1668
|
handoff.next_task = nextTask;
|
|
1527
1669
|
handoff.continue_work_command = continueWorkCommandForTask(task);
|
|
@@ -1980,6 +2122,7 @@ function compactTaskForStatus(task) {
|
|
|
1980
2122
|
if (task.review.proof) review.proof = clipStatusText(task.review.proof, 180);
|
|
1981
2123
|
if (task.review.lesson) review.lesson = clipStatusText(task.review.lesson, 180);
|
|
1982
2124
|
if (task.review.next_task) review.next_task = clipStatusText(task.review.next_task, 140);
|
|
2125
|
+
if (Object.prototype.hasOwnProperty.call(task.review, 'receipt_path')) review.receipt_path = task.review.receipt_path || null;
|
|
1983
2126
|
if (task.review.approval_status) review.approval_status = task.review.approval_status;
|
|
1984
2127
|
if (task.review.agent_review_pass_count) review.agent_review_pass_count = task.review.agent_review_pass_count;
|
|
1985
2128
|
if (task.review.agent_certified) review.agent_certified = task.review.agent_certified;
|
|
@@ -3055,6 +3198,7 @@ function taskCapabilitiesCheckReport(taskDb, db, args = [], options = {}) {
|
|
|
3055
3198
|
const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
|
|
3056
3199
|
const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
|
|
3057
3200
|
const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
|
|
3201
|
+
const everywhere = taskScopeEverywhere(args, options);
|
|
3058
3202
|
const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
|
|
3059
3203
|
const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
|
|
3060
3204
|
const standalone = taskCapabilitiesContract();
|
|
@@ -3062,6 +3206,7 @@ function taskCapabilitiesCheckReport(taskDb, db, args = [], options = {}) {
|
|
|
3062
3206
|
owner,
|
|
3063
3207
|
reviewer,
|
|
3064
3208
|
all,
|
|
3209
|
+
everywhere,
|
|
3065
3210
|
limit,
|
|
3066
3211
|
scope,
|
|
3067
3212
|
});
|
|
@@ -3326,7 +3471,11 @@ function selectTaskForCurrent(projection, { owner = DEFAULT_OWNER, scope = {}, h
|
|
|
3326
3471
|
if (reviewNeedsAgent) return { task: reviewNeedsAgent, reason: 'review_needs_agent_verification' };
|
|
3327
3472
|
const reviewProofBoundaryBlocked = columns.review.find(task => reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp })?.next_action === PROOF_BOUNDARY_BLOCKED_ACTION);
|
|
3328
3473
|
if (reviewProofBoundaryBlocked) return { task: reviewProofBoundaryBlocked, reason: 'review_proof_boundary_blocked' };
|
|
3329
|
-
|
|
3474
|
+
// Scoped selection (goal_id, tag, status, or review_state) implies a sequenced
|
|
3475
|
+
// work stream (e.g. golden-path "pass 1a" before "pass 2"): earlier-created
|
|
3476
|
+
// tasks must win over newer ones, not the newest-first default used for the
|
|
3477
|
+
// unscoped desk view.
|
|
3478
|
+
const planQueue = !taskQueueScopeIsEmpty(normalizedScope) ? sortTasksOldestFirst(columns.plan) : columns.plan;
|
|
3330
3479
|
const planReady = planQueue[0];
|
|
3331
3480
|
if (planReady) return { task: planReady, reason: 'plan_ready' };
|
|
3332
3481
|
const backlogIdea = columns.backlog[0];
|
|
@@ -3392,18 +3541,20 @@ function buildTaskCurrent(taskDb, db, args = [], options = {}) {
|
|
|
3392
3541
|
const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
|
|
3393
3542
|
const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
|
|
3394
3543
|
const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
|
|
3544
|
+
const everywhere = taskScopeEverywhere(args, options);
|
|
3545
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
3395
3546
|
const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
|
|
3396
3547
|
const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
|
|
3397
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
|
|
3548
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
|
|
3398
3549
|
const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(
|
|
3399
3550
|
taskDb,
|
|
3400
3551
|
db,
|
|
3401
|
-
|
|
3552
|
+
workspaceRoot,
|
|
3402
3553
|
);
|
|
3403
3554
|
const hasPendingReviewChat = buildPendingReviewChatPredicate(
|
|
3404
3555
|
taskDb,
|
|
3405
3556
|
db,
|
|
3406
|
-
|
|
3557
|
+
workspaceRoot,
|
|
3407
3558
|
);
|
|
3408
3559
|
return {
|
|
3409
3560
|
projection,
|
|
@@ -3676,12 +3827,14 @@ function taskReviewLaneDrainReport(taskDb, db, args = [], options = {}) {
|
|
|
3676
3827
|
const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
|
|
3677
3828
|
const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
|
|
3678
3829
|
const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
|
|
3830
|
+
const everywhere = taskScopeEverywhere(args, options);
|
|
3679
3831
|
const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
|
|
3680
3832
|
const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
|
|
3681
3833
|
const capabilitiesCheck = taskCapabilitiesCheckReport(taskDb, db, [], {
|
|
3682
3834
|
owner,
|
|
3683
3835
|
reviewer,
|
|
3684
3836
|
all,
|
|
3837
|
+
everywhere,
|
|
3685
3838
|
limit,
|
|
3686
3839
|
scope,
|
|
3687
3840
|
});
|
|
@@ -3689,6 +3842,7 @@ function taskReviewLaneDrainReport(taskDb, db, args = [], options = {}) {
|
|
|
3689
3842
|
owner,
|
|
3690
3843
|
reviewer,
|
|
3691
3844
|
all,
|
|
3845
|
+
everywhere,
|
|
3692
3846
|
limit,
|
|
3693
3847
|
scope,
|
|
3694
3848
|
excludeTaskIds: options.excludeTaskIds,
|
|
@@ -3749,6 +3903,7 @@ function taskReviewLaneActOptionsFromArgs(args = []) {
|
|
|
3749
3903
|
owner: flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER,
|
|
3750
3904
|
reviewer: reviewActor(flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review'),
|
|
3751
3905
|
all: hasFlag(args, '--all'),
|
|
3906
|
+
everywhere: hasFlag(args, '--everywhere'),
|
|
3752
3907
|
limit: taskQueueLimit(args),
|
|
3753
3908
|
scope: taskQueueScopeFromArgs(args),
|
|
3754
3909
|
dryRun: hasFlag(args, '--dry-run'),
|
|
@@ -3766,6 +3921,7 @@ function taskReviewLaneActOptionsFromBody(body = {}, searchParams = new URLSearc
|
|
|
3766
3921
|
owner: String(queryOwner || body.owner || body.as || body.actor || DEFAULT_OWNER),
|
|
3767
3922
|
reviewer: reviewActor(queryReviewer || body.reviewer || body.review_actor || body.reviewActor || 'codex-review'),
|
|
3768
3923
|
all: searchParams.get('all') === '1' || searchParams.get('all') === 'true' || Boolean(body.all),
|
|
3924
|
+
everywhere: searchParams.get('everywhere') === '1' || searchParams.get('everywhere') === 'true' || Boolean(body.everywhere),
|
|
3769
3925
|
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
3770
3926
|
scope: mergeTaskQueueScopes(queryScope, bodyScope),
|
|
3771
3927
|
dryRun: searchParams.get('dry_run') === '1'
|
|
@@ -3831,6 +3987,7 @@ function taskReviewLaneAct(taskDb, db, options = {}) {
|
|
|
3831
3987
|
owner,
|
|
3832
3988
|
reviewer,
|
|
3833
3989
|
all: Boolean(options.all),
|
|
3990
|
+
everywhere: Boolean(options.everywhere),
|
|
3834
3991
|
limit: options.limit !== undefined ? options.limit : 8,
|
|
3835
3992
|
scope,
|
|
3836
3993
|
excludeTaskIds: options.excludeTaskIds,
|
|
@@ -4069,6 +4226,7 @@ function taskReviewLaneLoop(taskDb, db, options = {}) {
|
|
|
4069
4226
|
owner,
|
|
4070
4227
|
reviewer,
|
|
4071
4228
|
all: Boolean(options.all),
|
|
4229
|
+
everywhere: Boolean(options.everywhere),
|
|
4072
4230
|
limit: options.limit !== undefined ? options.limit : 8,
|
|
4073
4231
|
scope,
|
|
4074
4232
|
excludeTaskIds,
|
|
@@ -4121,6 +4279,7 @@ function taskReviewLaneLoop(taskDb, db, options = {}) {
|
|
|
4121
4279
|
owner,
|
|
4122
4280
|
reviewer,
|
|
4123
4281
|
all: Boolean(options.all),
|
|
4282
|
+
everywhere: Boolean(options.everywhere),
|
|
4124
4283
|
limit: options.limit !== undefined ? options.limit : 8,
|
|
4125
4284
|
scope,
|
|
4126
4285
|
excludeTaskIds,
|
|
@@ -4308,6 +4467,7 @@ function taskReviewLaneRun(taskDb, db, options = {}) {
|
|
|
4308
4467
|
owner,
|
|
4309
4468
|
reviewer,
|
|
4310
4469
|
all: Boolean(options.all),
|
|
4470
|
+
everywhere: Boolean(options.everywhere),
|
|
4311
4471
|
limit: options.limit !== undefined ? options.limit : 8,
|
|
4312
4472
|
scope,
|
|
4313
4473
|
excludeTaskIds,
|
|
@@ -4488,6 +4648,19 @@ function reviewQueueItem(task, root = process.cwd(), evidence = undefined) {
|
|
|
4488
4648
|
return item;
|
|
4489
4649
|
}
|
|
4490
4650
|
|
|
4651
|
+
function blockedReviewQueueItem(task, root = process.cwd()) {
|
|
4652
|
+
const item = reviewQueueItem(task, root);
|
|
4653
|
+
const blocker = reviewBlockerForTask(task);
|
|
4654
|
+
item.queue_role = 'blocked';
|
|
4655
|
+
item.reason = blocker.reason;
|
|
4656
|
+
item.blocked_reason = blocker.reason;
|
|
4657
|
+
item.next_command = blocker.next_command;
|
|
4658
|
+
item.verify_command = blocker.verify_command;
|
|
4659
|
+
item.accept_command = null;
|
|
4660
|
+
item.land_command = null;
|
|
4661
|
+
return item;
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4491
4664
|
function reviewQueueHygiene(tasks) {
|
|
4492
4665
|
const genericContinuations = (tasks || []).map(task => {
|
|
4493
4666
|
const issues = genericContinuationIssues(task);
|
|
@@ -4526,7 +4699,10 @@ function taskReviewQueue(projection, args = []) {
|
|
|
4526
4699
|
const ordered = [...certified].sort((a, b) =>
|
|
4527
4700
|
evidenceRiskRank(evidenceByTaskId.get(b.id)) - evidenceRiskRank(evidenceByTaskId.get(a.id))
|
|
4528
4701
|
|| Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
|
4529
|
-
const
|
|
4702
|
+
const certifiedItems = ordered.slice(0, limit).map((task) => reviewQueueItem(task, root, evidenceByTaskId.get(task.id)));
|
|
4703
|
+
const blockedLimit = reviewQueueLimit(args, blocking.length);
|
|
4704
|
+
const blockedItems = blocking.slice(0, blockedLimit).map((task) => blockedReviewQueueItem(task, root));
|
|
4705
|
+
const items = [...certifiedItems, ...blockedItems];
|
|
4530
4706
|
return {
|
|
4531
4707
|
schema: 'atris.task_review_queue.v1',
|
|
4532
4708
|
generated_at: projection.generated_at,
|
|
@@ -4537,7 +4713,8 @@ function taskReviewQueue(projection, args = []) {
|
|
|
4537
4713
|
evidence_passing: certified.filter((task) => evidenceByTaskId.get(task.id)?.all_passing).length,
|
|
4538
4714
|
blocking: blocking.length,
|
|
4539
4715
|
proof_boundary_blocked: proofBoundaryBlocked.length,
|
|
4540
|
-
shown:
|
|
4716
|
+
shown: certifiedItems.length,
|
|
4717
|
+
blocking_shown: blockedItems.length,
|
|
4541
4718
|
},
|
|
4542
4719
|
hygiene: reviewQueueHygiene(reviewTasks),
|
|
4543
4720
|
items,
|
|
@@ -4635,11 +4812,13 @@ function cmdReviews(args) {
|
|
|
4635
4812
|
}
|
|
4636
4813
|
console.log('READY FOR APPROVAL');
|
|
4637
4814
|
console.log(`${queue.counts.certified} ready for approval / ${queue.counts.blocking} need one more check / ${queue.counts.review} total waiting`);
|
|
4638
|
-
|
|
4815
|
+
const approvalItems = queue.items.filter(item => item.queue_role !== 'blocked');
|
|
4816
|
+
const blockedItems = queue.items.filter(item => item.queue_role === 'blocked');
|
|
4817
|
+
if (!approvalItems.length && !blockedItems.length) {
|
|
4639
4818
|
console.log('Nothing is ready for approval.');
|
|
4640
4819
|
return;
|
|
4641
4820
|
}
|
|
4642
|
-
|
|
4821
|
+
approvalItems.forEach((item, index) => {
|
|
4643
4822
|
const tag = item.tag ? ` [${item.tag}]` : '';
|
|
4644
4823
|
const passes = item.review_pass_count ? ` (${item.review_pass_count} reviews)` : '';
|
|
4645
4824
|
const badge = item.evidence?.all_passing ? ' [evidence:passing]' : '';
|
|
@@ -4669,6 +4848,10 @@ function cmdReviews(args) {
|
|
|
4669
4848
|
else if (item.blocked_accept_reason) console.log(` approve: blocked (${item.blocked_accept_reason})`);
|
|
4670
4849
|
console.log(` rework: ${item.revise_command}`);
|
|
4671
4850
|
});
|
|
4851
|
+
for (const item of blockedItems) {
|
|
4852
|
+
console.log('');
|
|
4853
|
+
console.log(`blocked: ${item.display_id || taskRef(item.id)}: ${item.reason}; next: ${item.next_command}`);
|
|
4854
|
+
}
|
|
4672
4855
|
if (queue.counts.shown < queue.counts.certified) {
|
|
4673
4856
|
console.log('');
|
|
4674
4857
|
console.log(`Showing ${queue.counts.shown}/${queue.counts.certified}; rerun with --all for every row or --verbose for proof details.`);
|
|
@@ -4867,19 +5050,21 @@ function formatTaskLine(task) {
|
|
|
4867
5050
|
|
|
4868
5051
|
function cmdStatus(args) {
|
|
4869
5052
|
const all = hasFlag(args, '--all');
|
|
5053
|
+
const everywhere = taskScopeEverywhere(args);
|
|
4870
5054
|
const history = hasFlag(args, '--history');
|
|
4871
5055
|
const taskDb = getTaskDb();
|
|
4872
5056
|
const db = taskDb.open();
|
|
4873
|
-
const
|
|
5057
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
5058
|
+
const compact = writeDefaultProjection(taskDb, db, { all, everywhere });
|
|
4874
5059
|
const projection = history
|
|
4875
5060
|
? enrichTaskProjection(taskDb.taskProjection(db, {
|
|
4876
|
-
workspaceRoot
|
|
4877
|
-
limit: 500,
|
|
5061
|
+
workspaceRoot,
|
|
5062
|
+
limit: all ? null : 500,
|
|
4878
5063
|
includeHistory: true,
|
|
4879
5064
|
}))
|
|
4880
5065
|
: compact.projection;
|
|
4881
5066
|
const outPath = compact.outPath;
|
|
4882
|
-
const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(taskDb, db,
|
|
5067
|
+
const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(taskDb, db, workspaceRoot);
|
|
4883
5068
|
const status = taskStatusSummary(projection, { history, hasExistingReviewFollowUp });
|
|
4884
5069
|
if (wantsJson(args)) {
|
|
4885
5070
|
printJson({
|
|
@@ -4936,13 +5121,13 @@ function requireTaskId(taskDb, db, ref, label) {
|
|
|
4936
5121
|
}
|
|
4937
5122
|
}
|
|
4938
5123
|
|
|
4939
|
-
function workspaceRefRows(taskDb, db,
|
|
4940
|
-
return taskDb.listTasks(db, { workspaceRoot:
|
|
5124
|
+
function workspaceRefRows(taskDb, db, options = {}) {
|
|
5125
|
+
return taskDb.listTasks(db, { workspaceRoot: scopedWorkspaceRoot(taskDb, [], options) });
|
|
4941
5126
|
}
|
|
4942
5127
|
|
|
4943
5128
|
function renderTaskDesk(rows, refRows = rows) {
|
|
4944
5129
|
const displayRows = getTaskDb().withTaskDisplayRefs(rows, refRows);
|
|
4945
|
-
const active = displayRows.filter(r => r.status !== 'done');
|
|
5130
|
+
const active = displayRows.filter(r => r.status !== 'done' && r.status !== 'archived');
|
|
4946
5131
|
const done = displayRows.filter(r => r.status === 'done');
|
|
4947
5132
|
if (rows.length === 0) {
|
|
4948
5133
|
console.log('No tasks yet.');
|
|
@@ -5158,9 +5343,10 @@ function taskDayGroups(tasks, { now = Date.now() } = {}) {
|
|
|
5158
5343
|
|
|
5159
5344
|
function cmdDay(args) {
|
|
5160
5345
|
const all = hasFlag(args, '--all');
|
|
5346
|
+
const everywhere = taskScopeEverywhere(args);
|
|
5161
5347
|
const taskDb = getTaskDb();
|
|
5162
5348
|
const db = taskDb.open();
|
|
5163
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
|
|
5349
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
|
|
5164
5350
|
const { groups, staleFailed } = taskDayGroups(projection.tasks || []);
|
|
5165
5351
|
const counts = {
|
|
5166
5352
|
active: groups.reduce((sum, group) => sum + group.tasks.length, 0),
|
|
@@ -5212,13 +5398,15 @@ function cmdDay(args) {
|
|
|
5212
5398
|
|
|
5213
5399
|
function cmdHome(args) {
|
|
5214
5400
|
const all = hasFlag(args, '--all');
|
|
5401
|
+
const everywhere = taskScopeEverywhere(args);
|
|
5215
5402
|
const taskDb = getTaskDb();
|
|
5216
5403
|
const db = taskDb.open();
|
|
5404
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
5217
5405
|
const rows = taskDb.listTasks(db, {
|
|
5218
|
-
workspaceRoot
|
|
5219
|
-
limit: 200,
|
|
5406
|
+
workspaceRoot,
|
|
5407
|
+
limit: all ? null : 200,
|
|
5220
5408
|
});
|
|
5221
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
|
|
5409
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
|
|
5222
5410
|
if (wantsJson(args)) {
|
|
5223
5411
|
printJson({
|
|
5224
5412
|
ok: true,
|
|
@@ -5235,18 +5423,20 @@ function cmdHome(args) {
|
|
|
5235
5423
|
|
|
5236
5424
|
function cmdList(args) {
|
|
5237
5425
|
const all = hasFlag(args, '--all');
|
|
5426
|
+
const everywhere = taskScopeEverywhere(args);
|
|
5238
5427
|
const status = flag(args, '--status');
|
|
5239
5428
|
const scope = taskQueueScopeFromArgs(args);
|
|
5240
5429
|
const scoped = !taskQueueScopeIsEmpty(scope);
|
|
5241
5430
|
const taskDb = getTaskDb();
|
|
5242
5431
|
const db = taskDb.open();
|
|
5432
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
5243
5433
|
const rawRows = taskDb.listTasks(db, {
|
|
5244
|
-
workspaceRoot
|
|
5434
|
+
workspaceRoot,
|
|
5245
5435
|
status: typeof status === 'string' ? status : null,
|
|
5246
|
-
limit: scoped ? null : 200,
|
|
5436
|
+
limit: scoped || all ? null : 200,
|
|
5247
5437
|
});
|
|
5248
5438
|
const rows = filterTasksByScope(rawRows, scope);
|
|
5249
|
-
const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db,
|
|
5439
|
+
const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, { everywhere }));
|
|
5250
5440
|
if (wantsJson(args)) {
|
|
5251
5441
|
printJson({ ok: true, action: 'list', scope: normalizeTaskQueueScope(scope), tasks: displayRows });
|
|
5252
5442
|
return;
|
|
@@ -5263,12 +5453,49 @@ function cmdList(args) {
|
|
|
5263
5453
|
}
|
|
5264
5454
|
}
|
|
5265
5455
|
|
|
5456
|
+
// judge != worker support: reserved system names (autoland-verifier and co)
|
|
5457
|
+
// can never be assumed via --as, and unknown names warn by default or fail
|
|
5458
|
+
// under ATRIS_ACTOR_VALIDATION=enforce. Only explicit --as values are
|
|
5459
|
+
// checked; the DEFAULT_OWNER fallback stays silent.
|
|
5460
|
+
function guardExplicitActor(command, value) {
|
|
5461
|
+
if (typeof value !== 'string' || !value.trim()) return;
|
|
5462
|
+
const check = reviewIntegrity.validateActor(value, { root: process.cwd() });
|
|
5463
|
+
if (!check.ok) {
|
|
5464
|
+
if (check.reason === 'reserved_actor') {
|
|
5465
|
+
console.error(`${command}: reserved_actor: '${value}' is a system actor and cannot be used with --as`);
|
|
5466
|
+
} else {
|
|
5467
|
+
console.error(`${command}: actor_not_on_roster: '${value}' is not a workspace member or engine (actor validation is enforced)`);
|
|
5468
|
+
}
|
|
5469
|
+
process.exit(1);
|
|
5470
|
+
}
|
|
5471
|
+
if (check.reason === 'actor_not_on_roster' && check.mode === 'warn') {
|
|
5472
|
+
console.error(`Warning: '${value}' is not a workspace member or engine; reviews under unknown names weaken the audit trail.`);
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
|
|
5476
|
+
// A claim against an already-done task burns a whole dispatch when a
|
|
5477
|
+
// rendered view (atris/TODO.md) is stale: the agent claims, builds, then
|
|
5478
|
+
// discovers the work was already done. Point straight at a real open task
|
|
5479
|
+
// instead of just reporting the failure, straight from the live projection
|
|
5480
|
+
// (never the rendered file), preferring the same tag when one is open.
|
|
5481
|
+
function suggestNextClaimableTask(projection, { excludeId = null, tag = '' } = {}) {
|
|
5482
|
+
const open = (projection && projection.tasks || []).filter((t) => t && t.status === 'open' && t.id !== excludeId);
|
|
5483
|
+
if (!open.length) return null;
|
|
5484
|
+
const normalizedTag = String(tag || '').trim().toLowerCase();
|
|
5485
|
+
if (normalizedTag) {
|
|
5486
|
+
const sameTag = open.find((t) => String(t.tag || '').trim().toLowerCase() === normalizedTag);
|
|
5487
|
+
if (sameTag) return sameTag;
|
|
5488
|
+
}
|
|
5489
|
+
return open[0];
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5266
5492
|
function cmdClaim(args) {
|
|
5267
5493
|
const pos = positional(args);
|
|
5268
5494
|
const id = pos[0];
|
|
5269
5495
|
if (!id) {
|
|
5270
5496
|
failTask('atris task claim', 'missing_id', 'id required');
|
|
5271
5497
|
}
|
|
5498
|
+
guardExplicitActor('atris task claim', flag(args, '--as'));
|
|
5272
5499
|
const owner = flag(args, '--as') || DEFAULT_OWNER;
|
|
5273
5500
|
const taskDb = getTaskDb();
|
|
5274
5501
|
const db = taskDb.open();
|
|
@@ -5292,6 +5519,12 @@ function cmdClaim(args) {
|
|
|
5292
5519
|
const recoveryCommand = result.reason === 'already_claimed' && result.claimed_by
|
|
5293
5520
|
? `atris task release ${id} --as ${result.claimed_by}`
|
|
5294
5521
|
: null;
|
|
5522
|
+
let nextClaimable = null;
|
|
5523
|
+
if (result.reason === 'already_done') {
|
|
5524
|
+
const doneRow = taskDb.getTask(db, taskId);
|
|
5525
|
+
const { projection } = writeDefaultProjection(taskDb, db);
|
|
5526
|
+
nextClaimable = suggestNextClaimableTask(projection, { excludeId: taskId, tag: doneRow && doneRow.tag });
|
|
5527
|
+
}
|
|
5295
5528
|
if (wantsJson(args)) {
|
|
5296
5529
|
printJson({
|
|
5297
5530
|
ok: false,
|
|
@@ -5299,12 +5532,16 @@ function cmdClaim(args) {
|
|
|
5299
5532
|
reason: result.reason,
|
|
5300
5533
|
claimed_by: result.claimed_by || null,
|
|
5301
5534
|
recovery_command: recoveryCommand,
|
|
5535
|
+
next_claimable: nextClaimable ? { id: nextClaimable.id, ref: taskRef(nextClaimable), tag: nextClaimable.tag || null, title: nextClaimable.title } : null,
|
|
5302
5536
|
detail: `claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
|
|
5303
5537
|
});
|
|
5304
5538
|
process.exit(1);
|
|
5305
5539
|
}
|
|
5306
5540
|
console.error(`claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
|
|
5307
5541
|
if (recoveryCommand) console.error(`Recovery: ${recoveryCommand}`);
|
|
5542
|
+
if (nextClaimable) {
|
|
5543
|
+
console.error(`next claimable: ${taskRef(nextClaimable)} ${String(nextClaimable.title || '').slice(0, 80)} (atris task claim ${taskRef(nextClaimable)} --as ${owner})`);
|
|
5544
|
+
}
|
|
5308
5545
|
process.exit(1);
|
|
5309
5546
|
}
|
|
5310
5547
|
}
|
|
@@ -5724,6 +5961,79 @@ function cmdNote(args) {
|
|
|
5724
5961
|
console.log(`noted ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
|
|
5725
5962
|
}
|
|
5726
5963
|
|
|
5964
|
+
// Collect EVERY value for a repeatable flag (flag() only returns the first),
|
|
5965
|
+
// so `--add a --add b` and `--add a,b` both work.
|
|
5966
|
+
function collectFlagValues(args, name) {
|
|
5967
|
+
const values = [];
|
|
5968
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
5969
|
+
if (args[i] === name && i + 1 < args.length && !String(args[i + 1]).startsWith('--')) {
|
|
5970
|
+
values.push(args[i + 1]);
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5973
|
+
return values
|
|
5974
|
+
.flatMap((value) => String(value).split(','))
|
|
5975
|
+
.map((value) => value.trim())
|
|
5976
|
+
.filter(Boolean);
|
|
5977
|
+
}
|
|
5978
|
+
|
|
5979
|
+
function cmdTag(args) {
|
|
5980
|
+
const pos = positional(args);
|
|
5981
|
+
const id = pos[0];
|
|
5982
|
+
if (!id) failTask('atris task tag', 'missing_id', 'task id required');
|
|
5983
|
+
const add = collectFlagValues(args, '--add');
|
|
5984
|
+
const remove = collectFlagValues(args, '--remove');
|
|
5985
|
+
if (!add.length && !remove.length) {
|
|
5986
|
+
failTask('atris task tag', 'missing_tags', 'at least one --add <tag> or --remove <tag> required');
|
|
5987
|
+
}
|
|
5988
|
+
const actor = flag(args, '--as') || DEFAULT_OWNER;
|
|
5989
|
+
const taskDb = getTaskDb();
|
|
5990
|
+
const db = taskDb.open();
|
|
5991
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task tag');
|
|
5992
|
+
const result = taskDb.tagTask(db, { id: taskId, actor: String(actor), add, remove });
|
|
5993
|
+
if (!result.tagged) {
|
|
5994
|
+
if (result.reason === 'no_changes') {
|
|
5995
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
5996
|
+
const task = compactTaskFromProjection(projection, taskId);
|
|
5997
|
+
if (wantsJson(args)) {
|
|
5998
|
+
printJson({
|
|
5999
|
+
ok: true,
|
|
6000
|
+
action: 'unchanged',
|
|
6001
|
+
task_id: taskId,
|
|
6002
|
+
added: [],
|
|
6003
|
+
removed: [],
|
|
6004
|
+
tags: result.tags || [],
|
|
6005
|
+
projection_path: outPath,
|
|
6006
|
+
task,
|
|
6007
|
+
});
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
6010
|
+
console.log(`no change ${taskRef(task)} tags [${(result.tags || []).join(', ')}]`);
|
|
6011
|
+
return;
|
|
6012
|
+
}
|
|
6013
|
+
failTask('atris task tag', result.reason || 'tag_failed', `tag failed: ${result.reason || 'unknown'}`, 1);
|
|
6014
|
+
}
|
|
6015
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
6016
|
+
const task = compactTaskFromProjection(projection, taskId);
|
|
6017
|
+
if (wantsJson(args)) {
|
|
6018
|
+
printJson({
|
|
6019
|
+
ok: true,
|
|
6020
|
+
action: 'tagged',
|
|
6021
|
+
task_id: taskId,
|
|
6022
|
+
added: result.added,
|
|
6023
|
+
removed: result.removed,
|
|
6024
|
+
tags: result.tags,
|
|
6025
|
+
version: result.event.version,
|
|
6026
|
+
projection_path: outPath,
|
|
6027
|
+
task,
|
|
6028
|
+
});
|
|
6029
|
+
return;
|
|
6030
|
+
}
|
|
6031
|
+
const parts = [];
|
|
6032
|
+
if (result.added.length) parts.push(`+${result.added.join(' +')}`);
|
|
6033
|
+
if (result.removed.length) parts.push(`-${result.removed.join(' -')}`);
|
|
6034
|
+
console.log(`tagged ${taskRef(task)} ${parts.join(' ')} -> [${result.tags.join(', ')}] v${result.event.version}`);
|
|
6035
|
+
}
|
|
6036
|
+
|
|
5727
6037
|
function cmdChat(args) {
|
|
5728
6038
|
const pos = positional(args);
|
|
5729
6039
|
const id = pos[0];
|
|
@@ -7839,6 +8149,117 @@ function cmdFinish(args) {
|
|
|
7839
8149
|
console.log(`finished ${taskRef(compactTaskFromProjection(projection, taskId))}`);
|
|
7840
8150
|
}
|
|
7841
8151
|
|
|
8152
|
+
// Distinct from `done --failed`: a bulk sweep of duplicates/off-roadmap work
|
|
8153
|
+
// closes the task without ever claiming it did or didn't succeed. Writing
|
|
8154
|
+
// 'failed' here would corrupt the reward signal readers rely on (see
|
|
8155
|
+
// atris/reports/failed-tasks-analysis-2026-07-03.md, cluster 2, OBL-1622).
|
|
8156
|
+
function cmdArchive(args) {
|
|
8157
|
+
const pos = positional(args);
|
|
8158
|
+
const id = pos[0];
|
|
8159
|
+
if (!id) {
|
|
8160
|
+
failTask('atris task archive', 'missing_id', 'id required');
|
|
8161
|
+
}
|
|
8162
|
+
const reason = textFlag(args, ['--reason']);
|
|
8163
|
+
if (!reason) {
|
|
8164
|
+
failTask('atris task archive', 'missing_reason', 'atris task archive requires --reason "<why this is being swept, not failed>"');
|
|
8165
|
+
}
|
|
8166
|
+
const taskDb = getTaskDb();
|
|
8167
|
+
const db = taskDb.open();
|
|
8168
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task archive');
|
|
8169
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
8170
|
+
// Explicit opt-in for sanctioned failed→archived cleanup (e.g. duplicate
|
|
8171
|
+
// loop-tick orphans fail-closed before 'archived' existed). Without the
|
|
8172
|
+
// flag, failed rows stay failed; done rows are never archivable.
|
|
8173
|
+
const fromFailed = hasFlag(args, '--from-failed');
|
|
8174
|
+
const result = taskDb.archiveTask(db, { id: taskId, actor, reason, fromFailed });
|
|
8175
|
+
if (result.archived) {
|
|
8176
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
8177
|
+
if (wantsJson(args)) {
|
|
8178
|
+
printJson({
|
|
8179
|
+
ok: true,
|
|
8180
|
+
action: 'archived',
|
|
8181
|
+
task_id: taskId,
|
|
8182
|
+
reason,
|
|
8183
|
+
archived_from: result.row && result.row.metadata && result.row.metadata.archived_from || null,
|
|
8184
|
+
projection_path: outPath,
|
|
8185
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
8186
|
+
});
|
|
8187
|
+
return;
|
|
8188
|
+
}
|
|
8189
|
+
const fromNote = result.row && result.row.metadata && result.row.metadata.archived_from
|
|
8190
|
+
? ` (was ${result.row.metadata.archived_from})`
|
|
8191
|
+
: '';
|
|
8192
|
+
console.log(`archived ${taskRef(compactTaskFromProjection(projection, taskId))}${fromNote}: ${reason}`);
|
|
8193
|
+
} else {
|
|
8194
|
+
const hint = result.reason === 'already_failed'
|
|
8195
|
+
? ' (use --from-failed to archive a fail-closed duplicate/off-roadmap row)'
|
|
8196
|
+
: '';
|
|
8197
|
+
const detail = `archive failed: ${taskId} ${result.reason}${hint}`;
|
|
8198
|
+
if (wantsJson(args)) {
|
|
8199
|
+
printJson({ ok: false, command: 'atris task archive', reason: result.reason, task_id: taskId, detail });
|
|
8200
|
+
process.exit(1);
|
|
8201
|
+
}
|
|
8202
|
+
console.error(detail);
|
|
8203
|
+
process.exit(1);
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8206
|
+
|
|
8207
|
+
// One-time migration for OBL-1622: the 2026-06-10 "first-principles backlog
|
|
8208
|
+
// reset" archived ~125 certified, proof-backed tasks by writing status
|
|
8209
|
+
// 'failed' (no distinct archived status existed yet). This relabels exactly
|
|
8210
|
+
// the rows that carry that reset's metadata marker, using the same
|
|
8211
|
+
// UPDATE+appendTaskEvent write path as every other status transition in
|
|
8212
|
+
// lib/task-db.js — never a raw projection-JSON edit.
|
|
8213
|
+
function cmdRelabelArchived(args) {
|
|
8214
|
+
const apply = hasFlag(args, '--apply');
|
|
8215
|
+
const taskDb = getTaskDb();
|
|
8216
|
+
const db = taskDb.open();
|
|
8217
|
+
const workspaceRoot = taskDb.workspaceRoot();
|
|
8218
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
8219
|
+
const result = taskDb.relabelArchivedTasks(db, { workspaceRoot, apply, actor });
|
|
8220
|
+
if (apply && result.count > 0) {
|
|
8221
|
+
appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count: result.count, ids: result.ids });
|
|
8222
|
+
}
|
|
8223
|
+
if (wantsJson(args)) {
|
|
8224
|
+
printJson({
|
|
8225
|
+
ok: true,
|
|
8226
|
+
action: apply ? 'relabeled' : 'preview',
|
|
8227
|
+
dry_run: !apply,
|
|
8228
|
+
workspace_root: workspaceRoot,
|
|
8229
|
+
...result,
|
|
8230
|
+
});
|
|
8231
|
+
return;
|
|
8232
|
+
}
|
|
8233
|
+
if (!apply) {
|
|
8234
|
+
console.log(`relabel-archived (dry-run): ${result.count} failed task(s) match the June 10 backlog-reset marker.`);
|
|
8235
|
+
for (const s of result.sample) console.log(` - ${s.id} ${s.title}`);
|
|
8236
|
+
if (result.count > result.sample.length) console.log(` ...and ${result.count - result.sample.length} more`);
|
|
8237
|
+
console.log('Run with --apply --as <you> to relabel these failed -> archived.');
|
|
8238
|
+
return;
|
|
8239
|
+
}
|
|
8240
|
+
console.log(`relabeled ${result.count} task(s) failed -> archived (June 10 backlog reset, OBL-1622).`);
|
|
8241
|
+
}
|
|
8242
|
+
|
|
8243
|
+
function appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count, ids }) {
|
|
8244
|
+
if (!workspaceRoot || !fs.existsSync(path.join(workspaceRoot, 'atris'))) return null;
|
|
8245
|
+
const now = new Date();
|
|
8246
|
+
const logName = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
|
|
8247
|
+
const stamp = now.toTimeString().slice(0, 5);
|
|
8248
|
+
const projectDir = path.join(workspaceRoot, 'atris', 'logs', logName.slice(0, 4));
|
|
8249
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
8250
|
+
const logPath = path.join(projectDir, logName);
|
|
8251
|
+
const idPreview = ids.slice(0, 10).join(', ') + (ids.length > 10 ? `, ...(+${ids.length - 10} more)` : '');
|
|
8252
|
+
fs.appendFileSync(logPath, [
|
|
8253
|
+
`## ${stamp} · Task relabel: failed -> archived (OBL-1622)`,
|
|
8254
|
+
`- count: ${count}`,
|
|
8255
|
+
`- reason: June 10 backlog-reset rows mislabeled failed; relabeled to archived`,
|
|
8256
|
+
`- actor: ${actor}`,
|
|
8257
|
+
`- ids: ${idPreview}`,
|
|
8258
|
+
'',
|
|
8259
|
+
].join('\n'), 'utf8');
|
|
8260
|
+
return logPath;
|
|
8261
|
+
}
|
|
8262
|
+
|
|
7842
8263
|
function cmdReady(args) {
|
|
7843
8264
|
const pos = positional(args);
|
|
7844
8265
|
const id = pos[0];
|
|
@@ -7851,18 +8272,26 @@ function cmdReady(args) {
|
|
|
7851
8272
|
// turning a claim into executed evidence. --verify can carry an optional --proof note.
|
|
7852
8273
|
const proofFlag = flag(args, '--proof');
|
|
7853
8274
|
const verifyFlag = flag(args, '--verify');
|
|
8275
|
+
const usedVerify = typeof verifyFlag === 'string' ? verifyFlag.trim() : '';
|
|
7854
8276
|
let proof = typeof proofFlag === 'string' ? proofFlag : '';
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
8277
|
+
const verifyAutoCertifyAllowed = !usedVerify || isAutoCertifyVerifyCommandAllowed(verifyFlag);
|
|
8278
|
+
if (usedVerify) {
|
|
8279
|
+
// Run the verifier once and write a receipt (pass or fail) so the review
|
|
8280
|
+
// gate in lib/receipt-evidence.js can validate the exact path named in
|
|
8281
|
+
// the proof, not just trust the prose.
|
|
8282
|
+
const { writeTaskReceipt } = require('../lib/task-receipt');
|
|
8283
|
+
const receipt = writeTaskReceipt({ taskId: id, command: verifyFlag, root: process.cwd() });
|
|
8284
|
+
if (!receipt.passed) {
|
|
8285
|
+
const detail = receipt.exit != null ? ` (exit ${receipt.exit})` : (receipt.signal ? ` (signal ${receipt.signal})` : '');
|
|
7859
8286
|
console.error(`atris task ready: verifier failed${detail}: ${verifyFlag}`);
|
|
7860
|
-
if (
|
|
7861
|
-
else if (
|
|
8287
|
+
if (receipt.output) console.error(receipt.output);
|
|
8288
|
+
else if (receipt.error) console.error(receipt.error);
|
|
8289
|
+
if (receipt.receiptPath) console.error(`receipt: ${receipt.receiptPath}`);
|
|
7862
8290
|
process.exit(1);
|
|
7863
8291
|
}
|
|
7864
|
-
|
|
7865
|
-
|
|
8292
|
+
const base = proof.trim();
|
|
8293
|
+
proof = `[verified] \`${verifyFlag}\` passed (exit 0)${base ? ` — ${base}` : ''}${receipt.output ? `\n${receipt.output}` : ''}\nReceipt: ${receipt.receiptPath}`;
|
|
8294
|
+
if (!wantsJson(args)) console.log(`✓ verified: \`${verifyFlag}\` exited 0 (receipt ${receipt.receiptPath})`);
|
|
7866
8295
|
}
|
|
7867
8296
|
if (!proof) {
|
|
7868
8297
|
console.error('atris task ready: --proof or --verify required');
|
|
@@ -7872,6 +8301,7 @@ function cmdReady(args) {
|
|
|
7872
8301
|
const lesson = flag(args, '--lesson') || '';
|
|
7873
8302
|
const nextTaskInput = normalizeReviewNextTaskInput(typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '');
|
|
7874
8303
|
const landing = landingFlags(args);
|
|
8304
|
+
guardExplicitActor('atris task ready', flag(args, '--as'));
|
|
7875
8305
|
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
7876
8306
|
const resultFields = {
|
|
7877
8307
|
changed: textFlag(args, ['--changed', '--result', '--done']),
|
|
@@ -7906,6 +8336,11 @@ function cmdReady(args) {
|
|
|
7906
8336
|
console.error(`ready failed: ${result.reason}`);
|
|
7907
8337
|
process.exit(1);
|
|
7908
8338
|
}
|
|
8339
|
+
// Store the exact verifier command on the task itself (not just baked into
|
|
8340
|
+
// proof prose) so sweep --auto-accept and certify-verified can re-run it
|
|
8341
|
+
// live against the current checkout later, instead of re-deriving it from
|
|
8342
|
+
// text or trusting a receipt file that may no longer exist.
|
|
8343
|
+
if (usedVerify) stampReadyVerifyMetadata(taskDb, db, taskId, usedVerify);
|
|
7909
8344
|
const landingAdvisory = warnIfLandingNeedsDayOnePm(landing, result.row && result.row.title);
|
|
7910
8345
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
7911
8346
|
const agentCertified = result.event.payload.agent_certified === true;
|
|
@@ -7918,13 +8353,18 @@ function cmdReady(args) {
|
|
|
7918
8353
|
nextTask: nextTaskInput.nextTask,
|
|
7919
8354
|
});
|
|
7920
8355
|
const reviewChat = taskReviewChatHandoff(verifierTask, { reviewer: 'codex-review' });
|
|
8356
|
+
const autolandOn = require('../lib/autoland').liveAcceptAuthorization(taskDb.workspaceRoot()).ok;
|
|
7921
8357
|
const handoff = {
|
|
7922
8358
|
native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
|
|
7923
8359
|
career_xp_status: 'pending_human_accept',
|
|
7924
8360
|
next_action: agentCertified ? certifiedReviewNextAction(nextTaskInput.nextTask) : 'agent_review_again',
|
|
7925
|
-
rule:
|
|
7926
|
-
?
|
|
7927
|
-
|
|
8361
|
+
rule: autolandOn
|
|
8362
|
+
? (agentCertified
|
|
8363
|
+
? 'double-check complete; autoland will accept this on the next tick.'
|
|
8364
|
+
: 'proof is ready; autoland runs the second check and lands it on the next tick.')
|
|
8365
|
+
: (agentCertified
|
|
8366
|
+
? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
|
|
8367
|
+
: 'proof is ready; one more agent check before human approval. XP waits for the human.'),
|
|
7928
8368
|
};
|
|
7929
8369
|
if (reviewChat) {
|
|
7930
8370
|
handoff.review_chat_command = reviewChat.command;
|
|
@@ -7958,11 +8398,59 @@ function cmdReady(args) {
|
|
|
7958
8398
|
console.log(`ready for approval ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
|
|
7959
8399
|
if (resultTrace) console.log('Result trace recorded.');
|
|
7960
8400
|
console.log(handoff.rule);
|
|
8401
|
+
if (!verifyAutoCertifyAllowed) {
|
|
8402
|
+
const ref = taskRef(compactTaskFromProjection(projection, taskId) || result.row || taskId);
|
|
8403
|
+
console.log(`note: this verify command is outside the auto-certify allowlist, so autoland cannot run the second check itself. use a test command like node --test <file> or git diff --check, or have a second agent run: atris task review-chat ${ref} --as <reviewer>`);
|
|
8404
|
+
}
|
|
7961
8405
|
for (const hint of policyHints) {
|
|
7962
8406
|
console.log(`policy (${hint.id}): ${hint.hint}`);
|
|
7963
8407
|
}
|
|
7964
8408
|
}
|
|
7965
8409
|
|
|
8410
|
+
// Standalone receipt writer: runs a verifier for a task and writes atris/runs/
|
|
8411
|
+
// evidence without moving the task to ready. Useful when you want a receipt
|
|
8412
|
+
// on record before or independent of a ready call, or to record a failed
|
|
8413
|
+
// verifier run for the audit trail. `atris task ready --verify` calls the
|
|
8414
|
+
// same writer inline and folds the resulting path into the proof.
|
|
8415
|
+
function cmdTaskReceipt(args) {
|
|
8416
|
+
const pos = positional(args);
|
|
8417
|
+
const id = pos[0];
|
|
8418
|
+
if (!id) {
|
|
8419
|
+
console.error('atris task receipt: id required');
|
|
8420
|
+
process.exit(2);
|
|
8421
|
+
}
|
|
8422
|
+
const verifyFlag = flag(args, '--verify');
|
|
8423
|
+
if (typeof verifyFlag !== 'string' || !verifyFlag.trim()) {
|
|
8424
|
+
console.error('atris task receipt: --verify "<cmd>" required');
|
|
8425
|
+
process.exit(2);
|
|
8426
|
+
}
|
|
8427
|
+
const taskDb = getTaskDb();
|
|
8428
|
+
const db = taskDb.open();
|
|
8429
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task receipt');
|
|
8430
|
+
const { writeTaskReceipt } = require('../lib/task-receipt');
|
|
8431
|
+
const receipt = writeTaskReceipt({ taskId, command: verifyFlag, root: process.cwd() });
|
|
8432
|
+
if (wantsJson(args)) {
|
|
8433
|
+
printJson({
|
|
8434
|
+
ok: receipt.passed,
|
|
8435
|
+
task_id: taskId,
|
|
8436
|
+
command: verifyFlag,
|
|
8437
|
+
receipt_path: receipt.receiptPath,
|
|
8438
|
+
exit: receipt.exit,
|
|
8439
|
+
passed: receipt.passed,
|
|
8440
|
+
});
|
|
8441
|
+
if (!receipt.passed) process.exit(1);
|
|
8442
|
+
return;
|
|
8443
|
+
}
|
|
8444
|
+
if (receipt.passed) {
|
|
8445
|
+
console.log(`receipt written: ${receipt.receiptPath} (exit 0)`);
|
|
8446
|
+
console.log(`use: atris task ready ${taskId} --proof "Receipt: ${receipt.receiptPath}"`);
|
|
8447
|
+
} else {
|
|
8448
|
+
console.error(`verifier failed (exit ${receipt.exit}); receipt written: ${receipt.receiptPath}`);
|
|
8449
|
+
if (receipt.output) console.error(receipt.output);
|
|
8450
|
+
process.exit(1);
|
|
8451
|
+
}
|
|
8452
|
+
}
|
|
8453
|
+
|
|
7966
8454
|
async function cmdAccept(args) {
|
|
7967
8455
|
const pos = positional(args);
|
|
7968
8456
|
const id = pos[0];
|
|
@@ -8041,6 +8529,7 @@ async function cmdAccept(args) {
|
|
|
8041
8529
|
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
8042
8530
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
8043
8531
|
const workspaceRoot = projection.workspace_root || process.cwd();
|
|
8532
|
+
refreshExistingTodoMarkdown(taskDb, db, workspaceRoot);
|
|
8044
8533
|
const brainScorecards = refreshBrainScorecardsAfterAccept(workspaceRoot);
|
|
8045
8534
|
const nextMissionRoute = nextMissionRouteAfterAccept(workspaceRoot);
|
|
8046
8535
|
// Inform the gate, never block it: show what the receipts named in the proof
|
|
@@ -8093,7 +8582,7 @@ function stampAutoAcceptMetadata(taskDb, db, taskId, actor, policy) {
|
|
|
8093
8582
|
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
8094
8583
|
}
|
|
8095
8584
|
|
|
8096
|
-
function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '' }) {
|
|
8585
|
+
function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '', autoAccepted = false }) {
|
|
8097
8586
|
const done = taskDb.doneTask(db, {
|
|
8098
8587
|
id: taskId,
|
|
8099
8588
|
status: 'done',
|
|
@@ -8101,6 +8590,7 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
8101
8590
|
allowReview: true,
|
|
8102
8591
|
action: 'accepted',
|
|
8103
8592
|
proof,
|
|
8593
|
+
autoAccepted,
|
|
8104
8594
|
});
|
|
8105
8595
|
if (!done.updated) {
|
|
8106
8596
|
return { ok: false, reason: 'not_open_claimed_or_review' };
|
|
@@ -8113,7 +8603,10 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
8113
8603
|
nextTask,
|
|
8114
8604
|
proof,
|
|
8115
8605
|
careerXpEligible: true,
|
|
8606
|
+
autoAccepted,
|
|
8116
8607
|
});
|
|
8608
|
+
const acceptedRow = taskDb.getTask(db, taskId);
|
|
8609
|
+
refreshExistingTodoMarkdown(taskDb, db, acceptedRow && acceptedRow.workspace_root);
|
|
8117
8610
|
return { ok: true, reviewed };
|
|
8118
8611
|
}
|
|
8119
8612
|
|
|
@@ -8145,6 +8638,25 @@ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
|
|
|
8145
8638
|
metadata.verify = metadata.verify || verify;
|
|
8146
8639
|
metadata.certified_verified_at = new Date().toISOString();
|
|
8147
8640
|
metadata.certified_verified_by = actor;
|
|
8641
|
+
metadata.machine_verified = true;
|
|
8642
|
+
db.prepare(`
|
|
8643
|
+
UPDATE tasks
|
|
8644
|
+
SET metadata = ?,
|
|
8645
|
+
updated_at = ?
|
|
8646
|
+
WHERE id = ?
|
|
8647
|
+
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
8648
|
+
}
|
|
8649
|
+
|
|
8650
|
+
// `atris task ready --verify "<cmd>"` already runs the command live and
|
|
8651
|
+
// gates on exit 0, but that alone leaves no machine re-runnable trace once
|
|
8652
|
+
// the proof text scrolls out of easy reach: storing the exact command on
|
|
8653
|
+
// metadata.verify is what lets sweep --auto-accept (and certify-verified)
|
|
8654
|
+
// re-run it later against the current checkout instead of re-parsing prose.
|
|
8655
|
+
function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
|
|
8656
|
+
const row = taskDb.getTask(db, taskId);
|
|
8657
|
+
if (!row) return;
|
|
8658
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
8659
|
+
metadata.verify = verify;
|
|
8148
8660
|
db.prepare(`
|
|
8149
8661
|
UPDATE tasks
|
|
8150
8662
|
SET metadata = ?,
|
|
@@ -8196,16 +8708,28 @@ function cmdCertifyVerified(args) {
|
|
|
8196
8708
|
// something an executed second-actor check cannot cure. A row with two
|
|
8197
8709
|
// passes from ONE actor is exactly what this command exists to cure —
|
|
8198
8710
|
// "certified" alone is not landable.
|
|
8711
|
+
// strictVerify stays off in this eligibility probe: certify-verified runs
|
|
8712
|
+
// the check itself right below, and strict mode here would execute it a
|
|
8713
|
+
// second time per row before the real run.
|
|
8199
8714
|
const evaluation = evaluateAutoAccept(task, { strictVerify: false });
|
|
8200
8715
|
if (evaluation.eligible) {
|
|
8201
8716
|
results.push({ ref, action: 'skipped', reason: 'already_landable' });
|
|
8202
8717
|
continue;
|
|
8203
8718
|
}
|
|
8204
|
-
|
|
8719
|
+
// proof_not_executed is curable by definition: this command re-runs the
|
|
8720
|
+
// named check and replaces the free-text claim with executed evidence.
|
|
8721
|
+
const curable = ['not_agent_certified', 'needs_independent_reviewer', 'needs_second_reviewer_or_third_pass', 'insufficient_review_passes', 'proof_not_executed'];
|
|
8205
8722
|
if (!curable.includes(evaluation.reason)) {
|
|
8206
8723
|
results.push({ ref, action: 'skipped', reason: evaluation.reason });
|
|
8207
8724
|
continue;
|
|
8208
8725
|
}
|
|
8726
|
+
// judge != worker: the re-run only counts as an independent check when
|
|
8727
|
+
// its actor is not the builder of the row it is judging.
|
|
8728
|
+
const builder = reviewIntegrity.taskBuilder(task);
|
|
8729
|
+
if (builder && reviewIntegrity.normalizeActor(actor) === builder) {
|
|
8730
|
+
results.push({ ref, action: 'skipped', reason: 'verifier_is_builder' });
|
|
8731
|
+
continue;
|
|
8732
|
+
}
|
|
8209
8733
|
const verify = certifyVerifyCandidate(task);
|
|
8210
8734
|
if (!verify) {
|
|
8211
8735
|
results.push({ ref, action: 'skipped', reason: 'no_runnable_check_in_proof' });
|
|
@@ -8262,9 +8786,26 @@ function cmdCertifyVerified(args) {
|
|
|
8262
8786
|
return payload;
|
|
8263
8787
|
}
|
|
8264
8788
|
|
|
8789
|
+
// The landing: everything certified, one summary, one human gesture.
|
|
8790
|
+
// Review-by-N-pastes was the operator pain; this is the batch gate that
|
|
8791
|
+
// keeps human accept as the one gate without making it N gates.
|
|
8792
|
+
function cmdLanding(args) {
|
|
8793
|
+
if (hasFlag(args, '--accept')) {
|
|
8794
|
+
const passthrough = args.filter(arg => arg !== '--accept');
|
|
8795
|
+
if (!passthrough.includes('--confirm-human-accept')) passthrough.push('--confirm-human-accept');
|
|
8796
|
+
return cmdAutoAcceptCertified(passthrough);
|
|
8797
|
+
}
|
|
8798
|
+
cmdReviews(args);
|
|
8799
|
+
console.log('');
|
|
8800
|
+
console.log('land everything certified above in one gesture:');
|
|
8801
|
+
console.log(' atris task landing --accept --as <you>');
|
|
8802
|
+
console.log('(items needing one more check stay in review; only certified work lands)');
|
|
8803
|
+
}
|
|
8804
|
+
|
|
8265
8805
|
function cmdAutoAcceptCertified(args) {
|
|
8266
8806
|
const dryRun = hasFlag(args, '--dry-run');
|
|
8267
|
-
const
|
|
8807
|
+
const acceptAll = hasFlag(args, '--all');
|
|
8808
|
+
const strictVerify = !hasFlag(args, '--no-strict-verify') && !acceptAll;
|
|
8268
8809
|
const actorFlag = flag(args, '--as');
|
|
8269
8810
|
const hasHumanActor = validHumanActorFlag(actorFlag);
|
|
8270
8811
|
const confirmedHumanAccept = hasFlag(args, '--confirm-human-accept');
|
|
@@ -8277,7 +8818,17 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8277
8818
|
: { ok: false };
|
|
8278
8819
|
const actor = String(actorFlag || (policyAuth.ok ? policyAuth.actor : 'auto-accept-certified'));
|
|
8279
8820
|
const limitRaw = flag(args, '--limit');
|
|
8280
|
-
const
|
|
8821
|
+
const hasExplicitLimit = Boolean(limitRaw) && limitRaw !== true;
|
|
8822
|
+
// --all means sweep the full certified backlog, not just the first page of
|
|
8823
|
+
// it. Before this fix `max` was hard-capped at 12 even under --all, so a
|
|
8824
|
+
// real backlog (78 certified rows observed live) only ever drained 12/run —
|
|
8825
|
+
// an invisible undercount the autoland heartbeat repeated every hour.
|
|
8826
|
+
// AUTO_ACCEPT_ALL_SWEEP_CAP is a safety ceiling, not a target: a well-formed
|
|
8827
|
+
// --all run should always scan fewer rows than this.
|
|
8828
|
+
const AUTO_ACCEPT_ALL_SWEEP_CAP = 500;
|
|
8829
|
+
const max = hasExplicitLimit
|
|
8830
|
+
? Math.max(1, Number(limitRaw) || 12)
|
|
8831
|
+
: (acceptAll ? AUTO_ACCEPT_ALL_SWEEP_CAP : 12);
|
|
8281
8832
|
const parsedReward = parseAcceptReward(flag(args, '--reward'));
|
|
8282
8833
|
if (!parsedReward.ok) {
|
|
8283
8834
|
console.error('atris task auto-accept-certified: reward must be a positive number');
|
|
@@ -8297,10 +8848,18 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8297
8848
|
'live auto-accept requires --as <human> so XP has an explicit human acceptance actor',
|
|
8298
8849
|
);
|
|
8299
8850
|
}
|
|
8300
|
-
|
|
8851
|
+
// The standing autoland policy clears this gate too, same as the two
|
|
8852
|
+
// per-run human gates above: the owner flipped the policy, accepts run as
|
|
8853
|
+
// that owner, and the cron tick would land the same rows an hour later
|
|
8854
|
+
// anyway. Without this exception `atris autoland tick` was blind whenever
|
|
8855
|
+
// invoked from an agent session (CLAUDECODE etc. in env): the spawned
|
|
8856
|
+
// sweep failTask'd with no summary and the tick receipt showed nulls.
|
|
8857
|
+
// A per-run --confirm-human-accept claim from an agent is still refused —
|
|
8858
|
+
// policyAuth is only consulted when no per-run confirmation is passed.
|
|
8859
|
+
if (agentProofOnlyMode() && !dryRun && !policyAuth.ok) {
|
|
8301
8860
|
failAgentProofOnly(
|
|
8302
8861
|
'atris task auto-accept-certified',
|
|
8303
|
-
'Agent proof-only mode can preview certified rows with --dry-run, but cannot live-accept them.',
|
|
8862
|
+
'Agent proof-only mode can preview certified rows with --dry-run, but cannot live-accept them without the standing autoland policy.',
|
|
8304
8863
|
);
|
|
8305
8864
|
}
|
|
8306
8865
|
|
|
@@ -8308,16 +8867,30 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8308
8867
|
const db = taskDb.open();
|
|
8309
8868
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
8310
8869
|
const queue = taskReviewQueue(projection, ['--limit', String(max)]);
|
|
8870
|
+
const pendingReview = (projection.tasks || [])
|
|
8871
|
+
.filter((t) => t && t.status === 'review' && t.review && t.review.approval_status === 'pending');
|
|
8872
|
+
// Honest denominator for the summary line below: how many rows the
|
|
8873
|
+
// projection actually certified, independent of any scan cap. If `scanned`
|
|
8874
|
+
// ever comes in under `certified`, the undercount is visible instead of
|
|
8875
|
+
// silent (the exact failure mode this fix closes).
|
|
8876
|
+
const certifiedTotal = pendingReview.filter((t) => isAgentCertified(t)).length;
|
|
8877
|
+
// The review queue only surfaces certified rows. Under --all the bar is
|
|
8878
|
+
// the protected lanes, not certification, so scan every pending review.
|
|
8879
|
+
const pool = acceptAll
|
|
8880
|
+
? pendingReview
|
|
8881
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
|
|
8882
|
+
.slice(0, max)
|
|
8883
|
+
: queue.items.filter(item => item.queue_role !== 'blocked');
|
|
8311
8884
|
const results = [];
|
|
8312
8885
|
|
|
8313
|
-
for (const item of
|
|
8886
|
+
for (const item of pool) {
|
|
8314
8887
|
const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
|
|
8315
8888
|
const task = fullProjection.tasks[0] || null;
|
|
8316
8889
|
if (!task) {
|
|
8317
8890
|
results.push({ ref: item.display_id || item.id, eligible: false, reason: 'task_not_found', action: 'skipped' });
|
|
8318
8891
|
continue;
|
|
8319
8892
|
}
|
|
8320
|
-
const evaluation = evaluateAutoAccept(task, { strictVerify });
|
|
8893
|
+
const evaluation = evaluateAutoAccept(task, { strictVerify, acceptAll });
|
|
8321
8894
|
if (!evaluation.eligible) {
|
|
8322
8895
|
results.push({ ...evaluation, action: 'skipped' });
|
|
8323
8896
|
continue;
|
|
@@ -8332,6 +8905,7 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8332
8905
|
reward: parsedReward.value,
|
|
8333
8906
|
lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
|
|
8334
8907
|
nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
|
|
8908
|
+
autoAccepted: true,
|
|
8335
8909
|
});
|
|
8336
8910
|
if (!accepted.ok) {
|
|
8337
8911
|
results.push({ ...evaluation, action: 'accept_failed', reason: accepted.reason });
|
|
@@ -8349,17 +8923,24 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8349
8923
|
|
|
8350
8924
|
const { projection: finalProjection, outPath: finalPath } = writeDefaultProjection(taskDb, db);
|
|
8351
8925
|
const summary = {
|
|
8352
|
-
|
|
8926
|
+
certified: certifiedTotal,
|
|
8927
|
+
scanned: pool.length,
|
|
8353
8928
|
accepted: results.filter(row => row.action === 'accepted').length,
|
|
8354
8929
|
would_accept: results.filter(row => row.action === 'would_accept').length,
|
|
8355
8930
|
skipped: results.filter(row => row.action === 'skipped').length,
|
|
8356
8931
|
failed: results.filter(row => row.action === 'accept_failed').length,
|
|
8932
|
+
// Visible undercount flag: true only if the pool was cut short by `max`
|
|
8933
|
+
// while certified rows still existed beyond it. --all uses a high safety
|
|
8934
|
+
// cap (AUTO_ACCEPT_ALL_SWEEP_CAP), so this should stay false in practice;
|
|
8935
|
+
// if it ever flips true, the cap itself needs raising, not silence.
|
|
8936
|
+
undercounted: certifiedTotal > pool.length,
|
|
8357
8937
|
};
|
|
8358
8938
|
if (wantsJson(args)) {
|
|
8359
8939
|
printJson({
|
|
8360
8940
|
ok: true,
|
|
8361
8941
|
action: dryRun ? 'auto_accept_certified_dry_run' : 'auto_accept_certified',
|
|
8362
8942
|
strict_verify: strictVerify,
|
|
8943
|
+
accept_all: acceptAll,
|
|
8363
8944
|
summary,
|
|
8364
8945
|
...summary,
|
|
8365
8946
|
results,
|
|
@@ -8369,7 +8950,7 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8369
8950
|
return;
|
|
8370
8951
|
}
|
|
8371
8952
|
console.log(`AUTO-ACCEPT CERTIFIED (${dryRun ? 'dry-run' : 'execute'})`);
|
|
8372
|
-
console.log(`${summary.accepted || summary.would_accept} accepted
|
|
8953
|
+
console.log(`${summary.certified} certified, ${summary.scanned} scanned, ${summary.accepted || summary.would_accept} accepted, ${summary.skipped} skipped${summary.failed ? `, ${summary.failed} failed` : ''}${summary.undercounted ? ' (UNDERCOUNTED — raise --limit or the sweep cap)' : ''}`);
|
|
8373
8954
|
for (const row of results) {
|
|
8374
8955
|
const nextAction = row.next_action ? ` next_action=${row.next_action}` : '';
|
|
8375
8956
|
const reviewChat = row.review_chat_command ? ` review_chat=${row.review_chat_command}` : '';
|
|
@@ -8377,6 +8958,246 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8377
8958
|
}
|
|
8378
8959
|
}
|
|
8379
8960
|
|
|
8961
|
+
const SWEEP_AUTO_ACCEPT_PROTECTED = new Set([
|
|
8962
|
+
'money', 'deploy', 'release', 'publish', 'security', 'customer', 'outward',
|
|
8963
|
+
]);
|
|
8964
|
+
|
|
8965
|
+
function autoAcceptSweepLabelValues(task) {
|
|
8966
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
8967
|
+
const tags = Array.isArray(metadata.tags) ? metadata.tags : [];
|
|
8968
|
+
return [
|
|
8969
|
+
task && task.tag,
|
|
8970
|
+
task && task.lane,
|
|
8971
|
+
metadata.tag,
|
|
8972
|
+
metadata.lane,
|
|
8973
|
+
metadata.mission_lane,
|
|
8974
|
+
metadata.stage,
|
|
8975
|
+
...tags,
|
|
8976
|
+
].filter(value => value !== undefined && value !== null && String(value).trim());
|
|
8977
|
+
}
|
|
8978
|
+
|
|
8979
|
+
function autoAcceptSweepNormalizedTokens(value) {
|
|
8980
|
+
const normalized = String(value || '').trim().toLowerCase().replace(/_/g, '-');
|
|
8981
|
+
if (!normalized) return [];
|
|
8982
|
+
return [normalized, ...normalized.split(/[^a-z0-9-]+/).filter(Boolean)];
|
|
8983
|
+
}
|
|
8984
|
+
|
|
8985
|
+
function autoAcceptSweepDeniedReason(task) {
|
|
8986
|
+
for (const value of autoAcceptSweepLabelValues(task)) {
|
|
8987
|
+
const tokens = autoAcceptSweepNormalizedTokens(value);
|
|
8988
|
+
for (const token of tokens) {
|
|
8989
|
+
if (token === 'needs-human' || token === 'needshuman') {
|
|
8990
|
+
return 'needs_human';
|
|
8991
|
+
}
|
|
8992
|
+
const protectedLane = [...SWEEP_AUTO_ACCEPT_PROTECTED].find((denied) =>
|
|
8993
|
+
token === denied || token.replace(/s$/, '') === denied
|
|
8994
|
+
);
|
|
8995
|
+
if (protectedLane) return 'protected_lane';
|
|
8996
|
+
}
|
|
8997
|
+
}
|
|
8998
|
+
return null;
|
|
8999
|
+
}
|
|
9000
|
+
|
|
9001
|
+
function autoAcceptSweepLatestProof(task) {
|
|
9002
|
+
const metadata = task && task.metadata || {};
|
|
9003
|
+
const review = task && task.review || {};
|
|
9004
|
+
return String(review.proof || metadata.latest_agent_proof || '').trim();
|
|
9005
|
+
}
|
|
9006
|
+
|
|
9007
|
+
function autoAcceptSweepVerifierEvidence(proof, root) {
|
|
9008
|
+
const evidence = extractReceiptEvidence(proof, root);
|
|
9009
|
+
if (!evidence || !evidence.receipts || !evidence.receipts.length) {
|
|
9010
|
+
return { ok: false, reason: 'no_passing_verifier', evidence: evidence || null };
|
|
9011
|
+
}
|
|
9012
|
+
if (evidence.missing && evidence.missing.length) {
|
|
9013
|
+
return { ok: false, reason: 'receipt_missing', evidence };
|
|
9014
|
+
}
|
|
9015
|
+
const passing = evidence.receipts.filter((receipt) => receipt.verifier_passed === true);
|
|
9016
|
+
if (!passing.length) {
|
|
9017
|
+
return { ok: false, reason: 'no_passing_verifier', evidence };
|
|
9018
|
+
}
|
|
9019
|
+
const notPassed = evidence.receipts.find((receipt) => receipt.verifier_passed !== true);
|
|
9020
|
+
if (notPassed) {
|
|
9021
|
+
return { ok: false, reason: 'receipt_verifier_not_passed', evidence };
|
|
9022
|
+
}
|
|
9023
|
+
return {
|
|
9024
|
+
ok: true,
|
|
9025
|
+
evidence,
|
|
9026
|
+
passing,
|
|
9027
|
+
proved_by: passing.map((receipt) => `${receipt.path} verifier_passed=true`),
|
|
9028
|
+
};
|
|
9029
|
+
}
|
|
9030
|
+
|
|
9031
|
+
function autoAcceptSweepHappened(task) {
|
|
9032
|
+
const review = task && task.review || {};
|
|
9033
|
+
return clipStatusText(
|
|
9034
|
+
review.landing?.happened
|
|
9035
|
+
|| review.result?.changed
|
|
9036
|
+
|| task?.title
|
|
9037
|
+
|| 'accepted verified task',
|
|
9038
|
+
180,
|
|
9039
|
+
);
|
|
9040
|
+
}
|
|
9041
|
+
|
|
9042
|
+
function evaluateSweepAutoAccept(task, root) {
|
|
9043
|
+
const ref = taskRef(task);
|
|
9044
|
+
if (!task) return { eligible: false, ref, reason: 'task_not_found' };
|
|
9045
|
+
if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
|
|
9046
|
+
const metadata = task.metadata || {};
|
|
9047
|
+
const review = task.review || {};
|
|
9048
|
+
const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
|
|
9049
|
+
if (approval !== 'pending') return { eligible: false, ref, reason: `approval_${approval}` };
|
|
9050
|
+
if (metadata.auto_accepted_at) return { eligible: false, ref, reason: 'already_auto_accepted' };
|
|
9051
|
+
const denied = autoAcceptSweepDeniedReason(task);
|
|
9052
|
+
if (denied) return { eligible: false, ref, reason: denied };
|
|
9053
|
+
const proof = autoAcceptSweepLatestProof(task);
|
|
9054
|
+
if (!proof) return { eligible: false, ref, reason: 'no_proof' };
|
|
9055
|
+
|
|
9056
|
+
// 1. An explicit, stored verifier (`atris task ready --verify`, or a prior
|
|
9057
|
+
// certify-verified stamp) is the strongest signal: re-run it live, right
|
|
9058
|
+
// now, against the current checkout, and let it decide outright. This is
|
|
9059
|
+
// what unblocks CLI-762/CLI-861-shaped proofs: a real green test cited (or
|
|
9060
|
+
// executed) in the proof, but the older receipt-path check below could not
|
|
9061
|
+
// find file evidence for it once `ready --verify` stopped writing a
|
|
9062
|
+
// receipt file and started embedding the executed result into proof text.
|
|
9063
|
+
const storedVerify = typeof metadata.verify === 'string' ? metadata.verify.trim() : '';
|
|
9064
|
+
if (storedVerify) {
|
|
9065
|
+
const result = runVerifyCommand(storedVerify, root);
|
|
9066
|
+
if (!result.ok) return { eligible: false, ref, reason: result.reason, verify: storedVerify };
|
|
9067
|
+
return {
|
|
9068
|
+
eligible: true,
|
|
9069
|
+
ref,
|
|
9070
|
+
reason: 'verified_command',
|
|
9071
|
+
policy: 'sweep_auto_accept_verified_command',
|
|
9072
|
+
proof,
|
|
9073
|
+
verify: storedVerify,
|
|
9074
|
+
proved_by: [`${storedVerify} exited 0`],
|
|
9075
|
+
happened: autoAcceptSweepHappened(task),
|
|
9076
|
+
};
|
|
9077
|
+
}
|
|
9078
|
+
|
|
9079
|
+
// 2. Legacy path: a receipt file explicitly named in proof text.
|
|
9080
|
+
const verifier = autoAcceptSweepVerifierEvidence(proof, root);
|
|
9081
|
+
if (verifier.ok) {
|
|
9082
|
+
return {
|
|
9083
|
+
eligible: true,
|
|
9084
|
+
ref,
|
|
9085
|
+
reason: 'verified_receipt',
|
|
9086
|
+
policy: 'sweep_auto_accept_verified',
|
|
9087
|
+
proof,
|
|
9088
|
+
evidence: verifier.evidence,
|
|
9089
|
+
proved_by: verifier.proved_by,
|
|
9090
|
+
happened: autoAcceptSweepHappened(task),
|
|
9091
|
+
};
|
|
9092
|
+
}
|
|
9093
|
+
|
|
9094
|
+
// 3. No stored verifier and no receipt was even cited: try deriving a
|
|
9095
|
+
// safe, runnable command straight from the proof text itself (same
|
|
9096
|
+
// extractor certify-verified uses) and re-run it live. Only reached when
|
|
9097
|
+
// there is nothing else to go on, so a proof that legitimately cites a
|
|
9098
|
+
// real receipt keeps taking the receipt path above rather than racing an
|
|
9099
|
+
// unrelated command mentioned in the same sentence.
|
|
9100
|
+
if (verifier.reason === 'no_passing_verifier') {
|
|
9101
|
+
const derived = certifyVerifyCandidate(task);
|
|
9102
|
+
if (derived) {
|
|
9103
|
+
const result = runVerifyCommand(derived, root);
|
|
9104
|
+
if (result.ok) {
|
|
9105
|
+
return {
|
|
9106
|
+
eligible: true,
|
|
9107
|
+
ref,
|
|
9108
|
+
reason: 'verified_derived_command',
|
|
9109
|
+
policy: 'sweep_auto_accept_verified_derived',
|
|
9110
|
+
proof,
|
|
9111
|
+
verify: derived,
|
|
9112
|
+
proved_by: [`${derived} exited 0`],
|
|
9113
|
+
happened: autoAcceptSweepHappened(task),
|
|
9114
|
+
};
|
|
9115
|
+
}
|
|
9116
|
+
return { eligible: false, ref, reason: result.reason, verify: derived };
|
|
9117
|
+
}
|
|
9118
|
+
}
|
|
9119
|
+
|
|
9120
|
+
return { eligible: false, ref, reason: verifier.reason, evidence: verifier.evidence };
|
|
9121
|
+
}
|
|
9122
|
+
|
|
9123
|
+
function cmdSweep(args) {
|
|
9124
|
+
if (!hasFlag(args, '--auto-accept')) {
|
|
9125
|
+
failTask(
|
|
9126
|
+
'atris task sweep',
|
|
9127
|
+
'missing_auto_accept',
|
|
9128
|
+
'atris task sweep currently requires --auto-accept for the explicit verified accept policy',
|
|
9129
|
+
);
|
|
9130
|
+
}
|
|
9131
|
+
const actor = String(flag(args, '--as') || 'orb-autoaccept');
|
|
9132
|
+
const parsedReward = parseAcceptReward(flag(args, '--reward'));
|
|
9133
|
+
if (!parsedReward.ok) {
|
|
9134
|
+
failTask('atris task sweep', 'invalid_reward', 'atris task sweep --auto-accept reward must be a positive number');
|
|
9135
|
+
}
|
|
9136
|
+
const limitRaw = flag(args, '--limit');
|
|
9137
|
+
const explicitLimit = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 0) : null;
|
|
9138
|
+
const taskDb = getTaskDb();
|
|
9139
|
+
const db = taskDb.open();
|
|
9140
|
+
const { projection } = writeDefaultProjection(taskDb, db);
|
|
9141
|
+
const root = projection.workspace_root || process.cwd();
|
|
9142
|
+
let pendingReview = (projection.tasks || [])
|
|
9143
|
+
.filter((task) => task && task.status === 'review' && task.review && task.review.approval_status === 'pending')
|
|
9144
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
|
9145
|
+
if (explicitLimit) pendingReview = pendingReview.slice(0, explicitLimit);
|
|
9146
|
+
const results = [];
|
|
9147
|
+
for (const item of pendingReview) {
|
|
9148
|
+
const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
|
|
9149
|
+
const task = fullProjection.tasks[0] || null;
|
|
9150
|
+
const evaluation = evaluateSweepAutoAccept(task, root);
|
|
9151
|
+
if (!evaluation.eligible) {
|
|
9152
|
+
results.push({ ...evaluation, action: 'skipped', task_id: task?.id || item.id || null });
|
|
9153
|
+
continue;
|
|
9154
|
+
}
|
|
9155
|
+
const accepted = acceptReviewTask(taskDb, db, task.id, {
|
|
9156
|
+
actor,
|
|
9157
|
+
proof: evaluation.proof,
|
|
9158
|
+
reward: parsedReward.value,
|
|
9159
|
+
lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
|
|
9160
|
+
nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
|
|
9161
|
+
autoAccepted: true,
|
|
9162
|
+
});
|
|
9163
|
+
if (!accepted.ok) {
|
|
9164
|
+
results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
|
|
9165
|
+
continue;
|
|
9166
|
+
}
|
|
9167
|
+
stampAutoAcceptMetadata(taskDb, db, task.id, actor, evaluation.policy);
|
|
9168
|
+
refreshCareerXpAfterReview(accepted.reviewed);
|
|
9169
|
+
results.push({
|
|
9170
|
+
...evaluation,
|
|
9171
|
+
action: 'accepted',
|
|
9172
|
+
task_id: task.id,
|
|
9173
|
+
reward: accepted.reviewed.episode.reward.value,
|
|
9174
|
+
});
|
|
9175
|
+
}
|
|
9176
|
+
const { outPath } = writeDefaultProjection(taskDb, db);
|
|
9177
|
+
const summary = {
|
|
9178
|
+
scanned: pendingReview.length,
|
|
9179
|
+
accepted: results.filter((row) => row.action === 'accepted').length,
|
|
9180
|
+
skipped: results.filter((row) => row.action === 'skipped').length,
|
|
9181
|
+
failed: results.filter((row) => row.action === 'accept_failed').length,
|
|
9182
|
+
};
|
|
9183
|
+
if (wantsJson(args)) {
|
|
9184
|
+
printJson({
|
|
9185
|
+
ok: true,
|
|
9186
|
+
action: 'sweep_auto_accept',
|
|
9187
|
+
actor,
|
|
9188
|
+
summary,
|
|
9189
|
+
...summary,
|
|
9190
|
+
results,
|
|
9191
|
+
projection_path: outPath,
|
|
9192
|
+
});
|
|
9193
|
+
return;
|
|
9194
|
+
}
|
|
9195
|
+
console.log(`TASK SWEEP AUTO-ACCEPT: ${summary.accepted} accepted / ${summary.scanned} scanned / ${summary.skipped} skipped${summary.failed ? ` / ${summary.failed} failed` : ''}`);
|
|
9196
|
+
for (const row of results.filter((item) => item.action === 'accepted')) {
|
|
9197
|
+
console.log(`ACCEPTED ${row.ref}: ${row.happened} | proved by ${row.proved_by.join(', ')}`);
|
|
9198
|
+
}
|
|
9199
|
+
}
|
|
9200
|
+
|
|
8380
9201
|
function cmdRevise(args) {
|
|
8381
9202
|
const pos = positional(args);
|
|
8382
9203
|
const id = pos[0];
|
|
@@ -8444,6 +9265,7 @@ function cmdReview(args) {
|
|
|
8444
9265
|
if (clearNextTask || (typeof nextTaskFlag === 'string' && !String(nextTaskFlag).trim())) clearedFields.push('next_task');
|
|
8445
9266
|
const proof = proofFlagValue(args);
|
|
8446
9267
|
const verify = textFlag(args, ['--verify']);
|
|
9268
|
+
guardExplicitActor('atris task review', flag(args, '--as'));
|
|
8447
9269
|
const actor = flag(args, '--as') || DEFAULT_OWNER;
|
|
8448
9270
|
const rewardValue = reward === true || reward === null ? 0 : reward;
|
|
8449
9271
|
if (agentProofOnlyMode() && Number(rewardValue) > 0) {
|
|
@@ -8481,7 +9303,11 @@ function cmdReview(args) {
|
|
|
8481
9303
|
clearedFields,
|
|
8482
9304
|
});
|
|
8483
9305
|
if (!result.reviewed) {
|
|
8484
|
-
|
|
9306
|
+
if (result.reason === 'judge_equals_worker') {
|
|
9307
|
+
console.error(`review failed: judge_equals_worker: ${result.builder} built this task and cannot judge it. Hand off: atris task review ${id} --reward 1 --as <another member>`);
|
|
9308
|
+
} else {
|
|
9309
|
+
console.error(`review failed: ${result.reason}`);
|
|
9310
|
+
}
|
|
8485
9311
|
process.exit(1);
|
|
8486
9312
|
}
|
|
8487
9313
|
const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
|
|
@@ -8587,21 +9413,23 @@ function cmdEvents(args) {
|
|
|
8587
9413
|
const pos = positional(args);
|
|
8588
9414
|
let taskId = pos[0] || null;
|
|
8589
9415
|
const all = hasFlag(args, '--all');
|
|
9416
|
+
const everywhere = taskScopeEverywhere(args);
|
|
8590
9417
|
const rawLimit = flag(args, '--limit');
|
|
8591
9418
|
const explicitLimit = rawLimit && rawLimit !== true ? Number(rawLimit) : null;
|
|
8592
9419
|
const defaultRecentLimit = 24;
|
|
8593
|
-
const limit = explicitLimit || (taskId ? 500 : (all ? null : defaultRecentLimit));
|
|
9420
|
+
const limit = explicitLimit || (taskId ? 500 : (all || everywhere ? null : defaultRecentLimit));
|
|
8594
9421
|
const taskDb = getTaskDb();
|
|
8595
9422
|
const db = taskDb.open();
|
|
8596
9423
|
if (taskId) taskId = requireTaskId(taskDb, db, taskId, 'atris task events');
|
|
9424
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
8597
9425
|
const events = taskDb.listTaskEvents(db, {
|
|
8598
9426
|
taskId,
|
|
8599
|
-
workspaceRoot:
|
|
9427
|
+
workspaceRoot: taskId ? null : workspaceRoot,
|
|
8600
9428
|
limit,
|
|
8601
|
-
order: taskId || all ? 'asc' : 'desc',
|
|
9429
|
+
order: taskId || all || everywhere ? 'asc' : 'desc',
|
|
8602
9430
|
});
|
|
8603
9431
|
const refRows = taskDb.listTasks(db, {
|
|
8604
|
-
workspaceRoot:
|
|
9432
|
+
workspaceRoot: everywhere ? null : (taskId ? (taskDb.getTask(db, taskId) || {}).workspace_root : workspaceRoot),
|
|
8605
9433
|
});
|
|
8606
9434
|
const refById = taskDb.taskDisplayRefMap(refRows);
|
|
8607
9435
|
if (wantsJson(args)) {
|
|
@@ -8609,7 +9437,7 @@ function cmdEvents(args) {
|
|
|
8609
9437
|
ok: true,
|
|
8610
9438
|
action: 'events',
|
|
8611
9439
|
task_id: taskId,
|
|
8612
|
-
mode: taskId ? 'task' : (all ? 'ledger' : 'recent'),
|
|
9440
|
+
mode: taskId ? 'task' : (everywhere ? 'ledger_everywhere' : (all ? 'ledger' : 'recent')),
|
|
8613
9441
|
limit,
|
|
8614
9442
|
events,
|
|
8615
9443
|
});
|
|
@@ -8619,7 +9447,7 @@ function cmdEvents(args) {
|
|
|
8619
9447
|
console.log('(no task events)');
|
|
8620
9448
|
return;
|
|
8621
9449
|
}
|
|
8622
|
-
if (!taskId && !all) {
|
|
9450
|
+
if (!taskId && !all && !everywhere) {
|
|
8623
9451
|
console.log('TASK EVENTS');
|
|
8624
9452
|
console.log(`recent ${events.length} event${events.length === 1 ? '' : 's'} (use --all for the full ledger, --limit N to adjust)`);
|
|
8625
9453
|
console.log('');
|
|
@@ -8720,12 +9548,13 @@ function cmdLineage(args) {
|
|
|
8720
9548
|
function cmdExport(args) {
|
|
8721
9549
|
const out = flag(args, '--out') || path.join('.atris', 'state', 'tasks.projection.json');
|
|
8722
9550
|
const all = hasFlag(args, '--all');
|
|
9551
|
+
const everywhere = taskScopeEverywhere(args);
|
|
8723
9552
|
const taskDb = getTaskDb();
|
|
8724
9553
|
const db = taskDb.open();
|
|
8725
9554
|
const outPath = path.resolve(String(out));
|
|
8726
9555
|
const projection = enrichTaskProjection(taskDb.taskProjection(db, {
|
|
8727
|
-
workspaceRoot:
|
|
8728
|
-
limit: 500,
|
|
9556
|
+
workspaceRoot: scopedWorkspaceRoot(taskDb, args, { everywhere }),
|
|
9557
|
+
limit: all ? null : 500,
|
|
8729
9558
|
}));
|
|
8730
9559
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
8731
9560
|
fs.writeFileSync(outPath, JSON.stringify(projection, null, 2) + '\n', 'utf8');
|
|
@@ -8902,21 +9731,42 @@ function taskRenderSummaryLine(counts) {
|
|
|
8902
9731
|
].join('; ');
|
|
8903
9732
|
}
|
|
8904
9733
|
|
|
9734
|
+
function refreshExistingTodoMarkdown(taskDb, db, workspaceRoot) {
|
|
9735
|
+
const ws = workspaceRoot || taskDb.workspaceRoot();
|
|
9736
|
+
const outPath = path.join(ws, 'atris', 'TODO.md');
|
|
9737
|
+
if (!fs.existsSync(outPath)) return null;
|
|
9738
|
+
const rows = taskDb.listTasks(db, { workspaceRoot: ws, limit: 500 });
|
|
9739
|
+
const refRows = taskDb.listTasks(db, { workspaceRoot: ws });
|
|
9740
|
+
const existingTodo = fs.readFileSync(outPath, 'utf8');
|
|
9741
|
+
const preservedSections = [];
|
|
9742
|
+
const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
|
|
9743
|
+
if (endgameSection) preservedSections.push(endgameSection);
|
|
9744
|
+
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
|
|
9745
|
+
const markdown = taskDb.renderTodoMarkdown([...rows, ...markdownRows], {
|
|
9746
|
+
refRows,
|
|
9747
|
+
preservedSections,
|
|
9748
|
+
});
|
|
9749
|
+
fs.writeFileSync(outPath, markdown, 'utf8');
|
|
9750
|
+
return outPath;
|
|
9751
|
+
}
|
|
9752
|
+
|
|
8905
9753
|
function cmdRender(args) {
|
|
8906
9754
|
const out = flag(args, '--out') || path.join('atris', 'TODO.md');
|
|
8907
9755
|
const all = hasFlag(args, '--all');
|
|
9756
|
+
const everywhere = taskScopeEverywhere(args);
|
|
8908
9757
|
const doneLimitRaw = flag(args, '--done-limit');
|
|
8909
9758
|
const doneLimit = doneLimitRaw && doneLimitRaw !== true ? Number(doneLimitRaw) : undefined;
|
|
8910
9759
|
const failedLimitRaw = flag(args, '--failed-limit');
|
|
8911
9760
|
const failedLimit = failedLimitRaw && failedLimitRaw !== true ? Number(failedLimitRaw) : undefined;
|
|
8912
9761
|
const taskDb = getTaskDb();
|
|
8913
9762
|
const db = taskDb.open();
|
|
9763
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
8914
9764
|
const rows = taskDb.listTasks(db, {
|
|
8915
|
-
workspaceRoot
|
|
8916
|
-
limit: 500,
|
|
9765
|
+
workspaceRoot,
|
|
9766
|
+
limit: all ? null : 500,
|
|
8917
9767
|
});
|
|
8918
9768
|
const refRows = taskDb.listTasks(db, {
|
|
8919
|
-
workspaceRoot
|
|
9769
|
+
workspaceRoot,
|
|
8920
9770
|
});
|
|
8921
9771
|
const outPath = path.resolve(String(out));
|
|
8922
9772
|
const existingTodo = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : '';
|
|
@@ -9525,6 +10375,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9525
10375
|
owner,
|
|
9526
10376
|
reviewer,
|
|
9527
10377
|
all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
|
|
10378
|
+
everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
|
|
9528
10379
|
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
9529
10380
|
scope,
|
|
9530
10381
|
});
|
|
@@ -9540,6 +10391,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9540
10391
|
owner,
|
|
9541
10392
|
reviewer,
|
|
9542
10393
|
all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
|
|
10394
|
+
everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
|
|
9543
10395
|
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
9544
10396
|
scope,
|
|
9545
10397
|
});
|
|
@@ -9570,6 +10422,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9570
10422
|
owner,
|
|
9571
10423
|
reviewer,
|
|
9572
10424
|
all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
|
|
10425
|
+
everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
|
|
9573
10426
|
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
9574
10427
|
scope,
|
|
9575
10428
|
});
|
|
@@ -10114,6 +10967,9 @@ async function run(args) {
|
|
|
10114
10967
|
return cmdPlanPreview(rest);
|
|
10115
10968
|
case 'note': return cmdNote(rest);
|
|
10116
10969
|
case 'say': return cmdNote(rest);
|
|
10970
|
+
case 'tag':
|
|
10971
|
+
case 'tags':
|
|
10972
|
+
return cmdTag(rest);
|
|
10117
10973
|
case 'show': return cmdShow(rest);
|
|
10118
10974
|
case 'inspect': return cmdInspect(rest);
|
|
10119
10975
|
case 'page': return cmdPage(rest);
|
|
@@ -10122,11 +10978,17 @@ async function run(args) {
|
|
|
10122
10978
|
case 'chat-review':
|
|
10123
10979
|
return cmdReviewChat(rest);
|
|
10124
10980
|
case 'ready': return cmdReady(rest);
|
|
10981
|
+
case 'receipt': return cmdTaskReceipt(rest);
|
|
10125
10982
|
case 'result': return cmdResult(rest);
|
|
10126
10983
|
case 'accept': return cmdAccept(rest);
|
|
10984
|
+
case 'landing':
|
|
10985
|
+
case 'land-review':
|
|
10986
|
+
return cmdLanding(rest);
|
|
10127
10987
|
case 'auto-accept-certified':
|
|
10128
10988
|
case 'auto-accept':
|
|
10129
10989
|
return cmdAutoAcceptCertified(rest);
|
|
10990
|
+
case 'sweep':
|
|
10991
|
+
return cmdSweep(rest);
|
|
10130
10992
|
case 'certify-verified':
|
|
10131
10993
|
return cmdCertifyVerified(rest);
|
|
10132
10994
|
case 'accept-group':
|
|
@@ -10135,6 +10997,8 @@ async function run(args) {
|
|
|
10135
10997
|
case 'done': return cmdDone(rest);
|
|
10136
10998
|
case 'finish': return cmdFinish(rest);
|
|
10137
10999
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
11000
|
+
case 'archive': return cmdArchive(rest);
|
|
11001
|
+
case 'relabel-archived': return cmdRelabelArchived(rest);
|
|
10138
11002
|
case 'review': return cmdReview(rest);
|
|
10139
11003
|
case 'reviews':
|
|
10140
11004
|
case 'review-queue':
|