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/lib/task-db.js
CHANGED
|
@@ -30,6 +30,13 @@ const { DatabaseSync } = require('node:sqlite');
|
|
|
30
30
|
|
|
31
31
|
const DEFAULT_DB_PATH = path.join(os.homedir(), '.atris', 'tasks.db');
|
|
32
32
|
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
33
|
+
const TODO_RENDER_DONE_LIMIT = 8;
|
|
34
|
+
const PROJECTION_DONE_LIMIT = 8;
|
|
35
|
+
const PROJECTION_EVENT_LIMIT = 8;
|
|
36
|
+
const PROJECTION_MESSAGE_LIMIT = 6;
|
|
37
|
+
const PROJECTION_PAYLOAD_TEXT_LIMIT = 1000;
|
|
38
|
+
const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
|
|
39
|
+
const TASK_REF_GENERIC_TOKENS = new Set(['app', 'atris', 'atrisos', 'project', 'repo', 'workspace']);
|
|
33
40
|
|
|
34
41
|
const SCHEMA = `
|
|
35
42
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
@@ -160,12 +167,91 @@ function sourceKey(sourceFile, title) {
|
|
|
160
167
|
return h.digest('hex');
|
|
161
168
|
}
|
|
162
169
|
|
|
170
|
+
function normalizeTaskRef(value) {
|
|
171
|
+
return String(value || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function workspaceRefPrefix(ws) {
|
|
175
|
+
const base = path.basename(String(ws || 'task')).toLowerCase();
|
|
176
|
+
const parts = base.split(/[^a-z0-9]+/).filter(Boolean);
|
|
177
|
+
const useful = parts.filter(p => !TASK_REF_GENERIC_TOKENS.has(p));
|
|
178
|
+
if (useful.length > 1) {
|
|
179
|
+
const leading = useful.slice(0, -1).map(p => p[0]).join('').toUpperCase();
|
|
180
|
+
const last = useful[useful.length - 1].toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
181
|
+
const lastKey = last[0] + last.slice(1).replace(/[AEIOU]/g, '');
|
|
182
|
+
const combined = `${leading}${lastKey}`.replace(/[^A-Z0-9]/g, '');
|
|
183
|
+
if (combined) return combined.slice(0, 3).padEnd(3, 'X');
|
|
184
|
+
}
|
|
185
|
+
const picked = useful.pop()
|
|
186
|
+
|| parts[parts.length - 1]
|
|
187
|
+
|| 'task';
|
|
188
|
+
const token = picked.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
189
|
+
if (!token) return 'TSK';
|
|
190
|
+
if (token.length <= 3) return token.padEnd(3, 'X');
|
|
191
|
+
const consonantKey = token[0] + token.slice(1).replace(/[AEIOU]/g, '');
|
|
192
|
+
return (consonantKey.length >= 3 ? consonantKey : token).slice(0, 3);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function taskDisplayRef(row, index) {
|
|
196
|
+
return `${workspaceRefPrefix(row && row.workspace_root)}-${Number(index) + 1}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function shortestUniqueTaskRef(id, ids, minLength = 8) {
|
|
200
|
+
const normalized = normalizeTaskRef(id);
|
|
201
|
+
if (!normalized) return '';
|
|
202
|
+
const all = (Array.isArray(ids) ? ids : []).map(normalizeTaskRef).filter(Boolean);
|
|
203
|
+
for (let length = Math.min(minLength, normalized.length); length <= normalized.length; length += 1) {
|
|
204
|
+
const prefix = normalized.slice(0, length);
|
|
205
|
+
const matches = all.filter(candidate => candidate.startsWith(prefix));
|
|
206
|
+
if (matches.length <= 1) return prefix;
|
|
207
|
+
}
|
|
208
|
+
return normalized;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function withTaskDisplayRefs(rows, refRows = rows) {
|
|
212
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
213
|
+
const referenceInput = Array.isArray(refRows) ? refRows : list;
|
|
214
|
+
const referenceIds = new Set(referenceInput.map(row => row && row.id).filter(Boolean));
|
|
215
|
+
const referenceList = [
|
|
216
|
+
...referenceInput,
|
|
217
|
+
...list.filter(row => row && row.id && !referenceIds.has(row.id) && !(row.metadata && row.metadata.markdown_source)),
|
|
218
|
+
];
|
|
219
|
+
const byWorkspace = new Map();
|
|
220
|
+
for (const row of referenceList) {
|
|
221
|
+
const key = row && row.workspace_root || '';
|
|
222
|
+
if (!byWorkspace.has(key)) byWorkspace.set(key, []);
|
|
223
|
+
byWorkspace.get(key).push(row);
|
|
224
|
+
}
|
|
225
|
+
const refs = new Map();
|
|
226
|
+
for (const group of byWorkspace.values()) {
|
|
227
|
+
const sorted = [...group]
|
|
228
|
+
.sort((a, b) => (Number(a.created_at || 0) - Number(b.created_at || 0)) || String(a.id || '').localeCompare(String(b.id || '')))
|
|
229
|
+
const ids = sorted.map(row => row && row.id);
|
|
230
|
+
sorted.forEach((row, index) => {
|
|
231
|
+
refs.set(row.id, {
|
|
232
|
+
display_id: taskDisplayRef(row, index),
|
|
233
|
+
legacy_ref: shortestUniqueTaskRef(row.id, ids, 8),
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return list.map(row => ({ ...row, ...(refs.get(row.id) || {}) }));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function taskDisplayRefMap(rows) {
|
|
241
|
+
const map = new Map();
|
|
242
|
+
for (const row of withTaskDisplayRefs(rows)) {
|
|
243
|
+
map.set(row.id, row.display_id);
|
|
244
|
+
}
|
|
245
|
+
return map;
|
|
246
|
+
}
|
|
247
|
+
|
|
163
248
|
function addTask(db, { title, tag, workspaceRoot: ws, sourceKey: sk, metadata, status, claimedBy }) {
|
|
164
249
|
if (!title || !String(title).trim()) throw new Error('title required');
|
|
165
250
|
const now = Date.now();
|
|
166
251
|
const id = newId();
|
|
167
|
-
const taskStatus = ['open', 'claimed', 'done', 'failed'].includes(status) ? status : 'open';
|
|
168
|
-
const
|
|
252
|
+
const taskStatus = ['open', 'claimed', 'review', 'done', 'failed'].includes(status) ? status : 'open';
|
|
253
|
+
const initialClaimedBy = (taskStatus === 'claimed' || taskStatus === 'review') ? (claimedBy || null) : null;
|
|
254
|
+
const claimedAt = initialClaimedBy ? now : null;
|
|
169
255
|
// Idempotent on (workspace_root, source_key) when source_key supplied.
|
|
170
256
|
if (sk) {
|
|
171
257
|
const existing = db.prepare(
|
|
@@ -184,7 +270,7 @@ function addTask(db, { title, tag, workspaceRoot: ws, sourceKey: sk, metadata, s
|
|
|
184
270
|
tag || null,
|
|
185
271
|
ws,
|
|
186
272
|
sk || null,
|
|
187
|
-
|
|
273
|
+
initialClaimedBy,
|
|
188
274
|
claimedAt,
|
|
189
275
|
now,
|
|
190
276
|
now,
|
|
@@ -217,7 +303,7 @@ function listTasks(db, { workspaceRoot: ws, status, claimedBy, limit }) {
|
|
|
217
303
|
FROM tasks
|
|
218
304
|
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
219
305
|
ORDER BY
|
|
220
|
-
CASE status WHEN 'open' THEN 0 WHEN 'claimed' THEN 1 WHEN '
|
|
306
|
+
CASE status WHEN 'open' THEN 0 WHEN 'claimed' THEN 1 WHEN 'review' THEN 2 WHEN 'failed' THEN 3 WHEN 'done' THEN 4 ELSE 5 END,
|
|
221
307
|
created_at DESC
|
|
222
308
|
${limit ? 'LIMIT ' + Number(limit) : ''}
|
|
223
309
|
`;
|
|
@@ -272,23 +358,24 @@ function claimTask(db, { id, claimedBy }) {
|
|
|
272
358
|
return { claimed: false, reason: 'already_' + row.status, claimed_by: row.claimed_by };
|
|
273
359
|
}
|
|
274
360
|
|
|
275
|
-
function doneTask(db, { id, status }) {
|
|
361
|
+
function doneTask(db, { id, status, actor, allowReview = false }) {
|
|
276
362
|
if (!id) throw new Error('id required');
|
|
277
363
|
const final = status || 'done';
|
|
278
364
|
if (!['done', 'failed'].includes(final)) throw new Error('status must be done|failed');
|
|
279
365
|
const now = Date.now();
|
|
366
|
+
const allowedStatuses = allowReview ? "'open', 'claimed', 'review'" : "'open', 'claimed'";
|
|
280
367
|
const result = withBusyRetry(() => db.prepare(`
|
|
281
368
|
UPDATE tasks
|
|
282
369
|
SET status = ?, done_at = ?, updated_at = ?
|
|
283
370
|
WHERE id = ?
|
|
284
|
-
AND status IN (
|
|
371
|
+
AND status IN (${allowedStatuses})
|
|
285
372
|
`).run(final, now, now, id));
|
|
286
373
|
if (result.changes === 1) {
|
|
287
374
|
const row = db.prepare('SELECT id, workspace_root FROM tasks WHERE id = ?').get(id);
|
|
288
375
|
appendTaskEvent(db, {
|
|
289
376
|
taskId: id,
|
|
290
377
|
workspaceRoot: row.workspace_root,
|
|
291
|
-
actor: process.env.ATRIS_AGENT_ID || process.env.USER || null,
|
|
378
|
+
actor: actor || process.env.ATRIS_AGENT_ID || process.env.USER || null,
|
|
292
379
|
eventType: final === 'done' ? 'completed' : 'blocked',
|
|
293
380
|
payload: { status: final },
|
|
294
381
|
});
|
|
@@ -296,6 +383,117 @@ function doneTask(db, { id, status }) {
|
|
|
296
383
|
return { updated: result.changes === 1 };
|
|
297
384
|
}
|
|
298
385
|
|
|
386
|
+
function readyTask(db, { id, actor, proof, lesson, nextTask }) {
|
|
387
|
+
if (!id) throw new Error('id required');
|
|
388
|
+
const text = String(proof || '').trim();
|
|
389
|
+
if (!text) throw new Error('proof required');
|
|
390
|
+
const row = getTask(db, id);
|
|
391
|
+
if (!row) return { ready: false, reason: 'not_found' };
|
|
392
|
+
if (!['open', 'claimed', 'review'].includes(row.status)) {
|
|
393
|
+
return { ready: false, reason: `already_${row.status}` };
|
|
394
|
+
}
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
397
|
+
const reviewPassCount = Number(metadata.agent_review_pass_count || 0) + 1;
|
|
398
|
+
metadata.approval_status = 'pending';
|
|
399
|
+
metadata.agent_review_pass_count = reviewPassCount;
|
|
400
|
+
metadata.agent_reviewed_at = new Date(now).toISOString();
|
|
401
|
+
metadata.agent_reviewed_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
402
|
+
metadata.latest_agent_proof = text;
|
|
403
|
+
metadata.latest_agent_lesson = String(lesson || '').trim() || null;
|
|
404
|
+
metadata.latest_agent_next_task = String(nextTask || '').trim() || null;
|
|
405
|
+
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES) {
|
|
406
|
+
metadata.agent_certified = true;
|
|
407
|
+
metadata.agent_certified_at = new Date(now).toISOString();
|
|
408
|
+
metadata.agent_certified_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
409
|
+
metadata.agent_certification_policy = `${AGENT_CERTIFICATION_REVIEW_PASSES}_agent_review_passes`;
|
|
410
|
+
}
|
|
411
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
412
|
+
UPDATE tasks
|
|
413
|
+
SET status = 'review',
|
|
414
|
+
done_at = NULL,
|
|
415
|
+
updated_at = ?,
|
|
416
|
+
metadata = ?
|
|
417
|
+
WHERE id = ?
|
|
418
|
+
AND status IN ('open', 'claimed', 'review')
|
|
419
|
+
`).run(now, JSON.stringify(metadata), id));
|
|
420
|
+
if (result.changes !== 1) return { ready: false, reason: 'not_open_claimed_or_review' };
|
|
421
|
+
const updated = getTask(db, id);
|
|
422
|
+
const event = appendTaskEvent(db, {
|
|
423
|
+
taskId: id,
|
|
424
|
+
workspaceRoot: updated.workspace_root,
|
|
425
|
+
actor: actor || null,
|
|
426
|
+
eventType: 'proof_ready',
|
|
427
|
+
payload: {
|
|
428
|
+
proof: text,
|
|
429
|
+
lesson: metadata.latest_agent_lesson,
|
|
430
|
+
next_task: metadata.latest_agent_next_task,
|
|
431
|
+
approval_status: 'pending',
|
|
432
|
+
review_pass_count: reviewPassCount,
|
|
433
|
+
agent_certified: metadata.agent_certified === true,
|
|
434
|
+
agent_certification_policy: metadata.agent_certification_policy || null,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
return { ready: true, event, row: updated };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function reviseTask(db, { id, actor, note }) {
|
|
441
|
+
if (!id) throw new Error('id required');
|
|
442
|
+
const text = String(note || '').trim();
|
|
443
|
+
if (!text) throw new Error('note required');
|
|
444
|
+
const row = getTask(db, id);
|
|
445
|
+
if (!row) return { revised: false, reason: 'not_found' };
|
|
446
|
+
if (row.status !== 'review') {
|
|
447
|
+
return { revised: false, reason: `not_reviewable_${row.status}` };
|
|
448
|
+
}
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
451
|
+
const revisionCount = Number(metadata.human_revision_count || 0) + 1;
|
|
452
|
+
for (const key of [
|
|
453
|
+
'agent_review_pass_count',
|
|
454
|
+
'agent_reviewed_at',
|
|
455
|
+
'agent_reviewed_by',
|
|
456
|
+
'latest_agent_proof',
|
|
457
|
+
'latest_agent_lesson',
|
|
458
|
+
'latest_agent_next_task',
|
|
459
|
+
'agent_certified',
|
|
460
|
+
'agent_certified_at',
|
|
461
|
+
'agent_certified_by',
|
|
462
|
+
'agent_certification_policy',
|
|
463
|
+
]) {
|
|
464
|
+
delete metadata[key];
|
|
465
|
+
}
|
|
466
|
+
metadata.approval_status = 'revise';
|
|
467
|
+
metadata.human_revision_count = revisionCount;
|
|
468
|
+
metadata.human_revision_at = new Date(now).toISOString();
|
|
469
|
+
metadata.human_revision_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
470
|
+
metadata.human_revision_note = text;
|
|
471
|
+
const revisedStatus = row.claimed_by ? 'claimed' : 'open';
|
|
472
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
473
|
+
UPDATE tasks
|
|
474
|
+
SET status = ?,
|
|
475
|
+
done_at = NULL,
|
|
476
|
+
updated_at = ?,
|
|
477
|
+
metadata = ?
|
|
478
|
+
WHERE id = ?
|
|
479
|
+
AND status = 'review'
|
|
480
|
+
`).run(revisedStatus, now, JSON.stringify(metadata), id));
|
|
481
|
+
if (result.changes !== 1) return { revised: false, reason: 'not_updated' };
|
|
482
|
+
const updated = getTask(db, id);
|
|
483
|
+
const event = appendTaskEvent(db, {
|
|
484
|
+
taskId: id,
|
|
485
|
+
workspaceRoot: updated.workspace_root,
|
|
486
|
+
actor: actor || null,
|
|
487
|
+
eventType: 'revision_requested',
|
|
488
|
+
payload: {
|
|
489
|
+
note: text,
|
|
490
|
+
approval_status: 'revise',
|
|
491
|
+
revision_count: revisionCount,
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
return { revised: true, event, row: updated };
|
|
495
|
+
}
|
|
496
|
+
|
|
299
497
|
function appendTaskEvent(db, { taskId, workspaceRoot: ws, actor, eventType, payload }) {
|
|
300
498
|
if (!taskId) throw new Error('taskId required');
|
|
301
499
|
if (!ws) throw new Error('workspaceRoot required');
|
|
@@ -329,16 +527,17 @@ function appendTaskEvent(db, { taskId, workspaceRoot: ws, actor, eventType, payl
|
|
|
329
527
|
};
|
|
330
528
|
}
|
|
331
529
|
|
|
332
|
-
function listTaskEvents(db, { taskId, workspaceRoot: ws, limit }) {
|
|
530
|
+
function listTaskEvents(db, { taskId, workspaceRoot: ws, limit, order = 'asc' }) {
|
|
333
531
|
const where = [];
|
|
334
532
|
const args = [];
|
|
335
533
|
if (taskId) { where.push('task_id = ?'); args.push(taskId); }
|
|
336
534
|
if (ws) { where.push('workspace_root = ?'); args.push(ws); }
|
|
535
|
+
const sort = String(order || '').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
337
536
|
const sql = `
|
|
338
537
|
SELECT event_id, task_id, version, workspace_root, actor, event_type, payload, created_at
|
|
339
538
|
FROM task_events
|
|
340
539
|
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
341
|
-
ORDER BY created_at
|
|
540
|
+
ORDER BY created_at ${sort}, version ${sort}
|
|
342
541
|
${limit ? 'LIMIT ' + Number(limit) : ''}
|
|
343
542
|
`;
|
|
344
543
|
return db.prepare(sql).all(...args).map(r => ({
|
|
@@ -363,17 +562,34 @@ function noteTask(db, { id, actor, content }) {
|
|
|
363
562
|
return { noted: true, event };
|
|
364
563
|
}
|
|
365
564
|
|
|
366
|
-
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof }) {
|
|
565
|
+
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, careerXpEligible = false, clearedFields = [] }) {
|
|
367
566
|
if (!id) throw new Error('id required');
|
|
368
567
|
const row = getTask(db, id);
|
|
369
568
|
if (!row) return { reviewed: false, reason: 'not_found' };
|
|
370
569
|
const numericReward = Number.isFinite(Number(reward)) ? Number(reward) : 0;
|
|
570
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
571
|
+
if (numericReward > 0 && row.status === 'done') {
|
|
572
|
+
metadata.approval_status = 'accepted';
|
|
573
|
+
metadata.accepted_at = new Date().toISOString();
|
|
574
|
+
metadata.accepted_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
575
|
+
withBusyRetry(() => db.prepare(`
|
|
576
|
+
UPDATE tasks
|
|
577
|
+
SET metadata = ?,
|
|
578
|
+
updated_at = ?
|
|
579
|
+
WHERE id = ?
|
|
580
|
+
`).run(JSON.stringify(metadata), Date.now(), id));
|
|
581
|
+
}
|
|
371
582
|
const payload = {
|
|
372
583
|
reward: numericReward,
|
|
373
584
|
lesson: String(lesson || '').trim(),
|
|
374
585
|
next_task: String(nextTask || '').trim() || null,
|
|
375
586
|
proof: String(proof || '').trim() || null,
|
|
587
|
+
career_xp_eligible: Boolean(careerXpEligible),
|
|
376
588
|
};
|
|
589
|
+
const clearedReviewFields = Array.isArray(clearedFields)
|
|
590
|
+
? Array.from(new Set(clearedFields.filter(field => field === 'lesson' || field === 'next_task')))
|
|
591
|
+
: [];
|
|
592
|
+
if (clearedReviewFields.length) payload.cleared_review_fields = clearedReviewFields;
|
|
377
593
|
const event = appendTaskEvent(db, {
|
|
378
594
|
taskId: id,
|
|
379
595
|
workspaceRoot: row.workspace_root,
|
|
@@ -381,12 +597,44 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof }) {
|
|
|
381
597
|
eventType: 'reviewed',
|
|
382
598
|
payload,
|
|
383
599
|
});
|
|
384
|
-
const episode = taskEpisodeFromReview(row, event, payload);
|
|
600
|
+
const episode = taskEpisodeFromReview({ ...row, metadata }, event, payload);
|
|
385
601
|
appendTaskEpisode(row.workspace_root, episode);
|
|
386
602
|
return { reviewed: true, event, episode };
|
|
387
603
|
}
|
|
388
604
|
|
|
605
|
+
function compactEpisodeText(value, max = 240) {
|
|
606
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
607
|
+
if (!text) return null;
|
|
608
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function goalSignalFromTaskMetadata(metadata) {
|
|
612
|
+
const goalId = compactEpisodeText(metadata.goal_id || metadata.goalId || metadata.goal?.id || '', 120);
|
|
613
|
+
const objective = compactEpisodeText(
|
|
614
|
+
metadata.goal_objective || metadata.goalObjective || metadata.goal?.objective || metadata.goal || '',
|
|
615
|
+
240,
|
|
616
|
+
);
|
|
617
|
+
if (!goalId && !objective) return null;
|
|
618
|
+
return {
|
|
619
|
+
goal_id: goalId,
|
|
620
|
+
objective,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function reviewOutcomeLabel(reward) {
|
|
625
|
+
const value = Number(reward);
|
|
626
|
+
if (!Number.isFinite(value)) return 'reviewed';
|
|
627
|
+
if (value > 0) return 'accepted';
|
|
628
|
+
if (value < 0) return 'rejected';
|
|
629
|
+
return 'revised';
|
|
630
|
+
}
|
|
631
|
+
|
|
389
632
|
function taskEpisodeFromReview(row, event, payload) {
|
|
633
|
+
const metadata = row.metadata || {};
|
|
634
|
+
const rewardValue = Number(payload.reward);
|
|
635
|
+
const hasProof = Boolean(String(payload.proof || '').trim());
|
|
636
|
+
const label = reviewOutcomeLabel(payload.reward);
|
|
637
|
+
const doneForXp = row.status === 'done';
|
|
390
638
|
return {
|
|
391
639
|
schema: 'atris.task_episode.v1',
|
|
392
640
|
episode_id: event.event_id,
|
|
@@ -398,7 +646,7 @@ function taskEpisodeFromReview(row, event, payload) {
|
|
|
398
646
|
status: row.status,
|
|
399
647
|
tag: row.tag,
|
|
400
648
|
claimed_by: row.claimed_by,
|
|
401
|
-
metadata
|
|
649
|
+
metadata,
|
|
402
650
|
},
|
|
403
651
|
action: {
|
|
404
652
|
event_type: 'reviewed',
|
|
@@ -412,6 +660,21 @@ function taskEpisodeFromReview(row, event, payload) {
|
|
|
412
660
|
lesson: payload.lesson,
|
|
413
661
|
proof: payload.proof,
|
|
414
662
|
next_task_suggestion: payload.next_task,
|
|
663
|
+
goal: goalSignalFromTaskMetadata(metadata),
|
|
664
|
+
career_xp: {
|
|
665
|
+
eligible: payload.career_xp_eligible === true && label === 'accepted' && hasProof && doneForXp,
|
|
666
|
+
source: 'task_review',
|
|
667
|
+
reward: Number.isFinite(rewardValue) ? rewardValue : 0,
|
|
668
|
+
proof_required: true,
|
|
669
|
+
},
|
|
670
|
+
rl: {
|
|
671
|
+
label,
|
|
672
|
+
source: 'task_review',
|
|
673
|
+
reward: Number.isFinite(rewardValue) ? rewardValue : 0,
|
|
674
|
+
has_proof: hasProof,
|
|
675
|
+
has_lesson: Boolean(String(payload.lesson || '').trim()),
|
|
676
|
+
has_next_task: Boolean(String(payload.next_task || '').trim()),
|
|
677
|
+
},
|
|
415
678
|
};
|
|
416
679
|
}
|
|
417
680
|
|
|
@@ -422,10 +685,81 @@ function appendTaskEpisode(workspaceRoot, episode) {
|
|
|
422
685
|
return filePath;
|
|
423
686
|
}
|
|
424
687
|
|
|
425
|
-
function
|
|
688
|
+
function clipProjectionText(value, max = PROJECTION_PAYLOAD_TEXT_LIMIT) {
|
|
689
|
+
const text = String(value || '');
|
|
690
|
+
if (text.length <= max) return text;
|
|
691
|
+
return `${text.slice(0, Math.max(0, max - 1))}…`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function compactProjectionPayload(value) {
|
|
695
|
+
if (typeof value === 'string') return clipProjectionText(value);
|
|
696
|
+
if (!value || typeof value !== 'object') return value;
|
|
697
|
+
if (Array.isArray(value)) return value.slice(0, 20).map(compactProjectionPayload);
|
|
698
|
+
const out = {};
|
|
699
|
+
for (const [key, item] of Object.entries(value)) {
|
|
700
|
+
if (typeof item === 'string') out[key] = clipProjectionText(item);
|
|
701
|
+
else if (item && typeof item === 'object') out[key] = compactProjectionPayload(item);
|
|
702
|
+
else out[key] = item;
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function compactProjectionEvent(event) {
|
|
708
|
+
return {
|
|
709
|
+
...event,
|
|
710
|
+
payload: compactProjectionPayload(event.payload),
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function selectProjectionRows(rows, { taskId, includeHistory, doneLimit }) {
|
|
715
|
+
if (taskId || includeHistory) {
|
|
716
|
+
return {
|
|
717
|
+
visibleRows: rows,
|
|
718
|
+
hiddenDoneCount: 0,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const visibleRows = [];
|
|
722
|
+
let shownDone = 0;
|
|
723
|
+
let hiddenDoneCount = 0;
|
|
724
|
+
for (const row of rows) {
|
|
725
|
+
if (row.status === 'done') {
|
|
726
|
+
if (shownDone < doneLimit) {
|
|
727
|
+
visibleRows.push(row);
|
|
728
|
+
shownDone += 1;
|
|
729
|
+
} else {
|
|
730
|
+
hiddenDoneCount += 1;
|
|
731
|
+
}
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
visibleRows.push(row);
|
|
735
|
+
}
|
|
736
|
+
return { visibleRows, hiddenDoneCount };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function taskProjection(db, {
|
|
740
|
+
workspaceRoot: ws,
|
|
741
|
+
taskId,
|
|
742
|
+
limit = 500,
|
|
743
|
+
includeHistory = Boolean(taskId),
|
|
744
|
+
doneLimit = PROJECTION_DONE_LIMIT,
|
|
745
|
+
eventLimit = PROJECTION_EVENT_LIMIT,
|
|
746
|
+
messageLimit = PROJECTION_MESSAGE_LIMIT,
|
|
747
|
+
} = {}) {
|
|
426
748
|
const rows = taskId
|
|
427
749
|
? [getTask(db, taskId)].filter(Boolean)
|
|
428
750
|
: listTasks(db, { workspaceRoot: ws || null, limit });
|
|
751
|
+
const refRows = taskId && rows[0]
|
|
752
|
+
? listTasks(db, { workspaceRoot: rows[0].workspace_root })
|
|
753
|
+
: listTasks(db, { workspaceRoot: ws || null });
|
|
754
|
+
const refById = new Map(withTaskDisplayRefs(refRows).map(row => [row.id, {
|
|
755
|
+
display_id: row.display_id,
|
|
756
|
+
legacy_ref: row.legacy_ref,
|
|
757
|
+
}]));
|
|
758
|
+
const { visibleRows, hiddenDoneCount } = selectProjectionRows(rows, {
|
|
759
|
+
taskId,
|
|
760
|
+
includeHistory,
|
|
761
|
+
doneLimit: Math.max(0, Number(doneLimit) || 0),
|
|
762
|
+
});
|
|
429
763
|
const events = listTaskEvents(db, {
|
|
430
764
|
taskId: taskId || null,
|
|
431
765
|
workspaceRoot: taskId ? null : (ws || null),
|
|
@@ -440,19 +774,32 @@ function taskProjection(db, { workspaceRoot: ws, taskId, limit = 500 } = {}) {
|
|
|
440
774
|
schema: 'atris.task_projection.v1',
|
|
441
775
|
generated_at: new Date().toISOString(),
|
|
442
776
|
workspace_root: ws || (rows[0] && rows[0].workspace_root) || null,
|
|
443
|
-
|
|
777
|
+
surface: {
|
|
778
|
+
compact: !includeHistory,
|
|
779
|
+
full_task_count: rows.length,
|
|
780
|
+
visible_task_count: visibleRows.length,
|
|
781
|
+
hidden_done_count: hiddenDoneCount,
|
|
782
|
+
done_limit: includeHistory ? null : Math.max(0, Number(doneLimit) || 0),
|
|
783
|
+
event_limit: includeHistory ? null : Math.max(0, Number(eventLimit) || 0),
|
|
784
|
+
message_limit: includeHistory ? null : Math.max(0, Number(messageLimit) || 0),
|
|
785
|
+
full_ledger_command: taskId ? `atris task events ${taskId}` : 'atris task events --all',
|
|
786
|
+
},
|
|
787
|
+
tasks: visibleRows.map(row => {
|
|
444
788
|
const taskEvents = byTask.get(row.id) || [];
|
|
445
789
|
const latest = taskEvents.length ? taskEvents[taskEvents.length - 1] : null;
|
|
446
|
-
const
|
|
790
|
+
const allMessages = taskEvents
|
|
447
791
|
.filter(e => e.event_type === 'message')
|
|
448
792
|
.map(e => ({
|
|
449
793
|
version: e.version,
|
|
450
794
|
actor: e.actor,
|
|
451
|
-
content: e.payload && e.payload.content || '',
|
|
795
|
+
content: clipProjectionText(e.payload && e.payload.content || ''),
|
|
452
796
|
created_at: e.created_at,
|
|
453
797
|
}));
|
|
798
|
+
const visibleMessages = includeHistory ? allMessages : allMessages.slice(-Math.max(0, Number(messageLimit) || 0));
|
|
799
|
+
const visibleEvents = includeHistory ? taskEvents : taskEvents.slice(-Math.max(0, Number(eventLimit) || 0)).map(compactProjectionEvent);
|
|
454
800
|
return {
|
|
455
801
|
id: row.id,
|
|
802
|
+
...(refById.get(row.id) || {}),
|
|
456
803
|
title: row.title,
|
|
457
804
|
status: row.status,
|
|
458
805
|
tag: row.tag,
|
|
@@ -464,25 +811,47 @@ function taskProjection(db, { workspaceRoot: ws, taskId, limit = 500 } = {}) {
|
|
|
464
811
|
metadata: row.metadata || {},
|
|
465
812
|
current_version: latest ? latest.version : 0,
|
|
466
813
|
latest_event_type: latest ? latest.event_type : null,
|
|
467
|
-
messages,
|
|
468
|
-
events:
|
|
814
|
+
messages: visibleMessages,
|
|
815
|
+
events: visibleEvents,
|
|
816
|
+
history: {
|
|
817
|
+
event_count: taskEvents.length,
|
|
818
|
+
message_count: allMessages.length,
|
|
819
|
+
events_visible: visibleEvents.length,
|
|
820
|
+
messages_visible: visibleMessages.length,
|
|
821
|
+
events_truncated: !includeHistory && taskEvents.length > visibleEvents.length,
|
|
822
|
+
messages_truncated: !includeHistory && allMessages.length > visibleMessages.length,
|
|
823
|
+
},
|
|
469
824
|
};
|
|
470
825
|
}),
|
|
471
826
|
};
|
|
472
827
|
}
|
|
473
828
|
|
|
474
|
-
function renderTodoMarkdown(rows, { title = 'TODO.md' } = {}) {
|
|
829
|
+
function renderTodoMarkdown(rows, { title = 'TODO.md', doneLimit = TODO_RENDER_DONE_LIMIT, refRows = rows, preservedSections = [] } = {}) {
|
|
830
|
+
const displayRows = withTaskDisplayRefs(rows, refRows);
|
|
475
831
|
const buckets = {
|
|
476
|
-
open:
|
|
477
|
-
claimed:
|
|
478
|
-
|
|
479
|
-
|
|
832
|
+
open: displayRows.filter(r => r.status === 'open'),
|
|
833
|
+
claimed: displayRows.filter(r => r.status === 'claimed'),
|
|
834
|
+
review: displayRows.filter(r => r.status === 'review'),
|
|
835
|
+
failed: displayRows.filter(r => r.status === 'failed'),
|
|
836
|
+
done: displayRows.filter(r => r.status === 'done'),
|
|
480
837
|
};
|
|
481
838
|
const lines = [`# ${title}`, '', '> Regenerated from durable Atris task state. Do not treat this file as truth.', ''];
|
|
839
|
+
for (const section of preservedSections) {
|
|
840
|
+
const text = String(section || '').trim();
|
|
841
|
+
if (!text) continue;
|
|
842
|
+
lines.push(text, '');
|
|
843
|
+
}
|
|
482
844
|
appendSection(lines, 'Backlog', buckets.open);
|
|
483
845
|
appendSection(lines, 'In Progress', buckets.claimed);
|
|
846
|
+
appendSection(lines, 'Review', buckets.review);
|
|
484
847
|
appendSection(lines, 'Blocked', buckets.failed);
|
|
485
|
-
|
|
848
|
+
const renderedDone = buckets.done.slice(0, Math.max(0, Number(doneLimit) || 0));
|
|
849
|
+
appendSection(lines, 'Completed', renderedDone);
|
|
850
|
+
const archivedDone = Math.max(0, buckets.done.length - renderedDone.length);
|
|
851
|
+
if (archivedDone > 0) {
|
|
852
|
+
lines.push(`(${archivedDone} older completed task${archivedDone === 1 ? '' : 's'} archived in \`atris task list --status done\` and \`atris task events\`.)`, '');
|
|
853
|
+
}
|
|
854
|
+
while (lines[lines.length - 1] === '') lines.pop();
|
|
486
855
|
return lines.join('\n') + '\n';
|
|
487
856
|
}
|
|
488
857
|
|
|
@@ -493,10 +862,16 @@ function appendSection(lines, name, rows) {
|
|
|
493
862
|
return;
|
|
494
863
|
}
|
|
495
864
|
for (const row of rows) {
|
|
496
|
-
const tag = row.tag ? ` [${row.tag}]` : '';
|
|
497
|
-
lines.push(`- **[${row.id}]** ${row.title}${tag}`);
|
|
498
|
-
if (row.claimed_by && row.status === 'claimed') lines.push(` **Claimed by:** ${row.claimed_by}`);
|
|
499
865
|
const meta = row.metadata || {};
|
|
866
|
+
const tags = Array.isArray(meta.todo_tags) && meta.todo_tags.length
|
|
867
|
+
? meta.todo_tags
|
|
868
|
+
: (row.tag ? [row.tag] : []);
|
|
869
|
+
const tag = [...new Set(tags.filter(Boolean).map(value => String(value).trim()).filter(Boolean))]
|
|
870
|
+
.map(value => ` [${value}]`)
|
|
871
|
+
.join('');
|
|
872
|
+
const displayRef = meta.todo_id || row.display_id || row.id;
|
|
873
|
+
lines.push(`- **[${displayRef}]** ${row.title}${tag}`);
|
|
874
|
+
if (row.claimed_by && row.status === 'claimed') lines.push(` **Claimed by:** ${row.claimed_by}`);
|
|
500
875
|
if (meta.verify) lines.push(` **Verify:** ${meta.verify}`);
|
|
501
876
|
}
|
|
502
877
|
lines.push('');
|
|
@@ -543,12 +918,18 @@ module.exports = {
|
|
|
543
918
|
listTasks,
|
|
544
919
|
claimTask,
|
|
545
920
|
doneTask,
|
|
921
|
+
readyTask,
|
|
922
|
+
reviseTask,
|
|
546
923
|
noteTask,
|
|
547
924
|
reviewTask,
|
|
548
925
|
appendTaskEvent,
|
|
549
926
|
listTaskEvents,
|
|
550
927
|
taskProjection,
|
|
551
928
|
renderTodoMarkdown,
|
|
929
|
+
normalizeTaskRef,
|
|
930
|
+
shortestUniqueTaskRef,
|
|
931
|
+
taskDisplayRefMap,
|
|
932
|
+
withTaskDisplayRefs,
|
|
552
933
|
newId,
|
|
553
934
|
// Test surface
|
|
554
935
|
_SCHEMA: SCHEMA,
|
package/lib/todo-fallback.js
CHANGED
|
@@ -10,11 +10,12 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
12
|
function parseTodoFile(todoPath) {
|
|
13
|
-
if (!fs.existsSync(todoPath)) return { backlog: [], inProgress: [], completed: [] };
|
|
13
|
+
if (!fs.existsSync(todoPath)) return { backlog: [], inProgress: [], review: [], completed: [] };
|
|
14
14
|
const content = fs.readFileSync(todoPath, 'utf8');
|
|
15
15
|
return {
|
|
16
16
|
backlog: parseSection(content, 'Backlog'),
|
|
17
17
|
inProgress: parseSection(content, 'In Progress'),
|
|
18
|
+
review: parseSection(content, 'Review'),
|
|
18
19
|
completed: parseSection(content, 'Completed'),
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -37,7 +38,7 @@ function cleanTaskTitle(text) {
|
|
|
37
38
|
|
|
38
39
|
function parseSection(content, sectionName) {
|
|
39
40
|
const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
|
-
const match = content.match(new RegExp(`(?:^|\\n)##\\s+${escaped}
|
|
41
|
+
const match = content.match(new RegExp(`(?:^|\\n)##\\s+${escaped}[^\\n]*\\n([\\s\\S]*?)(?=\\n##(?!#)\\s+|$)`, 'i'));
|
|
41
42
|
if (!match) return [];
|
|
42
43
|
|
|
43
44
|
const body = (match[1] || '').trim();
|
|
@@ -50,6 +51,15 @@ function parseSection(content, sectionName) {
|
|
|
50
51
|
for (const rawLine of lines) {
|
|
51
52
|
const line = rawLine.trimEnd();
|
|
52
53
|
|
|
54
|
+
// Strikethrough = done. `- ~~**id:** ...~~ DONE ...` lines are kept in
|
|
55
|
+
// Backlog for rollback context but must not be picked as live work.
|
|
56
|
+
// Otherwise the autopilot picker re-selects them every tick and halts on
|
|
57
|
+
// "no Verify: field" (lessons.md: no-verify-field, 8 occurrences 2026-05-08..10).
|
|
58
|
+
if (/^- ~~/.test(line)) {
|
|
59
|
+
if (current) { tasks.push(current); current = null; }
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
const taskMatch = line.match(/^- \*\*([^*:\n]+):\*\*\s*(.+)$/);
|
|
54
64
|
if (taskMatch) {
|
|
55
65
|
if (current) tasks.push(current);
|
|
@@ -66,6 +76,22 @@ function parseSection(content, sectionName) {
|
|
|
66
76
|
continue;
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
const bracketTaskMatch = line.match(/^- \*\*\[([^\]\n]+)\]\*\*\s+(.+)$/);
|
|
80
|
+
if (bracketTaskMatch) {
|
|
81
|
+
if (current) tasks.push(current);
|
|
82
|
+
const { allTags, tag } = tagsFromText(bracketTaskMatch[2]);
|
|
83
|
+
current = {
|
|
84
|
+
id: bracketTaskMatch[1],
|
|
85
|
+
title: cleanTaskTitle(bracketTaskMatch[2]),
|
|
86
|
+
tag,
|
|
87
|
+
tags: allTags,
|
|
88
|
+
claimed: null,
|
|
89
|
+
stage: null,
|
|
90
|
+
verify: null,
|
|
91
|
+
};
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
const checkMatch = line.match(/^- \[[ x]\]\s+(.+)$/);
|
|
70
96
|
if (checkMatch) {
|
|
71
97
|
if (current) {
|