atris 3.79.0 → 3.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/atris/atris.md +66 -167
- package/atris/skills/security-audit/SKILL.md +77 -0
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris.md +66 -167
- package/bin/atris.js +6 -0
- package/commands/auth.js +2 -2
- package/commands/business.js +44 -1984
- package/commands/computer.js +165 -2412
- package/commands/member.js +189 -966
- package/commands/mission.js +229 -1409
- package/commands/pack.js +112 -1476
- package/commands/security-review.js +129 -0
- package/commands/site-deploy.js +5 -1
- package/commands/task.js +137 -1359
- package/lib/business-core.js +2022 -0
- package/lib/computer-core.js +2447 -0
- package/lib/engine-ask.js +1 -0
- package/lib/handoff-wake.js +8 -6
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +21 -2
- package/lib/member-board.js +12 -10
- package/lib/member-core.js +1014 -0
- package/lib/member-up.js +46 -3
- package/lib/mission-core.js +1559 -0
- package/lib/pack-core.js +1588 -0
- package/lib/security-audit-classes.js +318 -0
- package/lib/security-audit.js +1549 -0
- package/lib/task-core.js +1412 -0
- package/lib/task-decision.js +66 -0
- package/lib/task-explanation.js +20 -0
- package/lib/yes-lock.js +1 -1
- package/package.json +9 -2
- package/utils/auth.js +18 -4
package/commands/task.js
CHANGED
|
@@ -63,8 +63,12 @@ const {
|
|
|
63
63
|
buildInspectPayload,
|
|
64
64
|
} = require('../lib/inspect-fields');
|
|
65
65
|
const {
|
|
66
|
+
taskTagTokens,
|
|
67
|
+
isDecisionHoldTag,
|
|
66
68
|
isDecisionTask,
|
|
67
69
|
decisionMarkerFor,
|
|
70
|
+
summarizeSkippedDecisions,
|
|
71
|
+
formatDecisionRefusalLines,
|
|
68
72
|
DECISION_REFUSE_REASON,
|
|
69
73
|
} = require('../lib/task-decision');
|
|
70
74
|
const {
|
|
@@ -79,713 +83,93 @@ const {
|
|
|
79
83
|
taskNextCommand,
|
|
80
84
|
} = require('../lib/first-minute');
|
|
81
85
|
const { loadContext } = require('../lib/state-detection');
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return `
|
|
170
|
-
atris task - durable local task state (SQLite, gitignored)
|
|
171
|
-
|
|
172
|
-
golden path (one tick, by cron or by hand):
|
|
173
|
-
atris task delegate "fix the login bug" --to <member>
|
|
174
|
-
atris task claim <id> --as <member>
|
|
175
|
-
... build ...
|
|
176
|
-
atris task ready <id> --verify
|
|
177
|
-
atris autoland tick # second check runs, task lands
|
|
178
|
-
|
|
179
|
-
atris task Same two-line next as bare atris
|
|
180
|
-
atris task desk [--all] Show the full task desk
|
|
181
|
-
atris task new "<title>" [--what-changes "..."] [--why-it-matters "..."] [--done-looks-like "..."] [--verify <cmd>]
|
|
182
|
-
Create a task with a plain explanation; omitted fields get honest defaults
|
|
183
|
-
atris task next [--tag <tag>] [--create-next]
|
|
184
|
-
Same next command as bare atris and the task desk; --create-next still seeds Endgame fallback
|
|
185
|
-
atris task continue-work <id> Create/reuse a certified Review follow-up task
|
|
186
|
-
atris task say <id> "<message>" Add context to a task
|
|
187
|
-
atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
|
|
188
|
-
atris task ready <id> --proof "..." --result "<sentence>"
|
|
189
|
-
Agent proof ready; native goal can complete
|
|
190
|
-
atris task ready <id> --verify "<cmd>" --result "<sentence>"
|
|
191
|
-
Run <cmd>; only ready if it exits 0 (executed proof)
|
|
192
|
-
Writes atris/runs/ receipt (pass or fail); that local pass is enough proof
|
|
193
|
-
Optional CI URL only with --proof-url and --i-fetched
|
|
194
|
-
atris task receipt <id> --verify "<cmd>" Run <cmd> and write an atris/runs/ receipt without going to ready
|
|
195
|
-
atris task plan-preview "<purpose>" [--tag <tag>] [--owner <member>] [--task <id>]
|
|
196
|
-
Show the plain Plan before work starts
|
|
197
|
-
atris task ready <id> --proof "..." --result "<sentence>" [--changed "..." --checked "..." --saved "..." --try "..."]
|
|
198
|
-
Agent proof ready; records Result if needed
|
|
199
|
-
atris task ready <id> --proof "..." --result "<sentence>" [--happened "..." --checked "..." --tested "..." --decision "..."]
|
|
200
|
-
Agent proof ready; writes the human result receipt
|
|
201
|
-
atris task result <id> "<sentence>" Set or replace the day-one PM result sentence
|
|
202
|
-
atris task result <id> --changed "..." --checked "..." [--saved "..."] [--try "..."]
|
|
203
|
-
Show the plain Result and store trace on the task
|
|
204
|
-
atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
|
|
205
|
-
atris task accept <id> [--proof "..."] [--public]
|
|
206
|
-
Human accepts proof, marks done; --public also publishes AgentXP
|
|
207
|
-
atris task certify-verified [--dry-run] [--limit <n>] [--as <actor>]
|
|
208
|
-
Re-run the runnable check named in each Review proof as a second actor; passing rows certify (denied lanes and check-less rows wait for a human)
|
|
209
|
-
atris task auto-accept-certified --dry-run [--strict-verify] [--all] [--limit <n>]
|
|
210
|
-
Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
|
|
211
|
-
atris task sweep --auto-accept [--json] Auto-accept verified Review rows; protected lanes wait for human
|
|
212
|
-
atris task sweep [--json] [--stale-days N] [--claimer NAME] [--dry-run] Release expired claims
|
|
213
|
-
atris task sweep --held [--everywhere] [--dry-run] [--json] Check held jobs against GitHub: credit a merge, release a closed pull request
|
|
214
|
-
atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
|
|
215
|
-
atris task revise <id> --note "..." Send reviewed work back to Do
|
|
216
|
-
|
|
217
|
-
atris task add "<title>" [--tag <tag>] [--goal-id <id>] [--what-changes "..."] [--why-it-matters "..."] [--done-looks-like "..."] [--verify <cmd>] Create a task
|
|
218
|
-
atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] [--what-changes "..."] [--why-it-matters "..."] [--done-looks-like "..."] [--verify <cmd>] Create assigned work
|
|
219
|
-
atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
|
|
220
|
-
Record a task-owned Plan stage
|
|
221
|
-
atris task do <id> --as <owner> --first-move "..."
|
|
222
|
-
Start task-owned Do work from the plan
|
|
223
|
-
atris task backlog <id> [--reason "..."] Move a planned open task back to Backlog
|
|
224
|
-
atris task clear-plan --yes Move all planned open tasks back to Backlog
|
|
225
|
-
atris task day [--full] [--all] [--everywhere] [--json] show today's owner-grouped task list
|
|
226
|
-
text shows eight current rows; --full shows every active row
|
|
227
|
-
--all stays in this workspace; --everywhere spans workspaces
|
|
228
|
-
atris task list [--all] [--everywhere] [--status <s>]
|
|
229
|
-
list tasks in this workspace; --everywhere spans workspaces
|
|
230
|
-
atris task claim <id> [--as <owner>] Atomic claim
|
|
231
|
-
atris task release <id> [--as <owner>] [--reason "<why>"] Release your own mistaken claim back to open
|
|
232
|
-
atris task capabilities [--json] Read-only task CLI/API capability contract
|
|
233
|
-
atris task capabilities-check [--json] Verify task capability contract conformance
|
|
234
|
-
atris task review-lane-drain [--json] Pick next safe Review-lane agent action
|
|
235
|
-
atris task review-lane-act [--json] Execute next safe Review-lane agent action
|
|
236
|
-
atris task review-lane-loop [--json] Run bounded safe Review-lane actions
|
|
237
|
-
atris task review-lane-run [--json] Run bounded review-lane loops and write receipts
|
|
238
|
-
atris task current [--json] [--goal-id <id>] [--tag <tag>] [--status <s>] [--review-state <lane>]
|
|
239
|
-
Read-only best next task page + queue
|
|
240
|
-
atris task queue [--json] [--goal-id <id>] [--tag <tag>] [--status <s>] [--review-state <lane>]
|
|
241
|
-
Read-only task lanes + current page
|
|
242
|
-
atris task current-step [--json] [--goal-id <id>] [--tag <tag>] [--review-state <lane>]
|
|
243
|
-
Advance the scoped current task one safe step
|
|
244
|
-
review-state lanes: needs-agent, continue-work, human-accept-waiting, certified
|
|
245
|
-
atris task note <id> "<message>" Append dialogue/context to a task
|
|
246
|
-
atris task retitle <id> "<new title>" Rename a task and preserve the old title in dialogue
|
|
247
|
-
atris task tag <id> --add <tag> [--remove <tag>]
|
|
248
|
-
Update tags on an existing task (e.g. --add needs-human to hold it
|
|
249
|
-
from sweep + fleet staffing); logs a task_tags_updated event
|
|
250
|
-
atris task show <id> [--json] Show a task card + dialogue
|
|
251
|
-
atris task inspect <id> --fields review,status,title [--json]
|
|
252
|
-
Field-selectable task state (review metadata, status, title, owner, tag)
|
|
253
|
-
atris task page <id> [--json] Show the one-task page contract
|
|
254
|
-
atris task step <id> [--json] Refine chat, then advance one safe Plan/Do/Review step
|
|
255
|
-
atris task done <id> --proof "..." Mark complete with proof
|
|
256
|
-
atris task done <id> --failed [--proof "..."] Mark failed, optionally reviewed
|
|
257
|
-
atris task archive <id> --reason "..." [--from-failed] [--not-needed]
|
|
258
|
-
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
259
|
-
--from-failed opts in to relabel a fail-closed row (never done)
|
|
260
|
-
--not-needed: a person confirms the job did not need doing, so a
|
|
261
|
-
drop on it stops counting against the member (needs --reason;
|
|
262
|
-
refused inside an agent session or a dispatched worker)
|
|
263
|
-
atris task reopen <id> [--reason "..."] Send a done, waiting, failed, or archived task back to open
|
|
264
|
-
atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
|
|
265
|
-
atris task prune-test-junk [--yes] [--vacuum] [--json]
|
|
266
|
-
Count rows test runs left from temp folders; --yes backs up the db, then removes them
|
|
267
|
-
atris task keep [--json] Put away finished and untouched work, then refresh the list
|
|
268
|
-
atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
|
|
269
|
-
atris task relabel-archived [--dry-run|--apply]
|
|
270
|
-
One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
|
|
271
|
-
atris task finish <id> --proof "..." Legacy alias for done with proof
|
|
272
|
-
atris task review <id> --reward <n> [--verify "<cmd>"]
|
|
273
|
-
Write review event + RSI episode
|
|
274
|
-
atris task reviews [--all|--limit <n>] [--verbose] Show certified Review items for human accept/revise
|
|
275
|
-
atris task reviews --group-by <tag|owner|source> Cluster approval-ready work for fast triage
|
|
276
|
-
atris task accept-group <key>=<value> --spot-check K --confirm-human-accept --as <you> --verified <ids>
|
|
277
|
-
Accept a whole cluster; career XP only on the K you verified
|
|
278
|
-
atris task status [--json] [--history] Compact live status for web/Swarlo
|
|
279
|
-
atris task setup [--import-todo] Create/refresh task projection
|
|
280
|
-
atris task serve [--port <n>] Open local task factory board
|
|
281
|
-
atris task sync --dry-run Plan cloud/Swarlo task sync writes
|
|
282
|
-
atris task import <file> One-shot import from TODO.md
|
|
283
|
-
atris task lineage <id> [--json] Show endgame -> tasks -> commits chain
|
|
284
|
-
atris task events [id] [--limit <n>] print recent task events
|
|
285
|
-
atris task events --all print the full current-workspace ledger
|
|
286
|
-
atris task events --everywhere print the full ledger across workspaces
|
|
287
|
-
atris task export [--all] [--everywhere] [--out <file>]
|
|
288
|
-
write web/desktop JSON projection
|
|
289
|
-
atris task render [--all] [--everywhere] [--out <file>]
|
|
290
|
-
regenerate compact TODO.md view from state
|
|
291
|
-
atris task where Print db path + workspace scope
|
|
292
|
-
atris task help This help
|
|
293
|
-
|
|
294
|
-
Confidence Gate:
|
|
295
|
-
Before plan/do/review advances, find loopholes, patch them with proof,
|
|
296
|
-
verifier, owner, rollback, or name the residual risk.
|
|
297
|
-
|
|
298
|
-
Env:
|
|
299
|
-
ATRIS_TASKS_DB Override db path (default ~/.atris/tasks.db)
|
|
300
|
-
ATRIS_AGENT_ID Owner id for claim/done (default: $USER)
|
|
301
|
-
|
|
302
|
-
Refs:
|
|
303
|
-
Human views use semantic refs like OBL-18. Commands accept OBL-18,
|
|
304
|
-
OBL18, full 26-char task IDs, and any unique legacy prefix. JSON/API
|
|
305
|
-
keep the full id as canonical and also expose display_id + legacy_ref.
|
|
306
|
-
|
|
307
|
-
Headless:
|
|
308
|
-
Add --json to task commands for machine-readable output and stable automation.
|
|
309
|
-
`.trim();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function taskUsageLines() {
|
|
313
|
-
return taskUsageText().split('\n');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function help() {
|
|
317
|
-
console.log(taskUsageText());
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function flag(args, name) {
|
|
321
|
-
const i = args.indexOf(name);
|
|
322
|
-
if (i === -1) return null;
|
|
323
|
-
return args[i + 1] || true;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function taskScopeEverywhere(args = [], options = {}) {
|
|
327
|
-
if (options.everywhere !== undefined) return Boolean(options.everywhere);
|
|
328
|
-
return hasFlag(args, '--everywhere');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function scopedWorkspaceRoot(taskDb, args = [], options = {}) {
|
|
332
|
-
return taskScopeEverywhere(args, options) ? null : taskDb.workspaceRoot();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function hasEmptyFlagValue(args, name) {
|
|
336
|
-
const i = args.indexOf(name);
|
|
337
|
-
return i !== -1 && args[i + 1] === '';
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function wantsJson(args) {
|
|
341
|
-
return hasFlag(args, '--json');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function parseAcceptReward(value, { defaultValue = 1 } = {}) {
|
|
345
|
-
if (value === undefined || value === null || value === true) return { ok: true, value: defaultValue };
|
|
346
|
-
const numeric = Number(value);
|
|
347
|
-
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
348
|
-
return { ok: false, reason: 'invalid_reward' };
|
|
349
|
-
}
|
|
350
|
-
return { ok: true, value: numeric };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function validHumanActorFlag(value) {
|
|
354
|
-
if (typeof value !== 'string') return false;
|
|
355
|
-
const actor = value.trim();
|
|
356
|
-
return Boolean(actor) && !actor.startsWith('--') && actor !== 'auto-accept-certified';
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const AGENT_ENV_MARKERS = [
|
|
360
|
-
'CLAUDECODE',
|
|
361
|
-
'CLAUDE_CODE_ENTRYPOINT',
|
|
362
|
-
'CODEX_SANDBOX',
|
|
363
|
-
'CURSOR_AGENT',
|
|
364
|
-
'DEVIN_SESSION_ID',
|
|
365
|
-
];
|
|
366
|
-
|
|
367
|
-
function agentProofOnlyMode() {
|
|
368
|
-
const explicit = process.env.ATRIS_AGENT_PROOF_ONLY;
|
|
369
|
-
if (explicit === '1') return true;
|
|
370
|
-
if (explicit === '0') return false;
|
|
371
|
-
return AGENT_ENV_MARKERS.some((marker) => String(process.env[marker] || '').trim() !== '');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Everything an engine process carries: what the engine tools export in the
|
|
375
|
-
// shells they run, and what a mission flight sets on the drivers and engines it
|
|
376
|
-
// starts. ATRIS_AGENT_ID is left out on purpose: it is the owner's own id, which
|
|
377
|
-
// a person may export. The fleet's own dispatch adds no marker of its own, so
|
|
378
|
-
// it relies on the engine tool's.
|
|
379
|
-
const WORKER_ENV_MARKERS = [
|
|
380
|
-
...AGENT_ENV_MARKERS,
|
|
381
|
-
'CODEX_THREAD_ID',
|
|
382
|
-
'CODEX_SESSION_ID',
|
|
383
|
-
'ATRIS_MISSION_DRIVER_DETACHED',
|
|
384
|
-
'ATRIS_MISSION_DRIVER_PARENT_PID',
|
|
385
|
-
'ATRIS_DISPATCH_DEPTH',
|
|
386
|
-
];
|
|
387
|
-
|
|
388
|
-
// The first worker marker set in this shell, or ''. Unlike agentProofOnlyMode
|
|
389
|
-
// it cannot be switched off with ATRIS_AGENT_PROOF_ONLY=0.
|
|
390
|
-
function workerEnvMarker(env = process.env) {
|
|
391
|
-
const found = WORKER_ENV_MARKERS.find((marker) => String(env[marker] || '').trim() !== '');
|
|
392
|
-
if (found) return found;
|
|
393
|
-
return String(env.ATRIS_AGENT_PROOF_ONLY || '').trim() === '1' ? 'ATRIS_AGENT_PROOF_ONLY' : '';
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function failAgentProofOnly(label, detail) {
|
|
397
|
-
failTask(
|
|
398
|
-
label,
|
|
399
|
-
'agent_proof_only_human_accept_blocked',
|
|
400
|
-
detail || 'Agent proof-only mode can write notes, ready proof, and zero-reward reviews, but cannot mark tasks done or accept XP.',
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Write straight to stdout and wait until it is all out, so a command that exits right after
|
|
405
|
-
// printing (process.exit drops whatever a pipe still holds) never loses the end of its output.
|
|
406
|
-
function writeStdoutSync(text) {
|
|
407
|
-
const buffer = Buffer.from(text, 'utf8');
|
|
408
|
-
const retryWait = new Int32Array(new SharedArrayBuffer(4));
|
|
409
|
-
let offset = 0;
|
|
410
|
-
while (offset < buffer.length) {
|
|
411
|
-
try {
|
|
412
|
-
offset += fs.writeSync(process.stdout.fd, buffer, offset, buffer.length - offset);
|
|
413
|
-
} catch (err) {
|
|
414
|
-
if (err && err.code === 'EAGAIN') {
|
|
415
|
-
Atomics.wait(retryWait, 0, 0, 10);
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
throw err;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function printJson(value) {
|
|
424
|
-
writeStdoutSync(`${JSON.stringify(value, null, 2)}\n`);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function refreshCareerXpProjection(workspaceRoot) {
|
|
428
|
-
if (!workspaceRoot) return null;
|
|
429
|
-
try {
|
|
430
|
-
const { collectLocalXpProjection } = require('../commands/xp');
|
|
431
|
-
const { careerXpRoot } = require('../lib/career-xp-root');
|
|
432
|
-
return collectLocalXpProjection(['--workspace', careerXpRoot(workspaceRoot)]);
|
|
433
|
-
} catch (error) {
|
|
434
|
-
return {
|
|
435
|
-
ok: false,
|
|
436
|
-
error: error && error.message ? error.message : String(error),
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function refreshCareerXpAfterReview(reviewed) {
|
|
442
|
-
return refreshCareerXpProjection(reviewed?.episode?.workspace_root);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function compactCareerXpProjection(projection) {
|
|
446
|
-
if (!projection || typeof projection !== 'object') return projection;
|
|
447
|
-
const progress = projection.next_level_progress && typeof projection.next_level_progress === 'object'
|
|
448
|
-
? {
|
|
449
|
-
level: projection.next_level_progress.level ?? null,
|
|
450
|
-
next_level: projection.next_level_progress.next_level ?? null,
|
|
451
|
-
current_xp: projection.next_level_progress.current_xp ?? null,
|
|
452
|
-
required_xp: projection.next_level_progress.required_xp ?? null,
|
|
453
|
-
remaining_xp: projection.next_level_progress.remaining_xp ?? null,
|
|
454
|
-
percent: projection.next_level_progress.percent ?? null,
|
|
455
|
-
}
|
|
456
|
-
: null;
|
|
457
|
-
const latest = projection.latest_accepted_proof && typeof projection.latest_accepted_proof === 'object'
|
|
458
|
-
? {
|
|
459
|
-
label: projection.latest_accepted_proof.label ?? null,
|
|
460
|
-
receipt_id: projection.latest_accepted_proof.receipt_id ?? null,
|
|
461
|
-
source: projection.latest_accepted_proof.source ?? null,
|
|
462
|
-
source_task_id: projection.latest_accepted_proof.source_task_id ?? null,
|
|
463
|
-
title: projection.latest_accepted_proof.title ?? null,
|
|
464
|
-
xp: projection.latest_accepted_proof.xp ?? null,
|
|
465
|
-
reward: projection.latest_accepted_proof.reward ?? null,
|
|
466
|
-
accepted_at: projection.latest_accepted_proof.accepted_at ?? null,
|
|
467
|
-
goal: projection.latest_accepted_proof.goal ?? null,
|
|
468
|
-
}
|
|
469
|
-
: null;
|
|
470
|
-
const integrity = projection.integrity && typeof projection.integrity === 'object'
|
|
471
|
-
? {
|
|
472
|
-
status: projection.integrity.status ?? null,
|
|
473
|
-
receipts_count: projection.integrity.receipts_count ?? projection.receipts_count ?? null,
|
|
474
|
-
head_hash: projection.integrity.head_hash ?? null,
|
|
475
|
-
}
|
|
476
|
-
: null;
|
|
477
|
-
return {
|
|
478
|
-
schema: projection.schema,
|
|
479
|
-
compact: true,
|
|
480
|
-
total_xp: projection.total_xp ?? projection.total_agent_xp ?? projection.agent_xp ?? null,
|
|
481
|
-
agent_xp: projection.agent_xp ?? projection.total_agent_xp ?? null,
|
|
482
|
-
today_xp: projection.today_xp ?? projection.today_agent_xp ?? null,
|
|
483
|
-
collected_receipts: projection.collected_receipts ?? 0,
|
|
484
|
-
receipts_count: projection.receipts_count ?? null,
|
|
485
|
-
level: projection.level ?? null,
|
|
486
|
-
leaderboard_eligible: projection.leaderboard_eligible ?? null,
|
|
487
|
-
integrity_status: projection.integrity_status ?? integrity?.status ?? null,
|
|
488
|
-
next_level_progress: progress,
|
|
489
|
-
latest_accepted_proof: latest,
|
|
490
|
-
integrity,
|
|
491
|
-
omitted: [
|
|
492
|
-
'contribution_graph',
|
|
493
|
-
'career',
|
|
494
|
-
'earning_model',
|
|
495
|
-
'local_activity',
|
|
496
|
-
'ledger',
|
|
497
|
-
'sources',
|
|
498
|
-
],
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function jsonModeActive() {
|
|
503
|
-
return process.argv.includes('--json');
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function failTask(label, reason, detail, exitCode = 2) {
|
|
507
|
-
if (jsonModeActive()) {
|
|
508
|
-
printCliJson(
|
|
509
|
-
{
|
|
510
|
-
ok: false,
|
|
511
|
-
command: label,
|
|
512
|
-
reason,
|
|
513
|
-
detail: detail || null,
|
|
514
|
-
selected_ref: null,
|
|
515
|
-
next_command: null,
|
|
516
|
-
},
|
|
517
|
-
compactErrorPayload({ reason, detail: detail || null }),
|
|
518
|
-
process.argv.slice(2),
|
|
519
|
-
);
|
|
520
|
-
} else {
|
|
521
|
-
console.error(detail || `${label}: ${reason}`);
|
|
522
|
-
}
|
|
523
|
-
process.exit(exitCode);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function refuseCandidatePolicyGate(label, gate) {
|
|
527
|
-
if (jsonModeActive()) {
|
|
528
|
-
printJson({ ok: false, command: label, ...gate });
|
|
529
|
-
} else {
|
|
530
|
-
console.error(`${label}: ${gate.reason}: ${gate.message}`);
|
|
531
|
-
if (Array.isArray(gate.offenders)) {
|
|
532
|
-
gate.offenders.forEach((offender) => console.error(` ${offender}`));
|
|
533
|
-
}
|
|
534
|
-
if (Array.isArray(gate.lesson_ids) && gate.lesson_ids.length) {
|
|
535
|
-
console.error(` lessons: ${gate.lesson_ids.join(', ')}`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
process.exit(1);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function proofFlagValue(args) {
|
|
542
|
-
const proof = flag(args, '--proof');
|
|
543
|
-
return typeof proof === 'string' ? proof.trim() : '';
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function resultSentenceIssue(value, { allowCommandMention = false } = {}) {
|
|
547
|
-
const check = explainResult(value, { allowCommandMention });
|
|
548
|
-
return check.ok ? null : check.reason;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function readyResultDetail(reason) {
|
|
552
|
-
return reason ? `${READY_RESULT_TEACHING}\n${reason}` : READY_RESULT_TEACHING;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function requireResultSentence(label, value, { ready = false, allowCommandMention = false } = {}) {
|
|
556
|
-
const issue = resultSentenceIssue(value, { allowCommandMention });
|
|
557
|
-
if (!issue) return String(value || '').replace(/\s+/g, ' ').trim();
|
|
558
|
-
const detail = ready ? readyResultDetail(issue) : issue;
|
|
559
|
-
failTask(label, 'weak_result', detail);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function weakProofDetail(issue) {
|
|
563
|
-
return `meaningful proof required: ${issue}\n${LOCAL_SUCCESS_PROOF_EXAMPLE}`;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function requireMeaningfulTaskProof(label, proof, { required = true } = {}) {
|
|
567
|
-
const issue = meaningfulTaskProofIssue(proof, { required });
|
|
568
|
-
if (issue) failTask(label, 'weak_proof', weakProofDetail(issue));
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function requireRanNamedProofCommand(label, proof, ranCommand = '') {
|
|
572
|
-
const issue = unrunNamedProofCommandIssue(proof, ranCommand);
|
|
573
|
-
if (issue) failTask(label, issue.reason, issue.detail);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function sendProofIssue(res, proof, issue) {
|
|
577
|
-
return sendJson(res, 400, {
|
|
578
|
-
ok: false,
|
|
579
|
-
reason: String(proof || '').trim() ? 'weak_proof' : 'proof_required',
|
|
580
|
-
detail: weakProofDetail(issue),
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function textFlag(args, names) {
|
|
585
|
-
for (const name of names) {
|
|
586
|
-
const value = flag(args, name);
|
|
587
|
-
if (typeof value === 'string') return value.trim();
|
|
588
|
-
}
|
|
589
|
-
return '';
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Callers may write the plain explanation themselves. Anything they leave out
|
|
593
|
-
// gets an honest derived default at creation time.
|
|
594
|
-
function explanationFlags(args) {
|
|
595
|
-
return explanationFieldsFromInput({
|
|
596
|
-
what_changes: textFlag(args, ['--what-changes']),
|
|
597
|
-
why_it_matters: textFlag(args, ['--why-it-matters']),
|
|
598
|
-
done_looks_like: textFlag(args, ['--done-looks-like']),
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function landingFlags(args) {
|
|
603
|
-
return {
|
|
604
|
-
happened: textFlag(args, ['--landing', '--happened', '--what-happened']),
|
|
605
|
-
checked: textFlag(args, ['--checked', '--how-checked']),
|
|
606
|
-
tested: textFlag(args, ['--tested', '--what-tested']),
|
|
607
|
-
decision: textFlag(args, ['--decision', '--acceptance']),
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// The pull request a flight opened or merged, so the job record names it.
|
|
612
|
-
// Only a merge commit makes it count as landed. A number or link alone is an
|
|
613
|
-
// open pull request still waiting for a person to land it.
|
|
614
|
-
function readyMergeFlags(args) {
|
|
615
|
-
const commit = textFlag(args, ['--merge-commit']);
|
|
616
|
-
const number = textFlag(args, ['--pr-number']);
|
|
617
|
-
const url = textFlag(args, ['--pr-url']);
|
|
618
|
-
if (!commit && !number && !url) return null;
|
|
619
|
-
return {
|
|
620
|
-
merge_commit: commit,
|
|
621
|
-
number,
|
|
622
|
-
url,
|
|
623
|
-
merged_at: textFlag(args, ['--merged-at']),
|
|
624
|
-
self_landed: hasFlag(args, '--self-landed'),
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function normalizedLandingSentence(value) {
|
|
629
|
-
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function defaultLandingSentenceForTitle(title) {
|
|
633
|
-
return `Completed: ${String(title || '').trim()}.`;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function landingNeedsDayOnePm(sentence, title) {
|
|
637
|
-
const text = normalizedLandingSentence(sentence);
|
|
638
|
-
if (!text) return true;
|
|
639
|
-
if (text.toLowerCase() === normalizedLandingSentence(defaultLandingSentenceForTitle(title)).toLowerCase()) return true;
|
|
640
|
-
return hasAgentJargon(text) || /\bas\s+exists?\b/i.test(text) || !operatorReady(text);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function warnIfLandingNeedsDayOnePm(landing, title) {
|
|
644
|
-
const sentence = landing && typeof landing === 'object' ? landing.happened : '';
|
|
645
|
-
if (!landingNeedsDayOnePm(sentence, title)) return null;
|
|
646
|
-
const warning = 'Advisory: add --landing with one plain sentence saying what someone can do now, in words a new teammate would get. No flags, no ids.';
|
|
647
|
-
console.error(warning);
|
|
648
|
-
return warning;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function requireExplicitLandingDayOnePm(label, landing, title) {
|
|
652
|
-
const sentence = landing && typeof landing === 'object' ? normalizedLandingSentence(landing.happened) : '';
|
|
653
|
-
if (!sentence || !landingNeedsDayOnePm(sentence, title)) return;
|
|
654
|
-
failTask(
|
|
655
|
-
label,
|
|
656
|
-
'weak_landing',
|
|
657
|
-
'landing needs one plain sentence saying what someone can do now and why it matters, in words a new teammate would understand. no flags, ids, or unnamed filters.',
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function numericFlag(args, name) {
|
|
662
|
-
const value = flag(args, name);
|
|
663
|
-
if (value === null || value === true || value === undefined) return null;
|
|
664
|
-
const parsed = Number(value);
|
|
665
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function meaningfulTaskProofIssue(proof, { required = true } = {}) {
|
|
669
|
-
const text = String(proof || '').trim();
|
|
670
|
-
if (!required && !text) return null;
|
|
671
|
-
const state = taskProofState(text);
|
|
672
|
-
return state.ok ? null : state.reason;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function goldenPathMissionXpTask(task) {
|
|
676
|
-
if (!task) return false;
|
|
677
|
-
const metadata = task.metadata || {};
|
|
678
|
-
const text = [
|
|
679
|
-
task.title,
|
|
680
|
-
task.tag,
|
|
681
|
-
metadata.goal_id,
|
|
682
|
-
metadata.goalId,
|
|
683
|
-
metadata.mission_id,
|
|
684
|
-
metadata.stop_condition,
|
|
685
|
-
metadata.goal_objective,
|
|
686
|
-
metadata.objective,
|
|
687
|
-
].filter(Boolean).join(' ');
|
|
688
|
-
const missionXp = String(task.title || '').trim().toLowerCase().startsWith('mission xp:')
|
|
689
|
-
|| String(task.tag || '').toLowerCase() === 'agent-xp';
|
|
690
|
-
return missionXp
|
|
691
|
-
&& /\b(?:golden[- ]path|zero[- ]knowledge|zero[- ]papercuts?|fresh[- ](?:laptop|environment|install|home)|self[- ]landed)\b/i.test(text);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// A receipt that cannot be read drops out of review evidence; name it under
|
|
695
|
-
// ATRIS_DEBUG so the gap is findable, matching lib/todo.js.
|
|
696
|
-
function debugUnreadableReceipt(rel, err) {
|
|
697
|
-
if (process.env.ATRIS_DEBUG) console.error(`[task] receipt unreadable, skipped: ${rel}: ${err && err.message}`);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function receiptTextForProof(proof, root = process.cwd()) {
|
|
701
|
-
const chunks = [];
|
|
702
|
-
const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
|
|
703
|
-
let match;
|
|
704
|
-
while ((match = pattern.exec(String(proof || ''))) && chunks.length < 3) {
|
|
705
|
-
const rel = match[1];
|
|
706
|
-
if (!rel || rel.includes('*')) continue;
|
|
707
|
-
try {
|
|
708
|
-
const raw = fs.readFileSync(path.resolve(root, rel), 'utf8');
|
|
709
|
-
const parsed = JSON.parse(raw);
|
|
710
|
-
chunks.push(JSON.stringify({
|
|
711
|
-
schema: parsed.schema || null,
|
|
712
|
-
mission_id: parsed.mission_id || null,
|
|
713
|
-
result: parsed.result || null,
|
|
714
|
-
landing: parsed.landing || null,
|
|
715
|
-
last_landing: parsed.last_landing || null,
|
|
716
|
-
summary: parsed.summary || null,
|
|
717
|
-
}).slice(0, 12000));
|
|
718
|
-
} catch (err) {
|
|
719
|
-
debugUnreadableReceipt(rel, err);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
return chunks.join(' ');
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Mission ticks already compose a real landing sentence into the run receipt.
|
|
726
|
-
// When a mission-bridged task lands with only a receipt proof, lift that
|
|
727
|
-
// sentence so the review queue shows the work instead of echoing the title.
|
|
728
|
-
function missionReceiptResultForProof(task, proof, root = process.cwd()) {
|
|
729
|
-
const metadata = task?.metadata || {};
|
|
730
|
-
if (!metadata.mission_id && !metadata.goal_id) return null;
|
|
731
|
-
const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
|
|
732
|
-
let match;
|
|
733
|
-
while ((match = pattern.exec(String(proof || '')))) {
|
|
734
|
-
const rel = match[1];
|
|
735
|
-
if (!rel || rel.includes('*')) continue;
|
|
736
|
-
let landing = null;
|
|
737
|
-
try {
|
|
738
|
-
const parsed = JSON.parse(fs.readFileSync(path.resolve(root, rel), 'utf8'));
|
|
739
|
-
landing = parsed?.result?.landing || parsed?.landing || parsed?.last_landing || null;
|
|
740
|
-
} catch (err) {
|
|
741
|
-
debugUnreadableReceipt(rel, err);
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
const changed = String(landing?.changed || landing?.happened || '').replace(/\s+/g, ' ').trim();
|
|
745
|
-
if (!changed) continue;
|
|
746
|
-
if (/\brecorded tick \d+\.?$/i.test(changed) || /^recorded a proof heartbeat\b/i.test(changed)) continue;
|
|
747
|
-
const reason = String(landing?.reason || landing?.why || '').replace(/\s+/g, ' ').trim();
|
|
748
|
-
return { changed, reason: reason && !isRetiredFillerReason(reason) ? reason : null };
|
|
749
|
-
}
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function missionXpEndToEndProofIssue(task, proof, root = process.cwd()) {
|
|
754
|
-
if (!goldenPathMissionXpTask(task)) return null;
|
|
755
|
-
const corpus = `${String(proof || '')} ${receiptTextForProof(proof, root)}`
|
|
756
|
-
.replace(/\s+/g, ' ')
|
|
757
|
-
.trim();
|
|
758
|
-
const hasZeroPapercut = /\b(?:zero|0|no)\s+(?:new\s+)?papercuts?\b|\bzero[- ]papercut\b/i.test(corpus);
|
|
759
|
-
const hasEndToEnd = /\bend[- ]to[- ]end\b|\bfull\s+(?:fresh[- ](?:laptop|environment)\s+)?pass\b|\bfresh[- ](?:laptop|environment|install)\b|\bclean\s+temp\s+home\b|\bnpm\s+pack\b/i.test(corpus);
|
|
760
|
-
const hasSelfLanded = /\bself[- ]landed\b|\bfirst\s+self[- ]landed\s+task\b|\btask\s+reaches\s+done\b|\binstall\b.{0,120}\binit\b.{0,120}\bmission\b.{0,120}\b(?:self[- ]landed|task)\b/i.test(corpus);
|
|
761
|
-
return hasZeroPapercut && hasEndToEnd && hasSelfLanded ? null : MISSION_XP_END_TO_END_DETAIL;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function missionXpProofBoundaryEvaluation(task, proofOverride = null) {
|
|
765
|
-
if (!goldenPathMissionXpTask(task)) return null;
|
|
766
|
-
const metadata = task.metadata || {};
|
|
767
|
-
const review = task.review || {};
|
|
768
|
-
const proof = proofOverride === null
|
|
769
|
-
? String(review.proof || metadata.latest_agent_proof || '').trim()
|
|
770
|
-
: String(proofOverride || '').trim();
|
|
771
|
-
const issue = missionXpEndToEndProofIssue(task, proof, task.workspace_root || process.cwd());
|
|
772
|
-
if (!issue) return null;
|
|
773
|
-
return {
|
|
774
|
-
eligible: false,
|
|
775
|
-
ref: taskRef(task),
|
|
776
|
-
reason: MISSION_XP_END_TO_END_REASON,
|
|
777
|
-
next_action: 'attach the zero-papercut end-to-end fresh-laptop receipt, then resubmit Mission XP proof',
|
|
778
|
-
proof,
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function positional(args) {
|
|
783
|
-
return args.filter((a, i) => {
|
|
784
|
-
if (a.startsWith('--')) return false;
|
|
785
|
-
if (i > 0 && args[i - 1].startsWith('--')) return false;
|
|
786
|
-
return true;
|
|
787
|
-
});
|
|
788
|
-
}
|
|
86
|
+
const {
|
|
87
|
+
DEFAULT_OWNER,
|
|
88
|
+
AGENT_CERTIFICATION_REVIEW_PASSES,
|
|
89
|
+
RESULT_SAVED_TEXT_LIMIT,
|
|
90
|
+
REVIEW_LANE_LOOP_DEFAULT_MAX_STEPS,
|
|
91
|
+
REVIEW_LANE_LOOP_MAX_STEPS,
|
|
92
|
+
REVIEW_LANE_RUN_DEFAULT_MAX_RUNS,
|
|
93
|
+
REVIEW_LANE_RUN_MAX_RUNS,
|
|
94
|
+
PENDING_REVIEW_CHAT_STOP_REASON,
|
|
95
|
+
PROOF_BOUNDARY_BLOCKED_ACTION,
|
|
96
|
+
PROOF_BOUNDARY_BLOCKED_REASON,
|
|
97
|
+
MISSION_XP_END_TO_END_REASON,
|
|
98
|
+
MISSION_XP_END_TO_END_DETAIL,
|
|
99
|
+
READY_RESULT_TEACHING,
|
|
100
|
+
STATUS_PLAN_TAGS,
|
|
101
|
+
TASK_QUEUE_COLUMN_ORDER,
|
|
102
|
+
TASK_QUEUE_COLUMN_LABELS,
|
|
103
|
+
TASK_REVIEW_STATE_LANES,
|
|
104
|
+
TASK_REVIEW_STATE_ALIASES,
|
|
105
|
+
AGENT_ENV_MARKERS,
|
|
106
|
+
WORKER_ENV_MARKERS,
|
|
107
|
+
TASK_BOARD_COLUMNS,
|
|
108
|
+
getTaskDb,
|
|
109
|
+
warnIfTaskTitleNeedsOperatorWhy,
|
|
110
|
+
taskUsageText,
|
|
111
|
+
taskUsageLines,
|
|
112
|
+
help,
|
|
113
|
+
flag,
|
|
114
|
+
taskScopeEverywhere,
|
|
115
|
+
scopedWorkspaceRoot,
|
|
116
|
+
hasEmptyFlagValue,
|
|
117
|
+
wantsJson,
|
|
118
|
+
parseAcceptReward,
|
|
119
|
+
validHumanActorFlag,
|
|
120
|
+
agentProofOnlyMode,
|
|
121
|
+
workerEnvMarker,
|
|
122
|
+
failAgentProofOnly,
|
|
123
|
+
writeStdoutSync,
|
|
124
|
+
printJson,
|
|
125
|
+
refreshCareerXpProjection,
|
|
126
|
+
refreshCareerXpAfterReview,
|
|
127
|
+
compactCareerXpProjection,
|
|
128
|
+
jsonModeActive,
|
|
129
|
+
failTask,
|
|
130
|
+
refuseCandidatePolicyGate,
|
|
131
|
+
proofFlagValue,
|
|
132
|
+
resultSentenceIssue,
|
|
133
|
+
readyResultDetail,
|
|
134
|
+
requireResultSentence,
|
|
135
|
+
weakProofDetail,
|
|
136
|
+
requireMeaningfulTaskProof,
|
|
137
|
+
requireRanNamedProofCommand,
|
|
138
|
+
sendProofIssue,
|
|
139
|
+
textFlag,
|
|
140
|
+
explanationFlags,
|
|
141
|
+
landingFlags,
|
|
142
|
+
readyMergeFlags,
|
|
143
|
+
normalizedLandingSentence,
|
|
144
|
+
defaultLandingSentenceForTitle,
|
|
145
|
+
landingNeedsDayOnePm,
|
|
146
|
+
warnIfLandingNeedsDayOnePm,
|
|
147
|
+
requireExplicitLandingDayOnePm,
|
|
148
|
+
numericFlag,
|
|
149
|
+
meaningfulTaskProofIssue,
|
|
150
|
+
goldenPathMissionXpTask,
|
|
151
|
+
debugUnreadableReceipt,
|
|
152
|
+
receiptTextForProof,
|
|
153
|
+
missionReceiptResultForProof,
|
|
154
|
+
missionXpEndToEndProofIssue,
|
|
155
|
+
missionXpProofBoundaryEvaluation,
|
|
156
|
+
positional,
|
|
157
|
+
taskFromProjection,
|
|
158
|
+
taskRef,
|
|
159
|
+
normalizeTaskLookupRef,
|
|
160
|
+
taskLookupRefs,
|
|
161
|
+
resolveProjectionTaskRef,
|
|
162
|
+
normalizedStatusPart,
|
|
163
|
+
taskIsPlannedOpen,
|
|
164
|
+
taskColumn,
|
|
165
|
+
taskHasReview,
|
|
166
|
+
taskBoardViewModel,
|
|
167
|
+
taskBoardTemplate,
|
|
168
|
+
taskBoardHtml,
|
|
169
|
+
readJsonBody,
|
|
170
|
+
sendJson,
|
|
171
|
+
sendHtml,
|
|
172
|
+
} = require('../lib/task-core');
|
|
789
173
|
|
|
790
174
|
// options.root is for a caller that is not the command line, the dispatch flight: it
|
|
791
175
|
// keeps the list for its own workspace instead of for the directory this process is in.
|
|
@@ -811,32 +195,6 @@ function writeDefaultProjection(taskDb, db, options = {}) {
|
|
|
811
195
|
return { projection, outPath };
|
|
812
196
|
}
|
|
813
197
|
|
|
814
|
-
function taskFromProjection(projection, id) {
|
|
815
|
-
return projection.tasks.find(t => t.id === id) || null;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function taskRef(taskOrId) {
|
|
819
|
-
if (!taskOrId) return 'TASK';
|
|
820
|
-
if (typeof taskOrId === 'string') return taskOrId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 8);
|
|
821
|
-
return taskOrId.display_id || taskOrId.legacy_ref || taskRef(taskOrId.id);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
function normalizeTaskLookupRef(value) {
|
|
825
|
-
return String(value || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function taskLookupRefs(task) {
|
|
829
|
-
if (!task) return [];
|
|
830
|
-
return [task.id, task.display_id, task.legacy_ref, taskRef(task)]
|
|
831
|
-
.map(normalizeTaskLookupRef)
|
|
832
|
-
.filter(Boolean);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function resolveProjectionTaskRef(ref, taskByRef) {
|
|
836
|
-
const key = normalizeTaskLookupRef(ref);
|
|
837
|
-
return key ? taskByRef.get(key) || null : null;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
198
|
function reviewNextTaskTitle(task) {
|
|
841
199
|
return normalizeReviewNextTaskInput(rawReviewNextTaskTitle(task)).nextTask;
|
|
842
200
|
}
|
|
@@ -2985,12 +2343,21 @@ function taskReviewStateMatches(task, reviewState, { hasExistingReviewFollowUp =
|
|
|
2985
2343
|
return false;
|
|
2986
2344
|
}
|
|
2987
2345
|
|
|
2346
|
+
function taskMatchesScopeTag(task, wantedTag) {
|
|
2347
|
+
if (!wantedTag) return true;
|
|
2348
|
+
if (taskScopeEquals(task && task.tag, wantedTag)) return true;
|
|
2349
|
+
const tokens = taskTagTokens(task);
|
|
2350
|
+
if (tokens.some((token) => taskScopeEquals(token, wantedTag))) return true;
|
|
2351
|
+
if (isDecisionHoldTag(wantedTag) && isDecisionTask(task)) return true;
|
|
2352
|
+
return false;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2988
2355
|
function taskMatchesQueueScope(task, scope = {}, options = {}) {
|
|
2989
2356
|
const normalized = normalizeTaskQueueScope(scope);
|
|
2990
2357
|
if (normalized.goal_id && !taskGoalScopeValues(task).some(value => taskScopeEquals(value, normalized.goal_id))) {
|
|
2991
2358
|
return false;
|
|
2992
2359
|
}
|
|
2993
|
-
if (normalized.tag && !
|
|
2360
|
+
if (normalized.tag && !taskMatchesScopeTag(task, normalized.tag)) {
|
|
2994
2361
|
return false;
|
|
2995
2362
|
}
|
|
2996
2363
|
if (normalized.status) {
|
|
@@ -5594,19 +4961,6 @@ function formatTaskEventCompact(event, refById = new Map()) {
|
|
|
5594
4961
|
return `${when}\t${event.event_type.padEnd(9)}\t${refById.get(event.task_id) || taskRef(event.task_id)}${actor}\t${taskEventSummary(event)}`;
|
|
5595
4962
|
}
|
|
5596
4963
|
|
|
5597
|
-
function normalizedStatusPart(value) {
|
|
5598
|
-
return String(value || '').trim().toLowerCase().replace(/\s+/g, '-');
|
|
5599
|
-
}
|
|
5600
|
-
|
|
5601
|
-
function taskIsPlannedOpen(task) {
|
|
5602
|
-
const metadata = task && task.metadata || {};
|
|
5603
|
-
const tag = normalizedStatusPart(task && task.tag);
|
|
5604
|
-
const stage = normalizedStatusPart(metadata.stage);
|
|
5605
|
-
return STATUS_PLAN_TAGS.has(tag)
|
|
5606
|
-
|| STATUS_PLAN_TAGS.has(stage)
|
|
5607
|
-
|| Boolean(metadata.verify || metadata.goal || metadata.loop || metadata.cron || metadata.next_run_at);
|
|
5608
|
-
}
|
|
5609
|
-
|
|
5610
4964
|
function formatTaskLine(task) {
|
|
5611
4965
|
if (!task) return 'none';
|
|
5612
4966
|
const explanation = task.explanation || taskExplanation(task);
|
|
@@ -6569,20 +5923,7 @@ function cmdNextTruth(args) {
|
|
|
6569
5923
|
const task = picked.task || null;
|
|
6570
5924
|
|
|
6571
5925
|
if (!task && openDecisions.length) {
|
|
6572
|
-
|
|
6573
|
-
printJson({
|
|
6574
|
-
ok: false,
|
|
6575
|
-
action: 'refused',
|
|
6576
|
-
reason: DECISION_REFUSE_REASON,
|
|
6577
|
-
skipped_decision_count: openDecisions.length,
|
|
6578
|
-
owner: String(owner),
|
|
6579
|
-
scope: normalizeTaskQueueScope(scope),
|
|
6580
|
-
projection_path: outPath,
|
|
6581
|
-
});
|
|
6582
|
-
process.exit(1);
|
|
6583
|
-
}
|
|
6584
|
-
console.error(DECISION_REFUSE_REASON);
|
|
6585
|
-
process.exit(1);
|
|
5926
|
+
reportSkippedDecisionsAndExit(openDecisions, { args, owner, scope, outPath });
|
|
6586
5927
|
}
|
|
6587
5928
|
|
|
6588
5929
|
if (wantsJson(args)) {
|
|
@@ -6607,6 +5948,33 @@ function cmdNextTruth(args) {
|
|
|
6607
5948
|
console.log(`next: ${command}`);
|
|
6608
5949
|
}
|
|
6609
5950
|
|
|
5951
|
+
function reportSkippedDecisionsAndExit(openDecisions, { args, owner, scope, outPath }) {
|
|
5952
|
+
const summary = summarizeSkippedDecisions(openDecisions);
|
|
5953
|
+
if (wantsJson(args)) {
|
|
5954
|
+
printJson({
|
|
5955
|
+
ok: false,
|
|
5956
|
+
action: 'refused',
|
|
5957
|
+
reason: DECISION_REFUSE_REASON,
|
|
5958
|
+
command: summary.command,
|
|
5959
|
+
show_command: summary.show_command,
|
|
5960
|
+
next_command: summary.next_command,
|
|
5961
|
+
choice: summary.choice,
|
|
5962
|
+
skipped_decision_count: summary.skipped_decision_count,
|
|
5963
|
+
skipped_decision_refs: summary.skipped_decision_refs,
|
|
5964
|
+
skipped_decisions: summary.skipped_decisions,
|
|
5965
|
+
decisions: summary.decisions,
|
|
5966
|
+
owner: String(owner),
|
|
5967
|
+
scope: normalizeTaskQueueScope(scope),
|
|
5968
|
+
projection_path: outPath,
|
|
5969
|
+
});
|
|
5970
|
+
process.exit(1);
|
|
5971
|
+
}
|
|
5972
|
+
const text = formatDecisionRefusalLines(openDecisions).join('\n');
|
|
5973
|
+
console.log(text);
|
|
5974
|
+
console.error(text);
|
|
5975
|
+
process.exit(1);
|
|
5976
|
+
}
|
|
5977
|
+
|
|
6610
5978
|
function isUninitializedTaskFolder(cwd = process.cwd()) {
|
|
6611
5979
|
const workspace = getTaskDb().workspaceRoot(cwd);
|
|
6612
5980
|
if (fs.existsSync(path.join(workspace, 'atris'))) return false;
|
|
@@ -6679,20 +6047,14 @@ function cmdNext(args) {
|
|
|
6679
6047
|
if (!open.length) {
|
|
6680
6048
|
const { projection, outPath } = reviewProjection;
|
|
6681
6049
|
if (skippedDecisions > 0 && !reviewTasks.length) {
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
projection_path: outPath,
|
|
6691
|
-
});
|
|
6692
|
-
process.exit(1);
|
|
6693
|
-
}
|
|
6694
|
-
console.error(DECISION_REFUSE_REASON);
|
|
6695
|
-
process.exit(1);
|
|
6050
|
+
const openDecisionTasks = filterTasksByScope(projection.tasks || [], scope)
|
|
6051
|
+
.filter((task) => task && isDecisionTask(task) && task.status === 'open');
|
|
6052
|
+
reportSkippedDecisionsAndExit(
|
|
6053
|
+
openDecisionTasks.length
|
|
6054
|
+
? openDecisionTasks
|
|
6055
|
+
: taskDb.withTaskDisplayRefs(openAll.filter((task) => isDecisionTask(task))),
|
|
6056
|
+
{ args, owner, scope, outPath }
|
|
6057
|
+
);
|
|
6696
6058
|
}
|
|
6697
6059
|
const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again')
|
|
6698
6060
|
|| reviewTasks.find(task => task.review.handoff.next_action === 'continue_work')
|
|
@@ -11967,590 +11329,6 @@ function cmdSync(args) {
|
|
|
11967
11329
|
}
|
|
11968
11330
|
}
|
|
11969
11331
|
|
|
11970
|
-
function taskColumn(task) {
|
|
11971
|
-
if (task.status === 'open') return taskIsPlannedOpen(task) ? 'open' : 'backlog';
|
|
11972
|
-
if (task.status === 'claimed') return 'doing';
|
|
11973
|
-
if (task.status === 'review') return 'review';
|
|
11974
|
-
if (task.status === 'failed' && taskHasReview(task)) return 'done';
|
|
11975
|
-
if (task.status === 'failed') return 'blocked';
|
|
11976
|
-
if (task.status === 'done' && !taskHasReview(task)) return 'review';
|
|
11977
|
-
return 'done';
|
|
11978
|
-
}
|
|
11979
|
-
|
|
11980
|
-
function taskHasReview(task) {
|
|
11981
|
-
if (task.latest_event_type === 'reviewed') return true;
|
|
11982
|
-
const review = task.review || {};
|
|
11983
|
-
return review.reward != null || Boolean(review.proof || review.lesson || review.next_task);
|
|
11984
|
-
}
|
|
11985
|
-
|
|
11986
|
-
const TASK_BOARD_COLUMNS = [
|
|
11987
|
-
['backlog', 'Backlog'],
|
|
11988
|
-
['open', 'Open'],
|
|
11989
|
-
['doing', 'Doing'],
|
|
11990
|
-
['review', 'Review'],
|
|
11991
|
-
['blocked', 'Blocked'],
|
|
11992
|
-
['done', 'Done'],
|
|
11993
|
-
];
|
|
11994
|
-
|
|
11995
|
-
// Pure state shaping: projection in, board view model out. No markup here.
|
|
11996
|
-
function taskBoardViewModel(projection = {}) {
|
|
11997
|
-
const tasks = Array.isArray(projection.tasks) ? projection.tasks : [];
|
|
11998
|
-
const rows = tasks.map((task) => ({
|
|
11999
|
-
id: task.id,
|
|
12000
|
-
task,
|
|
12001
|
-
column: taskColumn(task),
|
|
12002
|
-
decision: taskHasReview(task),
|
|
12003
|
-
}));
|
|
12004
|
-
const columns = TASK_BOARD_COLUMNS.map(([key, label]) => ({
|
|
12005
|
-
key,
|
|
12006
|
-
label,
|
|
12007
|
-
rows: rows.filter((row) => row.column === key),
|
|
12008
|
-
}));
|
|
12009
|
-
const counts = {};
|
|
12010
|
-
for (const column of columns) counts[column.key] = column.rows.length;
|
|
12011
|
-
return {
|
|
12012
|
-
columns,
|
|
12013
|
-
counts,
|
|
12014
|
-
rows,
|
|
12015
|
-
total: rows.length,
|
|
12016
|
-
planTags: Array.from(STATUS_PLAN_TAGS),
|
|
12017
|
-
};
|
|
12018
|
-
}
|
|
12019
|
-
|
|
12020
|
-
// Markup only: view model in, board page out. The page hydrates itself from
|
|
12021
|
-
// /api/tasks in the browser, so the template bakes in just the shared
|
|
12022
|
-
// plan-tag list; the column keys and labels must stay in step with
|
|
12023
|
-
// TASK_BOARD_COLUMNS above.
|
|
12024
|
-
function taskBoardTemplate(model) {
|
|
12025
|
-
return `<!doctype html>
|
|
12026
|
-
<html lang="en">
|
|
12027
|
-
<head>
|
|
12028
|
-
<meta charset="utf-8">
|
|
12029
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
12030
|
-
<title>Atris Task Factory</title>
|
|
12031
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12032
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12033
|
-
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
12034
|
-
<style>
|
|
12035
|
-
/* aesthetic: machine-room telemetry, warm-tinted dark, mono data, calm signals (no neon) */
|
|
12036
|
-
:root {
|
|
12037
|
-
color-scheme: dark;
|
|
12038
|
-
--bg: oklch(18% 0.012 160);
|
|
12039
|
-
--panel: oklch(22% 0.014 160);
|
|
12040
|
-
--panel-2: oklch(25% 0.016 160);
|
|
12041
|
-
--line: oklch(32% 0.015 160);
|
|
12042
|
-
--text: oklch(93% 0.012 150);
|
|
12043
|
-
--muted: oklch(70% 0.018 160);
|
|
12044
|
-
--accent: oklch(74% 0.115 158);
|
|
12045
|
-
--warn: oklch(81% 0.11 80);
|
|
12046
|
-
--bad: oklch(69% 0.14 25);
|
|
12047
|
-
--info: oklch(75% 0.10 240);
|
|
12048
|
-
--violet: oklch(73% 0.10 300);
|
|
12049
|
-
--sans: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
12050
|
-
--mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
12051
|
-
}
|
|
12052
|
-
* { box-sizing: border-box; }
|
|
12053
|
-
body {
|
|
12054
|
-
margin:0; color:var(--text);
|
|
12055
|
-
font-family: var(--sans);
|
|
12056
|
-
-webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
|
|
12057
|
-
background:
|
|
12058
|
-
radial-gradient(1100px 520px at 82% -12%, oklch(30% 0.04 160 / 0.55), transparent 62%),
|
|
12059
|
-
repeating-linear-gradient(0deg, oklch(32% 0.015 160 / 0.16) 0 1px, transparent 1px 34px),
|
|
12060
|
-
var(--bg);
|
|
12061
|
-
background-attachment: fixed;
|
|
12062
|
-
}
|
|
12063
|
-
header { height:60px; display:flex; align-items:center; justify-content:space-between; padding:0 22px; border-bottom:1px solid var(--line); background:linear-gradient(180deg, var(--panel-2), var(--panel)); }
|
|
12064
|
-
h1 { font-size:17px; margin:0; font-weight:700; letter-spacing:-0.01em; }
|
|
12065
|
-
.sub { color:var(--muted); font-size:12px; }
|
|
12066
|
-
main { display:grid; grid-template-columns: 320px 1fr; height:calc(100vh - 60px); }
|
|
12067
|
-
aside { border-right:1px solid var(--line); padding:16px; overflow:auto; background:var(--panel); }
|
|
12068
|
-
section { min-width:0; overflow:auto; padding:16px; }
|
|
12069
|
-
label { display:block; color:var(--muted); font-size:12px; margin:10px 0 5px; }
|
|
12070
|
-
input, textarea, select { width:100%; border:1px solid var(--line); background:oklch(15% 0.012 160); color:var(--text); border-radius:8px; padding:9px 11px; font:inherit; font-size:13px; transition:border-color .18s cubic-bezier(0.25,1,0.5,1); }
|
|
12071
|
-
input:focus, textarea:focus, select:focus { outline:2px solid var(--accent); outline-offset:1px; border-color:transparent; }
|
|
12072
|
-
textarea { min-height:82px; resize:vertical; font-family:var(--mono); font-size:12px; }
|
|
12073
|
-
button { border:1px solid var(--line); background:var(--panel-2); color:var(--text); border-radius:8px; padding:8px 12px; font:inherit; font-size:12px; cursor:pointer; transition:background .18s cubic-bezier(0.25,1,0.5,1), border-color .18s; }
|
|
12074
|
-
button:hover { border-color:var(--muted); background:oklch(29% 0.018 160); }
|
|
12075
|
-
button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
|
12076
|
-
button:active { transform:translateY(1px); }
|
|
12077
|
-
.primary { background:oklch(38% 0.07 158); border-color:oklch(48% 0.09 158); color:oklch(96% 0.02 158); }
|
|
12078
|
-
.primary:hover { background:oklch(43% 0.085 158); border-color:var(--accent); }
|
|
12079
|
-
.grid { display:grid; grid-template-columns: repeat(var(--board-columns, 6), minmax(160px, 1fr)); gap:12px; align-items:start; }
|
|
12080
|
-
.overview { display:grid; grid-template-columns: minmax(260px, 1.4fr) minmax(260px, 1fr); gap:12px; margin-bottom:12px; }
|
|
12081
|
-
.goalbox, .chainbox { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:88px; }
|
|
12082
|
-
.goalbox h2, .chainbox h2 { margin:0 0 8px; color:var(--muted); font-size:12px; font-weight:650; }
|
|
12083
|
-
.goalitem { font-size:13px; line-height:1.3; margin:5px 0; }
|
|
12084
|
-
.chainitem { display:grid; grid-template-columns:72px 1fr; gap:8px; font-size:12px; line-height:1.3; margin:5px 0; color:var(--muted); }
|
|
12085
|
-
.chainitem strong { color:var(--text); font-weight:600; }
|
|
12086
|
-
.streams { display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap:12px; margin-bottom:12px; }
|
|
12087
|
-
.stream { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:126px; }
|
|
12088
|
-
.stream h2 { margin:0 0 8px; font-size:12px; color:var(--text); line-height:1.25; font-weight:500; }
|
|
12089
|
-
.streambar { display:flex; height:7px; overflow:hidden; border-radius:999px; background:oklch(15% 0.012 160); border:1px solid var(--line); margin:8px 0; }
|
|
12090
|
-
.streambar span { display:block; min-width:2px; }
|
|
12091
|
-
.seg-open { background:var(--muted); }
|
|
12092
|
-
.seg-doing { background:var(--accent); }
|
|
12093
|
-
.seg-review { background:var(--warn); }
|
|
12094
|
-
.seg-blocked { background:var(--bad); }
|
|
12095
|
-
.streamtask { display:grid; grid-template-columns:64px 1fr; gap:8px; color:var(--muted); font-size:11px; line-height:1.25; margin-top:6px; }
|
|
12096
|
-
.streamtask strong { color:var(--text); font-weight:550; }
|
|
12097
|
-
.col { background:var(--panel); border:1px solid var(--line); border-radius:8px; min-height:160px; overflow:hidden; }
|
|
12098
|
-
.col h2 { margin:0; padding:10px 11px; font-size:12px; color:var(--muted); border-bottom:1px solid var(--line); display:flex; justify-content:space-between; }
|
|
12099
|
-
.cards { padding:8px; display:flex; flex-direction:column; gap:8px; }
|
|
12100
|
-
.card { text-align:left; width:100%; background:var(--panel-2); border:1px solid var(--line); border-radius:8px; padding:9px; transition:transform .18s cubic-bezier(0.25,1,0.5,1), border-color .18s, background .18s; }
|
|
12101
|
-
.card:hover { transform:scale(1.02); border-color:var(--muted); background:oklch(28% 0.018 160); }
|
|
12102
|
-
.card.active { border-color:var(--accent); box-shadow:0 0 0 1px oklch(74% 0.115 158 / 0.3); }
|
|
12103
|
-
.title { font-size:13px; line-height:1.25; }
|
|
12104
|
-
.meta { margin-top:6px; color:var(--muted); font-size:11px; display:flex; gap:6px; flex-wrap:wrap; font-family:var(--mono); }
|
|
12105
|
-
.pill { border:1px solid var(--line); border-radius:999px; padding:1px 6px; }
|
|
12106
|
-
.why { margin-top:7px; color:var(--muted); font-size:11px; line-height:1.25; }
|
|
12107
|
-
.plain { margin-top:6px; font-size:11px; line-height:1.35; color:var(--text); }
|
|
12108
|
-
.plain div + div { color:var(--muted); }
|
|
12109
|
-
.fact { margin:10px 0; background:oklch(15% 0.012 160); border:1px solid var(--line); border-radius:7px; padding:8px; font-size:12px; line-height:1.35; }
|
|
12110
|
-
.fact b { color:var(--muted); font-size:11px; display:block; margin-bottom:3px; }
|
|
12111
|
-
.room { margin-top:14px; border-top:1px solid var(--line); padding-top:12px; }
|
|
12112
|
-
.room h3 { margin:0 0 4px; font-size:14px; }
|
|
12113
|
-
.thread { margin:10px 0; display:flex; flex-direction:column; gap:7px; }
|
|
12114
|
-
.msg { background:oklch(15% 0.012 160); border:1px solid var(--line); border-radius:7px; padding:8px; font-size:12px; }
|
|
12115
|
-
.msg .who { color:var(--muted); font-size:11px; margin-bottom:3px; }
|
|
12116
|
-
.actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:10px; }
|
|
12117
|
-
.full { grid-column:1 / -1; }
|
|
12118
|
-
.empty { color:var(--muted); font-size:12px; padding:10px; }
|
|
12119
|
-
/* heartbeat strip */
|
|
12120
|
-
.beat { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); font-family:var(--mono); }
|
|
12121
|
-
.beat .dot { width:9px; height:9px; border-radius:50%; background:var(--muted); flex:none; }
|
|
12122
|
-
.beat.alive .dot { background:var(--accent); box-shadow:0 0 0 0 oklch(74% 0.115 158 / 0.6); animation:beat 2s cubic-bezier(0.25,1,0.5,1) infinite; }
|
|
12123
|
-
.beat.stale .dot { background:var(--bad); }
|
|
12124
|
-
.beat b { color:var(--accent); font-weight:600; }
|
|
12125
|
-
.beat .warn { color:var(--warn); }
|
|
12126
|
-
@keyframes beat { 0%{box-shadow:0 0 0 0 oklch(74% 0.115 158 / 0.5)} 70%{box-shadow:0 0 0 8px oklch(74% 0.115 158 / 0)} 100%{box-shadow:0 0 0 0 oklch(74% 0.115 158 / 0)} }
|
|
12127
|
-
@media (prefers-reduced-motion: reduce) { .beat.alive .dot { animation:none; } *, *::before, *::after { transition:none !important; } }
|
|
12128
|
-
/* activity feed */
|
|
12129
|
-
.activity { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:0; margin-bottom:12px; max-height:46vh; overflow:auto; }
|
|
12130
|
-
.activity h2 { margin:0; position:sticky; top:0; background:var(--panel); padding:11px; font-size:12px; color:var(--muted); font-weight:650; border-bottom:1px solid var(--line); z-index:1; }
|
|
12131
|
-
.ev { display:grid; grid-template-columns:52px 64px 1fr auto; gap:10px; align-items:baseline; padding:7px 11px; border-bottom:1px solid oklch(28% 0.013 160); font-size:12px; line-height:1.3; font-family:var(--mono); transition:background .15s ease; }
|
|
12132
|
-
.ev:last-child { border-bottom:0; }
|
|
12133
|
-
.ev:hover { background:oklch(25% 0.016 160); }
|
|
12134
|
-
.ev .t { color:var(--muted); font-variant-numeric:tabular-nums; font-size:11px; }
|
|
12135
|
-
.ev .src { font-size:11px; border:1px solid var(--line); border-radius:999px; padding:1px 7px; color:var(--muted); text-align:center; }
|
|
12136
|
-
.ev .src.pulse { color:var(--accent); border-color:oklch(74% 0.115 158 / 0.45); }
|
|
12137
|
-
.ev .src.reward { color:var(--warn); border-color:oklch(81% 0.11 80 / 0.45); }
|
|
12138
|
-
.ev .src.xp { color:var(--info); border-color:oklch(75% 0.10 240 / 0.45); }
|
|
12139
|
-
.ev .src.mission { color:var(--violet); border-color:oklch(73% 0.10 300 / 0.45); }
|
|
12140
|
-
.ev .msg { color:var(--text); min-width:0; }
|
|
12141
|
-
.ev .msg .d { color:var(--muted); font-size:11px; }
|
|
12142
|
-
.ev.bad .msg { color:var(--bad); }
|
|
12143
|
-
.ev .rw { font-variant-numeric:tabular-nums; font-size:11px; color:var(--muted); }
|
|
12144
|
-
.ev .rw.pos { color:var(--accent); } .ev .rw.neg { color:var(--bad); }
|
|
12145
|
-
@media (max-width: 980px) { main { grid-template-columns:1fr; height:auto; } aside { border-right:0; border-bottom:1px solid var(--line); } .grid, .overview { grid-template-columns:1fr; } }
|
|
12146
|
-
</style>
|
|
12147
|
-
</head>
|
|
12148
|
-
<body>
|
|
12149
|
-
<header>
|
|
12150
|
-
<div>
|
|
12151
|
-
<h1>Atris Task Factory</h1>
|
|
12152
|
-
<div class="beat" id="heartbeat"><span class="dot"></span><span>heartbeat: loading…</span></div>
|
|
12153
|
-
</div>
|
|
12154
|
-
<button id="refresh">Refresh</button>
|
|
12155
|
-
</header>
|
|
12156
|
-
<main>
|
|
12157
|
-
<aside>
|
|
12158
|
-
<form id="create">
|
|
12159
|
-
<label>New task</label>
|
|
12160
|
-
<textarea id="title" placeholder="Need something done..."></textarea>
|
|
12161
|
-
<label>Lane</label>
|
|
12162
|
-
<input id="tag" value="tasks">
|
|
12163
|
-
<button class="primary full" type="submit" style="margin-top:10px;width:100%">Create task</button>
|
|
12164
|
-
</form>
|
|
12165
|
-
<div class="room" id="room">
|
|
12166
|
-
<div class="empty">Select a task to open its room.</div>
|
|
12167
|
-
</div>
|
|
12168
|
-
</aside>
|
|
12169
|
-
<section>
|
|
12170
|
-
<div class="overview" id="overview"></div>
|
|
12171
|
-
<div class="activity" id="activity"><h2>Live Stream</h2><div class="empty">waiting for the agent…</div></div>
|
|
12172
|
-
<div class="streams" id="streams"></div>
|
|
12173
|
-
<div class="grid" id="board"></div>
|
|
12174
|
-
</section>
|
|
12175
|
-
</main>
|
|
12176
|
-
<script>
|
|
12177
|
-
const columns = [
|
|
12178
|
-
['backlog', 'Backlog'],
|
|
12179
|
-
['open', 'Open'],
|
|
12180
|
-
['doing', 'Doing'],
|
|
12181
|
-
['review', 'Review'],
|
|
12182
|
-
['blocked', 'Blocked'],
|
|
12183
|
-
['done', 'Done']
|
|
12184
|
-
];
|
|
12185
|
-
const planTags = new Set(${JSON.stringify(model.planTags)});
|
|
12186
|
-
let state = { tasks: [] };
|
|
12187
|
-
let selected = null;
|
|
12188
|
-
const $ = (id) => document.getElementById(id);
|
|
12189
|
-
|
|
12190
|
-
async function api(path, options = {}) {
|
|
12191
|
-
const res = await fetch(path, {
|
|
12192
|
-
...options,
|
|
12193
|
-
headers: { 'content-type': 'application/json', 'x-atris-token': new URLSearchParams(location.search).get('t') || '', ...(options.headers || {}) }
|
|
12194
|
-
});
|
|
12195
|
-
const data = await res.json();
|
|
12196
|
-
if (!res.ok || data.ok === false) throw new Error(data.detail || data.reason || 'request failed');
|
|
12197
|
-
return data;
|
|
12198
|
-
}
|
|
12199
|
-
|
|
12200
|
-
function taskColumn(task) {
|
|
12201
|
-
if (task.status === 'open') {
|
|
12202
|
-
const metadata = task.metadata || {};
|
|
12203
|
-
const tag = String(task.tag || '').trim().toLowerCase().replace(/\\s+/g, '-');
|
|
12204
|
-
const stage = String(metadata.stage || '').trim().toLowerCase().replace(/\\s+/g, '-');
|
|
12205
|
-
const planned = planTags.has(tag) || planTags.has(stage) || metadata.verify || metadata.goal || metadata.loop || metadata.cron || metadata.next_run_at;
|
|
12206
|
-
return planned ? 'open' : 'backlog';
|
|
12207
|
-
}
|
|
12208
|
-
if (task.status === 'claimed') return 'doing';
|
|
12209
|
-
if (task.status === 'review') return 'review';
|
|
12210
|
-
const reviewed = task.latest_event_type === 'reviewed' || !!(task.review && (task.review.reward != null || task.review.proof || task.review.lesson || task.review.next_task));
|
|
12211
|
-
if (task.status === 'failed' && reviewed) return 'done';
|
|
12212
|
-
if (task.status === 'failed') return 'blocked';
|
|
12213
|
-
if (task.status === 'done' && !reviewed) return 'review';
|
|
12214
|
-
return 'done';
|
|
12215
|
-
}
|
|
12216
|
-
|
|
12217
|
-
function esc(s) {
|
|
12218
|
-
return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
12219
|
-
}
|
|
12220
|
-
|
|
12221
|
-
function fmtTime(ms) {
|
|
12222
|
-
try { return new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
|
|
12223
|
-
catch (e) { return ''; }
|
|
12224
|
-
}
|
|
12225
|
-
|
|
12226
|
-
function renderHeartbeat(hb) {
|
|
12227
|
-
const el = $('heartbeat');
|
|
12228
|
-
el.className = 'beat ' + (hb.state || 'idle');
|
|
12229
|
-
if (hb.state === 'stale') {
|
|
12230
|
-
el.innerHTML = '<span class="dot"></span><span class="warn">stale</span><span>· ' + esc(hb.stale_reason || '') + ' · ' + (hb.total_ticks || 0) + ' ticks</span>';
|
|
12231
|
-
return;
|
|
12232
|
-
}
|
|
12233
|
-
if (!hb.total_ticks) {
|
|
12234
|
-
el.innerHTML = '<span class="dot"></span><span>idle · no ticks yet · run: atris pulse tick</span>';
|
|
12235
|
-
return;
|
|
12236
|
-
}
|
|
12237
|
-
const age = hb.last_tick_age_min == null ? '' : (hb.last_tick_age_min === 0 ? 'just now' : hb.last_tick_age_min + 'm ago');
|
|
12238
|
-
el.innerHTML = '<span class="dot"></span><b>alive</b>'
|
|
12239
|
-
+ '<span>· last tick ' + age + '</span>'
|
|
12240
|
-
+ (hb.last_what ? '<span>· ' + esc(hb.last_what) + '</span>' : '')
|
|
12241
|
-
+ '<span>· ' + hb.total_ticks + ' ticks, reward ' + (hb.reward_sum || 0) + '</span>';
|
|
12242
|
-
}
|
|
12243
|
-
|
|
12244
|
-
function renderActivity(events) {
|
|
12245
|
-
const el = $('activity');
|
|
12246
|
-
if (!events.length) { el.innerHTML = '<h2>Live Stream</h2><div class="empty">no activity yet, fire a tick: atris pulse tick</div>'; return; }
|
|
12247
|
-
let html = '<h2>Live Stream · ' + events.length + ' events</h2>';
|
|
12248
|
-
for (const e of events) {
|
|
12249
|
-
const rwCls = e.reward == null ? '' : (e.reward > 0 ? 'pos' : (e.reward < 0 ? 'neg' : ''));
|
|
12250
|
-
const rw = e.reward == null ? '' : (e.reward > 0 ? '+' + e.reward : '' + e.reward);
|
|
12251
|
-
html += '<div class="ev ' + (e.status === 'bad' ? 'bad' : '') + '">'
|
|
12252
|
-
+ '<span class="t">' + (e.ms ? fmtTime(e.ms) : '') + '</span>'
|
|
12253
|
-
+ '<span class="src ' + esc(e.source) + '">' + esc(e.source) + '</span>'
|
|
12254
|
-
+ '<span class="msg">' + esc(e.title) + (e.detail ? ' <span class="d">' + esc(e.detail) + '</span>' : '') + '</span>'
|
|
12255
|
-
+ '<span class="rw ' + rwCls + '">' + rw + '</span>'
|
|
12256
|
-
+ '</div>';
|
|
12257
|
-
}
|
|
12258
|
-
el.innerHTML = html;
|
|
12259
|
-
}
|
|
12260
|
-
|
|
12261
|
-
async function loadStream() {
|
|
12262
|
-
const data = await api('/api/stream');
|
|
12263
|
-
renderHeartbeat(data.heartbeat || {});
|
|
12264
|
-
renderActivity(data.events || []);
|
|
12265
|
-
}
|
|
12266
|
-
|
|
12267
|
-
async function load() {
|
|
12268
|
-
const data = await api('/api/tasks');
|
|
12269
|
-
state = data.projection;
|
|
12270
|
-
render();
|
|
12271
|
-
loadStream().catch(() => {});
|
|
12272
|
-
}
|
|
12273
|
-
|
|
12274
|
-
function render() {
|
|
12275
|
-
renderOverview();
|
|
12276
|
-
renderStreams();
|
|
12277
|
-
const board = $('board');
|
|
12278
|
-
board.style.setProperty('--board-columns', columns.length);
|
|
12279
|
-
board.innerHTML = '';
|
|
12280
|
-
for (const [key, label] of columns) {
|
|
12281
|
-
const tasks = state.tasks.filter((task) => taskColumn(task) === key);
|
|
12282
|
-
const col = document.createElement('div');
|
|
12283
|
-
col.className = 'col';
|
|
12284
|
-
col.innerHTML = '<h2><span>' + label + '</span><span>' + tasks.length + '</span></h2><div class="cards"></div>';
|
|
12285
|
-
const cards = col.querySelector('.cards');
|
|
12286
|
-
if (!tasks.length) cards.innerHTML = '<div class="empty">No tasks</div>';
|
|
12287
|
-
for (const task of tasks) cards.appendChild(card(task));
|
|
12288
|
-
board.appendChild(col);
|
|
12289
|
-
}
|
|
12290
|
-
renderRoom();
|
|
12291
|
-
}
|
|
12292
|
-
|
|
12293
|
-
function taskById(id) {
|
|
12294
|
-
return state.tasks.find((task) => task.id === id) || null;
|
|
12295
|
-
}
|
|
12296
|
-
|
|
12297
|
-
function renderOverview() {
|
|
12298
|
-
const active = state.tasks.filter((task) => task.status !== 'done');
|
|
12299
|
-
const reviewed = state.tasks.filter((task) => task.latest_event_type === 'reviewed');
|
|
12300
|
-
const goals = state.goals && state.goals.items || [];
|
|
12301
|
-
const goalHtml = goals.length
|
|
12302
|
-
? goals.slice(0, 4).map((goal) => '<div class="goalitem"></div>').join('')
|
|
12303
|
-
: '<div class="empty">No atris/goals.md found. Add goals to give tasks a north star.</div>';
|
|
12304
|
-
const latest = reviewed.slice(0, 3);
|
|
12305
|
-
const chainHtml = latest.length
|
|
12306
|
-
? latest.map((task) => '<div class="chainitem"><span>' + (task.display_id || task.id.slice(0, 8)) + '</span><strong></strong></div>').join('')
|
|
12307
|
-
: '<div class="empty">Complete a task with proof to start the chain.</div>';
|
|
12308
|
-
$('overview').innerHTML = [
|
|
12309
|
-
'<div class="goalbox"><h2>Goals</h2>' + goalHtml + '</div>',
|
|
12310
|
-
'<div class="chainbox"><h2>Compounding Chain</h2><div class="chainitem"><span>active</span><strong>' + active.length + ' open loops</strong></div>' + chainHtml + '</div>'
|
|
12311
|
-
].join('');
|
|
12312
|
-
$('overview').querySelectorAll('.goalitem').forEach((el, i) => { el.textContent = goals[i]; });
|
|
12313
|
-
$('overview').querySelectorAll('.chainbox .chainitem strong').forEach((el, i) => {
|
|
12314
|
-
if (i === 0) return;
|
|
12315
|
-
const task = latest[i - 1];
|
|
12316
|
-
const what = task.explanation && task.explanation.what_changes || task.title;
|
|
12317
|
-
el.textContent = (task.review && task.review.next_task) ? what + ' -> ' + task.review.next_task : what;
|
|
12318
|
-
});
|
|
12319
|
-
}
|
|
12320
|
-
|
|
12321
|
-
function renderStreams() {
|
|
12322
|
-
const streams = (state.streams || []).filter((stream) => stream.active_count || stream.done_count).slice(0, 6);
|
|
12323
|
-
const root = $('streams');
|
|
12324
|
-
if (!streams.length) {
|
|
12325
|
-
root.innerHTML = '';
|
|
12326
|
-
return;
|
|
12327
|
-
}
|
|
12328
|
-
root.innerHTML = streams.map((stream) => {
|
|
12329
|
-
const total = Math.max(1, stream.open_count + stream.doing_count + stream.review_count + stream.blocked_count);
|
|
12330
|
-
const widths = {
|
|
12331
|
-
open: Math.max(0, Math.round(stream.open_count / total * 100)),
|
|
12332
|
-
doing: Math.max(0, Math.round(stream.doing_count / total * 100)),
|
|
12333
|
-
review: Math.max(0, Math.round(stream.review_count / total * 100)),
|
|
12334
|
-
blocked: Math.max(0, Math.round(stream.blocked_count / total * 100))
|
|
12335
|
-
};
|
|
12336
|
-
const tasks = stream.tasks.filter((task) => task.status !== 'done').slice(0, 3);
|
|
12337
|
-
const taskHtml = tasks.length
|
|
12338
|
-
? tasks.map((task) => '<div class="streamtask"><span>' + (task.display_id || task.id.slice(0, 8)) + '</span><strong></strong></div>').join('')
|
|
12339
|
-
: '<div class="empty">No active tasks in this stream.</div>';
|
|
12340
|
-
return [
|
|
12341
|
-
'<div class="stream">',
|
|
12342
|
-
'<h2></h2>',
|
|
12343
|
-
'<div class="meta"><span class="pill">' + stream.active_count + ' active</span><span class="pill">' + stream.done_count + ' done</span></div>',
|
|
12344
|
-
'<div class="streambar"><span class="seg-open" style="width:' + widths.open + '%"></span><span class="seg-doing" style="width:' + widths.doing + '%"></span><span class="seg-review" style="width:' + widths.review + '%"></span><span class="seg-blocked" style="width:' + widths.blocked + '%"></span></div>',
|
|
12345
|
-
taskHtml,
|
|
12346
|
-
'</div>'
|
|
12347
|
-
].join('');
|
|
12348
|
-
}).join('');
|
|
12349
|
-
root.querySelectorAll('.stream h2').forEach((el, i) => { el.textContent = streams[i].objective; });
|
|
12350
|
-
root.querySelectorAll('.stream').forEach((streamEl, i) => {
|
|
12351
|
-
const tasks = streams[i].tasks.filter((task) => task.status !== 'done').slice(0, 3);
|
|
12352
|
-
streamEl.querySelectorAll('.streamtask strong').forEach((el, idx) => {
|
|
12353
|
-
el.textContent = tasks[idx].explanation && tasks[idx].explanation.what_changes || tasks[idx].title;
|
|
12354
|
-
});
|
|
12355
|
-
});
|
|
12356
|
-
}
|
|
12357
|
-
|
|
12358
|
-
function card(task) {
|
|
12359
|
-
const btn = document.createElement('button');
|
|
12360
|
-
btn.className = 'card' + (selected === task.id ? ' active' : '');
|
|
12361
|
-
btn.onclick = () => { selected = task.id; render(); };
|
|
12362
|
-
const owner = task.claimed_by ? '@' + task.claimed_by : 'unowned';
|
|
12363
|
-
// Plain layer first: what changes, why it matters, what done looks like.
|
|
12364
|
-
// Ids, owner, and version stay below it as detail.
|
|
12365
|
-
btn.innerHTML = '<div class="title"></div><div class="plain"></div>'
|
|
12366
|
-
+ '<div class="meta"><span class="pill"></span><span class="pill"></span><span class="pill"></span></div><div class="why"></div>';
|
|
12367
|
-
const plain = task.explanation || {};
|
|
12368
|
-
btn.querySelector('.title').textContent = plain.what_changes || task.title;
|
|
12369
|
-
const plainRows = [];
|
|
12370
|
-
plainRows.push('Why it matters: ' + (plain.why_it_matters || 'No reason recorded yet.'));
|
|
12371
|
-
plainRows.push('Done looks like: ' + (plain.done_looks_like || 'Proof of the work, then review.'));
|
|
12372
|
-
const plainBox = btn.querySelector('.plain');
|
|
12373
|
-
for (const row of plainRows) {
|
|
12374
|
-
const line = document.createElement('div');
|
|
12375
|
-
line.textContent = row;
|
|
12376
|
-
plainBox.appendChild(line);
|
|
12377
|
-
}
|
|
12378
|
-
const pills = btn.querySelectorAll('.pill');
|
|
12379
|
-
pills[0].textContent = task.display_id || task.id.slice(0, 8);
|
|
12380
|
-
pills[1].textContent = owner;
|
|
12381
|
-
pills[2].textContent = 'v' + task.current_version;
|
|
12382
|
-
btn.querySelector('.why').textContent = 'Technical details: ' + task.title;
|
|
12383
|
-
return btn;
|
|
12384
|
-
}
|
|
12385
|
-
|
|
12386
|
-
function renderRoom() {
|
|
12387
|
-
const task = state.tasks.find((t) => t.id === selected);
|
|
12388
|
-
const room = $('room');
|
|
12389
|
-
if (!task) {
|
|
12390
|
-
room.innerHTML = '<div class="empty">Select a task to open its room.</div>';
|
|
12391
|
-
return;
|
|
12392
|
-
}
|
|
12393
|
-
const messages = task.messages.map((m) => '<div class="msg"><div class="who">' + (m.actor || 'unknown') + ' / v' + m.version + '</div><div></div></div>').join('');
|
|
12394
|
-
const parent = task.lineage && task.lineage.parent_title ? task.lineage.parent_title : 'none';
|
|
12395
|
-
const children = task.lineage && task.lineage.child_titles && task.lineage.child_titles.length ? task.lineage.child_titles.join(' | ') : (task.review && task.review.next_task || 'none yet');
|
|
12396
|
-
room.innerHTML = [
|
|
12397
|
-
'<h3></h3>',
|
|
12398
|
-
'<div class="meta"><span class="pill">' + task.status + '</span><span class="pill">' + (task.claimed_by || 'unowned') + '</span><span class="pill">v' + task.current_version + '</span></div>',
|
|
12399
|
-
'<div class="fact"><b>What changes</b><div id="taskWhatChanges"></div></div>',
|
|
12400
|
-
'<div class="fact"><b>Why it matters</b><div id="taskWhyItMatters"></div></div>',
|
|
12401
|
-
'<div class="fact"><b>Done looks like</b><div id="taskDoneLooksLike"></div></div>',
|
|
12402
|
-
'<div class="fact"><b>Approval</b><div id="taskApproval"></div></div>',
|
|
12403
|
-
'<div class="fact"><b>Technical details</b><div id="taskTechnicalDetails"></div></div>',
|
|
12404
|
-
'<div class="fact"><b>Goal</b><div id="taskGoal"></div></div>',
|
|
12405
|
-
'<div class="fact"><b>Lineage</b><div id="taskLineage"></div></div>',
|
|
12406
|
-
'<div class="fact"><b>Result</b><div id="taskHappened"></div></div>',
|
|
12407
|
-
'<div class="fact"><b>How I checked</b><div id="taskChecked"></div></div>',
|
|
12408
|
-
'<div class="fact"><b>What I tested</b><div id="taskTested"></div></div>',
|
|
12409
|
-
'<div class="fact"><b>Decision</b><div id="taskDecision"></div></div>',
|
|
12410
|
-
'<div class="fact"><b>Proof / lesson</b><div id="taskProof"></div></div>',
|
|
12411
|
-
'<div class="thread">' + (messages || '<div class="empty">No thread yet.</div>') + '</div>',
|
|
12412
|
-
'<label>Add context</label><textarea id="note" placeholder="Decision, blocker, context, update..."></textarea>',
|
|
12413
|
-
'<label>Proof</label><input id="proof" placeholder="npm test, PR link, screenshot, blocked reason...">',
|
|
12414
|
-
'<label>Lesson</label><textarea id="lesson" placeholder="What did this task teach us?"></textarea>',
|
|
12415
|
-
'<label>Next task</label><input id="nextTask" placeholder="Optional next sharper task">',
|
|
12416
|
-
'<div class="actions"><button id="claim">Claim</button><button id="saveNote">Say</button><button id="requestChange">Ask for a change</button><button id="finish" class="primary full"></button></div>'
|
|
12417
|
-
].join('');
|
|
12418
|
-
const plain = task.explanation || {};
|
|
12419
|
-
const approval = task.approval || {};
|
|
12420
|
-
room.querySelector('h3').textContent = plain.what_changes || task.title;
|
|
12421
|
-
$('taskWhatChanges').textContent = plain.what_changes || task.title;
|
|
12422
|
-
$('taskWhyItMatters').textContent = plain.why_it_matters || 'No reason recorded yet.';
|
|
12423
|
-
$('taskDoneLooksLike').textContent = plain.done_looks_like || 'Proof of the work, then review.';
|
|
12424
|
-
$('taskApproval').textContent = approval.approve && approval.approve.enabled
|
|
12425
|
-
? approval.question
|
|
12426
|
-
: (approval.approve && approval.approve.blocked_reason) || 'Nothing to approve yet.';
|
|
12427
|
-
$('taskTechnicalDetails').textContent = task.title;
|
|
12428
|
-
$('taskGoal').textContent = task.objective || 'No matching goal yet.';
|
|
12429
|
-
$('taskLineage').textContent = 'parent: ' + parent + ' / next: ' + children;
|
|
12430
|
-
const result = task.review && task.review.result || {};
|
|
12431
|
-
const landing = task.review && task.review.landing || {
|
|
12432
|
-
happened: result.changed,
|
|
12433
|
-
checked: result.checked,
|
|
12434
|
-
tested: task.review && task.review.proof ? 'Proof is attached below.' : '',
|
|
12435
|
-
decision: result.accept,
|
|
12436
|
-
};
|
|
12437
|
-
$('taskHappened').textContent = landing.happened || (task.review && task.review.summary) || 'No review result yet.';
|
|
12438
|
-
$('taskChecked').textContent = landing.checked || 'No check yet.';
|
|
12439
|
-
$('taskTested').textContent = landing.tested || 'No test recorded yet.';
|
|
12440
|
-
$('taskDecision').textContent = landing.decision || 'No accept action yet.';
|
|
12441
|
-
$('taskProof').textContent = task.review && (task.review.proof || task.review.lesson)
|
|
12442
|
-
? ((task.review.proof || 'no proof') + ' / ' + (task.review.lesson || 'no lesson'))
|
|
12443
|
-
: 'No proof yet.';
|
|
12444
|
-
room.querySelectorAll('.msg div:last-child').forEach((el, i) => { el.textContent = task.messages[i].content; });
|
|
12445
|
-
const canApprove = Boolean(approval.approve && approval.approve.enabled);
|
|
12446
|
-
const planned = task.metadata && task.metadata.stage === 'plan';
|
|
12447
|
-
$('finish').textContent = canApprove
|
|
12448
|
-
? approval.approve.label
|
|
12449
|
-
: task.status === 'claimed' ? 'Move to Review' : 'Approval not ready';
|
|
12450
|
-
$('finish').disabled = !canApprove && task.status !== 'claimed';
|
|
12451
|
-
$('claim').onclick = () => mutate('/api/tasks/' + task.id + '/claim', { owner: 'operator' });
|
|
12452
|
-
// Ask for a change runs the existing revise gate; it never lands work.
|
|
12453
|
-
$('requestChange').disabled = !(approval.request_change && approval.request_change.enabled);
|
|
12454
|
-
$('requestChange').textContent = approval.request_change && approval.request_change.label || 'Ask for a change';
|
|
12455
|
-
$('requestChange').onclick = () => {
|
|
12456
|
-
const note = $('note').value.trim();
|
|
12457
|
-
if (!note) { $('note').focus(); return; }
|
|
12458
|
-
const action = task.status === 'review' ? 'revise' : planned ? 'backlog' : 'message';
|
|
12459
|
-
const body = action === 'message' ? { actor: 'operator', content: note } : { actor: 'operator', note: note, reason: note };
|
|
12460
|
-
mutate('/api/tasks/' + task.id + '/' + action, body);
|
|
12461
|
-
};
|
|
12462
|
-
$('saveNote').onclick = () => mutate('/api/tasks/' + task.id + '/message', { actor: 'operator', content: $('note').value });
|
|
12463
|
-
$('finish').onclick = () => {
|
|
12464
|
-
const proof = $('proof').value.trim();
|
|
12465
|
-
const lesson = $('lesson').value.trim();
|
|
12466
|
-
const nextTask = $('nextTask').value.trim();
|
|
12467
|
-
const payload = { actor: 'operator' };
|
|
12468
|
-
if (proof) payload.proof = proof;
|
|
12469
|
-
if (lesson) payload.lesson = lesson;
|
|
12470
|
-
if (nextTask) payload.next = nextTask;
|
|
12471
|
-
if (task.status === 'review' && canApprove) {
|
|
12472
|
-
payload.createNext = Boolean(nextTask || (task.review && task.review.next_task));
|
|
12473
|
-
mutate('/api/tasks/' + task.id + '/accept', payload);
|
|
12474
|
-
} else if (planned && canApprove) {
|
|
12475
|
-
mutate('/api/tasks/' + task.id + '/do', { actor: task.metadata.assigned_to || task.claimed_by || 'operator', first_move: task.metadata.first_move });
|
|
12476
|
-
} else {
|
|
12477
|
-
mutate('/api/tasks/' + task.id + '/ready', payload);
|
|
12478
|
-
}
|
|
12479
|
-
};
|
|
12480
|
-
}
|
|
12481
|
-
|
|
12482
|
-
async function mutate(path, body) {
|
|
12483
|
-
await api(path, { method: 'POST', body: JSON.stringify(body) });
|
|
12484
|
-
await load();
|
|
12485
|
-
}
|
|
12486
|
-
|
|
12487
|
-
$('create').onsubmit = async (e) => {
|
|
12488
|
-
e.preventDefault();
|
|
12489
|
-
const title = $('title').value.trim();
|
|
12490
|
-
if (!title) return;
|
|
12491
|
-
const data = await api('/api/tasks', { method: 'POST', body: JSON.stringify({ title, tag: $('tag').value || 'tasks' }) });
|
|
12492
|
-
selected = data.task_id;
|
|
12493
|
-
$('title').value = '';
|
|
12494
|
-
await load();
|
|
12495
|
-
};
|
|
12496
|
-
$('refresh').onclick = load;
|
|
12497
|
-
load();
|
|
12498
|
-
setInterval(load, 2500);
|
|
12499
|
-
</script>
|
|
12500
|
-
</body>
|
|
12501
|
-
</html>`;
|
|
12502
|
-
}
|
|
12503
|
-
|
|
12504
|
-
function taskBoardHtml() {
|
|
12505
|
-
return taskBoardTemplate(taskBoardViewModel());
|
|
12506
|
-
}
|
|
12507
|
-
|
|
12508
|
-
function readJsonBody(req) {
|
|
12509
|
-
if (String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json') {
|
|
12510
|
-
return Promise.reject(Object.assign(new Error('json required'), { statusCode: 415 }));
|
|
12511
|
-
}
|
|
12512
|
-
return new Promise((resolve, reject) => {
|
|
12513
|
-
let size = 0;
|
|
12514
|
-
const chunks = [];
|
|
12515
|
-
req.on('data', chunk => {
|
|
12516
|
-
if (size > 1_000_000) return;
|
|
12517
|
-
size += chunk.length;
|
|
12518
|
-
if (size > 1_000_000) {
|
|
12519
|
-
reject(Object.assign(new Error('body too large'), { statusCode: 413 }));
|
|
12520
|
-
return;
|
|
12521
|
-
}
|
|
12522
|
-
chunks.push(chunk);
|
|
12523
|
-
});
|
|
12524
|
-
req.on('end', () => {
|
|
12525
|
-
if (size > 1_000_000) return;
|
|
12526
|
-
const body = Buffer.concat(chunks).toString('utf8');
|
|
12527
|
-
if (!body.trim()) return resolve({});
|
|
12528
|
-
try {
|
|
12529
|
-
const value = JSON.parse(body);
|
|
12530
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('object required');
|
|
12531
|
-
resolve(value);
|
|
12532
|
-
} catch (e) { reject(Object.assign(e, { statusCode: 400 })); }
|
|
12533
|
-
});
|
|
12534
|
-
req.on('error', reject);
|
|
12535
|
-
});
|
|
12536
|
-
}
|
|
12537
|
-
|
|
12538
|
-
function sendJson(res, status, value, headers = {}) {
|
|
12539
|
-
res.writeHead(status, {
|
|
12540
|
-
'content-type': 'application/json; charset=utf-8',
|
|
12541
|
-
'cache-control': 'no-store',
|
|
12542
|
-
'referrer-policy': 'no-referrer',
|
|
12543
|
-
'x-content-type-options': 'nosniff',
|
|
12544
|
-
...headers,
|
|
12545
|
-
});
|
|
12546
|
-
res.end(JSON.stringify(value, null, 2));
|
|
12547
|
-
}
|
|
12548
|
-
|
|
12549
|
-
function sendHtml(res, value) {
|
|
12550
|
-
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'referrer-policy': 'no-referrer', 'x-content-type-options': 'nosniff' });
|
|
12551
|
-
res.end(value);
|
|
12552
|
-
}
|
|
12553
|
-
|
|
12554
11332
|
function taskApiQueueOptions(url) {
|
|
12555
11333
|
const limitParam = url.searchParams.get('limit');
|
|
12556
11334
|
const limit = limitParam ? Number(limitParam) : 8;
|