atris 3.15.13 → 3.15.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +84 -8
- package/README.md +5 -1
- package/atris/AGENTS.md +46 -1
- package/atris/CLAUDE.md +36 -1
- package/atris/GEMINI.md +14 -1
- package/atris/atris.md +12 -1
- package/atris/atrisDev.md +3 -2
- package/atris/context/README.md +11 -0
- package/atris/features/company-brain-sync/validate.md +5 -5
- package/atris/learnings.jsonl +1 -0
- package/atris/policies/atris-design.md +2 -0
- package/atris/skills/aeo/SKILL.md +2 -2
- package/atris/skills/atris/SKILL.md +15 -62
- package/atris/skills/design/SKILL.md +2 -0
- package/atris/skills/imessage/SKILL.md +19 -2
- package/atris/skills/loop/SKILL.md +6 -5
- package/atris/skills/magic-inbox/SKILL.md +1 -1
- package/atris/team/_template/MEMBER.md +23 -1
- package/atris/team/brainstormer/START_HERE.md +6 -0
- package/atris/team/executor/MEMBER.md +13 -0
- package/atris/team/executor/START_HERE.md +6 -0
- package/atris/team/launcher/START_HERE.md +6 -0
- package/atris/team/mission-lead/MEMBER.md +39 -0
- package/atris/team/mission-lead/MISSION.md +33 -0
- package/atris/team/mission-lead/START_HERE.md +6 -0
- package/atris/team/navigator/MEMBER.md +11 -0
- package/atris/team/navigator/START_HERE.md +6 -0
- package/atris/team/opus-overnight/MEMBER.md +39 -0
- package/atris/team/opus-overnight/MISSION.md +61 -0
- package/atris/team/opus-overnight/START_HERE.md +6 -0
- package/atris/team/opus-overnight/STEERING.md +35 -0
- package/atris/team/researcher/START_HERE.md +6 -0
- package/atris/team/validator/MEMBER.md +26 -6
- package/atris/team/validator/START_HERE.md +6 -0
- package/atris/wiki/concepts/agent-activation-contract.md +79 -0
- package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
- package/atris/wiki/index.md +27 -0
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
- package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
- package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
- package/atris.md +49 -13
- package/bin/atris.js +660 -22
- package/commands/activate.js +12 -3
- package/commands/aeo.js +1 -1
- package/commands/align.js +10 -10
- package/commands/analytics.js +9 -4
- package/commands/app.js +2 -0
- package/commands/apps.js +276 -0
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +74 -5
- package/commands/brain.js +536 -61
- package/commands/brainstorm.js +12 -12
- package/commands/business-sync.js +142 -24
- package/commands/clean.js +9 -6
- package/commands/codex-goal.js +311 -0
- package/commands/errors.js +11 -1
- package/commands/feedback.js +55 -17
- package/commands/fork.js +2 -2
- package/commands/gm.js +376 -0
- package/commands/init.js +80 -3
- package/commands/integrations.js +524 -0
- package/commands/learn.js +25 -16
- package/commands/lesson.js +41 -0
- package/commands/lifecycle.js +2 -2
- package/commands/member.js +2416 -9
- package/commands/mission.js +1776 -0
- package/commands/now.js +48 -7
- package/commands/play.js +425 -0
- package/commands/publish.js +2 -1
- package/commands/pull.js +72 -29
- package/commands/push.js +199 -17
- package/commands/review.js +51 -13
- package/commands/skill.js +2 -2
- package/commands/soul.js +19 -13
- package/commands/status.js +6 -1
- package/commands/sync.js +5 -4
- package/commands/task.js +1041 -147
- package/commands/terminal.js +5 -5
- package/commands/verify.js +7 -5
- package/commands/visualize.js +7 -0
- package/commands/wiki.js +53 -16
- package/commands/workflow.js +298 -54
- package/commands/workspace-clean.js +1 -1
- package/commands/worktree.js +468 -0
- package/commands/xp.js +1608 -0
- package/lib/manifest.js +34 -4
- package/lib/scorecard.js +3 -2
- package/lib/task-db.js +408 -27
- package/lib/todo-fallback.js +28 -2
- package/lib/todo.js +5 -3
- package/package.json +23 -2
- package/utils/update-check.js +51 -1
package/commands/task.js
CHANGED
|
@@ -12,6 +12,28 @@ const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
|
|
|
12
12
|
|| process.env.USER
|
|
13
13
|
|| os.userInfo().username
|
|
14
14
|
|| 'unknown';
|
|
15
|
+
const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
|
|
16
|
+
|
|
17
|
+
const STATUS_PLAN_TAGS = new Set([
|
|
18
|
+
'agent',
|
|
19
|
+
'autopilot',
|
|
20
|
+
'cron',
|
|
21
|
+
'endgame',
|
|
22
|
+
'execute',
|
|
23
|
+
'explore',
|
|
24
|
+
'feature',
|
|
25
|
+
'goal',
|
|
26
|
+
'goal-step',
|
|
27
|
+
'loop',
|
|
28
|
+
'plan',
|
|
29
|
+
'planned',
|
|
30
|
+
'schedule',
|
|
31
|
+
'scheduled',
|
|
32
|
+
'shape',
|
|
33
|
+
'shaping',
|
|
34
|
+
'ui',
|
|
35
|
+
'ux',
|
|
36
|
+
]);
|
|
15
37
|
|
|
16
38
|
let taskDbModule = null;
|
|
17
39
|
|
|
@@ -35,43 +57,64 @@ function getTaskDb() {
|
|
|
35
57
|
}
|
|
36
58
|
}
|
|
37
59
|
|
|
38
|
-
function
|
|
39
|
-
|
|
60
|
+
function taskUsageText() {
|
|
61
|
+
return `
|
|
40
62
|
atris task - durable local task state (SQLite, gitignored)
|
|
41
63
|
|
|
42
64
|
atris task Show the task desk
|
|
43
65
|
atris task new "<title>" Create a task
|
|
44
66
|
atris task next Claim/show the next open task
|
|
45
67
|
atris task say <id> "<message>" Add context to a task
|
|
46
|
-
atris task
|
|
68
|
+
atris task ready <id> --proof "..." Agent proof ready; native goal can complete
|
|
69
|
+
atris task accept <id> [--proof "..."] Human accepts proof, marks done
|
|
70
|
+
atris task revise <id> --note "..." Send reviewed work back to Do
|
|
47
71
|
|
|
48
|
-
atris task add "<title>" [--tag <tag>]
|
|
72
|
+
atris task add "<title>" [--tag <tag>] [--goal-id <id>] Create a task
|
|
49
73
|
atris task delegate "<title>" --to <id> Create an assigned task
|
|
50
74
|
atris task day [--json] Show today's owner-grouped task list
|
|
51
75
|
atris task list [--all] [--status <s>] List tasks (default: this workspace)
|
|
52
76
|
atris task claim <id> [--as <owner>] Atomic claim
|
|
53
77
|
atris task note <id> "<message>" Append dialogue/context to a task
|
|
54
78
|
atris task show <id> [--json] Show a task card + dialogue
|
|
55
|
-
atris task done <id> [--failed]
|
|
79
|
+
atris task done <id> [--failed] [--proof "..."] Mark complete (or failed), optionally reviewed
|
|
80
|
+
atris task finish <id> [--proof "..."] Legacy alias for done
|
|
56
81
|
atris task review <id> --reward <n> Write review event + RSI episode
|
|
57
|
-
atris task status [--json]
|
|
82
|
+
atris task status [--json] [--history] Compact live status for web/Swarlo
|
|
58
83
|
atris task setup [--import-todo] Create/refresh task projection
|
|
59
84
|
atris task serve [--port <n>] Open local task factory board
|
|
60
85
|
atris task sync --dry-run Plan cloud/Swarlo task sync writes
|
|
61
86
|
atris task import <file> One-shot import from TODO.md
|
|
62
|
-
atris task events [id]
|
|
87
|
+
atris task events [id] [--limit <n>] Print recent task events
|
|
88
|
+
atris task events --all Print the full append-only ledger
|
|
63
89
|
atris task export [--out <file>] Write web/desktop JSON projection
|
|
64
|
-
atris task render [--out <file>] Regenerate TODO.md view from state
|
|
90
|
+
atris task render [--out <file>] Regenerate compact TODO.md view from state
|
|
65
91
|
atris task where Print db path + workspace scope
|
|
66
92
|
atris task help This help
|
|
67
93
|
|
|
94
|
+
Confidence Gate:
|
|
95
|
+
Before plan/do/review advances, find loopholes, patch them with proof,
|
|
96
|
+
verifier, owner, rollback, or name the residual risk.
|
|
97
|
+
|
|
68
98
|
Env:
|
|
69
99
|
ATRIS_TASKS_DB Override db path (default ~/.atris/tasks.db)
|
|
70
100
|
ATRIS_AGENT_ID Owner id for claim/done (default: $USER)
|
|
71
101
|
|
|
102
|
+
Refs:
|
|
103
|
+
Human views use semantic refs like OBL-18. Commands accept OBL-18,
|
|
104
|
+
OBL18, full 26-char task IDs, and any unique legacy prefix. JSON/API
|
|
105
|
+
keep the full id as canonical and also expose display_id + legacy_ref.
|
|
106
|
+
|
|
72
107
|
Headless:
|
|
73
108
|
Add --json to task commands for machine-readable output and stable automation.
|
|
74
|
-
`.trim()
|
|
109
|
+
`.trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function taskUsageLines() {
|
|
113
|
+
return taskUsageText().split('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function help() {
|
|
117
|
+
console.log(taskUsageText());
|
|
75
118
|
}
|
|
76
119
|
|
|
77
120
|
function flag(args, name) {
|
|
@@ -84,12 +127,56 @@ function hasFlag(args, name) {
|
|
|
84
127
|
return args.indexOf(name) !== -1;
|
|
85
128
|
}
|
|
86
129
|
|
|
130
|
+
function hasEmptyFlagValue(args, name) {
|
|
131
|
+
const i = args.indexOf(name);
|
|
132
|
+
return i !== -1 && args[i + 1] === '';
|
|
133
|
+
}
|
|
134
|
+
|
|
87
135
|
function wantsJson(args) {
|
|
88
136
|
return hasFlag(args, '--json');
|
|
89
137
|
}
|
|
90
138
|
|
|
139
|
+
function parseAcceptReward(value, { defaultValue = 1 } = {}) {
|
|
140
|
+
if (value === undefined || value === null || value === true) return { ok: true, value: defaultValue };
|
|
141
|
+
const numeric = Number(value);
|
|
142
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
143
|
+
return { ok: false, reason: 'invalid_reward' };
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, value: numeric };
|
|
146
|
+
}
|
|
147
|
+
|
|
91
148
|
function printJson(value) {
|
|
92
|
-
|
|
149
|
+
const buffer = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
150
|
+
const retryWait = new Int32Array(new SharedArrayBuffer(4));
|
|
151
|
+
let offset = 0;
|
|
152
|
+
while (offset < buffer.length) {
|
|
153
|
+
try {
|
|
154
|
+
offset += fs.writeSync(process.stdout.fd, buffer, offset, buffer.length - offset);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err && err.code === 'EAGAIN') {
|
|
157
|
+
Atomics.wait(retryWait, 0, 0, 10);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function refreshCareerXpProjection(workspaceRoot) {
|
|
166
|
+
if (!workspaceRoot) return null;
|
|
167
|
+
try {
|
|
168
|
+
const { collectLocalXpProjection } = require('../commands/xp');
|
|
169
|
+
return collectLocalXpProjection(['--workspace', workspaceRoot]);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: error && error.message ? error.message : String(error),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function refreshCareerXpAfterReview(reviewed) {
|
|
179
|
+
return refreshCareerXpProjection(reviewed?.episode?.workspace_root);
|
|
93
180
|
}
|
|
94
181
|
|
|
95
182
|
function jsonModeActive() {
|
|
@@ -98,7 +185,7 @@ function jsonModeActive() {
|
|
|
98
185
|
|
|
99
186
|
function failTask(label, reason, detail, exitCode = 2) {
|
|
100
187
|
if (jsonModeActive()) {
|
|
101
|
-
console.
|
|
188
|
+
console.log(JSON.stringify({
|
|
102
189
|
ok: false,
|
|
103
190
|
command: label,
|
|
104
191
|
reason,
|
|
@@ -133,6 +220,28 @@ function taskFromProjection(projection, id) {
|
|
|
133
220
|
return projection.tasks.find(t => t.id === id) || null;
|
|
134
221
|
}
|
|
135
222
|
|
|
223
|
+
function taskRef(taskOrId) {
|
|
224
|
+
if (!taskOrId) return 'TASK';
|
|
225
|
+
if (typeof taskOrId === 'string') return taskOrId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 8);
|
|
226
|
+
return taskOrId.display_id || taskOrId.legacy_ref || taskRef(taskOrId.id);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function normalizeTaskLookupRef(value) {
|
|
230
|
+
return String(value || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function taskLookupRefs(task) {
|
|
234
|
+
if (!task) return [];
|
|
235
|
+
return [task.id, task.display_id, task.legacy_ref, taskRef(task)]
|
|
236
|
+
.map(normalizeTaskLookupRef)
|
|
237
|
+
.filter(Boolean);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function resolveProjectionTaskRef(ref, taskByRef) {
|
|
241
|
+
const key = normalizeTaskLookupRef(ref);
|
|
242
|
+
return key ? taskByRef.get(key) || null : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
136
245
|
function createNextTaskIfRequested(taskDb, db, args, currentTask, title) {
|
|
137
246
|
const nextTitle = String(title || '').trim();
|
|
138
247
|
if (!hasFlag(args, '--create-next') || !nextTitle) return null;
|
|
@@ -196,14 +305,95 @@ function readGoalSources(root = process.cwd()) {
|
|
|
196
305
|
return { path: null, goals: [] };
|
|
197
306
|
}
|
|
198
307
|
|
|
308
|
+
function reviewSummary(task, payload = {}) {
|
|
309
|
+
const metadata = task.metadata || {};
|
|
310
|
+
const explicit = payload.summary
|
|
311
|
+
|| payload.meaning
|
|
312
|
+
|| metadata.review_summary
|
|
313
|
+
|| metadata.review_meaning
|
|
314
|
+
|| metadata.plain_language_summary
|
|
315
|
+
|| metadata.human_summary;
|
|
316
|
+
if (explicit) return clipStatusText(explicit, 240);
|
|
317
|
+
|
|
318
|
+
const title = String(task.title || 'this task').replace(/\s+/g, ' ').trim();
|
|
319
|
+
const plainTitle = title ? title.charAt(0).toLowerCase() + title.slice(1) : 'this task';
|
|
320
|
+
const careerText = [
|
|
321
|
+
task.tag,
|
|
322
|
+
metadata.goal_id,
|
|
323
|
+
metadata.goal_objective,
|
|
324
|
+
metadata.review_goal,
|
|
325
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
326
|
+
if (
|
|
327
|
+
careerText.includes('career-xp')
|
|
328
|
+
|| careerText.includes('career xp')
|
|
329
|
+
|| careerText.includes('agent-xp')
|
|
330
|
+
|| careerText.includes('agent xp')
|
|
331
|
+
) {
|
|
332
|
+
if (task.status === 'done') {
|
|
333
|
+
return `This is accepted AgentXP work: ${plainTitle} is done and has a proof receipt.`;
|
|
334
|
+
}
|
|
335
|
+
if (task.status === 'review') {
|
|
336
|
+
return `This is AgentXP review: ${plainTitle} is agent-complete; accept only if the proof is real.`;
|
|
337
|
+
}
|
|
338
|
+
return `This explains what accepting ${plainTitle} would make real for AgentXP.`;
|
|
339
|
+
}
|
|
340
|
+
if (task.status === 'done') {
|
|
341
|
+
return `This is the accepted outcome: ${plainTitle} is done and counted as real work.`;
|
|
342
|
+
}
|
|
343
|
+
if (task.status === 'review') {
|
|
344
|
+
return `This is the human checkpoint: ${plainTitle} is agent-complete and needs acceptance before it counts as done.`;
|
|
345
|
+
}
|
|
346
|
+
return `This explains what accepting ${plainTitle} would make real.`;
|
|
347
|
+
}
|
|
348
|
+
|
|
199
349
|
function taskReviewSummary(task) {
|
|
200
|
-
const reviewed = (task.events || []).slice().reverse().find(e => e.event_type === 'reviewed');
|
|
350
|
+
const reviewed = (task.events || []).slice().reverse().find(e => e.event_type === 'reviewed' || e.event_type === 'proof_ready' || e.event_type === 'revision_requested');
|
|
201
351
|
const payload = reviewed && reviewed.payload || {};
|
|
352
|
+
const metadata = task.metadata || {};
|
|
353
|
+
if (!reviewed && !metadata.approval_status && !metadata.agent_review_pass_count && !metadata.human_revision_count && !metadata.agent_certified) return null;
|
|
354
|
+
if (reviewed && reviewed.event_type === 'revision_requested') {
|
|
355
|
+
return {
|
|
356
|
+
summary: reviewSummary(task, payload),
|
|
357
|
+
reward: null,
|
|
358
|
+
proof: null,
|
|
359
|
+
lesson: null,
|
|
360
|
+
next_task: null,
|
|
361
|
+
approval_status: metadata.approval_status || payload.approval_status || 'revise',
|
|
362
|
+
agent_review_pass_count: null,
|
|
363
|
+
agent_certified: false,
|
|
364
|
+
agent_certification_policy: null,
|
|
365
|
+
human_revision_count: metadata.human_revision_count || payload.revision_count || null,
|
|
366
|
+
human_revision_note: metadata.human_revision_note || payload.note || null,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const reviewPassCount = Number(metadata.agent_review_pass_count || payload.review_pass_count || 0);
|
|
370
|
+
const agentCertified = metadata.agent_certified === true
|
|
371
|
+
|| payload.agent_certified === true
|
|
372
|
+
|| reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES;
|
|
373
|
+
const reviewedEventHas = (key) => reviewed && reviewed.event_type === 'reviewed'
|
|
374
|
+
&& Object.prototype.hasOwnProperty.call(payload, key);
|
|
375
|
+
const clearedReviewFields = new Set(Array.isArray(payload.cleared_review_fields) ? payload.cleared_review_fields : []);
|
|
376
|
+
const readyField = (key, metadataKey) => {
|
|
377
|
+
if (reviewedEventHas(key)) {
|
|
378
|
+
if (payload[key]) return payload[key];
|
|
379
|
+
if (key === 'proof' || !clearedReviewFields.has(key)) return metadata[metadataKey] || null;
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return payload[key] || metadata[metadataKey] || null;
|
|
383
|
+
};
|
|
202
384
|
return {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
385
|
+
summary: reviewSummary(task, payload),
|
|
386
|
+
reward: reviewed && reviewed.event_type === 'reviewed' && payload.reward !== undefined ? payload.reward : null,
|
|
387
|
+
proof: readyField('proof', 'latest_agent_proof'),
|
|
388
|
+
lesson: readyField('lesson', 'latest_agent_lesson'),
|
|
389
|
+
next_task: readyField('next_task', 'latest_agent_next_task'),
|
|
390
|
+
approval_status: metadata.approval_status || (task.status === 'review' ? 'pending' : null),
|
|
391
|
+
agent_review_pass_count: reviewPassCount || null,
|
|
392
|
+
agent_certified: agentCertified,
|
|
393
|
+
agent_certification_policy: metadata.agent_certification_policy
|
|
394
|
+
|| payload.agent_certification_policy
|
|
395
|
+
|| (agentCertified ? `${AGENT_CERTIFICATION_REVIEW_PASSES}_agent_review_passes` : null),
|
|
396
|
+
human_revision_count: metadata.human_revision_count || null,
|
|
207
397
|
};
|
|
208
398
|
}
|
|
209
399
|
|
|
@@ -212,10 +402,26 @@ function taskAssignee(task) {
|
|
|
212
402
|
return metadata.assigned_to || task.claimed_by || null;
|
|
213
403
|
}
|
|
214
404
|
|
|
405
|
+
const GOAL_MATCH_STOPWORDS = new Set([
|
|
406
|
+
'daily',
|
|
407
|
+
'goal',
|
|
408
|
+
'goals',
|
|
409
|
+
'loop',
|
|
410
|
+
'loops',
|
|
411
|
+
'make',
|
|
412
|
+
'task',
|
|
413
|
+
'tasks',
|
|
414
|
+
'work',
|
|
415
|
+
]);
|
|
416
|
+
|
|
215
417
|
function scoreGoalMatch(task, goal) {
|
|
216
418
|
const haystack = `${task.title} ${task.tag || ''}`.toLowerCase();
|
|
217
|
-
const words = String(goal || '').toLowerCase().match(/[a-z0-9]{4,}/g) || []
|
|
218
|
-
|
|
419
|
+
const words = (String(goal || '').toLowerCase().match(/[a-z0-9]{4,}/g) || [])
|
|
420
|
+
.filter(word => !GOAL_MATCH_STOPWORDS.has(word));
|
|
421
|
+
return words.reduce((score, word) => {
|
|
422
|
+
const singular = word.endsWith('s') && word.length > 4 ? word.slice(0, -1) : word;
|
|
423
|
+
return score + (haystack.includes(word) || haystack.includes(singular) ? 1 : 0);
|
|
424
|
+
}, 0);
|
|
219
425
|
}
|
|
220
426
|
|
|
221
427
|
function pickTaskGoal(task, goals) {
|
|
@@ -229,7 +435,27 @@ function pickTaskGoal(task, goals) {
|
|
|
229
435
|
bestScore = score;
|
|
230
436
|
}
|
|
231
437
|
}
|
|
232
|
-
return best;
|
|
438
|
+
return bestScore > 0 ? best : null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function taskBaseObjective(task, goals) {
|
|
442
|
+
const metadata = task && task.metadata || {};
|
|
443
|
+
return task.objective
|
|
444
|
+
|| metadata.goal_objective
|
|
445
|
+
|| metadata.objective
|
|
446
|
+
|| pickTaskGoal(task, goals);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function taskObjective(task, parent, goals, { parentLinkType = null, baseObjectives = new Map() } = {}) {
|
|
450
|
+
const metadata = task && task.metadata || {};
|
|
451
|
+
const explicit = task.objective || metadata.goal_objective || metadata.objective;
|
|
452
|
+
if (explicit) return explicit;
|
|
453
|
+
if (parent) {
|
|
454
|
+
if (parentLinkType === 'parent_task_id') return baseObjectives.get(parent.id) || parent.title;
|
|
455
|
+
if (parentLinkType === 'goal_id') return parent.title;
|
|
456
|
+
return baseObjectives.get(parent.id) || parent.title;
|
|
457
|
+
}
|
|
458
|
+
return pickTaskGoal(task, goals);
|
|
233
459
|
}
|
|
234
460
|
|
|
235
461
|
function buildTaskStreams(tasks, goals) {
|
|
@@ -288,29 +514,42 @@ function buildTaskStreams(tasks, goals) {
|
|
|
288
514
|
function enrichTaskProjection(projection) {
|
|
289
515
|
const root = projection.workspace_root || process.cwd();
|
|
290
516
|
const goalSource = readGoalSources(root);
|
|
291
|
-
const
|
|
517
|
+
const byRef = new Map();
|
|
518
|
+
for (const task of projection.tasks || []) {
|
|
519
|
+
for (const ref of taskLookupRefs(task)) byRef.set(ref, task);
|
|
520
|
+
}
|
|
521
|
+
const baseObjectives = new Map();
|
|
522
|
+
for (const task of projection.tasks || []) {
|
|
523
|
+
const objective = taskBaseObjective(task, goalSource.goals);
|
|
524
|
+
if (objective) baseObjectives.set(task.id, objective);
|
|
525
|
+
}
|
|
292
526
|
const children = new Map();
|
|
293
527
|
for (const task of projection.tasks || []) {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
if (!
|
|
297
|
-
children.
|
|
528
|
+
const metadata = task.metadata || {};
|
|
529
|
+
const parent = resolveProjectionTaskRef(metadata.parent_task_id, byRef) || resolveProjectionTaskRef(metadata.goal_id, byRef);
|
|
530
|
+
if (!parent) continue;
|
|
531
|
+
if (!children.has(parent.id)) children.set(parent.id, []);
|
|
532
|
+
children.get(parent.id).push(task);
|
|
298
533
|
}
|
|
299
534
|
const enrichedTasks = (projection.tasks || []).map(task => {
|
|
300
|
-
const
|
|
301
|
-
const
|
|
535
|
+
const metadata = task.metadata || {};
|
|
536
|
+
const parentFromParentId = resolveProjectionTaskRef(metadata.parent_task_id, byRef);
|
|
537
|
+
const parentFromGoalId = resolveProjectionTaskRef(metadata.goal_id, byRef);
|
|
538
|
+
const parent = parentFromParentId || parentFromGoalId;
|
|
539
|
+
const parentLinkType = parentFromParentId ? 'parent_task_id' : parentFromGoalId ? 'goal_id' : null;
|
|
540
|
+
const parentId = parent ? parent.id : metadata.parent_task_id || null;
|
|
302
541
|
const childTasks = children.get(task.id) || [];
|
|
303
542
|
const review = taskReviewSummary(task);
|
|
304
543
|
return {
|
|
305
544
|
...task,
|
|
306
|
-
objective:
|
|
545
|
+
objective: taskObjective(task, parent, goalSource.goals, { parentLinkType, baseObjectives }),
|
|
307
546
|
review,
|
|
308
547
|
lineage: {
|
|
309
548
|
parent_task_id: parentId,
|
|
310
549
|
parent_title: parent ? parent.title : null,
|
|
311
550
|
child_task_ids: childTasks.map(child => child.id),
|
|
312
551
|
child_titles: childTasks.map(child => child.title),
|
|
313
|
-
next_task_suggestion: review.next_task,
|
|
552
|
+
next_task_suggestion: review ? review.next_task : null,
|
|
314
553
|
},
|
|
315
554
|
};
|
|
316
555
|
});
|
|
@@ -334,12 +573,19 @@ function taskTypeForCloud(task) {
|
|
|
334
573
|
}
|
|
335
574
|
|
|
336
575
|
function taskStateForCloud(task) {
|
|
576
|
+
if (task.status === 'review') return 'doing';
|
|
337
577
|
if (task.status === 'claimed') return 'doing';
|
|
578
|
+
if (task.status === 'failed' && taskHasReview(task)) return 'done';
|
|
338
579
|
if (task.status === 'failed') return 'blocked';
|
|
339
580
|
if (task.status === 'done') return 'done';
|
|
340
581
|
return 'open';
|
|
341
582
|
}
|
|
342
583
|
|
|
584
|
+
function taskNeedsApprovalForCloud(task) {
|
|
585
|
+
const approvalStatus = task?.review?.approval_status || task?.metadata?.approval_status || null;
|
|
586
|
+
return task?.status === 'review' || approvalStatus === 'pending';
|
|
587
|
+
}
|
|
588
|
+
|
|
343
589
|
function ownerMemberIdForCloud(task) {
|
|
344
590
|
const ownerValue = task.claimed_by || taskAssignee(task);
|
|
345
591
|
if (!ownerValue) return null;
|
|
@@ -366,6 +612,10 @@ function taskDescriptionForCloud(task) {
|
|
|
366
612
|
if (reviewed.payload.proof) lines.push('', `Proof: ${reviewed.payload.proof}`);
|
|
367
613
|
if (reviewed.payload.lesson) lines.push(`Lesson: ${reviewed.payload.lesson}`);
|
|
368
614
|
if (reviewed.payload.next_task) lines.push(`Next: ${reviewed.payload.next_task}`);
|
|
615
|
+
} else if (task.review && task.review.proof) {
|
|
616
|
+
lines.push('', `Proof: ${task.review.proof}`);
|
|
617
|
+
if (task.review.lesson) lines.push(`Lesson: ${task.review.lesson}`);
|
|
618
|
+
if (task.review.next_task) lines.push(`Next: ${task.review.next_task}`);
|
|
369
619
|
}
|
|
370
620
|
return lines.join('\n').slice(0, 5000);
|
|
371
621
|
}
|
|
@@ -378,7 +628,7 @@ function cloudPayloadForTask(task, businessId) {
|
|
|
378
628
|
title: String(task.title || '').slice(0, 200),
|
|
379
629
|
description: taskDescriptionForCloud(task),
|
|
380
630
|
owner_member_id: ownerMemberIdForCloud(task),
|
|
381
|
-
needs_approval:
|
|
631
|
+
needs_approval: taskNeedsApprovalForCloud(task),
|
|
382
632
|
metadata: {
|
|
383
633
|
...metadata,
|
|
384
634
|
source: 'atris_cli_task',
|
|
@@ -439,9 +689,106 @@ function latestTaskEvent(task) {
|
|
|
439
689
|
return events.length ? events[events.length - 1] : null;
|
|
440
690
|
}
|
|
441
691
|
|
|
442
|
-
function
|
|
692
|
+
function reviewHandoffForTask(task) {
|
|
693
|
+
const review = task && task.review || {};
|
|
694
|
+
if (task && task.status !== 'review') return null;
|
|
695
|
+
if (review.approval_status !== 'pending') return null;
|
|
696
|
+
const agentCertified = review.agent_certified === true;
|
|
697
|
+
return {
|
|
698
|
+
native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
|
|
699
|
+
career_xp_status: 'pending_human_accept',
|
|
700
|
+
next_action: agentCertified ? 'continue_work' : 'agent_review_again',
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function compactTaskForStatus(task) {
|
|
705
|
+
if (!task) return null;
|
|
706
|
+
const metadata = task.metadata || {};
|
|
707
|
+
const out = {
|
|
708
|
+
id: task.id,
|
|
709
|
+
display_id: task.display_id || null,
|
|
710
|
+
legacy_ref: task.legacy_ref || taskRef(task.id),
|
|
711
|
+
title: clipStatusText(task.title, 140),
|
|
712
|
+
status: task.status,
|
|
713
|
+
updated_at: task.updated_at,
|
|
714
|
+
};
|
|
715
|
+
if (task.tag) out.tag = task.tag;
|
|
716
|
+
if (task.claimed_by) out.claimed_by = task.claimed_by;
|
|
717
|
+
const assignedTo = taskAssignee(task);
|
|
718
|
+
if (assignedTo) out.assigned_to = assignedTo;
|
|
719
|
+
if (task.latest_event_type) out.latest_event_type = task.latest_event_type;
|
|
720
|
+
if (task.objective) out.objective = clipStatusText(task.objective, 180);
|
|
721
|
+
if (task.review) {
|
|
722
|
+
const review = {};
|
|
723
|
+
if (typeof task.review.reward === 'number') review.reward = task.review.reward;
|
|
724
|
+
else if (task.review.reward === null) review.reward = null;
|
|
725
|
+
if (task.review.summary) review.summary = clipStatusText(task.review.summary, 240);
|
|
726
|
+
if (task.review.proof) review.proof = clipStatusText(task.review.proof, 180);
|
|
727
|
+
if (task.review.lesson) review.lesson = clipStatusText(task.review.lesson, 180);
|
|
728
|
+
if (task.review.next_task) review.next_task = clipStatusText(task.review.next_task, 140);
|
|
729
|
+
if (task.review.approval_status) review.approval_status = task.review.approval_status;
|
|
730
|
+
if (task.review.agent_review_pass_count) review.agent_review_pass_count = task.review.agent_review_pass_count;
|
|
731
|
+
if (task.review.agent_certified) review.agent_certified = task.review.agent_certified;
|
|
732
|
+
if (task.review.agent_certification_policy) review.agent_certification_policy = task.review.agent_certification_policy;
|
|
733
|
+
if (task.review.human_revision_count) review.human_revision_count = task.review.human_revision_count;
|
|
734
|
+
const handoff = reviewHandoffForTask(task);
|
|
735
|
+
if (handoff) review.handoff = handoff;
|
|
736
|
+
if (Object.keys(review).length) out.review = review;
|
|
737
|
+
}
|
|
738
|
+
if (task.lineage) {
|
|
739
|
+
const lineage = {};
|
|
740
|
+
if (task.lineage.parent_task_id) lineage.parent_task_id = task.lineage.parent_task_id;
|
|
741
|
+
if (task.lineage.parent_title) lineage.parent_title = clipStatusText(task.lineage.parent_title, 140);
|
|
742
|
+
if (task.lineage.child_task_ids && task.lineage.child_task_ids.length) lineage.child_task_ids = task.lineage.child_task_ids;
|
|
743
|
+
if (task.lineage.next_task_suggestion) lineage.next_task_suggestion = clipStatusText(task.lineage.next_task_suggestion, 140);
|
|
744
|
+
if (Object.keys(lineage).length) out.lineage = lineage;
|
|
745
|
+
}
|
|
746
|
+
const compactMetadata = {};
|
|
747
|
+
for (const key of ['todo_id', 'stage', 'verify', 'delegate_via', 'goal_id', 'goal_objective', 'approval_status', 'agent_review_pass_count', 'agent_certified', 'agent_certification_policy', 'human_revision_count', 'human_revision_note']) {
|
|
748
|
+
if (metadata[key]) compactMetadata[key] = key === 'verify' ? clipStatusText(metadata[key], 180) : metadata[key];
|
|
749
|
+
}
|
|
750
|
+
if (Object.keys(compactMetadata).length) out.metadata = compactMetadata;
|
|
751
|
+
return out;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function compactTaskFromProjection(projection, id) {
|
|
755
|
+
return compactTaskForStatus(taskFromProjection(projection, id));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function compactEventPayload(payload) {
|
|
759
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
760
|
+
const out = {};
|
|
761
|
+
for (const key of ['title', 'status', 'tag', 'content', 'proof', 'lesson', 'reward', 'next_task']) {
|
|
762
|
+
if (payload[key] !== undefined && payload[key] !== null && payload[key] !== '') out[key] = payload[key];
|
|
763
|
+
}
|
|
764
|
+
return Object.keys(out).length ? out : null;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function compactTaskEvent(event) {
|
|
768
|
+
if (!event) return null;
|
|
769
|
+
return {
|
|
770
|
+
event_id: event.event_id,
|
|
771
|
+
task_id: event.task_id,
|
|
772
|
+
version: event.version,
|
|
773
|
+
actor: event.actor || null,
|
|
774
|
+
event_type: event.event_type,
|
|
775
|
+
created_at: event.created_at,
|
|
776
|
+
payload: compactEventPayload(event.payload),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function clipStatusText(value, max = 180) {
|
|
781
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
782
|
+
if (text.length <= max) return text;
|
|
783
|
+
return `${text.slice(0, max - 1)}…`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function taskStatusSummary(projection, { history = false } = {}) {
|
|
443
787
|
const tasks = projection.tasks || [];
|
|
788
|
+
const hiddenDoneCount = Math.max(0, Number(projection.surface && projection.surface.hidden_done_count || 0));
|
|
789
|
+
const fullTaskCount = Math.max(tasks.length + hiddenDoneCount, Number(projection.surface && projection.surface.full_task_count || 0));
|
|
444
790
|
const columns = {
|
|
791
|
+
backlog: tasks.filter(task => taskColumn(task) === 'backlog'),
|
|
445
792
|
plan: tasks.filter(task => taskColumn(task) === 'open'),
|
|
446
793
|
do: tasks.filter(task => taskColumn(task) === 'doing'),
|
|
447
794
|
review: tasks.filter(task => taskColumn(task) === 'review' || taskColumn(task) === 'blocked'),
|
|
@@ -449,10 +796,10 @@ function taskStatusSummary(projection) {
|
|
|
449
796
|
};
|
|
450
797
|
const active = [...columns.do, ...columns.review, ...columns.plan];
|
|
451
798
|
const lastUpdated = tasks.reduce((max, task) => Math.max(max, Number(task.updated_at || 0)), 0);
|
|
452
|
-
const swarloFeed = tasks
|
|
799
|
+
const swarloFeed = history ? tasks
|
|
453
800
|
.flatMap(task => (task.events || []).map(event => ({
|
|
454
801
|
task_id: task.id,
|
|
455
|
-
task_title: task.title,
|
|
802
|
+
task_title: clipStatusText(task.title, 120),
|
|
456
803
|
actor: event.actor || task.claimed_by || null,
|
|
457
804
|
kind: event.event_type === 'claimed'
|
|
458
805
|
? 'claim'
|
|
@@ -460,8 +807,11 @@ function taskStatusSummary(projection) {
|
|
|
460
807
|
? 'result'
|
|
461
808
|
: 'note',
|
|
462
809
|
channel: task.tag || 'tasks',
|
|
463
|
-
content:
|
|
464
|
-
|
|
810
|
+
content: clipStatusText(
|
|
811
|
+
event.payload && (event.payload.content || event.payload.proof || event.payload.lesson)
|
|
812
|
+
|| humanEventType(event.event_type),
|
|
813
|
+
180,
|
|
814
|
+
),
|
|
465
815
|
created_at: event.created_at,
|
|
466
816
|
metadata: {
|
|
467
817
|
swarlo: {
|
|
@@ -472,23 +822,24 @@ function taskStatusSummary(projection) {
|
|
|
472
822
|
},
|
|
473
823
|
})))
|
|
474
824
|
.sort((a, b) => b.created_at - a.created_at)
|
|
475
|
-
.slice(0, 12);
|
|
476
|
-
|
|
825
|
+
.slice(0, 12) : [];
|
|
826
|
+
const status = {
|
|
477
827
|
schema: 'atris.task_status.v1',
|
|
478
828
|
generated_at: projection.generated_at,
|
|
479
829
|
workspace_root: projection.workspace_root,
|
|
480
830
|
goals: projection.goals || { source_path: null, items: [] },
|
|
481
831
|
counts: {
|
|
482
|
-
total:
|
|
483
|
-
active:
|
|
832
|
+
total: fullTaskCount,
|
|
833
|
+
active: columns.plan.length + columns.do.length + columns.review.length,
|
|
834
|
+
backlog: columns.backlog.length,
|
|
484
835
|
plan: columns.plan.length,
|
|
485
836
|
do: columns.do.length,
|
|
486
837
|
review: columns.review.length,
|
|
487
|
-
done:
|
|
838
|
+
done: tasks.filter(task => task.status === 'done' || (task.status === 'failed' && taskHasReview(task))).length + hiddenDoneCount,
|
|
488
839
|
},
|
|
489
|
-
current: columns.do[0] || columns.review[0] || null,
|
|
490
|
-
next: columns.plan[0] || null,
|
|
491
|
-
needs_review: columns.review.slice(0, 5),
|
|
840
|
+
current: compactTaskForStatus(columns.do[0] || columns.review[0] || null),
|
|
841
|
+
next: compactTaskForStatus(columns.plan[0] || null),
|
|
842
|
+
needs_review: columns.review.slice(0, 5).map(compactTaskForStatus),
|
|
492
843
|
streams: (projection.streams || []).slice(0, 8).map(stream => ({
|
|
493
844
|
objective: stream.objective,
|
|
494
845
|
active_count: stream.active_count,
|
|
@@ -498,38 +849,75 @@ function taskStatusSummary(projection) {
|
|
|
498
849
|
review_count: stream.review_count,
|
|
499
850
|
blocked_count: stream.blocked_count,
|
|
500
851
|
})),
|
|
501
|
-
last_event: active.map(task => ({ task, event: latestTaskEvent(task) })).filter(row => row.event)
|
|
502
|
-
.sort((a, b) => b.event.created_at - a.event.created_at)[0] || null,
|
|
503
852
|
last_updated_at: lastUpdated ? new Date(lastUpdated).toISOString() : null,
|
|
504
|
-
|
|
853
|
+
};
|
|
854
|
+
if (history) {
|
|
855
|
+
status.last_event = active.map(task => ({ task: compactTaskForStatus(task), event: compactTaskEvent(latestTaskEvent(task)) })).filter(row => row.event)
|
|
856
|
+
.sort((a, b) => b.event.created_at - a.event.created_at)[0] || null;
|
|
857
|
+
status.swarlo = {
|
|
505
858
|
feed: swarloFeed,
|
|
506
859
|
realtime_contract: {
|
|
507
860
|
claim: 'Swarlo claim -> canonical task state=doing + lease metadata',
|
|
508
861
|
report_done: 'Swarlo report(done) -> canonical task state=done + proof metadata',
|
|
509
862
|
web: 'atrisos-web reads canonical tasks through /api/agent/:id/tasks or /api/business/* and live activity through public business/Swarlo posts',
|
|
510
863
|
},
|
|
511
|
-
}
|
|
512
|
-
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
return status;
|
|
513
867
|
}
|
|
514
868
|
|
|
515
869
|
function humanEventType(type) {
|
|
516
870
|
return String(type || 'event').replace(/_/g, ' ');
|
|
517
871
|
}
|
|
518
872
|
|
|
873
|
+
function taskEventSummary(event) {
|
|
874
|
+
const payload = event && event.payload || {};
|
|
875
|
+
const raw = payload.content || payload.proof || payload.lesson || payload.title || payload.status || humanEventType(event && event.event_type);
|
|
876
|
+
return clipStatusText(raw, 140);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function formatTaskEventCompact(event, refById = new Map()) {
|
|
880
|
+
const actor = event.actor ? ` @${event.actor}` : '';
|
|
881
|
+
const when = event.created_at ? new Date(Number(event.created_at)).toISOString() : '';
|
|
882
|
+
return `${when}\t${event.event_type.padEnd(9)}\t${refById.get(event.task_id) || taskRef(event.task_id)}${actor}\t${taskEventSummary(event)}`;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function normalizedStatusPart(value) {
|
|
886
|
+
return String(value || '').trim().toLowerCase().replace(/\s+/g, '-');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function taskIsPlannedOpen(task) {
|
|
890
|
+
const metadata = task && task.metadata || {};
|
|
891
|
+
const tag = normalizedStatusPart(task && task.tag);
|
|
892
|
+
const stage = normalizedStatusPart(metadata.stage);
|
|
893
|
+
return STATUS_PLAN_TAGS.has(tag)
|
|
894
|
+
|| STATUS_PLAN_TAGS.has(stage)
|
|
895
|
+
|| Boolean(metadata.verify || metadata.goal || metadata.loop || metadata.cron || metadata.next_run_at);
|
|
896
|
+
}
|
|
897
|
+
|
|
519
898
|
function formatTaskLine(task) {
|
|
520
899
|
if (!task) return 'none';
|
|
521
900
|
const owner = task.claimed_by ? ` @${task.claimed_by}` : '';
|
|
522
901
|
const assigned = !task.claimed_by && taskAssignee(task) ? ` -> ${taskAssignee(task)}` : '';
|
|
523
902
|
const tag = task.tag ? ` #${task.tag}` : '';
|
|
524
|
-
return `${task
|
|
903
|
+
return `${taskRef(task)}${owner}${assigned}${tag} ${task.title}`;
|
|
525
904
|
}
|
|
526
905
|
|
|
527
906
|
function cmdStatus(args) {
|
|
528
907
|
const all = hasFlag(args, '--all');
|
|
908
|
+
const history = hasFlag(args, '--history');
|
|
529
909
|
const taskDb = getTaskDb();
|
|
530
910
|
const db = taskDb.open();
|
|
531
|
-
const
|
|
532
|
-
const
|
|
911
|
+
const compact = writeDefaultProjection(taskDb, db, { all });
|
|
912
|
+
const projection = history
|
|
913
|
+
? enrichTaskProjection(taskDb.taskProjection(db, {
|
|
914
|
+
workspaceRoot: all ? null : taskDb.workspaceRoot(),
|
|
915
|
+
limit: 500,
|
|
916
|
+
includeHistory: true,
|
|
917
|
+
}))
|
|
918
|
+
: compact.projection;
|
|
919
|
+
const outPath = compact.outPath;
|
|
920
|
+
const status = taskStatusSummary(projection, { history });
|
|
533
921
|
if (wantsJson(args)) {
|
|
534
922
|
printJson({
|
|
535
923
|
ok: true,
|
|
@@ -541,14 +929,14 @@ function cmdStatus(args) {
|
|
|
541
929
|
}
|
|
542
930
|
console.log('TASK STATUS');
|
|
543
931
|
console.log(`workspace ${status.workspace_root || '(all)'}`);
|
|
544
|
-
console.log(`plan ${status.counts.plan} / do ${status.counts.do} / review ${status.counts.review} / done ${status.counts.done}`);
|
|
932
|
+
console.log(`plan ${status.counts.plan} / do ${status.counts.do} / review ${status.counts.review} / backlog ${status.counts.backlog} / done ${status.counts.done}`);
|
|
545
933
|
console.log(`current ${formatTaskLine(status.current)}`);
|
|
546
934
|
console.log(`next ${formatTaskLine(status.next)}`);
|
|
547
935
|
if (status.needs_review.length) {
|
|
548
936
|
console.log('review');
|
|
549
937
|
for (const task of status.needs_review.slice(0, 3)) console.log(` ${formatTaskLine(task)}`);
|
|
550
938
|
}
|
|
551
|
-
console.log(`
|
|
939
|
+
if (history) console.log(`history feed ${status.swarlo.feed.length} event${status.swarlo.feed.length === 1 ? '' : 's'}`);
|
|
552
940
|
}
|
|
553
941
|
|
|
554
942
|
function resolveTaskRef(taskDb, db, ref) {
|
|
@@ -556,8 +944,18 @@ function resolveTaskRef(taskDb, db, ref) {
|
|
|
556
944
|
if (!token) return { ok: false, reason: 'missing' };
|
|
557
945
|
const exact = taskDb.getTask(db, token);
|
|
558
946
|
if (exact) return { ok: true, id: exact.id, row: exact };
|
|
559
|
-
const
|
|
560
|
-
const
|
|
947
|
+
const normalized = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(token) : token.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
948
|
+
const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot: taskDb.workspaceRoot() }));
|
|
949
|
+
const seen = new Set();
|
|
950
|
+
const matches = rows.filter(r => {
|
|
951
|
+
const id = String(r.id || '').toUpperCase();
|
|
952
|
+
const display = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.display_id) : String(r.display_id || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
953
|
+
const legacy = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.legacy_ref) : String(r.legacy_ref || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
954
|
+
const matched = id.startsWith(normalized) || display === normalized || legacy === normalized;
|
|
955
|
+
if (!matched || seen.has(r.id)) return false;
|
|
956
|
+
seen.add(r.id);
|
|
957
|
+
return true;
|
|
958
|
+
});
|
|
561
959
|
if (matches.length === 1) return { ok: true, id: matches[0].id, row: matches[0] };
|
|
562
960
|
if (matches.length > 1) return { ok: false, reason: 'ambiguous', matches };
|
|
563
961
|
return { ok: false, reason: 'not_found' };
|
|
@@ -575,9 +973,14 @@ function requireTaskId(taskDb, db, ref, label) {
|
|
|
575
973
|
}
|
|
576
974
|
}
|
|
577
975
|
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
|
|
976
|
+
function workspaceRefRows(taskDb, db, all = false) {
|
|
977
|
+
return taskDb.listTasks(db, { workspaceRoot: all ? null : taskDb.workspaceRoot() });
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function renderTaskDesk(rows, refRows = rows) {
|
|
981
|
+
const displayRows = getTaskDb().withTaskDisplayRefs(rows, refRows);
|
|
982
|
+
const active = displayRows.filter(r => r.status !== 'done');
|
|
983
|
+
const done = displayRows.filter(r => r.status === 'done');
|
|
581
984
|
if (rows.length === 0) {
|
|
582
985
|
console.log('No tasks yet.');
|
|
583
986
|
console.log('Start with: atris task new "Ship the smallest useful thing"');
|
|
@@ -589,7 +992,7 @@ function renderTaskDesk(rows) {
|
|
|
589
992
|
const owner = r.claimed_by ? ` @${r.claimed_by}` : '';
|
|
590
993
|
const assigned = !r.claimed_by && taskAssignee(r) ? ` -> ${taskAssignee(r)}` : '';
|
|
591
994
|
const tag = r.tag ? ` #${r.tag}` : '';
|
|
592
|
-
console.log(`${r.status.padEnd(7)} ${r
|
|
995
|
+
console.log(`${r.status.padEnd(7)} ${taskRef(r)}${owner}${assigned}${tag}`);
|
|
593
996
|
console.log(` ${r.title}`);
|
|
594
997
|
}
|
|
595
998
|
if (active.length === 0) console.log('clear no active tasks');
|
|
@@ -602,10 +1005,14 @@ function cmdAdd(args) {
|
|
|
602
1005
|
const pos = positional(args);
|
|
603
1006
|
const title = pos.join(' ').trim();
|
|
604
1007
|
if (!title) {
|
|
605
|
-
|
|
606
|
-
process.exit(2);
|
|
1008
|
+
failTask('atris task add', 'missing_title', 'title required');
|
|
607
1009
|
}
|
|
608
1010
|
const tag = flag(args, '--tag');
|
|
1011
|
+
const goalId = flag(args, '--goal-id');
|
|
1012
|
+
const goalObjective = flag(args, '--goal-objective') || flag(args, '--goal');
|
|
1013
|
+
const metadata = {};
|
|
1014
|
+
if (goalId && goalId !== true) metadata.goal_id = String(goalId);
|
|
1015
|
+
if (goalObjective && goalObjective !== true) metadata.goal_objective = String(goalObjective);
|
|
609
1016
|
const taskDb = getTaskDb();
|
|
610
1017
|
const db = taskDb.open();
|
|
611
1018
|
const ws = taskDb.workspaceRoot();
|
|
@@ -613,8 +1020,10 @@ function cmdAdd(args) {
|
|
|
613
1020
|
title,
|
|
614
1021
|
tag: typeof tag === 'string' ? tag : null,
|
|
615
1022
|
workspaceRoot: ws,
|
|
1023
|
+
metadata: Object.keys(metadata).length ? metadata : null,
|
|
616
1024
|
});
|
|
617
1025
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1026
|
+
const task = compactTaskFromProjection(projection, result.id);
|
|
618
1027
|
if (wantsJson(args)) {
|
|
619
1028
|
printJson({
|
|
620
1029
|
ok: true,
|
|
@@ -622,21 +1031,21 @@ function cmdAdd(args) {
|
|
|
622
1031
|
task_id: result.id,
|
|
623
1032
|
inserted: result.inserted !== false,
|
|
624
1033
|
projection_path: outPath,
|
|
625
|
-
task
|
|
1034
|
+
task,
|
|
626
1035
|
});
|
|
627
1036
|
return;
|
|
628
1037
|
}
|
|
629
|
-
console.log(`${
|
|
1038
|
+
console.log(`${taskRef(task)}\t${title}`);
|
|
630
1039
|
}
|
|
631
1040
|
|
|
632
|
-
function delegateHandoff(
|
|
633
|
-
const
|
|
1041
|
+
function delegateHandoff(task, owner, via, tag) {
|
|
1042
|
+
const ref = taskRef(task);
|
|
634
1043
|
const handoff = {
|
|
635
|
-
command: `atris task claim ${
|
|
1044
|
+
command: `atris task claim ${ref} --as ${owner}`,
|
|
636
1045
|
};
|
|
637
1046
|
if (via === 'swarlo') {
|
|
638
1047
|
handoff.swarlo = {
|
|
639
|
-
task_key:
|
|
1048
|
+
task_key: task.id,
|
|
640
1049
|
action: 'claim',
|
|
641
1050
|
channel: tag || 'tasks',
|
|
642
1051
|
assignee: owner,
|
|
@@ -650,12 +1059,10 @@ function cmdDelegate(args) {
|
|
|
650
1059
|
const title = pos.join(' ').trim();
|
|
651
1060
|
const owner = flag(args, '--to') || flag(args, '--as');
|
|
652
1061
|
if (!title) {
|
|
653
|
-
|
|
654
|
-
process.exit(2);
|
|
1062
|
+
failTask('atris task delegate', 'missing_title', 'title required');
|
|
655
1063
|
}
|
|
656
1064
|
if (!owner || owner === true) {
|
|
657
|
-
|
|
658
|
-
process.exit(2);
|
|
1065
|
+
failTask('atris task delegate', 'missing_owner', '--to <owner> required');
|
|
659
1066
|
}
|
|
660
1067
|
const viaFlag = flag(args, '--via');
|
|
661
1068
|
const via = viaFlag === 'swarlo' ? 'swarlo' : 'local';
|
|
@@ -683,8 +1090,8 @@ function cmdDelegate(args) {
|
|
|
683
1090
|
taskDb.noteTask(db, { id: result.id, actor: DEFAULT_OWNER, content: note });
|
|
684
1091
|
}
|
|
685
1092
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
686
|
-
const task =
|
|
687
|
-
const handoff = delegateHandoff(
|
|
1093
|
+
const task = compactTaskFromProjection(projection, result.id);
|
|
1094
|
+
const handoff = delegateHandoff(task, String(owner), via, typeof tag === 'string' ? tag : null);
|
|
688
1095
|
if (wantsJson(args)) {
|
|
689
1096
|
printJson({
|
|
690
1097
|
ok: true,
|
|
@@ -700,7 +1107,7 @@ function cmdDelegate(args) {
|
|
|
700
1107
|
return;
|
|
701
1108
|
}
|
|
702
1109
|
const tagText = tag && tag !== true ? ` #${tag}` : '';
|
|
703
|
-
console.log(`delegated ${
|
|
1110
|
+
console.log(`delegated ${taskRef(task)} -> ${owner}${tagText} via=${via}`);
|
|
704
1111
|
console.log(`claim: ${handoff.command}`);
|
|
705
1112
|
if (handoff.swarlo) console.log(`swarlo: ${handoff.swarlo.channel}/${handoff.swarlo.action}`);
|
|
706
1113
|
}
|
|
@@ -739,7 +1146,7 @@ function cmdDay(args) {
|
|
|
739
1146
|
owners: groups.length,
|
|
740
1147
|
open: (projection.tasks || []).filter(task => task.status === 'open').length,
|
|
741
1148
|
claimed: (projection.tasks || []).filter(task => task.status === 'claimed').length,
|
|
742
|
-
review: (projection.tasks || []).filter(task => task.status === 'failed' || (task.status === 'done' && task.review && task.review.reward === null)).length,
|
|
1149
|
+
review: (projection.tasks || []).filter(task => task.status === 'review' || task.status === 'failed' || (task.status === 'done' && task.review && task.review.reward === null)).length,
|
|
743
1150
|
};
|
|
744
1151
|
const date = new Date().toISOString().slice(0, 10);
|
|
745
1152
|
if (wantsJson(args)) {
|
|
@@ -764,7 +1171,7 @@ function cmdDay(args) {
|
|
|
764
1171
|
for (const task of group.tasks.slice(0, 8)) {
|
|
765
1172
|
const tag = task.tag ? ` #${task.tag}` : '';
|
|
766
1173
|
const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
|
|
767
|
-
console.log(` ${task.status.padEnd(7)} ${task
|
|
1174
|
+
console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${task.title}`);
|
|
768
1175
|
}
|
|
769
1176
|
}
|
|
770
1177
|
console.log('');
|
|
@@ -791,7 +1198,7 @@ function cmdHome(args) {
|
|
|
791
1198
|
});
|
|
792
1199
|
return;
|
|
793
1200
|
}
|
|
794
|
-
renderTaskDesk(rows);
|
|
1201
|
+
renderTaskDesk(rows, rows);
|
|
795
1202
|
}
|
|
796
1203
|
|
|
797
1204
|
function cmdList(args) {
|
|
@@ -804,18 +1211,19 @@ function cmdList(args) {
|
|
|
804
1211
|
status: typeof status === 'string' ? status : null,
|
|
805
1212
|
limit: 200,
|
|
806
1213
|
});
|
|
1214
|
+
const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, all));
|
|
807
1215
|
if (wantsJson(args)) {
|
|
808
|
-
printJson({ ok: true, action: 'list', tasks:
|
|
1216
|
+
printJson({ ok: true, action: 'list', tasks: displayRows });
|
|
809
1217
|
return;
|
|
810
1218
|
}
|
|
811
1219
|
if (rows.length === 0) {
|
|
812
1220
|
console.log('(no tasks)');
|
|
813
1221
|
return;
|
|
814
1222
|
}
|
|
815
|
-
for (const r of
|
|
1223
|
+
for (const r of displayRows) {
|
|
816
1224
|
const claim = r.claimed_by ? ` [${r.claimed_by}]` : '';
|
|
817
1225
|
const tag = r.tag ? ` #${r.tag}` : '';
|
|
818
|
-
console.log(`${r.status.padEnd(8)} ${r
|
|
1226
|
+
console.log(`${r.status.padEnd(8)} ${taskRef(r)}${claim}${tag}\t${r.title}`);
|
|
819
1227
|
}
|
|
820
1228
|
}
|
|
821
1229
|
|
|
@@ -823,8 +1231,7 @@ function cmdClaim(args) {
|
|
|
823
1231
|
const pos = positional(args);
|
|
824
1232
|
const id = pos[0];
|
|
825
1233
|
if (!id) {
|
|
826
|
-
|
|
827
|
-
process.exit(2);
|
|
1234
|
+
failTask('atris task claim', 'missing_id', 'id required');
|
|
828
1235
|
}
|
|
829
1236
|
const owner = flag(args, '--as') || DEFAULT_OWNER;
|
|
830
1237
|
const taskDb = getTaskDb();
|
|
@@ -840,12 +1247,22 @@ function cmdClaim(args) {
|
|
|
840
1247
|
task_id: taskId,
|
|
841
1248
|
owner: String(owner),
|
|
842
1249
|
projection_path: outPath,
|
|
843
|
-
task:
|
|
1250
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
844
1251
|
});
|
|
845
1252
|
return;
|
|
846
1253
|
}
|
|
847
|
-
console.log(`claimed ${taskId} as ${owner}`);
|
|
1254
|
+
console.log(`claimed ${taskRef(compactTaskFromProjection(projection, taskId))} as ${owner}`);
|
|
848
1255
|
} else {
|
|
1256
|
+
if (wantsJson(args)) {
|
|
1257
|
+
printJson({
|
|
1258
|
+
ok: false,
|
|
1259
|
+
command: 'atris task claim',
|
|
1260
|
+
reason: result.reason,
|
|
1261
|
+
claimed_by: result.claimed_by || null,
|
|
1262
|
+
detail: `claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
|
|
1263
|
+
});
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
849
1266
|
console.error(`claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
|
|
850
1267
|
process.exit(1);
|
|
851
1268
|
}
|
|
@@ -870,21 +1287,68 @@ function cmdNext(args) {
|
|
|
870
1287
|
task_id: claimed[0].id,
|
|
871
1288
|
owner: String(owner),
|
|
872
1289
|
projection_path: outPath,
|
|
873
|
-
task:
|
|
1290
|
+
task: compactTaskFromProjection(projection, claimed[0].id),
|
|
874
1291
|
});
|
|
875
1292
|
return;
|
|
876
1293
|
}
|
|
877
|
-
console.log(`current ${claimed[0].id
|
|
1294
|
+
console.log(`current ${taskRef(compactTaskFromProjection(projection, claimed[0].id))} @${owner}`);
|
|
878
1295
|
console.log(claimed[0].title);
|
|
879
1296
|
return;
|
|
880
1297
|
}
|
|
1298
|
+
const reviewProjection = writeDefaultProjection(taskDb, db);
|
|
1299
|
+
const reviewTasks = (reviewProjection.projection.tasks || [])
|
|
1300
|
+
.map(compactTaskForStatus)
|
|
1301
|
+
.filter(task => task && task.review && task.review.handoff);
|
|
1302
|
+
const secondReviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again');
|
|
1303
|
+
if (secondReviewTask) {
|
|
1304
|
+
const handoff = secondReviewTask.review.handoff;
|
|
1305
|
+
if (wantsJson(args)) {
|
|
1306
|
+
printJson({
|
|
1307
|
+
ok: true,
|
|
1308
|
+
action: handoff.next_action,
|
|
1309
|
+
task_id: secondReviewTask.id,
|
|
1310
|
+
owner: String(owner),
|
|
1311
|
+
projection_path: reviewProjection.outPath,
|
|
1312
|
+
handoff,
|
|
1313
|
+
review_task: secondReviewTask,
|
|
1314
|
+
});
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
console.log(`${taskRef(secondReviewTask)} needs one more agent review before continuation.`);
|
|
1318
|
+
console.log('Review this task again before claiming new work.');
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
881
1321
|
const open = taskDb.listTasks(db, {
|
|
882
1322
|
workspaceRoot: taskDb.workspaceRoot(),
|
|
883
1323
|
status: 'open',
|
|
884
1324
|
limit: 1,
|
|
885
1325
|
});
|
|
886
1326
|
if (!open.length) {
|
|
887
|
-
const { outPath } =
|
|
1327
|
+
const { projection, outPath } = reviewProjection;
|
|
1328
|
+
const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'continue_work');
|
|
1329
|
+
if (reviewTask) {
|
|
1330
|
+
const handoff = reviewTask.review.handoff;
|
|
1331
|
+
if (wantsJson(args)) {
|
|
1332
|
+
printJson({
|
|
1333
|
+
ok: true,
|
|
1334
|
+
action: handoff.next_action,
|
|
1335
|
+
task_id: handoff.next_action === 'agent_review_again' ? reviewTask.id : null,
|
|
1336
|
+
owner: String(owner),
|
|
1337
|
+
projection_path: outPath,
|
|
1338
|
+
handoff,
|
|
1339
|
+
review_task: reviewTask,
|
|
1340
|
+
});
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
console.log('No open tasks.');
|
|
1344
|
+
console.log(handoff.next_action === 'continue_work'
|
|
1345
|
+
? `${taskRef(reviewTask)} is agent-certified and waiting for human accept.`
|
|
1346
|
+
: `${taskRef(reviewTask)} needs one more agent review before continuation.`);
|
|
1347
|
+
console.log(handoff.next_action === 'continue_work'
|
|
1348
|
+
? 'Continue work elsewhere; AgentXP waits for human accept.'
|
|
1349
|
+
: 'Review this task again before continuing.');
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
888
1352
|
if (wantsJson(args)) {
|
|
889
1353
|
printJson({
|
|
890
1354
|
ok: true,
|
|
@@ -912,11 +1376,11 @@ function cmdNext(args) {
|
|
|
912
1376
|
task_id: open[0].id,
|
|
913
1377
|
owner: String(owner),
|
|
914
1378
|
projection_path: outPath,
|
|
915
|
-
task:
|
|
1379
|
+
task: compactTaskFromProjection(projection, open[0].id),
|
|
916
1380
|
});
|
|
917
1381
|
return;
|
|
918
1382
|
}
|
|
919
|
-
console.log(`next ${open[0].id
|
|
1383
|
+
console.log(`next ${taskRef(compactTaskFromProjection(projection, open[0].id))} @${owner}`);
|
|
920
1384
|
console.log(open[0].title);
|
|
921
1385
|
}
|
|
922
1386
|
|
|
@@ -925,8 +1389,7 @@ function cmdNote(args) {
|
|
|
925
1389
|
const id = pos[0];
|
|
926
1390
|
const content = pos.slice(1).join(' ').trim();
|
|
927
1391
|
if (!id || !content) {
|
|
928
|
-
|
|
929
|
-
process.exit(2);
|
|
1392
|
+
failTask('atris task note', 'missing_args', 'id and message required');
|
|
930
1393
|
}
|
|
931
1394
|
const actor = flag(args, '--as') || DEFAULT_OWNER;
|
|
932
1395
|
const taskDb = getTaskDb();
|
|
@@ -945,37 +1408,45 @@ function cmdNote(args) {
|
|
|
945
1408
|
task_id: taskId,
|
|
946
1409
|
version: result.event.version,
|
|
947
1410
|
projection_path: outPath,
|
|
948
|
-
task:
|
|
1411
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
949
1412
|
});
|
|
950
1413
|
return;
|
|
951
1414
|
}
|
|
952
|
-
console.log(`noted ${taskId} v${result.event.version}`);
|
|
1415
|
+
console.log(`noted ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
|
|
953
1416
|
}
|
|
954
1417
|
|
|
955
1418
|
function cmdShow(args) {
|
|
956
1419
|
const pos = positional(args);
|
|
957
1420
|
const id = pos[0];
|
|
958
1421
|
if (!id) {
|
|
959
|
-
|
|
960
|
-
process.exit(2);
|
|
1422
|
+
failTask('atris task show', 'missing_id', 'id required');
|
|
961
1423
|
}
|
|
962
1424
|
const taskDb = getTaskDb();
|
|
963
1425
|
const db = taskDb.open();
|
|
964
1426
|
const taskId = requireTaskId(taskDb, db, id, 'atris task show');
|
|
965
|
-
const projection = taskDb.taskProjection(db, { taskId });
|
|
1427
|
+
const projection = enrichTaskProjection(taskDb.taskProjection(db, { taskId }));
|
|
966
1428
|
const task = projection.tasks[0];
|
|
967
1429
|
if (!task) {
|
|
968
1430
|
console.error(`task not found: ${id}`);
|
|
969
1431
|
process.exit(1);
|
|
970
1432
|
}
|
|
971
1433
|
if (hasFlag(args, '--json')) {
|
|
972
|
-
|
|
1434
|
+
printJson(task);
|
|
973
1435
|
return;
|
|
974
1436
|
}
|
|
975
1437
|
const owner = task.claimed_by ? ` / ${task.claimed_by}` : '';
|
|
976
1438
|
const tag = task.tag ? ` #${task.tag}` : '';
|
|
977
|
-
console.log(`${task.status.toUpperCase()} ${task
|
|
1439
|
+
console.log(`${task.status.toUpperCase()} ${taskRef(task)} v${task.current_version}${owner}${tag}`);
|
|
978
1440
|
console.log(task.title);
|
|
1441
|
+
if (task.review) {
|
|
1442
|
+
console.log('');
|
|
1443
|
+
if (task.review.summary) console.log(`Summary: ${task.review.summary}`);
|
|
1444
|
+
if (task.review.proof) console.log(`Proof: ${task.review.proof}`);
|
|
1445
|
+
if (task.review.lesson) console.log(`Lesson: ${task.review.lesson}`);
|
|
1446
|
+
if (task.review.next_task) console.log(`Next: ${task.review.next_task}`);
|
|
1447
|
+
if (task.review.approval_status) console.log(`Approval: ${task.review.approval_status}`);
|
|
1448
|
+
if (task.review.agent_certified) console.log(`Agent certified: yes (${task.review.agent_review_pass_count || AGENT_CERTIFICATION_REVIEW_PASSES} reviews)`);
|
|
1449
|
+
}
|
|
979
1450
|
if (task.messages.length) {
|
|
980
1451
|
console.log('');
|
|
981
1452
|
console.log('Dialogue:');
|
|
@@ -990,29 +1461,60 @@ function cmdDone(args) {
|
|
|
990
1461
|
const pos = positional(args);
|
|
991
1462
|
const id = pos[0];
|
|
992
1463
|
if (!id) {
|
|
993
|
-
|
|
994
|
-
process.exit(2);
|
|
1464
|
+
failTask('atris task done', 'missing_id', 'id required');
|
|
995
1465
|
}
|
|
996
1466
|
const failed = hasFlag(args, '--failed');
|
|
997
1467
|
const taskDb = getTaskDb();
|
|
998
1468
|
const db = taskDb.open();
|
|
999
1469
|
const taskId = requireTaskId(taskDb, db, id, 'atris task done');
|
|
1000
|
-
const
|
|
1470
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
1471
|
+
const result = taskDb.doneTask(db, { id: taskId, status: failed ? 'failed' : 'done', actor });
|
|
1001
1472
|
if (result.updated) {
|
|
1473
|
+
const hasReview = hasFlag(args, '--review') || flag(args, '--lesson') || flag(args, '--next') || flag(args, '--proof') || flag(args, '--reward');
|
|
1474
|
+
const review = hasReview ? taskDb.reviewTask(db, {
|
|
1475
|
+
id: taskId,
|
|
1476
|
+
actor,
|
|
1477
|
+
reward: flag(args, '--reward') || (failed ? 0 : 1),
|
|
1478
|
+
lesson: typeof flag(args, '--lesson') === 'string' ? flag(args, '--lesson') : '',
|
|
1479
|
+
nextTask: typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '',
|
|
1480
|
+
proof: typeof flag(args, '--proof') === 'string' ? flag(args, '--proof') : '',
|
|
1481
|
+
careerXpEligible: false,
|
|
1482
|
+
}) : null;
|
|
1483
|
+
const xpProjection = refreshCareerXpAfterReview(review);
|
|
1002
1484
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1003
1485
|
if (wantsJson(args)) {
|
|
1004
1486
|
printJson({
|
|
1005
1487
|
ok: true,
|
|
1006
1488
|
action: failed ? 'failed' : 'done',
|
|
1007
1489
|
task_id: taskId,
|
|
1490
|
+
reviewed: Boolean(review && review.reviewed),
|
|
1491
|
+
reward: review && review.episode ? review.episode.reward.value : null,
|
|
1492
|
+
episode: review && review.episode || null,
|
|
1493
|
+
xp_projection: xpProjection,
|
|
1008
1494
|
projection_path: outPath,
|
|
1009
|
-
task:
|
|
1495
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
1010
1496
|
});
|
|
1011
1497
|
return;
|
|
1012
1498
|
}
|
|
1013
|
-
|
|
1499
|
+
const task = compactTaskFromProjection(projection, taskId);
|
|
1500
|
+
if (review && review.reviewed) {
|
|
1501
|
+
console.log(`${failed ? 'failed' : 'done'} ${taskRef(task)} reward=${review.episode.reward.value}`);
|
|
1502
|
+
} else {
|
|
1503
|
+
console.log(`${failed ? 'failed' : 'done'} ${taskRef(task)}`);
|
|
1504
|
+
}
|
|
1014
1505
|
} else {
|
|
1015
|
-
|
|
1506
|
+
const detail = `done failed: ${taskId} not in open|claimed`;
|
|
1507
|
+
if (wantsJson(args)) {
|
|
1508
|
+
printJson({
|
|
1509
|
+
ok: false,
|
|
1510
|
+
command: 'atris task done',
|
|
1511
|
+
reason: 'not_open_or_claimed',
|
|
1512
|
+
task_id: taskId,
|
|
1513
|
+
detail,
|
|
1514
|
+
});
|
|
1515
|
+
process.exit(1);
|
|
1516
|
+
}
|
|
1517
|
+
console.error(detail);
|
|
1016
1518
|
process.exit(1);
|
|
1017
1519
|
}
|
|
1018
1520
|
}
|
|
@@ -1021,29 +1523,42 @@ function cmdFinish(args) {
|
|
|
1021
1523
|
const pos = positional(args);
|
|
1022
1524
|
const id = pos[0];
|
|
1023
1525
|
if (!id) {
|
|
1024
|
-
|
|
1025
|
-
process.exit(2);
|
|
1526
|
+
failTask('atris task finish', 'missing_id', 'id required');
|
|
1026
1527
|
}
|
|
1027
1528
|
const taskDb = getTaskDb();
|
|
1028
1529
|
const db = taskDb.open();
|
|
1029
1530
|
const taskId = requireTaskId(taskDb, db, id, 'atris task finish');
|
|
1030
1531
|
const currentTask = taskDb.getTask(db, taskId);
|
|
1031
|
-
const
|
|
1532
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
1533
|
+
const done = taskDb.doneTask(db, { id: taskId, status: hasFlag(args, '--failed') ? 'failed' : 'done', actor });
|
|
1032
1534
|
if (!done.updated) {
|
|
1033
|
-
|
|
1535
|
+
const detail = `finish failed: ${taskId} not in open|claimed`;
|
|
1536
|
+
if (wantsJson(args)) {
|
|
1537
|
+
printJson({
|
|
1538
|
+
ok: false,
|
|
1539
|
+
command: 'atris task finish',
|
|
1540
|
+
reason: 'not_open_or_claimed',
|
|
1541
|
+
task_id: taskId,
|
|
1542
|
+
detail,
|
|
1543
|
+
});
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1546
|
+
console.error(detail);
|
|
1034
1547
|
process.exit(1);
|
|
1035
1548
|
}
|
|
1036
1549
|
const hasReview = hasFlag(args, '--review') || flag(args, '--lesson') || flag(args, '--next') || flag(args, '--proof') || flag(args, '--reward');
|
|
1037
1550
|
if (hasReview) {
|
|
1038
1551
|
const result = taskDb.reviewTask(db, {
|
|
1039
1552
|
id: taskId,
|
|
1040
|
-
actor
|
|
1553
|
+
actor,
|
|
1041
1554
|
reward: flag(args, '--reward') || 1,
|
|
1042
1555
|
lesson: typeof flag(args, '--lesson') === 'string' ? flag(args, '--lesson') : '',
|
|
1043
1556
|
nextTask: typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '',
|
|
1044
1557
|
proof: typeof flag(args, '--proof') === 'string' ? flag(args, '--proof') : '',
|
|
1558
|
+
careerXpEligible: false,
|
|
1045
1559
|
});
|
|
1046
1560
|
const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
|
|
1561
|
+
const xpProjection = refreshCareerXpAfterReview(result);
|
|
1047
1562
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1048
1563
|
if (wantsJson(args)) {
|
|
1049
1564
|
printJson({
|
|
@@ -1053,16 +1568,17 @@ function cmdFinish(args) {
|
|
|
1053
1568
|
reviewed: true,
|
|
1054
1569
|
reward: result.episode.reward.value,
|
|
1055
1570
|
episode: result.episode,
|
|
1571
|
+
xp_projection: xpProjection,
|
|
1056
1572
|
next_task_id: nextCreated ? nextCreated.id : null,
|
|
1057
1573
|
projection_path: outPath,
|
|
1058
|
-
projection,
|
|
1059
|
-
|
|
1574
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
1575
|
+
next_task: nextCreated ? compactTaskFromProjection(projection, nextCreated.id) : null,
|
|
1060
1576
|
});
|
|
1061
1577
|
return;
|
|
1062
1578
|
}
|
|
1063
|
-
console.log(`finished ${taskId} reward=${result.episode.reward.value}`);
|
|
1579
|
+
console.log(`finished ${taskRef(compactTaskFromProjection(projection, taskId))} reward=${result.episode.reward.value}`);
|
|
1064
1580
|
if (result.episode.next_task_suggestion) console.log(`next: ${result.episode.next_task_suggestion}`);
|
|
1065
|
-
if (nextCreated) console.log(`created next ${nextCreated.id}`);
|
|
1581
|
+
if (nextCreated) console.log(`created next ${taskRef(compactTaskFromProjection(projection, nextCreated.id))}`);
|
|
1066
1582
|
return;
|
|
1067
1583
|
}
|
|
1068
1584
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
@@ -1073,20 +1589,195 @@ function cmdFinish(args) {
|
|
|
1073
1589
|
task_id: taskId,
|
|
1074
1590
|
reviewed: false,
|
|
1075
1591
|
projection_path: outPath,
|
|
1076
|
-
task:
|
|
1592
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
1077
1593
|
});
|
|
1078
1594
|
return;
|
|
1079
1595
|
}
|
|
1080
|
-
console.log(`finished ${taskId}`);
|
|
1596
|
+
console.log(`finished ${taskRef(compactTaskFromProjection(projection, taskId))}`);
|
|
1081
1597
|
}
|
|
1082
1598
|
|
|
1083
|
-
function
|
|
1599
|
+
function cmdReady(args) {
|
|
1600
|
+
const pos = positional(args);
|
|
1601
|
+
const id = pos[0];
|
|
1602
|
+
if (!id) {
|
|
1603
|
+
console.error('atris task ready: id required');
|
|
1604
|
+
process.exit(2);
|
|
1605
|
+
}
|
|
1606
|
+
const proof = flag(args, '--proof');
|
|
1607
|
+
if (!proof || proof === true) {
|
|
1608
|
+
console.error('atris task ready: --proof required');
|
|
1609
|
+
process.exit(2);
|
|
1610
|
+
}
|
|
1611
|
+
const lesson = flag(args, '--lesson') || '';
|
|
1612
|
+
const nextTask = flag(args, '--next') || '';
|
|
1613
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
1614
|
+
const taskDb = getTaskDb();
|
|
1615
|
+
const db = taskDb.open();
|
|
1616
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task ready');
|
|
1617
|
+
const result = taskDb.readyTask(db, {
|
|
1618
|
+
id: taskId,
|
|
1619
|
+
actor,
|
|
1620
|
+
proof: String(proof),
|
|
1621
|
+
lesson: typeof lesson === 'string' ? lesson : '',
|
|
1622
|
+
nextTask: typeof nextTask === 'string' ? nextTask : '',
|
|
1623
|
+
});
|
|
1624
|
+
if (!result.ready) {
|
|
1625
|
+
console.error(`ready failed: ${result.reason}`);
|
|
1626
|
+
process.exit(1);
|
|
1627
|
+
}
|
|
1628
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1629
|
+
const agentCertified = result.event.payload.agent_certified === true;
|
|
1630
|
+
const handoff = {
|
|
1631
|
+
native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
|
|
1632
|
+
career_xp_status: 'pending_human_accept',
|
|
1633
|
+
next_action: agentCertified ? 'continue_work' : 'agent_review_again',
|
|
1634
|
+
rule: agentCertified
|
|
1635
|
+
? 'Agent double-check complete; continue work. AgentXP waits for human accept.'
|
|
1636
|
+
: 'Proof is in Review; one more agent review pass certifies continuation. AgentXP waits for human accept.',
|
|
1637
|
+
};
|
|
1638
|
+
if (wantsJson(args)) {
|
|
1639
|
+
printJson({
|
|
1640
|
+
ok: true,
|
|
1641
|
+
action: 'ready',
|
|
1642
|
+
task_id: taskId,
|
|
1643
|
+
version: result.event.version,
|
|
1644
|
+
approval_status: 'pending',
|
|
1645
|
+
review_pass_count: result.event.payload.review_pass_count,
|
|
1646
|
+
agent_certified: agentCertified,
|
|
1647
|
+
handoff,
|
|
1648
|
+
projection_path: outPath,
|
|
1649
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
1650
|
+
});
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
console.log(`ready ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version} pending approval`);
|
|
1654
|
+
console.log(handoff.rule);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function cmdAccept(args) {
|
|
1084
1658
|
const pos = positional(args);
|
|
1085
1659
|
const id = pos[0];
|
|
1086
1660
|
if (!id) {
|
|
1087
|
-
console.error('atris task
|
|
1661
|
+
console.error('atris task accept: id required');
|
|
1088
1662
|
process.exit(2);
|
|
1089
1663
|
}
|
|
1664
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
1665
|
+
const reward = flag(args, '--reward');
|
|
1666
|
+
const lessonFlag = flag(args, '--lesson');
|
|
1667
|
+
const nextTaskFlag = flag(args, '--next');
|
|
1668
|
+
const taskDb = getTaskDb();
|
|
1669
|
+
const db = taskDb.open();
|
|
1670
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task accept');
|
|
1671
|
+
const beforeProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId }));
|
|
1672
|
+
const beforeTask = beforeProjection.tasks[0] || null;
|
|
1673
|
+
const proofFlag = flag(args, '--proof');
|
|
1674
|
+
const hasExplicitProof = typeof proofFlag === 'string';
|
|
1675
|
+
const proof = hasExplicitProof
|
|
1676
|
+
? proofFlag
|
|
1677
|
+
: String(beforeTask?.metadata?.latest_agent_proof || '').trim();
|
|
1678
|
+
if (!proof) {
|
|
1679
|
+
console.error('atris task accept: proof required or task must already have fresh proof_ready proof');
|
|
1680
|
+
process.exit(2);
|
|
1681
|
+
}
|
|
1682
|
+
const readyReview = beforeTask?.review || {};
|
|
1683
|
+
const clearLesson = hasEmptyFlagValue(args, '--lesson');
|
|
1684
|
+
const clearNextTask = hasEmptyFlagValue(args, '--next');
|
|
1685
|
+
const lesson = clearLesson
|
|
1686
|
+
? ''
|
|
1687
|
+
: typeof lessonFlag === 'string'
|
|
1688
|
+
? lessonFlag
|
|
1689
|
+
: String(readyReview.lesson || beforeTask?.metadata?.latest_agent_lesson || '');
|
|
1690
|
+
const nextTask = clearNextTask
|
|
1691
|
+
? ''
|
|
1692
|
+
: typeof nextTaskFlag === 'string'
|
|
1693
|
+
? nextTaskFlag
|
|
1694
|
+
: String(readyReview.next_task || beforeTask?.metadata?.latest_agent_next_task || '');
|
|
1695
|
+
const clearedFields = [];
|
|
1696
|
+
if (clearLesson || (typeof lessonFlag === 'string' && !String(lessonFlag).trim())) clearedFields.push('lesson');
|
|
1697
|
+
if (clearNextTask || (typeof nextTaskFlag === 'string' && !String(nextTaskFlag).trim())) clearedFields.push('next_task');
|
|
1698
|
+
const parsedReward = parseAcceptReward(reward);
|
|
1699
|
+
if (!parsedReward.ok) {
|
|
1700
|
+
console.error('atris task accept: reward must be a positive number');
|
|
1701
|
+
process.exit(2);
|
|
1702
|
+
}
|
|
1703
|
+
const done = taskDb.doneTask(db, { id: taskId, status: 'done', actor, allowReview: true });
|
|
1704
|
+
if (!done.updated) {
|
|
1705
|
+
console.error(`accept failed: ${taskId} not open|claimed|review`);
|
|
1706
|
+
process.exit(1);
|
|
1707
|
+
}
|
|
1708
|
+
const reviewed = taskDb.reviewTask(db, {
|
|
1709
|
+
id: taskId,
|
|
1710
|
+
actor,
|
|
1711
|
+
reward: parsedReward.value,
|
|
1712
|
+
lesson,
|
|
1713
|
+
nextTask,
|
|
1714
|
+
proof,
|
|
1715
|
+
careerXpEligible: true,
|
|
1716
|
+
clearedFields,
|
|
1717
|
+
});
|
|
1718
|
+
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
1719
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1720
|
+
if (wantsJson(args)) {
|
|
1721
|
+
printJson({
|
|
1722
|
+
ok: true,
|
|
1723
|
+
action: 'accepted',
|
|
1724
|
+
task_id: taskId,
|
|
1725
|
+
reviewed: true,
|
|
1726
|
+
reward: reviewed.episode.reward.value,
|
|
1727
|
+
episode: reviewed.episode,
|
|
1728
|
+
xp_projection: xpProjection,
|
|
1729
|
+
projection_path: outPath,
|
|
1730
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
1731
|
+
});
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
console.log(`accepted ${taskRef(compactTaskFromProjection(projection, taskId))} reward=${reviewed.episode.reward.value}`);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function cmdRevise(args) {
|
|
1738
|
+
const pos = positional(args);
|
|
1739
|
+
const id = pos[0];
|
|
1740
|
+
if (!id) {
|
|
1741
|
+
console.error('atris task revise: id required');
|
|
1742
|
+
process.exit(2);
|
|
1743
|
+
}
|
|
1744
|
+
const note = flag(args, '--note') || flag(args, '--reason') || pos.slice(1).join(' ');
|
|
1745
|
+
if (!note || note === true) {
|
|
1746
|
+
console.error('atris task revise: --note required');
|
|
1747
|
+
process.exit(2);
|
|
1748
|
+
}
|
|
1749
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
1750
|
+
const taskDb = getTaskDb();
|
|
1751
|
+
const db = taskDb.open();
|
|
1752
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task revise');
|
|
1753
|
+
const result = taskDb.reviseTask(db, { id: taskId, actor, note: String(note) });
|
|
1754
|
+
if (!result.revised) {
|
|
1755
|
+
console.error(`revise failed: ${result.reason}`);
|
|
1756
|
+
process.exit(1);
|
|
1757
|
+
}
|
|
1758
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1759
|
+
if (wantsJson(args)) {
|
|
1760
|
+
printJson({
|
|
1761
|
+
ok: true,
|
|
1762
|
+
action: 'revise',
|
|
1763
|
+
task_id: taskId,
|
|
1764
|
+
version: result.event.version,
|
|
1765
|
+
approval_status: 'revise',
|
|
1766
|
+
revision_count: result.event.payload.revision_count,
|
|
1767
|
+
projection_path: outPath,
|
|
1768
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
1769
|
+
});
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
console.log(`revise ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function cmdReview(args) {
|
|
1776
|
+
const pos = positional(args);
|
|
1777
|
+
const id = pos[0];
|
|
1778
|
+
if (!id) {
|
|
1779
|
+
failTask('atris task review', 'missing_id', 'id required');
|
|
1780
|
+
}
|
|
1090
1781
|
const reward = flag(args, '--reward');
|
|
1091
1782
|
const lesson = flag(args, '--lesson') || '';
|
|
1092
1783
|
const nextTask = flag(args, '--next') || '';
|
|
@@ -1103,12 +1794,14 @@ function cmdReview(args) {
|
|
|
1103
1794
|
lesson: typeof lesson === 'string' ? lesson : '',
|
|
1104
1795
|
nextTask: typeof nextTask === 'string' ? nextTask : '',
|
|
1105
1796
|
proof: typeof proof === 'string' ? proof : '',
|
|
1797
|
+
careerXpEligible: false,
|
|
1106
1798
|
});
|
|
1107
1799
|
if (!result.reviewed) {
|
|
1108
1800
|
console.error(`review failed: ${result.reason}`);
|
|
1109
1801
|
process.exit(1);
|
|
1110
1802
|
}
|
|
1111
1803
|
const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
|
|
1804
|
+
const xpProjection = refreshCareerXpAfterReview(result);
|
|
1112
1805
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1113
1806
|
if (wantsJson(args)) {
|
|
1114
1807
|
printJson({
|
|
@@ -1118,16 +1811,17 @@ function cmdReview(args) {
|
|
|
1118
1811
|
version: result.event.version,
|
|
1119
1812
|
reward: result.episode.reward.value,
|
|
1120
1813
|
episode: result.episode,
|
|
1814
|
+
xp_projection: xpProjection,
|
|
1121
1815
|
next_task_id: nextCreated ? nextCreated.id : null,
|
|
1122
1816
|
projection_path: outPath,
|
|
1123
|
-
projection,
|
|
1124
|
-
|
|
1817
|
+
task: compactTaskFromProjection(projection, taskId),
|
|
1818
|
+
next_task: nextCreated ? compactTaskFromProjection(projection, nextCreated.id) : null,
|
|
1125
1819
|
});
|
|
1126
1820
|
return;
|
|
1127
1821
|
}
|
|
1128
|
-
console.log(`reviewed ${taskId} v${result.event.version} reward=${result.episode.reward.value}`);
|
|
1822
|
+
console.log(`reviewed ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version} reward=${result.episode.reward.value}`);
|
|
1129
1823
|
if (result.episode.next_task_suggestion) console.log(`next: ${result.episode.next_task_suggestion}`);
|
|
1130
|
-
if (nextCreated) console.log(`created next ${nextCreated.id}`);
|
|
1824
|
+
if (nextCreated) console.log(`created next ${taskRef(compactTaskFromProjection(projection, nextCreated.id))}`);
|
|
1131
1825
|
}
|
|
1132
1826
|
|
|
1133
1827
|
function importTodoFile(taskDb, db, target) {
|
|
@@ -1141,6 +1835,7 @@ function importTodoFile(taskDb, db, target) {
|
|
|
1141
1835
|
const all = [
|
|
1142
1836
|
...parsed.backlog.map(t => ({ ...t, importStatus: 'open' })),
|
|
1143
1837
|
...parsed.inProgress.map(t => ({ ...t, importStatus: 'claimed' })),
|
|
1838
|
+
...(parsed.review || []).map(t => ({ ...t, importStatus: 'review' })),
|
|
1144
1839
|
];
|
|
1145
1840
|
let inserted = 0;
|
|
1146
1841
|
let skipped = 0;
|
|
@@ -1154,7 +1849,7 @@ function importTodoFile(taskDb, db, target) {
|
|
|
1154
1849
|
sourceKey: sk,
|
|
1155
1850
|
status: t.importStatus,
|
|
1156
1851
|
claimedBy: t.claimed || null,
|
|
1157
|
-
metadata: { todo_id: t.id, claimed: t.claimed, stage: t.stage, verify: t.verify },
|
|
1852
|
+
metadata: { todo_id: t.id, todo_tags: t.tags || [], claimed: t.claimed, stage: t.stage, verify: t.verify },
|
|
1158
1853
|
});
|
|
1159
1854
|
if (result.inserted) inserted++; else skipped++;
|
|
1160
1855
|
}
|
|
@@ -1207,25 +1902,48 @@ function cmdEvents(args) {
|
|
|
1207
1902
|
const pos = positional(args);
|
|
1208
1903
|
let taskId = pos[0] || null;
|
|
1209
1904
|
const all = hasFlag(args, '--all');
|
|
1905
|
+
const rawLimit = flag(args, '--limit');
|
|
1906
|
+
const explicitLimit = rawLimit && rawLimit !== true ? Number(rawLimit) : null;
|
|
1907
|
+
const defaultRecentLimit = 24;
|
|
1908
|
+
const limit = explicitLimit || (taskId ? 500 : (all ? null : defaultRecentLimit));
|
|
1210
1909
|
const taskDb = getTaskDb();
|
|
1211
1910
|
const db = taskDb.open();
|
|
1212
1911
|
if (taskId) taskId = requireTaskId(taskDb, db, taskId, 'atris task events');
|
|
1213
1912
|
const events = taskDb.listTaskEvents(db, {
|
|
1214
1913
|
taskId,
|
|
1215
1914
|
workspaceRoot: all || taskId ? null : taskDb.workspaceRoot(),
|
|
1216
|
-
limit
|
|
1915
|
+
limit,
|
|
1916
|
+
order: taskId || all ? 'asc' : 'desc',
|
|
1917
|
+
});
|
|
1918
|
+
const refRows = taskDb.listTasks(db, {
|
|
1919
|
+
workspaceRoot: all ? null : (taskId ? (taskDb.getTask(db, taskId) || {}).workspace_root : taskDb.workspaceRoot()),
|
|
1217
1920
|
});
|
|
1921
|
+
const refById = taskDb.taskDisplayRefMap(refRows);
|
|
1218
1922
|
if (wantsJson(args)) {
|
|
1219
|
-
printJson({
|
|
1923
|
+
printJson({
|
|
1924
|
+
ok: true,
|
|
1925
|
+
action: 'events',
|
|
1926
|
+
task_id: taskId,
|
|
1927
|
+
mode: taskId ? 'task' : (all ? 'ledger' : 'recent'),
|
|
1928
|
+
limit,
|
|
1929
|
+
events,
|
|
1930
|
+
});
|
|
1220
1931
|
return;
|
|
1221
1932
|
}
|
|
1222
1933
|
if (events.length === 0) {
|
|
1223
1934
|
console.log('(no task events)');
|
|
1224
1935
|
return;
|
|
1225
1936
|
}
|
|
1937
|
+
if (!taskId && !all) {
|
|
1938
|
+
console.log('TASK EVENTS');
|
|
1939
|
+
console.log(`recent ${events.length} event${events.length === 1 ? '' : 's'} (use --all for the full ledger, --limit N to adjust)`);
|
|
1940
|
+
console.log('');
|
|
1941
|
+
for (const e of events) console.log(formatTaskEventCompact(e, refById));
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1226
1944
|
for (const e of events) {
|
|
1227
1945
|
const actor = e.actor ? ` actor=${e.actor}` : '';
|
|
1228
|
-
console.log(`${e.version}\t${e.event_type}\t${e.task_id}${actor}\t${JSON.stringify(e.payload || {})}`);
|
|
1946
|
+
console.log(`${e.version}\t${e.event_type}\t${refById.get(e.task_id) || taskRef(e.task_id)}${actor}\t${JSON.stringify(e.payload || {})}`);
|
|
1229
1947
|
}
|
|
1230
1948
|
}
|
|
1231
1949
|
|
|
@@ -1289,29 +2007,102 @@ function cmdSetup(args) {
|
|
|
1289
2007
|
}
|
|
1290
2008
|
}
|
|
1291
2009
|
|
|
2010
|
+
function extractTodoSectionMarkdown(content, sectionName) {
|
|
2011
|
+
const escaped = String(sectionName || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2012
|
+
const match = String(content || '').match(new RegExp(`(?:^|\\n)(##\\s+${escaped}[^\\n]*\\n[\\s\\S]*?)(?=\\n##(?!#)\\s+|$)`, 'i'));
|
|
2013
|
+
return match ? match[1].trimEnd() : null;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows) {
|
|
2017
|
+
if (!existingTodoPath || !fs.existsSync(existingTodoPath)) return [];
|
|
2018
|
+
const { parseTodoFile } = require('../lib/todo-fallback');
|
|
2019
|
+
const parsed = parseTodoFile(existingTodoPath);
|
|
2020
|
+
const ws = taskDb.workspaceRoot();
|
|
2021
|
+
const existingSourceKeys = new Set(
|
|
2022
|
+
(Array.isArray(refRows) ? refRows : [])
|
|
2023
|
+
.map(row => row && row.source_key)
|
|
2024
|
+
.filter(Boolean)
|
|
2025
|
+
);
|
|
2026
|
+
const existingTitles = new Set(
|
|
2027
|
+
[...(Array.isArray(rows) ? rows : []), ...(Array.isArray(refRows) ? refRows : [])]
|
|
2028
|
+
.map(row => taskDb.normalizeTitle(row && row.title))
|
|
2029
|
+
.filter(Boolean)
|
|
2030
|
+
);
|
|
2031
|
+
const sections = [
|
|
2032
|
+
['backlog', 'open'],
|
|
2033
|
+
['inProgress', 'claimed'],
|
|
2034
|
+
['review', 'review'],
|
|
2035
|
+
['completed', 'done'],
|
|
2036
|
+
];
|
|
2037
|
+
const out = [];
|
|
2038
|
+
let index = 0;
|
|
2039
|
+
for (const [bucket, status] of sections) {
|
|
2040
|
+
for (const task of parsed[bucket] || []) {
|
|
2041
|
+
if (!task.title) continue;
|
|
2042
|
+
const sk = taskDb.sourceKey(existingTodoPath, task.title);
|
|
2043
|
+
const normalizedTitle = taskDb.normalizeTitle(task.title);
|
|
2044
|
+
if ((sk && existingSourceKeys.has(sk)) || existingTitles.has(normalizedTitle)) continue;
|
|
2045
|
+
out.push({
|
|
2046
|
+
id: `markdown:${status}:${task.id || index}:${sk ? sk.slice(0, 10) : index}`,
|
|
2047
|
+
title: task.title,
|
|
2048
|
+
status,
|
|
2049
|
+
tag: task.tag || null,
|
|
2050
|
+
workspace_root: ws,
|
|
2051
|
+
claimed_by: status === 'claimed' ? (task.claimed || null) : null,
|
|
2052
|
+
created_at: index,
|
|
2053
|
+
updated_at: index,
|
|
2054
|
+
done_at: null,
|
|
2055
|
+
metadata: {
|
|
2056
|
+
todo_id: task.id || null,
|
|
2057
|
+
todo_tags: task.tags || [],
|
|
2058
|
+
claimed: task.claimed || null,
|
|
2059
|
+
stage: task.stage || null,
|
|
2060
|
+
verify: task.verify || null,
|
|
2061
|
+
markdown_source: existingTodoPath,
|
|
2062
|
+
},
|
|
2063
|
+
});
|
|
2064
|
+
if (sk) existingSourceKeys.add(sk);
|
|
2065
|
+
existingTitles.add(normalizedTitle);
|
|
2066
|
+
index += 1;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
return out;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1292
2072
|
function cmdRender(args) {
|
|
1293
2073
|
const out = flag(args, '--out') || path.join('atris', 'TODO.md');
|
|
1294
2074
|
const all = hasFlag(args, '--all');
|
|
2075
|
+
const doneLimitRaw = flag(args, '--done-limit');
|
|
2076
|
+
const doneLimit = doneLimitRaw && doneLimitRaw !== true ? Number(doneLimitRaw) : undefined;
|
|
1295
2077
|
const taskDb = getTaskDb();
|
|
1296
2078
|
const db = taskDb.open();
|
|
1297
2079
|
const rows = taskDb.listTasks(db, {
|
|
1298
2080
|
workspaceRoot: all ? null : taskDb.workspaceRoot(),
|
|
1299
2081
|
limit: 500,
|
|
1300
2082
|
});
|
|
1301
|
-
const
|
|
2083
|
+
const refRows = taskDb.listTasks(db, {
|
|
2084
|
+
workspaceRoot: all ? null : taskDb.workspaceRoot(),
|
|
2085
|
+
});
|
|
1302
2086
|
const outPath = path.resolve(String(out));
|
|
2087
|
+
const existingTodo = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : '';
|
|
2088
|
+
const preservedSections = [];
|
|
2089
|
+
const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
|
|
2090
|
+
if (endgameSection) preservedSections.push(endgameSection);
|
|
2091
|
+
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
|
|
2092
|
+
const rowsToRender = [...rows, ...markdownRows];
|
|
2093
|
+
const markdown = taskDb.renderTodoMarkdown(rowsToRender, { doneLimit, refRows, preservedSections });
|
|
1303
2094
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1304
2095
|
fs.writeFileSync(outPath, markdown, 'utf8');
|
|
1305
2096
|
if (wantsJson(args)) {
|
|
1306
2097
|
printJson({
|
|
1307
2098
|
ok: true,
|
|
1308
2099
|
action: 'rendered',
|
|
1309
|
-
count:
|
|
2100
|
+
count: rowsToRender.length,
|
|
1310
2101
|
path: outPath,
|
|
1311
2102
|
});
|
|
1312
2103
|
return;
|
|
1313
2104
|
}
|
|
1314
|
-
console.log(`rendered ${
|
|
2105
|
+
console.log(`rendered ${rowsToRender.length} task${rowsToRender.length === 1 ? '' : 's'} -> ${outPath}`);
|
|
1315
2106
|
}
|
|
1316
2107
|
|
|
1317
2108
|
function cmdSync(args) {
|
|
@@ -1358,8 +2149,9 @@ function cmdSync(args) {
|
|
|
1358
2149
|
|
|
1359
2150
|
console.log(`task sync dry-run: ${plan.length} planned write${plan.length === 1 ? '' : 's'}`);
|
|
1360
2151
|
console.log(`business: ${businessId}`);
|
|
2152
|
+
const refById = taskDb.taskDisplayRefMap(projection.tasks || []);
|
|
1361
2153
|
for (const item of plan) {
|
|
1362
|
-
console.log(`${item.method.padEnd(5)} ${item.endpoint} <= ${item.local_task_id.
|
|
2154
|
+
console.log(`${item.method.padEnd(5)} ${item.endpoint} <= ${refById.get(item.local_task_id) || taskRef(item.local_task_id)} ${item.body.title}`);
|
|
1363
2155
|
for (const followup of item.after_create || []) {
|
|
1364
2156
|
console.log(` then ${followup.method} ${followup.endpoint} state=${followup.body.state}`);
|
|
1365
2157
|
}
|
|
@@ -1367,13 +2159,21 @@ function cmdSync(args) {
|
|
|
1367
2159
|
}
|
|
1368
2160
|
|
|
1369
2161
|
function taskColumn(task) {
|
|
1370
|
-
if (task.status === 'open') return 'open';
|
|
2162
|
+
if (task.status === 'open') return taskIsPlannedOpen(task) ? 'open' : 'backlog';
|
|
1371
2163
|
if (task.status === 'claimed') return 'doing';
|
|
2164
|
+
if (task.status === 'review') return 'review';
|
|
2165
|
+
if (task.status === 'failed' && taskHasReview(task)) return 'done';
|
|
1372
2166
|
if (task.status === 'failed') return 'blocked';
|
|
1373
|
-
if (task.status === 'done' && task
|
|
2167
|
+
if (task.status === 'done' && !taskHasReview(task)) return 'review';
|
|
1374
2168
|
return 'done';
|
|
1375
2169
|
}
|
|
1376
2170
|
|
|
2171
|
+
function taskHasReview(task) {
|
|
2172
|
+
if (task.latest_event_type === 'reviewed') return true;
|
|
2173
|
+
const review = task.review || {};
|
|
2174
|
+
return review.reward != null || Boolean(review.proof || review.lesson || review.next_task);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
1377
2177
|
function taskBoardHtml() {
|
|
1378
2178
|
return `<!doctype html>
|
|
1379
2179
|
<html lang="en">
|
|
@@ -1397,7 +2197,7 @@ function taskBoardHtml() {
|
|
|
1397
2197
|
button { border:1px solid var(--line); background:#20242a; color:var(--text); border-radius:7px; padding:8px 10px; font:inherit; font-size:12px; cursor:pointer; }
|
|
1398
2198
|
button:hover { border-color:#3b414b; background:#252a32; }
|
|
1399
2199
|
.primary { background:#214b35; border-color:#2f684a; }
|
|
1400
|
-
.grid { display:grid; grid-template-columns: repeat(
|
|
2200
|
+
.grid { display:grid; grid-template-columns: repeat(var(--board-columns, 6), minmax(160px, 1fr)); gap:12px; align-items:start; }
|
|
1401
2201
|
.overview { display:grid; grid-template-columns: minmax(260px, 1.4fr) minmax(260px, 1fr); gap:12px; margin-bottom:12px; }
|
|
1402
2202
|
.goalbox, .chainbox { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:88px; }
|
|
1403
2203
|
.goalbox h2, .chainbox h2 { margin:0 0 8px; color:var(--muted); font-size:12px; font-weight:650; }
|
|
@@ -1441,7 +2241,7 @@ function taskBoardHtml() {
|
|
|
1441
2241
|
<header>
|
|
1442
2242
|
<div>
|
|
1443
2243
|
<h1>Atris Task Factory</h1>
|
|
1444
|
-
<div class="sub"
|
|
2244
|
+
<div class="sub" data-smoke="hello-from-ui">hello from UI</div>
|
|
1445
2245
|
</div>
|
|
1446
2246
|
<button id="refresh">Refresh</button>
|
|
1447
2247
|
</header>
|
|
@@ -1466,12 +2266,14 @@ function taskBoardHtml() {
|
|
|
1466
2266
|
</main>
|
|
1467
2267
|
<script>
|
|
1468
2268
|
const columns = [
|
|
2269
|
+
['backlog', 'Backlog'],
|
|
1469
2270
|
['open', 'Open'],
|
|
1470
2271
|
['doing', 'Doing'],
|
|
1471
2272
|
['review', 'Review'],
|
|
1472
2273
|
['blocked', 'Blocked'],
|
|
1473
2274
|
['done', 'Done']
|
|
1474
2275
|
];
|
|
2276
|
+
const planTags = new Set(${JSON.stringify(Array.from(STATUS_PLAN_TAGS))});
|
|
1475
2277
|
let state = { tasks: [] };
|
|
1476
2278
|
let selected = null;
|
|
1477
2279
|
const $ = (id) => document.getElementById(id);
|
|
@@ -1487,10 +2289,19 @@ function taskBoardHtml() {
|
|
|
1487
2289
|
}
|
|
1488
2290
|
|
|
1489
2291
|
function taskColumn(task) {
|
|
1490
|
-
if (task.status === 'open')
|
|
2292
|
+
if (task.status === 'open') {
|
|
2293
|
+
const metadata = task.metadata || {};
|
|
2294
|
+
const tag = String(task.tag || '').trim().toLowerCase().replace(/\\s+/g, '-');
|
|
2295
|
+
const stage = String(metadata.stage || '').trim().toLowerCase().replace(/\\s+/g, '-');
|
|
2296
|
+
const planned = planTags.has(tag) || planTags.has(stage) || metadata.verify || metadata.goal || metadata.loop || metadata.cron || metadata.next_run_at;
|
|
2297
|
+
return planned ? 'open' : 'backlog';
|
|
2298
|
+
}
|
|
1491
2299
|
if (task.status === 'claimed') return 'doing';
|
|
2300
|
+
if (task.status === 'review') return 'review';
|
|
2301
|
+
const reviewed = task.latest_event_type === 'reviewed' || !!(task.review && (task.review.reward != null || task.review.proof || task.review.lesson || task.review.next_task));
|
|
2302
|
+
if (task.status === 'failed' && reviewed) return 'done';
|
|
1492
2303
|
if (task.status === 'failed') return 'blocked';
|
|
1493
|
-
if (task.status === 'done' &&
|
|
2304
|
+
if (task.status === 'done' && !reviewed) return 'review';
|
|
1494
2305
|
return 'done';
|
|
1495
2306
|
}
|
|
1496
2307
|
|
|
@@ -1504,6 +2315,7 @@ function taskBoardHtml() {
|
|
|
1504
2315
|
renderOverview();
|
|
1505
2316
|
renderStreams();
|
|
1506
2317
|
const board = $('board');
|
|
2318
|
+
board.style.setProperty('--board-columns', columns.length);
|
|
1507
2319
|
board.innerHTML = '';
|
|
1508
2320
|
for (const [key, label] of columns) {
|
|
1509
2321
|
const tasks = state.tasks.filter((task) => taskColumn(task) === key);
|
|
@@ -1531,7 +2343,7 @@ function taskBoardHtml() {
|
|
|
1531
2343
|
: '<div class="empty">No atris/goals.md found. Add goals to give tasks a north star.</div>';
|
|
1532
2344
|
const latest = reviewed.slice(0, 3);
|
|
1533
2345
|
const chainHtml = latest.length
|
|
1534
|
-
? latest.map((task) => '<div class="chainitem"><span>' + task.id.slice(0, 8) + '</span><strong></strong></div>').join('')
|
|
2346
|
+
? latest.map((task) => '<div class="chainitem"><span>' + (task.display_id || task.id.slice(0, 8)) + '</span><strong></strong></div>').join('')
|
|
1535
2347
|
: '<div class="empty">Complete a task with proof to start the chain.</div>';
|
|
1536
2348
|
$('overview').innerHTML = [
|
|
1537
2349
|
'<div class="goalbox"><h2>Goals</h2>' + goalHtml + '</div>',
|
|
@@ -1562,7 +2374,7 @@ function taskBoardHtml() {
|
|
|
1562
2374
|
};
|
|
1563
2375
|
const tasks = stream.tasks.filter((task) => task.status !== 'done').slice(0, 3);
|
|
1564
2376
|
const taskHtml = tasks.length
|
|
1565
|
-
? tasks.map((task) => '<div class="streamtask"><span>' + task.id.slice(0, 8) + '</span><strong></strong></div>').join('')
|
|
2377
|
+
? tasks.map((task) => '<div class="streamtask"><span>' + (task.display_id || task.id.slice(0, 8)) + '</span><strong></strong></div>').join('')
|
|
1566
2378
|
: '<div class="empty">No active tasks in this stream.</div>';
|
|
1567
2379
|
return [
|
|
1568
2380
|
'<div class="stream">',
|
|
@@ -1588,7 +2400,7 @@ function taskBoardHtml() {
|
|
|
1588
2400
|
btn.innerHTML = '<div class="title"></div><div class="meta"><span class="pill"></span><span class="pill"></span><span class="pill"></span></div><div class="why"></div>';
|
|
1589
2401
|
btn.querySelector('.title').textContent = task.title;
|
|
1590
2402
|
const pills = btn.querySelectorAll('.pill');
|
|
1591
|
-
pills[0].textContent = task.id.slice(0, 8);
|
|
2403
|
+
pills[0].textContent = task.display_id || task.id.slice(0, 8);
|
|
1592
2404
|
pills[1].textContent = owner;
|
|
1593
2405
|
pills[2].textContent = 'v' + task.current_version;
|
|
1594
2406
|
const why = task.objective || (task.lineage && task.lineage.parent_title) || (task.review && task.review.proof) || '';
|
|
@@ -1611,30 +2423,43 @@ function taskBoardHtml() {
|
|
|
1611
2423
|
'<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>',
|
|
1612
2424
|
'<div class="fact"><b>Goal</b><div id="taskGoal"></div></div>',
|
|
1613
2425
|
'<div class="fact"><b>Lineage</b><div id="taskLineage"></div></div>',
|
|
2426
|
+
'<div class="fact"><b>Summary</b><div id="taskSummary"></div></div>',
|
|
1614
2427
|
'<div class="fact"><b>Proof / lesson</b><div id="taskProof"></div></div>',
|
|
1615
2428
|
'<div class="thread">' + (messages || '<div class="empty">No thread yet.</div>') + '</div>',
|
|
1616
2429
|
'<label>Add context</label><textarea id="note" placeholder="Decision, blocker, context, update..."></textarea>',
|
|
1617
2430
|
'<label>Proof</label><input id="proof" placeholder="npm test, PR link, screenshot, blocked reason...">',
|
|
1618
2431
|
'<label>Lesson</label><textarea id="lesson" placeholder="What did this task teach us?"></textarea>',
|
|
1619
2432
|
'<label>Next task</label><input id="nextTask" placeholder="Optional next sharper task">',
|
|
1620
|
-
'<div class="actions"><button id="claim">Claim</button><button id="saveNote">Say</button><button id="finish" class="primary full"
|
|
2433
|
+
'<div class="actions"><button id="claim">Claim</button><button id="saveNote">Say</button><button id="finish" class="primary full"></button></div>'
|
|
1621
2434
|
].join('');
|
|
1622
2435
|
room.querySelector('h3').textContent = task.title;
|
|
1623
2436
|
$('taskGoal').textContent = task.objective || 'No matching goal yet.';
|
|
1624
2437
|
$('taskLineage').textContent = 'parent: ' + parent + ' / next: ' + children;
|
|
2438
|
+
$('taskSummary').textContent = task.review && task.review.summary
|
|
2439
|
+
? task.review.summary
|
|
2440
|
+
: 'No review summary yet.';
|
|
1625
2441
|
$('taskProof').textContent = task.review && (task.review.proof || task.review.lesson)
|
|
1626
2442
|
? ((task.review.proof || 'no proof') + ' / ' + (task.review.lesson || 'no lesson'))
|
|
1627
2443
|
: 'No proof yet.';
|
|
1628
2444
|
room.querySelectorAll('.msg div:last-child').forEach((el, i) => { el.textContent = task.messages[i].content; });
|
|
2445
|
+
$('finish').textContent = task.status === 'review' ? 'Accept proof' : 'Move to Review';
|
|
1629
2446
|
$('claim').onclick = () => mutate('/api/tasks/' + task.id + '/claim', { owner: 'operator' });
|
|
1630
2447
|
$('saveNote').onclick = () => mutate('/api/tasks/' + task.id + '/message', { actor: 'operator', content: $('note').value });
|
|
1631
|
-
$('finish').onclick = () =>
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2448
|
+
$('finish').onclick = () => {
|
|
2449
|
+
const proof = $('proof').value.trim();
|
|
2450
|
+
const lesson = $('lesson').value.trim();
|
|
2451
|
+
const nextTask = $('nextTask').value.trim();
|
|
2452
|
+
const payload = { actor: 'operator' };
|
|
2453
|
+
if (proof) payload.proof = proof;
|
|
2454
|
+
if (lesson) payload.lesson = lesson;
|
|
2455
|
+
if (nextTask) payload.next = nextTask;
|
|
2456
|
+
if (task.status === 'review') {
|
|
2457
|
+
payload.createNext = Boolean(nextTask || (task.review && task.review.next_task));
|
|
2458
|
+
mutate('/api/tasks/' + task.id + '/accept', payload);
|
|
2459
|
+
} else {
|
|
2460
|
+
mutate('/api/tasks/' + task.id + '/ready', payload);
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
1638
2463
|
}
|
|
1639
2464
|
|
|
1640
2465
|
async function mutate(path, body) {
|
|
@@ -1710,7 +2535,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
1710
2535
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1711
2536
|
return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, projection_path: outPath, task: taskFromProjection(projection, result.id) });
|
|
1712
2537
|
}
|
|
1713
|
-
const match = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(claim|message|finish|review|events)$/);
|
|
2538
|
+
const match = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(claim|message|ready|accept|revise|finish|review|events)$/);
|
|
1714
2539
|
if (!match) return sendJson(res, 404, { ok: false, reason: 'not_found' });
|
|
1715
2540
|
const resolved = resolveTaskRef(taskDb, db, match[1]);
|
|
1716
2541
|
if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
|
|
@@ -1742,6 +2567,7 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
1742
2567
|
const shouldReview = body.proof || body.lesson || body.next || body.reward !== undefined;
|
|
1743
2568
|
let episode = null;
|
|
1744
2569
|
let nextCreated = null;
|
|
2570
|
+
let xpProjection = null;
|
|
1745
2571
|
if (shouldReview) {
|
|
1746
2572
|
const reviewed = taskDb.reviewTask(db, {
|
|
1747
2573
|
id: taskId,
|
|
@@ -1750,9 +2576,11 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
1750
2576
|
lesson: String(body.lesson || ''),
|
|
1751
2577
|
nextTask: String(body.next || ''),
|
|
1752
2578
|
proof: String(body.proof || ''),
|
|
2579
|
+
careerXpEligible: false,
|
|
1753
2580
|
});
|
|
1754
2581
|
episode = reviewed.episode;
|
|
1755
2582
|
nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, episode.next_task_suggestion) : null;
|
|
2583
|
+
xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
1756
2584
|
}
|
|
1757
2585
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1758
2586
|
return sendJson(res, 200, {
|
|
@@ -1761,11 +2589,63 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
1761
2589
|
task_id: taskId,
|
|
1762
2590
|
reviewed: Boolean(episode),
|
|
1763
2591
|
episode,
|
|
2592
|
+
xp_projection: xpProjection,
|
|
1764
2593
|
next_task_id: nextCreated ? nextCreated.id : null,
|
|
1765
2594
|
projection_path: outPath,
|
|
1766
2595
|
task: taskFromProjection(projection, taskId),
|
|
1767
2596
|
});
|
|
1768
2597
|
}
|
|
2598
|
+
if (op === 'ready') {
|
|
2599
|
+
const proof = String(body.proof || '').trim();
|
|
2600
|
+
if (!proof) return sendJson(res, 400, { ok: false, reason: 'proof_required' });
|
|
2601
|
+
const result = taskDb.readyTask(db, {
|
|
2602
|
+
id: taskId,
|
|
2603
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
2604
|
+
proof,
|
|
2605
|
+
lesson: String(body.lesson || ''),
|
|
2606
|
+
nextTask: String(body.next || ''),
|
|
2607
|
+
});
|
|
2608
|
+
if (!result.ready) return sendJson(res, 409, { ok: false, reason: result.reason });
|
|
2609
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
2610
|
+
return sendJson(res, 200, { ok: true, action: 'ready', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
2611
|
+
}
|
|
2612
|
+
if (op === 'accept') {
|
|
2613
|
+
const currentTask = enrichTaskProjection(taskDb.taskProjection(db, { taskId })).tasks[0] || null;
|
|
2614
|
+
const hasExplicitProof = Object.prototype.hasOwnProperty.call(body, 'proof');
|
|
2615
|
+
const proof = String(hasExplicitProof ? body.proof : currentTask?.metadata?.latest_agent_proof || '').trim();
|
|
2616
|
+
if (!proof) return sendJson(res, 400, { ok: false, reason: 'proof_required' });
|
|
2617
|
+
const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
|
|
2618
|
+
const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next');
|
|
2619
|
+
const lesson = hasExplicitLesson ? String(body.lesson || '') : String(currentTask?.review?.lesson || currentTask?.metadata?.latest_agent_lesson || '');
|
|
2620
|
+
const nextTask = hasExplicitNext ? String(body.next || '') : String(currentTask?.review?.next_task || currentTask?.metadata?.latest_agent_next_task || '');
|
|
2621
|
+
const clearedFields = [];
|
|
2622
|
+
if (hasExplicitLesson && !lesson.trim()) clearedFields.push('lesson');
|
|
2623
|
+
if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
|
|
2624
|
+
const parsedReward = parseAcceptReward(body.reward);
|
|
2625
|
+
if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
|
|
2626
|
+
const done = taskDb.doneTask(db, { id: taskId, status: 'done', actor: String(body.actor || DEFAULT_OWNER), allowReview: true });
|
|
2627
|
+
if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
|
|
2628
|
+
const reviewed = taskDb.reviewTask(db, {
|
|
2629
|
+
id: taskId,
|
|
2630
|
+
actor: String(body.actor || DEFAULT_OWNER),
|
|
2631
|
+
reward: parsedReward.value,
|
|
2632
|
+
lesson,
|
|
2633
|
+
nextTask,
|
|
2634
|
+
proof,
|
|
2635
|
+
careerXpEligible: true,
|
|
2636
|
+
clearedFields,
|
|
2637
|
+
});
|
|
2638
|
+
const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
|
|
2639
|
+
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
2640
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
2641
|
+
return sendJson(res, 200, { ok: true, action: 'accepted', task_id: taskId, episode: reviewed.episode, xp_projection: xpProjection, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
2642
|
+
}
|
|
2643
|
+
if (op === 'revise') {
|
|
2644
|
+
const result = taskDb.reviseTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), note: String(body.note || body.reason || '') });
|
|
2645
|
+
if (!result.revised) return sendJson(res, 409, { ok: false, reason: result.reason });
|
|
2646
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
2647
|
+
return sendJson(res, 200, { ok: true, action: 'revise', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
2648
|
+
}
|
|
1769
2649
|
if (op === 'review') {
|
|
1770
2650
|
const currentTask = taskDb.getTask(db, taskId);
|
|
1771
2651
|
const reviewed = taskDb.reviewTask(db, {
|
|
@@ -1775,10 +2655,12 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
1775
2655
|
lesson: String(body.lesson || ''),
|
|
1776
2656
|
nextTask: String(body.next || ''),
|
|
1777
2657
|
proof: String(body.proof || ''),
|
|
2658
|
+
careerXpEligible: false,
|
|
1778
2659
|
});
|
|
1779
2660
|
const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
|
|
2661
|
+
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
1780
2662
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
1781
|
-
return sendJson(res, 200, { ok: true, action: 'reviewed', task_id: taskId, episode: reviewed.episode, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
2663
|
+
return sendJson(res, 200, { ok: true, action: 'reviewed', task_id: taskId, episode: reviewed.episode, xp_projection: xpProjection, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
|
|
1782
2664
|
}
|
|
1783
2665
|
}
|
|
1784
2666
|
|
|
@@ -1811,6 +2693,7 @@ function cmdServe(args) {
|
|
|
1811
2693
|
|
|
1812
2694
|
async function run(args) {
|
|
1813
2695
|
const raw = args || [];
|
|
2696
|
+
if (raw.includes('--help') || raw.includes('-h')) return help();
|
|
1814
2697
|
const first = raw[0];
|
|
1815
2698
|
const sub = !first || first.startsWith('--') ? 'desk' : first;
|
|
1816
2699
|
const rest = !first || first.startsWith('--') ? raw : raw.slice(1);
|
|
@@ -1830,6 +2713,9 @@ async function run(args) {
|
|
|
1830
2713
|
case 'note': return cmdNote(rest);
|
|
1831
2714
|
case 'say': return cmdNote(rest);
|
|
1832
2715
|
case 'show': return cmdShow(rest);
|
|
2716
|
+
case 'ready': return cmdReady(rest);
|
|
2717
|
+
case 'accept': return cmdAccept(rest);
|
|
2718
|
+
case 'revise': return cmdRevise(rest);
|
|
1833
2719
|
case 'done': return cmdDone(rest);
|
|
1834
2720
|
case 'finish': return cmdFinish(rest);
|
|
1835
2721
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
@@ -1848,6 +2734,14 @@ async function run(args) {
|
|
|
1848
2734
|
case '-h':
|
|
1849
2735
|
return help();
|
|
1850
2736
|
default:
|
|
2737
|
+
if (wantsJson(raw)) {
|
|
2738
|
+
printJson({
|
|
2739
|
+
ok: false,
|
|
2740
|
+
error: `unknown task subcommand: ${sub}`,
|
|
2741
|
+
usage: taskUsageLines(),
|
|
2742
|
+
});
|
|
2743
|
+
process.exit(2);
|
|
2744
|
+
}
|
|
1851
2745
|
console.error(`atris task: unknown subcommand "${sub}"`);
|
|
1852
2746
|
help();
|
|
1853
2747
|
process.exit(2);
|