atris 3.37.1 → 3.40.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 +25 -6
- package/README.md +2 -0
- package/atris/PERSONA.md +11 -11
- package/atris.md +17 -2
- package/ax +160 -34
- package/bin/atris.js +78 -6
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +13 -18
- package/commands/autoland.js +76 -47
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/bench.js +10 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +452 -12
- package/commands/chat-scan.js +5 -7
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +8 -12
- package/commands/computer.js +6 -5
- package/commands/console.js +64 -11
- package/commands/decide.js +166 -0
- package/commands/deck.js +30 -12
- package/commands/dream.js +0 -7
- package/commands/drill.js +14 -25
- package/commands/engine.js +206 -26
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +17 -7
- package/commands/goal.js +0 -6
- package/commands/harvest.js +1 -5
- package/commands/improve.js +29 -8
- package/commands/init.js +41 -37
- package/commands/integrations.js +0 -7
- package/commands/land.js +77 -20
- package/commands/launchpad.js +1 -16
- package/commands/lesson.js +17 -7
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +622 -573
- package/commands/mission.js +790 -314
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +3599 -424
- package/commands/play.js +6 -6
- package/commands/probe.js +12 -6
- package/commands/pulse.js +88 -64
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/release.js +10 -9
- package/commands/report.js +0 -1
- package/commands/router.js +320 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/site-deploy.js +870 -0
- package/commands/site.js +11 -2
- package/commands/skill.js +7 -2
- package/commands/slop.js +237 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +4 -20
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +999 -595
- package/commands/taste.js +101 -0
- package/commands/teach.js +86 -0
- package/commands/team.js +83 -3
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/vercel.js +4 -2
- package/commands/visualize.js +0 -5
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -28
- package/commands/wiki.js +42 -15
- package/commands/wish.js +0 -1
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +88 -28
- package/commands/xp.js +28 -30
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +126 -0
- package/lib/activity-stream.js +0 -2
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +261 -3
- package/lib/autoland.js +66 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +18 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +7 -2
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +157 -28
- package/lib/experiments/daily.js +0 -6
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +275 -42
- package/lib/functional-owner.js +0 -3
- package/lib/git-spawn.js +15 -0
- package/lib/inspect-fields.js +0 -1
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +11 -19
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +296 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +47 -6
- package/lib/orb-context.js +14 -4
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/pack-capabilities.js +685 -0
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +51 -3
- package/lib/runner-command.js +0 -10
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +45 -14
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +137 -7
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/team-presence.js +0 -1
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +129 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +21 -7
- package/lib/wish-delegate.js +27 -21
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +95 -8
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
package/commands/task.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const http = require('http');
|
|
8
|
+
const { execSync } = require('node:child_process');
|
|
8
9
|
const path = require('path');
|
|
9
10
|
const os = require('os');
|
|
11
|
+
const { hasFlag } = require('../lib/arg-parser');
|
|
10
12
|
const { taskProofState } = require('../lib/task-proof');
|
|
11
13
|
const {
|
|
12
14
|
evaluateAutoAccept,
|
|
@@ -17,10 +19,11 @@ const {
|
|
|
17
19
|
runVerifyCommandCached,
|
|
18
20
|
DENIED_TAGS,
|
|
19
21
|
} = require('../lib/auto-accept-certified');
|
|
22
|
+
const { evaluateAcceptVerify } = require('../lib/accept-verify-gate');
|
|
20
23
|
const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
|
|
21
24
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
22
25
|
const reviewIntegrity = require('../lib/review-integrity');
|
|
23
|
-
const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord } = require('../lib/voice-gate');
|
|
26
|
+
const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord, plainLandingReason } = require('../lib/voice-gate');
|
|
24
27
|
const {
|
|
25
28
|
normalizeOwnerSlug,
|
|
26
29
|
resolveFunctionalOwner: resolveFunctionalTaskOwner,
|
|
@@ -34,6 +37,11 @@ const {
|
|
|
34
37
|
inspectTextLines,
|
|
35
38
|
buildInspectPayload,
|
|
36
39
|
} = require('../lib/inspect-fields');
|
|
40
|
+
const {
|
|
41
|
+
isDecisionTask,
|
|
42
|
+
decisionMarkerFor,
|
|
43
|
+
DECISION_REFUSE_REASON,
|
|
44
|
+
} = require('../lib/task-decision');
|
|
37
45
|
|
|
38
46
|
const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
|
|
39
47
|
|| process.env.USER
|
|
@@ -128,7 +136,7 @@ function taskUsageText() {
|
|
|
128
136
|
return `
|
|
129
137
|
atris task - durable local task state (SQLite, gitignored)
|
|
130
138
|
|
|
131
|
-
golden path (
|
|
139
|
+
golden path (one tick, by cron or by hand):
|
|
132
140
|
atris task delegate "fix the login bug" --to <member>
|
|
133
141
|
atris task claim <id> --as <member>
|
|
134
142
|
... build ...
|
|
@@ -136,7 +144,7 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
136
144
|
atris autoland tick # second check runs, task lands
|
|
137
145
|
|
|
138
146
|
atris task Show the task desk
|
|
139
|
-
atris task new "<title>"
|
|
147
|
+
atris task new "<title>" [--verify <cmd>] Create a task; weak or missing checks are marked degraded
|
|
140
148
|
atris task next [--tag <tag>] [--create-next]
|
|
141
149
|
Claim/show next open task; optionally create the generated Endgame fallback
|
|
142
150
|
atris task continue-work <id> Create/reuse a certified Review follow-up task
|
|
@@ -168,8 +176,8 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
168
176
|
atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
|
|
169
177
|
atris task revise <id> --note "..." Send reviewed work back to Do
|
|
170
178
|
|
|
171
|
-
atris task add "<title>" [--tag <tag>] [--goal-id <id>] Create a task
|
|
172
|
-
atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] Create assigned work
|
|
179
|
+
atris task add "<title>" [--tag <tag>] [--goal-id <id>] [--verify <cmd>] Create a task
|
|
180
|
+
atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] [--verify <cmd>] Create assigned work
|
|
173
181
|
atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
|
|
174
182
|
Record a task-owned Plan stage
|
|
175
183
|
atris task do <id> --as <owner> --first-move "..."
|
|
@@ -212,6 +220,7 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
212
220
|
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
213
221
|
--from-failed opts in to relabel a fail-closed row (never done)
|
|
214
222
|
atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
|
|
223
|
+
atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
|
|
215
224
|
atris task relabel-archived [--dry-run|--apply]
|
|
216
225
|
One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
|
|
217
226
|
atris task finish <id> --proof "..." Legacy alias for done with proof
|
|
@@ -269,10 +278,6 @@ function flag(args, name) {
|
|
|
269
278
|
return args[i + 1] || true;
|
|
270
279
|
}
|
|
271
280
|
|
|
272
|
-
function hasFlag(args, name) {
|
|
273
|
-
return args.indexOf(name) !== -1;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
281
|
function taskScopeEverywhere(args = [], options = {}) {
|
|
277
282
|
if (options.everywhere !== undefined) return Boolean(options.everywhere);
|
|
278
283
|
return hasFlag(args, '--everywhere');
|
|
@@ -880,10 +885,15 @@ function certifiedReviewNextAction(nextTaskTitle) {
|
|
|
880
885
|
function proofBoundaryBlockedEvaluation(task) {
|
|
881
886
|
const missionXpBoundary = missionXpProofBoundaryEvaluation(task);
|
|
882
887
|
if (missionXpBoundary) return missionXpBoundary;
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
|
|
888
|
+
// This is a render-path probe for the boundary reason only. strictVerify
|
|
889
|
+
// alone is not enough to keep it read-only because probationary actors
|
|
890
|
+
// still require strict verification. Never execute a stored verifier while
|
|
891
|
+
// drawing task reviews, status, queues, or projections.
|
|
892
|
+
const evaluation = evaluateAutoAccept(task, {
|
|
893
|
+
strictVerify: false,
|
|
894
|
+
minPasses: 0,
|
|
895
|
+
executeVerify: false,
|
|
896
|
+
});
|
|
887
897
|
return evaluation && evaluation.reason === 'proof_unmerged_or_draft_pr_boundary'
|
|
888
898
|
? evaluation
|
|
889
899
|
: null;
|
|
@@ -1583,9 +1593,74 @@ function enrichTaskProjection(projection) {
|
|
|
1583
1593
|
},
|
|
1584
1594
|
streams: buildTaskStreams(enrichedTasks, goalSource.goals),
|
|
1585
1595
|
tasks: enrichedTasks,
|
|
1596
|
+
missions: projectionMissions(root),
|
|
1597
|
+
wishes: projectionWishes(root),
|
|
1586
1598
|
};
|
|
1587
1599
|
}
|
|
1588
1600
|
|
|
1601
|
+
// The desktop app renders projection.missions but the writer never filled it,
|
|
1602
|
+
// so live workers were invisible on every dashboard. Best-effort: a broken
|
|
1603
|
+
// mission or wish store must never take the task projection down with it.
|
|
1604
|
+
const PROJECTION_MISSION_LIMIT = 50;
|
|
1605
|
+
const PROJECTION_WISH_LIMIT = 30;
|
|
1606
|
+
const PROJECTION_CLIP = 240;
|
|
1607
|
+
|
|
1608
|
+
function clipForProjection(value) {
|
|
1609
|
+
const text = String(value || '');
|
|
1610
|
+
return text.length > PROJECTION_CLIP ? `${text.slice(0, PROJECTION_CLIP - 3)}...` : text;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function projectionMissions(root) {
|
|
1614
|
+
try {
|
|
1615
|
+
const { listMissions } = require('./mission.js');
|
|
1616
|
+
const recentCutoff = Date.now() - 48 * 60 * 60 * 1000;
|
|
1617
|
+
return listMissions(root)
|
|
1618
|
+
.filter(mission => {
|
|
1619
|
+
const status = String(mission.status || '');
|
|
1620
|
+
if (!['complete', 'stopped'].includes(status)) return true;
|
|
1621
|
+
const updated = Date.parse(mission.updated_at || mission.created_at || '') || 0;
|
|
1622
|
+
return updated >= recentCutoff;
|
|
1623
|
+
})
|
|
1624
|
+
.slice(0, PROJECTION_MISSION_LIMIT)
|
|
1625
|
+
.map(mission => ({
|
|
1626
|
+
id: mission.id,
|
|
1627
|
+
objective: clipForProjection(String(mission.objective || '')),
|
|
1628
|
+
status: mission.status || null,
|
|
1629
|
+
owner: mission.owner || null,
|
|
1630
|
+
runner: mission.runner || null,
|
|
1631
|
+
cadence: mission.cadence || null,
|
|
1632
|
+
lane: mission.lane || null,
|
|
1633
|
+
next_action: clipForProjection(String(mission.next_action || '')),
|
|
1634
|
+
last_tick_at: mission.last_tick_at || null,
|
|
1635
|
+
last_tick_status: mission.last_tick_status || null,
|
|
1636
|
+
receipt_path: mission.receipt_path || null,
|
|
1637
|
+
wish_id: mission.wish_id || null,
|
|
1638
|
+
created_at: mission.created_at || null,
|
|
1639
|
+
updated_at: mission.updated_at || null,
|
|
1640
|
+
}));
|
|
1641
|
+
} catch {
|
|
1642
|
+
return [];
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function projectionWishes(root) {
|
|
1647
|
+
try {
|
|
1648
|
+
const { readWishes } = require('../lib/wish-store');
|
|
1649
|
+
return readWishes(root)
|
|
1650
|
+
.filter(wish => String(wish.status || '') !== 'closed')
|
|
1651
|
+
.slice(-PROJECTION_WISH_LIMIT)
|
|
1652
|
+
.map(wish => ({
|
|
1653
|
+
id: wish.id,
|
|
1654
|
+
n: wish.n ?? null,
|
|
1655
|
+
text: clipForProjection(String(wish.text || '')),
|
|
1656
|
+
status: wish.status || null,
|
|
1657
|
+
created_at: wish.first_ts || wish.ts || null,
|
|
1658
|
+
}));
|
|
1659
|
+
} catch {
|
|
1660
|
+
return [];
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1589
1664
|
function taskTypeForCloud(task) {
|
|
1590
1665
|
const tag = String(task.tag || '').toLowerCase();
|
|
1591
1666
|
if (['inbound', 'outbound', 'creative', 'improvement'].includes(tag)) return tag;
|
|
@@ -5105,13 +5180,49 @@ function evaluateReviewAutoAccept(task, root) {
|
|
|
5105
5180
|
if (!protectedGate.ok) return { eligible: false, ref, reason: protectedGate.reason };
|
|
5106
5181
|
const sizeGate = reviewAutoAcceptSizeGate(task, root);
|
|
5107
5182
|
if (!sizeGate.ok) return { eligible: false, ref, reason: sizeGate.reason, size: sizeGate.stats };
|
|
5108
|
-
|
|
5183
|
+
// executeVerify: false — this is a READ path. The verdict comes from
|
|
5184
|
+
// metadata.verify_cache, stamped by the two lanes allowed to spawn
|
|
5185
|
+
// checks (certify-verified, autoland landing re-check). Spawning here is
|
|
5186
|
+
// what turned `atris task reviews` into a fork-bomb multiplier.
|
|
5187
|
+
const evaluation = evaluateAutoAccept(task, { strictVerify: true, executeVerify: false });
|
|
5109
5188
|
if (!evaluation.eligible) {
|
|
5110
5189
|
return {
|
|
5111
5190
|
...evaluation,
|
|
5112
5191
|
size: sizeGate.stats,
|
|
5113
5192
|
};
|
|
5114
5193
|
}
|
|
5194
|
+
if (evaluation.verification_pending) {
|
|
5195
|
+
const verdict = cachedVerifyVerdict(task, root);
|
|
5196
|
+
if (!verdict.fresh) {
|
|
5197
|
+
return {
|
|
5198
|
+
eligible: false,
|
|
5199
|
+
ref,
|
|
5200
|
+
reason: 'verification_pending',
|
|
5201
|
+
verify: verdict.command || null,
|
|
5202
|
+
next_action: 'no fresh executed verify on record; `atris task certify-verified` or the autoland tick runs the stored check and stamps metadata.verify_cache',
|
|
5203
|
+
size: sizeGate.stats,
|
|
5204
|
+
};
|
|
5205
|
+
}
|
|
5206
|
+
if (!verdict.ok) {
|
|
5207
|
+
return {
|
|
5208
|
+
eligible: false,
|
|
5209
|
+
ref,
|
|
5210
|
+
reason: verdict.reason || 'verify_failed',
|
|
5211
|
+
verify: verdict.command,
|
|
5212
|
+
verify_cache: verdict,
|
|
5213
|
+
size: sizeGate.stats,
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
return {
|
|
5217
|
+
...evaluation,
|
|
5218
|
+
verification_pending: false,
|
|
5219
|
+
verify_cache: verdict,
|
|
5220
|
+
eligible: true,
|
|
5221
|
+
reason: 'certified_small',
|
|
5222
|
+
policy: REVIEW_AUTO_ACCEPT_POLICY,
|
|
5223
|
+
size: sizeGate.stats,
|
|
5224
|
+
};
|
|
5225
|
+
}
|
|
5115
5226
|
return {
|
|
5116
5227
|
...evaluation,
|
|
5117
5228
|
eligible: true,
|
|
@@ -5442,19 +5553,17 @@ function taskReviewLandingLines(item) {
|
|
|
5442
5553
|
function cmdReviews(args) {
|
|
5443
5554
|
const taskDb = getTaskDb();
|
|
5444
5555
|
const db = taskDb.open();
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
last_count: autoAcceptRollup.count,
|
|
5457
|
-
});
|
|
5556
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
5557
|
+
// Reviews is a view, not an execution lane. Certification and landing are
|
|
5558
|
+
// persisted by certify-verified, explicit receipt/ready commands, sweep,
|
|
5559
|
+
// and autoland; this command only reports that stored state.
|
|
5560
|
+
const autoAcceptView = {
|
|
5561
|
+
enabled: false,
|
|
5562
|
+
read_only: true,
|
|
5563
|
+
accepted_now: 0,
|
|
5564
|
+
accepted_since_last_look: 0,
|
|
5565
|
+
results: [],
|
|
5566
|
+
};
|
|
5458
5567
|
const groupByRaw = flag(args, '--group-by');
|
|
5459
5568
|
if (groupByRaw) {
|
|
5460
5569
|
const key = reviewGroupKey(groupByRaw);
|
|
@@ -5464,20 +5573,12 @@ function cmdReviews(args) {
|
|
|
5464
5573
|
ok: true,
|
|
5465
5574
|
action: 'review_groups',
|
|
5466
5575
|
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
|
-
},
|
|
5576
|
+
autoaccept: autoAcceptView,
|
|
5473
5577
|
groups,
|
|
5474
5578
|
});
|
|
5475
5579
|
return;
|
|
5476
5580
|
}
|
|
5477
5581
|
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
|
-
}
|
|
5481
5582
|
const visibleGroups = groups.groups.slice(0, reviewGroupTextLimit(args, groups.groups.length));
|
|
5482
5583
|
visibleGroups.forEach((g, index) => {
|
|
5483
5584
|
console.log('');
|
|
@@ -5498,12 +5599,7 @@ function cmdReviews(args) {
|
|
|
5498
5599
|
ok: true,
|
|
5499
5600
|
action: 'review_queue',
|
|
5500
5601
|
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
|
-
},
|
|
5602
|
+
autoaccept: autoAcceptView,
|
|
5507
5603
|
queue,
|
|
5508
5604
|
});
|
|
5509
5605
|
return;
|
|
@@ -5512,9 +5608,6 @@ function cmdReviews(args) {
|
|
|
5512
5608
|
const blockedItems = queue.items.filter(item => item.queue_role === 'blocked');
|
|
5513
5609
|
if (!approvalItems.length && !blockedItems.length) {
|
|
5514
5610
|
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
|
-
}
|
|
5518
5611
|
return;
|
|
5519
5612
|
}
|
|
5520
5613
|
if (queue.counts.certified > 0) {
|
|
@@ -5528,9 +5621,6 @@ function cmdReviews(args) {
|
|
|
5528
5621
|
? 'one more is almost ready; a second check is still running.'
|
|
5529
5622
|
: `${numberWord(queue.counts.blocking)} more are almost ready; second checks are still running.`).text);
|
|
5530
5623
|
}
|
|
5531
|
-
if (autoAcceptRollup.count > 0) {
|
|
5532
|
-
console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
|
|
5533
|
-
}
|
|
5534
5624
|
approvalItems.forEach((item, index) => {
|
|
5535
5625
|
if (index > 0) console.log('');
|
|
5536
5626
|
const ref = item.display_id || taskRef(item.id);
|
|
@@ -5555,7 +5645,7 @@ function cmdReviews(args) {
|
|
|
5555
5645
|
if (item.accept_command) {
|
|
5556
5646
|
console.log(` say yes: atris task accept ${item.display_id || taskRef(item.id)}`);
|
|
5557
5647
|
} else if (item.blocked_accept_reason) {
|
|
5558
|
-
console.log(` approve: blocked
|
|
5648
|
+
console.log(` approve: blocked; ${plainLandingReason(item.blocked_accept_reason)}`);
|
|
5559
5649
|
console.log(` rework: ${item.revise_command}`);
|
|
5560
5650
|
}
|
|
5561
5651
|
});
|
|
@@ -5764,7 +5854,8 @@ function formatTaskLine(task) {
|
|
|
5764
5854
|
const owner = task.claimed_by ? ` @${task.claimed_by}` : '';
|
|
5765
5855
|
const assigned = !task.claimed_by && taskAssignee(task) ? ` -> ${taskAssignee(task)}` : '';
|
|
5766
5856
|
const tag = task.tag ? ` #${task.tag}` : '';
|
|
5767
|
-
|
|
5857
|
+
const decision = decisionMarkerFor(task) ? ` ${decisionMarkerFor(task)}` : '';
|
|
5858
|
+
return `${taskRef(task)}${owner}${assigned}${tag}${decision} ${task.title}`;
|
|
5768
5859
|
}
|
|
5769
5860
|
|
|
5770
5861
|
function cmdStatus(args) {
|
|
@@ -5859,7 +5950,8 @@ function renderTaskDesk(rows, refRows = rows) {
|
|
|
5859
5950
|
const owner = r.claimed_by ? ` @${r.claimed_by}` : '';
|
|
5860
5951
|
const assigned = !r.claimed_by && taskAssignee(r) ? ` -> ${taskAssignee(r)}` : '';
|
|
5861
5952
|
const tag = r.tag ? ` #${r.tag}` : '';
|
|
5862
|
-
|
|
5953
|
+
const decision = decisionMarkerFor(r) ? ` ${decisionMarkerFor(r)}` : '';
|
|
5954
|
+
console.log(`${r.status.padEnd(7)} ${taskRef(r)}${owner}${assigned}${tag}${decision}`);
|
|
5863
5955
|
console.log(` ${r.title}`);
|
|
5864
5956
|
}
|
|
5865
5957
|
if (active.length === 0) console.log('clear no active tasks');
|
|
@@ -5877,12 +5969,14 @@ function cmdAdd(args) {
|
|
|
5877
5969
|
const tag = flag(args, '--tag');
|
|
5878
5970
|
const goalId = flag(args, '--goal-id');
|
|
5879
5971
|
const goalObjective = flag(args, '--goal-objective') || flag(args, '--goal');
|
|
5972
|
+
const verify = flag(args, '--verify');
|
|
5880
5973
|
const metadata = {};
|
|
5881
5974
|
if (goalId && goalId !== true) metadata.goal_id = String(goalId);
|
|
5882
5975
|
if (goalObjective && goalObjective !== true) {
|
|
5883
5976
|
metadata.task_goal = String(goalObjective);
|
|
5884
5977
|
metadata.goal_objective = String(goalObjective);
|
|
5885
5978
|
}
|
|
5979
|
+
if (typeof verify === 'string' && verify.trim()) metadata.verify = verify.trim();
|
|
5886
5980
|
const taskDb = getTaskDb();
|
|
5887
5981
|
const db = taskDb.open();
|
|
5888
5982
|
const ws = taskDb.workspaceRoot();
|
|
@@ -5949,6 +6043,7 @@ function delegateTask(args, options = {}) {
|
|
|
5949
6043
|
const note = flag(args, '--note');
|
|
5950
6044
|
const goalId = flag(args, '--goal-id');
|
|
5951
6045
|
const goalObjective = flag(args, '--goal-objective') || flag(args, '--goal');
|
|
6046
|
+
const verify = flag(args, '--verify');
|
|
5952
6047
|
const claimNow = hasFlag(args, '--claim');
|
|
5953
6048
|
const requestedOwner = flag(args, '--to') || flag(args, '--as');
|
|
5954
6049
|
const explicitExecutedBy = flag(args, '--executed-by');
|
|
@@ -5986,6 +6081,7 @@ function delegateTask(args, options = {}) {
|
|
|
5986
6081
|
metadata.task_goal = String(goalObjective);
|
|
5987
6082
|
metadata.goal_objective = String(goalObjective);
|
|
5988
6083
|
}
|
|
6084
|
+
if (typeof verify === 'string' && verify.trim()) metadata.verify = verify.trim();
|
|
5989
6085
|
const result = taskDb.addTask(db, {
|
|
5990
6086
|
title,
|
|
5991
6087
|
tag: typeof tag === 'string' ? tag : null,
|
|
@@ -6141,7 +6237,8 @@ function cmdDay(args) {
|
|
|
6141
6237
|
for (const task of group.tasks) {
|
|
6142
6238
|
const tag = task.tag ? ` #${task.tag}` : '';
|
|
6143
6239
|
const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
|
|
6144
|
-
|
|
6240
|
+
const decision = decisionMarkerFor(task) ? ` ${decisionMarkerFor(task)}` : '';
|
|
6241
|
+
console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag}${decision} ${taskDayTitle(task.title)}`);
|
|
6145
6242
|
}
|
|
6146
6243
|
}
|
|
6147
6244
|
if (textView.hiddenTasks > 0) {
|
|
@@ -6210,7 +6307,8 @@ function cmdList(args) {
|
|
|
6210
6307
|
for (const r of displayRows) {
|
|
6211
6308
|
const claim = r.claimed_by ? ` [${r.claimed_by}]` : '';
|
|
6212
6309
|
const tag = r.tag ? ` #${r.tag}` : '';
|
|
6213
|
-
|
|
6310
|
+
const decision = decisionMarkerFor(r) ? ` ${decisionMarkerFor(r)}` : '';
|
|
6311
|
+
console.log(`${r.status.padEnd(8)} ${taskRef(r)}${claim}${tag}${decision}\t${r.title}`);
|
|
6214
6312
|
}
|
|
6215
6313
|
}
|
|
6216
6314
|
|
|
@@ -6240,7 +6338,11 @@ function guardExplicitActor(command, value) {
|
|
|
6240
6338
|
// instead of just reporting the failure, straight from the live projection
|
|
6241
6339
|
// (never the rendered file), preferring the same tag when one is open.
|
|
6242
6340
|
function suggestNextClaimableTask(projection, { excludeId = null, tag = '' } = {}) {
|
|
6243
|
-
|
|
6341
|
+
// Decision rows (needs-human / decision) belong to a human. Autonomous
|
|
6342
|
+
// recovery after already_done must not hand an agent a judgment call.
|
|
6343
|
+
const open = (projection && projection.tasks || []).filter((t) => (
|
|
6344
|
+
t && t.status === 'open' && t.id !== excludeId && !isDecisionTask(t)
|
|
6345
|
+
));
|
|
6244
6346
|
if (!open.length) return null;
|
|
6245
6347
|
const normalizedTag = String(tag || '').trim().toLowerCase();
|
|
6246
6348
|
if (normalizedTag) {
|
|
@@ -6490,13 +6592,31 @@ function cmdNext(args) {
|
|
|
6490
6592
|
const reviewTasks = filterTasksByScope(reviewProjection.projection.tasks || [], scope)
|
|
6491
6593
|
.map(compactTaskForStatus)
|
|
6492
6594
|
.filter(task => task && task.review && task.review.handoff);
|
|
6493
|
-
const
|
|
6595
|
+
const openAll = filterTasksByScope(taskDb.listTasks(db, {
|
|
6494
6596
|
workspaceRoot: taskDb.workspaceRoot(),
|
|
6495
6597
|
status: 'open',
|
|
6496
|
-
limit: scoped ? null :
|
|
6598
|
+
limit: scoped ? null : 50,
|
|
6497
6599
|
}), scope);
|
|
6600
|
+
const open = openAll.filter((task) => !isDecisionTask(task));
|
|
6601
|
+
const skippedDecisions = openAll.length - open.length;
|
|
6498
6602
|
if (!open.length) {
|
|
6499
6603
|
const { projection, outPath } = reviewProjection;
|
|
6604
|
+
if (skippedDecisions > 0 && !reviewTasks.length) {
|
|
6605
|
+
if (wantsJson(args)) {
|
|
6606
|
+
printJson({
|
|
6607
|
+
ok: false,
|
|
6608
|
+
action: 'refused',
|
|
6609
|
+
reason: DECISION_REFUSE_REASON,
|
|
6610
|
+
skipped_decision_count: skippedDecisions,
|
|
6611
|
+
owner: String(owner),
|
|
6612
|
+
scope: normalizeTaskQueueScope(scope),
|
|
6613
|
+
projection_path: outPath,
|
|
6614
|
+
});
|
|
6615
|
+
process.exit(1);
|
|
6616
|
+
}
|
|
6617
|
+
console.error(DECISION_REFUSE_REASON);
|
|
6618
|
+
process.exit(1);
|
|
6619
|
+
}
|
|
6500
6620
|
const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again')
|
|
6501
6621
|
|| reviewTasks.find(task => task.review.handoff.next_action === 'continue_work')
|
|
6502
6622
|
|| reviewTasks.find(task => task.review.handoff.next_action === 'human_accept_waiting');
|
|
@@ -7147,6 +7267,12 @@ function cmdShow(args) {
|
|
|
7147
7267
|
: task.status.toUpperCase();
|
|
7148
7268
|
console.log(`${statusLabel} ${taskRef(task)} v${task.current_version}${owner}${tag}`);
|
|
7149
7269
|
console.log(task.title);
|
|
7270
|
+
if (task.metadata?.verification_status === 'degraded') {
|
|
7271
|
+
const reason = task.metadata.verification_degraded_reason === 'diff_only_verify'
|
|
7272
|
+
? 'diff-only verify command'
|
|
7273
|
+
: 'missing verify command';
|
|
7274
|
+
console.log(`verification: degraded (${reason})`);
|
|
7275
|
+
}
|
|
7150
7276
|
if (task.review) {
|
|
7151
7277
|
console.log('');
|
|
7152
7278
|
printReviewLanding(task.review);
|
|
@@ -9145,6 +9271,36 @@ function cmdClearDone(args) {
|
|
|
9145
9271
|
console.log(`cleared ${candidates.length} completed task(s).`);
|
|
9146
9272
|
}
|
|
9147
9273
|
|
|
9274
|
+
function cmdReapMissionBlockers(args) {
|
|
9275
|
+
const taskDb = getTaskDb();
|
|
9276
|
+
const db = taskDb.open();
|
|
9277
|
+
const workspaceRoot = taskDb.workspaceRoot();
|
|
9278
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
9279
|
+
const missions = require('./mission').listMissions(workspaceRoot);
|
|
9280
|
+
const result = taskDb.reapMissionBlockerTasks(db, { workspaceRoot, missions, actor });
|
|
9281
|
+
const { outPath } = writeDefaultProjection(taskDb, db);
|
|
9282
|
+
const taskById = new Map(taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot, limit: null }))
|
|
9283
|
+
.map(task => [task.id, task]));
|
|
9284
|
+
const closed = result.closed.map(row => ({
|
|
9285
|
+
...row,
|
|
9286
|
+
task_ref: taskRef(taskById.get(row.task_id)),
|
|
9287
|
+
}));
|
|
9288
|
+
if (wantsJson(args)) {
|
|
9289
|
+
printJson({
|
|
9290
|
+
ok: true,
|
|
9291
|
+
action: 'reaped_mission_blockers',
|
|
9292
|
+
closed_count: closed.length,
|
|
9293
|
+
closed,
|
|
9294
|
+
projection_path: outPath,
|
|
9295
|
+
});
|
|
9296
|
+
return;
|
|
9297
|
+
}
|
|
9298
|
+
console.log(`closed ${closed.length} mission blocker task(s).`);
|
|
9299
|
+
for (const row of closed) {
|
|
9300
|
+
console.log(`closed ${row.task_ref} because its mission is ${row.mission_status}.`);
|
|
9301
|
+
}
|
|
9302
|
+
}
|
|
9303
|
+
|
|
9148
9304
|
// One-time migration for OBL-1622: the 2026-06-10 "first-principles backlog
|
|
9149
9305
|
// reset" archived ~125 certified, proof-backed tasks by writing status
|
|
9150
9306
|
// 'failed' (no distinct archived status existed yet). This relabels exactly
|
|
@@ -9231,6 +9387,20 @@ function cmdReady(args) {
|
|
|
9231
9387
|
if (receipt.receiptPath) console.error(`receipt: ${receipt.receiptPath}`);
|
|
9232
9388
|
process.exit(1);
|
|
9233
9389
|
}
|
|
9390
|
+
// Exit 0 says the check passed. It does not say the check could have
|
|
9391
|
+
// failed. Run it again with none of the work present: a check anchored to
|
|
9392
|
+
// this codebase fails there, and one that passes anywhere passes there too.
|
|
9393
|
+
if (!hasFlag(args, '--no-falsify-check')) {
|
|
9394
|
+
const { probeVerifierCanFail } = require('../lib/falsifier-probe');
|
|
9395
|
+
const probe = probeVerifierCanFail({ command: verifyFlag });
|
|
9396
|
+
if (probe.probed && probe.canFail === false) {
|
|
9397
|
+
console.error(`atris task ready: this check cannot fail — ${probe.reason}`);
|
|
9398
|
+
console.error(` command: ${verifyFlag}`);
|
|
9399
|
+
console.error(' give a check that fails when the work is missing, or pass --no-falsify-check to accept anyway.');
|
|
9400
|
+
process.exit(1);
|
|
9401
|
+
}
|
|
9402
|
+
if (!probe.probed && !wantsJson(args)) console.log(` ${probe.reason}`);
|
|
9403
|
+
}
|
|
9234
9404
|
const base = proof.trim();
|
|
9235
9405
|
proof = `[verified] \`${verifyFlag}\` passed (exit 0)${base ? ` — ${base}` : ''}${receipt.output ? `\n${receipt.output}` : ''}\nReceipt: ${receipt.receiptPath}`;
|
|
9236
9406
|
if (!wantsJson(args)) console.log(`✓ verified: \`${verifyFlag}\` exited 0 (receipt ${receipt.receiptPath})`);
|
|
@@ -9303,7 +9473,10 @@ function cmdReady(args) {
|
|
|
9303
9473
|
nextTask: nextTaskInput.nextTask,
|
|
9304
9474
|
});
|
|
9305
9475
|
const reviewChat = taskReviewChatHandoff(verifierTask, { reviewer: 'codex-review' });
|
|
9306
|
-
const
|
|
9476
|
+
const autolandLib = require('../lib/autoland');
|
|
9477
|
+
const workspaceRoot = taskDb.workspaceRoot();
|
|
9478
|
+
const autolandOn = autolandLib.liveAcceptAuthorization(workspaceRoot).ok;
|
|
9479
|
+
const whenLands = autolandLib.whenAutolandLands(workspaceRoot);
|
|
9307
9480
|
const needsExternalVerifier = Boolean(usedVerify && !verifyAutoCertifyAllowed);
|
|
9308
9481
|
const handoff = {
|
|
9309
9482
|
native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
|
|
@@ -9311,10 +9484,10 @@ function cmdReady(args) {
|
|
|
9311
9484
|
next_action: agentCertified ? certifiedReviewNextAction(nextTaskInput.nextTask) : 'agent_review_again',
|
|
9312
9485
|
rule: autolandOn
|
|
9313
9486
|
? (agentCertified
|
|
9314
|
-
?
|
|
9487
|
+
? `double-check complete; autoland will accept this ${whenLands}.`
|
|
9315
9488
|
: needsExternalVerifier
|
|
9316
9489
|
? 'proof is ready; this verifier needs a second agent review because autoland cannot rerun it.'
|
|
9317
|
-
:
|
|
9490
|
+
: `proof is ready; autoland runs the second check and lands it ${whenLands}.`)
|
|
9318
9491
|
: (agentCertified
|
|
9319
9492
|
? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
|
|
9320
9493
|
: 'proof is ready; one more agent check before human approval. XP waits for the human.'),
|
|
@@ -9440,6 +9613,28 @@ async function cmdAccept(args) {
|
|
|
9440
9613
|
if (missionXpIssue) {
|
|
9441
9614
|
failTask('atris task accept', MISSION_XP_END_TO_END_REASON, missionXpIssue);
|
|
9442
9615
|
}
|
|
9616
|
+
// The proof must be able to fail. Run the stored verify command and refuse the
|
|
9617
|
+
// accept if it does not parse, does not run, or does not pass. Prose describing a
|
|
9618
|
+
// check is not a check.
|
|
9619
|
+
const acceptVerify = evaluateAcceptVerify(beforeTask, taskDb.workspaceRoot());
|
|
9620
|
+
const verifyOverride = hasFlag(args, '--accept-unverified');
|
|
9621
|
+
const overrideReason = String(flag(args, '--reason') || '').trim();
|
|
9622
|
+
if (!acceptVerify.ok && !verifyOverride) {
|
|
9623
|
+
failTask(
|
|
9624
|
+
'atris task accept',
|
|
9625
|
+
acceptVerify.reason,
|
|
9626
|
+
`${acceptVerify.detail}. Stored verify: ${acceptVerify.command || '(none)'}. `
|
|
9627
|
+
+ 'Fix the verify command and re-run, or accept deliberately with '
|
|
9628
|
+
+ '--accept-unverified --reason "why this cannot be machine-checked".',
|
|
9629
|
+
);
|
|
9630
|
+
}
|
|
9631
|
+
if (!acceptVerify.ok && verifyOverride && !overrideReason) {
|
|
9632
|
+
failTask(
|
|
9633
|
+
'atris task accept',
|
|
9634
|
+
'unverified_accept_needs_reason',
|
|
9635
|
+
'--accept-unverified records an unfalsifiable accept in the ledger, so it requires --reason "<why>".',
|
|
9636
|
+
);
|
|
9637
|
+
}
|
|
9443
9638
|
const readyReview = beforeTask?.review || {};
|
|
9444
9639
|
const clearLesson = hasEmptyFlagValue(args, '--lesson');
|
|
9445
9640
|
const clearNextTask = hasEmptyFlagValue(args, '--next');
|
|
@@ -9609,6 +9804,52 @@ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
|
|
|
9609
9804
|
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
9610
9805
|
}
|
|
9611
9806
|
|
|
9807
|
+
function currentHeadSha(workspaceRoot) {
|
|
9808
|
+
try {
|
|
9809
|
+
return execSync('git rev-parse HEAD', {
|
|
9810
|
+
cwd: workspaceRoot || process.cwd(),
|
|
9811
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9812
|
+
encoding: 'utf8',
|
|
9813
|
+
}).trim();
|
|
9814
|
+
} catch {
|
|
9815
|
+
return null;
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
|
|
9819
|
+
// metadata.verify_cache is the read path's only verify verdict: `reviews`
|
|
9820
|
+
// must never spawn a check (that read-side execution fork-bombed the fleet
|
|
9821
|
+
// on 2026-07-29), so the two lanes that legitimately execute verifies —
|
|
9822
|
+
// certify-verified and the autoland landing re-check — persist the outcome
|
|
9823
|
+
// here for the read path to consult.
|
|
9824
|
+
function stampVerifyCacheMetadata(taskDb, db, taskId, { verify, result, actor, workspaceRoot }) {
|
|
9825
|
+
const row = taskDb.getTask(db, taskId);
|
|
9826
|
+
if (!row) return;
|
|
9827
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
9828
|
+
metadata.verify_cache = {
|
|
9829
|
+
command: verify,
|
|
9830
|
+
ok: result && result.ok === true,
|
|
9831
|
+
reason: (result && result.reason) || null,
|
|
9832
|
+
ran_at: new Date().toISOString(),
|
|
9833
|
+
ran_by: actor,
|
|
9834
|
+
code_sha: currentHeadSha(workspaceRoot),
|
|
9835
|
+
};
|
|
9836
|
+
db.prepare(`
|
|
9837
|
+
UPDATE tasks
|
|
9838
|
+
SET metadata = ?,
|
|
9839
|
+
updated_at = ?
|
|
9840
|
+
WHERE id = ?
|
|
9841
|
+
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
9842
|
+
}
|
|
9843
|
+
|
|
9844
|
+
function cachedVerifyVerdict(task, root) {
|
|
9845
|
+
const metadata = (task && task.metadata) || {};
|
|
9846
|
+
const cache = metadata.verify_cache;
|
|
9847
|
+
if (!cache || typeof cache !== 'object') return { fresh: false, command: metadata.verify || null };
|
|
9848
|
+
const fresh = cache.command === metadata.verify
|
|
9849
|
+
&& (!cache.code_sha || cache.code_sha === currentHeadSha(task.workspace_root || root));
|
|
9850
|
+
return { ...cache, fresh: Boolean(fresh) };
|
|
9851
|
+
}
|
|
9852
|
+
|
|
9612
9853
|
// `atris task ready --verify "<cmd>"` already runs the command live and
|
|
9613
9854
|
// gates on exit 0, but that alone leaves no machine re-runnable trace once
|
|
9614
9855
|
// the proof text scrolls out of easy reach: storing the exact command on
|
|
@@ -9650,6 +9891,7 @@ function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier',
|
|
|
9650
9891
|
};
|
|
9651
9892
|
}
|
|
9652
9893
|
const result = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
|
|
9894
|
+
stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result, actor, workspaceRoot: task.workspace_root });
|
|
9653
9895
|
if (result.ok) return { ok: true, verify, result };
|
|
9654
9896
|
const note = landingVerifyFailureNote(verify, result);
|
|
9655
9897
|
const revised = taskDb.reviseTask(db, {
|
|
@@ -9719,8 +9961,10 @@ function cmdCertifyVerified(args, options = {}) {
|
|
|
9719
9961
|
// "certified" alone is not landable.
|
|
9720
9962
|
// strictVerify stays off in this eligibility probe: certify-verified runs
|
|
9721
9963
|
// the check itself right below, and strict mode here would execute it a
|
|
9722
|
-
// second time per row before the real run.
|
|
9723
|
-
|
|
9964
|
+
// second time per row before the real run. executeVerify: false as well —
|
|
9965
|
+
// an untrusted trust tier re-enables the strict block internally even
|
|
9966
|
+
// with strictVerify off, which is how this probe spawned a verify per row.
|
|
9967
|
+
const evaluation = evaluateAutoAccept(task, { strictVerify: false, executeVerify: false });
|
|
9724
9968
|
if (evaluation.eligible) {
|
|
9725
9969
|
results.push({ ref, action: 'skipped', reason: 'already_landable' });
|
|
9726
9970
|
continue;
|
|
@@ -9749,6 +9993,7 @@ function cmdCertifyVerified(args, options = {}) {
|
|
|
9749
9993
|
continue;
|
|
9750
9994
|
}
|
|
9751
9995
|
const run = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
|
|
9996
|
+
stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result: run, actor, workspaceRoot: task.workspace_root });
|
|
9752
9997
|
if (!run.ok) {
|
|
9753
9998
|
results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
|
|
9754
9999
|
continue;
|
|
@@ -9927,11 +10172,19 @@ function cmdAutoAcceptCertified(args) {
|
|
|
9927
10172
|
}
|
|
9928
10173
|
const landingVerify = reverifyBeforeLanding(taskDb, db, task, { verifyCache });
|
|
9929
10174
|
if (!landingVerify.ok) {
|
|
10175
|
+
// A refused landing whose revise also failed is the worst state in the
|
|
10176
|
+
// loop: the work does not land AND nobody is told to fix it, so the row
|
|
10177
|
+
// sits in review forever being re-refused every hour. Mark it so the
|
|
10178
|
+
// heartbeat can raise it instead of counting it as routine.
|
|
9930
10179
|
results.push({
|
|
9931
10180
|
...evaluation,
|
|
9932
10181
|
eligible: false,
|
|
9933
10182
|
action: landingVerify.revised ? 'revised' : 'revise_failed',
|
|
9934
10183
|
reason: landingVerify.result.reason || landingVerify.revise_reason || 'verify_failed',
|
|
10184
|
+
...(landingVerify.result.unrunnable_cause
|
|
10185
|
+
? { unrunnable_cause: landingVerify.result.unrunnable_cause }
|
|
10186
|
+
: {}),
|
|
10187
|
+
alarm: landingVerify.revised !== true || landingVerify.result.reason === 'verify_unrunnable',
|
|
9935
10188
|
verify: landingVerify.verify,
|
|
9936
10189
|
exit_code: landingVerify.result.status,
|
|
9937
10190
|
revision_note: landingVerify.note,
|
|
@@ -10029,7 +10282,7 @@ function autoAcceptSweepDeniedReason(task) {
|
|
|
10029
10282
|
for (const value of autoAcceptSweepLabelValues(task)) {
|
|
10030
10283
|
const tokens = autoAcceptSweepNormalizedTokens(value);
|
|
10031
10284
|
for (const token of tokens) {
|
|
10032
|
-
if (token === 'needs-human' || token === 'needshuman') {
|
|
10285
|
+
if (token === 'needs-human' || token === 'needshuman' || token === 'decision') {
|
|
10033
10286
|
return 'needs_human';
|
|
10034
10287
|
}
|
|
10035
10288
|
const protectedLane = [...SWEEP_AUTO_ACCEPT_PROTECTED].find((denied) =>
|
|
@@ -11057,7 +11310,45 @@ function taskHasReview(task) {
|
|
|
11057
11310
|
return review.reward != null || Boolean(review.proof || review.lesson || review.next_task);
|
|
11058
11311
|
}
|
|
11059
11312
|
|
|
11060
|
-
|
|
11313
|
+
const TASK_BOARD_COLUMNS = [
|
|
11314
|
+
['backlog', 'Backlog'],
|
|
11315
|
+
['open', 'Open'],
|
|
11316
|
+
['doing', 'Doing'],
|
|
11317
|
+
['review', 'Review'],
|
|
11318
|
+
['blocked', 'Blocked'],
|
|
11319
|
+
['done', 'Done'],
|
|
11320
|
+
];
|
|
11321
|
+
|
|
11322
|
+
// Pure state shaping: projection in, board view model out. No markup here.
|
|
11323
|
+
function taskBoardViewModel(projection = {}) {
|
|
11324
|
+
const tasks = Array.isArray(projection.tasks) ? projection.tasks : [];
|
|
11325
|
+
const rows = tasks.map((task) => ({
|
|
11326
|
+
id: task.id,
|
|
11327
|
+
task,
|
|
11328
|
+
column: taskColumn(task),
|
|
11329
|
+
decision: taskHasReview(task),
|
|
11330
|
+
}));
|
|
11331
|
+
const columns = TASK_BOARD_COLUMNS.map(([key, label]) => ({
|
|
11332
|
+
key,
|
|
11333
|
+
label,
|
|
11334
|
+
rows: rows.filter((row) => row.column === key),
|
|
11335
|
+
}));
|
|
11336
|
+
const counts = {};
|
|
11337
|
+
for (const column of columns) counts[column.key] = column.rows.length;
|
|
11338
|
+
return {
|
|
11339
|
+
columns,
|
|
11340
|
+
counts,
|
|
11341
|
+
rows,
|
|
11342
|
+
total: rows.length,
|
|
11343
|
+
planTags: Array.from(STATUS_PLAN_TAGS),
|
|
11344
|
+
};
|
|
11345
|
+
}
|
|
11346
|
+
|
|
11347
|
+
// Markup only: view model in, board page out. The page hydrates itself from
|
|
11348
|
+
// /api/tasks in the browser, so the template bakes in just the shared
|
|
11349
|
+
// plan-tag list; the column keys and labels must stay in step with
|
|
11350
|
+
// TASK_BOARD_COLUMNS above.
|
|
11351
|
+
function taskBoardTemplate(model) {
|
|
11061
11352
|
return `<!doctype html>
|
|
11062
11353
|
<html lang="en">
|
|
11063
11354
|
<head>
|
|
@@ -11216,7 +11507,7 @@ function taskBoardHtml() {
|
|
|
11216
11507
|
['blocked', 'Blocked'],
|
|
11217
11508
|
['done', 'Done']
|
|
11218
11509
|
];
|
|
11219
|
-
const planTags = new Set(${JSON.stringify(
|
|
11510
|
+
const planTags = new Set(${JSON.stringify(model.planTags)});
|
|
11220
11511
|
let state = { tasks: [] };
|
|
11221
11512
|
let selected = null;
|
|
11222
11513
|
const $ = (id) => document.getElementById(id);
|
|
@@ -11489,6 +11780,10 @@ function taskBoardHtml() {
|
|
|
11489
11780
|
</html>`;
|
|
11490
11781
|
}
|
|
11491
11782
|
|
|
11783
|
+
function taskBoardHtml() {
|
|
11784
|
+
return taskBoardTemplate(taskBoardViewModel());
|
|
11785
|
+
}
|
|
11786
|
+
|
|
11492
11787
|
function readJsonBody(req) {
|
|
11493
11788
|
return new Promise((resolve, reject) => {
|
|
11494
11789
|
let body = '';
|
|
@@ -11507,10 +11802,11 @@ function readJsonBody(req) {
|
|
|
11507
11802
|
});
|
|
11508
11803
|
}
|
|
11509
11804
|
|
|
11510
|
-
function sendJson(res, status, value) {
|
|
11805
|
+
function sendJson(res, status, value, headers = {}) {
|
|
11511
11806
|
res.writeHead(status, {
|
|
11512
11807
|
'content-type': 'application/json; charset=utf-8',
|
|
11513
11808
|
'access-control-allow-origin': 'http://localhost',
|
|
11809
|
+
...headers,
|
|
11514
11810
|
});
|
|
11515
11811
|
res.end(JSON.stringify(value, null, 2));
|
|
11516
11812
|
}
|
|
@@ -11520,551 +11816,655 @@ function sendHtml(res, value) {
|
|
|
11520
11816
|
res.end(value);
|
|
11521
11817
|
}
|
|
11522
11818
|
|
|
11523
|
-
|
|
11524
|
-
const
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
|
|
11529
|
-
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11819
|
+
function taskApiQueueOptions(url) {
|
|
11820
|
+
const limitParam = url.searchParams.get('limit');
|
|
11821
|
+
const limit = limitParam ? Number(limitParam) : 8;
|
|
11822
|
+
return {
|
|
11823
|
+
owner: url.searchParams.get('owner') || url.searchParams.get('as') || DEFAULT_OWNER,
|
|
11824
|
+
reviewer: url.searchParams.get('reviewer') || url.searchParams.get('as_reviewer') || 'codex-review',
|
|
11825
|
+
all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
|
|
11826
|
+
everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
|
|
11827
|
+
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
|
|
11828
|
+
scope: taskQueueScopeFromSearchParams(url.searchParams),
|
|
11829
|
+
};
|
|
11830
|
+
}
|
|
11831
|
+
|
|
11832
|
+
function serveTaskApiOptions({ res }) {
|
|
11833
|
+
return sendJson(res, 200, { ok: true });
|
|
11834
|
+
}
|
|
11835
|
+
|
|
11836
|
+
function serveTaskBoardPage({ res }) {
|
|
11837
|
+
return sendHtml(res, taskBoardHtml());
|
|
11838
|
+
}
|
|
11839
|
+
|
|
11840
|
+
function serveTaskList({ res, taskDb, db }) {
|
|
11841
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11842
|
+
return sendJson(res, 200, { ok: true, projection_path: outPath, projection });
|
|
11843
|
+
}
|
|
11844
|
+
|
|
11845
|
+
function serveTaskActivityStream({ res, taskDb, db }) {
|
|
11846
|
+
// Time-ordered feed of what the agent actually did + heartbeat liveness.
|
|
11847
|
+
const { projection } = writeDefaultProjection(taskDb, db);
|
|
11848
|
+
const root = projection.workspace_root || process.cwd();
|
|
11849
|
+
const stateDir = path.join(root, '.atris', 'state');
|
|
11850
|
+
const activity = require('../lib/activity-stream');
|
|
11851
|
+
const { readJsonl } = require('../lib/pulse');
|
|
11852
|
+
const readStateRows = (file) => readJsonl(path.join(stateDir, file));
|
|
11853
|
+
const pulseReceipts = readStateRows('pulse_agi_loop_receipts.jsonl');
|
|
11854
|
+
const events = activity.buildActivityStream({
|
|
11855
|
+
pulseReceipts,
|
|
11856
|
+
scorecards: readStateRows('scorecards.jsonl'),
|
|
11857
|
+
taskEpisodes: readStateRows('task_episodes.jsonl').slice(-200),
|
|
11858
|
+
xpReceipts: readStateRows('career_xp_receipts.jsonl').slice(-200),
|
|
11859
|
+
missionEvents: readStateRows('mission_events.jsonl').slice(-200),
|
|
11860
|
+
}, { limit: 60 });
|
|
11861
|
+
return sendJson(res, 200, {
|
|
11862
|
+
ok: true,
|
|
11863
|
+
heartbeat: activity.buildHeartbeat(pulseReceipts),
|
|
11864
|
+
events,
|
|
11865
|
+
}, {
|
|
11866
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
11867
|
+
'cache-control': 'no-cache',
|
|
11868
|
+
connection: 'keep-alive',
|
|
11869
|
+
});
|
|
11870
|
+
}
|
|
11871
|
+
|
|
11872
|
+
function serveTaskCapabilities({ res }) {
|
|
11873
|
+
return sendJson(res, 200, {
|
|
11874
|
+
ok: true,
|
|
11875
|
+
action: 'capabilities',
|
|
11876
|
+
capabilities: taskCapabilitiesContract(),
|
|
11877
|
+
safety: {
|
|
11878
|
+
read_only: true,
|
|
11879
|
+
claims_work: false,
|
|
11880
|
+
human_accept: false,
|
|
11881
|
+
xp_after_human_accept: true,
|
|
11882
|
+
},
|
|
11883
|
+
});
|
|
11884
|
+
}
|
|
11885
|
+
|
|
11886
|
+
function serveTaskCapabilitiesCheck({ res, taskDb, db, url }) {
|
|
11887
|
+
const report = taskCapabilitiesCheckReport(taskDb, db, [], taskApiQueueOptions(url));
|
|
11888
|
+
return sendJson(res, report.ok ? 200 : 409, report);
|
|
11889
|
+
}
|
|
11890
|
+
|
|
11891
|
+
function serveTaskReviewLaneDrain({ res, taskDb, db, url }) {
|
|
11892
|
+
const report = taskReviewLaneDrainReport(taskDb, db, [], taskApiQueueOptions(url));
|
|
11893
|
+
return sendJson(res, report.ok ? 200 : 409, report);
|
|
11894
|
+
}
|
|
11895
|
+
|
|
11896
|
+
async function postTaskReviewLaneAct({ req, res, taskDb, db, url }) {
|
|
11897
|
+
const body = await readJsonBody(req);
|
|
11898
|
+
const result = taskReviewLaneAct(taskDb, db, taskReviewLaneActOptionsFromBody(body, url.searchParams));
|
|
11899
|
+
return sendJson(res, result.ok ? 200 : result.status || 409, result);
|
|
11900
|
+
}
|
|
11901
|
+
|
|
11902
|
+
async function postTaskReviewLaneLoop({ req, res, taskDb, db, url }) {
|
|
11903
|
+
const body = await readJsonBody(req);
|
|
11904
|
+
const result = taskReviewLaneLoop(taskDb, db, taskReviewLaneLoopOptionsFromBody(body, url.searchParams));
|
|
11905
|
+
return sendJson(res, result.ok ? 200 : result.status || 409, result);
|
|
11906
|
+
}
|
|
11907
|
+
|
|
11908
|
+
async function postTaskReviewLaneRun({ req, res, taskDb, db, url }) {
|
|
11909
|
+
const body = await readJsonBody(req);
|
|
11910
|
+
const result = taskReviewLaneRun(taskDb, db, taskReviewLaneRunOptionsFromBody(body, url.searchParams));
|
|
11911
|
+
return sendJson(res, result.ok ? 200 : result.status || 409, result);
|
|
11912
|
+
}
|
|
11913
|
+
|
|
11914
|
+
function serveTaskCurrent({ res, taskDb, db, url }) {
|
|
11915
|
+
const { outPath, current } = buildTaskCurrent(taskDb, db, [], taskApiQueueOptions(url));
|
|
11916
|
+
const action = url.pathname.endsWith('/queue') ? 'queue' : 'current';
|
|
11917
|
+
return sendJson(res, 200, {
|
|
11918
|
+
ok: true,
|
|
11919
|
+
action,
|
|
11920
|
+
projection_path: outPath,
|
|
11921
|
+
current,
|
|
11922
|
+
selected: current.selected,
|
|
11923
|
+
page: current.page,
|
|
11924
|
+
queue: current.queue,
|
|
11925
|
+
});
|
|
11926
|
+
}
|
|
11927
|
+
|
|
11928
|
+
async function postCurrentTaskStep({ req, res, taskDb, db, url }) {
|
|
11929
|
+
const body = await readJsonBody(req);
|
|
11930
|
+
const options = taskCurrentStepOptionsFromBody(body, url.searchParams);
|
|
11931
|
+
try {
|
|
11932
|
+
const result = runCurrentTaskStep(taskDb, db, options);
|
|
11933
|
+
return sendJson(res, 200, result);
|
|
11934
|
+
} catch (error) {
|
|
11935
|
+
const errorCurrent = error.current || null;
|
|
11936
|
+
return sendJson(res, error.status || 409, {
|
|
11937
|
+
ok: false,
|
|
11938
|
+
action: 'current_step',
|
|
11939
|
+
reason: error.reason || 'step_failed',
|
|
11940
|
+
detail: error.message,
|
|
11941
|
+
selected_task_id: errorCurrent ? errorCurrent.selected_task_id : null,
|
|
11942
|
+
selected_ref: errorCurrent ? errorCurrent.selected_ref : null,
|
|
11943
|
+
selected_next_key: selectedNextKeyFromCurrent(errorCurrent),
|
|
11944
|
+
current: errorCurrent,
|
|
11945
|
+
page: error.page || null,
|
|
11560
11946
|
});
|
|
11561
11947
|
}
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11948
|
+
}
|
|
11949
|
+
|
|
11950
|
+
async function postTaskCreate({ req, res, taskDb, db }) {
|
|
11951
|
+
const body = await readJsonBody(req);
|
|
11952
|
+
const title = String(body.title || '').trim();
|
|
11953
|
+
if (!title) return sendJson(res, 400, { ok: false, reason: 'missing_title', detail: 'title required' });
|
|
11954
|
+
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
|
|
11955
|
+
const result = taskDb.addTask(db, {
|
|
11956
|
+
title,
|
|
11957
|
+
tag: body.tag ? String(body.tag) : 'tasks',
|
|
11958
|
+
workspaceRoot: taskDb.workspaceRoot(),
|
|
11959
|
+
metadata: body.verify ? { verify: String(body.verify).trim() } : null,
|
|
11960
|
+
});
|
|
11961
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11962
|
+
return sendJson(res, 200, {
|
|
11963
|
+
ok: true,
|
|
11964
|
+
action: 'created',
|
|
11965
|
+
task_id: result.id,
|
|
11966
|
+
operator_title_warning: operatorTitleWarning,
|
|
11967
|
+
projection_path: outPath,
|
|
11968
|
+
task: taskFromProjection(projection, result.id),
|
|
11969
|
+
});
|
|
11970
|
+
}
|
|
11971
|
+
|
|
11972
|
+
async function postTaskClearPlan({ req, res, taskDb, db }) {
|
|
11973
|
+
const body = await readJsonBody(req);
|
|
11974
|
+
if (!body.confirm && !body.yes) {
|
|
11975
|
+
return sendJson(res, 400, { ok: false, reason: 'confirm_required', detail: stageErrorDetail('task clear-plan', 'confirm_required') });
|
|
11976
|
+
}
|
|
11977
|
+
const result = taskDb.clearPlanTasks(db, {
|
|
11978
|
+
workspaceRoot: taskDb.workspaceRoot(),
|
|
11979
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
11980
|
+
reason: String(body.reason || body.note || 'clear_plan'),
|
|
11981
|
+
tag: String(body.tag || 'capture'),
|
|
11982
|
+
});
|
|
11983
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11984
|
+
const taskById = new Map((projection.tasks || []).map(task => [task.id, task]));
|
|
11985
|
+
return sendJson(res, 200, {
|
|
11986
|
+
ok: true,
|
|
11987
|
+
action: 'clear_plan',
|
|
11988
|
+
cleared_count: result.cleared.length,
|
|
11989
|
+
skipped_count: result.skipped.length,
|
|
11990
|
+
skipped: result.skipped,
|
|
11991
|
+
projection_path: outPath,
|
|
11992
|
+
tasks: result.cleared.map(task => taskFromProjection(projection, task.id) || taskById.get(task.id)).filter(Boolean),
|
|
11993
|
+
});
|
|
11994
|
+
}
|
|
11995
|
+
|
|
11996
|
+
const TASK_API_ROUTES = [
|
|
11997
|
+
{ method: 'OPTIONS', pathname: null, serve: serveTaskApiOptions },
|
|
11998
|
+
{ method: 'GET', pathname: '/', serve: serveTaskBoardPage },
|
|
11999
|
+
{ method: 'GET', pathname: '/api/tasks', serve: serveTaskList },
|
|
12000
|
+
{ method: 'GET', pathname: '/api/stream', serve: serveTaskActivityStream },
|
|
12001
|
+
{ method: 'GET', pathname: '/api/tasks/capabilities', serve: serveTaskCapabilities },
|
|
12002
|
+
{ method: 'GET', pathname: '/api/tasks/capabilities/check', serve: serveTaskCapabilitiesCheck },
|
|
12003
|
+
{ method: 'GET', pathname: '/api/tasks/review-lane-drain', serve: serveTaskReviewLaneDrain },
|
|
12004
|
+
{ method: 'POST', pathname: '/api/tasks/review-lane-act', serve: postTaskReviewLaneAct },
|
|
12005
|
+
{ method: 'POST', pathname: '/api/tasks/review-lane-loop', serve: postTaskReviewLaneLoop },
|
|
12006
|
+
{ method: 'POST', pathname: '/api/tasks/review-lane-run', serve: postTaskReviewLaneRun },
|
|
12007
|
+
{ method: 'GET', pathname: '/api/tasks/current', serve: serveTaskCurrent },
|
|
12008
|
+
{ method: 'GET', pathname: '/api/tasks/queue', serve: serveTaskCurrent },
|
|
12009
|
+
{ method: 'POST', pathname: '/api/tasks/current/step', serve: postCurrentTaskStep },
|
|
12010
|
+
{ method: 'POST', pathname: '/api/tasks', serve: postTaskCreate },
|
|
12011
|
+
{ method: 'POST', pathname: '/api/tasks/clear-plan', serve: postTaskClearPlan },
|
|
12012
|
+
{ pattern: /^\/api\/tasks\/([^/]+)$/, serve: serveTaskDetail },
|
|
12013
|
+
{ pattern: /^\/api\/tasks\/([^/]+)\/page$/, serve: serveTaskPage },
|
|
12014
|
+
{ pattern: /^\/api\/tasks\/([^/]+)\/(claim|message|chat|step|plan|do|backlog|ready|accept|revise|finish|review|review-chat|continue-work|events)$/, serve: serveTaskOperation },
|
|
12015
|
+
];
|
|
12016
|
+
|
|
12017
|
+
function serveTaskDetail({ req, res, taskDb, db, match }) {
|
|
12018
|
+
if (req.method !== 'GET') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
|
|
12019
|
+
const resolved = resolveTaskRef(taskDb, db, match[1]);
|
|
12020
|
+
if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
|
|
12021
|
+
const task = taskDetail(taskDb, db, resolved.id);
|
|
12022
|
+
if (!task) return sendJson(res, 404, { ok: false, reason: 'not_found' });
|
|
12023
|
+
const { outPath } = writeDefaultProjection(taskDb, db);
|
|
12024
|
+
return sendJson(res, 200, { ok: true, action: 'detail', task_id: resolved.id, projection_path: outPath, task, page: taskPageContract(task) });
|
|
12025
|
+
}
|
|
12026
|
+
|
|
12027
|
+
function serveTaskPage({ req, res, taskDb, db, match }) {
|
|
12028
|
+
if (req.method !== 'GET') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
|
|
12029
|
+
const resolved = resolveTaskRef(taskDb, db, match[1]);
|
|
12030
|
+
if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
|
|
12031
|
+
const task = taskDetail(taskDb, db, resolved.id);
|
|
12032
|
+
if (!task) return sendJson(res, 404, { ok: false, reason: 'not_found' });
|
|
12033
|
+
const { outPath } = writeDefaultProjection(taskDb, db);
|
|
12034
|
+
return sendJson(res, 200, { ok: true, action: 'page', task_id: resolved.id, projection_path: outPath, page: taskPageContract(task) });
|
|
12035
|
+
}
|
|
12036
|
+
|
|
12037
|
+
function postTaskApiStep({ res, taskDb, db, taskId, body }) {
|
|
12038
|
+
try {
|
|
12039
|
+
const result = runTaskStep(taskDb, db, taskId, taskStepOptionsFromBody(body));
|
|
12040
|
+
return sendJson(res, 200, result);
|
|
12041
|
+
} catch (error) {
|
|
12042
|
+
return sendJson(res, error.status || 409, {
|
|
12043
|
+
ok: false,
|
|
12044
|
+
action: 'step',
|
|
12045
|
+
task_id: taskId,
|
|
12046
|
+
reason: error.reason || 'step_failed',
|
|
12047
|
+
detail: error.message,
|
|
12048
|
+
page: error.page || null,
|
|
11622
12049
|
});
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
12050
|
+
}
|
|
12051
|
+
}
|
|
12052
|
+
|
|
12053
|
+
function postTaskApiContinueWork({ res, taskDb, db, taskId, body }) {
|
|
12054
|
+
try {
|
|
12055
|
+
const result = continueWorkForReviewTask(taskDb, db, taskId, { owner: body.owner || body.actor || DEFAULT_OWNER });
|
|
12056
|
+
return sendJson(res, 200, result);
|
|
12057
|
+
} catch (error) {
|
|
12058
|
+
return sendJson(res, error.status || 409, {
|
|
12059
|
+
ok: false,
|
|
12060
|
+
action: 'continue_work',
|
|
12061
|
+
task_id: taskId,
|
|
12062
|
+
reason: error.reason || 'continue_work_failed',
|
|
12063
|
+
detail: error.message,
|
|
11632
12064
|
});
|
|
11633
12065
|
}
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
12066
|
+
}
|
|
12067
|
+
|
|
12068
|
+
function postTaskApiClaim({ res, taskDb, db, taskId, body }) {
|
|
12069
|
+
const owner = String(body.owner || body.actor || DEFAULT_OWNER);
|
|
12070
|
+
const result = taskDb.claimTask(db, { id: taskId, claimedBy: owner });
|
|
12071
|
+
if (!result.claimed) return sendJson(res, 409, { ok: false, reason: result.reason, claimed_by: result.claimed_by || null });
|
|
12072
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12073
|
+
return sendJson(res, 200, { ok: true, action: 'claimed', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
12074
|
+
}
|
|
12075
|
+
|
|
12076
|
+
function postTaskApiMessage({ res, taskDb, db, taskId, body }) {
|
|
12077
|
+
const result = taskDb.noteTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), content: String(body.content || '') });
|
|
12078
|
+
if (!result.noted) return sendJson(res, 404, { ok: false, reason: result.reason });
|
|
12079
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12080
|
+
return sendJson(res, 200, { ok: true, action: 'noted', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
12081
|
+
}
|
|
12082
|
+
|
|
12083
|
+
function postTaskApiChat({ res, taskDb, db, taskId, body }) {
|
|
12084
|
+
const result = taskDb.chatTask(db, {
|
|
12085
|
+
id: taskId,
|
|
12086
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
12087
|
+
content: String(body.content || body.message || body.text || ''),
|
|
12088
|
+
goal: String(body.goal || body.objective || ''),
|
|
12089
|
+
summary: String(body.summary || ''),
|
|
12090
|
+
});
|
|
12091
|
+
if (!result.chatted) {
|
|
12092
|
+
const status = result.reason === 'content_required' ? 400 : result.reason === 'not_found' ? 404 : 409;
|
|
12093
|
+
return sendJson(res, status, { ok: false, reason: result.reason, detail: stageErrorDetail('task chat', result.reason, result) });
|
|
11654
12094
|
}
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
12095
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12096
|
+
return sendJson(res, 200, {
|
|
12097
|
+
ok: true,
|
|
12098
|
+
action: 'chatted',
|
|
12099
|
+
task_id: taskId,
|
|
12100
|
+
version: result.event.version,
|
|
12101
|
+
goal_changed: result.goal_changed,
|
|
12102
|
+
chat_packet: result.chat_packet,
|
|
12103
|
+
projection_path: outPath,
|
|
12104
|
+
task: taskFromProjection(projection, taskId),
|
|
12105
|
+
});
|
|
12106
|
+
}
|
|
12107
|
+
|
|
12108
|
+
function postTaskApiPlan({ res, taskDb, db, taskId, body }) {
|
|
12109
|
+
const actor = String(body.actor || DEFAULT_OWNER);
|
|
12110
|
+
const goal = String(body.goal || body.objective || '');
|
|
12111
|
+
const summary = String(body.summary || body.plan || '');
|
|
12112
|
+
const owner = String(body.owner || body.assignee || '');
|
|
12113
|
+
const exit = String(body.exit || body.exit_condition || '');
|
|
12114
|
+
const firstMove = String(body.first_move || body.firstMove || body.first || '');
|
|
12115
|
+
const task = taskDetail(taskDb, db, taskId);
|
|
12116
|
+
const automaticPlan = buildAutomaticPlanTrace(taskDb, task, {
|
|
12117
|
+
actor,
|
|
12118
|
+
actorExplicit: Boolean(body.actor),
|
|
12119
|
+
owner,
|
|
12120
|
+
goal,
|
|
12121
|
+
summary,
|
|
12122
|
+
firstMove,
|
|
12123
|
+
exit,
|
|
12124
|
+
});
|
|
12125
|
+
const result = taskDb.stageTask(db, {
|
|
12126
|
+
id: taskId,
|
|
12127
|
+
actor,
|
|
12128
|
+
stage: 'plan',
|
|
12129
|
+
goal,
|
|
12130
|
+
summary,
|
|
12131
|
+
owner: automaticPlan.ownerForStage || owner,
|
|
12132
|
+
exit,
|
|
12133
|
+
proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
|
|
12134
|
+
firstMove,
|
|
12135
|
+
nextButton: String(body.next_button || body.nextButton || ''),
|
|
12136
|
+
confidence: body.confidence,
|
|
12137
|
+
planTrace: automaticPlan.trace,
|
|
12138
|
+
});
|
|
12139
|
+
if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task plan', result.reason, result) });
|
|
12140
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12141
|
+
return sendJson(res, 200, { ok: true, action: 'planned', task_id: taskId, version: result.event.version, plan_trace: automaticPlan.trace, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
12142
|
+
}
|
|
12143
|
+
|
|
12144
|
+
function postTaskApiDo({ res, taskDb, db, taskId, body }) {
|
|
12145
|
+
const firstMove = String(body.first_move || body.firstMove || body.first || '').trim();
|
|
12146
|
+
if (!firstMove) return sendJson(res, 400, { ok: false, reason: 'first_move_required', detail: 'task do: first_move required' });
|
|
12147
|
+
const result = taskDb.stageTask(db, {
|
|
12148
|
+
id: taskId,
|
|
12149
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
12150
|
+
stage: 'do',
|
|
12151
|
+
goal: String(body.goal || body.objective || ''),
|
|
12152
|
+
summary: String(body.summary || ''),
|
|
12153
|
+
owner: String(body.actor || DEFAULT_OWNER),
|
|
12154
|
+
exit: String(body.exit || body.exit_condition || body.exitCondition || ''),
|
|
12155
|
+
proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
|
|
12156
|
+
firstMove,
|
|
12157
|
+
nextButton: String(body.next_button || body.nextButton || ''),
|
|
12158
|
+
confidence: body.confidence,
|
|
12159
|
+
});
|
|
12160
|
+
if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task do', result.reason, result) });
|
|
12161
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12162
|
+
return sendJson(res, 200, { ok: true, action: 'doing', task_id: taskId, version: result.event.version, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
12163
|
+
}
|
|
12164
|
+
|
|
12165
|
+
function postTaskApiBacklog({ res, taskDb, db, taskId, body }) {
|
|
12166
|
+
const result = taskDb.backlogTask(db, {
|
|
12167
|
+
id: taskId,
|
|
12168
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
12169
|
+
reason: String(body.reason || body.note || 'clear_plan'),
|
|
12170
|
+
tag: String(body.tag || 'capture'),
|
|
12171
|
+
});
|
|
12172
|
+
if (!result.backlogged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task backlog', result.reason, result) });
|
|
12173
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12174
|
+
return sendJson(res, 200, {
|
|
12175
|
+
ok: true,
|
|
12176
|
+
action: 'backlogged',
|
|
12177
|
+
task_id: taskId,
|
|
12178
|
+
version: result.event.version,
|
|
12179
|
+
cleared_keys: result.cleared_keys,
|
|
12180
|
+
projection_path: outPath,
|
|
12181
|
+
task: taskFromProjection(projection, taskId),
|
|
12182
|
+
});
|
|
12183
|
+
}
|
|
12184
|
+
|
|
12185
|
+
function postTaskApiReviewChat({ res, taskDb, db, taskId, body }) {
|
|
12186
|
+
try {
|
|
12187
|
+
const result = appendTaskReviewChat(taskDb, db, taskId, {
|
|
12188
|
+
reviewer: body.reviewer || body.actor || 'codex-review',
|
|
12189
|
+
dryRun: Boolean(body.dryRun || body.noNote),
|
|
12190
|
+
});
|
|
12191
|
+
const { event, compactProjection, outPath, ...payload } = result;
|
|
12192
|
+
return sendJson(res, 200, payload);
|
|
12193
|
+
} catch (error) {
|
|
12194
|
+
return sendJson(res, error.status || 409, {
|
|
12195
|
+
ok: false,
|
|
12196
|
+
reason: error.reason || 'review_chat_failed',
|
|
12197
|
+
detail: error.message,
|
|
11664
12198
|
});
|
|
11665
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11666
|
-
return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, operator_title_warning: operatorTitleWarning, projection_path: outPath, task: taskFromProjection(projection, result.id) });
|
|
11667
12199
|
}
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
12200
|
+
}
|
|
12201
|
+
|
|
12202
|
+
function postTaskApiFinish({ res, taskDb, db, taskId, body }) {
|
|
12203
|
+
const currentTask = taskDb.getTask(db, taskId);
|
|
12204
|
+
const failed = Boolean(body.failed);
|
|
12205
|
+
const proof = String(body.proof || '').trim();
|
|
12206
|
+
const shouldReview = Boolean(body.proof || body.lesson || body.next || body.reward !== undefined);
|
|
12207
|
+
const proofIssue = meaningfulTaskProofIssue(proof, { required: !failed || shouldReview });
|
|
12208
|
+
if (proofIssue) return sendProofIssue(res, proof, proofIssue);
|
|
12209
|
+
const done = taskDb.doneTask(db, {
|
|
12210
|
+
id: taskId,
|
|
12211
|
+
status: failed ? 'failed' : 'done',
|
|
12212
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
12213
|
+
action: failed ? 'failed' : 'finished',
|
|
12214
|
+
proof,
|
|
12215
|
+
});
|
|
12216
|
+
if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_or_claimed' });
|
|
12217
|
+
let episode = null;
|
|
12218
|
+
let nextCreated = null;
|
|
12219
|
+
let xpProjection = null;
|
|
12220
|
+
if (shouldReview) {
|
|
12221
|
+
const reviewed = taskDb.reviewTask(db, {
|
|
12222
|
+
id: taskId,
|
|
11675
12223
|
actor: String(body.actor || DEFAULT_OWNER),
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
11681
|
-
return sendJson(res, 200, {
|
|
11682
|
-
ok: true,
|
|
11683
|
-
action: 'clear_plan',
|
|
11684
|
-
cleared_count: result.cleared.length,
|
|
11685
|
-
skipped_count: result.skipped.length,
|
|
11686
|
-
skipped: result.skipped,
|
|
11687
|
-
projection_path: outPath,
|
|
11688
|
-
tasks: result.cleared.map(task => taskFromProjection(projection, task.id) || taskById.get(task.id)).filter(Boolean),
|
|
12224
|
+
reward: body.reward === undefined ? 1 : body.reward,
|
|
12225
|
+
lesson: String(body.lesson || ''),
|
|
12226
|
+
nextTask: String(body.next || ''),
|
|
12227
|
+
proof: String(body.proof || ''),
|
|
12228
|
+
careerXpEligible: false,
|
|
11689
12229
|
});
|
|
12230
|
+
episode = reviewed.episode;
|
|
12231
|
+
nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, episode.next_task_suggestion) : null;
|
|
12232
|
+
xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
11690
12233
|
}
|
|
11691
|
-
const
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
12234
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12235
|
+
return sendJson(res, 200, {
|
|
12236
|
+
ok: true,
|
|
12237
|
+
action: 'finished',
|
|
12238
|
+
task_id: taskId,
|
|
12239
|
+
reviewed: Boolean(episode),
|
|
12240
|
+
episode,
|
|
12241
|
+
xp_projection: xpProjection,
|
|
12242
|
+
next_task_id: nextCreated ? nextCreated.id : null,
|
|
12243
|
+
projection_path: outPath,
|
|
12244
|
+
task: taskFromProjection(projection, taskId),
|
|
12245
|
+
});
|
|
12246
|
+
}
|
|
12247
|
+
|
|
12248
|
+
function postTaskApiReady({ res, taskDb, db, taskId, body }) {
|
|
12249
|
+
const proof = String(body.proof || '').trim();
|
|
12250
|
+
const proofIssue = meaningfulTaskProofIssue(proof);
|
|
12251
|
+
if (proofIssue) return sendProofIssue(res, proof, proofIssue);
|
|
12252
|
+
const nextTaskInput = normalizeReviewNextTaskInput(body.next);
|
|
12253
|
+
const actor = String(body.actor || DEFAULT_OWNER);
|
|
12254
|
+
const resultText = String(body.result || '').replace(/\s+/g, ' ').trim();
|
|
12255
|
+
if (resultText) {
|
|
12256
|
+
const resultIssue = resultSentenceIssue(resultText);
|
|
12257
|
+
if (resultIssue) return sendJson(res, 400, { ok: false, reason: 'weak_result', detail: resultIssue });
|
|
11710
12258
|
}
|
|
11711
|
-
const
|
|
11712
|
-
|
|
12259
|
+
const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
|
|
12260
|
+
actor,
|
|
12261
|
+
proof,
|
|
12262
|
+
changed: body.changed || resultText || body.done,
|
|
12263
|
+
checked: body.checked || body.check || body.verified,
|
|
12264
|
+
passed: body.passed || body.pass,
|
|
12265
|
+
failed: body.failed || body.fail,
|
|
12266
|
+
cost: body.cost,
|
|
12267
|
+
saved: body.saved || body.savings,
|
|
12268
|
+
tryNext: body.try_next || body.tryNext || body.try || body.handoff,
|
|
12269
|
+
status: body.status,
|
|
12270
|
+
files: body.files,
|
|
12271
|
+
commands: body.commands || body.command,
|
|
12272
|
+
});
|
|
12273
|
+
const result = taskDb.readyTask(db, {
|
|
12274
|
+
id: taskId,
|
|
12275
|
+
actor,
|
|
12276
|
+
proof,
|
|
12277
|
+
lesson: String(body.lesson || ''),
|
|
12278
|
+
nextTask: nextTaskInput.nextTask,
|
|
12279
|
+
resultTrace: resultTrace && resultTrace.trace,
|
|
12280
|
+
result: resultText,
|
|
12281
|
+
landing: body.landing || {
|
|
12282
|
+
happened: body.happened,
|
|
12283
|
+
checked: body.checked,
|
|
12284
|
+
tested: body.tested,
|
|
12285
|
+
decision: body.decision,
|
|
12286
|
+
},
|
|
12287
|
+
});
|
|
12288
|
+
if (!result.ready) return sendJson(res, 409, { ok: false, reason: result.reason });
|
|
12289
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12290
|
+
return sendJson(res, 200, {
|
|
12291
|
+
ok: true,
|
|
12292
|
+
action: 'ready',
|
|
12293
|
+
task_id: taskId,
|
|
12294
|
+
result_trace: resultTrace,
|
|
12295
|
+
...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
|
|
12296
|
+
projection_path: outPath,
|
|
12297
|
+
task: taskFromProjection(projection, taskId),
|
|
12298
|
+
});
|
|
12299
|
+
}
|
|
12300
|
+
|
|
12301
|
+
function postTaskApiAccept({ res, taskDb, db, taskId, body }) {
|
|
12302
|
+
const currentTask = enrichTaskProjection(taskDb.taskProjection(db, { taskId })).tasks[0] || null;
|
|
12303
|
+
const hasExplicitProof = Object.prototype.hasOwnProperty.call(body, 'proof');
|
|
12304
|
+
const proof = String(hasExplicitProof ? body.proof : currentTask?.metadata?.latest_agent_proof || '').trim();
|
|
12305
|
+
const proofIssue = meaningfulTaskProofIssue(proof);
|
|
12306
|
+
if (proofIssue) return sendProofIssue(res, proof, proofIssue);
|
|
12307
|
+
const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
|
|
12308
|
+
const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next');
|
|
12309
|
+
const lesson = hasExplicitLesson ? String(body.lesson || '') : String(currentTask?.review?.lesson || currentTask?.metadata?.latest_agent_lesson || '');
|
|
12310
|
+
const nextTask = hasExplicitNext ? String(body.next || '') : String(currentTask?.review?.next_task || currentTask?.metadata?.latest_agent_next_task || '');
|
|
12311
|
+
const clearedFields = [];
|
|
12312
|
+
if (hasExplicitLesson && !lesson.trim()) clearedFields.push('lesson');
|
|
12313
|
+
if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
|
|
12314
|
+
const parsedReward = parseAcceptReward(body.reward);
|
|
12315
|
+
if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
|
|
12316
|
+
const done = taskDb.doneTask(db, {
|
|
12317
|
+
id: taskId,
|
|
12318
|
+
status: 'done',
|
|
12319
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
12320
|
+
allowReview: true,
|
|
12321
|
+
action: 'accepted',
|
|
12322
|
+
proof,
|
|
12323
|
+
});
|
|
12324
|
+
if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
|
|
12325
|
+
const reviewed = taskDb.reviewTask(db, {
|
|
12326
|
+
id: taskId,
|
|
12327
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
12328
|
+
reward: parsedReward.value,
|
|
12329
|
+
lesson,
|
|
12330
|
+
nextTask,
|
|
12331
|
+
proof,
|
|
12332
|
+
careerXpEligible: true,
|
|
12333
|
+
clearedFields,
|
|
12334
|
+
});
|
|
12335
|
+
const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
|
|
12336
|
+
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
12337
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12338
|
+
return sendJson(res, 200, { ok: true, action: 'accepted', task_id: taskId, episode: reviewed.episode, xp_projection: xpProjection, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
12339
|
+
}
|
|
12340
|
+
|
|
12341
|
+
function postTaskApiRevise({ res, taskDb, db, taskId, body }) {
|
|
12342
|
+
const result = taskDb.reviseTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), note: String(body.note || body.reason || '') });
|
|
12343
|
+
if (!result.revised) return sendJson(res, 409, { ok: false, reason: result.reason });
|
|
12344
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12345
|
+
return sendJson(res, 200, { ok: true, action: 'revise', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
12346
|
+
}
|
|
12347
|
+
|
|
12348
|
+
function postTaskApiReview({ res, taskDb, db, taskId, body }) {
|
|
12349
|
+
const currentTask = taskDb.getTask(db, taskId);
|
|
12350
|
+
const rewardValue = body.reward === undefined ? 0 : body.reward;
|
|
12351
|
+
const proof = String(body.proof || '').trim();
|
|
12352
|
+
const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
|
|
12353
|
+
const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next')
|
|
12354
|
+
|| Object.prototype.hasOwnProperty.call(body, 'next_task')
|
|
12355
|
+
|| Object.prototype.hasOwnProperty.call(body, 'nextTask');
|
|
12356
|
+
const lessonText = hasExplicitLesson ? String(body.lesson || '') : '';
|
|
12357
|
+
const rawNext = Object.prototype.hasOwnProperty.call(body, 'next')
|
|
12358
|
+
? body.next
|
|
12359
|
+
: Object.prototype.hasOwnProperty.call(body, 'next_task')
|
|
12360
|
+
? body.next_task
|
|
12361
|
+
: body.nextTask;
|
|
12362
|
+
const nextTaskInput = normalizeReviewNextTaskInput(hasExplicitNext ? rawNext : '');
|
|
12363
|
+
const clearedFields = [];
|
|
12364
|
+
if (hasExplicitLesson && !lessonText.trim()) clearedFields.push('lesson');
|
|
12365
|
+
if (hasExplicitNext && !String(rawNext || '').trim()) clearedFields.push('next_task');
|
|
12366
|
+
const proofIssue = Number(rewardValue) > 0 || proof
|
|
12367
|
+
? meaningfulTaskProofIssue(proof)
|
|
12368
|
+
: null;
|
|
12369
|
+
if (proofIssue) return sendProofIssue(res, proof, proofIssue);
|
|
12370
|
+
const reviewed = taskDb.reviewTask(db, {
|
|
12371
|
+
id: taskId,
|
|
12372
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
12373
|
+
reward: rewardValue,
|
|
12374
|
+
lesson: lessonText,
|
|
12375
|
+
nextTask: nextTaskInput.nextTask,
|
|
12376
|
+
proof,
|
|
12377
|
+
careerXpEligible: false,
|
|
12378
|
+
clearedFields,
|
|
12379
|
+
});
|
|
12380
|
+
const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
|
|
12381
|
+
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
12382
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12383
|
+
return sendJson(res, 200, {
|
|
12384
|
+
ok: true,
|
|
12385
|
+
action: 'reviewed',
|
|
12386
|
+
task_id: taskId,
|
|
12387
|
+
episode: reviewed.episode,
|
|
12388
|
+
xp_projection: xpProjection,
|
|
12389
|
+
next_task_id: nextCreated ? nextCreated.id : null,
|
|
12390
|
+
...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
|
|
12391
|
+
projection_path: outPath,
|
|
12392
|
+
task: taskFromProjection(projection, taskId),
|
|
12393
|
+
});
|
|
12394
|
+
}
|
|
12395
|
+
|
|
12396
|
+
const TASK_API_POST_OPERATIONS = {
|
|
12397
|
+
step: postTaskApiStep,
|
|
12398
|
+
'continue-work': postTaskApiContinueWork,
|
|
12399
|
+
claim: postTaskApiClaim,
|
|
12400
|
+
message: postTaskApiMessage,
|
|
12401
|
+
chat: postTaskApiChat,
|
|
12402
|
+
plan: postTaskApiPlan,
|
|
12403
|
+
do: postTaskApiDo,
|
|
12404
|
+
backlog: postTaskApiBacklog,
|
|
12405
|
+
'review-chat': postTaskApiReviewChat,
|
|
12406
|
+
finish: postTaskApiFinish,
|
|
12407
|
+
ready: postTaskApiReady,
|
|
12408
|
+
accept: postTaskApiAccept,
|
|
12409
|
+
revise: postTaskApiRevise,
|
|
12410
|
+
review: postTaskApiReview,
|
|
12411
|
+
};
|
|
12412
|
+
|
|
12413
|
+
function serveTaskEvents({ res, taskDb, db, taskId }) {
|
|
12414
|
+
const events = taskDb.listTaskEvents(db, { taskId, limit: 500 });
|
|
12415
|
+
return sendJson(res, 200, { ok: true, events });
|
|
12416
|
+
}
|
|
12417
|
+
|
|
12418
|
+
async function serveTaskOperation({ req, res, taskDb, db, match }) {
|
|
11713
12419
|
const resolved = resolveTaskRef(taskDb, db, match[1]);
|
|
11714
12420
|
if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
|
|
11715
12421
|
const taskId = resolved.id;
|
|
11716
|
-
const
|
|
11717
|
-
if (req.method === 'GET' &&
|
|
11718
|
-
|
|
11719
|
-
return sendJson(res, 200, { ok: true, events });
|
|
12422
|
+
const operation = match[2];
|
|
12423
|
+
if (req.method === 'GET' && operation === 'events') {
|
|
12424
|
+
return serveTaskEvents({ res, taskDb, db, taskId });
|
|
11720
12425
|
}
|
|
11721
12426
|
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
|
|
11722
12427
|
const body = await readJsonBody(req);
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
page: error.page || null,
|
|
11735
|
-
});
|
|
11736
|
-
}
|
|
11737
|
-
}
|
|
11738
|
-
if (op === 'continue-work') {
|
|
11739
|
-
try {
|
|
11740
|
-
const result = continueWorkForReviewTask(taskDb, db, taskId, { owner: body.owner || body.actor || DEFAULT_OWNER });
|
|
11741
|
-
return sendJson(res, 200, result);
|
|
11742
|
-
} catch (error) {
|
|
11743
|
-
return sendJson(res, error.status || 409, {
|
|
11744
|
-
ok: false,
|
|
11745
|
-
action: 'continue_work',
|
|
11746
|
-
task_id: taskId,
|
|
11747
|
-
reason: error.reason || 'continue_work_failed',
|
|
11748
|
-
detail: error.message,
|
|
11749
|
-
});
|
|
11750
|
-
}
|
|
11751
|
-
}
|
|
11752
|
-
if (op === 'claim') {
|
|
11753
|
-
const owner = String(body.owner || body.actor || DEFAULT_OWNER);
|
|
11754
|
-
const result = taskDb.claimTask(db, { id: taskId, claimedBy: owner });
|
|
11755
|
-
if (!result.claimed) return sendJson(res, 409, { ok: false, reason: result.reason, claimed_by: result.claimed_by || null });
|
|
11756
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11757
|
-
return sendJson(res, 200, { ok: true, action: 'claimed', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
11758
|
-
}
|
|
11759
|
-
if (op === 'message') {
|
|
11760
|
-
const result = taskDb.noteTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), content: String(body.content || '') });
|
|
11761
|
-
if (!result.noted) return sendJson(res, 404, { ok: false, reason: result.reason });
|
|
11762
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11763
|
-
return sendJson(res, 200, { ok: true, action: 'noted', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
11764
|
-
}
|
|
11765
|
-
if (op === 'chat') {
|
|
11766
|
-
const result = taskDb.chatTask(db, {
|
|
11767
|
-
id: taskId,
|
|
11768
|
-
actor: String(body.actor || DEFAULT_OWNER),
|
|
11769
|
-
content: String(body.content || body.message || body.text || ''),
|
|
11770
|
-
goal: String(body.goal || body.objective || ''),
|
|
11771
|
-
summary: String(body.summary || ''),
|
|
11772
|
-
});
|
|
11773
|
-
if (!result.chatted) {
|
|
11774
|
-
const status = result.reason === 'content_required' ? 400 : result.reason === 'not_found' ? 404 : 409;
|
|
11775
|
-
return sendJson(res, status, { ok: false, reason: result.reason, detail: stageErrorDetail('task chat', result.reason, result) });
|
|
11776
|
-
}
|
|
11777
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11778
|
-
return sendJson(res, 200, {
|
|
11779
|
-
ok: true,
|
|
11780
|
-
action: 'chatted',
|
|
11781
|
-
task_id: taskId,
|
|
11782
|
-
version: result.event.version,
|
|
11783
|
-
goal_changed: result.goal_changed,
|
|
11784
|
-
chat_packet: result.chat_packet,
|
|
11785
|
-
projection_path: outPath,
|
|
11786
|
-
task: taskFromProjection(projection, taskId),
|
|
11787
|
-
});
|
|
11788
|
-
}
|
|
11789
|
-
if (op === 'plan') {
|
|
11790
|
-
const actor = String(body.actor || DEFAULT_OWNER);
|
|
11791
|
-
const goal = String(body.goal || body.objective || '');
|
|
11792
|
-
const summary = String(body.summary || body.plan || '');
|
|
11793
|
-
const owner = String(body.owner || body.assignee || '');
|
|
11794
|
-
const exit = String(body.exit || body.exit_condition || '');
|
|
11795
|
-
const firstMove = String(body.first_move || body.firstMove || body.first || '');
|
|
11796
|
-
const task = taskDetail(taskDb, db, taskId);
|
|
11797
|
-
const automaticPlan = buildAutomaticPlanTrace(taskDb, task, {
|
|
11798
|
-
actor,
|
|
11799
|
-
actorExplicit: Boolean(body.actor),
|
|
11800
|
-
owner,
|
|
11801
|
-
goal,
|
|
11802
|
-
summary,
|
|
11803
|
-
firstMove,
|
|
11804
|
-
exit,
|
|
11805
|
-
});
|
|
11806
|
-
const result = taskDb.stageTask(db, {
|
|
11807
|
-
id: taskId,
|
|
11808
|
-
actor,
|
|
11809
|
-
stage: 'plan',
|
|
11810
|
-
goal,
|
|
11811
|
-
summary,
|
|
11812
|
-
owner: automaticPlan.ownerForStage || owner,
|
|
11813
|
-
exit,
|
|
11814
|
-
proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
|
|
11815
|
-
firstMove,
|
|
11816
|
-
nextButton: String(body.next_button || body.nextButton || ''),
|
|
11817
|
-
confidence: body.confidence,
|
|
11818
|
-
planTrace: automaticPlan.trace,
|
|
11819
|
-
});
|
|
11820
|
-
if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task plan', result.reason, result) });
|
|
11821
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11822
|
-
return sendJson(res, 200, { ok: true, action: 'planned', task_id: taskId, version: result.event.version, plan_trace: automaticPlan.trace, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
11823
|
-
}
|
|
11824
|
-
if (op === 'do') {
|
|
11825
|
-
const firstMove = String(body.first_move || body.firstMove || body.first || '').trim();
|
|
11826
|
-
if (!firstMove) return sendJson(res, 400, { ok: false, reason: 'first_move_required', detail: 'task do: first_move required' });
|
|
11827
|
-
const result = taskDb.stageTask(db, {
|
|
11828
|
-
id: taskId,
|
|
11829
|
-
actor: String(body.actor || DEFAULT_OWNER),
|
|
11830
|
-
stage: 'do',
|
|
11831
|
-
goal: String(body.goal || body.objective || ''),
|
|
11832
|
-
summary: String(body.summary || ''),
|
|
11833
|
-
owner: String(body.actor || DEFAULT_OWNER),
|
|
11834
|
-
exit: String(body.exit || body.exit_condition || body.exitCondition || ''),
|
|
11835
|
-
proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
|
|
11836
|
-
firstMove,
|
|
11837
|
-
nextButton: String(body.next_button || body.nextButton || ''),
|
|
11838
|
-
confidence: body.confidence,
|
|
11839
|
-
});
|
|
11840
|
-
if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task do', result.reason, result) });
|
|
11841
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11842
|
-
return sendJson(res, 200, { ok: true, action: 'doing', task_id: taskId, version: result.event.version, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
11843
|
-
}
|
|
11844
|
-
if (op === 'backlog') {
|
|
11845
|
-
const result = taskDb.backlogTask(db, {
|
|
11846
|
-
id: taskId,
|
|
11847
|
-
actor: String(body.actor || DEFAULT_OWNER),
|
|
11848
|
-
reason: String(body.reason || body.note || 'clear_plan'),
|
|
11849
|
-
tag: String(body.tag || 'capture'),
|
|
11850
|
-
});
|
|
11851
|
-
if (!result.backlogged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task backlog', result.reason, result) });
|
|
11852
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11853
|
-
return sendJson(res, 200, {
|
|
11854
|
-
ok: true,
|
|
11855
|
-
action: 'backlogged',
|
|
11856
|
-
task_id: taskId,
|
|
11857
|
-
version: result.event.version,
|
|
11858
|
-
cleared_keys: result.cleared_keys,
|
|
11859
|
-
projection_path: outPath,
|
|
11860
|
-
task: taskFromProjection(projection, taskId),
|
|
11861
|
-
});
|
|
11862
|
-
}
|
|
11863
|
-
if (op === 'review-chat') {
|
|
11864
|
-
try {
|
|
11865
|
-
const result = appendTaskReviewChat(taskDb, db, taskId, {
|
|
11866
|
-
reviewer: body.reviewer || body.actor || 'codex-review',
|
|
11867
|
-
dryRun: Boolean(body.dryRun || body.noNote),
|
|
11868
|
-
});
|
|
11869
|
-
const { event, compactProjection, outPath, ...payload } = result;
|
|
11870
|
-
return sendJson(res, 200, payload);
|
|
11871
|
-
} catch (error) {
|
|
11872
|
-
return sendJson(res, error.status || 409, {
|
|
11873
|
-
ok: false,
|
|
11874
|
-
reason: error.reason || 'review_chat_failed',
|
|
11875
|
-
detail: error.message,
|
|
11876
|
-
});
|
|
12428
|
+
const postTaskOperation = TASK_API_POST_OPERATIONS[operation];
|
|
12429
|
+
if (!postTaskOperation) return;
|
|
12430
|
+
return postTaskOperation({ res, taskDb, db, taskId, body });
|
|
12431
|
+
}
|
|
12432
|
+
|
|
12433
|
+
function matchTaskApiRoute(req, url) {
|
|
12434
|
+
for (const route of TASK_API_ROUTES) {
|
|
12435
|
+
if (route.method && route.method !== req.method) continue;
|
|
12436
|
+
if (Object.prototype.hasOwnProperty.call(route, 'pathname')) {
|
|
12437
|
+
if (route.pathname === null || route.pathname === url.pathname) return { route, match: null };
|
|
12438
|
+
continue;
|
|
11877
12439
|
}
|
|
12440
|
+
const match = url.pathname.match(route.pattern);
|
|
12441
|
+
if (match) return { route, match };
|
|
11878
12442
|
}
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
lesson: String(body.lesson || ''),
|
|
11903
|
-
nextTask: String(body.next || ''),
|
|
11904
|
-
proof: String(body.proof || ''),
|
|
11905
|
-
careerXpEligible: false,
|
|
11906
|
-
});
|
|
11907
|
-
episode = reviewed.episode;
|
|
11908
|
-
nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, episode.next_task_suggestion) : null;
|
|
11909
|
-
xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
11910
|
-
}
|
|
11911
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11912
|
-
return sendJson(res, 200, {
|
|
11913
|
-
ok: true,
|
|
11914
|
-
action: 'finished',
|
|
11915
|
-
task_id: taskId,
|
|
11916
|
-
reviewed: Boolean(episode),
|
|
11917
|
-
episode,
|
|
11918
|
-
xp_projection: xpProjection,
|
|
11919
|
-
next_task_id: nextCreated ? nextCreated.id : null,
|
|
11920
|
-
projection_path: outPath,
|
|
11921
|
-
task: taskFromProjection(projection, taskId),
|
|
11922
|
-
});
|
|
11923
|
-
}
|
|
11924
|
-
if (op === 'ready') {
|
|
11925
|
-
const proof = String(body.proof || '').trim();
|
|
11926
|
-
const proofIssue = meaningfulTaskProofIssue(proof);
|
|
11927
|
-
if (proofIssue) return sendProofIssue(res, proof, proofIssue);
|
|
11928
|
-
const nextTaskInput = normalizeReviewNextTaskInput(body.next);
|
|
11929
|
-
const actor = String(body.actor || DEFAULT_OWNER);
|
|
11930
|
-
const resultText = String(body.result || '').replace(/\s+/g, ' ').trim();
|
|
11931
|
-
if (resultText) {
|
|
11932
|
-
const resultIssue = resultSentenceIssue(resultText);
|
|
11933
|
-
if (resultIssue) return sendJson(res, 400, { ok: false, reason: 'weak_result', detail: resultIssue });
|
|
11934
|
-
}
|
|
11935
|
-
const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
|
|
11936
|
-
actor,
|
|
11937
|
-
proof,
|
|
11938
|
-
changed: body.changed || resultText || body.done,
|
|
11939
|
-
checked: body.checked || body.check || body.verified,
|
|
11940
|
-
passed: body.passed || body.pass,
|
|
11941
|
-
failed: body.failed || body.fail,
|
|
11942
|
-
cost: body.cost,
|
|
11943
|
-
saved: body.saved || body.savings,
|
|
11944
|
-
tryNext: body.try_next || body.tryNext || body.try || body.handoff,
|
|
11945
|
-
status: body.status,
|
|
11946
|
-
files: body.files,
|
|
11947
|
-
commands: body.commands || body.command,
|
|
11948
|
-
});
|
|
11949
|
-
const result = taskDb.readyTask(db, {
|
|
11950
|
-
id: taskId,
|
|
11951
|
-
actor,
|
|
11952
|
-
proof,
|
|
11953
|
-
lesson: String(body.lesson || ''),
|
|
11954
|
-
nextTask: nextTaskInput.nextTask,
|
|
11955
|
-
resultTrace: resultTrace && resultTrace.trace,
|
|
11956
|
-
result: resultText,
|
|
11957
|
-
landing: body.landing || {
|
|
11958
|
-
happened: body.happened,
|
|
11959
|
-
checked: body.checked,
|
|
11960
|
-
tested: body.tested,
|
|
11961
|
-
decision: body.decision,
|
|
11962
|
-
},
|
|
11963
|
-
});
|
|
11964
|
-
if (!result.ready) return sendJson(res, 409, { ok: false, reason: result.reason });
|
|
11965
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
11966
|
-
return sendJson(res, 200, {
|
|
11967
|
-
ok: true,
|
|
11968
|
-
action: 'ready',
|
|
11969
|
-
task_id: taskId,
|
|
11970
|
-
result_trace: resultTrace,
|
|
11971
|
-
...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
|
|
11972
|
-
projection_path: outPath,
|
|
11973
|
-
task: taskFromProjection(projection, taskId),
|
|
11974
|
-
});
|
|
11975
|
-
}
|
|
11976
|
-
if (op === 'accept') {
|
|
11977
|
-
const currentTask = enrichTaskProjection(taskDb.taskProjection(db, { taskId })).tasks[0] || null;
|
|
11978
|
-
const hasExplicitProof = Object.prototype.hasOwnProperty.call(body, 'proof');
|
|
11979
|
-
const proof = String(hasExplicitProof ? body.proof : currentTask?.metadata?.latest_agent_proof || '').trim();
|
|
11980
|
-
const proofIssue = meaningfulTaskProofIssue(proof);
|
|
11981
|
-
if (proofIssue) return sendProofIssue(res, proof, proofIssue);
|
|
11982
|
-
const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
|
|
11983
|
-
const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next');
|
|
11984
|
-
const lesson = hasExplicitLesson ? String(body.lesson || '') : String(currentTask?.review?.lesson || currentTask?.metadata?.latest_agent_lesson || '');
|
|
11985
|
-
const nextTask = hasExplicitNext ? String(body.next || '') : String(currentTask?.review?.next_task || currentTask?.metadata?.latest_agent_next_task || '');
|
|
11986
|
-
const clearedFields = [];
|
|
11987
|
-
if (hasExplicitLesson && !lesson.trim()) clearedFields.push('lesson');
|
|
11988
|
-
if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
|
|
11989
|
-
const parsedReward = parseAcceptReward(body.reward);
|
|
11990
|
-
if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
|
|
11991
|
-
const done = taskDb.doneTask(db, {
|
|
11992
|
-
id: taskId,
|
|
11993
|
-
status: 'done',
|
|
11994
|
-
actor: String(body.actor || DEFAULT_OWNER),
|
|
11995
|
-
allowReview: true,
|
|
11996
|
-
action: 'accepted',
|
|
11997
|
-
proof,
|
|
11998
|
-
});
|
|
11999
|
-
if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
|
|
12000
|
-
const reviewed = taskDb.reviewTask(db, {
|
|
12001
|
-
id: taskId,
|
|
12002
|
-
actor: String(body.actor || DEFAULT_OWNER),
|
|
12003
|
-
reward: parsedReward.value,
|
|
12004
|
-
lesson,
|
|
12005
|
-
nextTask,
|
|
12006
|
-
proof,
|
|
12007
|
-
careerXpEligible: true,
|
|
12008
|
-
clearedFields,
|
|
12009
|
-
});
|
|
12010
|
-
const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
|
|
12011
|
-
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
12012
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12013
|
-
return sendJson(res, 200, { ok: true, action: 'accepted', task_id: taskId, episode: reviewed.episode, xp_projection: xpProjection, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
12014
|
-
}
|
|
12015
|
-
if (op === 'revise') {
|
|
12016
|
-
const result = taskDb.reviseTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), note: String(body.note || body.reason || '') });
|
|
12017
|
-
if (!result.revised) return sendJson(res, 409, { ok: false, reason: result.reason });
|
|
12018
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12019
|
-
return sendJson(res, 200, { ok: true, action: 'revise', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
12020
|
-
}
|
|
12021
|
-
if (op === 'review') {
|
|
12022
|
-
const currentTask = taskDb.getTask(db, taskId);
|
|
12023
|
-
const rewardValue = body.reward === undefined ? 0 : body.reward;
|
|
12024
|
-
const proof = String(body.proof || '').trim();
|
|
12025
|
-
const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
|
|
12026
|
-
const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next')
|
|
12027
|
-
|| Object.prototype.hasOwnProperty.call(body, 'next_task')
|
|
12028
|
-
|| Object.prototype.hasOwnProperty.call(body, 'nextTask');
|
|
12029
|
-
const lessonText = hasExplicitLesson ? String(body.lesson || '') : '';
|
|
12030
|
-
const rawNext = Object.prototype.hasOwnProperty.call(body, 'next')
|
|
12031
|
-
? body.next
|
|
12032
|
-
: Object.prototype.hasOwnProperty.call(body, 'next_task')
|
|
12033
|
-
? body.next_task
|
|
12034
|
-
: body.nextTask;
|
|
12035
|
-
const nextTaskInput = normalizeReviewNextTaskInput(hasExplicitNext ? rawNext : '');
|
|
12036
|
-
const clearedFields = [];
|
|
12037
|
-
if (hasExplicitLesson && !lessonText.trim()) clearedFields.push('lesson');
|
|
12038
|
-
if (hasExplicitNext && !String(rawNext || '').trim()) clearedFields.push('next_task');
|
|
12039
|
-
const proofIssue = Number(rewardValue) > 0 || proof
|
|
12040
|
-
? meaningfulTaskProofIssue(proof)
|
|
12041
|
-
: null;
|
|
12042
|
-
if (proofIssue) return sendProofIssue(res, proof, proofIssue);
|
|
12043
|
-
const reviewed = taskDb.reviewTask(db, {
|
|
12044
|
-
id: taskId,
|
|
12045
|
-
actor: String(body.actor || DEFAULT_OWNER),
|
|
12046
|
-
reward: rewardValue,
|
|
12047
|
-
lesson: lessonText,
|
|
12048
|
-
nextTask: nextTaskInput.nextTask,
|
|
12049
|
-
proof,
|
|
12050
|
-
careerXpEligible: false,
|
|
12051
|
-
clearedFields,
|
|
12052
|
-
});
|
|
12053
|
-
const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
|
|
12054
|
-
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
12055
|
-
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
12056
|
-
return sendJson(res, 200, {
|
|
12057
|
-
ok: true,
|
|
12058
|
-
action: 'reviewed',
|
|
12059
|
-
task_id: taskId,
|
|
12060
|
-
episode: reviewed.episode,
|
|
12061
|
-
xp_projection: xpProjection,
|
|
12062
|
-
next_task_id: nextCreated ? nextCreated.id : null,
|
|
12063
|
-
...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
|
|
12064
|
-
projection_path: outPath,
|
|
12065
|
-
task: taskFromProjection(projection, taskId),
|
|
12443
|
+
return null;
|
|
12444
|
+
}
|
|
12445
|
+
|
|
12446
|
+
async function handleTaskApi(req, res, taskDb, db) {
|
|
12447
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
12448
|
+
const matchedRoute = matchTaskApiRoute(req, url);
|
|
12449
|
+
if (!matchedRoute) return sendJson(res, 404, { ok: false, reason: 'not_found' });
|
|
12450
|
+
return matchedRoute.route.serve({
|
|
12451
|
+
req,
|
|
12452
|
+
res,
|
|
12453
|
+
taskDb,
|
|
12454
|
+
db,
|
|
12455
|
+
url,
|
|
12456
|
+
match: matchedRoute.match,
|
|
12457
|
+
});
|
|
12458
|
+
}
|
|
12459
|
+
|
|
12460
|
+
function createTaskApiServer(taskApi = {}) {
|
|
12461
|
+
const taskDb = taskApi.taskDb || getTaskDb();
|
|
12462
|
+
const db = taskApi.db || taskDb.open();
|
|
12463
|
+
return http.createServer((req, res) => {
|
|
12464
|
+
handleTaskApi(req, res, taskDb, db).catch((error) => {
|
|
12465
|
+
sendJson(res, 500, { ok: false, reason: 'server_error', detail: String(error && error.message || error) });
|
|
12066
12466
|
});
|
|
12067
|
-
}
|
|
12467
|
+
});
|
|
12068
12468
|
}
|
|
12069
12469
|
|
|
12070
12470
|
function cmdServe(args) {
|
|
@@ -12072,11 +12472,7 @@ function cmdServe(args) {
|
|
|
12072
12472
|
const port = Number(flag(args, '--port') || process.env.PORT || 8787);
|
|
12073
12473
|
const taskDb = getTaskDb();
|
|
12074
12474
|
const db = taskDb.open();
|
|
12075
|
-
const server =
|
|
12076
|
-
handleTaskApi(req, res, taskDb, db).catch((e) => {
|
|
12077
|
-
sendJson(res, 500, { ok: false, reason: 'server_error', detail: String(e && e.message || e) });
|
|
12078
|
-
});
|
|
12079
|
-
});
|
|
12475
|
+
const server = createTaskApiServer({ taskDb, db });
|
|
12080
12476
|
return new Promise((resolve, reject) => {
|
|
12081
12477
|
server.on('error', reject);
|
|
12082
12478
|
server.listen(port, host, () => {
|
|
@@ -12201,6 +12597,9 @@ async function runTaskCommand(args) {
|
|
|
12201
12597
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
12202
12598
|
case 'archive': return cmdArchive(rest);
|
|
12203
12599
|
case 'clear-done': return cmdClearDone(rest);
|
|
12600
|
+
case 'reap-mission-blockers':
|
|
12601
|
+
case 'reap-blockers':
|
|
12602
|
+
return cmdReapMissionBlockers(rest);
|
|
12204
12603
|
case 'relabel-archived': return cmdRelabelArchived(rest);
|
|
12205
12604
|
case 'review': return cmdReview(rest);
|
|
12206
12605
|
case 'reviews':
|
|
@@ -12241,7 +12640,7 @@ const MUTATING_TASK_COMMANDS = new Set([
|
|
|
12241
12640
|
'continue-work', 'continue', 'chat', 'note', 'say', 'retitle', 'tag', 'tags', 'step',
|
|
12242
12641
|
'ready', 'result', 'accept', 'landing', 'land-review', 'auto-accept-certified',
|
|
12243
12642
|
'auto-accept', 'sweep', 'audit', 'certify-verified', 'accept-group', 'revise',
|
|
12244
|
-
'done', 'finish', 'fail', 'archive', 'clear-done', 'relabel-archived', 'review', 'import',
|
|
12643
|
+
'done', 'finish', 'fail', 'archive', 'clear-done', 'reap-mission-blockers', 'reap-blockers', 'relabel-archived', 'review', 'import',
|
|
12245
12644
|
'setup', 'review-lane-act', 'review-act', 'act-review', 'review-lane-loop',
|
|
12246
12645
|
'review-loop', 'loop-review', 'review-lane-run', 'review-run', 'run-review',
|
|
12247
12646
|
]);
|
|
@@ -12257,6 +12656,7 @@ async function run(args) {
|
|
|
12257
12656
|
|
|
12258
12657
|
module.exports = {
|
|
12259
12658
|
run,
|
|
12659
|
+
createTaskApiServer,
|
|
12260
12660
|
taskDayGroups,
|
|
12261
12661
|
taskDayTextGroups,
|
|
12262
12662
|
taskDayTitle,
|
|
@@ -12265,4 +12665,8 @@ module.exports = {
|
|
|
12265
12665
|
delegateTask,
|
|
12266
12666
|
AGENT_ENV_MARKERS,
|
|
12267
12667
|
autoRenderTodoFromDb,
|
|
12668
|
+
projectionMissions,
|
|
12669
|
+
projectionWishes,
|
|
12670
|
+
taskBoardViewModel,
|
|
12671
|
+
taskBoardTemplate,
|
|
12268
12672
|
};
|