atris 3.34.0 → 3.36.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 +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- 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/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/commands/task.js
CHANGED
|
@@ -7,15 +7,25 @@ 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 {
|
|
12
|
-
|
|
10
|
+
const { taskProofState } = require('../lib/task-proof');
|
|
11
|
+
const {
|
|
12
|
+
evaluateAutoAccept,
|
|
13
|
+
isAgentCertified,
|
|
14
|
+
isAutoCertifyVerifyCommandAllowed,
|
|
15
|
+
parseVerifyCommand,
|
|
16
|
+
runVerifyCommand,
|
|
17
|
+
runVerifyCommandCached,
|
|
18
|
+
DENIED_TAGS,
|
|
19
|
+
} = require('../lib/auto-accept-certified');
|
|
20
|
+
const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
|
|
13
21
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
22
|
+
const reviewIntegrity = require('../lib/review-integrity');
|
|
23
|
+
const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord } = require('../lib/voice-gate');
|
|
14
24
|
const {
|
|
15
25
|
normalizeOwnerSlug,
|
|
16
26
|
resolveFunctionalOwner: resolveFunctionalTaskOwner,
|
|
17
27
|
} = require('../lib/functional-owner');
|
|
18
|
-
const { operatorReady, hasAgentJargon } = require('./autoland');
|
|
28
|
+
const { operatorReady, hasAgentJargon, explainResult } = require('./autoland');
|
|
19
29
|
const {
|
|
20
30
|
TASK_INSPECT_FIELDS,
|
|
21
31
|
readFieldsFlag,
|
|
@@ -38,6 +48,13 @@ const REVIEW_LANE_RUN_MAX_RUNS = 20;
|
|
|
38
48
|
const PENDING_REVIEW_CHAT_STOP_REASON = 'pending_review_chat_waiting_for_agent_review';
|
|
39
49
|
const PROOF_BOUNDARY_BLOCKED_ACTION = 'proof_boundary_blocked';
|
|
40
50
|
const PROOF_BOUNDARY_BLOCKED_REASON = 'proof_boundary_blocked_requires_revision';
|
|
51
|
+
const MISSION_XP_END_TO_END_REASON = 'mission_xp_requires_end_to_end_receipt';
|
|
52
|
+
const MISSION_XP_END_TO_END_DETAIL = 'mission XP proof must name a zero-papercut end-to-end fresh-laptop pass through install, init, first mission, and first self-landed task; generic mission/tick receipts are not enough';
|
|
53
|
+
const READY_RESULT_TEACHING = 'ready needs --result: one plain sentence someone new to the project can understand. say what someone can do now and why it matters. no ids, no paths, no commands. example: operators can now read the whole team day on one page instead of scrolling raw logs.';
|
|
54
|
+
const REVIEW_AUTO_ACCEPT_ACTOR = 'auto (certified, small)';
|
|
55
|
+
const REVIEW_AUTO_ACCEPT_POLICY = 'review_autoaccept_certified_small';
|
|
56
|
+
const REVIEW_AUTO_ACCEPT_FILE_LIMIT = 10;
|
|
57
|
+
const REVIEW_AUTO_ACCEPT_LINE_LIMIT = 300;
|
|
41
58
|
|
|
42
59
|
const STATUS_PLAN_TAGS = new Set([
|
|
43
60
|
'agent',
|
|
@@ -99,11 +116,11 @@ function getTaskDb() {
|
|
|
99
116
|
}
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
function warnIfTaskTitleNeedsOperatorWhy(title) {
|
|
119
|
+
function warnIfTaskTitleNeedsOperatorWhy(title, options = {}) {
|
|
103
120
|
const text = String(title || '').trim();
|
|
104
121
|
if (!text || operatorReady(text)) return null;
|
|
105
|
-
const warning = 'Warning:
|
|
106
|
-
console.error(warning);
|
|
122
|
+
const warning = 'Warning: put the why in this task title in plain words: what it buys or costs, and who benefits. Drop flags and ids.';
|
|
123
|
+
if (options.print !== false) console.error(warning);
|
|
107
124
|
return warning;
|
|
108
125
|
}
|
|
109
126
|
|
|
@@ -111,6 +128,13 @@ function taskUsageText() {
|
|
|
111
128
|
return `
|
|
112
129
|
atris task - durable local task state (SQLite, gitignored)
|
|
113
130
|
|
|
131
|
+
golden path (zero human turns):
|
|
132
|
+
atris task delegate "fix the login bug" --to <member>
|
|
133
|
+
atris task claim <id> --as <member>
|
|
134
|
+
... build ...
|
|
135
|
+
atris task ready <id> --verify
|
|
136
|
+
atris autoland tick # second check runs, task lands
|
|
137
|
+
|
|
114
138
|
atris task Show the task desk
|
|
115
139
|
atris task new "<title>" Create a task
|
|
116
140
|
atris task next [--tag <tag>] [--create-next]
|
|
@@ -118,14 +142,19 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
118
142
|
atris task continue-work <id> Create/reuse a certified Review follow-up task
|
|
119
143
|
atris task say <id> "<message>" Add context to a task
|
|
120
144
|
atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
|
|
121
|
-
atris task ready <id> --proof "..."
|
|
122
|
-
|
|
145
|
+
atris task ready <id> --proof "..." --result "<sentence>"
|
|
146
|
+
Agent proof ready; native goal can complete
|
|
147
|
+
atris task ready <id> --verify "<cmd>" --result "<sentence>"
|
|
148
|
+
Run <cmd>; only ready if it exits 0 (executed proof)
|
|
149
|
+
Writes atris/runs/ receipt (pass or fail), folds path into proof
|
|
150
|
+
atris task receipt <id> --verify "<cmd>" Run <cmd> and write an atris/runs/ receipt without going to ready
|
|
123
151
|
atris task plan-preview "<purpose>" [--tag <tag>] [--owner <member>] [--task <id>]
|
|
124
152
|
Show the plain Plan before work starts
|
|
125
|
-
atris task ready <id> --proof "..." [--changed "..." --checked "..." --saved "..." --try "..."]
|
|
153
|
+
atris task ready <id> --proof "..." --result "<sentence>" [--changed "..." --checked "..." --saved "..." --try "..."]
|
|
126
154
|
Agent proof ready; records Result if needed
|
|
127
|
-
atris task ready <id> --proof "..." [--happened "..." --checked "..." --tested "..." --decision "..."]
|
|
155
|
+
atris task ready <id> --proof "..." --result "<sentence>" [--happened "..." --checked "..." --tested "..." --decision "..."]
|
|
128
156
|
Agent proof ready; writes the human result receipt
|
|
157
|
+
atris task result <id> "<sentence>" Set or replace the day-one PM result sentence
|
|
129
158
|
atris task result <id> --changed "..." --checked "..." [--saved "..."] [--try "..."]
|
|
130
159
|
Show the plain Result and store trace on the task
|
|
131
160
|
atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
|
|
@@ -133,20 +162,25 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
133
162
|
Human accepts proof, marks done; --public also publishes AgentXP
|
|
134
163
|
atris task certify-verified [--dry-run] [--limit <n>] [--as <actor>]
|
|
135
164
|
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>]
|
|
165
|
+
atris task auto-accept-certified --dry-run [--strict-verify] [--all] [--limit <n>]
|
|
137
166
|
Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
|
|
167
|
+
atris task sweep --auto-accept [--json] Auto-accept verified Review rows; protected lanes wait for human
|
|
168
|
+
atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
|
|
138
169
|
atris task revise <id> --note "..." Send reviewed work back to Do
|
|
139
170
|
|
|
140
171
|
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
|
|
172
|
+
atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] Create assigned work
|
|
142
173
|
atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
|
|
143
174
|
Record a task-owned Plan stage
|
|
144
175
|
atris task do <id> --as <owner> --first-move "..."
|
|
145
176
|
Start task-owned Do work from the plan
|
|
146
177
|
atris task backlog <id> [--reason "..."] Move a planned open task back to Backlog
|
|
147
178
|
atris task clear-plan --yes Move all planned open tasks back to Backlog
|
|
148
|
-
atris task day [--json]
|
|
149
|
-
|
|
179
|
+
atris task day [--full] [--all] [--everywhere] [--json] show today's owner-grouped task list
|
|
180
|
+
text shows eight current rows; --full shows every active row
|
|
181
|
+
--all stays in this workspace; --everywhere spans workspaces
|
|
182
|
+
atris task list [--all] [--everywhere] [--status <s>]
|
|
183
|
+
list tasks in this workspace; --everywhere spans workspaces
|
|
150
184
|
atris task claim <id> [--as <owner>] Atomic claim
|
|
151
185
|
atris task release <id> [--as <owner>] Release your own mistaken claim back to open
|
|
152
186
|
atris task capabilities [--json] Read-only task CLI/API capability contract
|
|
@@ -163,6 +197,10 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
163
197
|
Advance the scoped current task one safe step
|
|
164
198
|
review-state lanes: needs-agent, continue-work, human-accept-waiting, certified
|
|
165
199
|
atris task note <id> "<message>" Append dialogue/context to a task
|
|
200
|
+
atris task retitle <id> "<new title>" Rename a task and preserve the old title in dialogue
|
|
201
|
+
atris task tag <id> --add <tag> [--remove <tag>]
|
|
202
|
+
Update tags on an existing task (e.g. --add needs-human to hold it
|
|
203
|
+
from sweep + fleet staffing); logs a task_tags_updated event
|
|
166
204
|
atris task show <id> [--json] Show a task card + dialogue
|
|
167
205
|
atris task inspect <id> --fields review,status,title [--json]
|
|
168
206
|
Field-selectable task state (review metadata, status, title, owner, tag)
|
|
@@ -170,6 +208,12 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
170
208
|
atris task step <id> [--json] Refine chat, then advance one safe Plan/Do/Review step
|
|
171
209
|
atris task done <id> --proof "..." Mark complete with proof
|
|
172
210
|
atris task done <id> --failed [--proof "..."] Mark failed, optionally reviewed
|
|
211
|
+
atris task archive <id> --reason "..." [--from-failed]
|
|
212
|
+
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
213
|
+
--from-failed opts in to relabel a fail-closed row (never done)
|
|
214
|
+
atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
|
|
215
|
+
atris task relabel-archived [--dry-run|--apply]
|
|
216
|
+
One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
|
|
173
217
|
atris task finish <id> --proof "..." Legacy alias for done with proof
|
|
174
218
|
atris task review <id> --reward <n> [--verify "<cmd>"]
|
|
175
219
|
Write review event + RSI episode
|
|
@@ -183,10 +227,13 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
183
227
|
atris task sync --dry-run Plan cloud/Swarlo task sync writes
|
|
184
228
|
atris task import <file> One-shot import from TODO.md
|
|
185
229
|
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
|
|
230
|
+
atris task events [id] [--limit <n>] print recent task events
|
|
231
|
+
atris task events --all print the full current-workspace ledger
|
|
232
|
+
atris task events --everywhere print the full ledger across workspaces
|
|
233
|
+
atris task export [--all] [--everywhere] [--out <file>]
|
|
234
|
+
write web/desktop JSON projection
|
|
235
|
+
atris task render [--all] [--everywhere] [--out <file>]
|
|
236
|
+
regenerate compact TODO.md view from state
|
|
190
237
|
atris task where Print db path + workspace scope
|
|
191
238
|
atris task help This help
|
|
192
239
|
|
|
@@ -226,6 +273,15 @@ function hasFlag(args, name) {
|
|
|
226
273
|
return args.indexOf(name) !== -1;
|
|
227
274
|
}
|
|
228
275
|
|
|
276
|
+
function taskScopeEverywhere(args = [], options = {}) {
|
|
277
|
+
if (options.everywhere !== undefined) return Boolean(options.everywhere);
|
|
278
|
+
return hasFlag(args, '--everywhere');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function scopedWorkspaceRoot(taskDb, args = [], options = {}) {
|
|
282
|
+
return taskScopeEverywhere(args, options) ? null : taskDb.workspaceRoot();
|
|
283
|
+
}
|
|
284
|
+
|
|
229
285
|
function hasEmptyFlagValue(args, name) {
|
|
230
286
|
const i = args.indexOf(name);
|
|
231
287
|
return i !== -1 && args[i + 1] === '';
|
|
@@ -387,6 +443,22 @@ function proofFlagValue(args) {
|
|
|
387
443
|
return typeof proof === 'string' ? proof.trim() : '';
|
|
388
444
|
}
|
|
389
445
|
|
|
446
|
+
function resultSentenceIssue(value) {
|
|
447
|
+
const check = explainResult(value);
|
|
448
|
+
return check.ok ? null : check.reason;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function readyResultDetail(reason) {
|
|
452
|
+
return reason ? `${READY_RESULT_TEACHING}\n${reason}` : READY_RESULT_TEACHING;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function requireResultSentence(label, value, { ready = false } = {}) {
|
|
456
|
+
const issue = resultSentenceIssue(value);
|
|
457
|
+
if (!issue) return String(value || '').replace(/\s+/g, ' ').trim();
|
|
458
|
+
const detail = ready ? readyResultDetail(issue) : issue;
|
|
459
|
+
failTask(label, 'weak_result', detail);
|
|
460
|
+
}
|
|
461
|
+
|
|
390
462
|
function textFlag(args, names) {
|
|
391
463
|
for (const name of names) {
|
|
392
464
|
const value = flag(args, name);
|
|
@@ -416,17 +488,27 @@ function landingNeedsDayOnePm(sentence, title) {
|
|
|
416
488
|
const text = normalizedLandingSentence(sentence);
|
|
417
489
|
if (!text) return true;
|
|
418
490
|
if (text.toLowerCase() === normalizedLandingSentence(defaultLandingSentenceForTitle(title)).toLowerCase()) return true;
|
|
419
|
-
return hasAgentJargon(text) || !operatorReady(text);
|
|
491
|
+
return hasAgentJargon(text) || /\bas\s+exists?\b/i.test(text) || !operatorReady(text);
|
|
420
492
|
}
|
|
421
493
|
|
|
422
494
|
function warnIfLandingNeedsDayOnePm(landing, title) {
|
|
423
495
|
const sentence = landing && typeof landing === 'object' ? landing.happened : '';
|
|
424
496
|
if (!landingNeedsDayOnePm(sentence, title)) return null;
|
|
425
|
-
const warning = 'Advisory: add --landing with one
|
|
497
|
+
const warning = 'Advisory: add --landing with one plain sentence saying what someone can do now, in words a new teammate would get. No flags, no ids.';
|
|
426
498
|
console.error(warning);
|
|
427
499
|
return warning;
|
|
428
500
|
}
|
|
429
501
|
|
|
502
|
+
function requireExplicitLandingDayOnePm(label, landing, title) {
|
|
503
|
+
const sentence = landing && typeof landing === 'object' ? normalizedLandingSentence(landing.happened) : '';
|
|
504
|
+
if (!sentence || !landingNeedsDayOnePm(sentence, title)) return;
|
|
505
|
+
failTask(
|
|
506
|
+
label,
|
|
507
|
+
'weak_landing',
|
|
508
|
+
'landing needs one plain sentence saying what someone can do now and why it matters, in words a new teammate would understand. no flags, ids, or unnamed filters.',
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
430
512
|
function numericFlag(args, name) {
|
|
431
513
|
const value = flag(args, name);
|
|
432
514
|
if (value === null || value === true || value === undefined) return null;
|
|
@@ -441,6 +523,102 @@ function meaningfulTaskProofIssue(proof, { required = true } = {}) {
|
|
|
441
523
|
return state.ok ? null : state.reason;
|
|
442
524
|
}
|
|
443
525
|
|
|
526
|
+
function goldenPathMissionXpTask(task) {
|
|
527
|
+
if (!task) return false;
|
|
528
|
+
const metadata = task.metadata || {};
|
|
529
|
+
const text = [
|
|
530
|
+
task.title,
|
|
531
|
+
task.tag,
|
|
532
|
+
metadata.goal_id,
|
|
533
|
+
metadata.goalId,
|
|
534
|
+
metadata.mission_id,
|
|
535
|
+
metadata.stop_condition,
|
|
536
|
+
metadata.goal_objective,
|
|
537
|
+
metadata.objective,
|
|
538
|
+
].filter(Boolean).join(' ');
|
|
539
|
+
const missionXp = String(task.title || '').trim().toLowerCase().startsWith('mission xp:')
|
|
540
|
+
|| String(task.tag || '').toLowerCase() === 'agent-xp';
|
|
541
|
+
return missionXp
|
|
542
|
+
&& /\b(?:golden[- ]path|zero[- ]knowledge|zero[- ]papercuts?|fresh[- ](?:laptop|environment|install|home)|self[- ]landed)\b/i.test(text);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function receiptTextForProof(proof, root = process.cwd()) {
|
|
546
|
+
const chunks = [];
|
|
547
|
+
const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
|
|
548
|
+
let match;
|
|
549
|
+
while ((match = pattern.exec(String(proof || ''))) && chunks.length < 3) {
|
|
550
|
+
const rel = match[1];
|
|
551
|
+
if (!rel || rel.includes('*')) continue;
|
|
552
|
+
try {
|
|
553
|
+
const raw = fs.readFileSync(path.resolve(root, rel), 'utf8');
|
|
554
|
+
const parsed = JSON.parse(raw);
|
|
555
|
+
chunks.push(JSON.stringify({
|
|
556
|
+
schema: parsed.schema || null,
|
|
557
|
+
mission_id: parsed.mission_id || null,
|
|
558
|
+
result: parsed.result || null,
|
|
559
|
+
landing: parsed.landing || null,
|
|
560
|
+
last_landing: parsed.last_landing || null,
|
|
561
|
+
summary: parsed.summary || null,
|
|
562
|
+
}).slice(0, 12000));
|
|
563
|
+
} catch {}
|
|
564
|
+
}
|
|
565
|
+
return chunks.join(' ');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Mission ticks already compose a real landing sentence into the run receipt.
|
|
569
|
+
// When a mission-bridged task lands with only a receipt proof, lift that
|
|
570
|
+
// sentence so the review queue shows the work instead of echoing the title.
|
|
571
|
+
function missionReceiptResultForProof(task, proof, root = process.cwd()) {
|
|
572
|
+
const metadata = task?.metadata || {};
|
|
573
|
+
if (!metadata.mission_id && !metadata.goal_id) return null;
|
|
574
|
+
const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
|
|
575
|
+
let match;
|
|
576
|
+
while ((match = pattern.exec(String(proof || '')))) {
|
|
577
|
+
const rel = match[1];
|
|
578
|
+
if (!rel || rel.includes('*')) continue;
|
|
579
|
+
let landing = null;
|
|
580
|
+
try {
|
|
581
|
+
const parsed = JSON.parse(fs.readFileSync(path.resolve(root, rel), 'utf8'));
|
|
582
|
+
landing = parsed?.result?.landing || parsed?.landing || parsed?.last_landing || null;
|
|
583
|
+
} catch { continue; }
|
|
584
|
+
const changed = String(landing?.changed || landing?.happened || '').replace(/\s+/g, ' ').trim();
|
|
585
|
+
if (!changed) continue;
|
|
586
|
+
if (/\brecorded tick \d+\.?$/i.test(changed) || /^recorded a proof heartbeat\b/i.test(changed)) continue;
|
|
587
|
+
const reason = String(landing?.reason || landing?.why || '').replace(/\s+/g, ' ').trim();
|
|
588
|
+
return { changed, reason: reason && !isRetiredFillerReason(reason) ? reason : null };
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function missionXpEndToEndProofIssue(task, proof, root = process.cwd()) {
|
|
594
|
+
if (!goldenPathMissionXpTask(task)) return null;
|
|
595
|
+
const corpus = `${String(proof || '')} ${receiptTextForProof(proof, root)}`
|
|
596
|
+
.replace(/\s+/g, ' ')
|
|
597
|
+
.trim();
|
|
598
|
+
const hasZeroPapercut = /\b(?:zero|0|no)\s+(?:new\s+)?papercuts?\b|\bzero[- ]papercut\b/i.test(corpus);
|
|
599
|
+
const hasEndToEnd = /\bend[- ]to[- ]end\b|\bfull\s+(?:fresh[- ](?:laptop|environment)\s+)?pass\b|\bfresh[- ](?:laptop|environment|install)\b|\bclean\s+temp\s+home\b|\bnpm\s+pack\b/i.test(corpus);
|
|
600
|
+
const hasSelfLanded = /\bself[- ]landed\b|\bfirst\s+self[- ]landed\s+task\b|\btask\s+reaches\s+done\b|\binstall\b.{0,120}\binit\b.{0,120}\bmission\b.{0,120}\b(?:self[- ]landed|task)\b/i.test(corpus);
|
|
601
|
+
return hasZeroPapercut && hasEndToEnd && hasSelfLanded ? null : MISSION_XP_END_TO_END_DETAIL;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function missionXpProofBoundaryEvaluation(task, proofOverride = null) {
|
|
605
|
+
if (!goldenPathMissionXpTask(task)) return null;
|
|
606
|
+
const metadata = task.metadata || {};
|
|
607
|
+
const review = task.review || {};
|
|
608
|
+
const proof = proofOverride === null
|
|
609
|
+
? String(review.proof || metadata.latest_agent_proof || '').trim()
|
|
610
|
+
: String(proofOverride || '').trim();
|
|
611
|
+
const issue = missionXpEndToEndProofIssue(task, proof, task.workspace_root || process.cwd());
|
|
612
|
+
if (!issue) return null;
|
|
613
|
+
return {
|
|
614
|
+
eligible: false,
|
|
615
|
+
ref: taskRef(task),
|
|
616
|
+
reason: MISSION_XP_END_TO_END_REASON,
|
|
617
|
+
next_action: 'attach the zero-papercut end-to-end fresh-laptop receipt, then resubmit Mission XP proof',
|
|
618
|
+
proof,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
444
622
|
function requireMeaningfulTaskProof(label, proof, { required = true } = {}) {
|
|
445
623
|
const issue = meaningfulTaskProofIssue(proof, { required });
|
|
446
624
|
if (issue) failTask(label, 'weak_proof', `meaningful proof required: ${issue}`);
|
|
@@ -462,14 +640,22 @@ function positional(args) {
|
|
|
462
640
|
});
|
|
463
641
|
}
|
|
464
642
|
|
|
465
|
-
function writeDefaultProjection(taskDb, db,
|
|
643
|
+
function writeDefaultProjection(taskDb, db, options = {}) {
|
|
644
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, [], options);
|
|
466
645
|
const projection = enrichTaskProjection(taskDb.taskProjection(db, {
|
|
467
|
-
workspaceRoot
|
|
468
|
-
limit: 500,
|
|
646
|
+
workspaceRoot,
|
|
647
|
+
limit: options.all ? null : 500,
|
|
469
648
|
}));
|
|
470
649
|
const outPath = path.resolve(path.join('.atris', 'state', 'tasks.projection.json'));
|
|
650
|
+
const output = JSON.stringify(projection, null, 2) + '\n';
|
|
471
651
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
472
|
-
|
|
652
|
+
let shouldWrite = true;
|
|
653
|
+
try {
|
|
654
|
+
shouldWrite = fs.readFileSync(outPath, 'utf8') !== output;
|
|
655
|
+
} catch {
|
|
656
|
+
shouldWrite = true;
|
|
657
|
+
}
|
|
658
|
+
if (shouldWrite) fs.writeFileSync(outPath, output, 'utf8');
|
|
473
659
|
return { projection, outPath };
|
|
474
660
|
}
|
|
475
661
|
|
|
@@ -692,6 +878,11 @@ function certifiedReviewNextAction(nextTaskTitle) {
|
|
|
692
878
|
}
|
|
693
879
|
|
|
694
880
|
function proofBoundaryBlockedEvaluation(task) {
|
|
881
|
+
const missionXpBoundary = missionXpProofBoundaryEvaluation(task);
|
|
882
|
+
if (missionXpBoundary) return missionXpBoundary;
|
|
883
|
+
// strictVerify stays off here: this is a render-path probe for the boundary
|
|
884
|
+
// reason only, and the default-true strict mode would spawn the verify
|
|
885
|
+
// subprocess for every certified row just to draw the desk.
|
|
695
886
|
const evaluation = evaluateAutoAccept(task, { strictVerify: false, minPasses: 0 });
|
|
696
887
|
return evaluation && evaluation.reason === 'proof_unmerged_or_draft_pr_boundary'
|
|
697
888
|
? evaluation
|
|
@@ -882,26 +1073,11 @@ function proofToReasonText(proof) {
|
|
|
882
1073
|
return /[.!?]$/.test(section) ? section : `${section}.`;
|
|
883
1074
|
}
|
|
884
1075
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
if (/\bapprove\b/.test(text) && /\b(command|exact|preview|ux)\b/.test(text)) {
|
|
892
|
-
return 'It lets the operator see the next command without hunting.';
|
|
893
|
-
}
|
|
894
|
-
if (/\b(stale|expire|expired)\b/.test(text) && /\bapproval/.test(text)) {
|
|
895
|
-
return 'It stops old approvals from running after their context has gone stale.';
|
|
896
|
-
}
|
|
897
|
-
if (/\bapproval|approve|permission\b/.test(text)) {
|
|
898
|
-
return 'It keeps real-world side effects behind a clear human decision.';
|
|
899
|
-
}
|
|
900
|
-
if (/\btest|self-test|harness|verifier|proof\b/.test(text)) {
|
|
901
|
-
return 'It gives the human a repeatable check before approval.';
|
|
902
|
-
}
|
|
903
|
-
return 'It turns the task title into a concrete result the human can approve.';
|
|
904
|
-
}
|
|
1076
|
+
// A landing sentence written for --result already carries its own why
|
|
1077
|
+
// ("..., so operators keep deciding instead of waiting"). Reuse that clause
|
|
1078
|
+
// as the reason instead of inventing one; with no clause, stay silent.
|
|
1079
|
+
// landingWhyClause lives in lib/voice-gate.js so every human-bound landing
|
|
1080
|
+
// composer (task reviews, mission receipts) shares the same why extraction.
|
|
905
1081
|
|
|
906
1082
|
function proofToHumanCheck(proof) {
|
|
907
1083
|
const text = String(proof || '').replace(/\s+/g, ' ').trim();
|
|
@@ -1011,17 +1187,30 @@ function taskReviewLanding(task, review = {}, payload = {}) {
|
|
|
1011
1187
|
const agentCertified = review.agent_certified === true || metadata.agent_certified === true;
|
|
1012
1188
|
const approvalStatus = review.approval_status || metadata.approval_status || null;
|
|
1013
1189
|
const explicitHappened = landingPayloadValue(payload, metadata, 'happened')
|
|
1014
|
-
|| payload.changed || metadata.result_changed || metadata.human_changed || metadata.changed;
|
|
1190
|
+
|| payload.result || metadata.result || payload.changed || metadata.result_changed || metadata.human_changed || metadata.changed;
|
|
1015
1191
|
const explicitChecked = landingPayloadValue(payload, metadata, 'checked')
|
|
1016
1192
|
|| payload.checked || metadata.result_checked || metadata.human_checked || metadata.checked;
|
|
1017
1193
|
const explicitTested = landingPayloadValue(payload, metadata, 'tested');
|
|
1018
1194
|
const explicitDecision = landingPayloadValue(payload, metadata, 'decision');
|
|
1019
|
-
const
|
|
1195
|
+
const explicitReasonRaw = landingPayloadValue(payload, metadata, 'reason')
|
|
1020
1196
|
|| landingPayloadValue(payload, metadata, 'why')
|
|
1021
1197
|
|| payload.reason || payload.why || metadata.result_reason || metadata.review_reason || metadata.why_it_matters;
|
|
1198
|
+
const explicitReason = explicitReasonRaw && !isRetiredFillerReason(explicitReasonRaw) ? explicitReasonRaw : null;
|
|
1199
|
+
const missionLift = explicitHappened
|
|
1200
|
+
? null
|
|
1201
|
+
: missionReceiptResultForProof(task, proof, task.workspace_root || process.cwd());
|
|
1202
|
+
let happened = clipStatusText(explicitHappened || (missionLift && missionLift.changed) || titleToResultText(task.title), 220);
|
|
1203
|
+
let reason = clipStatusText(explicitReason || proofToReasonText(proof) || (missionLift && missionLift.reason) || '', 220);
|
|
1204
|
+
if (!reason) {
|
|
1205
|
+
const clause = landingWhyClause(happened);
|
|
1206
|
+
if (clause) {
|
|
1207
|
+
happened = clipStatusText(clause.change, 220);
|
|
1208
|
+
reason = clipStatusText(clause.why, 220);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1022
1211
|
return {
|
|
1023
|
-
happened
|
|
1024
|
-
reason
|
|
1212
|
+
happened,
|
|
1213
|
+
reason,
|
|
1025
1214
|
checked: clipStatusText(explicitChecked || proofToHumanCheck(proof), 220),
|
|
1026
1215
|
tested: clipStatusText(explicitTested || taskReviewLandingTested(proof), 260),
|
|
1027
1216
|
decision: clipStatusText(explicitDecision || (task.status === 'done'
|
|
@@ -1057,11 +1246,65 @@ function taskReviewSavedText(task, review = {}, ref = taskRef(task)) {
|
|
|
1057
1246
|
return `Work record saved as ${ref}.`;
|
|
1058
1247
|
}
|
|
1059
1248
|
|
|
1249
|
+
function cleanReviewProofText(value) {
|
|
1250
|
+
const text = String(value || '').trim();
|
|
1251
|
+
return text || null;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function reviewMessageLooksLikeProof(value) {
|
|
1255
|
+
const text = cleanReviewProofText(value);
|
|
1256
|
+
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));
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function taskReviewEventProof(task) {
|
|
1260
|
+
const events = Array.isArray(task.events) ? task.events : [];
|
|
1261
|
+
for (const event of events.slice().reverse()) {
|
|
1262
|
+
const payload = event && event.payload && typeof event.payload === 'object' ? event.payload : {};
|
|
1263
|
+
const explicit = cleanReviewProofText(payload.proof || payload.verify);
|
|
1264
|
+
if (explicit) return explicit;
|
|
1265
|
+
const message = cleanReviewProofText(payload.content || payload.chat_packet || payload.stage_packet);
|
|
1266
|
+
if (message && reviewMessageLooksLikeProof(message)) return message;
|
|
1267
|
+
}
|
|
1268
|
+
const messages = Array.isArray(task.messages) ? task.messages : [];
|
|
1269
|
+
for (const message of messages.slice().reverse()) {
|
|
1270
|
+
const content = cleanReviewProofText(message && message.content);
|
|
1271
|
+
if (content && reviewMessageLooksLikeProof(content)) return content;
|
|
1272
|
+
}
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function taskReviewProofFallback(task, payload = {}) {
|
|
1277
|
+
if (!task || task.status !== 'review') return null;
|
|
1278
|
+
const metadata = task.metadata || {};
|
|
1279
|
+
return cleanReviewProofText(metadata.latest_agent_proof)
|
|
1280
|
+
|| cleanReviewProofText(payload.proof)
|
|
1281
|
+
|| cleanReviewProofText(metadata.proof)
|
|
1282
|
+
|| cleanReviewProofText(metadata.verify)
|
|
1283
|
+
|| cleanReviewProofText(metadata.latest_agent_verify)
|
|
1284
|
+
|| cleanReviewProofText(payload.verify)
|
|
1285
|
+
|| taskReviewEventProof(task);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function reviewReceiptPath(proofText, root) {
|
|
1289
|
+
const evidence = extractReceiptEvidence(proofText, root);
|
|
1290
|
+
if (!evidence) return null;
|
|
1291
|
+
return evidence.receipts?.[0]?.path || evidence.missing?.[0] || null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function withReviewReceiptPath(review, root) {
|
|
1295
|
+
if (!review) return null;
|
|
1296
|
+
return {
|
|
1297
|
+
...review,
|
|
1298
|
+
receipt_path: reviewReceiptPath(review.proof, root),
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1060
1302
|
function taskReviewSummary(task) {
|
|
1061
1303
|
const reviewed = (task.events || []).slice().reverse().find(e => e.event_type === 'reviewed' || e.event_type === 'proof_ready' || e.event_type === 'revision_requested');
|
|
1062
1304
|
const payload = reviewed && reviewed.payload || {};
|
|
1063
1305
|
const metadata = task.metadata || {};
|
|
1064
|
-
|
|
1306
|
+
const fallbackProof = taskReviewProofFallback(task, payload);
|
|
1307
|
+
if (!reviewed && !metadata.approval_status && !metadata.agent_review_pass_count && !metadata.human_revision_count && !metadata.agent_certified && !fallbackProof) return null;
|
|
1065
1308
|
if (reviewed && reviewed.event_type === 'revision_requested') {
|
|
1066
1309
|
const review = {
|
|
1067
1310
|
summary: reviewSummary(task, payload),
|
|
@@ -1101,7 +1344,7 @@ function taskReviewSummary(task) {
|
|
|
1101
1344
|
const review = {
|
|
1102
1345
|
summary: reviewSummary(task, payload),
|
|
1103
1346
|
reward: reviewed && reviewed.event_type === 'reviewed' && payload.reward !== undefined ? payload.reward : null,
|
|
1104
|
-
proof: readyField('proof', 'latest_agent_proof'),
|
|
1347
|
+
proof: readyField('proof', 'latest_agent_proof') || fallbackProof,
|
|
1105
1348
|
lesson: readyField('lesson', 'latest_agent_lesson'),
|
|
1106
1349
|
next_task: readyField('next_task', 'latest_agent_next_task'),
|
|
1107
1350
|
approval_status: metadata.approval_status || (task.status === 'review' ? 'pending' : null),
|
|
@@ -1132,6 +1375,7 @@ function taskReviewInspectMetadata(task) {
|
|
|
1132
1375
|
human_revision_count: review.human_revision_count || null,
|
|
1133
1376
|
human_revision_note: review.human_revision_note || null,
|
|
1134
1377
|
reward: review.reward ?? null,
|
|
1378
|
+
receipt_path: review.receipt_path || null,
|
|
1135
1379
|
};
|
|
1136
1380
|
}
|
|
1137
1381
|
|
|
@@ -1317,7 +1561,7 @@ function enrichTaskProjection(projection) {
|
|
|
1317
1561
|
const parentLinkType = parentFromParentId ? 'parent_task_id' : parentFromGoalId ? 'goal_id' : null;
|
|
1318
1562
|
const parentId = parent ? parent.id : metadata.parent_task_id || null;
|
|
1319
1563
|
const childTasks = children.get(task.id) || [];
|
|
1320
|
-
const review = taskReviewSummary(task);
|
|
1564
|
+
const review = withReviewReceiptPath(taskReviewSummary(task), root);
|
|
1321
1565
|
return {
|
|
1322
1566
|
...task,
|
|
1323
1567
|
objective: taskObjective(task, parent, goalSource.goals, { parentLinkType, baseObjectives }),
|
|
@@ -1502,6 +1746,44 @@ function latestTaskEvent(task) {
|
|
|
1502
1746
|
return events.length ? events[events.length - 1] : null;
|
|
1503
1747
|
}
|
|
1504
1748
|
|
|
1749
|
+
function verifiedProofCommand(proof) {
|
|
1750
|
+
const match = String(proof || '').match(/\[verified\]\s+`([^`]+)`\s+passed\s+\(exit 0\)/i);
|
|
1751
|
+
return match ? String(match[1] || '').trim() : '';
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function autoCertifyCommandCandidatesForTask(task) {
|
|
1755
|
+
const metadata = task && task.metadata || {};
|
|
1756
|
+
const review = task && task.review || {};
|
|
1757
|
+
const proof = String(review.proof || metadata.latest_agent_proof || '');
|
|
1758
|
+
const candidates = [
|
|
1759
|
+
metadata.verify,
|
|
1760
|
+
metadata.latest_agent_verify,
|
|
1761
|
+
verifiedProofCommand(review.proof),
|
|
1762
|
+
verifiedProofCommand(metadata.latest_agent_proof),
|
|
1763
|
+
...taskReviewEvidenceCommands(proof),
|
|
1764
|
+
].map(value => String(value || '').trim()).filter(Boolean);
|
|
1765
|
+
const seen = new Set();
|
|
1766
|
+
return candidates.filter((candidate) => {
|
|
1767
|
+
const key = candidate.toLowerCase();
|
|
1768
|
+
if (seen.has(key)) return false;
|
|
1769
|
+
seen.add(key);
|
|
1770
|
+
return true;
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function reviewBlockerForTask(task) {
|
|
1775
|
+
const ref = taskRef(task);
|
|
1776
|
+
const candidates = autoCertifyCommandCandidatesForTask(task);
|
|
1777
|
+
const safe = candidates.find(command => isAutoCertifyVerifyCommandAllowed(command));
|
|
1778
|
+
const unsafe = candidates.find(command => !isAutoCertifyVerifyCommandAllowed(command));
|
|
1779
|
+
const reason = !safe && unsafe ? 'verify_command_not_allowed' : 'needs_second_actor_review';
|
|
1780
|
+
return {
|
|
1781
|
+
reason,
|
|
1782
|
+
verify_command: (!safe && unsafe) ? unsafe : (safe || candidates[0] || null),
|
|
1783
|
+
next_command: `atris task review-chat ${ref} --as codex-review`,
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1505
1787
|
function reviewHandoffForTask(task, { suppressExistingFollowUp = false, hasExistingReviewFollowUp = null } = {}) {
|
|
1506
1788
|
const review = task && task.review || {};
|
|
1507
1789
|
if (task && task.status !== 'review') return null;
|
|
@@ -1521,7 +1803,15 @@ function reviewHandoffForTask(task, { suppressExistingFollowUp = false, hasExist
|
|
|
1521
1803
|
if (proofBoundary) {
|
|
1522
1804
|
handoff.reason = proofBoundary.reason;
|
|
1523
1805
|
handoff.next_action_detail = proofBoundary.next_action || null;
|
|
1524
|
-
|
|
1806
|
+
const note = proofBoundary.reason === MISSION_XP_END_TO_END_REASON
|
|
1807
|
+
? '<attach zero-papercut end-to-end fresh-laptop receipt or move back to Do>'
|
|
1808
|
+
: '<replace stale PR proof with merged proof or move back to Do>';
|
|
1809
|
+
handoff.revise_command = `atris task revise ${taskRef(task)} --note "${note}"`;
|
|
1810
|
+
} else if (!agentCertified) {
|
|
1811
|
+
const blocker = reviewBlockerForTask(task);
|
|
1812
|
+
handoff.reason = blocker.reason;
|
|
1813
|
+
handoff.next_action_detail = blocker.verify_command || null;
|
|
1814
|
+
handoff.review_chat_command = blocker.next_command;
|
|
1525
1815
|
} else if (agentCertified && nextTask && !hasExistingFollowUp) {
|
|
1526
1816
|
handoff.next_task = nextTask;
|
|
1527
1817
|
handoff.continue_work_command = continueWorkCommandForTask(task);
|
|
@@ -1960,7 +2250,8 @@ function compactTaskForStatus(task) {
|
|
|
1960
2250
|
id: task.id,
|
|
1961
2251
|
display_id: task.display_id || null,
|
|
1962
2252
|
legacy_ref: task.legacy_ref || taskRef(task.id),
|
|
1963
|
-
title:
|
|
2253
|
+
title: clipStatusTitle(task.title, 140),
|
|
2254
|
+
result: clipStatusText(task.result || metadata.result, 180) || null,
|
|
1964
2255
|
status: task.status,
|
|
1965
2256
|
updated_at: task.updated_at,
|
|
1966
2257
|
};
|
|
@@ -1980,6 +2271,7 @@ function compactTaskForStatus(task) {
|
|
|
1980
2271
|
if (task.review.proof) review.proof = clipStatusText(task.review.proof, 180);
|
|
1981
2272
|
if (task.review.lesson) review.lesson = clipStatusText(task.review.lesson, 180);
|
|
1982
2273
|
if (task.review.next_task) review.next_task = clipStatusText(task.review.next_task, 140);
|
|
2274
|
+
if (Object.prototype.hasOwnProperty.call(task.review, 'receipt_path')) review.receipt_path = task.review.receipt_path || null;
|
|
1983
2275
|
if (task.review.approval_status) review.approval_status = task.review.approval_status;
|
|
1984
2276
|
if (task.review.agent_review_pass_count) review.agent_review_pass_count = task.review.agent_review_pass_count;
|
|
1985
2277
|
if (task.review.agent_certified) review.agent_certified = task.review.agent_certified;
|
|
@@ -2013,7 +2305,7 @@ function compactTaskFromProjection(projection, id) {
|
|
|
2013
2305
|
function compactEventPayload(payload) {
|
|
2014
2306
|
if (!payload || typeof payload !== 'object') return null;
|
|
2015
2307
|
const out = {};
|
|
2016
|
-
for (const key of ['title', 'status', 'tag', 'content', 'goal', 'summary', 'proof', 'lesson', 'reward', 'next_task']) {
|
|
2308
|
+
for (const key of ['title', 'status', 'tag', 'content', 'goal', 'summary', 'proof', 'lesson', 'reward', 'next_task', 'result']) {
|
|
2017
2309
|
if (payload[key] !== undefined && payload[key] !== null && payload[key] !== '') out[key] = payload[key];
|
|
2018
2310
|
}
|
|
2019
2311
|
return Object.keys(out).length ? out : null;
|
|
@@ -2038,6 +2330,19 @@ function clipStatusText(value, max = 180) {
|
|
|
2038
2330
|
return `${text.slice(0, max - 1)}…`;
|
|
2039
2331
|
}
|
|
2040
2332
|
|
|
2333
|
+
function clipStatusTitle(value, max = 140) {
|
|
2334
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
2335
|
+
if (text.length <= max) return text;
|
|
2336
|
+
const cut = text.slice(0, max + 1);
|
|
2337
|
+
const boundaries = [...cut.matchAll(/[.!?;](?=\s|$)/g)];
|
|
2338
|
+
const boundary = boundaries.map((match) => match.index).filter((index) => index >= max * 0.45).pop();
|
|
2339
|
+
if (Number.isInteger(boundary)) {
|
|
2340
|
+
return cut.slice(0, boundary + 1).replace(/[;:]+$/, '').trim();
|
|
2341
|
+
}
|
|
2342
|
+
const wholeWords = text.slice(0, max).replace(/\s+\S*$/, '').trim();
|
|
2343
|
+
return `${wholeWords || text.slice(0, max).trim()}...`;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2041
2346
|
function compactReviewActionRef(task, { hasExistingReviewFollowUp = null } = {}) {
|
|
2042
2347
|
if (!task) return null;
|
|
2043
2348
|
const handoff = reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp }) || {};
|
|
@@ -3055,6 +3360,7 @@ function taskCapabilitiesCheckReport(taskDb, db, args = [], options = {}) {
|
|
|
3055
3360
|
const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
|
|
3056
3361
|
const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
|
|
3057
3362
|
const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
|
|
3363
|
+
const everywhere = taskScopeEverywhere(args, options);
|
|
3058
3364
|
const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
|
|
3059
3365
|
const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
|
|
3060
3366
|
const standalone = taskCapabilitiesContract();
|
|
@@ -3062,6 +3368,7 @@ function taskCapabilitiesCheckReport(taskDb, db, args = [], options = {}) {
|
|
|
3062
3368
|
owner,
|
|
3063
3369
|
reviewer,
|
|
3064
3370
|
all,
|
|
3371
|
+
everywhere,
|
|
3065
3372
|
limit,
|
|
3066
3373
|
scope,
|
|
3067
3374
|
});
|
|
@@ -3326,7 +3633,11 @@ function selectTaskForCurrent(projection, { owner = DEFAULT_OWNER, scope = {}, h
|
|
|
3326
3633
|
if (reviewNeedsAgent) return { task: reviewNeedsAgent, reason: 'review_needs_agent_verification' };
|
|
3327
3634
|
const reviewProofBoundaryBlocked = columns.review.find(task => reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp })?.next_action === PROOF_BOUNDARY_BLOCKED_ACTION);
|
|
3328
3635
|
if (reviewProofBoundaryBlocked) return { task: reviewProofBoundaryBlocked, reason: 'review_proof_boundary_blocked' };
|
|
3329
|
-
|
|
3636
|
+
// Scoped selection (goal_id, tag, status, or review_state) implies a sequenced
|
|
3637
|
+
// work stream (e.g. golden-path "pass 1a" before "pass 2"): earlier-created
|
|
3638
|
+
// tasks must win over newer ones, not the newest-first default used for the
|
|
3639
|
+
// unscoped desk view.
|
|
3640
|
+
const planQueue = !taskQueueScopeIsEmpty(normalizedScope) ? sortTasksOldestFirst(columns.plan) : columns.plan;
|
|
3330
3641
|
const planReady = planQueue[0];
|
|
3331
3642
|
if (planReady) return { task: planReady, reason: 'plan_ready' };
|
|
3332
3643
|
const backlogIdea = columns.backlog[0];
|
|
@@ -3392,18 +3703,20 @@ function buildTaskCurrent(taskDb, db, args = [], options = {}) {
|
|
|
3392
3703
|
const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
|
|
3393
3704
|
const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
|
|
3394
3705
|
const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
|
|
3706
|
+
const everywhere = taskScopeEverywhere(args, options);
|
|
3707
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
3395
3708
|
const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
|
|
3396
3709
|
const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
|
|
3397
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
|
|
3710
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
|
|
3398
3711
|
const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(
|
|
3399
3712
|
taskDb,
|
|
3400
3713
|
db,
|
|
3401
|
-
|
|
3714
|
+
workspaceRoot,
|
|
3402
3715
|
);
|
|
3403
3716
|
const hasPendingReviewChat = buildPendingReviewChatPredicate(
|
|
3404
3717
|
taskDb,
|
|
3405
3718
|
db,
|
|
3406
|
-
|
|
3719
|
+
workspaceRoot,
|
|
3407
3720
|
);
|
|
3408
3721
|
return {
|
|
3409
3722
|
projection,
|
|
@@ -3676,12 +3989,14 @@ function taskReviewLaneDrainReport(taskDb, db, args = [], options = {}) {
|
|
|
3676
3989
|
const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
|
|
3677
3990
|
const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
|
|
3678
3991
|
const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
|
|
3992
|
+
const everywhere = taskScopeEverywhere(args, options);
|
|
3679
3993
|
const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
|
|
3680
3994
|
const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
|
|
3681
3995
|
const capabilitiesCheck = taskCapabilitiesCheckReport(taskDb, db, [], {
|
|
3682
3996
|
owner,
|
|
3683
3997
|
reviewer,
|
|
3684
3998
|
all,
|
|
3999
|
+
everywhere,
|
|
3685
4000
|
limit,
|
|
3686
4001
|
scope,
|
|
3687
4002
|
});
|
|
@@ -3689,6 +4004,7 @@ function taskReviewLaneDrainReport(taskDb, db, args = [], options = {}) {
|
|
|
3689
4004
|
owner,
|
|
3690
4005
|
reviewer,
|
|
3691
4006
|
all,
|
|
4007
|
+
everywhere,
|
|
3692
4008
|
limit,
|
|
3693
4009
|
scope,
|
|
3694
4010
|
excludeTaskIds: options.excludeTaskIds,
|
|
@@ -3749,6 +4065,7 @@ function taskReviewLaneActOptionsFromArgs(args = []) {
|
|
|
3749
4065
|
owner: flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER,
|
|
3750
4066
|
reviewer: reviewActor(flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review'),
|
|
3751
4067
|
all: hasFlag(args, '--all'),
|
|
4068
|
+
everywhere: hasFlag(args, '--everywhere'),
|
|
3752
4069
|
limit: taskQueueLimit(args),
|
|
3753
4070
|
scope: taskQueueScopeFromArgs(args),
|
|
3754
4071
|
dryRun: hasFlag(args, '--dry-run'),
|
|
@@ -3766,6 +4083,7 @@ function taskReviewLaneActOptionsFromBody(body = {}, searchParams = new URLSearc
|
|
|
3766
4083
|
owner: String(queryOwner || body.owner || body.as || body.actor || DEFAULT_OWNER),
|
|
3767
4084
|
reviewer: reviewActor(queryReviewer || body.reviewer || body.review_actor || body.reviewActor || 'codex-review'),
|
|
3768
4085
|
all: searchParams.get('all') === '1' || searchParams.get('all') === 'true' || Boolean(body.all),
|
|
4086
|
+
everywhere: searchParams.get('everywhere') === '1' || searchParams.get('everywhere') === 'true' || Boolean(body.everywhere),
|
|
3769
4087
|
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
3770
4088
|
scope: mergeTaskQueueScopes(queryScope, bodyScope),
|
|
3771
4089
|
dryRun: searchParams.get('dry_run') === '1'
|
|
@@ -3831,6 +4149,7 @@ function taskReviewLaneAct(taskDb, db, options = {}) {
|
|
|
3831
4149
|
owner,
|
|
3832
4150
|
reviewer,
|
|
3833
4151
|
all: Boolean(options.all),
|
|
4152
|
+
everywhere: Boolean(options.everywhere),
|
|
3834
4153
|
limit: options.limit !== undefined ? options.limit : 8,
|
|
3835
4154
|
scope,
|
|
3836
4155
|
excludeTaskIds: options.excludeTaskIds,
|
|
@@ -4069,6 +4388,7 @@ function taskReviewLaneLoop(taskDb, db, options = {}) {
|
|
|
4069
4388
|
owner,
|
|
4070
4389
|
reviewer,
|
|
4071
4390
|
all: Boolean(options.all),
|
|
4391
|
+
everywhere: Boolean(options.everywhere),
|
|
4072
4392
|
limit: options.limit !== undefined ? options.limit : 8,
|
|
4073
4393
|
scope,
|
|
4074
4394
|
excludeTaskIds,
|
|
@@ -4121,6 +4441,7 @@ function taskReviewLaneLoop(taskDb, db, options = {}) {
|
|
|
4121
4441
|
owner,
|
|
4122
4442
|
reviewer,
|
|
4123
4443
|
all: Boolean(options.all),
|
|
4444
|
+
everywhere: Boolean(options.everywhere),
|
|
4124
4445
|
limit: options.limit !== undefined ? options.limit : 8,
|
|
4125
4446
|
scope,
|
|
4126
4447
|
excludeTaskIds,
|
|
@@ -4308,6 +4629,7 @@ function taskReviewLaneRun(taskDb, db, options = {}) {
|
|
|
4308
4629
|
owner,
|
|
4309
4630
|
reviewer,
|
|
4310
4631
|
all: Boolean(options.all),
|
|
4632
|
+
everywhere: Boolean(options.everywhere),
|
|
4311
4633
|
limit: options.limit !== undefined ? options.limit : 8,
|
|
4312
4634
|
scope,
|
|
4313
4635
|
excludeTaskIds,
|
|
@@ -4417,6 +4739,483 @@ function cmdReviewLaneRun(args) {
|
|
|
4417
4739
|
if (!result.ok) process.exit(1);
|
|
4418
4740
|
}
|
|
4419
4741
|
|
|
4742
|
+
function configValueDisabled(value) {
|
|
4743
|
+
if (value === false) return true;
|
|
4744
|
+
const text = String(value === undefined || value === null ? '' : value).trim().toLowerCase();
|
|
4745
|
+
return ['0', 'false', 'off', 'no'].includes(text);
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
function reviewAutoAcceptEnabled() {
|
|
4749
|
+
try {
|
|
4750
|
+
const { loadConfig } = require('../utils/config');
|
|
4751
|
+
const config = loadConfig();
|
|
4752
|
+
const value = Object.prototype.hasOwnProperty.call(config, 'autoaccept')
|
|
4753
|
+
? config.autoaccept
|
|
4754
|
+
: Object.prototype.hasOwnProperty.call(config, 'review_autoaccept')
|
|
4755
|
+
? config.review_autoaccept
|
|
4756
|
+
: config.reviewAutoaccept;
|
|
4757
|
+
return !configValueDisabled(value);
|
|
4758
|
+
} catch {
|
|
4759
|
+
return true;
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
function reviewAutoAcceptStatePath(root = process.cwd()) {
|
|
4764
|
+
return path.join(root, '.atris', 'state', 'review-autoaccept.json');
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
function readReviewAutoAcceptState(root = process.cwd()) {
|
|
4768
|
+
const file = reviewAutoAcceptStatePath(root);
|
|
4769
|
+
try {
|
|
4770
|
+
if (!fs.existsSync(file)) return {};
|
|
4771
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4772
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
4773
|
+
} catch {
|
|
4774
|
+
return {};
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
function writeReviewAutoAcceptState(root = process.cwd(), state = {}) {
|
|
4779
|
+
const file = reviewAutoAcceptStatePath(root);
|
|
4780
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
4781
|
+
fs.writeFileSync(file, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
function finiteNumber(value) {
|
|
4785
|
+
const number = Number(value);
|
|
4786
|
+
return Number.isFinite(number) ? number : null;
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
function firstNumberFromObject(source, keys) {
|
|
4790
|
+
if (!source || typeof source !== 'object') return null;
|
|
4791
|
+
for (const key of keys) {
|
|
4792
|
+
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
|
|
4793
|
+
const value = source[key];
|
|
4794
|
+
if (Array.isArray(value)) return value.length;
|
|
4795
|
+
if (typeof value === 'string' && value.includes(',')) {
|
|
4796
|
+
const parts = value.split(',').map(part => part.trim()).filter(Boolean);
|
|
4797
|
+
if (parts.length > 1) return parts.length;
|
|
4798
|
+
}
|
|
4799
|
+
const number = finiteNumber(value);
|
|
4800
|
+
if (number !== null) return number;
|
|
4801
|
+
}
|
|
4802
|
+
return null;
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
function arrayFromPathValue(value) {
|
|
4806
|
+
if (!value) return [];
|
|
4807
|
+
if (Array.isArray(value)) return value.flatMap(arrayFromPathValue);
|
|
4808
|
+
if (typeof value === 'object') {
|
|
4809
|
+
if (typeof value.path === 'string') return [value.path];
|
|
4810
|
+
if (typeof value.file === 'string') return [value.file];
|
|
4811
|
+
if (typeof value.filename === 'string') return [value.filename];
|
|
4812
|
+
return [];
|
|
4813
|
+
}
|
|
4814
|
+
return String(value)
|
|
4815
|
+
.split(/[\n,]+/)
|
|
4816
|
+
.map(part => part.trim())
|
|
4817
|
+
.filter(Boolean);
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
function collectRecordedDiffPaths(source) {
|
|
4821
|
+
if (!source || typeof source !== 'object') return [];
|
|
4822
|
+
const keys = [
|
|
4823
|
+
'files',
|
|
4824
|
+
'paths',
|
|
4825
|
+
'touched_files',
|
|
4826
|
+
'touchedFiles',
|
|
4827
|
+
'changed_files',
|
|
4828
|
+
'changedFiles',
|
|
4829
|
+
'modified_files',
|
|
4830
|
+
'modifiedFiles',
|
|
4831
|
+
];
|
|
4832
|
+
const out = [];
|
|
4833
|
+
for (const key of keys) out.push(...arrayFromPathValue(source[key]));
|
|
4834
|
+
return out;
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
function normalizeRecordedDiffStats(source, label = 'recorded') {
|
|
4838
|
+
if (!source || typeof source !== 'object') return null;
|
|
4839
|
+
const fileKeys = [
|
|
4840
|
+
'files_touched',
|
|
4841
|
+
'filesTouched',
|
|
4842
|
+
'changed_files_count',
|
|
4843
|
+
'changedFilesCount',
|
|
4844
|
+
'files_changed',
|
|
4845
|
+
'filesChanged',
|
|
4846
|
+
'file_count',
|
|
4847
|
+
'fileCount',
|
|
4848
|
+
'files',
|
|
4849
|
+
'changed_files',
|
|
4850
|
+
'changedFiles',
|
|
4851
|
+
];
|
|
4852
|
+
const changedLineKeys = [
|
|
4853
|
+
'changed_lines',
|
|
4854
|
+
'changedLines',
|
|
4855
|
+
'lines_changed',
|
|
4856
|
+
'linesChanged',
|
|
4857
|
+
'changed_line_count',
|
|
4858
|
+
'changedLineCount',
|
|
4859
|
+
'total_changed_lines',
|
|
4860
|
+
'totalChangedLines',
|
|
4861
|
+
'line_count',
|
|
4862
|
+
'lineCount',
|
|
4863
|
+
];
|
|
4864
|
+
const insertions = firstNumberFromObject(source, ['insertions', 'added', 'additions', 'lines_added', 'linesAdded']);
|
|
4865
|
+
const deletions = firstNumberFromObject(source, ['deletions', 'deleted', 'removals', 'lines_deleted', 'linesDeleted']);
|
|
4866
|
+
const paths = collectRecordedDiffPaths(source);
|
|
4867
|
+
const filesTouched = firstNumberFromObject(source, fileKeys) ?? (paths.length ? paths.length : null);
|
|
4868
|
+
const changedLines = firstNumberFromObject(source, changedLineKeys)
|
|
4869
|
+
?? (insertions !== null || deletions !== null ? Number(insertions || 0) + Number(deletions || 0) : null);
|
|
4870
|
+
if (filesTouched === null && changedLines === null && !paths.length) return null;
|
|
4871
|
+
return {
|
|
4872
|
+
source: label,
|
|
4873
|
+
files_touched: filesTouched,
|
|
4874
|
+
changed_lines: changedLines,
|
|
4875
|
+
paths,
|
|
4876
|
+
};
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
function recordedDiffStatsForTask(task) {
|
|
4880
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
4881
|
+
const candidates = [
|
|
4882
|
+
metadata.diff_stats,
|
|
4883
|
+
metadata.diffStats,
|
|
4884
|
+
metadata.git_diff_stats,
|
|
4885
|
+
metadata.gitDiffStats,
|
|
4886
|
+
metadata.change_stats,
|
|
4887
|
+
metadata.changeStats,
|
|
4888
|
+
metadata.stats,
|
|
4889
|
+
metadata,
|
|
4890
|
+
];
|
|
4891
|
+
for (const candidate of candidates) {
|
|
4892
|
+
const stats = normalizeRecordedDiffStats(candidate, 'recorded');
|
|
4893
|
+
if (stats) return stats;
|
|
4894
|
+
}
|
|
4895
|
+
return null;
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4898
|
+
function proofDiffStats(proof) {
|
|
4899
|
+
const text = String(proof || '').replace(/\s+/g, ' ').trim();
|
|
4900
|
+
if (!text) return null;
|
|
4901
|
+
let filesTouched = null;
|
|
4902
|
+
let changedLines = null;
|
|
4903
|
+
const gitStat = text.match(/(\d+)\s+files?\s+changed(?:,\s*(\d+)\s+insertions?\(\+\))?(?:,\s*(\d+)\s+deletions?\(-\))?/i);
|
|
4904
|
+
if (gitStat) {
|
|
4905
|
+
filesTouched = Number(gitStat[1]);
|
|
4906
|
+
const insertions = gitStat[2] ? Number(gitStat[2]) : 0;
|
|
4907
|
+
const deletions = gitStat[3] ? Number(gitStat[3]) : 0;
|
|
4908
|
+
if (gitStat[2] || gitStat[3]) changedLines = insertions + deletions;
|
|
4909
|
+
}
|
|
4910
|
+
const filePatterns = [
|
|
4911
|
+
/files?\s+(?:touched|changed|modified)\s*[:=]\s*(\d+)/i,
|
|
4912
|
+
/(\d+)\s+files?\s+(?:touched|changed|modified)/i,
|
|
4913
|
+
];
|
|
4914
|
+
for (const pattern of filePatterns) {
|
|
4915
|
+
const match = text.match(pattern);
|
|
4916
|
+
if (match) filesTouched = Number(match[1]);
|
|
4917
|
+
}
|
|
4918
|
+
const linePatterns = [
|
|
4919
|
+
/changed\s+lines?\s*[:=]\s*(\d+)/i,
|
|
4920
|
+
/(\d+)\s+changed\s+lines?/i,
|
|
4921
|
+
/lines?\s+changed\s*[:=]\s*(\d+)/i,
|
|
4922
|
+
];
|
|
4923
|
+
for (const pattern of linePatterns) {
|
|
4924
|
+
const match = text.match(pattern);
|
|
4925
|
+
if (match) changedLines = Number(match[1]);
|
|
4926
|
+
}
|
|
4927
|
+
if (filesTouched === null && changedLines === null) return null;
|
|
4928
|
+
return {
|
|
4929
|
+
source: 'proof',
|
|
4930
|
+
files_touched: filesTouched,
|
|
4931
|
+
changed_lines: changedLines,
|
|
4932
|
+
paths: [],
|
|
4933
|
+
};
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
function safeGitRef(value) {
|
|
4937
|
+
const text = String(value || '').trim();
|
|
4938
|
+
if (!text || text.startsWith('-') || text.includes('..')) return null;
|
|
4939
|
+
if (!/^[a-zA-Z0-9_./@=+~^-]+$/.test(text)) return null;
|
|
4940
|
+
return text;
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4943
|
+
function runGitForAutoAccept(root, args) {
|
|
4944
|
+
try {
|
|
4945
|
+
const { spawnSync } = require('child_process');
|
|
4946
|
+
return spawnSync('git', args, {
|
|
4947
|
+
cwd: root || process.cwd(),
|
|
4948
|
+
encoding: 'utf8',
|
|
4949
|
+
timeout: 10000,
|
|
4950
|
+
});
|
|
4951
|
+
} catch {
|
|
4952
|
+
return { status: 1, stdout: '', stderr: '' };
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
function gitRefExists(root, ref) {
|
|
4957
|
+
const result = runGitForAutoAccept(root, ['rev-parse', '--verify', `${ref}^{commit}`]);
|
|
4958
|
+
return result.status === 0;
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
function defaultDiffBase(root) {
|
|
4962
|
+
for (const ref of ['origin/master', 'origin/main', 'master', 'main']) {
|
|
4963
|
+
if (gitRefExists(root, ref)) return ref;
|
|
4964
|
+
}
|
|
4965
|
+
return null;
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
function branchDiffStatsForTask(task, root) {
|
|
4969
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
4970
|
+
const branch = safeGitRef(
|
|
4971
|
+
metadata.branch
|
|
4972
|
+
|| metadata.worktree_branch
|
|
4973
|
+
|| metadata.git_branch
|
|
4974
|
+
|| metadata.pr_branch
|
|
4975
|
+
|| metadata.head_branch
|
|
4976
|
+
|| metadata.worktree?.branch
|
|
4977
|
+
);
|
|
4978
|
+
if (!branch) return null;
|
|
4979
|
+
const base = safeGitRef(
|
|
4980
|
+
metadata.base
|
|
4981
|
+
|| metadata.base_ref
|
|
4982
|
+
|| metadata.target_ref
|
|
4983
|
+
|| metadata.target_branch
|
|
4984
|
+
|| metadata.worktree?.base
|
|
4985
|
+
) || defaultDiffBase(root);
|
|
4986
|
+
if (!base) return null;
|
|
4987
|
+
const result = runGitForAutoAccept(root, ['diff', '--numstat', `${base}...${branch}`]);
|
|
4988
|
+
if (result.status !== 0) return null;
|
|
4989
|
+
const lines = String(result.stdout || '').split(/\r?\n/).filter(Boolean);
|
|
4990
|
+
if (!lines.length) return { source: 'branch', files_touched: 0, changed_lines: 0, paths: [] };
|
|
4991
|
+
let changedLines = 0;
|
|
4992
|
+
let unknownLines = false;
|
|
4993
|
+
const paths = [];
|
|
4994
|
+
for (const line of lines) {
|
|
4995
|
+
const parts = line.split(/\t+/);
|
|
4996
|
+
const added = Number(parts[0]);
|
|
4997
|
+
const deleted = Number(parts[1]);
|
|
4998
|
+
if (!Number.isFinite(added) || !Number.isFinite(deleted)) unknownLines = true;
|
|
4999
|
+
else changedLines += added + deleted;
|
|
5000
|
+
if (parts[2]) paths.push(parts.slice(2).join('\t'));
|
|
5001
|
+
}
|
|
5002
|
+
return {
|
|
5003
|
+
source: 'branch',
|
|
5004
|
+
files_touched: lines.length,
|
|
5005
|
+
changed_lines: unknownLines ? null : changedLines,
|
|
5006
|
+
paths,
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
|
|
5010
|
+
function reviewAutoAcceptDiffStats(task, root) {
|
|
5011
|
+
const recorded = recordedDiffStatsForTask(task);
|
|
5012
|
+
if (recorded) return recorded;
|
|
5013
|
+
const branch = branchDiffStatsForTask(task, root);
|
|
5014
|
+
if (branch) return branch;
|
|
5015
|
+
const proof = String(task?.review?.proof || task?.metadata?.latest_agent_proof || '');
|
|
5016
|
+
return proofDiffStats(proof);
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
function reviewAutoAcceptBigTitle(task) {
|
|
5020
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
5021
|
+
const text = [
|
|
5022
|
+
task && task.title,
|
|
5023
|
+
metadata.kind,
|
|
5024
|
+
metadata.type,
|
|
5025
|
+
metadata.category,
|
|
5026
|
+
metadata.report_type,
|
|
5027
|
+
].map(value => String(value || '')).join(' ').toLowerCase();
|
|
5028
|
+
const match = text.match(/\b(daily\s+update|summary|report|digest|retro|retrospective)\b/i);
|
|
5029
|
+
if (!match) return null;
|
|
5030
|
+
return {
|
|
5031
|
+
ok: false,
|
|
5032
|
+
reason: `big_title_${match[1].toLowerCase().replace(/\s+/g, '_')}`,
|
|
5033
|
+
};
|
|
5034
|
+
}
|
|
5035
|
+
|
|
5036
|
+
function reviewAutoAcceptSizeGate(task, root) {
|
|
5037
|
+
const titleGate = reviewAutoAcceptBigTitle(task);
|
|
5038
|
+
if (titleGate) return titleGate;
|
|
5039
|
+
const stats = reviewAutoAcceptDiffStats(task, root);
|
|
5040
|
+
if (!stats) return { ok: false, reason: 'size_unknown', stats: null };
|
|
5041
|
+
const filesTouched = finiteNumber(stats.files_touched);
|
|
5042
|
+
const changedLines = finiteNumber(stats.changed_lines);
|
|
5043
|
+
if (filesTouched !== null && filesTouched > REVIEW_AUTO_ACCEPT_FILE_LIMIT) {
|
|
5044
|
+
return { ok: false, reason: 'big_files', stats };
|
|
5045
|
+
}
|
|
5046
|
+
if (changedLines !== null && changedLines > REVIEW_AUTO_ACCEPT_LINE_LIMIT) {
|
|
5047
|
+
return { ok: false, reason: 'big_changed_lines', stats };
|
|
5048
|
+
}
|
|
5049
|
+
if (filesTouched === null || changedLines === null) {
|
|
5050
|
+
return { ok: false, reason: 'size_unknown', stats };
|
|
5051
|
+
}
|
|
5052
|
+
return { ok: true, reason: 'small_change', stats };
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
function reviewAutoAcceptMetadataText(task) {
|
|
5056
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
5057
|
+
return [
|
|
5058
|
+
task && task.title,
|
|
5059
|
+
task && task.tag,
|
|
5060
|
+
task && task.source_key,
|
|
5061
|
+
metadata.kind,
|
|
5062
|
+
metadata.type,
|
|
5063
|
+
metadata.category,
|
|
5064
|
+
metadata.lane,
|
|
5065
|
+
metadata.stage,
|
|
5066
|
+
metadata.area,
|
|
5067
|
+
JSON.stringify(metadata),
|
|
5068
|
+
].map(value => String(value || '')).join(' ').toLowerCase();
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
function reviewAutoAcceptTouchedPaths(task) {
|
|
5072
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
5073
|
+
const stats = recordedDiffStatsForTask(task);
|
|
5074
|
+
const proof = String(task?.review?.proof || metadata.latest_agent_proof || '');
|
|
5075
|
+
return [
|
|
5076
|
+
...(stats && stats.paths ? stats.paths : []),
|
|
5077
|
+
...collectRecordedDiffPaths(metadata),
|
|
5078
|
+
...taskReviewEvidencePaths(proof, 50),
|
|
5079
|
+
].map(value => String(value || '').trim()).filter(Boolean);
|
|
5080
|
+
}
|
|
5081
|
+
|
|
5082
|
+
function reviewProtectedMatch(task) {
|
|
5083
|
+
const text = reviewAutoAcceptMetadataText(task);
|
|
5084
|
+
const paths = reviewAutoAcceptTouchedPaths(task).join(' ').toLowerCase();
|
|
5085
|
+
const combined = `${text} ${paths}`;
|
|
5086
|
+
const checks = [
|
|
5087
|
+
['auth', /\b(auth|authentication|authorization|oauth|login|session)\b/i],
|
|
5088
|
+
['credentials', /\b(credentials?|secrets?|passwords?|api[-_ ]?keys?|tokens?)\b/i],
|
|
5089
|
+
['csp', /\b(csp|content[-_ ]security[-_ ]policy)\b/i],
|
|
5090
|
+
['sandbox', /\b(sandbox|allow-scripts|allow-same-origin)\b/i],
|
|
5091
|
+
['billing', /\b(billing|invoice|invoices|subscription|subscriptions)\b/i],
|
|
5092
|
+
['payments', /\b(payments?|stripe|checkout|refunds?)\b/i],
|
|
5093
|
+
['outbound_sends', /\b(outbound|send|sending|email|sms|webhook|notification|notifications)\b/i],
|
|
5094
|
+
];
|
|
5095
|
+
for (const [key, pattern] of checks) {
|
|
5096
|
+
if (pattern.test(combined)) return { ok: false, reason: `protected_${key}` };
|
|
5097
|
+
}
|
|
5098
|
+
return { ok: true };
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
function evaluateReviewAutoAccept(task, root) {
|
|
5102
|
+
const ref = taskRef(task);
|
|
5103
|
+
if (!task) return { eligible: false, ref, reason: 'task_not_found' };
|
|
5104
|
+
const protectedGate = reviewProtectedMatch(task);
|
|
5105
|
+
if (!protectedGate.ok) return { eligible: false, ref, reason: protectedGate.reason };
|
|
5106
|
+
const sizeGate = reviewAutoAcceptSizeGate(task, root);
|
|
5107
|
+
if (!sizeGate.ok) return { eligible: false, ref, reason: sizeGate.reason, size: sizeGate.stats };
|
|
5108
|
+
const evaluation = evaluateAutoAccept(task, { strictVerify: true });
|
|
5109
|
+
if (!evaluation.eligible) {
|
|
5110
|
+
return {
|
|
5111
|
+
...evaluation,
|
|
5112
|
+
size: sizeGate.stats,
|
|
5113
|
+
};
|
|
5114
|
+
}
|
|
5115
|
+
return {
|
|
5116
|
+
...evaluation,
|
|
5117
|
+
eligible: true,
|
|
5118
|
+
reason: 'certified_small',
|
|
5119
|
+
policy: REVIEW_AUTO_ACCEPT_POLICY,
|
|
5120
|
+
size: sizeGate.stats,
|
|
5121
|
+
};
|
|
5122
|
+
}
|
|
5123
|
+
|
|
5124
|
+
function autoAcceptCertifiedSmallReviews(taskDb, db, projection) {
|
|
5125
|
+
const enabled = reviewAutoAcceptEnabled();
|
|
5126
|
+
const root = projection.workspace_root || process.cwd();
|
|
5127
|
+
const results = [];
|
|
5128
|
+
if (!enabled) {
|
|
5129
|
+
return {
|
|
5130
|
+
enabled,
|
|
5131
|
+
scanned: 0,
|
|
5132
|
+
accepted: 0,
|
|
5133
|
+
changed: false,
|
|
5134
|
+
results,
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
5137
|
+
const certified = certifiedPendingReviewTasks(projection);
|
|
5138
|
+
for (const item of certified) {
|
|
5139
|
+
const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
|
|
5140
|
+
const task = fullProjection.tasks[0] || null;
|
|
5141
|
+
const evaluation = evaluateReviewAutoAccept(task, root);
|
|
5142
|
+
if (!evaluation.eligible) {
|
|
5143
|
+
results.push({ ...evaluation, action: 'queued', task_id: task?.id || item.id || null });
|
|
5144
|
+
continue;
|
|
5145
|
+
}
|
|
5146
|
+
const accepted = acceptReviewTask(taskDb, db, task.id, {
|
|
5147
|
+
actor: REVIEW_AUTO_ACCEPT_ACTOR,
|
|
5148
|
+
proof: evaluation.proof,
|
|
5149
|
+
reward: 1,
|
|
5150
|
+
lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
|
|
5151
|
+
nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
|
|
5152
|
+
autoAccepted: true,
|
|
5153
|
+
});
|
|
5154
|
+
if (!accepted.ok) {
|
|
5155
|
+
results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
|
|
5156
|
+
continue;
|
|
5157
|
+
}
|
|
5158
|
+
stampAutoAcceptMetadata(taskDb, db, task.id, REVIEW_AUTO_ACCEPT_ACTOR, REVIEW_AUTO_ACCEPT_POLICY);
|
|
5159
|
+
refreshCareerXpAfterReview(accepted.reviewed);
|
|
5160
|
+
results.push({
|
|
5161
|
+
...evaluation,
|
|
5162
|
+
action: 'accepted',
|
|
5163
|
+
task_id: task.id,
|
|
5164
|
+
reward: accepted.reviewed.episode.reward.value,
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
5167
|
+
const acceptedCount = results.filter(row => row.action === 'accepted').length;
|
|
5168
|
+
return {
|
|
5169
|
+
enabled,
|
|
5170
|
+
scanned: certified.length,
|
|
5171
|
+
accepted: acceptedCount,
|
|
5172
|
+
changed: acceptedCount > 0,
|
|
5173
|
+
results,
|
|
5174
|
+
};
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5177
|
+
function autoAcceptedReviewRowsSince(taskDb, db, workspaceRoot, sinceIso, acceptedNowIds = []) {
|
|
5178
|
+
const sinceMs = Date.parse(sinceIso || '');
|
|
5179
|
+
const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot }));
|
|
5180
|
+
const acceptedNow = new Set(acceptedNowIds.filter(Boolean));
|
|
5181
|
+
const seen = new Set();
|
|
5182
|
+
const out = [];
|
|
5183
|
+
for (const row of rows) {
|
|
5184
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
|
|
5185
|
+
const acceptedAt = Date.parse(metadata.auto_accepted_at || metadata.accepted_at || '');
|
|
5186
|
+
const policyMatch = metadata.auto_accept_policy === REVIEW_AUTO_ACCEPT_POLICY
|
|
5187
|
+
|| metadata.auto_accepted_by === REVIEW_AUTO_ACCEPT_ACTOR
|
|
5188
|
+
|| metadata.accepted_by === REVIEW_AUTO_ACCEPT_ACTOR;
|
|
5189
|
+
const isCurrent = acceptedNow.has(row.id);
|
|
5190
|
+
const isSince = Number.isFinite(sinceMs) && Number.isFinite(acceptedAt) && acceptedAt > sinceMs;
|
|
5191
|
+
const firstLook = !Number.isFinite(sinceMs) && Number.isFinite(acceptedAt);
|
|
5192
|
+
if (row.status !== 'done' || !policyMatch || (!isCurrent && !isSince && !firstLook)) continue;
|
|
5193
|
+
if (seen.has(row.id)) continue;
|
|
5194
|
+
seen.add(row.id);
|
|
5195
|
+
out.push({
|
|
5196
|
+
id: row.id,
|
|
5197
|
+
ref: row.display_id || taskRef(row),
|
|
5198
|
+
title: row.title,
|
|
5199
|
+
accepted_at: metadata.auto_accepted_at || metadata.accepted_at || null,
|
|
5200
|
+
});
|
|
5201
|
+
}
|
|
5202
|
+
out.sort((a, b) => String(b.accepted_at || '').localeCompare(String(a.accepted_at || '')));
|
|
5203
|
+
return out;
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
function reviewAutoAcceptRollup(taskDb, db, workspaceRoot, previousState, autoAcceptRun) {
|
|
5207
|
+
const acceptedNowIds = (autoAcceptRun.results || [])
|
|
5208
|
+
.filter(row => row.action === 'accepted')
|
|
5209
|
+
.map(row => row.task_id)
|
|
5210
|
+
.filter(Boolean);
|
|
5211
|
+
const rows = autoAcceptedReviewRowsSince(taskDb, db, workspaceRoot, previousState.last_look_at, acceptedNowIds);
|
|
5212
|
+
return {
|
|
5213
|
+
count: rows.length,
|
|
5214
|
+
items: rows,
|
|
5215
|
+
since: previousState.last_look_at || null,
|
|
5216
|
+
};
|
|
5217
|
+
}
|
|
5218
|
+
|
|
4420
5219
|
function reviewQueueLimit(args, total) {
|
|
4421
5220
|
if (hasFlag(args, '--all')) return total;
|
|
4422
5221
|
const raw = flag(args, '--limit');
|
|
@@ -4435,6 +5234,17 @@ function reviewGroupTextLimit(args, total) {
|
|
|
4435
5234
|
return Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 10;
|
|
4436
5235
|
}
|
|
4437
5236
|
|
|
5237
|
+
function plainReviewBlockerMessage(item) {
|
|
5238
|
+
const ref = item.display_id || taskRef(item.id);
|
|
5239
|
+
if (item.reason === 'verify_command_not_allowed') {
|
|
5240
|
+
return `${ref} used a check command the system does not allow; rerun with an approved check`;
|
|
5241
|
+
}
|
|
5242
|
+
if (item.reason === 'needs_second_actor_review') {
|
|
5243
|
+
return `${ref} needs a second reviewer before it can land`;
|
|
5244
|
+
}
|
|
5245
|
+
return `${ref} needs another check before it can land`;
|
|
5246
|
+
}
|
|
5247
|
+
|
|
4438
5248
|
// Risk order for human attention: named receipts that are missing/failing (0)
|
|
4439
5249
|
// beat prose-only proofs (1) beat fully validated green evidence (2).
|
|
4440
5250
|
function evidenceRiskRank(evidence) {
|
|
@@ -4488,7 +5298,20 @@ function reviewQueueItem(task, root = process.cwd(), evidence = undefined) {
|
|
|
4488
5298
|
return item;
|
|
4489
5299
|
}
|
|
4490
5300
|
|
|
4491
|
-
function
|
|
5301
|
+
function blockedReviewQueueItem(task, root = process.cwd()) {
|
|
5302
|
+
const item = reviewQueueItem(task, root);
|
|
5303
|
+
const blocker = reviewBlockerForTask(task);
|
|
5304
|
+
item.queue_role = 'blocked';
|
|
5305
|
+
item.reason = blocker.reason;
|
|
5306
|
+
item.blocked_reason = blocker.reason;
|
|
5307
|
+
item.next_command = blocker.next_command;
|
|
5308
|
+
item.verify_command = blocker.verify_command;
|
|
5309
|
+
item.accept_command = null;
|
|
5310
|
+
item.land_command = null;
|
|
5311
|
+
return item;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
function reviewQueueHygiene(tasks) {
|
|
4492
5315
|
const genericContinuations = (tasks || []).map(task => {
|
|
4493
5316
|
const issues = genericContinuationIssues(task);
|
|
4494
5317
|
if (!issues.length) return null;
|
|
@@ -4526,7 +5349,10 @@ function taskReviewQueue(projection, args = []) {
|
|
|
4526
5349
|
const ordered = [...certified].sort((a, b) =>
|
|
4527
5350
|
evidenceRiskRank(evidenceByTaskId.get(b.id)) - evidenceRiskRank(evidenceByTaskId.get(a.id))
|
|
4528
5351
|
|| Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
|
4529
|
-
const
|
|
5352
|
+
const certifiedItems = ordered.slice(0, limit).map((task) => reviewQueueItem(task, root, evidenceByTaskId.get(task.id)));
|
|
5353
|
+
const blockedLimit = reviewQueueLimit(args, blocking.length);
|
|
5354
|
+
const blockedItems = blocking.slice(0, blockedLimit).map((task) => blockedReviewQueueItem(task, root));
|
|
5355
|
+
const items = [...certifiedItems, ...blockedItems];
|
|
4530
5356
|
return {
|
|
4531
5357
|
schema: 'atris.task_review_queue.v1',
|
|
4532
5358
|
generated_at: projection.generated_at,
|
|
@@ -4537,7 +5363,8 @@ function taskReviewQueue(projection, args = []) {
|
|
|
4537
5363
|
evidence_passing: certified.filter((task) => evidenceByTaskId.get(task.id)?.all_passing).length,
|
|
4538
5364
|
blocking: blocking.length,
|
|
4539
5365
|
proof_boundary_blocked: proofBoundaryBlocked.length,
|
|
4540
|
-
shown:
|
|
5366
|
+
shown: certifiedItems.length,
|
|
5367
|
+
blocking_shown: blockedItems.length,
|
|
4541
5368
|
},
|
|
4542
5369
|
hygiene: reviewQueueHygiene(reviewTasks),
|
|
4543
5370
|
items,
|
|
@@ -4595,30 +5422,72 @@ function taskReviewGroups(projection, key) {
|
|
|
4595
5422
|
};
|
|
4596
5423
|
}
|
|
4597
5424
|
|
|
5425
|
+
function taskReviewLandingLines(item) {
|
|
5426
|
+
if (!item?.landing) return [];
|
|
5427
|
+
const clean = value => value
|
|
5428
|
+
? gateForHuman(value, { title: item.title }).text
|
|
5429
|
+
: '';
|
|
5430
|
+
const happened = clean(item.landing.happened);
|
|
5431
|
+
const reason = clean(item.landing.reason);
|
|
5432
|
+
const unperiod = value => value.replace(/\.$/, '');
|
|
5433
|
+
const checked = unperiod(clean(item.landing.checked));
|
|
5434
|
+
const tested = unperiod(clean(item.landing.tested));
|
|
5435
|
+
return [
|
|
5436
|
+
happened ? ` what's new: ${happened}` : '',
|
|
5437
|
+
reason ? ` why it matters: ${reason}` : '',
|
|
5438
|
+
checked ? ` checked: ${checked}${tested && tested !== checked ? `; tested: ${tested}` : ''}.` : '',
|
|
5439
|
+
].filter(Boolean);
|
|
5440
|
+
}
|
|
5441
|
+
|
|
4598
5442
|
function cmdReviews(args) {
|
|
4599
5443
|
const taskDb = getTaskDb();
|
|
4600
5444
|
const db = taskDb.open();
|
|
4601
|
-
|
|
5445
|
+
let { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
5446
|
+
const workspaceRoot = projection.workspace_root || process.cwd();
|
|
5447
|
+
const previousAutoAcceptState = readReviewAutoAcceptState(workspaceRoot);
|
|
5448
|
+
const autoAcceptRun = autoAcceptCertifiedSmallReviews(taskDb, db, projection);
|
|
5449
|
+
if (autoAcceptRun.changed) {
|
|
5450
|
+
({ projection, outPath } = writeDefaultProjection(taskDb, db));
|
|
5451
|
+
}
|
|
5452
|
+
const autoAcceptRollup = reviewAutoAcceptRollup(taskDb, db, workspaceRoot, previousAutoAcceptState, autoAcceptRun);
|
|
5453
|
+
writeReviewAutoAcceptState(workspaceRoot, {
|
|
5454
|
+
...previousAutoAcceptState,
|
|
5455
|
+
last_look_at: new Date().toISOString(),
|
|
5456
|
+
last_count: autoAcceptRollup.count,
|
|
5457
|
+
});
|
|
4602
5458
|
const groupByRaw = flag(args, '--group-by');
|
|
4603
5459
|
if (groupByRaw) {
|
|
4604
5460
|
const key = reviewGroupKey(groupByRaw);
|
|
4605
5461
|
const groups = taskReviewGroups(projection, key);
|
|
4606
5462
|
if (wantsJson(args)) {
|
|
4607
|
-
printJson({
|
|
5463
|
+
printJson({
|
|
5464
|
+
ok: true,
|
|
5465
|
+
action: 'review_groups',
|
|
5466
|
+
projection_path: outPath,
|
|
5467
|
+
autoaccept: {
|
|
5468
|
+
enabled: autoAcceptRun.enabled,
|
|
5469
|
+
accepted_now: autoAcceptRun.accepted,
|
|
5470
|
+
accepted_since_last_look: autoAcceptRollup.count,
|
|
5471
|
+
results: autoAcceptRun.results,
|
|
5472
|
+
},
|
|
5473
|
+
groups,
|
|
5474
|
+
});
|
|
4608
5475
|
return;
|
|
4609
5476
|
}
|
|
4610
|
-
console.log(
|
|
4611
|
-
|
|
5477
|
+
console.log(gateForHuman(`${numberWord(groups.total_certified)} finished things are waiting, grouped by ${key}.`).text);
|
|
5478
|
+
if (autoAcceptRollup.count > 0) {
|
|
5479
|
+
console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
|
|
5480
|
+
}
|
|
4612
5481
|
const visibleGroups = groups.groups.slice(0, reviewGroupTextLimit(args, groups.groups.length));
|
|
4613
5482
|
visibleGroups.forEach((g, index) => {
|
|
4614
5483
|
console.log('');
|
|
4615
|
-
console.log(`${index + 1}. ${g.value}
|
|
4616
|
-
g.sample_titles.forEach(title => console.log(` • ${title}`));
|
|
5484
|
+
console.log(`${index + 1}. ${g.value} - ${numberWord(g.count)} task${g.count === 1 ? '' : 's'}`);
|
|
5485
|
+
g.sample_titles.forEach(title => console.log(` • ${gateForHuman(title, { title }).text}`));
|
|
4617
5486
|
console.log(` approve this group: ${g.accept_group_command} --confirm-human-accept --as <you>`);
|
|
4618
5487
|
});
|
|
4619
5488
|
if (visibleGroups.length < groups.groups.length) {
|
|
4620
5489
|
console.log('');
|
|
4621
|
-
console.log(`
|
|
5490
|
+
console.log(`showing ${numberWord(visibleGroups.length)} of ${numberWord(groups.groups.length)} groups; rerun with --all for every group or --limit N to adjust.`);
|
|
4622
5491
|
}
|
|
4623
5492
|
return;
|
|
4624
5493
|
}
|
|
@@ -4629,30 +5498,45 @@ function cmdReviews(args) {
|
|
|
4629
5498
|
ok: true,
|
|
4630
5499
|
action: 'review_queue',
|
|
4631
5500
|
projection_path: outPath,
|
|
5501
|
+
autoaccept: {
|
|
5502
|
+
enabled: autoAcceptRun.enabled,
|
|
5503
|
+
accepted_now: autoAcceptRun.accepted,
|
|
5504
|
+
accepted_since_last_look: autoAcceptRollup.count,
|
|
5505
|
+
results: autoAcceptRun.results,
|
|
5506
|
+
},
|
|
4632
5507
|
queue,
|
|
4633
5508
|
});
|
|
4634
5509
|
return;
|
|
4635
5510
|
}
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
if (!
|
|
4639
|
-
console.log('
|
|
5511
|
+
const approvalItems = queue.items.filter(item => item.queue_role !== 'blocked');
|
|
5512
|
+
const blockedItems = queue.items.filter(item => item.queue_role === 'blocked');
|
|
5513
|
+
if (!approvalItems.length && !blockedItems.length) {
|
|
5514
|
+
console.log('nothing is waiting on you. everything that finished has already landed.');
|
|
5515
|
+
if (autoAcceptRollup.count > 0) {
|
|
5516
|
+
console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
|
|
5517
|
+
}
|
|
4640
5518
|
return;
|
|
4641
5519
|
}
|
|
4642
|
-
queue.
|
|
4643
|
-
const
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
console.log(
|
|
4647
|
-
|
|
5520
|
+
if (queue.counts.certified > 0) {
|
|
5521
|
+
const header = queue.counts.certified === 1
|
|
5522
|
+
? 'one finished thing is waiting for your ok. it passed both checks.'
|
|
5523
|
+
: `${numberWord(queue.counts.certified)} finished things are waiting for your ok. all of them passed both checks.`;
|
|
5524
|
+
console.log(gateForHuman(header).text);
|
|
5525
|
+
}
|
|
5526
|
+
if (queue.counts.blocking > 0) {
|
|
5527
|
+
console.log(gateForHuman(queue.counts.blocking === 1
|
|
5528
|
+
? 'one more is almost ready; a second check is still running.'
|
|
5529
|
+
: `${numberWord(queue.counts.blocking)} more are almost ready; second checks are still running.`).text);
|
|
5530
|
+
}
|
|
5531
|
+
if (autoAcceptRollup.count > 0) {
|
|
5532
|
+
console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
|
|
5533
|
+
}
|
|
5534
|
+
approvalItems.forEach((item, index) => {
|
|
5535
|
+
if (index > 0) console.log('');
|
|
5536
|
+
console.log(`${index + 1}. ${gateForHuman(item.title, { title: item.title }).text}`);
|
|
4648
5537
|
if (item.landing) {
|
|
4649
|
-
console.log(
|
|
4650
|
-
if (item.
|
|
4651
|
-
if (item.landing.reason) console.log(` Why it matters: ${item.landing.reason}`);
|
|
4652
|
-
if (item.landing.checked) console.log(` How I checked: ${item.landing.checked}`);
|
|
4653
|
-
if (item.landing.tested) console.log(` What I tested: ${item.landing.tested}`);
|
|
4654
|
-
if (item.result?.saved) console.log(` Saved: ${item.result.saved}`);
|
|
4655
|
-
if (item.landing.decision) console.log(` Decision: ${item.landing.decision}`);
|
|
5538
|
+
taskReviewLandingLines(item).forEach(line => console.log(line));
|
|
5539
|
+
if (verbose && item.result?.saved) console.log(` saved: ${item.result.saved}`);
|
|
4656
5540
|
}
|
|
4657
5541
|
if (verbose && item.proof) console.log(` details: ${item.proof}`);
|
|
4658
5542
|
if (verbose && item.evidence) {
|
|
@@ -4664,14 +5548,23 @@ function cmdReviews(args) {
|
|
|
4664
5548
|
item.evidence.missing.forEach((missingPath) => console.log(` receipt: ${missingPath} MISSING`));
|
|
4665
5549
|
}
|
|
4666
5550
|
if (verbose && item.review_chat_command) console.log(` /codex: ${item.review_chat_command}`);
|
|
4667
|
-
if (item.
|
|
4668
|
-
|
|
4669
|
-
else if (item.blocked_accept_reason)
|
|
4670
|
-
|
|
5551
|
+
if (item.accept_command) {
|
|
5552
|
+
console.log(` say yes: atris task accept ${item.display_id || taskRef(item.id)}`);
|
|
5553
|
+
} else if (item.blocked_accept_reason) {
|
|
5554
|
+
console.log(` approve: blocked (${item.blocked_accept_reason})`);
|
|
5555
|
+
console.log(` rework: ${item.revise_command}`);
|
|
5556
|
+
}
|
|
4671
5557
|
});
|
|
5558
|
+
if (blockedItems.length > 0) {
|
|
5559
|
+
if (approvalItems.length > 0) console.log('');
|
|
5560
|
+
console.log('still being checked:');
|
|
5561
|
+
for (const item of blockedItems) {
|
|
5562
|
+
console.log(`${plainReviewBlockerMessage(item)}; next: ${item.next_command}`);
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
4672
5565
|
if (queue.counts.shown < queue.counts.certified) {
|
|
4673
5566
|
console.log('');
|
|
4674
|
-
console.log(`
|
|
5567
|
+
console.log(`showing ${numberWord(queue.counts.shown)} of ${numberWord(queue.counts.certified)}; rerun with --all for every row or --verbose for proof details.`);
|
|
4675
5568
|
}
|
|
4676
5569
|
}
|
|
4677
5570
|
|
|
@@ -4786,6 +5679,11 @@ function cmdAcceptGroup(args) {
|
|
|
4786
5679
|
const isVerified = verifiedIds.has(task.id);
|
|
4787
5680
|
const proof = String(task.review?.proof || task.metadata?.latest_agent_proof || '').trim()
|
|
4788
5681
|
|| `Accepted via group spot-check (${groupLabel}); human ${actor} verified ${verifiedIds.size}/${group.length}.`;
|
|
5682
|
+
const missionXpIssue = missionXpEndToEndProofIssue(task, proof, task.workspace_root || taskDb.workspaceRoot());
|
|
5683
|
+
if (missionXpIssue) {
|
|
5684
|
+
accepted.push({ id: task.id, ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue });
|
|
5685
|
+
continue;
|
|
5686
|
+
}
|
|
4789
5687
|
const done = taskDb.doneTask(db, {
|
|
4790
5688
|
id: task.id,
|
|
4791
5689
|
status: 'done',
|
|
@@ -4867,19 +5765,21 @@ function formatTaskLine(task) {
|
|
|
4867
5765
|
|
|
4868
5766
|
function cmdStatus(args) {
|
|
4869
5767
|
const all = hasFlag(args, '--all');
|
|
5768
|
+
const everywhere = taskScopeEverywhere(args);
|
|
4870
5769
|
const history = hasFlag(args, '--history');
|
|
4871
5770
|
const taskDb = getTaskDb();
|
|
4872
5771
|
const db = taskDb.open();
|
|
4873
|
-
const
|
|
5772
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
5773
|
+
const compact = writeDefaultProjection(taskDb, db, { all, everywhere });
|
|
4874
5774
|
const projection = history
|
|
4875
5775
|
? enrichTaskProjection(taskDb.taskProjection(db, {
|
|
4876
|
-
workspaceRoot
|
|
4877
|
-
limit: 500,
|
|
5776
|
+
workspaceRoot,
|
|
5777
|
+
limit: all ? null : 500,
|
|
4878
5778
|
includeHistory: true,
|
|
4879
5779
|
}))
|
|
4880
5780
|
: compact.projection;
|
|
4881
5781
|
const outPath = compact.outPath;
|
|
4882
|
-
const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(taskDb, db,
|
|
5782
|
+
const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(taskDb, db, workspaceRoot);
|
|
4883
5783
|
const status = taskStatusSummary(projection, { history, hasExistingReviewFollowUp });
|
|
4884
5784
|
if (wantsJson(args)) {
|
|
4885
5785
|
printJson({
|
|
@@ -4936,13 +5836,13 @@ function requireTaskId(taskDb, db, ref, label) {
|
|
|
4936
5836
|
}
|
|
4937
5837
|
}
|
|
4938
5838
|
|
|
4939
|
-
function workspaceRefRows(taskDb, db,
|
|
4940
|
-
return taskDb.listTasks(db, { workspaceRoot:
|
|
5839
|
+
function workspaceRefRows(taskDb, db, options = {}) {
|
|
5840
|
+
return taskDb.listTasks(db, { workspaceRoot: scopedWorkspaceRoot(taskDb, [], options) });
|
|
4941
5841
|
}
|
|
4942
5842
|
|
|
4943
5843
|
function renderTaskDesk(rows, refRows = rows) {
|
|
4944
5844
|
const displayRows = getTaskDb().withTaskDisplayRefs(rows, refRows);
|
|
4945
|
-
const active = displayRows.filter(r => r.status !== 'done');
|
|
5845
|
+
const active = displayRows.filter(r => r.status !== 'done' && r.status !== 'archived');
|
|
4946
5846
|
const done = displayRows.filter(r => r.status === 'done');
|
|
4947
5847
|
if (rows.length === 0) {
|
|
4948
5848
|
console.log('No tasks yet.');
|
|
@@ -5033,7 +5933,7 @@ function delegateHandoff(task, owner, via, tag) {
|
|
|
5033
5933
|
return handoff;
|
|
5034
5934
|
}
|
|
5035
5935
|
|
|
5036
|
-
function
|
|
5936
|
+
function delegateTask(args, options = {}) {
|
|
5037
5937
|
const pos = positional(args);
|
|
5038
5938
|
const title = pos.join(' ').trim();
|
|
5039
5939
|
if (!title) {
|
|
@@ -5051,7 +5951,7 @@ function cmdDelegate(args) {
|
|
|
5051
5951
|
const taskDb = getTaskDb();
|
|
5052
5952
|
const db = taskDb.open();
|
|
5053
5953
|
const ws = taskDb.workspaceRoot();
|
|
5054
|
-
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
|
|
5954
|
+
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title, { print: options.warnOperatorTitle !== false });
|
|
5055
5955
|
const ownerResolution = resolveFunctionalTaskOwner({
|
|
5056
5956
|
requestedOwner: requestedOwner && requestedOwner !== true ? requestedOwner : null,
|
|
5057
5957
|
title,
|
|
@@ -5096,34 +5996,69 @@ function cmdDelegate(args) {
|
|
|
5096
5996
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
5097
5997
|
const task = compactTaskFromProjection(projection, result.id);
|
|
5098
5998
|
const handoff = delegateHandoff(task, owner, via, typeof tag === 'string' ? tag : null);
|
|
5999
|
+
return {
|
|
6000
|
+
ok: true,
|
|
6001
|
+
action: 'delegated',
|
|
6002
|
+
task_id: result.id,
|
|
6003
|
+
inserted: result.inserted !== false,
|
|
6004
|
+
owner,
|
|
6005
|
+
owner_resolution: ownerResolution,
|
|
6006
|
+
executed_by: executedBy || null,
|
|
6007
|
+
via,
|
|
6008
|
+
tag: typeof tag === 'string' ? tag : null,
|
|
6009
|
+
handoff,
|
|
6010
|
+
proposed_member_command: metadata.proposed_member_command || null,
|
|
6011
|
+
operator_title_warning: operatorTitleWarning,
|
|
6012
|
+
projection_path: outPath,
|
|
6013
|
+
task,
|
|
6014
|
+
};
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
function cmdDelegate(args) {
|
|
6018
|
+
const payload = delegateTask(args);
|
|
5099
6019
|
if (wantsJson(args)) {
|
|
5100
|
-
printJson(
|
|
5101
|
-
ok: true,
|
|
5102
|
-
action: 'delegated',
|
|
5103
|
-
task_id: result.id,
|
|
5104
|
-
inserted: result.inserted !== false,
|
|
5105
|
-
owner,
|
|
5106
|
-
owner_resolution: ownerResolution,
|
|
5107
|
-
executed_by: executedBy || null,
|
|
5108
|
-
via,
|
|
5109
|
-
handoff,
|
|
5110
|
-
operator_title_warning: operatorTitleWarning,
|
|
5111
|
-
projection_path: outPath,
|
|
5112
|
-
task,
|
|
5113
|
-
});
|
|
6020
|
+
printJson(payload);
|
|
5114
6021
|
return;
|
|
5115
6022
|
}
|
|
5116
|
-
const tagText = tag
|
|
5117
|
-
console.log(`delegated ${taskRef(task)} -> ${owner}${tagText} via=${via}`);
|
|
5118
|
-
if (
|
|
5119
|
-
if (
|
|
5120
|
-
console.log(`claim: ${handoff.command}`);
|
|
5121
|
-
if (handoff.swarlo) console.log(`swarlo: ${handoff.swarlo.channel}/${handoff.swarlo.action}`);
|
|
6023
|
+
const tagText = payload.tag ? ` #${payload.tag}` : '';
|
|
6024
|
+
console.log(`delegated ${taskRef(payload.task)} -> ${payload.owner}${tagText} via=${payload.via}`);
|
|
6025
|
+
if (payload.executed_by) console.log(`executed_by: ${payload.executed_by}`);
|
|
6026
|
+
if (payload.proposed_member_command) console.log(`member: ${payload.proposed_member_command}`);
|
|
6027
|
+
console.log(`claim: ${payload.handoff.command}`);
|
|
6028
|
+
if (payload.handoff.swarlo) console.log(`swarlo: ${payload.handoff.swarlo.channel}/${payload.handoff.swarlo.action}`);
|
|
5122
6029
|
}
|
|
5123
6030
|
|
|
5124
6031
|
// Failed tasks older than this stop earning a daily owner-group row;
|
|
5125
6032
|
// they collapse into one stale summary line instead (target state = clean day view).
|
|
5126
6033
|
const DAY_STALE_FAILED_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6034
|
+
const DAY_TEXT_TASK_LIMIT = 8;
|
|
6035
|
+
|
|
6036
|
+
function taskDayTextGroups(groups, { full = false } = {}) {
|
|
6037
|
+
if (full) return { groups, hiddenTasks: 0, hiddenOwners: 0 };
|
|
6038
|
+
const statusOrder = { claimed: 0, open: 1, review: 2, failed: 3, done: 4 };
|
|
6039
|
+
const ranked = groups
|
|
6040
|
+
.flatMap((group) => group.tasks)
|
|
6041
|
+
.sort((a, b) => ((statusOrder[a.status] ?? 5) - (statusOrder[b.status] ?? 5))
|
|
6042
|
+
|| ((b.updated_at || 0) - (a.updated_at || 0)));
|
|
6043
|
+
const selected = new Set(ranked.slice(0, DAY_TEXT_TASK_LIMIT));
|
|
6044
|
+
const visibleGroups = groups
|
|
6045
|
+
.map((group) => ({ ...group, tasks: group.tasks.filter((task) => selected.has(task)) }))
|
|
6046
|
+
.filter((group) => group.tasks.length > 0);
|
|
6047
|
+
return {
|
|
6048
|
+
groups: visibleGroups,
|
|
6049
|
+
hiddenTasks: Math.max(0, ranked.length - selected.size),
|
|
6050
|
+
hiddenOwners: Math.max(0, groups.length - visibleGroups.length),
|
|
6051
|
+
};
|
|
6052
|
+
}
|
|
6053
|
+
|
|
6054
|
+
function taskDayTitle(title, maxLength = 120) {
|
|
6055
|
+
const text = String(title || '').replace(/\s*[—–]\s*/g, ' - ').replace(/\s+/g, ' ').trim();
|
|
6056
|
+
if (text.length <= maxLength) return text;
|
|
6057
|
+
const sentence = text.slice(0, maxLength + 1).match(/^(.{48,}?[.!?])(?:\s|$)/);
|
|
6058
|
+
if (sentence) return sentence[1];
|
|
6059
|
+
const clipped = text.slice(0, maxLength).replace(/\s+\S*$/, '').trim();
|
|
6060
|
+
return `${clipped || text.slice(0, maxLength).trim()}...`;
|
|
6061
|
+
}
|
|
5127
6062
|
|
|
5128
6063
|
function taskDayGroups(tasks, { now = Date.now() } = {}) {
|
|
5129
6064
|
const active = tasks.filter(task => task.status !== 'done');
|
|
@@ -5158,9 +6093,11 @@ function taskDayGroups(tasks, { now = Date.now() } = {}) {
|
|
|
5158
6093
|
|
|
5159
6094
|
function cmdDay(args) {
|
|
5160
6095
|
const all = hasFlag(args, '--all');
|
|
6096
|
+
const full = hasFlag(args, '--full');
|
|
6097
|
+
const everywhere = taskScopeEverywhere(args);
|
|
5161
6098
|
const taskDb = getTaskDb();
|
|
5162
6099
|
const db = taskDb.open();
|
|
5163
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
|
|
6100
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
|
|
5164
6101
|
const { groups, staleFailed } = taskDayGroups(projection.tasks || []);
|
|
5165
6102
|
const counts = {
|
|
5166
6103
|
active: groups.reduce((sum, group) => sum + group.tasks.length, 0),
|
|
@@ -5187,24 +6124,30 @@ function cmdDay(args) {
|
|
|
5187
6124
|
});
|
|
5188
6125
|
return;
|
|
5189
6126
|
}
|
|
5190
|
-
|
|
6127
|
+
const textView = taskDayTextGroups(groups, { full });
|
|
6128
|
+
console.log('task day');
|
|
5191
6129
|
const failedText = counts.failed > 0 ? ` / failed ${counts.failed}` : '';
|
|
5192
6130
|
console.log(`${date} active ${counts.active} / owners ${counts.owners} / review ${counts.review}${failedText}`);
|
|
5193
6131
|
console.log('');
|
|
5194
6132
|
if (!groups.length) {
|
|
5195
6133
|
console.log('clear no active tasks');
|
|
5196
6134
|
}
|
|
5197
|
-
for (const group of groups) {
|
|
6135
|
+
for (const group of textView.groups) {
|
|
5198
6136
|
console.log(`${group.owner}`);
|
|
5199
|
-
for (const task of group.tasks
|
|
6137
|
+
for (const task of group.tasks) {
|
|
5200
6138
|
const tag = task.tag ? ` #${task.tag}` : '';
|
|
5201
6139
|
const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
|
|
5202
|
-
console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${task.title}`);
|
|
6140
|
+
console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${taskDayTitle(task.title)}`);
|
|
5203
6141
|
}
|
|
5204
6142
|
}
|
|
6143
|
+
if (textView.hiddenTasks > 0) {
|
|
6144
|
+
console.log('');
|
|
6145
|
+
const ownerText = textView.hiddenOwners > 0 ? `, ${textView.hiddenOwners} owners not shown` : '';
|
|
6146
|
+
console.log(`more ${textView.hiddenTasks} active rows hidden${ownerText} - atris task day --full`);
|
|
6147
|
+
}
|
|
5205
6148
|
if (staleFailed.length) {
|
|
5206
6149
|
console.log('');
|
|
5207
|
-
console.log(`stale ${staleFailed.length} failed >7d hidden
|
|
6150
|
+
console.log(`stale ${staleFailed.length} failed >7d hidden - atris task list --status failed`);
|
|
5208
6151
|
}
|
|
5209
6152
|
console.log('');
|
|
5210
6153
|
console.log('add: atris task delegate "..." --to task-planner --tag tasks');
|
|
@@ -5212,13 +6155,15 @@ function cmdDay(args) {
|
|
|
5212
6155
|
|
|
5213
6156
|
function cmdHome(args) {
|
|
5214
6157
|
const all = hasFlag(args, '--all');
|
|
6158
|
+
const everywhere = taskScopeEverywhere(args);
|
|
5215
6159
|
const taskDb = getTaskDb();
|
|
5216
6160
|
const db = taskDb.open();
|
|
6161
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
5217
6162
|
const rows = taskDb.listTasks(db, {
|
|
5218
|
-
workspaceRoot
|
|
5219
|
-
limit: 200,
|
|
6163
|
+
workspaceRoot,
|
|
6164
|
+
limit: all ? null : 200,
|
|
5220
6165
|
});
|
|
5221
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
|
|
6166
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
|
|
5222
6167
|
if (wantsJson(args)) {
|
|
5223
6168
|
printJson({
|
|
5224
6169
|
ok: true,
|
|
@@ -5235,18 +6180,20 @@ function cmdHome(args) {
|
|
|
5235
6180
|
|
|
5236
6181
|
function cmdList(args) {
|
|
5237
6182
|
const all = hasFlag(args, '--all');
|
|
6183
|
+
const everywhere = taskScopeEverywhere(args);
|
|
5238
6184
|
const status = flag(args, '--status');
|
|
5239
6185
|
const scope = taskQueueScopeFromArgs(args);
|
|
5240
6186
|
const scoped = !taskQueueScopeIsEmpty(scope);
|
|
5241
6187
|
const taskDb = getTaskDb();
|
|
5242
6188
|
const db = taskDb.open();
|
|
6189
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
5243
6190
|
const rawRows = taskDb.listTasks(db, {
|
|
5244
|
-
workspaceRoot
|
|
6191
|
+
workspaceRoot,
|
|
5245
6192
|
status: typeof status === 'string' ? status : null,
|
|
5246
|
-
limit: scoped ? null : 200,
|
|
6193
|
+
limit: scoped || all ? null : 200,
|
|
5247
6194
|
});
|
|
5248
6195
|
const rows = filterTasksByScope(rawRows, scope);
|
|
5249
|
-
const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db,
|
|
6196
|
+
const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, { everywhere }));
|
|
5250
6197
|
if (wantsJson(args)) {
|
|
5251
6198
|
printJson({ ok: true, action: 'list', scope: normalizeTaskQueueScope(scope), tasks: displayRows });
|
|
5252
6199
|
return;
|
|
@@ -5263,12 +6210,49 @@ function cmdList(args) {
|
|
|
5263
6210
|
}
|
|
5264
6211
|
}
|
|
5265
6212
|
|
|
6213
|
+
// judge != worker support: reserved system names (autoland-verifier and co)
|
|
6214
|
+
// can never be assumed via --as, and unknown names warn by default or fail
|
|
6215
|
+
// under ATRIS_ACTOR_VALIDATION=enforce. Only explicit --as values are
|
|
6216
|
+
// checked; the DEFAULT_OWNER fallback stays silent.
|
|
6217
|
+
function guardExplicitActor(command, value) {
|
|
6218
|
+
if (typeof value !== 'string' || !value.trim()) return;
|
|
6219
|
+
const check = reviewIntegrity.validateActor(value, { root: process.cwd() });
|
|
6220
|
+
if (!check.ok) {
|
|
6221
|
+
if (check.reason === 'reserved_actor') {
|
|
6222
|
+
console.error(`${command}: reserved_actor: '${value}' is a system actor and cannot be used with --as`);
|
|
6223
|
+
} else {
|
|
6224
|
+
console.error(`${command}: actor_not_on_roster: '${value}' is not a workspace member or engine (actor validation is enforced)`);
|
|
6225
|
+
}
|
|
6226
|
+
process.exit(1);
|
|
6227
|
+
}
|
|
6228
|
+
if (check.reason === 'actor_not_on_roster' && check.mode === 'warn') {
|
|
6229
|
+
console.error(`Warning: '${value}' is not a workspace member or engine; reviews under unknown names weaken the audit trail.`);
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
|
|
6233
|
+
// A claim against an already-done task burns a whole dispatch when a
|
|
6234
|
+
// rendered view (atris/TODO.md) is stale: the agent claims, builds, then
|
|
6235
|
+
// discovers the work was already done. Point straight at a real open task
|
|
6236
|
+
// instead of just reporting the failure, straight from the live projection
|
|
6237
|
+
// (never the rendered file), preferring the same tag when one is open.
|
|
6238
|
+
function suggestNextClaimableTask(projection, { excludeId = null, tag = '' } = {}) {
|
|
6239
|
+
const open = (projection && projection.tasks || []).filter((t) => t && t.status === 'open' && t.id !== excludeId);
|
|
6240
|
+
if (!open.length) return null;
|
|
6241
|
+
const normalizedTag = String(tag || '').trim().toLowerCase();
|
|
6242
|
+
if (normalizedTag) {
|
|
6243
|
+
const sameTag = open.find((t) => String(t.tag || '').trim().toLowerCase() === normalizedTag);
|
|
6244
|
+
if (sameTag) return sameTag;
|
|
6245
|
+
}
|
|
6246
|
+
return open[0];
|
|
6247
|
+
}
|
|
6248
|
+
|
|
5266
6249
|
function cmdClaim(args) {
|
|
5267
6250
|
const pos = positional(args);
|
|
5268
6251
|
const id = pos[0];
|
|
5269
6252
|
if (!id) {
|
|
5270
6253
|
failTask('atris task claim', 'missing_id', 'id required');
|
|
5271
6254
|
}
|
|
6255
|
+
guardExplicitActor('atris task claim', flag(args, '--as'));
|
|
5272
6256
|
const owner = flag(args, '--as') || DEFAULT_OWNER;
|
|
5273
6257
|
const taskDb = getTaskDb();
|
|
5274
6258
|
const db = taskDb.open();
|
|
@@ -5287,11 +6271,21 @@ function cmdClaim(args) {
|
|
|
5287
6271
|
});
|
|
5288
6272
|
return;
|
|
5289
6273
|
}
|
|
5290
|
-
|
|
6274
|
+
const ref = taskRef(compactTaskFromProjection(projection, taskId));
|
|
6275
|
+
console.log(`claimed ${ref} as ${owner}`);
|
|
6276
|
+
console.log(`Next: make the change, then run: atris task ready ${ref} --verify "git diff --check" --result "<who can do what now and why>" --landing "<what someone can do now>"`);
|
|
6277
|
+
console.log('Use a different verifier only if autoland can rerun it, such as `node --test <file>`.');
|
|
6278
|
+
console.log('Then: atris autoland tick');
|
|
5291
6279
|
} else {
|
|
5292
6280
|
const recoveryCommand = result.reason === 'already_claimed' && result.claimed_by
|
|
5293
6281
|
? `atris task release ${id} --as ${result.claimed_by}`
|
|
5294
6282
|
: null;
|
|
6283
|
+
let nextClaimable = null;
|
|
6284
|
+
if (result.reason === 'already_done') {
|
|
6285
|
+
const doneRow = taskDb.getTask(db, taskId);
|
|
6286
|
+
const { projection } = writeDefaultProjection(taskDb, db);
|
|
6287
|
+
nextClaimable = suggestNextClaimableTask(projection, { excludeId: taskId, tag: doneRow && doneRow.tag });
|
|
6288
|
+
}
|
|
5295
6289
|
if (wantsJson(args)) {
|
|
5296
6290
|
printJson({
|
|
5297
6291
|
ok: false,
|
|
@@ -5299,12 +6293,16 @@ function cmdClaim(args) {
|
|
|
5299
6293
|
reason: result.reason,
|
|
5300
6294
|
claimed_by: result.claimed_by || null,
|
|
5301
6295
|
recovery_command: recoveryCommand,
|
|
6296
|
+
next_claimable: nextClaimable ? { id: nextClaimable.id, ref: taskRef(nextClaimable), tag: nextClaimable.tag || null, title: nextClaimable.title } : null,
|
|
5302
6297
|
detail: `claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
|
|
5303
6298
|
});
|
|
5304
6299
|
process.exit(1);
|
|
5305
6300
|
}
|
|
5306
6301
|
console.error(`claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
|
|
5307
6302
|
if (recoveryCommand) console.error(`Recovery: ${recoveryCommand}`);
|
|
6303
|
+
if (nextClaimable) {
|
|
6304
|
+
console.error(`next claimable: ${taskRef(nextClaimable)} ${String(nextClaimable.title || '').slice(0, 80)} (atris task claim ${taskRef(nextClaimable)} --as ${owner})`);
|
|
6305
|
+
}
|
|
5308
6306
|
process.exit(1);
|
|
5309
6307
|
}
|
|
5310
6308
|
}
|
|
@@ -5503,7 +6501,7 @@ function cmdNext(args) {
|
|
|
5503
6501
|
const continueWorkCommand = handoff.next_action === 'continue_work'
|
|
5504
6502
|
? continueWorkCommandForTask(reviewTask, { owner })
|
|
5505
6503
|
: null;
|
|
5506
|
-
const nextAgentAction = handoff.next_action === 'human_accept_waiting'
|
|
6504
|
+
const nextAgentAction = handoff.next_action === 'human_accept_waiting' && !scoped
|
|
5507
6505
|
? readEndgameAgentAction(taskDb.workspaceRoot(), owner, { tasks: projection.tasks || [] })
|
|
5508
6506
|
: null;
|
|
5509
6507
|
if (hasFlag(args, '--create-next')) {
|
|
@@ -5724,6 +6722,131 @@ function cmdNote(args) {
|
|
|
5724
6722
|
console.log(`noted ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
|
|
5725
6723
|
}
|
|
5726
6724
|
|
|
6725
|
+
function cmdRetitle(args) {
|
|
6726
|
+
const pos = positional(args);
|
|
6727
|
+
const id = pos[0];
|
|
6728
|
+
const title = pos.slice(1).join(' ').trim();
|
|
6729
|
+
if (!id) failTask('atris task retitle', 'missing_id', 'task id required');
|
|
6730
|
+
if (!title) failTask('atris task retitle', 'missing_title', 'new title required');
|
|
6731
|
+
warnIfTaskTitleNeedsOperatorWhy(title);
|
|
6732
|
+
|
|
6733
|
+
const actor = flag(args, '--as') || DEFAULT_OWNER;
|
|
6734
|
+
const taskDb = getTaskDb();
|
|
6735
|
+
const db = taskDb.open();
|
|
6736
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task retitle');
|
|
6737
|
+
const current = taskDb.getTask(db, taskId);
|
|
6738
|
+
const oldTitle = String(current.title || '');
|
|
6739
|
+
const now = Math.max(Date.now(), Number(current.updated_at || 0) + 1);
|
|
6740
|
+
const updated = db.prepare(`
|
|
6741
|
+
UPDATE tasks
|
|
6742
|
+
SET title = ?,
|
|
6743
|
+
updated_at = ?
|
|
6744
|
+
WHERE id = ?
|
|
6745
|
+
AND updated_at = ?
|
|
6746
|
+
`).run(title, now, taskId, current.updated_at);
|
|
6747
|
+
if (updated.changes !== 1) {
|
|
6748
|
+
failTask('atris task retitle', 'stale_task_state', 'retitle failed: stale task state', 1);
|
|
6749
|
+
}
|
|
6750
|
+
const history = taskDb.noteTask(db, {
|
|
6751
|
+
id: taskId,
|
|
6752
|
+
actor: String(actor),
|
|
6753
|
+
content: `previous title: ${oldTitle}`,
|
|
6754
|
+
});
|
|
6755
|
+
if (!history.noted) {
|
|
6756
|
+
failTask('atris task retitle', history.reason || 'history_failed', `retitle history failed: ${history.reason || 'unknown'}`, 1);
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6759
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
6760
|
+
const task = compactTaskFromProjection(projection, taskId);
|
|
6761
|
+
if (wantsJson(args)) {
|
|
6762
|
+
printJson({
|
|
6763
|
+
ok: true,
|
|
6764
|
+
action: 'retitled',
|
|
6765
|
+
task_id: taskId,
|
|
6766
|
+
old_title: oldTitle,
|
|
6767
|
+
title,
|
|
6768
|
+
version: history.event.version,
|
|
6769
|
+
projection_path: outPath,
|
|
6770
|
+
task,
|
|
6771
|
+
});
|
|
6772
|
+
return;
|
|
6773
|
+
}
|
|
6774
|
+
console.log(`retitled ${taskRef(task)}: ${title}`);
|
|
6775
|
+
}
|
|
6776
|
+
|
|
6777
|
+
// Collect EVERY value for a repeatable flag (flag() only returns the first),
|
|
6778
|
+
// so `--add a --add b` and `--add a,b` both work.
|
|
6779
|
+
function collectFlagValues(args, name) {
|
|
6780
|
+
const values = [];
|
|
6781
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
6782
|
+
if (args[i] === name && i + 1 < args.length && !String(args[i + 1]).startsWith('--')) {
|
|
6783
|
+
values.push(args[i + 1]);
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
return values
|
|
6787
|
+
.flatMap((value) => String(value).split(','))
|
|
6788
|
+
.map((value) => value.trim())
|
|
6789
|
+
.filter(Boolean);
|
|
6790
|
+
}
|
|
6791
|
+
|
|
6792
|
+
function cmdTag(args) {
|
|
6793
|
+
const pos = positional(args);
|
|
6794
|
+
const id = pos[0];
|
|
6795
|
+
if (!id) failTask('atris task tag', 'missing_id', 'task id required');
|
|
6796
|
+
const add = collectFlagValues(args, '--add');
|
|
6797
|
+
const remove = collectFlagValues(args, '--remove');
|
|
6798
|
+
if (!add.length && !remove.length) {
|
|
6799
|
+
failTask('atris task tag', 'missing_tags', 'at least one --add <tag> or --remove <tag> required');
|
|
6800
|
+
}
|
|
6801
|
+
const actor = flag(args, '--as') || DEFAULT_OWNER;
|
|
6802
|
+
const taskDb = getTaskDb();
|
|
6803
|
+
const db = taskDb.open();
|
|
6804
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task tag');
|
|
6805
|
+
const result = taskDb.tagTask(db, { id: taskId, actor: String(actor), add, remove });
|
|
6806
|
+
if (!result.tagged) {
|
|
6807
|
+
if (result.reason === 'no_changes') {
|
|
6808
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
6809
|
+
const task = compactTaskFromProjection(projection, taskId);
|
|
6810
|
+
if (wantsJson(args)) {
|
|
6811
|
+
printJson({
|
|
6812
|
+
ok: true,
|
|
6813
|
+
action: 'unchanged',
|
|
6814
|
+
task_id: taskId,
|
|
6815
|
+
added: [],
|
|
6816
|
+
removed: [],
|
|
6817
|
+
tags: result.tags || [],
|
|
6818
|
+
projection_path: outPath,
|
|
6819
|
+
task,
|
|
6820
|
+
});
|
|
6821
|
+
return;
|
|
6822
|
+
}
|
|
6823
|
+
console.log(`no change ${taskRef(task)} tags [${(result.tags || []).join(', ')}]`);
|
|
6824
|
+
return;
|
|
6825
|
+
}
|
|
6826
|
+
failTask('atris task tag', result.reason || 'tag_failed', `tag failed: ${result.reason || 'unknown'}`, 1);
|
|
6827
|
+
}
|
|
6828
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
6829
|
+
const task = compactTaskFromProjection(projection, taskId);
|
|
6830
|
+
if (wantsJson(args)) {
|
|
6831
|
+
printJson({
|
|
6832
|
+
ok: true,
|
|
6833
|
+
action: 'tagged',
|
|
6834
|
+
task_id: taskId,
|
|
6835
|
+
added: result.added,
|
|
6836
|
+
removed: result.removed,
|
|
6837
|
+
tags: result.tags,
|
|
6838
|
+
version: result.event.version,
|
|
6839
|
+
projection_path: outPath,
|
|
6840
|
+
task,
|
|
6841
|
+
});
|
|
6842
|
+
return;
|
|
6843
|
+
}
|
|
6844
|
+
const parts = [];
|
|
6845
|
+
if (result.added.length) parts.push(`+${result.added.join(' +')}`);
|
|
6846
|
+
if (result.removed.length) parts.push(`-${result.removed.join(' -')}`);
|
|
6847
|
+
console.log(`tagged ${taskRef(task)} ${parts.join(' ')} -> [${result.tags.join(', ')}] v${result.event.version}`);
|
|
6848
|
+
}
|
|
6849
|
+
|
|
5727
6850
|
function cmdChat(args) {
|
|
5728
6851
|
const pos = positional(args);
|
|
5729
6852
|
const id = pos[0];
|
|
@@ -6014,7 +7137,7 @@ function cmdShow(args) {
|
|
|
6014
7137
|
const owner = task.claimed_by ? ` / ${task.claimed_by}` : '';
|
|
6015
7138
|
const tag = task.tag ? ` #${task.tag}` : '';
|
|
6016
7139
|
const statusLabel = task.status === 'review'
|
|
6017
|
-
? '
|
|
7140
|
+
? 'ready for approval'
|
|
6018
7141
|
: task.status === 'done'
|
|
6019
7142
|
? 'DONE'
|
|
6020
7143
|
: task.status.toUpperCase();
|
|
@@ -6774,9 +7897,38 @@ function cmdResult(args) {
|
|
|
6774
7897
|
const pos = positional(args);
|
|
6775
7898
|
const id = pos[0];
|
|
6776
7899
|
if (!id) failTask('atris task result', 'missing_id', 'id required');
|
|
7900
|
+
const sentence = pos.slice(1).join(' ').trim();
|
|
7901
|
+
if (sentence) {
|
|
7902
|
+
const resultSentence = requireResultSentence('atris task result', sentence);
|
|
7903
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
7904
|
+
const taskDb = getTaskDb();
|
|
7905
|
+
const db = taskDb.open();
|
|
7906
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task result');
|
|
7907
|
+
const saved = taskDb.setTaskResult(db, {
|
|
7908
|
+
id: taskId,
|
|
7909
|
+
actor,
|
|
7910
|
+
result: resultSentence,
|
|
7911
|
+
});
|
|
7912
|
+
if (!saved.saved) failTask('atris task result', saved.reason || 'result_failed', `result failed: ${saved.reason || 'result_failed'}`, 1);
|
|
7913
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
7914
|
+
if (wantsJson(args)) {
|
|
7915
|
+
printJson({
|
|
7916
|
+
ok: true,
|
|
7917
|
+
action: 'result',
|
|
7918
|
+
task_id: taskId,
|
|
7919
|
+
version: saved.event.version,
|
|
7920
|
+
result: resultSentence,
|
|
7921
|
+
projection_path: outPath,
|
|
7922
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
7923
|
+
});
|
|
7924
|
+
return;
|
|
7925
|
+
}
|
|
7926
|
+
console.log(`result saved ${taskRef(compactTaskFromProjection(projection, taskId))}: ${resultSentence}`);
|
|
7927
|
+
return;
|
|
7928
|
+
}
|
|
6777
7929
|
const fields = {
|
|
6778
7930
|
purpose: textFlag(args, ['--purpose', '--goal', '--objective']),
|
|
6779
|
-
changed: textFlag(args, ['--changed', '--
|
|
7931
|
+
changed: textFlag(args, ['--changed', '--done']),
|
|
6780
7932
|
checked: textFlag(args, ['--checked', '--check', '--verified']),
|
|
6781
7933
|
passed: textFlag(args, ['--passed', '--pass']),
|
|
6782
7934
|
failed: textFlag(args, ['--failed', '--fail']),
|
|
@@ -6886,7 +8038,7 @@ function taskPageActions(task, { reviewer = 'codex-review', hasExistingReviewFol
|
|
|
6886
8038
|
note_command: `atris task note ${ref} "<context>" --as ${owner}`,
|
|
6887
8039
|
plan_command: `atris task plan ${ref} --goal ${taskCommandQuote(goal)} --exit "<exit condition>" --proof-needed "<verification command>" --first-move "<first move>"`,
|
|
6888
8040
|
do_command: `atris task do ${ref} --as ${owner} --first-move "<first move>"`,
|
|
6889
|
-
ready_command: `atris task ready ${ref} --as ${owner} --proof "<specific proof command/result>" --happened "<what happened>" --checked "<how you know>" --tested "<what you ran or inspected>" --decision "<accept/rework guidance>"`,
|
|
8041
|
+
ready_command: `atris task ready ${ref} --as ${owner} --proof "<specific proof command/result>" --result "<one day-one PM sentence>" --happened "<what happened>" --checked "<how you know>" --tested "<what you ran or inspected>" --decision "<accept/rework guidance>"`,
|
|
6890
8042
|
review_command: `atris task review ${ref} --reward 0 --as ${actor} --proof "<specific proof command/result>" --verify "<safe verifier command>"`,
|
|
6891
8043
|
};
|
|
6892
8044
|
if (task && task.status === 'review') {
|
|
@@ -7322,9 +8474,14 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
|
|
|
7322
8474
|
if (proofIssue) {
|
|
7323
8475
|
throw taskStepError(proof ? 'weak_proof' : 'proof_required', `meaningful proof required: ${proofIssue}`, { status: 400, exitCode: 2, page: actionPage });
|
|
7324
8476
|
}
|
|
8477
|
+
const missionXpIssue = missionXpEndToEndProofIssue(task, proof, task.workspace_root || process.cwd());
|
|
8478
|
+
if (missionXpIssue) {
|
|
8479
|
+
throw taskStepError(MISSION_XP_END_TO_END_REASON, missionXpIssue, { status: 409, exitCode: 1, page: actionPage });
|
|
8480
|
+
}
|
|
7325
8481
|
const lesson = String(options.lesson || '');
|
|
7326
8482
|
const nextTask = String(options.nextTask || '');
|
|
7327
8483
|
const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, { actor, proof });
|
|
8484
|
+
const missionResult = missionReceiptResultForProof(task, proof, task.workspace_root || process.cwd());
|
|
7328
8485
|
const ready = taskDb.readyTask(db, {
|
|
7329
8486
|
id: taskId,
|
|
7330
8487
|
actor,
|
|
@@ -7332,6 +8489,8 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
|
|
|
7332
8489
|
lesson,
|
|
7333
8490
|
nextTask,
|
|
7334
8491
|
resultTrace: resultTrace && resultTrace.trace,
|
|
8492
|
+
result: missionResult ? missionResult.changed : undefined,
|
|
8493
|
+
reason: missionResult ? missionResult.reason : undefined,
|
|
7335
8494
|
});
|
|
7336
8495
|
if (!ready.ready) taskStepFailure('atris task step', ready, actionPage);
|
|
7337
8496
|
task = taskDetail(taskDb, db, taskId) || task;
|
|
@@ -7341,7 +8500,10 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
|
|
|
7341
8500
|
} else if (current === 'review' && task.status === 'review') {
|
|
7342
8501
|
const handoffState = reviewHandoffForTask(task, { suppressExistingFollowUp: true });
|
|
7343
8502
|
if (handoffState && handoffState.next_action === PROOF_BOUNDARY_BLOCKED_ACTION) {
|
|
7344
|
-
|
|
8503
|
+
const detail = handoffState.reason === MISSION_XP_END_TO_END_REASON
|
|
8504
|
+
? `atris task step: ${MISSION_XP_END_TO_END_DETAIL}`
|
|
8505
|
+
: 'atris task step: Review proof cites an open/draft/unmerged PR boundary; revise the row before further stepping';
|
|
8506
|
+
throw taskStepError(PROOF_BOUNDARY_BLOCKED_REASON, detail, { status: 409, exitCode: 1, page: actionPage });
|
|
7345
8507
|
}
|
|
7346
8508
|
if (handoffState && (handoffState.next_action === 'continue_work' || handoffState.next_action === 'human_accept_waiting')) {
|
|
7347
8509
|
const reason = handoffState.next_action === 'continue_work'
|
|
@@ -7466,9 +8628,13 @@ function runCurrentTaskStep(taskDb, db, { owner = DEFAULT_OWNER, reviewer = 'cod
|
|
|
7466
8628
|
throw error;
|
|
7467
8629
|
}
|
|
7468
8630
|
if (nextActionKey === PROOF_BOUNDARY_BLOCKED_ACTION) {
|
|
8631
|
+
const boundaryReason = current.page?.review?.handoff?.reason || current.selected?.review?.handoff?.reason || '';
|
|
8632
|
+
const detail = boundaryReason === MISSION_XP_END_TO_END_REASON
|
|
8633
|
+
? `atris task current-step: ${MISSION_XP_END_TO_END_DETAIL}`
|
|
8634
|
+
: 'atris task current-step: selected Review row has stale/open/draft/unmerged PR proof; revise it instead of accepting or auto-stepping';
|
|
7469
8635
|
const error = taskStepError(
|
|
7470
8636
|
PROOF_BOUNDARY_BLOCKED_REASON,
|
|
7471
|
-
|
|
8637
|
+
detail,
|
|
7472
8638
|
{
|
|
7473
8639
|
status: 409,
|
|
7474
8640
|
exitCode: 1,
|
|
@@ -7675,7 +8841,7 @@ function cmdDone(args) {
|
|
|
7675
8841
|
if (agentProofOnlyMode() && !failed) {
|
|
7676
8842
|
failAgentProofOnly(
|
|
7677
8843
|
'atris task done',
|
|
7678
|
-
'Agent proof-only mode cannot mark tasks done. Use `atris task ready <id> --proof "..."` or `atris task review <id> --reward 0 --proof "..."`.',
|
|
8844
|
+
'Agent proof-only mode cannot mark tasks done. Use `atris task ready <id> --proof "..." --result "<day-one PM sentence>"` or `atris task review <id> --reward 0 --proof "..."`.',
|
|
7679
8845
|
);
|
|
7680
8846
|
}
|
|
7681
8847
|
const canComplete = beforeTask && (beforeTask.status === 'open' || beforeTask.status === 'claimed');
|
|
@@ -7757,13 +8923,14 @@ function cmdFinish(args) {
|
|
|
7757
8923
|
if (agentProofOnlyMode() && !failed) {
|
|
7758
8924
|
failAgentProofOnly(
|
|
7759
8925
|
'atris task finish',
|
|
7760
|
-
'Agent proof-only mode cannot finish tasks. Use `atris task ready <id> --proof "..."` or `atris task review <id> --reward 0 --proof "..."`.',
|
|
8926
|
+
'Agent proof-only mode cannot finish tasks. Use `atris task ready <id> --proof "..." --result "<day-one PM sentence>"` or `atris task review <id> --reward 0 --proof "..."`.',
|
|
7761
8927
|
);
|
|
7762
8928
|
}
|
|
7763
8929
|
const canComplete = currentTask && (currentTask.status === 'open' || currentTask.status === 'claimed');
|
|
7764
8930
|
if (canComplete) {
|
|
7765
8931
|
if (!failed || hasReview) requireMeaningfulTaskProof('atris task finish', proof);
|
|
7766
8932
|
else if (proof) requireMeaningfulTaskProof('atris task finish', proof);
|
|
8933
|
+
if (!failed && hasReview) requireExplicitLandingDayOnePm('atris task finish', landing, currentTask.title);
|
|
7767
8934
|
}
|
|
7768
8935
|
const done = taskDb.doneTask(db, {
|
|
7769
8936
|
id: taskId,
|
|
@@ -7839,6 +9006,197 @@ function cmdFinish(args) {
|
|
|
7839
9006
|
console.log(`finished ${taskRef(compactTaskFromProjection(projection, taskId))}`);
|
|
7840
9007
|
}
|
|
7841
9008
|
|
|
9009
|
+
// Distinct from `done --failed`: a bulk sweep of duplicates/off-roadmap work
|
|
9010
|
+
// closes the task without ever claiming it did or didn't succeed. Writing
|
|
9011
|
+
// 'failed' here would corrupt the reward signal readers rely on (see
|
|
9012
|
+
// atris/reports/failed-tasks-analysis-2026-07-03.md, cluster 2, OBL-1622).
|
|
9013
|
+
function cmdArchive(args) {
|
|
9014
|
+
const pos = positional(args);
|
|
9015
|
+
const id = pos[0];
|
|
9016
|
+
if (!id) {
|
|
9017
|
+
failTask('atris task archive', 'missing_id', 'id required');
|
|
9018
|
+
}
|
|
9019
|
+
const reason = textFlag(args, ['--reason']);
|
|
9020
|
+
if (!reason) {
|
|
9021
|
+
failTask('atris task archive', 'missing_reason', 'atris task archive requires --reason "<why this is being swept, not failed>"');
|
|
9022
|
+
}
|
|
9023
|
+
const taskDb = getTaskDb();
|
|
9024
|
+
const db = taskDb.open();
|
|
9025
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task archive');
|
|
9026
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
9027
|
+
// Explicit opt-in for sanctioned failed→archived cleanup (e.g. duplicate
|
|
9028
|
+
// loop-tick orphans fail-closed before 'archived' existed). Without the
|
|
9029
|
+
// flag, failed rows stay failed; done rows are never archivable.
|
|
9030
|
+
const fromFailed = hasFlag(args, '--from-failed');
|
|
9031
|
+
const result = taskDb.archiveTask(db, { id: taskId, actor, reason, fromFailed });
|
|
9032
|
+
if (result.archived) {
|
|
9033
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
9034
|
+
if (wantsJson(args)) {
|
|
9035
|
+
printJson({
|
|
9036
|
+
ok: true,
|
|
9037
|
+
action: 'archived',
|
|
9038
|
+
task_id: taskId,
|
|
9039
|
+
reason,
|
|
9040
|
+
archived_from: result.row && result.row.metadata && result.row.metadata.archived_from || null,
|
|
9041
|
+
projection_path: outPath,
|
|
9042
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
9043
|
+
});
|
|
9044
|
+
return;
|
|
9045
|
+
}
|
|
9046
|
+
const fromNote = result.row && result.row.metadata && result.row.metadata.archived_from
|
|
9047
|
+
? ` (was ${result.row.metadata.archived_from})`
|
|
9048
|
+
: '';
|
|
9049
|
+
console.log(`archived ${taskRef(compactTaskFromProjection(projection, taskId))}${fromNote}: ${reason}`);
|
|
9050
|
+
} else {
|
|
9051
|
+
const hint = result.reason === 'already_failed'
|
|
9052
|
+
? ' (use --from-failed to archive a fail-closed duplicate/off-roadmap row)'
|
|
9053
|
+
: '';
|
|
9054
|
+
const detail = `archive failed: ${taskId} ${result.reason}${hint}`;
|
|
9055
|
+
if (wantsJson(args)) {
|
|
9056
|
+
printJson({ ok: false, command: 'atris task archive', reason: result.reason, task_id: taskId, detail });
|
|
9057
|
+
process.exit(1);
|
|
9058
|
+
}
|
|
9059
|
+
console.error(detail);
|
|
9060
|
+
process.exit(1);
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
function taskCompletionTime(row) {
|
|
9065
|
+
const doneAt = Number(row && row.done_at);
|
|
9066
|
+
if (Number.isFinite(doneAt) && doneAt > 0) return doneAt;
|
|
9067
|
+
const acceptedAt = Date.parse(String(row && row.metadata && row.metadata.accepted_at || ''));
|
|
9068
|
+
if (Number.isFinite(acceptedAt)) return acceptedAt;
|
|
9069
|
+
return Number(row && (row.updated_at || row.created_at) || 0);
|
|
9070
|
+
}
|
|
9071
|
+
|
|
9072
|
+
function cmdClearDone(args) {
|
|
9073
|
+
const beforeRaw = flag(args, '--before');
|
|
9074
|
+
let beforeDays = null;
|
|
9075
|
+
if (beforeRaw !== null) {
|
|
9076
|
+
beforeDays = Number(beforeRaw);
|
|
9077
|
+
if (beforeRaw === true || !Number.isFinite(beforeDays) || beforeDays < 0) {
|
|
9078
|
+
failTask('atris task clear-done', 'invalid_before', '--before requires a non-negative number of days');
|
|
9079
|
+
}
|
|
9080
|
+
}
|
|
9081
|
+
|
|
9082
|
+
const dryRun = hasFlag(args, '--dry-run');
|
|
9083
|
+
const taskDb = getTaskDb();
|
|
9084
|
+
const db = taskDb.open();
|
|
9085
|
+
const workspaceRoot = taskDb.workspaceRoot();
|
|
9086
|
+
const cutoff = beforeDays === null ? null : Date.now() - (beforeDays * 24 * 60 * 60 * 1000);
|
|
9087
|
+
const candidates = taskDb.listTasks(db, { workspaceRoot, status: 'done', limit: null })
|
|
9088
|
+
.filter(row => cutoff === null || taskCompletionTime(row) < cutoff)
|
|
9089
|
+
.sort((a, b) => taskCompletionTime(a) - taskCompletionTime(b) || String(a.id).localeCompare(String(b.id)));
|
|
9090
|
+
const sample = candidates.slice(0, 5).map(row => ({
|
|
9091
|
+
task_id: row.id,
|
|
9092
|
+
title: row.title,
|
|
9093
|
+
completed_at: new Date(taskCompletionTime(row)).toISOString(),
|
|
9094
|
+
}));
|
|
9095
|
+
|
|
9096
|
+
if (dryRun) {
|
|
9097
|
+
if (wantsJson(args)) {
|
|
9098
|
+
printJson({
|
|
9099
|
+
ok: true,
|
|
9100
|
+
action: 'clear-done',
|
|
9101
|
+
dry_run: true,
|
|
9102
|
+
before_days: beforeDays,
|
|
9103
|
+
count: candidates.length,
|
|
9104
|
+
sample,
|
|
9105
|
+
});
|
|
9106
|
+
return;
|
|
9107
|
+
}
|
|
9108
|
+
console.log(`clear-done dry-run: ${candidates.length} completed task(s) would be archived.`);
|
|
9109
|
+
for (const row of sample) console.log(` - ${row.title}`);
|
|
9110
|
+
if (candidates.length > sample.length) console.log(` ...and ${candidates.length - sample.length} more`);
|
|
9111
|
+
return;
|
|
9112
|
+
}
|
|
9113
|
+
|
|
9114
|
+
const reason = 'cleared by clear-done sweep';
|
|
9115
|
+
for (const row of candidates) {
|
|
9116
|
+
const result = taskDb.archiveTask(db, {
|
|
9117
|
+
id: row.id,
|
|
9118
|
+
actor: String(flag(args, '--as') || DEFAULT_OWNER),
|
|
9119
|
+
reason,
|
|
9120
|
+
fromDone: true,
|
|
9121
|
+
});
|
|
9122
|
+
if (!result.archived) {
|
|
9123
|
+
failTask('atris task clear-done', result.reason, `clear-done failed: ${row.id} ${result.reason}`, 1);
|
|
9124
|
+
}
|
|
9125
|
+
}
|
|
9126
|
+
const { outPath } = writeDefaultProjection(taskDb, db);
|
|
9127
|
+
|
|
9128
|
+
if (wantsJson(args)) {
|
|
9129
|
+
printJson({
|
|
9130
|
+
ok: true,
|
|
9131
|
+
action: 'clear-done',
|
|
9132
|
+
dry_run: false,
|
|
9133
|
+
before_days: beforeDays,
|
|
9134
|
+
count: candidates.length,
|
|
9135
|
+
reason,
|
|
9136
|
+
sample,
|
|
9137
|
+
projection_path: outPath,
|
|
9138
|
+
});
|
|
9139
|
+
return;
|
|
9140
|
+
}
|
|
9141
|
+
console.log(`cleared ${candidates.length} completed task(s).`);
|
|
9142
|
+
}
|
|
9143
|
+
|
|
9144
|
+
// One-time migration for OBL-1622: the 2026-06-10 "first-principles backlog
|
|
9145
|
+
// reset" archived ~125 certified, proof-backed tasks by writing status
|
|
9146
|
+
// 'failed' (no distinct archived status existed yet). This relabels exactly
|
|
9147
|
+
// the rows that carry that reset's metadata marker, using the same
|
|
9148
|
+
// UPDATE+appendTaskEvent write path as every other status transition in
|
|
9149
|
+
// lib/task-db.js — never a raw projection-JSON edit.
|
|
9150
|
+
function cmdRelabelArchived(args) {
|
|
9151
|
+
const apply = hasFlag(args, '--apply');
|
|
9152
|
+
const taskDb = getTaskDb();
|
|
9153
|
+
const db = taskDb.open();
|
|
9154
|
+
const workspaceRoot = taskDb.workspaceRoot();
|
|
9155
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
9156
|
+
const result = taskDb.relabelArchivedTasks(db, { workspaceRoot, apply, actor });
|
|
9157
|
+
if (apply && result.count > 0) {
|
|
9158
|
+
appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count: result.count, ids: result.ids });
|
|
9159
|
+
}
|
|
9160
|
+
if (wantsJson(args)) {
|
|
9161
|
+
printJson({
|
|
9162
|
+
ok: true,
|
|
9163
|
+
action: apply ? 'relabeled' : 'preview',
|
|
9164
|
+
dry_run: !apply,
|
|
9165
|
+
workspace_root: workspaceRoot,
|
|
9166
|
+
...result,
|
|
9167
|
+
});
|
|
9168
|
+
return;
|
|
9169
|
+
}
|
|
9170
|
+
if (!apply) {
|
|
9171
|
+
console.log(`relabel-archived (dry-run): ${result.count} failed task(s) match the June 10 backlog-reset marker.`);
|
|
9172
|
+
for (const s of result.sample) console.log(` - ${s.id} ${s.title}`);
|
|
9173
|
+
if (result.count > result.sample.length) console.log(` ...and ${result.count - result.sample.length} more`);
|
|
9174
|
+
console.log('Run with --apply --as <you> to relabel these failed -> archived.');
|
|
9175
|
+
return;
|
|
9176
|
+
}
|
|
9177
|
+
console.log(`relabeled ${result.count} task(s) failed -> archived (June 10 backlog reset, OBL-1622).`);
|
|
9178
|
+
}
|
|
9179
|
+
|
|
9180
|
+
function appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count, ids }) {
|
|
9181
|
+
if (!workspaceRoot || !fs.existsSync(path.join(workspaceRoot, 'atris'))) return null;
|
|
9182
|
+
const now = new Date();
|
|
9183
|
+
const logName = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
|
|
9184
|
+
const stamp = now.toTimeString().slice(0, 5);
|
|
9185
|
+
const projectDir = path.join(workspaceRoot, 'atris', 'logs', logName.slice(0, 4));
|
|
9186
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
9187
|
+
const logPath = path.join(projectDir, logName);
|
|
9188
|
+
const idPreview = ids.slice(0, 10).join(', ') + (ids.length > 10 ? `, ...(+${ids.length - 10} more)` : '');
|
|
9189
|
+
fs.appendFileSync(logPath, [
|
|
9190
|
+
`## ${stamp} · Task relabel: failed -> archived (OBL-1622)`,
|
|
9191
|
+
`- count: ${count}`,
|
|
9192
|
+
`- reason: June 10 backlog-reset rows mislabeled failed; relabeled to archived`,
|
|
9193
|
+
`- actor: ${actor}`,
|
|
9194
|
+
`- ids: ${idPreview}`,
|
|
9195
|
+
'',
|
|
9196
|
+
].join('\n'), 'utf8');
|
|
9197
|
+
return logPath;
|
|
9198
|
+
}
|
|
9199
|
+
|
|
7842
9200
|
function cmdReady(args) {
|
|
7843
9201
|
const pos = positional(args);
|
|
7844
9202
|
const id = pos[0];
|
|
@@ -7851,18 +9209,27 @@ function cmdReady(args) {
|
|
|
7851
9209
|
// turning a claim into executed evidence. --verify can carry an optional --proof note.
|
|
7852
9210
|
const proofFlag = flag(args, '--proof');
|
|
7853
9211
|
const verifyFlag = flag(args, '--verify');
|
|
9212
|
+
const resultSentence = requireResultSentence('atris task ready', textFlag(args, ['--result']), { ready: true });
|
|
9213
|
+
const usedVerify = typeof verifyFlag === 'string' ? verifyFlag.trim() : '';
|
|
7854
9214
|
let proof = typeof proofFlag === 'string' ? proofFlag : '';
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
9215
|
+
const verifyAutoCertifyAllowed = !usedVerify || isAutoCertifyVerifyCommandAllowed(verifyFlag);
|
|
9216
|
+
if (usedVerify) {
|
|
9217
|
+
// Run the verifier once and write a receipt (pass or fail) so the review
|
|
9218
|
+
// gate in lib/receipt-evidence.js can validate the exact path named in
|
|
9219
|
+
// the proof, not just trust the prose.
|
|
9220
|
+
const { writeTaskReceipt } = require('../lib/task-receipt');
|
|
9221
|
+
const receipt = writeTaskReceipt({ taskId: id, command: verifyFlag, root: process.cwd() });
|
|
9222
|
+
if (!receipt.passed) {
|
|
9223
|
+
const detail = receipt.exit != null ? ` (exit ${receipt.exit})` : (receipt.signal ? ` (signal ${receipt.signal})` : '');
|
|
7859
9224
|
console.error(`atris task ready: verifier failed${detail}: ${verifyFlag}`);
|
|
7860
|
-
if (
|
|
7861
|
-
else if (
|
|
9225
|
+
if (receipt.output) console.error(receipt.output);
|
|
9226
|
+
else if (receipt.error) console.error(receipt.error);
|
|
9227
|
+
if (receipt.receiptPath) console.error(`receipt: ${receipt.receiptPath}`);
|
|
7862
9228
|
process.exit(1);
|
|
7863
9229
|
}
|
|
7864
|
-
|
|
7865
|
-
|
|
9230
|
+
const base = proof.trim();
|
|
9231
|
+
proof = `[verified] \`${verifyFlag}\` passed (exit 0)${base ? ` — ${base}` : ''}${receipt.output ? `\n${receipt.output}` : ''}\nReceipt: ${receipt.receiptPath}`;
|
|
9232
|
+
if (!wantsJson(args)) console.log(`✓ verified: \`${verifyFlag}\` exited 0 (receipt ${receipt.receiptPath})`);
|
|
7866
9233
|
}
|
|
7867
9234
|
if (!proof) {
|
|
7868
9235
|
console.error('atris task ready: --proof or --verify required');
|
|
@@ -7872,9 +9239,10 @@ function cmdReady(args) {
|
|
|
7872
9239
|
const lesson = flag(args, '--lesson') || '';
|
|
7873
9240
|
const nextTaskInput = normalizeReviewNextTaskInput(typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '');
|
|
7874
9241
|
const landing = landingFlags(args);
|
|
9242
|
+
guardExplicitActor('atris task ready', flag(args, '--as'));
|
|
7875
9243
|
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
7876
9244
|
const resultFields = {
|
|
7877
|
-
changed: textFlag(args, ['--changed', '--
|
|
9245
|
+
changed: textFlag(args, ['--changed', '--done']),
|
|
7878
9246
|
checked: textFlag(args, ['--checked', '--check', '--verified']),
|
|
7879
9247
|
passed: textFlag(args, ['--passed', '--pass']),
|
|
7880
9248
|
failed: textFlag(args, ['--failed', '--fail']),
|
|
@@ -7888,10 +9256,17 @@ function cmdReady(args) {
|
|
|
7888
9256
|
const taskDb = getTaskDb();
|
|
7889
9257
|
const db = taskDb.open();
|
|
7890
9258
|
const taskId = requireTaskId(taskDb, db, id, 'atris task ready');
|
|
9259
|
+
const beforeTask = taskDetail(taskDb, db, taskId);
|
|
9260
|
+
requireExplicitLandingDayOnePm('atris task ready', landing, beforeTask && beforeTask.title);
|
|
9261
|
+
const missionXpIssue = missionXpEndToEndProofIssue(beforeTask, proof, taskDb.workspaceRoot());
|
|
9262
|
+
if (missionXpIssue) {
|
|
9263
|
+
failTask('atris task ready', MISSION_XP_END_TO_END_REASON, missionXpIssue);
|
|
9264
|
+
}
|
|
7891
9265
|
const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
|
|
7892
9266
|
actor,
|
|
7893
9267
|
proof: String(proof),
|
|
7894
9268
|
...resultFields,
|
|
9269
|
+
changed: resultFields.changed || resultSentence,
|
|
7895
9270
|
});
|
|
7896
9271
|
const result = taskDb.readyTask(db, {
|
|
7897
9272
|
id: taskId,
|
|
@@ -7901,11 +9276,17 @@ function cmdReady(args) {
|
|
|
7901
9276
|
nextTask: nextTaskInput.nextTask,
|
|
7902
9277
|
resultTrace: resultTrace && resultTrace.trace,
|
|
7903
9278
|
landing,
|
|
9279
|
+
result: resultSentence,
|
|
7904
9280
|
});
|
|
7905
9281
|
if (!result.ready) {
|
|
7906
9282
|
console.error(`ready failed: ${result.reason}`);
|
|
7907
9283
|
process.exit(1);
|
|
7908
9284
|
}
|
|
9285
|
+
// Store the exact verifier command on the task itself (not just baked into
|
|
9286
|
+
// proof prose) so sweep --auto-accept and certify-verified can re-run it
|
|
9287
|
+
// live against the current checkout later, instead of re-deriving it from
|
|
9288
|
+
// text or trusting a receipt file that may no longer exist.
|
|
9289
|
+
if (usedVerify) stampReadyVerifyMetadata(taskDb, db, taskId, usedVerify);
|
|
7909
9290
|
const landingAdvisory = warnIfLandingNeedsDayOnePm(landing, result.row && result.row.title);
|
|
7910
9291
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
7911
9292
|
const agentCertified = result.event.payload.agent_certified === true;
|
|
@@ -7918,13 +9299,21 @@ function cmdReady(args) {
|
|
|
7918
9299
|
nextTask: nextTaskInput.nextTask,
|
|
7919
9300
|
});
|
|
7920
9301
|
const reviewChat = taskReviewChatHandoff(verifierTask, { reviewer: 'codex-review' });
|
|
9302
|
+
const autolandOn = require('../lib/autoland').liveAcceptAuthorization(taskDb.workspaceRoot()).ok;
|
|
9303
|
+
const needsExternalVerifier = Boolean(usedVerify && !verifyAutoCertifyAllowed);
|
|
7921
9304
|
const handoff = {
|
|
7922
9305
|
native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
|
|
7923
9306
|
career_xp_status: 'pending_human_accept',
|
|
7924
9307
|
next_action: agentCertified ? certifiedReviewNextAction(nextTaskInput.nextTask) : 'agent_review_again',
|
|
7925
|
-
rule:
|
|
7926
|
-
?
|
|
7927
|
-
|
|
9308
|
+
rule: autolandOn
|
|
9309
|
+
? (agentCertified
|
|
9310
|
+
? 'double-check complete; autoland will accept this on the next tick.'
|
|
9311
|
+
: needsExternalVerifier
|
|
9312
|
+
? 'proof is ready; this verifier needs a second agent review because autoland cannot rerun it.'
|
|
9313
|
+
: 'proof is ready; autoland runs the second check and lands it on the next tick.')
|
|
9314
|
+
: (agentCertified
|
|
9315
|
+
? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
|
|
9316
|
+
: 'proof is ready; one more agent check before human approval. XP waits for the human.'),
|
|
7928
9317
|
};
|
|
7929
9318
|
if (reviewChat) {
|
|
7930
9319
|
handoff.review_chat_command = reviewChat.command;
|
|
@@ -7958,11 +9347,59 @@ function cmdReady(args) {
|
|
|
7958
9347
|
console.log(`ready for approval ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
|
|
7959
9348
|
if (resultTrace) console.log('Result trace recorded.');
|
|
7960
9349
|
console.log(handoff.rule);
|
|
9350
|
+
if (!verifyAutoCertifyAllowed) {
|
|
9351
|
+
const ref = taskRef(compactTaskFromProjection(projection, taskId) || result.row || taskId);
|
|
9352
|
+
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>`);
|
|
9353
|
+
}
|
|
7961
9354
|
for (const hint of policyHints) {
|
|
7962
9355
|
console.log(`policy (${hint.id}): ${hint.hint}`);
|
|
7963
9356
|
}
|
|
7964
9357
|
}
|
|
7965
9358
|
|
|
9359
|
+
// Standalone receipt writer: runs a verifier for a task and writes atris/runs/
|
|
9360
|
+
// evidence without moving the task to ready. Useful when you want a receipt
|
|
9361
|
+
// on record before or independent of a ready call, or to record a failed
|
|
9362
|
+
// verifier run for the audit trail. `atris task ready --verify` calls the
|
|
9363
|
+
// same writer inline and folds the resulting path into the proof.
|
|
9364
|
+
function cmdTaskReceipt(args) {
|
|
9365
|
+
const pos = positional(args);
|
|
9366
|
+
const id = pos[0];
|
|
9367
|
+
if (!id) {
|
|
9368
|
+
console.error('atris task receipt: id required');
|
|
9369
|
+
process.exit(2);
|
|
9370
|
+
}
|
|
9371
|
+
const verifyFlag = flag(args, '--verify');
|
|
9372
|
+
if (typeof verifyFlag !== 'string' || !verifyFlag.trim()) {
|
|
9373
|
+
console.error('atris task receipt: --verify "<cmd>" required');
|
|
9374
|
+
process.exit(2);
|
|
9375
|
+
}
|
|
9376
|
+
const taskDb = getTaskDb();
|
|
9377
|
+
const db = taskDb.open();
|
|
9378
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task receipt');
|
|
9379
|
+
const { writeTaskReceipt } = require('../lib/task-receipt');
|
|
9380
|
+
const receipt = writeTaskReceipt({ taskId, command: verifyFlag, root: process.cwd() });
|
|
9381
|
+
if (wantsJson(args)) {
|
|
9382
|
+
printJson({
|
|
9383
|
+
ok: receipt.passed,
|
|
9384
|
+
task_id: taskId,
|
|
9385
|
+
command: verifyFlag,
|
|
9386
|
+
receipt_path: receipt.receiptPath,
|
|
9387
|
+
exit: receipt.exit,
|
|
9388
|
+
passed: receipt.passed,
|
|
9389
|
+
});
|
|
9390
|
+
if (!receipt.passed) process.exit(1);
|
|
9391
|
+
return;
|
|
9392
|
+
}
|
|
9393
|
+
if (receipt.passed) {
|
|
9394
|
+
console.log(`receipt written: ${receipt.receiptPath} (exit 0)`);
|
|
9395
|
+
console.log(`use: atris task ready ${taskId} --proof "Receipt: ${receipt.receiptPath}" --result "<what someone can do now and why it matters>"`);
|
|
9396
|
+
} else {
|
|
9397
|
+
console.error(`verifier failed (exit ${receipt.exit}); receipt written: ${receipt.receiptPath}`);
|
|
9398
|
+
if (receipt.output) console.error(receipt.output);
|
|
9399
|
+
process.exit(1);
|
|
9400
|
+
}
|
|
9401
|
+
}
|
|
9402
|
+
|
|
7966
9403
|
async function cmdAccept(args) {
|
|
7967
9404
|
const pos = positional(args);
|
|
7968
9405
|
const id = pos[0];
|
|
@@ -7995,6 +9432,10 @@ async function cmdAccept(args) {
|
|
|
7995
9432
|
process.exit(2);
|
|
7996
9433
|
}
|
|
7997
9434
|
requireMeaningfulTaskProof('atris task accept', proof);
|
|
9435
|
+
const missionXpIssue = missionXpEndToEndProofIssue(beforeTask, proof, taskDb.workspaceRoot());
|
|
9436
|
+
if (missionXpIssue) {
|
|
9437
|
+
failTask('atris task accept', MISSION_XP_END_TO_END_REASON, missionXpIssue);
|
|
9438
|
+
}
|
|
7998
9439
|
const readyReview = beforeTask?.review || {};
|
|
7999
9440
|
const clearLesson = hasEmptyFlagValue(args, '--lesson');
|
|
8000
9441
|
const clearNextTask = hasEmptyFlagValue(args, '--next');
|
|
@@ -8041,6 +9482,7 @@ async function cmdAccept(args) {
|
|
|
8041
9482
|
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
8042
9483
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
8043
9484
|
const workspaceRoot = projection.workspace_root || process.cwd();
|
|
9485
|
+
refreshExistingTodoMarkdown(taskDb, db, workspaceRoot);
|
|
8044
9486
|
const brainScorecards = refreshBrainScorecardsAfterAccept(workspaceRoot);
|
|
8045
9487
|
const nextMissionRoute = nextMissionRouteAfterAccept(workspaceRoot);
|
|
8046
9488
|
// Inform the gate, never block it: show what the receipts named in the proof
|
|
@@ -8093,7 +9535,12 @@ function stampAutoAcceptMetadata(taskDb, db, taskId, actor, policy) {
|
|
|
8093
9535
|
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
8094
9536
|
}
|
|
8095
9537
|
|
|
8096
|
-
function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '' }) {
|
|
9538
|
+
function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '', autoAccepted = false }) {
|
|
9539
|
+
const task = taskDetail(taskDb, db, taskId);
|
|
9540
|
+
const missionXpIssue = missionXpEndToEndProofIssue(task, proof, taskDb.workspaceRoot());
|
|
9541
|
+
if (missionXpIssue) {
|
|
9542
|
+
return { ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue };
|
|
9543
|
+
}
|
|
8097
9544
|
const done = taskDb.doneTask(db, {
|
|
8098
9545
|
id: taskId,
|
|
8099
9546
|
status: 'done',
|
|
@@ -8101,6 +9548,7 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
8101
9548
|
allowReview: true,
|
|
8102
9549
|
action: 'accepted',
|
|
8103
9550
|
proof,
|
|
9551
|
+
autoAccepted,
|
|
8104
9552
|
});
|
|
8105
9553
|
if (!done.updated) {
|
|
8106
9554
|
return { ok: false, reason: 'not_open_claimed_or_review' };
|
|
@@ -8113,7 +9561,10 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
8113
9561
|
nextTask,
|
|
8114
9562
|
proof,
|
|
8115
9563
|
careerXpEligible: true,
|
|
9564
|
+
autoAccepted,
|
|
8116
9565
|
});
|
|
9566
|
+
const acceptedRow = taskDb.getTask(db, taskId);
|
|
9567
|
+
refreshExistingTodoMarkdown(taskDb, db, acceptedRow && acceptedRow.workspace_root);
|
|
8117
9568
|
return { ok: true, reviewed };
|
|
8118
9569
|
}
|
|
8119
9570
|
|
|
@@ -8145,6 +9596,7 @@ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
|
|
|
8145
9596
|
metadata.verify = metadata.verify || verify;
|
|
8146
9597
|
metadata.certified_verified_at = new Date().toISOString();
|
|
8147
9598
|
metadata.certified_verified_by = actor;
|
|
9599
|
+
metadata.machine_verified = true;
|
|
8148
9600
|
db.prepare(`
|
|
8149
9601
|
UPDATE tasks
|
|
8150
9602
|
SET metadata = ?,
|
|
@@ -8153,9 +9605,69 @@ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
|
|
|
8153
9605
|
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
8154
9606
|
}
|
|
8155
9607
|
|
|
8156
|
-
|
|
9608
|
+
// `atris task ready --verify "<cmd>"` already runs the command live and
|
|
9609
|
+
// gates on exit 0, but that alone leaves no machine re-runnable trace once
|
|
9610
|
+
// the proof text scrolls out of easy reach: storing the exact command on
|
|
9611
|
+
// metadata.verify is what lets sweep --auto-accept (and certify-verified)
|
|
9612
|
+
// re-run it later against the current checkout instead of re-parsing prose.
|
|
9613
|
+
function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
|
|
9614
|
+
const row = taskDb.getTask(db, taskId);
|
|
9615
|
+
if (!row) return;
|
|
9616
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
9617
|
+
metadata.verify = verify;
|
|
9618
|
+
db.prepare(`
|
|
9619
|
+
UPDATE tasks
|
|
9620
|
+
SET metadata = ?,
|
|
9621
|
+
updated_at = ?
|
|
9622
|
+
WHERE id = ?
|
|
9623
|
+
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
9624
|
+
}
|
|
9625
|
+
|
|
9626
|
+
function landingVerifyFailureNote(verify, result) {
|
|
9627
|
+
const exit = result && result.status != null ? result.status : 'unknown';
|
|
9628
|
+
return `Autoland re-ran allowlisted verify at landing and it failed: ${verify} (exit ${exit}).`;
|
|
9629
|
+
}
|
|
9630
|
+
|
|
9631
|
+
function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier', verifyCache = null } = {}) {
|
|
9632
|
+
const verify = certifyVerifyCandidate(task);
|
|
9633
|
+
if (!verify) {
|
|
9634
|
+
// No runnable check on record. Landing anyway converts "never verified"
|
|
9635
|
+
// into "accepted with XP" — the exact signal poisoning this gate exists
|
|
9636
|
+
// to prevent — so bounce the task back for a recorded verify instead.
|
|
9637
|
+
const note = 'Autoland refused to land without a runnable verify command: record one with `atris task ready --verify "<cmd>" --result "<sentence>"`.';
|
|
9638
|
+
const revised = taskDb.reviseTask(db, { id: task.id, actor, note });
|
|
9639
|
+
return {
|
|
9640
|
+
ok: false,
|
|
9641
|
+
verify: null,
|
|
9642
|
+
result: { reason: 'strict_verify_missing', status: null },
|
|
9643
|
+
note,
|
|
9644
|
+
revised: revised.revised === true,
|
|
9645
|
+
revise_reason: revised.reason || null,
|
|
9646
|
+
};
|
|
9647
|
+
}
|
|
9648
|
+
const result = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
|
|
9649
|
+
if (result.ok) return { ok: true, verify, result };
|
|
9650
|
+
const note = landingVerifyFailureNote(verify, result);
|
|
9651
|
+
const revised = taskDb.reviseTask(db, {
|
|
9652
|
+
id: task.id,
|
|
9653
|
+
actor,
|
|
9654
|
+
note,
|
|
9655
|
+
});
|
|
9656
|
+
return {
|
|
9657
|
+
ok: false,
|
|
9658
|
+
verify,
|
|
9659
|
+
result,
|
|
9660
|
+
note,
|
|
9661
|
+
revised: revised.revised === true,
|
|
9662
|
+
revise_reason: revised.reason || null,
|
|
9663
|
+
};
|
|
9664
|
+
}
|
|
9665
|
+
|
|
9666
|
+
function cmdCertifyVerified(args, options = {}) {
|
|
8157
9667
|
const dryRun = hasFlag(args, '--dry-run');
|
|
8158
9668
|
const asJson = wantsJson(args);
|
|
9669
|
+
const silent = options.silent === true;
|
|
9670
|
+
const verifyCache = options.verifyCache || null;
|
|
8159
9671
|
const actor = String(flag(args, '--as') || 'autoland-verifier');
|
|
8160
9672
|
const limitRaw = flag(args, '--limit');
|
|
8161
9673
|
const max = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 6) : 6;
|
|
@@ -8192,20 +9704,37 @@ function cmdCertifyVerified(args) {
|
|
|
8192
9704
|
results.push({ ref, action: 'skipped', reason: `denied_tag_${tag}` });
|
|
8193
9705
|
continue;
|
|
8194
9706
|
}
|
|
9707
|
+
const proofBoundary = proofBoundaryBlockedEvaluation(task);
|
|
9708
|
+
if (proofBoundary) {
|
|
9709
|
+
results.push({ ref, action: 'skipped', reason: proofBoundary.reason });
|
|
9710
|
+
continue;
|
|
9711
|
+
}
|
|
8195
9712
|
// Skip only rows the accept lane can already land, or rows blocked by
|
|
8196
9713
|
// something an executed second-actor check cannot cure. A row with two
|
|
8197
9714
|
// passes from ONE actor is exactly what this command exists to cure —
|
|
8198
9715
|
// "certified" alone is not landable.
|
|
9716
|
+
// strictVerify stays off in this eligibility probe: certify-verified runs
|
|
9717
|
+
// the check itself right below, and strict mode here would execute it a
|
|
9718
|
+
// second time per row before the real run.
|
|
8199
9719
|
const evaluation = evaluateAutoAccept(task, { strictVerify: false });
|
|
8200
9720
|
if (evaluation.eligible) {
|
|
8201
9721
|
results.push({ ref, action: 'skipped', reason: 'already_landable' });
|
|
8202
9722
|
continue;
|
|
8203
9723
|
}
|
|
8204
|
-
|
|
9724
|
+
// proof_not_executed is curable by definition: this command re-runs the
|
|
9725
|
+
// named check and replaces the free-text claim with executed evidence.
|
|
9726
|
+
const curable = ['not_agent_certified', 'needs_independent_reviewer', 'needs_second_reviewer_or_third_pass', 'insufficient_review_passes', 'proof_not_executed'];
|
|
8205
9727
|
if (!curable.includes(evaluation.reason)) {
|
|
8206
9728
|
results.push({ ref, action: 'skipped', reason: evaluation.reason });
|
|
8207
9729
|
continue;
|
|
8208
9730
|
}
|
|
9731
|
+
// judge != worker: the re-run only counts as an independent check when
|
|
9732
|
+
// its actor is not the builder of the row it is judging.
|
|
9733
|
+
const builder = reviewIntegrity.taskBuilder(task);
|
|
9734
|
+
if (builder && reviewIntegrity.normalizeActor(actor) === builder) {
|
|
9735
|
+
results.push({ ref, action: 'skipped', reason: 'verifier_is_builder' });
|
|
9736
|
+
continue;
|
|
9737
|
+
}
|
|
8209
9738
|
const verify = certifyVerifyCandidate(task);
|
|
8210
9739
|
if (!verify) {
|
|
8211
9740
|
results.push({ ref, action: 'skipped', reason: 'no_runnable_check_in_proof' });
|
|
@@ -8215,7 +9744,7 @@ function cmdCertifyVerified(args) {
|
|
|
8215
9744
|
results.push({ ref, action: 'would_certify', verify });
|
|
8216
9745
|
continue;
|
|
8217
9746
|
}
|
|
8218
|
-
const run =
|
|
9747
|
+
const run = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
|
|
8219
9748
|
if (!run.ok) {
|
|
8220
9749
|
results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
|
|
8221
9750
|
continue;
|
|
@@ -8249,6 +9778,9 @@ function cmdCertifyVerified(args) {
|
|
|
8249
9778
|
results,
|
|
8250
9779
|
projection_path: outPath,
|
|
8251
9780
|
};
|
|
9781
|
+
if (silent) {
|
|
9782
|
+
return payload;
|
|
9783
|
+
}
|
|
8252
9784
|
if (asJson) {
|
|
8253
9785
|
console.log(JSON.stringify(payload, null, 2));
|
|
8254
9786
|
} else if (results.length === 0) {
|
|
@@ -8262,9 +9794,27 @@ function cmdCertifyVerified(args) {
|
|
|
8262
9794
|
return payload;
|
|
8263
9795
|
}
|
|
8264
9796
|
|
|
9797
|
+
// The landing: everything certified, one summary, one human gesture.
|
|
9798
|
+
// Review-by-N-pastes was the operator pain; this is the batch gate that
|
|
9799
|
+
// keeps human accept as the one gate without making it N gates.
|
|
9800
|
+
function cmdLanding(args) {
|
|
9801
|
+
if (hasFlag(args, '--accept')) {
|
|
9802
|
+
const passthrough = args.filter(arg => arg !== '--accept');
|
|
9803
|
+
if (!passthrough.includes('--confirm-human-accept')) passthrough.push('--confirm-human-accept');
|
|
9804
|
+
return cmdAutoAcceptCertified(passthrough);
|
|
9805
|
+
}
|
|
9806
|
+
cmdReviews(args);
|
|
9807
|
+
console.log('');
|
|
9808
|
+
console.log('land everything certified above in one gesture:');
|
|
9809
|
+
console.log(' atris task landing --accept --as <you>');
|
|
9810
|
+
console.log('(items needing one more check stay in review; only certified work lands)');
|
|
9811
|
+
}
|
|
9812
|
+
|
|
8265
9813
|
function cmdAutoAcceptCertified(args) {
|
|
8266
9814
|
const dryRun = hasFlag(args, '--dry-run');
|
|
8267
|
-
const
|
|
9815
|
+
const acceptAll = hasFlag(args, '--all');
|
|
9816
|
+
const certifyFirst = hasFlag(args, '--certify-first');
|
|
9817
|
+
const strictVerify = !hasFlag(args, '--no-strict-verify') && !acceptAll;
|
|
8268
9818
|
const actorFlag = flag(args, '--as');
|
|
8269
9819
|
const hasHumanActor = validHumanActorFlag(actorFlag);
|
|
8270
9820
|
const confirmedHumanAccept = hasFlag(args, '--confirm-human-accept');
|
|
@@ -8277,7 +9827,17 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8277
9827
|
: { ok: false };
|
|
8278
9828
|
const actor = String(actorFlag || (policyAuth.ok ? policyAuth.actor : 'auto-accept-certified'));
|
|
8279
9829
|
const limitRaw = flag(args, '--limit');
|
|
8280
|
-
const
|
|
9830
|
+
const hasExplicitLimit = Boolean(limitRaw) && limitRaw !== true;
|
|
9831
|
+
// --all means sweep the full certified backlog, not just the first page of
|
|
9832
|
+
// it. Before this fix `max` was hard-capped at 12 even under --all, so a
|
|
9833
|
+
// real backlog (78 certified rows observed live) only ever drained 12/run —
|
|
9834
|
+
// an invisible undercount the autoland heartbeat repeated every hour.
|
|
9835
|
+
// AUTO_ACCEPT_ALL_SWEEP_CAP is a safety ceiling, not a target: a well-formed
|
|
9836
|
+
// --all run should always scan fewer rows than this.
|
|
9837
|
+
const AUTO_ACCEPT_ALL_SWEEP_CAP = 500;
|
|
9838
|
+
const max = hasExplicitLimit
|
|
9839
|
+
? Math.max(1, Number(limitRaw) || 12)
|
|
9840
|
+
: (acceptAll ? AUTO_ACCEPT_ALL_SWEEP_CAP : 12);
|
|
8281
9841
|
const parsedReward = parseAcceptReward(flag(args, '--reward'));
|
|
8282
9842
|
if (!parsedReward.ok) {
|
|
8283
9843
|
console.error('atris task auto-accept-certified: reward must be a positive number');
|
|
@@ -8297,27 +9857,62 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8297
9857
|
'live auto-accept requires --as <human> so XP has an explicit human acceptance actor',
|
|
8298
9858
|
);
|
|
8299
9859
|
}
|
|
8300
|
-
|
|
9860
|
+
// The standing autoland policy clears this gate too, same as the two
|
|
9861
|
+
// per-run human gates above: the owner flipped the policy, accepts run as
|
|
9862
|
+
// that owner, and the cron tick would land the same rows an hour later
|
|
9863
|
+
// anyway. Without this exception `atris autoland tick` was blind whenever
|
|
9864
|
+
// invoked from an agent session (CLAUDECODE etc. in env): the spawned
|
|
9865
|
+
// sweep failTask'd with no summary and the tick receipt showed nulls.
|
|
9866
|
+
// A per-run --confirm-human-accept claim from an agent is still refused —
|
|
9867
|
+
// policyAuth is only consulted when no per-run confirmation is passed.
|
|
9868
|
+
if (agentProofOnlyMode() && !dryRun && !policyAuth.ok) {
|
|
8301
9869
|
failAgentProofOnly(
|
|
8302
9870
|
'atris task auto-accept-certified',
|
|
8303
|
-
'Agent proof-only mode can preview certified rows with --dry-run, but cannot live-accept them.',
|
|
9871
|
+
'Agent proof-only mode can preview certified rows with --dry-run, but cannot live-accept them without the standing autoland policy.',
|
|
8304
9872
|
);
|
|
8305
9873
|
}
|
|
8306
9874
|
|
|
9875
|
+
// A heartbeat certifies and lands in one process so its live verifier result
|
|
9876
|
+
// remains available to the landing gate. The cache is process-local and is
|
|
9877
|
+
// never persisted: a later heartbeat must prove the checkout again.
|
|
9878
|
+
const verifyCache = new Map();
|
|
9879
|
+
const certification = certifyFirst && !dryRun
|
|
9880
|
+
? cmdCertifyVerified([], { verifyCache, silent: true })
|
|
9881
|
+
: null;
|
|
9882
|
+
|
|
8307
9883
|
const taskDb = getTaskDb();
|
|
8308
9884
|
const db = taskDb.open();
|
|
8309
9885
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
8310
9886
|
const queue = taskReviewQueue(projection, ['--limit', String(max)]);
|
|
9887
|
+
const pendingReview = (projection.tasks || [])
|
|
9888
|
+
.filter((t) => t && t.status === 'review' && t.review && t.review.approval_status === 'pending');
|
|
9889
|
+
// Honest denominator for the summary line below: how many rows the
|
|
9890
|
+
// projection actually certified, independent of any scan cap. If `scanned`
|
|
9891
|
+
// ever comes in under `certified`, the undercount is visible instead of
|
|
9892
|
+
// silent (the exact failure mode this fix closes).
|
|
9893
|
+
const certifiedTotal = pendingReview.filter((t) => isAgentCertified(t)).length;
|
|
9894
|
+
// The review queue only surfaces certified rows. Under --all the bar is
|
|
9895
|
+
// the protected lanes, not certification, so scan every pending review.
|
|
9896
|
+
const pool = acceptAll
|
|
9897
|
+
? pendingReview
|
|
9898
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
|
|
9899
|
+
.slice(0, max)
|
|
9900
|
+
: queue.items.filter(item => item.queue_role !== 'blocked');
|
|
8311
9901
|
const results = [];
|
|
8312
9902
|
|
|
8313
|
-
for (const item of
|
|
9903
|
+
for (const item of pool) {
|
|
8314
9904
|
const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
|
|
8315
9905
|
const task = fullProjection.tasks[0] || null;
|
|
8316
9906
|
if (!task) {
|
|
8317
9907
|
results.push({ ref: item.display_id || item.id, eligible: false, reason: 'task_not_found', action: 'skipped' });
|
|
8318
9908
|
continue;
|
|
8319
9909
|
}
|
|
8320
|
-
const
|
|
9910
|
+
const proofBoundary = proofBoundaryBlockedEvaluation(task);
|
|
9911
|
+
if (proofBoundary) {
|
|
9912
|
+
results.push({ ...proofBoundary, action: 'skipped' });
|
|
9913
|
+
continue;
|
|
9914
|
+
}
|
|
9915
|
+
const evaluation = evaluateAutoAccept(task, { strictVerify, acceptAll, verifyCache });
|
|
8321
9916
|
if (!evaluation.eligible) {
|
|
8322
9917
|
results.push({ ...evaluation, action: 'skipped' });
|
|
8323
9918
|
continue;
|
|
@@ -8326,12 +9921,27 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8326
9921
|
results.push({ ...evaluation, action: 'would_accept', reward: parsedReward.value });
|
|
8327
9922
|
continue;
|
|
8328
9923
|
}
|
|
9924
|
+
const landingVerify = reverifyBeforeLanding(taskDb, db, task, { verifyCache });
|
|
9925
|
+
if (!landingVerify.ok) {
|
|
9926
|
+
results.push({
|
|
9927
|
+
...evaluation,
|
|
9928
|
+
eligible: false,
|
|
9929
|
+
action: landingVerify.revised ? 'revised' : 'revise_failed',
|
|
9930
|
+
reason: landingVerify.result.reason || landingVerify.revise_reason || 'verify_failed',
|
|
9931
|
+
verify: landingVerify.verify,
|
|
9932
|
+
exit_code: landingVerify.result.status,
|
|
9933
|
+
revision_note: landingVerify.note,
|
|
9934
|
+
task_id: task.id,
|
|
9935
|
+
});
|
|
9936
|
+
continue;
|
|
9937
|
+
}
|
|
8329
9938
|
const accepted = acceptReviewTask(taskDb, db, task.id, {
|
|
8330
9939
|
actor,
|
|
8331
9940
|
proof: evaluation.proof,
|
|
8332
9941
|
reward: parsedReward.value,
|
|
8333
9942
|
lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
|
|
8334
9943
|
nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
|
|
9944
|
+
autoAccepted: true,
|
|
8335
9945
|
});
|
|
8336
9946
|
if (!accepted.ok) {
|
|
8337
9947
|
results.push({ ...evaluation, action: 'accept_failed', reason: accepted.reason });
|
|
@@ -8349,32 +9959,282 @@ function cmdAutoAcceptCertified(args) {
|
|
|
8349
9959
|
|
|
8350
9960
|
const { projection: finalProjection, outPath: finalPath } = writeDefaultProjection(taskDb, db);
|
|
8351
9961
|
const summary = {
|
|
8352
|
-
|
|
9962
|
+
certified: certifiedTotal,
|
|
9963
|
+
scanned: pool.length,
|
|
8353
9964
|
accepted: results.filter(row => row.action === 'accepted').length,
|
|
8354
9965
|
would_accept: results.filter(row => row.action === 'would_accept').length,
|
|
9966
|
+
revised: results.filter(row => row.action === 'revised').length,
|
|
8355
9967
|
skipped: results.filter(row => row.action === 'skipped').length,
|
|
8356
|
-
failed: results.filter(row => row.action === 'accept_failed').length,
|
|
9968
|
+
failed: results.filter(row => row.action === 'accept_failed' || row.action === 'revise_failed').length,
|
|
9969
|
+
// Visible undercount flag: true only if the pool was cut short by `max`
|
|
9970
|
+
// while certified rows still existed beyond it. --all uses a high safety
|
|
9971
|
+
// cap (AUTO_ACCEPT_ALL_SWEEP_CAP), so this should stay false in practice;
|
|
9972
|
+
// if it ever flips true, the cap itself needs raising, not silence.
|
|
9973
|
+
undercounted: certifiedTotal > pool.length,
|
|
8357
9974
|
};
|
|
8358
9975
|
if (wantsJson(args)) {
|
|
8359
9976
|
printJson({
|
|
8360
9977
|
ok: true,
|
|
8361
9978
|
action: dryRun ? 'auto_accept_certified_dry_run' : 'auto_accept_certified',
|
|
8362
9979
|
strict_verify: strictVerify,
|
|
9980
|
+
accept_all: acceptAll,
|
|
8363
9981
|
summary,
|
|
8364
9982
|
...summary,
|
|
8365
9983
|
results,
|
|
9984
|
+
certification,
|
|
8366
9985
|
projection_path: finalPath,
|
|
8367
9986
|
queue,
|
|
8368
9987
|
});
|
|
8369
|
-
return;
|
|
9988
|
+
return { ...summary, results, certification, projection_path: finalPath, queue };
|
|
8370
9989
|
}
|
|
8371
9990
|
console.log(`AUTO-ACCEPT CERTIFIED (${dryRun ? 'dry-run' : 'execute'})`);
|
|
8372
|
-
console.log(`${summary.accepted || summary.would_accept} accepted
|
|
9991
|
+
console.log(`${summary.certified} certified, ${summary.scanned} scanned, ${summary.accepted || summary.would_accept} accepted, ${summary.skipped} skipped${summary.revised ? `, ${summary.revised} revised` : ''}${summary.failed ? `, ${summary.failed} failed` : ''}${summary.undercounted ? ' (UNDERCOUNTED — raise --limit or the sweep cap)' : ''}`);
|
|
8373
9992
|
for (const row of results) {
|
|
8374
9993
|
const nextAction = row.next_action ? ` next_action=${row.next_action}` : '';
|
|
8375
9994
|
const reviewChat = row.review_chat_command ? ` review_chat=${row.review_chat_command}` : '';
|
|
8376
9995
|
console.log(`${row.action.toUpperCase()} ${row.ref}: ${row.reason}${row.reward ? ` reward=${row.reward}` : ''}${nextAction}${reviewChat}`);
|
|
8377
9996
|
}
|
|
9997
|
+
return { ...summary, results, certification, projection_path: finalPath, queue };
|
|
9998
|
+
}
|
|
9999
|
+
|
|
10000
|
+
const SWEEP_AUTO_ACCEPT_PROTECTED = new Set([
|
|
10001
|
+
'money', 'deploy', 'release', 'publish', 'security', 'customer', 'outward',
|
|
10002
|
+
]);
|
|
10003
|
+
|
|
10004
|
+
function autoAcceptSweepLabelValues(task) {
|
|
10005
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
10006
|
+
const tags = Array.isArray(metadata.tags) ? metadata.tags : [];
|
|
10007
|
+
return [
|
|
10008
|
+
task && task.tag,
|
|
10009
|
+
task && task.lane,
|
|
10010
|
+
metadata.tag,
|
|
10011
|
+
metadata.lane,
|
|
10012
|
+
metadata.mission_lane,
|
|
10013
|
+
metadata.stage,
|
|
10014
|
+
...tags,
|
|
10015
|
+
].filter(value => value !== undefined && value !== null && String(value).trim());
|
|
10016
|
+
}
|
|
10017
|
+
|
|
10018
|
+
function autoAcceptSweepNormalizedTokens(value) {
|
|
10019
|
+
const normalized = String(value || '').trim().toLowerCase().replace(/_/g, '-');
|
|
10020
|
+
if (!normalized) return [];
|
|
10021
|
+
return [normalized, ...normalized.split(/[^a-z0-9-]+/).filter(Boolean)];
|
|
10022
|
+
}
|
|
10023
|
+
|
|
10024
|
+
function autoAcceptSweepDeniedReason(task) {
|
|
10025
|
+
for (const value of autoAcceptSweepLabelValues(task)) {
|
|
10026
|
+
const tokens = autoAcceptSweepNormalizedTokens(value);
|
|
10027
|
+
for (const token of tokens) {
|
|
10028
|
+
if (token === 'needs-human' || token === 'needshuman') {
|
|
10029
|
+
return 'needs_human';
|
|
10030
|
+
}
|
|
10031
|
+
const protectedLane = [...SWEEP_AUTO_ACCEPT_PROTECTED].find((denied) =>
|
|
10032
|
+
token === denied || token.replace(/s$/, '') === denied
|
|
10033
|
+
);
|
|
10034
|
+
if (protectedLane) return 'protected_lane';
|
|
10035
|
+
}
|
|
10036
|
+
}
|
|
10037
|
+
return null;
|
|
10038
|
+
}
|
|
10039
|
+
|
|
10040
|
+
function autoAcceptSweepLatestProof(task) {
|
|
10041
|
+
const metadata = task && task.metadata || {};
|
|
10042
|
+
const review = task && task.review || {};
|
|
10043
|
+
return String(review.proof || metadata.latest_agent_proof || '').trim();
|
|
10044
|
+
}
|
|
10045
|
+
|
|
10046
|
+
function autoAcceptSweepVerifierEvidence(proof, root) {
|
|
10047
|
+
const evidence = extractReceiptEvidence(proof, root);
|
|
10048
|
+
if (!evidence || !evidence.receipts || !evidence.receipts.length) {
|
|
10049
|
+
return { ok: false, reason: 'no_passing_verifier', evidence: evidence || null };
|
|
10050
|
+
}
|
|
10051
|
+
if (evidence.missing && evidence.missing.length) {
|
|
10052
|
+
return { ok: false, reason: 'receipt_missing', evidence };
|
|
10053
|
+
}
|
|
10054
|
+
const passing = evidence.receipts.filter((receipt) => receipt.verifier_passed === true);
|
|
10055
|
+
if (!passing.length) {
|
|
10056
|
+
return { ok: false, reason: 'no_passing_verifier', evidence };
|
|
10057
|
+
}
|
|
10058
|
+
const notPassed = evidence.receipts.find((receipt) => receipt.verifier_passed !== true);
|
|
10059
|
+
if (notPassed) {
|
|
10060
|
+
return { ok: false, reason: 'receipt_verifier_not_passed', evidence };
|
|
10061
|
+
}
|
|
10062
|
+
return {
|
|
10063
|
+
ok: true,
|
|
10064
|
+
evidence,
|
|
10065
|
+
passing,
|
|
10066
|
+
proved_by: passing.map((receipt) => `${receipt.path} verifier_passed=true`),
|
|
10067
|
+
};
|
|
10068
|
+
}
|
|
10069
|
+
|
|
10070
|
+
function autoAcceptSweepHappened(task) {
|
|
10071
|
+
const review = task && task.review || {};
|
|
10072
|
+
return clipStatusText(
|
|
10073
|
+
review.landing?.happened
|
|
10074
|
+
|| review.result?.changed
|
|
10075
|
+
|| task?.title
|
|
10076
|
+
|| 'accepted verified task',
|
|
10077
|
+
180,
|
|
10078
|
+
);
|
|
10079
|
+
}
|
|
10080
|
+
|
|
10081
|
+
function evaluateSweepAutoAccept(task, root) {
|
|
10082
|
+
const ref = taskRef(task);
|
|
10083
|
+
if (!task) return { eligible: false, ref, reason: 'task_not_found' };
|
|
10084
|
+
if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
|
|
10085
|
+
const metadata = task.metadata || {};
|
|
10086
|
+
const review = task.review || {};
|
|
10087
|
+
const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
|
|
10088
|
+
if (approval !== 'pending') return { eligible: false, ref, reason: `approval_${approval}` };
|
|
10089
|
+
if (metadata.auto_accepted_at) return { eligible: false, ref, reason: 'already_auto_accepted' };
|
|
10090
|
+
const denied = autoAcceptSweepDeniedReason(task);
|
|
10091
|
+
if (denied) return { eligible: false, ref, reason: denied };
|
|
10092
|
+
const proof = autoAcceptSweepLatestProof(task);
|
|
10093
|
+
if (!proof) return { eligible: false, ref, reason: 'no_proof' };
|
|
10094
|
+
|
|
10095
|
+
// 1. An explicit, stored verifier (`atris task ready --verify`, or a prior
|
|
10096
|
+
// certify-verified stamp) is the strongest signal: re-run it live, right
|
|
10097
|
+
// now, against the current checkout, and let it decide outright. This is
|
|
10098
|
+
// what unblocks CLI-762/CLI-861-shaped proofs: a real green test cited (or
|
|
10099
|
+
// executed) in the proof, but the older receipt-path check below could not
|
|
10100
|
+
// find file evidence for it once `ready --verify` stopped writing a
|
|
10101
|
+
// receipt file and started embedding the executed result into proof text.
|
|
10102
|
+
const storedVerify = typeof metadata.verify === 'string' ? metadata.verify.trim() : '';
|
|
10103
|
+
if (storedVerify) {
|
|
10104
|
+
const result = runVerifyCommand(storedVerify, root);
|
|
10105
|
+
if (!result.ok) return { eligible: false, ref, reason: result.reason, verify: storedVerify };
|
|
10106
|
+
return {
|
|
10107
|
+
eligible: true,
|
|
10108
|
+
ref,
|
|
10109
|
+
reason: 'verified_command',
|
|
10110
|
+
policy: 'sweep_auto_accept_verified_command',
|
|
10111
|
+
proof,
|
|
10112
|
+
verify: storedVerify,
|
|
10113
|
+
proved_by: [`${storedVerify} exited 0`],
|
|
10114
|
+
happened: autoAcceptSweepHappened(task),
|
|
10115
|
+
};
|
|
10116
|
+
}
|
|
10117
|
+
|
|
10118
|
+
// 2. Legacy path: a receipt file explicitly named in proof text.
|
|
10119
|
+
const verifier = autoAcceptSweepVerifierEvidence(proof, root);
|
|
10120
|
+
if (verifier.ok) {
|
|
10121
|
+
return {
|
|
10122
|
+
eligible: true,
|
|
10123
|
+
ref,
|
|
10124
|
+
reason: 'verified_receipt',
|
|
10125
|
+
policy: 'sweep_auto_accept_verified',
|
|
10126
|
+
proof,
|
|
10127
|
+
evidence: verifier.evidence,
|
|
10128
|
+
proved_by: verifier.proved_by,
|
|
10129
|
+
happened: autoAcceptSweepHappened(task),
|
|
10130
|
+
};
|
|
10131
|
+
}
|
|
10132
|
+
|
|
10133
|
+
// 3. No stored verifier and no receipt was even cited: try deriving a
|
|
10134
|
+
// safe, runnable command straight from the proof text itself (same
|
|
10135
|
+
// extractor certify-verified uses) and re-run it live. Only reached when
|
|
10136
|
+
// there is nothing else to go on, so a proof that legitimately cites a
|
|
10137
|
+
// real receipt keeps taking the receipt path above rather than racing an
|
|
10138
|
+
// unrelated command mentioned in the same sentence.
|
|
10139
|
+
if (verifier.reason === 'no_passing_verifier') {
|
|
10140
|
+
const derived = certifyVerifyCandidate(task);
|
|
10141
|
+
if (derived) {
|
|
10142
|
+
const result = runVerifyCommand(derived, root);
|
|
10143
|
+
if (result.ok) {
|
|
10144
|
+
return {
|
|
10145
|
+
eligible: true,
|
|
10146
|
+
ref,
|
|
10147
|
+
reason: 'verified_derived_command',
|
|
10148
|
+
policy: 'sweep_auto_accept_verified_derived',
|
|
10149
|
+
proof,
|
|
10150
|
+
verify: derived,
|
|
10151
|
+
proved_by: [`${derived} exited 0`],
|
|
10152
|
+
happened: autoAcceptSweepHappened(task),
|
|
10153
|
+
};
|
|
10154
|
+
}
|
|
10155
|
+
return { eligible: false, ref, reason: result.reason, verify: derived };
|
|
10156
|
+
}
|
|
10157
|
+
}
|
|
10158
|
+
|
|
10159
|
+
return { eligible: false, ref, reason: verifier.reason, evidence: verifier.evidence };
|
|
10160
|
+
}
|
|
10161
|
+
|
|
10162
|
+
function cmdSweep(args) {
|
|
10163
|
+
if (!hasFlag(args, '--auto-accept')) {
|
|
10164
|
+
failTask(
|
|
10165
|
+
'atris task sweep',
|
|
10166
|
+
'missing_auto_accept',
|
|
10167
|
+
'atris task sweep currently requires --auto-accept for the explicit verified accept policy',
|
|
10168
|
+
);
|
|
10169
|
+
}
|
|
10170
|
+
const actor = String(flag(args, '--as') || 'orb-autoaccept');
|
|
10171
|
+
const parsedReward = parseAcceptReward(flag(args, '--reward'));
|
|
10172
|
+
if (!parsedReward.ok) {
|
|
10173
|
+
failTask('atris task sweep', 'invalid_reward', 'atris task sweep --auto-accept reward must be a positive number');
|
|
10174
|
+
}
|
|
10175
|
+
const limitRaw = flag(args, '--limit');
|
|
10176
|
+
const explicitLimit = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 0) : null;
|
|
10177
|
+
const taskDb = getTaskDb();
|
|
10178
|
+
const db = taskDb.open();
|
|
10179
|
+
const { projection } = writeDefaultProjection(taskDb, db);
|
|
10180
|
+
const root = projection.workspace_root || process.cwd();
|
|
10181
|
+
let pendingReview = (projection.tasks || [])
|
|
10182
|
+
.filter((task) => task && task.status === 'review' && task.review && task.review.approval_status === 'pending')
|
|
10183
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
|
10184
|
+
if (explicitLimit) pendingReview = pendingReview.slice(0, explicitLimit);
|
|
10185
|
+
const results = [];
|
|
10186
|
+
for (const item of pendingReview) {
|
|
10187
|
+
const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
|
|
10188
|
+
const task = fullProjection.tasks[0] || null;
|
|
10189
|
+
const evaluation = evaluateSweepAutoAccept(task, root);
|
|
10190
|
+
if (!evaluation.eligible) {
|
|
10191
|
+
results.push({ ...evaluation, action: 'skipped', task_id: task?.id || item.id || null });
|
|
10192
|
+
continue;
|
|
10193
|
+
}
|
|
10194
|
+
const accepted = acceptReviewTask(taskDb, db, task.id, {
|
|
10195
|
+
actor,
|
|
10196
|
+
proof: evaluation.proof,
|
|
10197
|
+
reward: parsedReward.value,
|
|
10198
|
+
lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
|
|
10199
|
+
nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
|
|
10200
|
+
autoAccepted: true,
|
|
10201
|
+
});
|
|
10202
|
+
if (!accepted.ok) {
|
|
10203
|
+
results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
|
|
10204
|
+
continue;
|
|
10205
|
+
}
|
|
10206
|
+
stampAutoAcceptMetadata(taskDb, db, task.id, actor, evaluation.policy);
|
|
10207
|
+
refreshCareerXpAfterReview(accepted.reviewed);
|
|
10208
|
+
results.push({
|
|
10209
|
+
...evaluation,
|
|
10210
|
+
action: 'accepted',
|
|
10211
|
+
task_id: task.id,
|
|
10212
|
+
reward: accepted.reviewed.episode.reward.value,
|
|
10213
|
+
});
|
|
10214
|
+
}
|
|
10215
|
+
const { outPath } = writeDefaultProjection(taskDb, db);
|
|
10216
|
+
const summary = {
|
|
10217
|
+
scanned: pendingReview.length,
|
|
10218
|
+
accepted: results.filter((row) => row.action === 'accepted').length,
|
|
10219
|
+
skipped: results.filter((row) => row.action === 'skipped').length,
|
|
10220
|
+
failed: results.filter((row) => row.action === 'accept_failed').length,
|
|
10221
|
+
};
|
|
10222
|
+
if (wantsJson(args)) {
|
|
10223
|
+
printJson({
|
|
10224
|
+
ok: true,
|
|
10225
|
+
action: 'sweep_auto_accept',
|
|
10226
|
+
actor,
|
|
10227
|
+
summary,
|
|
10228
|
+
...summary,
|
|
10229
|
+
results,
|
|
10230
|
+
projection_path: outPath,
|
|
10231
|
+
});
|
|
10232
|
+
return;
|
|
10233
|
+
}
|
|
10234
|
+
console.log(`TASK SWEEP AUTO-ACCEPT: ${summary.accepted} accepted / ${summary.scanned} scanned / ${summary.skipped} skipped${summary.failed ? ` / ${summary.failed} failed` : ''}`);
|
|
10235
|
+
for (const row of results.filter((item) => item.action === 'accepted')) {
|
|
10236
|
+
console.log(`ACCEPTED ${row.ref}: ${row.happened} | proved by ${row.proved_by.join(', ')}`);
|
|
10237
|
+
}
|
|
8378
10238
|
}
|
|
8379
10239
|
|
|
8380
10240
|
function cmdRevise(args) {
|
|
@@ -8416,6 +10276,138 @@ function cmdRevise(args) {
|
|
|
8416
10276
|
console.log(`revise ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
|
|
8417
10277
|
}
|
|
8418
10278
|
|
|
10279
|
+
function taskAuditTimestamp(task) {
|
|
10280
|
+
const acceptedAt = Date.parse(String(task?.metadata?.accepted_at || ''));
|
|
10281
|
+
if (Number.isFinite(acceptedAt)) return acceptedAt;
|
|
10282
|
+
return Number(task?.done_at || task?.updated_at || task?.created_at || 0);
|
|
10283
|
+
}
|
|
10284
|
+
|
|
10285
|
+
function taskAuditOutput(value, max = 800) {
|
|
10286
|
+
const text = String(value || '').trim();
|
|
10287
|
+
if (text.length <= max) return text;
|
|
10288
|
+
return `...${text.slice(-max)}`;
|
|
10289
|
+
}
|
|
10290
|
+
|
|
10291
|
+
function taskAuditReceiptPath(at) {
|
|
10292
|
+
const safeTime = at.replace(/[:.]/g, '-');
|
|
10293
|
+
return path.join('atris', 'runs', `task-audit-${safeTime}.json`);
|
|
10294
|
+
}
|
|
10295
|
+
|
|
10296
|
+
function runTaskAuditVerify(task, verify) {
|
|
10297
|
+
const { spawnSync } = require('child_process');
|
|
10298
|
+
const result = spawnSync('bash', ['-c', verify], {
|
|
10299
|
+
cwd: task.workspace_root,
|
|
10300
|
+
encoding: 'utf8',
|
|
10301
|
+
timeout: 120000,
|
|
10302
|
+
});
|
|
10303
|
+
const passed = !result.error && result.status === 0;
|
|
10304
|
+
return {
|
|
10305
|
+
passed,
|
|
10306
|
+
exit: result.status,
|
|
10307
|
+
signal: result.signal || null,
|
|
10308
|
+
error: result.error ? result.error.message : null,
|
|
10309
|
+
output: taskAuditOutput(`${result.stdout || ''}${result.stderr || ''}`),
|
|
10310
|
+
};
|
|
10311
|
+
}
|
|
10312
|
+
|
|
10313
|
+
function cmdAudit(args) {
|
|
10314
|
+
const limitRaw = flag(args, '--limit');
|
|
10315
|
+
const limit = limitRaw === null ? 20 : Number(limitRaw);
|
|
10316
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
10317
|
+
console.error('atris task audit: --limit must be a positive integer');
|
|
10318
|
+
process.exit(2);
|
|
10319
|
+
}
|
|
10320
|
+
|
|
10321
|
+
const revise = hasFlag(args, '--revise');
|
|
10322
|
+
const actor = String(flag(args, '--as') || 'task-audit');
|
|
10323
|
+
const taskDb = getTaskDb();
|
|
10324
|
+
const db = taskDb.open();
|
|
10325
|
+
const workspaceRoot = taskDb.workspaceRoot();
|
|
10326
|
+
const allRows = taskDb.listTasks(db, { workspaceRoot });
|
|
10327
|
+
const accepted = taskDb.withTaskDisplayRefs(
|
|
10328
|
+
allRows
|
|
10329
|
+
.filter(task => task.status === 'done' && task.metadata?.approval_status === 'accepted')
|
|
10330
|
+
.sort((a, b) => taskAuditTimestamp(b) - taskAuditTimestamp(a))
|
|
10331
|
+
.slice(0, limit),
|
|
10332
|
+
allRows,
|
|
10333
|
+
);
|
|
10334
|
+
const at = new Date().toISOString();
|
|
10335
|
+
const receiptPath = taskAuditReceiptPath(at);
|
|
10336
|
+
const results = accepted.map(task => {
|
|
10337
|
+
const verify = typeof task.metadata?.verify === 'string' ? task.metadata.verify : '';
|
|
10338
|
+
if (!verify.trim()) {
|
|
10339
|
+
return {
|
|
10340
|
+
task_id: task.id,
|
|
10341
|
+
ref: task.display_id || task.legacy_ref || taskRef(task),
|
|
10342
|
+
status: 'skipped-no-verify',
|
|
10343
|
+
verify: null,
|
|
10344
|
+
};
|
|
10345
|
+
}
|
|
10346
|
+
const run = runTaskAuditVerify(task, verify);
|
|
10347
|
+
return {
|
|
10348
|
+
task_id: task.id,
|
|
10349
|
+
ref: task.display_id || task.legacy_ref || taskRef(task),
|
|
10350
|
+
status: run.passed ? 'passed' : 'failed',
|
|
10351
|
+
verify,
|
|
10352
|
+
exit: run.exit,
|
|
10353
|
+
signal: run.signal,
|
|
10354
|
+
error: run.error,
|
|
10355
|
+
output: run.output,
|
|
10356
|
+
};
|
|
10357
|
+
});
|
|
10358
|
+
|
|
10359
|
+
const failing = results.filter(row => row.status === 'failed');
|
|
10360
|
+
if (revise) {
|
|
10361
|
+
const note = `task audit re-ran the stored verify and it failed; see ${receiptPath}`;
|
|
10362
|
+
for (const row of failing) {
|
|
10363
|
+
const revised = taskDb.reviseTask(db, {
|
|
10364
|
+
id: row.task_id,
|
|
10365
|
+
actor,
|
|
10366
|
+
note,
|
|
10367
|
+
allowDone: true,
|
|
10368
|
+
});
|
|
10369
|
+
row.revised = revised.revised === true;
|
|
10370
|
+
row.revise_reason = revised.revised ? null : revised.reason;
|
|
10371
|
+
}
|
|
10372
|
+
if (failing.some(row => row.revised)) writeDefaultProjection(taskDb, db);
|
|
10373
|
+
}
|
|
10374
|
+
|
|
10375
|
+
const summary = {
|
|
10376
|
+
sampled: results.length,
|
|
10377
|
+
passed: results.filter(row => row.status === 'passed').length,
|
|
10378
|
+
failed: failing.length,
|
|
10379
|
+
'skipped-no-verify': results.filter(row => row.status === 'skipped-no-verify').length,
|
|
10380
|
+
revised: results.filter(row => row.revised === true).length,
|
|
10381
|
+
};
|
|
10382
|
+
const receipt = {
|
|
10383
|
+
schema: 'atris.task_audit_receipt.v1',
|
|
10384
|
+
at,
|
|
10385
|
+
workspace_root: workspaceRoot,
|
|
10386
|
+
limit,
|
|
10387
|
+
revise,
|
|
10388
|
+
summary,
|
|
10389
|
+
failing_task_ids: failing.map(row => row.task_id),
|
|
10390
|
+
results,
|
|
10391
|
+
};
|
|
10392
|
+
const receiptFile = path.join(workspaceRoot, receiptPath);
|
|
10393
|
+
fs.mkdirSync(path.dirname(receiptFile), { recursive: true });
|
|
10394
|
+
fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
|
|
10395
|
+
|
|
10396
|
+
if (wantsJson(args)) {
|
|
10397
|
+
printJson({
|
|
10398
|
+
ok: true,
|
|
10399
|
+
action: 'task_audit',
|
|
10400
|
+
receipt_path: receiptPath,
|
|
10401
|
+
...receipt,
|
|
10402
|
+
});
|
|
10403
|
+
return;
|
|
10404
|
+
}
|
|
10405
|
+
console.log(`task audit: sampled ${summary.sampled}, passed ${summary.passed}, failed ${summary.failed}, skipped-no-verify ${summary['skipped-no-verify']}`);
|
|
10406
|
+
console.log(`failing task ids: ${failing.length ? failing.map(row => row.ref).join(', ') : 'none'}`);
|
|
10407
|
+
if (revise) console.log(`revised: ${summary.revised}`);
|
|
10408
|
+
console.log(`receipt: ${receiptPath}`);
|
|
10409
|
+
}
|
|
10410
|
+
|
|
8419
10411
|
function cmdReview(args) {
|
|
8420
10412
|
const pos = positional(args);
|
|
8421
10413
|
const id = pos[0];
|
|
@@ -8444,6 +10436,7 @@ function cmdReview(args) {
|
|
|
8444
10436
|
if (clearNextTask || (typeof nextTaskFlag === 'string' && !String(nextTaskFlag).trim())) clearedFields.push('next_task');
|
|
8445
10437
|
const proof = proofFlagValue(args);
|
|
8446
10438
|
const verify = textFlag(args, ['--verify']);
|
|
10439
|
+
guardExplicitActor('atris task review', flag(args, '--as'));
|
|
8447
10440
|
const actor = flag(args, '--as') || DEFAULT_OWNER;
|
|
8448
10441
|
const rewardValue = reward === true || reward === null ? 0 : reward;
|
|
8449
10442
|
if (agentProofOnlyMode() && Number(rewardValue) > 0) {
|
|
@@ -8481,7 +10474,11 @@ function cmdReview(args) {
|
|
|
8481
10474
|
clearedFields,
|
|
8482
10475
|
});
|
|
8483
10476
|
if (!result.reviewed) {
|
|
8484
|
-
|
|
10477
|
+
if (result.reason === 'judge_equals_worker') {
|
|
10478
|
+
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>`);
|
|
10479
|
+
} else {
|
|
10480
|
+
console.error(`review failed: ${result.reason}`);
|
|
10481
|
+
}
|
|
8485
10482
|
process.exit(1);
|
|
8486
10483
|
}
|
|
8487
10484
|
const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
|
|
@@ -8587,21 +10584,23 @@ function cmdEvents(args) {
|
|
|
8587
10584
|
const pos = positional(args);
|
|
8588
10585
|
let taskId = pos[0] || null;
|
|
8589
10586
|
const all = hasFlag(args, '--all');
|
|
10587
|
+
const everywhere = taskScopeEverywhere(args);
|
|
8590
10588
|
const rawLimit = flag(args, '--limit');
|
|
8591
10589
|
const explicitLimit = rawLimit && rawLimit !== true ? Number(rawLimit) : null;
|
|
8592
10590
|
const defaultRecentLimit = 24;
|
|
8593
|
-
const limit = explicitLimit || (taskId ? 500 : (all ? null : defaultRecentLimit));
|
|
10591
|
+
const limit = explicitLimit || (taskId ? 500 : (all || everywhere ? null : defaultRecentLimit));
|
|
8594
10592
|
const taskDb = getTaskDb();
|
|
8595
10593
|
const db = taskDb.open();
|
|
8596
10594
|
if (taskId) taskId = requireTaskId(taskDb, db, taskId, 'atris task events');
|
|
10595
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
8597
10596
|
const events = taskDb.listTaskEvents(db, {
|
|
8598
10597
|
taskId,
|
|
8599
|
-
workspaceRoot:
|
|
10598
|
+
workspaceRoot: taskId ? null : workspaceRoot,
|
|
8600
10599
|
limit,
|
|
8601
|
-
order: taskId || all ? 'asc' : 'desc',
|
|
10600
|
+
order: taskId || all || everywhere ? 'asc' : 'desc',
|
|
8602
10601
|
});
|
|
8603
10602
|
const refRows = taskDb.listTasks(db, {
|
|
8604
|
-
workspaceRoot:
|
|
10603
|
+
workspaceRoot: everywhere ? null : (taskId ? (taskDb.getTask(db, taskId) || {}).workspace_root : workspaceRoot),
|
|
8605
10604
|
});
|
|
8606
10605
|
const refById = taskDb.taskDisplayRefMap(refRows);
|
|
8607
10606
|
if (wantsJson(args)) {
|
|
@@ -8609,7 +10608,7 @@ function cmdEvents(args) {
|
|
|
8609
10608
|
ok: true,
|
|
8610
10609
|
action: 'events',
|
|
8611
10610
|
task_id: taskId,
|
|
8612
|
-
mode: taskId ? 'task' : (all ? 'ledger' : 'recent'),
|
|
10611
|
+
mode: taskId ? 'task' : (everywhere ? 'ledger_everywhere' : (all ? 'ledger' : 'recent')),
|
|
8613
10612
|
limit,
|
|
8614
10613
|
events,
|
|
8615
10614
|
});
|
|
@@ -8619,7 +10618,7 @@ function cmdEvents(args) {
|
|
|
8619
10618
|
console.log('(no task events)');
|
|
8620
10619
|
return;
|
|
8621
10620
|
}
|
|
8622
|
-
if (!taskId && !all) {
|
|
10621
|
+
if (!taskId && !all && !everywhere) {
|
|
8623
10622
|
console.log('TASK EVENTS');
|
|
8624
10623
|
console.log(`recent ${events.length} event${events.length === 1 ? '' : 's'} (use --all for the full ledger, --limit N to adjust)`);
|
|
8625
10624
|
console.log('');
|
|
@@ -8720,12 +10719,13 @@ function cmdLineage(args) {
|
|
|
8720
10719
|
function cmdExport(args) {
|
|
8721
10720
|
const out = flag(args, '--out') || path.join('.atris', 'state', 'tasks.projection.json');
|
|
8722
10721
|
const all = hasFlag(args, '--all');
|
|
10722
|
+
const everywhere = taskScopeEverywhere(args);
|
|
8723
10723
|
const taskDb = getTaskDb();
|
|
8724
10724
|
const db = taskDb.open();
|
|
8725
10725
|
const outPath = path.resolve(String(out));
|
|
8726
10726
|
const projection = enrichTaskProjection(taskDb.taskProjection(db, {
|
|
8727
|
-
workspaceRoot:
|
|
8728
|
-
limit: 500,
|
|
10727
|
+
workspaceRoot: scopedWorkspaceRoot(taskDb, args, { everywhere }),
|
|
10728
|
+
limit: all ? null : 500,
|
|
8729
10729
|
}));
|
|
8730
10730
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
8731
10731
|
fs.writeFileSync(outPath, JSON.stringify(projection, null, 2) + '\n', 'utf8');
|
|
@@ -8902,21 +10902,61 @@ function taskRenderSummaryLine(counts) {
|
|
|
8902
10902
|
].join('; ');
|
|
8903
10903
|
}
|
|
8904
10904
|
|
|
10905
|
+
function refreshExistingTodoMarkdown(taskDb, db, workspaceRoot) {
|
|
10906
|
+
const ws = workspaceRoot || taskDb.workspaceRoot();
|
|
10907
|
+
const outPath = path.join(ws, 'atris', 'TODO.md');
|
|
10908
|
+
if (!fs.existsSync(outPath)) return null;
|
|
10909
|
+
const rows = taskDb.listTasks(db, { workspaceRoot: ws, limit: 500 });
|
|
10910
|
+
const refRows = taskDb.listTasks(db, { workspaceRoot: ws });
|
|
10911
|
+
const existingTodo = fs.readFileSync(outPath, 'utf8');
|
|
10912
|
+
const preservedSections = [];
|
|
10913
|
+
const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
|
|
10914
|
+
if (endgameSection) preservedSections.push(endgameSection);
|
|
10915
|
+
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
|
|
10916
|
+
const markdown = taskDb.renderTodoMarkdown([...rows, ...markdownRows], {
|
|
10917
|
+
refRows,
|
|
10918
|
+
preservedSections,
|
|
10919
|
+
});
|
|
10920
|
+
fs.writeFileSync(outPath, markdown, 'utf8');
|
|
10921
|
+
return outPath;
|
|
10922
|
+
}
|
|
10923
|
+
|
|
10924
|
+
// TODO.md is a projection of task-db state. Keep this best-effort and silent
|
|
10925
|
+
// because it runs after every successful mutating task command.
|
|
10926
|
+
function autoRenderTodoFromDb(cwd = process.cwd()) {
|
|
10927
|
+
try {
|
|
10928
|
+
const atrisDir = path.join(cwd, 'atris');
|
|
10929
|
+
if (!fs.existsSync(atrisDir)) return null;
|
|
10930
|
+
const taskDb = getTaskDb();
|
|
10931
|
+
const db = taskDb.open();
|
|
10932
|
+
const outPath = path.join(atrisDir, 'TODO.md');
|
|
10933
|
+
if (fs.existsSync(outPath)) return refreshExistingTodoMarkdown(taskDb, db, cwd);
|
|
10934
|
+
const rows = taskDb.listTasks(db, { workspaceRoot: cwd, limit: 500 });
|
|
10935
|
+
const refRows = taskDb.listTasks(db, { workspaceRoot: cwd });
|
|
10936
|
+
fs.writeFileSync(outPath, taskDb.renderTodoMarkdown(rows, { refRows }), 'utf8');
|
|
10937
|
+
return outPath;
|
|
10938
|
+
} catch {
|
|
10939
|
+
return null;
|
|
10940
|
+
}
|
|
10941
|
+
}
|
|
10942
|
+
|
|
8905
10943
|
function cmdRender(args) {
|
|
8906
10944
|
const out = flag(args, '--out') || path.join('atris', 'TODO.md');
|
|
8907
10945
|
const all = hasFlag(args, '--all');
|
|
10946
|
+
const everywhere = taskScopeEverywhere(args);
|
|
8908
10947
|
const doneLimitRaw = flag(args, '--done-limit');
|
|
8909
10948
|
const doneLimit = doneLimitRaw && doneLimitRaw !== true ? Number(doneLimitRaw) : undefined;
|
|
8910
10949
|
const failedLimitRaw = flag(args, '--failed-limit');
|
|
8911
10950
|
const failedLimit = failedLimitRaw && failedLimitRaw !== true ? Number(failedLimitRaw) : undefined;
|
|
8912
10951
|
const taskDb = getTaskDb();
|
|
8913
10952
|
const db = taskDb.open();
|
|
10953
|
+
const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
|
|
8914
10954
|
const rows = taskDb.listTasks(db, {
|
|
8915
|
-
workspaceRoot
|
|
8916
|
-
limit: 500,
|
|
10955
|
+
workspaceRoot,
|
|
10956
|
+
limit: all ? null : 500,
|
|
8917
10957
|
});
|
|
8918
10958
|
const refRows = taskDb.listTasks(db, {
|
|
8919
|
-
workspaceRoot
|
|
10959
|
+
workspaceRoot,
|
|
8920
10960
|
});
|
|
8921
10961
|
const outPath = path.resolve(String(out));
|
|
8922
10962
|
const existingTodo = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : '';
|
|
@@ -9525,6 +11565,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9525
11565
|
owner,
|
|
9526
11566
|
reviewer,
|
|
9527
11567
|
all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
|
|
11568
|
+
everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
|
|
9528
11569
|
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
9529
11570
|
scope,
|
|
9530
11571
|
});
|
|
@@ -9540,6 +11581,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9540
11581
|
owner,
|
|
9541
11582
|
reviewer,
|
|
9542
11583
|
all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
|
|
11584
|
+
everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
|
|
9543
11585
|
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
9544
11586
|
scope,
|
|
9545
11587
|
});
|
|
@@ -9570,6 +11612,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9570
11612
|
owner,
|
|
9571
11613
|
reviewer,
|
|
9572
11614
|
all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
|
|
11615
|
+
everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
|
|
9573
11616
|
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
9574
11617
|
scope,
|
|
9575
11618
|
});
|
|
@@ -9880,10 +11923,15 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9880
11923
|
if (proofIssue) return sendProofIssue(res, proof, proofIssue);
|
|
9881
11924
|
const nextTaskInput = normalizeReviewNextTaskInput(body.next);
|
|
9882
11925
|
const actor = String(body.actor || DEFAULT_OWNER);
|
|
11926
|
+
const resultText = String(body.result || '').replace(/\s+/g, ' ').trim();
|
|
11927
|
+
if (resultText) {
|
|
11928
|
+
const resultIssue = resultSentenceIssue(resultText);
|
|
11929
|
+
if (resultIssue) return sendJson(res, 400, { ok: false, reason: 'weak_result', detail: resultIssue });
|
|
11930
|
+
}
|
|
9883
11931
|
const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
|
|
9884
11932
|
actor,
|
|
9885
11933
|
proof,
|
|
9886
|
-
changed: body.changed ||
|
|
11934
|
+
changed: body.changed || resultText || body.done,
|
|
9887
11935
|
checked: body.checked || body.check || body.verified,
|
|
9888
11936
|
passed: body.passed || body.pass,
|
|
9889
11937
|
failed: body.failed || body.fail,
|
|
@@ -9901,6 +11949,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9901
11949
|
lesson: String(body.lesson || ''),
|
|
9902
11950
|
nextTask: nextTaskInput.nextTask,
|
|
9903
11951
|
resultTrace: resultTrace && resultTrace.trace,
|
|
11952
|
+
result: resultText,
|
|
9904
11953
|
landing: body.landing || {
|
|
9905
11954
|
happened: body.happened,
|
|
9906
11955
|
checked: body.checked,
|
|
@@ -10041,7 +12090,7 @@ function cmdServe(args) {
|
|
|
10041
12090
|
});
|
|
10042
12091
|
}
|
|
10043
12092
|
|
|
10044
|
-
async function
|
|
12093
|
+
async function runTaskCommand(args) {
|
|
10045
12094
|
const raw = args || [];
|
|
10046
12095
|
if (raw.includes('--help') || raw.includes('-h')) return help();
|
|
10047
12096
|
const first = raw[0];
|
|
@@ -10114,6 +12163,10 @@ async function run(args) {
|
|
|
10114
12163
|
return cmdPlanPreview(rest);
|
|
10115
12164
|
case 'note': return cmdNote(rest);
|
|
10116
12165
|
case 'say': return cmdNote(rest);
|
|
12166
|
+
case 'retitle': return cmdRetitle(rest);
|
|
12167
|
+
case 'tag':
|
|
12168
|
+
case 'tags':
|
|
12169
|
+
return cmdTag(rest);
|
|
10117
12170
|
case 'show': return cmdShow(rest);
|
|
10118
12171
|
case 'inspect': return cmdInspect(rest);
|
|
10119
12172
|
case 'page': return cmdPage(rest);
|
|
@@ -10122,11 +12175,18 @@ async function run(args) {
|
|
|
10122
12175
|
case 'chat-review':
|
|
10123
12176
|
return cmdReviewChat(rest);
|
|
10124
12177
|
case 'ready': return cmdReady(rest);
|
|
12178
|
+
case 'receipt': return cmdTaskReceipt(rest);
|
|
10125
12179
|
case 'result': return cmdResult(rest);
|
|
10126
12180
|
case 'accept': return cmdAccept(rest);
|
|
12181
|
+
case 'landing':
|
|
12182
|
+
case 'land-review':
|
|
12183
|
+
return cmdLanding(rest);
|
|
10127
12184
|
case 'auto-accept-certified':
|
|
10128
12185
|
case 'auto-accept':
|
|
10129
12186
|
return cmdAutoAcceptCertified(rest);
|
|
12187
|
+
case 'sweep':
|
|
12188
|
+
return cmdSweep(rest);
|
|
12189
|
+
case 'audit': return cmdAudit(rest);
|
|
10130
12190
|
case 'certify-verified':
|
|
10131
12191
|
return cmdCertifyVerified(rest);
|
|
10132
12192
|
case 'accept-group':
|
|
@@ -10135,6 +12195,9 @@ async function run(args) {
|
|
|
10135
12195
|
case 'done': return cmdDone(rest);
|
|
10136
12196
|
case 'finish': return cmdFinish(rest);
|
|
10137
12197
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
12198
|
+
case 'archive': return cmdArchive(rest);
|
|
12199
|
+
case 'clear-done': return cmdClearDone(rest);
|
|
12200
|
+
case 'relabel-archived': return cmdRelabelArchived(rest);
|
|
10138
12201
|
case 'review': return cmdReview(rest);
|
|
10139
12202
|
case 'reviews':
|
|
10140
12203
|
case 'review-queue':
|
|
@@ -10168,4 +12231,34 @@ async function run(args) {
|
|
|
10168
12231
|
}
|
|
10169
12232
|
}
|
|
10170
12233
|
|
|
10171
|
-
|
|
12234
|
+
const MUTATING_TASK_COMMANDS = new Set([
|
|
12235
|
+
'add', 'new', 'delegate', 'assign', 'plan', 'do', 'backlog', 'unplan',
|
|
12236
|
+
'clear-plan', 'clearplan', 'claim', 'start', 'release', 'unclaim', 'next',
|
|
12237
|
+
'continue-work', 'continue', 'chat', 'note', 'say', 'retitle', 'tag', 'tags', 'step',
|
|
12238
|
+
'ready', 'result', 'accept', 'landing', 'land-review', 'auto-accept-certified',
|
|
12239
|
+
'auto-accept', 'sweep', 'audit', 'certify-verified', 'accept-group', 'revise',
|
|
12240
|
+
'done', 'finish', 'fail', 'archive', 'clear-done', 'relabel-archived', 'review', 'import',
|
|
12241
|
+
'setup', 'review-lane-act', 'review-act', 'act-review', 'review-lane-loop',
|
|
12242
|
+
'review-loop', 'loop-review', 'review-lane-run', 'review-run', 'run-review',
|
|
12243
|
+
]);
|
|
12244
|
+
|
|
12245
|
+
async function run(args) {
|
|
12246
|
+
const raw = args || [];
|
|
12247
|
+
const sub = !raw[0] || raw[0].startsWith('--') ? 'desk' : raw[0];
|
|
12248
|
+
const result = await runTaskCommand(raw);
|
|
12249
|
+
const skipsRender = sub === 'clear-done' && hasFlag(raw, '--dry-run');
|
|
12250
|
+
if (MUTATING_TASK_COMMANDS.has(sub) && !skipsRender) autoRenderTodoFromDb();
|
|
12251
|
+
return result;
|
|
12252
|
+
}
|
|
12253
|
+
|
|
12254
|
+
module.exports = {
|
|
12255
|
+
run,
|
|
12256
|
+
taskDayGroups,
|
|
12257
|
+
taskDayTextGroups,
|
|
12258
|
+
taskDayTitle,
|
|
12259
|
+
taskReviewLanding,
|
|
12260
|
+
taskReviewLandingLines,
|
|
12261
|
+
delegateTask,
|
|
12262
|
+
AGENT_ENV_MARKERS,
|
|
12263
|
+
autoRenderTodoFromDb,
|
|
12264
|
+
};
|