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.
@@ -0,0 +1,1412 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { hasFlag } = require('./arg-parser');
7
+ const {
8
+ compactErrorPayload,
9
+ printCliJson,
10
+ } = require('./cli-json');
11
+ const {
12
+ taskProofState,
13
+ LOCAL_SUCCESS_PROOF_EXAMPLE,
14
+ unrunNamedProofCommandIssue,
15
+ } = require('./task-proof');
16
+ const {
17
+ explanationFieldsFromInput,
18
+ } = require('./task-explanation');
19
+ const { RECEIPT_PATH_PATTERN } = require('./receipt-evidence');
20
+ const { isRetiredFillerReason } = require('./voice-gate');
21
+ const { operatorReady, hasAgentJargon, explainResult } = require('../commands/autoland');
22
+
23
+ const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
24
+ || process.env.USER
25
+ || os.userInfo().username
26
+ || 'unknown';
27
+ const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
28
+ const RESULT_SAVED_TEXT_LIMIT = 200;
29
+ const REVIEW_LANE_LOOP_DEFAULT_MAX_STEPS = 3;
30
+ const REVIEW_LANE_LOOP_MAX_STEPS = 10;
31
+ const REVIEW_LANE_RUN_DEFAULT_MAX_RUNS = 3;
32
+ const REVIEW_LANE_RUN_MAX_RUNS = 20;
33
+ const PENDING_REVIEW_CHAT_STOP_REASON = 'pending_review_chat_waiting_for_agent_review';
34
+ const PROOF_BOUNDARY_BLOCKED_ACTION = 'proof_boundary_blocked';
35
+ const PROOF_BOUNDARY_BLOCKED_REASON = 'proof_boundary_blocked_requires_revision';
36
+ const MISSION_XP_END_TO_END_REASON = 'mission_xp_requires_end_to_end_receipt';
37
+ const MISSION_XP_END_TO_END_DETAIL = 'mission XP proof must name a zero-papercut end-to-end fresh-laptop pass through install, init, first mission, and first self-landed task; generic mission/tick receipts are not enough';
38
+ const READY_RESULT_TEACHING = 'ready needs --result: one plain sentence someone new to the project can understand. say what someone can do now and why it matters. no ids, no paths, no commands. example: operators can now read the whole team day on one page instead of scrolling raw logs.';
39
+
40
+ const STATUS_PLAN_TAGS = new Set([
41
+ 'agent',
42
+ 'autopilot',
43
+ 'cron',
44
+ 'endgame',
45
+ 'execute',
46
+ 'explore',
47
+ 'feature',
48
+ 'goal',
49
+ 'goal-step',
50
+ 'loop',
51
+ 'plan',
52
+ 'planned',
53
+ 'schedule',
54
+ 'scheduled',
55
+ 'shape',
56
+ 'shaping',
57
+ 'ui',
58
+ 'ux',
59
+ ]);
60
+ const TASK_QUEUE_COLUMN_ORDER = ['do', 'review', 'plan', 'backlog', 'blocked', 'done'];
61
+ const TASK_QUEUE_COLUMN_LABELS = {
62
+ backlog: 'Backlog',
63
+ plan: 'Plan',
64
+ do: 'Do',
65
+ review: 'Review',
66
+ blocked: 'Blocked',
67
+ done: 'Done',
68
+ };
69
+ const TASK_REVIEW_STATE_LANES = ['needs-agent', 'continue-work', 'proof-boundary-blocked', 'human-accept-waiting', 'certified'];
70
+ const TASK_REVIEW_STATE_ALIASES = {
71
+ 'needs-agent': ['needs-review', 'agent-review'],
72
+ 'continue-work': ['continue', 'agent-actionable', 'executable'],
73
+ 'proof-boundary-blocked': ['proof-boundary', 'boundary-blocked', 'stale-pr-proof', 'unmerged-pr-proof'],
74
+ 'human-accept-waiting': ['human-accept', 'accept-waiting', 'waiting-accept', 'no-next-task'],
75
+ certified: ['waiting-human', 'human-waiting'],
76
+ };
77
+
78
+ let taskDbModule = null;
79
+
80
+ function getTaskDb() {
81
+ if (taskDbModule) return taskDbModule;
82
+ try {
83
+ taskDbModule = require('./task-db');
84
+ return taskDbModule;
85
+ } catch (e) {
86
+ const message = String(e && (e.message || e));
87
+ const missingSqlite = e && (
88
+ e.code === 'ERR_UNKNOWN_BUILTIN_MODULE'
89
+ || /node:sqlite|No such built-in module/i.test(message)
90
+ );
91
+ if (missingSqlite) {
92
+ console.error('atris task requires Node.js 22+ because it uses built-in node:sqlite.');
93
+ console.error('Use the markdown TODO.md flow on older Node versions.');
94
+ process.exit(1);
95
+ }
96
+ throw e;
97
+ }
98
+ }
99
+
100
+ function warnIfTaskTitleNeedsOperatorWhy(title, options = {}) {
101
+ const text = String(title || '').trim();
102
+ if (!text || operatorReady(text)) return null;
103
+ const warning = 'Warning: put the why in this task title in plain words: what it buys or costs, and who benefits. Drop flags and ids.';
104
+ if (options.print !== false) console.error(warning);
105
+ return warning;
106
+ }
107
+
108
+ function taskUsageText() {
109
+ return `
110
+ atris task - durable local task state (SQLite, gitignored)
111
+
112
+ golden path (one tick, by cron or by hand):
113
+ atris task delegate "fix the login bug" --to <member>
114
+ atris task claim <id> --as <member>
115
+ ... build ...
116
+ atris task ready <id> --verify
117
+ atris autoland tick # second check runs, task lands
118
+
119
+ atris task Same two-line next as bare atris
120
+ atris task desk [--all] Show the full task desk
121
+ atris task new "<title>" [--what-changes "..."] [--why-it-matters "..."] [--done-looks-like "..."] [--verify <cmd>]
122
+ Create a task with a plain explanation; omitted fields get honest defaults
123
+ atris task next [--tag <tag>] [--create-next]
124
+ Same next command as bare atris and the task desk; --create-next still seeds Endgame fallback
125
+ atris task continue-work <id> Create/reuse a certified Review follow-up task
126
+ atris task say <id> "<message>" Add context to a task
127
+ atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
128
+ atris task ready <id> --proof "..." --result "<sentence>"
129
+ Agent proof ready; native goal can complete
130
+ atris task ready <id> --verify "<cmd>" --result "<sentence>"
131
+ Run <cmd>; only ready if it exits 0 (executed proof)
132
+ Writes atris/runs/ receipt (pass or fail); that local pass is enough proof
133
+ Optional CI URL only with --proof-url and --i-fetched
134
+ atris task receipt <id> --verify "<cmd>" Run <cmd> and write an atris/runs/ receipt without going to ready
135
+ atris task plan-preview "<purpose>" [--tag <tag>] [--owner <member>] [--task <id>]
136
+ Show the plain Plan before work starts
137
+ atris task ready <id> --proof "..." --result "<sentence>" [--changed "..." --checked "..." --saved "..." --try "..."]
138
+ Agent proof ready; records Result if needed
139
+ atris task ready <id> --proof "..." --result "<sentence>" [--happened "..." --checked "..." --tested "..." --decision "..."]
140
+ Agent proof ready; writes the human result receipt
141
+ atris task result <id> "<sentence>" Set or replace the day-one PM result sentence
142
+ atris task result <id> --changed "..." --checked "..." [--saved "..."] [--try "..."]
143
+ Show the plain Result and store trace on the task
144
+ atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
145
+ atris task accept <id> [--proof "..."] [--public]
146
+ Human accepts proof, marks done; --public also publishes AgentXP
147
+ atris task certify-verified [--dry-run] [--limit <n>] [--as <actor>]
148
+ 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)
149
+ atris task auto-accept-certified --dry-run [--strict-verify] [--all] [--limit <n>]
150
+ Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
151
+ atris task sweep --auto-accept [--json] Auto-accept verified Review rows; protected lanes wait for human
152
+ atris task sweep [--json] [--stale-days N] [--claimer NAME] [--dry-run] Release expired claims
153
+ atris task sweep --held [--everywhere] [--dry-run] [--json] Check held jobs against GitHub: credit a merge, release a closed pull request
154
+ atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
155
+ atris task revise <id> --note "..." Send reviewed work back to Do
156
+
157
+ atris task add "<title>" [--tag <tag>] [--goal-id <id>] [--what-changes "..."] [--why-it-matters "..."] [--done-looks-like "..."] [--verify <cmd>] Create a task
158
+ 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
159
+ atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
160
+ Record a task-owned Plan stage
161
+ atris task do <id> --as <owner> --first-move "..."
162
+ Start task-owned Do work from the plan
163
+ atris task backlog <id> [--reason "..."] Move a planned open task back to Backlog
164
+ atris task clear-plan --yes Move all planned open tasks back to Backlog
165
+ atris task day [--full] [--all] [--everywhere] [--json] show today's owner-grouped task list
166
+ text shows eight current rows; --full shows every active row
167
+ --all stays in this workspace; --everywhere spans workspaces
168
+ atris task list [--all] [--everywhere] [--status <s>]
169
+ list tasks in this workspace; --everywhere spans workspaces
170
+ atris task claim <id> [--as <owner>] Atomic claim
171
+ atris task release <id> [--as <owner>] [--reason "<why>"] Release your own mistaken claim back to open
172
+ atris task capabilities [--json] Read-only task CLI/API capability contract
173
+ atris task capabilities-check [--json] Verify task capability contract conformance
174
+ atris task review-lane-drain [--json] Pick next safe Review-lane agent action
175
+ atris task review-lane-act [--json] Execute next safe Review-lane agent action
176
+ atris task review-lane-loop [--json] Run bounded safe Review-lane actions
177
+ atris task review-lane-run [--json] Run bounded review-lane loops and write receipts
178
+ atris task current [--json] [--goal-id <id>] [--tag <tag>] [--status <s>] [--review-state <lane>]
179
+ Read-only best next task page + queue
180
+ atris task queue [--json] [--goal-id <id>] [--tag <tag>] [--status <s>] [--review-state <lane>]
181
+ Read-only task lanes + current page
182
+ atris task current-step [--json] [--goal-id <id>] [--tag <tag>] [--review-state <lane>]
183
+ Advance the scoped current task one safe step
184
+ review-state lanes: needs-agent, continue-work, human-accept-waiting, certified
185
+ atris task note <id> "<message>" Append dialogue/context to a task
186
+ atris task retitle <id> "<new title>" Rename a task and preserve the old title in dialogue
187
+ atris task tag <id> --add <tag> [--remove <tag>]
188
+ Update tags on an existing task (e.g. --add needs-human to hold it
189
+ from sweep + fleet staffing); logs a task_tags_updated event
190
+ atris task show <id> [--json] Show a task card + dialogue
191
+ atris task inspect <id> --fields review,status,title [--json]
192
+ Field-selectable task state (review metadata, status, title, owner, tag)
193
+ atris task page <id> [--json] Show the one-task page contract
194
+ atris task step <id> [--json] Refine chat, then advance one safe Plan/Do/Review step
195
+ atris task done <id> --proof "..." Mark complete with proof
196
+ atris task done <id> --failed [--proof "..."] Mark failed, optionally reviewed
197
+ atris task archive <id> --reason "..." [--from-failed] [--not-needed]
198
+ Sweep off-roadmap/duplicate work as archived (not failed);
199
+ --from-failed opts in to relabel a fail-closed row (never done)
200
+ --not-needed: a person confirms the job did not need doing, so a
201
+ drop on it stops counting against the member (needs --reason;
202
+ refused inside an agent session or a dispatched worker)
203
+ atris task reopen <id> [--reason "..."] Send a done, waiting, failed, or archived task back to open
204
+ atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
205
+ atris task prune-test-junk [--yes] [--vacuum] [--json]
206
+ Count rows test runs left from temp folders; --yes backs up the db, then removes them
207
+ atris task keep [--json] Put away finished and untouched work, then refresh the list
208
+ atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
209
+ atris task relabel-archived [--dry-run|--apply]
210
+ One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
211
+ atris task finish <id> --proof "..." Legacy alias for done with proof
212
+ atris task review <id> --reward <n> [--verify "<cmd>"]
213
+ Write review event + RSI episode
214
+ atris task reviews [--all|--limit <n>] [--verbose] Show certified Review items for human accept/revise
215
+ atris task reviews --group-by <tag|owner|source> Cluster approval-ready work for fast triage
216
+ atris task accept-group <key>=<value> --spot-check K --confirm-human-accept --as <you> --verified <ids>
217
+ Accept a whole cluster; career XP only on the K you verified
218
+ atris task status [--json] [--history] Compact live status for web/Swarlo
219
+ atris task setup [--import-todo] Create/refresh task projection
220
+ atris task serve [--port <n>] Open local task factory board
221
+ atris task sync --dry-run Plan cloud/Swarlo task sync writes
222
+ atris task import <file> One-shot import from TODO.md
223
+ atris task lineage <id> [--json] Show endgame -> tasks -> commits chain
224
+ atris task events [id] [--limit <n>] print recent task events
225
+ atris task events --all print the full current-workspace ledger
226
+ atris task events --everywhere print the full ledger across workspaces
227
+ atris task export [--all] [--everywhere] [--out <file>]
228
+ write web/desktop JSON projection
229
+ atris task render [--all] [--everywhere] [--out <file>]
230
+ regenerate compact TODO.md view from state
231
+ atris task where Print db path + workspace scope
232
+ atris task help This help
233
+
234
+ Confidence Gate:
235
+ Before plan/do/review advances, find loopholes, patch them with proof,
236
+ verifier, owner, rollback, or name the residual risk.
237
+
238
+ Env:
239
+ ATRIS_TASKS_DB Override db path (default ~/.atris/tasks.db)
240
+ ATRIS_AGENT_ID Owner id for claim/done (default: $USER)
241
+
242
+ Refs:
243
+ Human views use semantic refs like OBL-18. Commands accept OBL-18,
244
+ OBL18, full 26-char task IDs, and any unique legacy prefix. JSON/API
245
+ keep the full id as canonical and also expose display_id + legacy_ref.
246
+
247
+ Headless:
248
+ Add --json to task commands for machine-readable output and stable automation.
249
+ `.trim();
250
+ }
251
+
252
+ function taskUsageLines() {
253
+ return taskUsageText().split('\n');
254
+ }
255
+
256
+ function help() {
257
+ console.log(taskUsageText());
258
+ }
259
+
260
+ function flag(args, name) {
261
+ const i = args.indexOf(name);
262
+ if (i === -1) return null;
263
+ return args[i + 1] || true;
264
+ }
265
+
266
+ function taskScopeEverywhere(args = [], options = {}) {
267
+ if (options.everywhere !== undefined) return Boolean(options.everywhere);
268
+ return hasFlag(args, '--everywhere');
269
+ }
270
+
271
+ function scopedWorkspaceRoot(taskDb, args = [], options = {}) {
272
+ return taskScopeEverywhere(args, options) ? null : taskDb.workspaceRoot();
273
+ }
274
+
275
+ function hasEmptyFlagValue(args, name) {
276
+ const i = args.indexOf(name);
277
+ return i !== -1 && args[i + 1] === '';
278
+ }
279
+
280
+ function wantsJson(args) {
281
+ return hasFlag(args, '--json');
282
+ }
283
+
284
+ function parseAcceptReward(value, { defaultValue = 1 } = {}) {
285
+ if (value === undefined || value === null || value === true) return { ok: true, value: defaultValue };
286
+ const numeric = Number(value);
287
+ if (!Number.isFinite(numeric) || numeric <= 0) {
288
+ return { ok: false, reason: 'invalid_reward' };
289
+ }
290
+ return { ok: true, value: numeric };
291
+ }
292
+
293
+ function validHumanActorFlag(value) {
294
+ if (typeof value !== 'string') return false;
295
+ const actor = value.trim();
296
+ return Boolean(actor) && !actor.startsWith('--') && actor !== 'auto-accept-certified';
297
+ }
298
+
299
+ const AGENT_ENV_MARKERS = [
300
+ 'CLAUDECODE',
301
+ 'CLAUDE_CODE_ENTRYPOINT',
302
+ 'CODEX_SANDBOX',
303
+ 'CURSOR_AGENT',
304
+ 'DEVIN_SESSION_ID',
305
+ ];
306
+
307
+ function agentProofOnlyMode() {
308
+ const explicit = process.env.ATRIS_AGENT_PROOF_ONLY;
309
+ if (explicit === '1') return true;
310
+ if (explicit === '0') return false;
311
+ return AGENT_ENV_MARKERS.some((marker) => String(process.env[marker] || '').trim() !== '');
312
+ }
313
+
314
+ const WORKER_ENV_MARKERS = [
315
+ ...AGENT_ENV_MARKERS,
316
+ 'CODEX_THREAD_ID',
317
+ 'CODEX_SESSION_ID',
318
+ 'ATRIS_MISSION_DRIVER_DETACHED',
319
+ 'ATRIS_MISSION_DRIVER_PARENT_PID',
320
+ 'ATRIS_DISPATCH_DEPTH',
321
+ ];
322
+
323
+ function workerEnvMarker(env = process.env) {
324
+ const found = WORKER_ENV_MARKERS.find((marker) => String(env[marker] || '').trim() !== '');
325
+ if (found) return found;
326
+ return String(env.ATRIS_AGENT_PROOF_ONLY || '').trim() === '1' ? 'ATRIS_AGENT_PROOF_ONLY' : '';
327
+ }
328
+
329
+ function failAgentProofOnly(label, detail) {
330
+ failTask(
331
+ label,
332
+ 'agent_proof_only_human_accept_blocked',
333
+ detail || 'Agent proof-only mode can write notes, ready proof, and zero-reward reviews, but cannot mark tasks done or accept XP.',
334
+ );
335
+ }
336
+
337
+ function writeStdoutSync(text) {
338
+ const buffer = Buffer.from(text, 'utf8');
339
+ const retryWait = new Int32Array(new SharedArrayBuffer(4));
340
+ let offset = 0;
341
+ while (offset < buffer.length) {
342
+ try {
343
+ offset += fs.writeSync(process.stdout.fd, buffer, offset, buffer.length - offset);
344
+ } catch (err) {
345
+ if (err && err.code === 'EAGAIN') {
346
+ Atomics.wait(retryWait, 0, 0, 10);
347
+ continue;
348
+ }
349
+ throw err;
350
+ }
351
+ }
352
+ }
353
+
354
+ function printJson(value) {
355
+ writeStdoutSync(`${JSON.stringify(value, null, 2)}\n`);
356
+ }
357
+
358
+ function refreshCareerXpProjection(workspaceRoot) {
359
+ if (!workspaceRoot) return null;
360
+ try {
361
+ const { collectLocalXpProjection } = require('../commands/xp');
362
+ const { careerXpRoot } = require('./career-xp-root');
363
+ return collectLocalXpProjection(['--workspace', careerXpRoot(workspaceRoot)]);
364
+ } catch (error) {
365
+ return {
366
+ ok: false,
367
+ error: error && error.message ? error.message : String(error),
368
+ };
369
+ }
370
+ }
371
+
372
+ function refreshCareerXpAfterReview(reviewed) {
373
+ return refreshCareerXpProjection(reviewed?.episode?.workspace_root);
374
+ }
375
+
376
+ function compactCareerXpProjection(projection) {
377
+ if (!projection || typeof projection !== 'object') return projection;
378
+ const progress = projection.next_level_progress && typeof projection.next_level_progress === 'object'
379
+ ? {
380
+ level: projection.next_level_progress.level ?? null,
381
+ next_level: projection.next_level_progress.next_level ?? null,
382
+ current_xp: projection.next_level_progress.current_xp ?? null,
383
+ required_xp: projection.next_level_progress.required_xp ?? null,
384
+ remaining_xp: projection.next_level_progress.remaining_xp ?? null,
385
+ percent: projection.next_level_progress.percent ?? null,
386
+ }
387
+ : null;
388
+ const latest = projection.latest_accepted_proof && typeof projection.latest_accepted_proof === 'object'
389
+ ? {
390
+ label: projection.latest_accepted_proof.label ?? null,
391
+ receipt_id: projection.latest_accepted_proof.receipt_id ?? null,
392
+ source: projection.latest_accepted_proof.source ?? null,
393
+ source_task_id: projection.latest_accepted_proof.source_task_id ?? null,
394
+ title: projection.latest_accepted_proof.title ?? null,
395
+ xp: projection.latest_accepted_proof.xp ?? null,
396
+ reward: projection.latest_accepted_proof.reward ?? null,
397
+ accepted_at: projection.latest_accepted_proof.accepted_at ?? null,
398
+ goal: projection.latest_accepted_proof.goal ?? null,
399
+ }
400
+ : null;
401
+ const integrity = projection.integrity && typeof projection.integrity === 'object'
402
+ ? {
403
+ status: projection.integrity.status ?? null,
404
+ receipts_count: projection.integrity.receipts_count ?? projection.receipts_count ?? null,
405
+ head_hash: projection.integrity.head_hash ?? null,
406
+ }
407
+ : null;
408
+ return {
409
+ schema: projection.schema,
410
+ compact: true,
411
+ total_xp: projection.total_xp ?? projection.total_agent_xp ?? projection.agent_xp ?? null,
412
+ agent_xp: projection.agent_xp ?? projection.total_agent_xp ?? null,
413
+ today_xp: projection.today_xp ?? projection.today_agent_xp ?? null,
414
+ collected_receipts: projection.collected_receipts ?? 0,
415
+ receipts_count: projection.receipts_count ?? null,
416
+ level: projection.level ?? null,
417
+ leaderboard_eligible: projection.leaderboard_eligible ?? null,
418
+ integrity_status: projection.integrity_status ?? integrity?.status ?? null,
419
+ next_level_progress: progress,
420
+ latest_accepted_proof: latest,
421
+ integrity,
422
+ omitted: [
423
+ 'contribution_graph',
424
+ 'career',
425
+ 'earning_model',
426
+ 'local_activity',
427
+ 'ledger',
428
+ 'sources',
429
+ ],
430
+ };
431
+ }
432
+
433
+ function jsonModeActive() {
434
+ return process.argv.includes('--json');
435
+ }
436
+
437
+ function failTask(label, reason, detail, exitCode = 2) {
438
+ if (jsonModeActive()) {
439
+ printCliJson(
440
+ {
441
+ ok: false,
442
+ command: label,
443
+ reason,
444
+ detail: detail || null,
445
+ selected_ref: null,
446
+ next_command: null,
447
+ },
448
+ compactErrorPayload({ reason, detail: detail || null }),
449
+ process.argv.slice(2),
450
+ );
451
+ } else {
452
+ console.error(detail || `${label}: ${reason}`);
453
+ }
454
+ process.exit(exitCode);
455
+ }
456
+
457
+ function refuseCandidatePolicyGate(label, gate) {
458
+ if (jsonModeActive()) {
459
+ printJson({ ok: false, command: label, ...gate });
460
+ } else {
461
+ console.error(`${label}: ${gate.reason}: ${gate.message}`);
462
+ if (Array.isArray(gate.offenders)) {
463
+ gate.offenders.forEach((offender) => console.error(` ${offender}`));
464
+ }
465
+ if (Array.isArray(gate.lesson_ids) && gate.lesson_ids.length) {
466
+ console.error(` lessons: ${gate.lesson_ids.join(', ')}`);
467
+ }
468
+ }
469
+ process.exit(1);
470
+ }
471
+
472
+ function proofFlagValue(args) {
473
+ const proof = flag(args, '--proof');
474
+ return typeof proof === 'string' ? proof.trim() : '';
475
+ }
476
+
477
+ function resultSentenceIssue(value, { allowCommandMention = false } = {}) {
478
+ const check = explainResult(value, { allowCommandMention });
479
+ return check.ok ? null : check.reason;
480
+ }
481
+
482
+ function readyResultDetail(reason) {
483
+ return reason ? `${READY_RESULT_TEACHING}\n${reason}` : READY_RESULT_TEACHING;
484
+ }
485
+
486
+ function requireResultSentence(label, value, { ready = false, allowCommandMention = false } = {}) {
487
+ const issue = resultSentenceIssue(value, { allowCommandMention });
488
+ if (!issue) return String(value || '').replace(/\s+/g, ' ').trim();
489
+ const detail = ready ? readyResultDetail(issue) : issue;
490
+ failTask(label, 'weak_result', detail);
491
+ }
492
+
493
+ function weakProofDetail(issue) {
494
+ return `meaningful proof required: ${issue}\n${LOCAL_SUCCESS_PROOF_EXAMPLE}`;
495
+ }
496
+
497
+ function requireMeaningfulTaskProof(label, proof, { required = true } = {}) {
498
+ const issue = meaningfulTaskProofIssue(proof, { required });
499
+ if (issue) failTask(label, 'weak_proof', weakProofDetail(issue));
500
+ }
501
+
502
+ function requireRanNamedProofCommand(label, proof, ranCommand = '') {
503
+ const issue = unrunNamedProofCommandIssue(proof, ranCommand);
504
+ if (issue) failTask(label, issue.reason, issue.detail);
505
+ }
506
+
507
+ function sendProofIssue(res, proof, issue) {
508
+ return sendJson(res, 400, {
509
+ ok: false,
510
+ reason: String(proof || '').trim() ? 'weak_proof' : 'proof_required',
511
+ detail: weakProofDetail(issue),
512
+ });
513
+ }
514
+
515
+ function textFlag(args, names) {
516
+ for (const name of names) {
517
+ const value = flag(args, name);
518
+ if (typeof value === 'string') return value.trim();
519
+ }
520
+ return '';
521
+ }
522
+
523
+ function explanationFlags(args) {
524
+ return explanationFieldsFromInput({
525
+ what_changes: textFlag(args, ['--what-changes']),
526
+ why_it_matters: textFlag(args, ['--why-it-matters']),
527
+ done_looks_like: textFlag(args, ['--done-looks-like']),
528
+ });
529
+ }
530
+
531
+ function landingFlags(args) {
532
+ return {
533
+ happened: textFlag(args, ['--landing', '--happened', '--what-happened']),
534
+ checked: textFlag(args, ['--checked', '--how-checked']),
535
+ tested: textFlag(args, ['--tested', '--what-tested']),
536
+ decision: textFlag(args, ['--decision', '--acceptance']),
537
+ };
538
+ }
539
+
540
+ function readyMergeFlags(args) {
541
+ const commit = textFlag(args, ['--merge-commit']);
542
+ const number = textFlag(args, ['--pr-number']);
543
+ const url = textFlag(args, ['--pr-url']);
544
+ if (!commit && !number && !url) return null;
545
+ return {
546
+ merge_commit: commit,
547
+ number,
548
+ url,
549
+ merged_at: textFlag(args, ['--merged-at']),
550
+ self_landed: hasFlag(args, '--self-landed'),
551
+ };
552
+ }
553
+
554
+ function normalizedLandingSentence(value) {
555
+ return String(value || '').replace(/\s+/g, ' ').trim();
556
+ }
557
+
558
+ function defaultLandingSentenceForTitle(title) {
559
+ return `Completed: ${String(title || '').trim()}.`;
560
+ }
561
+
562
+ function landingNeedsDayOnePm(sentence, title) {
563
+ const text = normalizedLandingSentence(sentence);
564
+ if (!text) return true;
565
+ if (text.toLowerCase() === normalizedLandingSentence(defaultLandingSentenceForTitle(title)).toLowerCase()) return true;
566
+ return hasAgentJargon(text) || /\bas\s+exists?\b/i.test(text) || !operatorReady(text);
567
+ }
568
+
569
+ function warnIfLandingNeedsDayOnePm(landing, title) {
570
+ const sentence = landing && typeof landing === 'object' ? landing.happened : '';
571
+ if (!landingNeedsDayOnePm(sentence, title)) return null;
572
+ 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.';
573
+ console.error(warning);
574
+ return warning;
575
+ }
576
+
577
+ function requireExplicitLandingDayOnePm(label, landing, title) {
578
+ const sentence = landing && typeof landing === 'object' ? normalizedLandingSentence(landing.happened) : '';
579
+ if (!sentence || !landingNeedsDayOnePm(sentence, title)) return;
580
+ failTask(
581
+ label,
582
+ 'weak_landing',
583
+ '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.',
584
+ );
585
+ }
586
+
587
+ function numericFlag(args, name) {
588
+ const value = flag(args, name);
589
+ if (value === null || value === true || value === undefined) return null;
590
+ const parsed = Number(value);
591
+ return Number.isFinite(parsed) ? parsed : null;
592
+ }
593
+
594
+ function meaningfulTaskProofIssue(proof, { required = true } = {}) {
595
+ const text = String(proof || '').trim();
596
+ if (!required && !text) return null;
597
+ const state = taskProofState(text);
598
+ return state.ok ? null : state.reason;
599
+ }
600
+
601
+ function goldenPathMissionXpTask(task) {
602
+ if (!task) return false;
603
+ const metadata = task.metadata || {};
604
+ const text = [
605
+ task.title,
606
+ task.tag,
607
+ metadata.goal_id,
608
+ metadata.goalId,
609
+ metadata.mission_id,
610
+ metadata.stop_condition,
611
+ metadata.goal_objective,
612
+ metadata.objective,
613
+ ].filter(Boolean).join(' ');
614
+ const missionXp = String(task.title || '').trim().toLowerCase().startsWith('mission xp:')
615
+ || String(task.tag || '').toLowerCase() === 'agent-xp';
616
+ return missionXp
617
+ && /\b(?:golden[- ]path|zero[- ]knowledge|zero[- ]papercuts?|fresh[- ](?:laptop|environment|install|home)|self[- ]landed)\b/i.test(text);
618
+ }
619
+
620
+ function debugUnreadableReceipt(rel, err) {
621
+ if (process.env.ATRIS_DEBUG) console.error(`[task] receipt unreadable, skipped: ${rel}: ${err && err.message}`);
622
+ }
623
+
624
+ function receiptTextForProof(proof, root = process.cwd()) {
625
+ const chunks = [];
626
+ const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
627
+ let match;
628
+ while ((match = pattern.exec(String(proof || ''))) && chunks.length < 3) {
629
+ const rel = match[1];
630
+ if (!rel || rel.includes('*')) continue;
631
+ try {
632
+ const raw = fs.readFileSync(path.resolve(root, rel), 'utf8');
633
+ const parsed = JSON.parse(raw);
634
+ chunks.push(JSON.stringify({
635
+ schema: parsed.schema || null,
636
+ mission_id: parsed.mission_id || null,
637
+ result: parsed.result || null,
638
+ landing: parsed.landing || null,
639
+ last_landing: parsed.last_landing || null,
640
+ summary: parsed.summary || null,
641
+ }).slice(0, 12000));
642
+ } catch (err) {
643
+ debugUnreadableReceipt(rel, err);
644
+ }
645
+ }
646
+ return chunks.join(' ');
647
+ }
648
+
649
+ function missionReceiptResultForProof(task, proof, root = process.cwd()) {
650
+ const metadata = task?.metadata || {};
651
+ if (!metadata.mission_id && !metadata.goal_id) return null;
652
+ const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
653
+ let match;
654
+ while ((match = pattern.exec(String(proof || '')))) {
655
+ const rel = match[1];
656
+ if (!rel || rel.includes('*')) continue;
657
+ let landing = null;
658
+ try {
659
+ const parsed = JSON.parse(fs.readFileSync(path.resolve(root, rel), 'utf8'));
660
+ landing = parsed?.result?.landing || parsed?.landing || parsed?.last_landing || null;
661
+ } catch (err) {
662
+ debugUnreadableReceipt(rel, err);
663
+ continue;
664
+ }
665
+ const changed = String(landing?.changed || landing?.happened || '').replace(/\s+/g, ' ').trim();
666
+ if (!changed) continue;
667
+ if (/\brecorded tick \d+\.?$/i.test(changed) || /^recorded a proof heartbeat\b/i.test(changed)) continue;
668
+ const reason = String(landing?.reason || landing?.why || '').replace(/\s+/g, ' ').trim();
669
+ return { changed, reason: reason && !isRetiredFillerReason(reason) ? reason : null };
670
+ }
671
+ return null;
672
+ }
673
+
674
+ function missionXpEndToEndProofIssue(task, proof, root = process.cwd()) {
675
+ if (!goldenPathMissionXpTask(task)) return null;
676
+ const corpus = `${String(proof || '')} ${receiptTextForProof(proof, root)}`
677
+ .replace(/\s+/g, ' ')
678
+ .trim();
679
+ const hasZeroPapercut = /\b(?:zero|0|no)\s+(?:new\s+)?papercuts?\b|\bzero[- ]papercut\b/i.test(corpus);
680
+ 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);
681
+ 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);
682
+ return hasZeroPapercut && hasEndToEnd && hasSelfLanded ? null : MISSION_XP_END_TO_END_DETAIL;
683
+ }
684
+
685
+ function missionXpProofBoundaryEvaluation(task, proofOverride = null) {
686
+ if (!goldenPathMissionXpTask(task)) return null;
687
+ const metadata = task.metadata || {};
688
+ const review = task.review || {};
689
+ const proof = proofOverride === null
690
+ ? String(review.proof || metadata.latest_agent_proof || '').trim()
691
+ : String(proofOverride || '').trim();
692
+ const issue = missionXpEndToEndProofIssue(task, proof, task.workspace_root || process.cwd());
693
+ if (!issue) return null;
694
+ return {
695
+ eligible: false,
696
+ ref: taskRef(task),
697
+ reason: MISSION_XP_END_TO_END_REASON,
698
+ next_action: 'attach the zero-papercut end-to-end fresh-laptop receipt, then resubmit Mission XP proof',
699
+ proof,
700
+ };
701
+ }
702
+
703
+ function positional(args) {
704
+ return args.filter((a, i) => {
705
+ if (a.startsWith('--')) return false;
706
+ if (i > 0 && args[i - 1].startsWith('--')) return false;
707
+ return true;
708
+ });
709
+ }
710
+
711
+ function taskFromProjection(projection, id) {
712
+ return projection.tasks.find(t => t.id === id) || null;
713
+ }
714
+
715
+ function taskRef(taskOrId) {
716
+ if (!taskOrId) return 'TASK';
717
+ if (typeof taskOrId === 'string') return taskOrId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 8);
718
+ return taskOrId.display_id || taskOrId.legacy_ref || taskRef(taskOrId.id);
719
+ }
720
+
721
+ function normalizeTaskLookupRef(value) {
722
+ return String(value || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
723
+ }
724
+
725
+ function taskLookupRefs(task) {
726
+ if (!task) return [];
727
+ return [task.id, task.display_id, task.legacy_ref, taskRef(task)]
728
+ .map(normalizeTaskLookupRef)
729
+ .filter(Boolean);
730
+ }
731
+
732
+ function resolveProjectionTaskRef(ref, taskByRef) {
733
+ const key = normalizeTaskLookupRef(ref);
734
+ return key ? taskByRef.get(key) || null : null;
735
+ }
736
+
737
+ function normalizedStatusPart(value) {
738
+ return String(value || '').trim().toLowerCase().replace(/\s+/g, '-');
739
+ }
740
+
741
+ function taskIsPlannedOpen(task) {
742
+ const metadata = task && task.metadata || {};
743
+ const tag = normalizedStatusPart(task && task.tag);
744
+ const stage = normalizedStatusPart(metadata.stage);
745
+ return STATUS_PLAN_TAGS.has(tag)
746
+ || STATUS_PLAN_TAGS.has(stage)
747
+ || Boolean(metadata.verify || metadata.goal || metadata.loop || metadata.cron || metadata.next_run_at);
748
+ }
749
+
750
+ function taskColumn(task) {
751
+ if (task.status === 'open') return taskIsPlannedOpen(task) ? 'open' : 'backlog';
752
+ if (task.status === 'claimed') return 'doing';
753
+ if (task.status === 'review') return 'review';
754
+ if (task.status === 'failed' && taskHasReview(task)) return 'done';
755
+ if (task.status === 'failed') return 'blocked';
756
+ if (task.status === 'done' && !taskHasReview(task)) return 'review';
757
+ return 'done';
758
+ }
759
+
760
+ function taskHasReview(task) {
761
+ if (task.latest_event_type === 'reviewed') return true;
762
+ const review = task.review || {};
763
+ return review.reward != null || Boolean(review.proof || review.lesson || review.next_task);
764
+ }
765
+
766
+ const TASK_BOARD_COLUMNS = [
767
+ ['backlog', 'Backlog'],
768
+ ['open', 'Open'],
769
+ ['doing', 'Doing'],
770
+ ['review', 'Review'],
771
+ ['blocked', 'Blocked'],
772
+ ['done', 'Done'],
773
+ ];
774
+
775
+ function taskBoardViewModel(projection = {}) {
776
+ const tasks = Array.isArray(projection.tasks) ? projection.tasks : [];
777
+ const rows = tasks.map((task) => ({
778
+ id: task.id,
779
+ task,
780
+ column: taskColumn(task),
781
+ decision: taskHasReview(task),
782
+ }));
783
+ const columns = TASK_BOARD_COLUMNS.map(([key, label]) => ({
784
+ key,
785
+ label,
786
+ rows: rows.filter((row) => row.column === key),
787
+ }));
788
+ const counts = {};
789
+ for (const column of columns) counts[column.key] = column.rows.length;
790
+ return {
791
+ columns,
792
+ counts,
793
+ rows,
794
+ total: rows.length,
795
+ planTags: Array.from(STATUS_PLAN_TAGS),
796
+ };
797
+ }
798
+
799
+ function taskBoardTemplate(model) {
800
+ return `<!doctype html>
801
+ <html lang="en">
802
+ <head>
803
+ <meta charset="utf-8">
804
+ <meta name="viewport" content="width=device-width, initial-scale=1">
805
+ <title>Atris Task Factory</title>
806
+ <link rel="preconnect" href="https://fonts.googleapis.com">
807
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
808
+ <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">
809
+ <style>
810
+ /* aesthetic: machine-room telemetry, warm-tinted dark, mono data, calm signals (no neon) */
811
+ :root {
812
+ color-scheme: dark;
813
+ --bg: oklch(18% 0.012 160);
814
+ --panel: oklch(22% 0.014 160);
815
+ --panel-2: oklch(25% 0.016 160);
816
+ --line: oklch(32% 0.015 160);
817
+ --text: oklch(93% 0.012 150);
818
+ --muted: oklch(70% 0.018 160);
819
+ --accent: oklch(74% 0.115 158);
820
+ --warn: oklch(81% 0.11 80);
821
+ --bad: oklch(69% 0.14 25);
822
+ --info: oklch(75% 0.10 240);
823
+ --violet: oklch(73% 0.10 300);
824
+ --sans: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif;
825
+ --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
826
+ }
827
+ * { box-sizing: border-box; }
828
+ body {
829
+ margin:0; color:var(--text);
830
+ font-family: var(--sans);
831
+ -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
832
+ background:
833
+ radial-gradient(1100px 520px at 82% -12%, oklch(30% 0.04 160 / 0.55), transparent 62%),
834
+ repeating-linear-gradient(0deg, oklch(32% 0.015 160 / 0.16) 0 1px, transparent 1px 34px),
835
+ var(--bg);
836
+ background-attachment: fixed;
837
+ }
838
+ 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)); }
839
+ h1 { font-size:17px; margin:0; font-weight:700; letter-spacing:-0.01em; }
840
+ .sub { color:var(--muted); font-size:12px; }
841
+ main { display:grid; grid-template-columns: 320px 1fr; height:calc(100vh - 60px); }
842
+ aside { border-right:1px solid var(--line); padding:16px; overflow:auto; background:var(--panel); }
843
+ section { min-width:0; overflow:auto; padding:16px; }
844
+ label { display:block; color:var(--muted); font-size:12px; margin:10px 0 5px; }
845
+ 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); }
846
+ input:focus, textarea:focus, select:focus { outline:2px solid var(--accent); outline-offset:1px; border-color:transparent; }
847
+ textarea { min-height:82px; resize:vertical; font-family:var(--mono); font-size:12px; }
848
+ 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; }
849
+ button:hover { border-color:var(--muted); background:oklch(29% 0.018 160); }
850
+ button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
851
+ button:active { transform:translateY(1px); }
852
+ .primary { background:oklch(38% 0.07 158); border-color:oklch(48% 0.09 158); color:oklch(96% 0.02 158); }
853
+ .primary:hover { background:oklch(43% 0.085 158); border-color:var(--accent); }
854
+ .grid { display:grid; grid-template-columns: repeat(var(--board-columns, 6), minmax(160px, 1fr)); gap:12px; align-items:start; }
855
+ .overview { display:grid; grid-template-columns: minmax(260px, 1.4fr) minmax(260px, 1fr); gap:12px; margin-bottom:12px; }
856
+ .goalbox, .chainbox { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:88px; }
857
+ .goalbox h2, .chainbox h2 { margin:0 0 8px; color:var(--muted); font-size:12px; font-weight:650; }
858
+ .goalitem { font-size:13px; line-height:1.3; margin:5px 0; }
859
+ .chainitem { display:grid; grid-template-columns:72px 1fr; gap:8px; font-size:12px; line-height:1.3; margin:5px 0; color:var(--muted); }
860
+ .chainitem strong { color:var(--text); font-weight:600; }
861
+ .streams { display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap:12px; margin-bottom:12px; }
862
+ .stream { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:126px; }
863
+ .stream h2 { margin:0 0 8px; font-size:12px; color:var(--text); line-height:1.25; font-weight:500; }
864
+ .streambar { display:flex; height:7px; overflow:hidden; border-radius:999px; background:oklch(15% 0.012 160); border:1px solid var(--line); margin:8px 0; }
865
+ .streambar span { display:block; min-width:2px; }
866
+ .seg-open { background:var(--muted); }
867
+ .seg-doing { background:var(--accent); }
868
+ .seg-review { background:var(--warn); }
869
+ .seg-blocked { background:var(--bad); }
870
+ .streamtask { display:grid; grid-template-columns:64px 1fr; gap:8px; color:var(--muted); font-size:11px; line-height:1.25; margin-top:6px; }
871
+ .streamtask strong { color:var(--text); font-weight:550; }
872
+ .col { background:var(--panel); border:1px solid var(--line); border-radius:8px; min-height:160px; overflow:hidden; }
873
+ .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; }
874
+ .cards { padding:8px; display:flex; flex-direction:column; gap:8px; }
875
+ .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; }
876
+ .card:hover { transform:scale(1.02); border-color:var(--muted); background:oklch(28% 0.018 160); }
877
+ .card.active { border-color:var(--accent); box-shadow:0 0 0 1px oklch(74% 0.115 158 / 0.3); }
878
+ .title { font-size:13px; line-height:1.25; }
879
+ .meta { margin-top:6px; color:var(--muted); font-size:11px; display:flex; gap:6px; flex-wrap:wrap; font-family:var(--mono); }
880
+ .pill { border:1px solid var(--line); border-radius:999px; padding:1px 6px; }
881
+ .why { margin-top:7px; color:var(--muted); font-size:11px; line-height:1.25; }
882
+ .plain { margin-top:6px; font-size:11px; line-height:1.35; color:var(--text); }
883
+ .plain div + div { color:var(--muted); }
884
+ .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; }
885
+ .fact b { color:var(--muted); font-size:11px; display:block; margin-bottom:3px; }
886
+ .room { margin-top:14px; border-top:1px solid var(--line); padding-top:12px; }
887
+ .room h3 { margin:0 0 4px; font-size:14px; }
888
+ .thread { margin:10px 0; display:flex; flex-direction:column; gap:7px; }
889
+ .msg { background:oklch(15% 0.012 160); border:1px solid var(--line); border-radius:7px; padding:8px; font-size:12px; }
890
+ .msg .who { color:var(--muted); font-size:11px; margin-bottom:3px; }
891
+ .actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:10px; }
892
+ .full { grid-column:1 / -1; }
893
+ .empty { color:var(--muted); font-size:12px; padding:10px; }
894
+ /* heartbeat strip */
895
+ .beat { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); font-family:var(--mono); }
896
+ .beat .dot { width:9px; height:9px; border-radius:50%; background:var(--muted); flex:none; }
897
+ .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; }
898
+ .beat.stale .dot { background:var(--bad); }
899
+ .beat b { color:var(--accent); font-weight:600; }
900
+ .beat .warn { color:var(--warn); }
901
+ @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)} }
902
+ @media (prefers-reduced-motion: reduce) { .beat.alive .dot { animation:none; } *, *::before, *::after { transition:none !important; } }
903
+ /* activity feed */
904
+ .activity { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:0; margin-bottom:12px; max-height:46vh; overflow:auto; }
905
+ .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; }
906
+ .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; }
907
+ .ev:last-child { border-bottom:0; }
908
+ .ev:hover { background:oklch(25% 0.016 160); }
909
+ .ev .t { color:var(--muted); font-variant-numeric:tabular-nums; font-size:11px; }
910
+ .ev .src { font-size:11px; border:1px solid var(--line); border-radius:999px; padding:1px 7px; color:var(--muted); text-align:center; }
911
+ .ev .src.pulse { color:var(--accent); border-color:oklch(74% 0.115 158 / 0.45); }
912
+ .ev .src.reward { color:var(--warn); border-color:oklch(81% 0.11 80 / 0.45); }
913
+ .ev .src.xp { color:var(--info); border-color:oklch(75% 0.10 240 / 0.45); }
914
+ .ev .src.mission { color:var(--violet); border-color:oklch(73% 0.10 300 / 0.45); }
915
+ .ev .msg { color:var(--text); min-width:0; }
916
+ .ev .msg .d { color:var(--muted); font-size:11px; }
917
+ .ev.bad .msg { color:var(--bad); }
918
+ .ev .rw { font-variant-numeric:tabular-nums; font-size:11px; color:var(--muted); }
919
+ .ev .rw.pos { color:var(--accent); } .ev .rw.neg { color:var(--bad); }
920
+ @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; } }
921
+ </style>
922
+ </head>
923
+ <body>
924
+ <header>
925
+ <div>
926
+ <h1>Atris Task Factory</h1>
927
+ <div class="beat" id="heartbeat"><span class="dot"></span><span>heartbeat: loading…</span></div>
928
+ </div>
929
+ <button id="refresh">Refresh</button>
930
+ </header>
931
+ <main>
932
+ <aside>
933
+ <form id="create">
934
+ <label>New task</label>
935
+ <textarea id="title" placeholder="Need something done..."></textarea>
936
+ <label>Lane</label>
937
+ <input id="tag" value="tasks">
938
+ <button class="primary full" type="submit" style="margin-top:10px;width:100%">Create task</button>
939
+ </form>
940
+ <div class="room" id="room">
941
+ <div class="empty">Select a task to open its room.</div>
942
+ </div>
943
+ </aside>
944
+ <section>
945
+ <div class="overview" id="overview"></div>
946
+ <div class="activity" id="activity"><h2>Live Stream</h2><div class="empty">waiting for the agent…</div></div>
947
+ <div class="streams" id="streams"></div>
948
+ <div class="grid" id="board"></div>
949
+ </section>
950
+ </main>
951
+ <script>
952
+ const columns = [
953
+ ['backlog', 'Backlog'],
954
+ ['open', 'Open'],
955
+ ['doing', 'Doing'],
956
+ ['review', 'Review'],
957
+ ['blocked', 'Blocked'],
958
+ ['done', 'Done']
959
+ ];
960
+ const planTags = new Set(${JSON.stringify(model.planTags)});
961
+ let state = { tasks: [] };
962
+ let selected = null;
963
+ const $ = (id) => document.getElementById(id);
964
+
965
+ async function api(path, options = {}) {
966
+ const res = await fetch(path, {
967
+ ...options,
968
+ headers: { 'content-type': 'application/json', 'x-atris-token': new URLSearchParams(location.search).get('t') || '', ...(options.headers || {}) }
969
+ });
970
+ const data = await res.json();
971
+ if (!res.ok || data.ok === false) throw new Error(data.detail || data.reason || 'request failed');
972
+ return data;
973
+ }
974
+
975
+ function taskColumn(task) {
976
+ if (task.status === 'open') {
977
+ const metadata = task.metadata || {};
978
+ const tag = String(task.tag || '').trim().toLowerCase().replace(/\\s+/g, '-');
979
+ const stage = String(metadata.stage || '').trim().toLowerCase().replace(/\\s+/g, '-');
980
+ const planned = planTags.has(tag) || planTags.has(stage) || metadata.verify || metadata.goal || metadata.loop || metadata.cron || metadata.next_run_at;
981
+ return planned ? 'open' : 'backlog';
982
+ }
983
+ if (task.status === 'claimed') return 'doing';
984
+ if (task.status === 'review') return 'review';
985
+ const reviewed = task.latest_event_type === 'reviewed' || !!(task.review && (task.review.reward != null || task.review.proof || task.review.lesson || task.review.next_task));
986
+ if (task.status === 'failed' && reviewed) return 'done';
987
+ if (task.status === 'failed') return 'blocked';
988
+ if (task.status === 'done' && !reviewed) return 'review';
989
+ return 'done';
990
+ }
991
+
992
+ function esc(s) {
993
+ return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
994
+ }
995
+
996
+ function fmtTime(ms) {
997
+ try { return new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
998
+ catch (e) { return ''; }
999
+ }
1000
+
1001
+ function renderHeartbeat(hb) {
1002
+ const el = $('heartbeat');
1003
+ el.className = 'beat ' + (hb.state || 'idle');
1004
+ if (hb.state === 'stale') {
1005
+ el.innerHTML = '<span class="dot"></span><span class="warn">stale</span><span>· ' + esc(hb.stale_reason || '') + ' · ' + (hb.total_ticks || 0) + ' ticks</span>';
1006
+ return;
1007
+ }
1008
+ if (!hb.total_ticks) {
1009
+ el.innerHTML = '<span class="dot"></span><span>idle · no ticks yet · run: atris pulse tick</span>';
1010
+ return;
1011
+ }
1012
+ const age = hb.last_tick_age_min == null ? '' : (hb.last_tick_age_min === 0 ? 'just now' : hb.last_tick_age_min + 'm ago');
1013
+ el.innerHTML = '<span class="dot"></span><b>alive</b>'
1014
+ + '<span>· last tick ' + age + '</span>'
1015
+ + (hb.last_what ? '<span>· ' + esc(hb.last_what) + '</span>' : '')
1016
+ + '<span>· ' + hb.total_ticks + ' ticks, reward ' + (hb.reward_sum || 0) + '</span>';
1017
+ }
1018
+
1019
+ function renderActivity(events) {
1020
+ const el = $('activity');
1021
+ if (!events.length) { el.innerHTML = '<h2>Live Stream</h2><div class="empty">no activity yet, fire a tick: atris pulse tick</div>'; return; }
1022
+ let html = '<h2>Live Stream · ' + events.length + ' events</h2>';
1023
+ for (const e of events) {
1024
+ const rwCls = e.reward == null ? '' : (e.reward > 0 ? 'pos' : (e.reward < 0 ? 'neg' : ''));
1025
+ const rw = e.reward == null ? '' : (e.reward > 0 ? '+' + e.reward : '' + e.reward);
1026
+ html += '<div class="ev ' + (e.status === 'bad' ? 'bad' : '') + '">'
1027
+ + '<span class="t">' + (e.ms ? fmtTime(e.ms) : '') + '</span>'
1028
+ + '<span class="src ' + esc(e.source) + '">' + esc(e.source) + '</span>'
1029
+ + '<span class="msg">' + esc(e.title) + (e.detail ? ' <span class="d">' + esc(e.detail) + '</span>' : '') + '</span>'
1030
+ + '<span class="rw ' + rwCls + '">' + rw + '</span>'
1031
+ + '</div>';
1032
+ }
1033
+ el.innerHTML = html;
1034
+ }
1035
+
1036
+ async function loadStream() {
1037
+ const data = await api('/api/stream');
1038
+ renderHeartbeat(data.heartbeat || {});
1039
+ renderActivity(data.events || []);
1040
+ }
1041
+
1042
+ async function load() {
1043
+ const data = await api('/api/tasks');
1044
+ state = data.projection;
1045
+ render();
1046
+ loadStream().catch(() => {});
1047
+ }
1048
+
1049
+ function render() {
1050
+ renderOverview();
1051
+ renderStreams();
1052
+ const board = $('board');
1053
+ board.style.setProperty('--board-columns', columns.length);
1054
+ board.innerHTML = '';
1055
+ for (const [key, label] of columns) {
1056
+ const tasks = state.tasks.filter((task) => taskColumn(task) === key);
1057
+ const col = document.createElement('div');
1058
+ col.className = 'col';
1059
+ col.innerHTML = '<h2><span>' + label + '</span><span>' + tasks.length + '</span></h2><div class="cards"></div>';
1060
+ const cards = col.querySelector('.cards');
1061
+ if (!tasks.length) cards.innerHTML = '<div class="empty">No tasks</div>';
1062
+ for (const task of tasks) cards.appendChild(card(task));
1063
+ board.appendChild(col);
1064
+ }
1065
+ renderRoom();
1066
+ }
1067
+
1068
+ function taskById(id) {
1069
+ return state.tasks.find((task) => task.id === id) || null;
1070
+ }
1071
+
1072
+ function renderOverview() {
1073
+ const active = state.tasks.filter((task) => task.status !== 'done');
1074
+ const reviewed = state.tasks.filter((task) => task.latest_event_type === 'reviewed');
1075
+ const goals = state.goals && state.goals.items || [];
1076
+ const goalHtml = goals.length
1077
+ ? goals.slice(0, 4).map((goal) => '<div class="goalitem"></div>').join('')
1078
+ : '<div class="empty">No atris/goals.md found. Add goals to give tasks a north star.</div>';
1079
+ const latest = reviewed.slice(0, 3);
1080
+ const chainHtml = latest.length
1081
+ ? latest.map((task) => '<div class="chainitem"><span>' + (task.display_id || task.id.slice(0, 8)) + '</span><strong></strong></div>').join('')
1082
+ : '<div class="empty">Complete a task with proof to start the chain.</div>';
1083
+ $('overview').innerHTML = [
1084
+ '<div class="goalbox"><h2>Goals</h2>' + goalHtml + '</div>',
1085
+ '<div class="chainbox"><h2>Compounding Chain</h2><div class="chainitem"><span>active</span><strong>' + active.length + ' open loops</strong></div>' + chainHtml + '</div>'
1086
+ ].join('');
1087
+ $('overview').querySelectorAll('.goalitem').forEach((el, i) => { el.textContent = goals[i]; });
1088
+ $('overview').querySelectorAll('.chainbox .chainitem strong').forEach((el, i) => {
1089
+ if (i === 0) return;
1090
+ const task = latest[i - 1];
1091
+ const what = task.explanation && task.explanation.what_changes || task.title;
1092
+ el.textContent = (task.review && task.review.next_task) ? what + ' -> ' + task.review.next_task : what;
1093
+ });
1094
+ }
1095
+
1096
+ function renderStreams() {
1097
+ const streams = (state.streams || []).filter((stream) => stream.active_count || stream.done_count).slice(0, 6);
1098
+ const root = $('streams');
1099
+ if (!streams.length) {
1100
+ root.innerHTML = '';
1101
+ return;
1102
+ }
1103
+ root.innerHTML = streams.map((stream) => {
1104
+ const total = Math.max(1, stream.open_count + stream.doing_count + stream.review_count + stream.blocked_count);
1105
+ const widths = {
1106
+ open: Math.max(0, Math.round(stream.open_count / total * 100)),
1107
+ doing: Math.max(0, Math.round(stream.doing_count / total * 100)),
1108
+ review: Math.max(0, Math.round(stream.review_count / total * 100)),
1109
+ blocked: Math.max(0, Math.round(stream.blocked_count / total * 100))
1110
+ };
1111
+ const tasks = stream.tasks.filter((task) => task.status !== 'done').slice(0, 3);
1112
+ const taskHtml = tasks.length
1113
+ ? tasks.map((task) => '<div class="streamtask"><span>' + (task.display_id || task.id.slice(0, 8)) + '</span><strong></strong></div>').join('')
1114
+ : '<div class="empty">No active tasks in this stream.</div>';
1115
+ return [
1116
+ '<div class="stream">',
1117
+ '<h2></h2>',
1118
+ '<div class="meta"><span class="pill">' + stream.active_count + ' active</span><span class="pill">' + stream.done_count + ' done</span></div>',
1119
+ '<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>',
1120
+ taskHtml,
1121
+ '</div>'
1122
+ ].join('');
1123
+ }).join('');
1124
+ root.querySelectorAll('.stream h2').forEach((el, i) => { el.textContent = streams[i].objective; });
1125
+ root.querySelectorAll('.stream').forEach((streamEl, i) => {
1126
+ const tasks = streams[i].tasks.filter((task) => task.status !== 'done').slice(0, 3);
1127
+ streamEl.querySelectorAll('.streamtask strong').forEach((el, idx) => {
1128
+ el.textContent = tasks[idx].explanation && tasks[idx].explanation.what_changes || tasks[idx].title;
1129
+ });
1130
+ });
1131
+ }
1132
+
1133
+ function card(task) {
1134
+ const btn = document.createElement('button');
1135
+ btn.className = 'card' + (selected === task.id ? ' active' : '');
1136
+ btn.onclick = () => { selected = task.id; render(); };
1137
+ const owner = task.claimed_by ? '@' + task.claimed_by : 'unowned';
1138
+ btn.innerHTML = '<div class="title"></div><div class="plain"></div>'
1139
+ + '<div class="meta"><span class="pill"></span><span class="pill"></span><span class="pill"></span></div><div class="why"></div>';
1140
+ const plain = task.explanation || {};
1141
+ btn.querySelector('.title').textContent = plain.what_changes || task.title;
1142
+ const plainRows = [];
1143
+ plainRows.push('Why it matters: ' + (plain.why_it_matters || 'No reason recorded yet.'));
1144
+ plainRows.push('Done looks like: ' + (plain.done_looks_like || 'Proof of the work, then review.'));
1145
+ const plainBox = btn.querySelector('.plain');
1146
+ for (const row of plainRows) {
1147
+ const line = document.createElement('div');
1148
+ line.textContent = row;
1149
+ plainBox.appendChild(line);
1150
+ }
1151
+ const pills = btn.querySelectorAll('.pill');
1152
+ pills[0].textContent = task.display_id || task.id.slice(0, 8);
1153
+ pills[1].textContent = owner;
1154
+ pills[2].textContent = 'v' + task.current_version;
1155
+ btn.querySelector('.why').textContent = 'Technical details: ' + task.title;
1156
+ return btn;
1157
+ }
1158
+
1159
+ function renderRoom() {
1160
+ const task = state.tasks.find((t) => t.id === selected);
1161
+ const room = $('room');
1162
+ if (!task) {
1163
+ room.innerHTML = '<div class="empty">Select a task to open its room.</div>';
1164
+ return;
1165
+ }
1166
+ const messages = task.messages.map((m) => '<div class="msg"><div class="who">' + (m.actor || 'unknown') + ' / v' + m.version + '</div><div></div></div>').join('');
1167
+ const parent = task.lineage && task.lineage.parent_title ? task.lineage.parent_title : 'none';
1168
+ 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');
1169
+ room.innerHTML = [
1170
+ '<h3></h3>',
1171
+ '<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>',
1172
+ '<div class="fact"><b>What changes</b><div id="taskWhatChanges"></div></div>',
1173
+ '<div class="fact"><b>Why it matters</b><div id="taskWhyItMatters"></div></div>',
1174
+ '<div class="fact"><b>Done looks like</b><div id="taskDoneLooksLike"></div></div>',
1175
+ '<div class="fact"><b>Approval</b><div id="taskApproval"></div></div>',
1176
+ '<div class="fact"><b>Technical details</b><div id="taskTechnicalDetails"></div></div>',
1177
+ '<div class="fact"><b>Goal</b><div id="taskGoal"></div></div>',
1178
+ '<div class="fact"><b>Lineage</b><div id="taskLineage"></div></div>',
1179
+ '<div class="fact"><b>Result</b><div id="taskHappened"></div></div>',
1180
+ '<div class="fact"><b>How I checked</b><div id="taskChecked"></div></div>',
1181
+ '<div class="fact"><b>What I tested</b><div id="taskTested"></div></div>',
1182
+ '<div class="fact"><b>Decision</b><div id="taskDecision"></div></div>',
1183
+ '<div class="fact"><b>Proof / lesson</b><div id="taskProof"></div></div>',
1184
+ '<div class="thread">' + (messages || '<div class="empty">No thread yet.</div>') + '</div>',
1185
+ '<label>Add context</label><textarea id="note" placeholder="Decision, blocker, context, update..."></textarea>',
1186
+ '<label>Proof</label><input id="proof" placeholder="npm test, PR link, screenshot, blocked reason...">',
1187
+ '<label>Lesson</label><textarea id="lesson" placeholder="What did this task teach us?"></textarea>',
1188
+ '<label>Next task</label><input id="nextTask" placeholder="Optional next sharper task">',
1189
+ '<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>'
1190
+ ].join('');
1191
+ const plain = task.explanation || {};
1192
+ const approval = task.approval || {};
1193
+ room.querySelector('h3').textContent = plain.what_changes || task.title;
1194
+ $('taskWhatChanges').textContent = plain.what_changes || task.title;
1195
+ $('taskWhyItMatters').textContent = plain.why_it_matters || 'No reason recorded yet.';
1196
+ $('taskDoneLooksLike').textContent = plain.done_looks_like || 'Proof of the work, then review.';
1197
+ $('taskApproval').textContent = approval.approve && approval.approve.enabled
1198
+ ? approval.question
1199
+ : (approval.approve && approval.approve.blocked_reason) || 'Nothing to approve yet.';
1200
+ $('taskTechnicalDetails').textContent = task.title;
1201
+ $('taskGoal').textContent = task.objective || 'No matching goal yet.';
1202
+ $('taskLineage').textContent = 'parent: ' + parent + ' / next: ' + children;
1203
+ const result = task.review && task.review.result || {};
1204
+ const landing = task.review && task.review.landing || {
1205
+ happened: result.changed,
1206
+ checked: result.checked,
1207
+ tested: task.review && task.review.proof ? 'Proof is attached below.' : '',
1208
+ decision: result.accept,
1209
+ };
1210
+ $('taskHappened').textContent = landing.happened || (task.review && task.review.summary) || 'No review result yet.';
1211
+ $('taskChecked').textContent = landing.checked || 'No check yet.';
1212
+ $('taskTested').textContent = landing.tested || 'No test recorded yet.';
1213
+ $('taskDecision').textContent = landing.decision || 'No accept action yet.';
1214
+ $('taskProof').textContent = task.review && (task.review.proof || task.review.lesson)
1215
+ ? ((task.review.proof || 'no proof') + ' / ' + (task.review.lesson || 'no lesson'))
1216
+ : 'No proof yet.';
1217
+ room.querySelectorAll('.msg div:last-child').forEach((el, i) => { el.textContent = task.messages[i].content; });
1218
+ const canApprove = Boolean(approval.approve && approval.approve.enabled);
1219
+ const planned = task.metadata && task.metadata.stage === 'plan';
1220
+ $('finish').textContent = canApprove
1221
+ ? approval.approve.label
1222
+ : task.status === 'claimed' ? 'Move to Review' : 'Approval not ready';
1223
+ $('finish').disabled = !canApprove && task.status !== 'claimed';
1224
+ $('claim').onclick = () => mutate('/api/tasks/' + task.id + '/claim', { owner: 'operator' });
1225
+ $('requestChange').disabled = !(approval.request_change && approval.request_change.enabled);
1226
+ $('requestChange').textContent = approval.request_change && approval.request_change.label || 'Ask for a change';
1227
+ $('requestChange').onclick = () => {
1228
+ const note = $('note').value.trim();
1229
+ if (!note) { $('note').focus(); return; }
1230
+ const action = task.status === 'review' ? 'revise' : planned ? 'backlog' : 'message';
1231
+ const body = action === 'message' ? { actor: 'operator', content: note } : { actor: 'operator', note: note, reason: note };
1232
+ mutate('/api/tasks/' + task.id + '/' + action, body);
1233
+ };
1234
+ $('saveNote').onclick = () => mutate('/api/tasks/' + task.id + '/message', { actor: 'operator', content: $('note').value });
1235
+ $('finish').onclick = () => {
1236
+ const proof = $('proof').value.trim();
1237
+ const lesson = $('lesson').value.trim();
1238
+ const nextTask = $('nextTask').value.trim();
1239
+ const payload = { actor: 'operator' };
1240
+ if (proof) payload.proof = proof;
1241
+ if (lesson) payload.lesson = lesson;
1242
+ if (nextTask) payload.next = nextTask;
1243
+ if (task.status === 'review' && canApprove) {
1244
+ payload.createNext = Boolean(nextTask || (task.review && task.review.next_task));
1245
+ mutate('/api/tasks/' + task.id + '/accept', payload);
1246
+ } else if (planned && canApprove) {
1247
+ mutate('/api/tasks/' + task.id + '/do', { actor: task.metadata.assigned_to || task.claimed_by || 'operator', first_move: task.metadata.first_move });
1248
+ } else {
1249
+ mutate('/api/tasks/' + task.id + '/ready', payload);
1250
+ }
1251
+ };
1252
+ }
1253
+
1254
+ async function mutate(path, body) {
1255
+ await api(path, { method: 'POST', body: JSON.stringify(body) });
1256
+ await load();
1257
+ }
1258
+
1259
+ $('create').onsubmit = async (e) => {
1260
+ e.preventDefault();
1261
+ const title = $('title').value.trim();
1262
+ if (!title) return;
1263
+ const data = await api('/api/tasks', { method: 'POST', body: JSON.stringify({ title, tag: $('tag').value || 'tasks' }) });
1264
+ selected = data.task_id;
1265
+ $('title').value = '';
1266
+ await load();
1267
+ };
1268
+ $('refresh').onclick = load;
1269
+ load();
1270
+ setInterval(load, 2500);
1271
+ </script>
1272
+ </body>
1273
+ </html>`;
1274
+ }
1275
+
1276
+ function taskBoardHtml() {
1277
+ return taskBoardTemplate(taskBoardViewModel());
1278
+ }
1279
+
1280
+ function readJsonBody(req) {
1281
+ if (String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json') {
1282
+ return Promise.reject(Object.assign(new Error('json required'), { statusCode: 415 }));
1283
+ }
1284
+ return new Promise((resolve, reject) => {
1285
+ let size = 0;
1286
+ const chunks = [];
1287
+ req.on('data', chunk => {
1288
+ if (size > 1_000_000) return;
1289
+ size += chunk.length;
1290
+ if (size > 1_000_000) {
1291
+ reject(Object.assign(new Error('body too large'), { statusCode: 413 }));
1292
+ return;
1293
+ }
1294
+ chunks.push(chunk);
1295
+ });
1296
+ req.on('end', () => {
1297
+ if (size > 1_000_000) return;
1298
+ const body = Buffer.concat(chunks).toString('utf8');
1299
+ if (!body.trim()) return resolve({});
1300
+ try {
1301
+ const value = JSON.parse(body);
1302
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('object required');
1303
+ resolve(value);
1304
+ } catch (e) { reject(Object.assign(e, { statusCode: 400 })); }
1305
+ });
1306
+ req.on('error', reject);
1307
+ });
1308
+ }
1309
+
1310
+ function sendJson(res, status, value, headers = {}) {
1311
+ res.writeHead(status, {
1312
+ 'content-type': 'application/json; charset=utf-8',
1313
+ 'cache-control': 'no-store',
1314
+ 'referrer-policy': 'no-referrer',
1315
+ 'x-content-type-options': 'nosniff',
1316
+ ...headers,
1317
+ });
1318
+ res.end(JSON.stringify(value, null, 2));
1319
+ }
1320
+
1321
+ function sendHtml(res, value) {
1322
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'referrer-policy': 'no-referrer', 'x-content-type-options': 'nosniff' });
1323
+ res.end(value);
1324
+ }
1325
+
1326
+ module.exports = {
1327
+ DEFAULT_OWNER,
1328
+ AGENT_CERTIFICATION_REVIEW_PASSES,
1329
+ RESULT_SAVED_TEXT_LIMIT,
1330
+ REVIEW_LANE_LOOP_DEFAULT_MAX_STEPS,
1331
+ REVIEW_LANE_LOOP_MAX_STEPS,
1332
+ REVIEW_LANE_RUN_DEFAULT_MAX_RUNS,
1333
+ REVIEW_LANE_RUN_MAX_RUNS,
1334
+ PENDING_REVIEW_CHAT_STOP_REASON,
1335
+ PROOF_BOUNDARY_BLOCKED_ACTION,
1336
+ PROOF_BOUNDARY_BLOCKED_REASON,
1337
+ MISSION_XP_END_TO_END_REASON,
1338
+ MISSION_XP_END_TO_END_DETAIL,
1339
+ READY_RESULT_TEACHING,
1340
+ STATUS_PLAN_TAGS,
1341
+ TASK_QUEUE_COLUMN_ORDER,
1342
+ TASK_QUEUE_COLUMN_LABELS,
1343
+ TASK_REVIEW_STATE_LANES,
1344
+ TASK_REVIEW_STATE_ALIASES,
1345
+ AGENT_ENV_MARKERS,
1346
+ WORKER_ENV_MARKERS,
1347
+ TASK_BOARD_COLUMNS,
1348
+ getTaskDb,
1349
+ warnIfTaskTitleNeedsOperatorWhy,
1350
+ taskUsageText,
1351
+ taskUsageLines,
1352
+ help,
1353
+ flag,
1354
+ taskScopeEverywhere,
1355
+ scopedWorkspaceRoot,
1356
+ hasEmptyFlagValue,
1357
+ wantsJson,
1358
+ parseAcceptReward,
1359
+ validHumanActorFlag,
1360
+ agentProofOnlyMode,
1361
+ workerEnvMarker,
1362
+ failAgentProofOnly,
1363
+ writeStdoutSync,
1364
+ printJson,
1365
+ refreshCareerXpProjection,
1366
+ refreshCareerXpAfterReview,
1367
+ compactCareerXpProjection,
1368
+ jsonModeActive,
1369
+ failTask,
1370
+ refuseCandidatePolicyGate,
1371
+ proofFlagValue,
1372
+ resultSentenceIssue,
1373
+ readyResultDetail,
1374
+ requireResultSentence,
1375
+ weakProofDetail,
1376
+ requireMeaningfulTaskProof,
1377
+ requireRanNamedProofCommand,
1378
+ sendProofIssue,
1379
+ textFlag,
1380
+ explanationFlags,
1381
+ landingFlags,
1382
+ readyMergeFlags,
1383
+ normalizedLandingSentence,
1384
+ defaultLandingSentenceForTitle,
1385
+ landingNeedsDayOnePm,
1386
+ warnIfLandingNeedsDayOnePm,
1387
+ requireExplicitLandingDayOnePm,
1388
+ numericFlag,
1389
+ meaningfulTaskProofIssue,
1390
+ goldenPathMissionXpTask,
1391
+ debugUnreadableReceipt,
1392
+ receiptTextForProof,
1393
+ missionReceiptResultForProof,
1394
+ missionXpEndToEndProofIssue,
1395
+ missionXpProofBoundaryEvaluation,
1396
+ positional,
1397
+ taskFromProjection,
1398
+ taskRef,
1399
+ normalizeTaskLookupRef,
1400
+ taskLookupRefs,
1401
+ resolveProjectionTaskRef,
1402
+ normalizedStatusPart,
1403
+ taskIsPlannedOpen,
1404
+ taskColumn,
1405
+ taskHasReview,
1406
+ taskBoardViewModel,
1407
+ taskBoardTemplate,
1408
+ taskBoardHtml,
1409
+ readJsonBody,
1410
+ sendJson,
1411
+ sendHtml,
1412
+ };