atris 3.33.3 → 3.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +0 -2
- package/FOR_AGENTS.md +5 -3
- package/atris/skills/engines/SKILL.md +16 -7
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris.md +19 -0
- package/ax +475 -17
- package/bin/atris.js +206 -44
- package/commands/aeo.js +52 -0
- package/commands/autoland.js +313 -19
- package/commands/business.js +91 -8
- package/commands/codex-goal.js +26 -2
- package/commands/drive.js +187 -0
- package/commands/engine.js +73 -2
- package/commands/feed.js +202 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +262 -3
- package/commands/init.js +13 -1
- package/commands/integrations.js +39 -11
- package/commands/interview.js +143 -0
- package/commands/land.js +114 -13
- package/commands/lesson.js +112 -1
- package/commands/linear.js +38 -0
- package/commands/loops.js +212 -0
- package/commands/member.js +398 -42
- package/commands/mission.js +598 -64
- package/commands/now.js +25 -1
- package/commands/radar.js +259 -14
- package/commands/serve.js +54 -0
- package/commands/status.js +50 -5
- package/commands/stripe.js +38 -0
- package/commands/supabase.js +39 -0
- package/commands/task.js +935 -71
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/worktree.js +68 -10
- package/commands/write.js +399 -0
- package/lib/auto-accept-certified.js +70 -19
- package/lib/autoland.js +39 -3
- package/lib/fleet.js +256 -13
- package/lib/memory-view.js +14 -5
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/official-cli-integration.js +174 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/review-integrity.js +147 -0
- package/lib/runner-command.js +23 -0
- package/lib/task-db.js +220 -7
- package/lib/task-proof.js +20 -0
- package/lib/task-receipt.js +93 -0
- package/package.json +1 -1
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/lib/task-db.js
CHANGED
|
@@ -27,6 +27,7 @@ const path = require('path');
|
|
|
27
27
|
const os = require('os');
|
|
28
28
|
const crypto = require('crypto');
|
|
29
29
|
const { DatabaseSync } = require('node:sqlite');
|
|
30
|
+
const reviewIntegrity = require('./review-integrity');
|
|
30
31
|
|
|
31
32
|
const DEFAULT_DB_PATH = path.join(os.homedir(), '.atris', 'tasks.db');
|
|
32
33
|
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
@@ -108,7 +109,10 @@ function appendTaskCompletionLogs(db, row, { status, actor, action, proof } = {}
|
|
|
108
109
|
const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
|
|
109
110
|
const proofText = compactLogText(proof || metadata.latest_agent_proof || metadata.verify || '', 500);
|
|
110
111
|
const member = existingMemberSlug(row.workspace_root, row, actor);
|
|
111
|
-
const title = status === '
|
|
112
|
+
const title = status === 'archived' ? 'Task archived'
|
|
113
|
+
: status === 'failed' ? 'Task failed'
|
|
114
|
+
: action === 'accepted' ? 'Task accepted'
|
|
115
|
+
: 'Task completed';
|
|
112
116
|
const fields = {
|
|
113
117
|
task: ref,
|
|
114
118
|
title: row.title,
|
|
@@ -413,7 +417,7 @@ function listTasks(db, { workspaceRoot: ws, status, claimedBy, limit }) {
|
|
|
413
417
|
FROM tasks
|
|
414
418
|
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
415
419
|
ORDER BY
|
|
416
|
-
CASE status WHEN 'open' THEN 0 WHEN 'claimed' THEN 1 WHEN 'review' THEN 2 WHEN 'failed' THEN 3 WHEN 'done' THEN 4
|
|
420
|
+
CASE status WHEN 'open' THEN 0 WHEN 'claimed' THEN 1 WHEN 'review' THEN 2 WHEN 'failed' THEN 3 WHEN 'done' THEN 4 WHEN 'archived' THEN 5 ELSE 6 END,
|
|
417
421
|
CASE WHEN tag='endgame' THEN 0 ELSE 1 END,
|
|
418
422
|
created_at DESC
|
|
419
423
|
${limit ? 'LIMIT ' + Number(limit) : ''}
|
|
@@ -506,10 +510,13 @@ function releaseTask(db, { id, actor }) {
|
|
|
506
510
|
return { released: true, row: updated, event };
|
|
507
511
|
}
|
|
508
512
|
|
|
509
|
-
function doneTask(db, { id, status, actor, allowReview = false, action, proof } = {}) {
|
|
513
|
+
function doneTask(db, { id, status, actor, allowReview = false, action, proof, autoAccepted = false } = {}) {
|
|
510
514
|
if (!id) throw new Error('id required');
|
|
511
515
|
const final = status || 'done';
|
|
512
516
|
if (!['done', 'failed'].includes(final)) throw new Error('status must be done|failed');
|
|
517
|
+
// 'archived' is a distinct terminal status written by archiveTask(), never
|
|
518
|
+
// by this function — a bulk sweep of duplicate/off-roadmap tasks must not
|
|
519
|
+
// read as a real failure. See archiveTask() below.
|
|
513
520
|
const now = Date.now();
|
|
514
521
|
const allowedStatuses = allowReview ? "'open', 'claimed', 'review'" : "'open', 'claimed'";
|
|
515
522
|
const result = withBusyRetry(() => db.prepare(`
|
|
@@ -525,7 +532,11 @@ function doneTask(db, { id, status, actor, allowReview = false, action, proof }
|
|
|
525
532
|
workspaceRoot: row.workspace_root,
|
|
526
533
|
actor: actor || process.env.ATRIS_AGENT_ID || process.env.USER || null,
|
|
527
534
|
eventType: final === 'done' ? 'completed' : 'blocked',
|
|
528
|
-
payload: {
|
|
535
|
+
payload: {
|
|
536
|
+
status: final,
|
|
537
|
+
action: action || final,
|
|
538
|
+
...(autoAccepted ? { auto_accepted: true } : {}),
|
|
539
|
+
},
|
|
529
540
|
});
|
|
530
541
|
const logs = appendTaskCompletionLogs(db, row, {
|
|
531
542
|
status: final,
|
|
@@ -538,11 +549,141 @@ function doneTask(db, { id, status, actor, allowReview = false, action, proof }
|
|
|
538
549
|
return { updated: false };
|
|
539
550
|
}
|
|
540
551
|
|
|
552
|
+
// Distinct terminal status for housekeeping sweeps (duplicate loop-ticks,
|
|
553
|
+
// off-roadmap backlog resets, synthetic-test cleanup). Never conflate this
|
|
554
|
+
// with `failed` — 'failed' means the work itself did not succeed, 'archived'
|
|
555
|
+
// means the work was swept off the board for reasons unrelated to whether it
|
|
556
|
+
// succeeded. Reward/RSI signal readers must treat the two differently.
|
|
557
|
+
// `fromFailed: true` is an explicit opt-in for sanctioned historical cleanup
|
|
558
|
+
// (e.g. the 109 duplicate "Loop tick:" orphans fail-closed before 'archived'
|
|
559
|
+
// existed — cluster 1 of failed-tasks-analysis-2026-07-03). It permits the
|
|
560
|
+
// failed→archived transition and records the prior status in
|
|
561
|
+
// metadata.archived_from. 'done' rows are never archivable: accepted work
|
|
562
|
+
// stays done, with or without the flag.
|
|
563
|
+
function archiveTask(db, { id, actor, reason, fromFailed = false } = {}) {
|
|
564
|
+
if (!id) throw new Error('id required');
|
|
565
|
+
const reasonText = String(reason || '').trim();
|
|
566
|
+
if (!reasonText) throw new Error('reason required');
|
|
567
|
+
const row = getTask(db, id);
|
|
568
|
+
if (!row) return { archived: false, reason: 'not_found' };
|
|
569
|
+
const allowedStatuses = fromFailed
|
|
570
|
+
? ['open', 'claimed', 'review', 'failed']
|
|
571
|
+
: ['open', 'claimed', 'review'];
|
|
572
|
+
if (!allowedStatuses.includes(row.status)) {
|
|
573
|
+
return { archived: false, reason: `already_${row.status}` };
|
|
574
|
+
}
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
577
|
+
metadata.archived_reason = reasonText;
|
|
578
|
+
metadata.archived_at = new Date(now).toISOString();
|
|
579
|
+
metadata.archived_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
580
|
+
metadata.approval_status = 'archived';
|
|
581
|
+
if (row.status === 'failed') metadata.archived_from = 'failed';
|
|
582
|
+
const statusPlaceholders = allowedStatuses.map(() => '?').join(', ');
|
|
583
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
584
|
+
UPDATE tasks
|
|
585
|
+
SET status = 'archived',
|
|
586
|
+
done_at = ?,
|
|
587
|
+
updated_at = ?,
|
|
588
|
+
metadata = ?
|
|
589
|
+
WHERE id = ?
|
|
590
|
+
AND status IN (${statusPlaceholders})
|
|
591
|
+
`).run(now, now, JSON.stringify(metadata), id, ...allowedStatuses));
|
|
592
|
+
if (result.changes !== 1) return { archived: false, reason: 'stale_task_state' };
|
|
593
|
+
const updated = getTask(db, id);
|
|
594
|
+
const event = appendTaskEvent(db, {
|
|
595
|
+
taskId: id,
|
|
596
|
+
workspaceRoot: updated.workspace_root,
|
|
597
|
+
actor: metadata.archived_by,
|
|
598
|
+
eventType: 'archived',
|
|
599
|
+
payload: { reason: reasonText, ...(row.status === 'failed' ? { previous_status: 'failed' } : {}) },
|
|
600
|
+
});
|
|
601
|
+
const logs = appendTaskCompletionLogs(db, updated, {
|
|
602
|
+
status: 'archived',
|
|
603
|
+
actor: metadata.archived_by,
|
|
604
|
+
action: 'archived',
|
|
605
|
+
proof: reasonText,
|
|
606
|
+
});
|
|
607
|
+
return { archived: true, event, row: updated, logs };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Marks metadata written by the 2026-06-10 "first-principles backlog reset"
|
|
611
|
+
// (see atris/logs/2026/2026-06-10.md) — a bulk pass that closed off-roadmap
|
|
612
|
+
// tasks, including certified/proof-backed done work, by writing status
|
|
613
|
+
// 'failed' because no distinct 'archived' status existed yet. This
|
|
614
|
+
// identifies exactly those rows so relabelArchivedTasks() can fix the label
|
|
615
|
+
// without touching any task that failed for a real reason.
|
|
616
|
+
function isJune10BacklogResetMarker(metadata) {
|
|
617
|
+
const m = metadata && typeof metadata === 'object' ? metadata : {};
|
|
618
|
+
if (!m.archived_at) return false;
|
|
619
|
+
return /first-principles backlog reset 2026-06-10/i.test(String(m.archive_reason || ''));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// One-time migration (OBL-1622): relabel the June-10 mislabeled rows from
|
|
623
|
+
// 'failed' to 'archived'. Dry-run only counts/samples; --apply performs the
|
|
624
|
+
// same status write path as archiveTask() (direct projection UPDATE +
|
|
625
|
+
// appendTaskEvent), never a raw projection-JSON edit.
|
|
626
|
+
function relabelArchivedTasks(db, { workspaceRoot: ws, apply = false, actor, limit = 5000 } = {}) {
|
|
627
|
+
const candidates = listTasks(db, { workspaceRoot: ws || null, status: 'failed', limit })
|
|
628
|
+
.filter(row => isJune10BacklogResetMarker(row.metadata));
|
|
629
|
+
const sample = candidates.slice(0, 10).map(row => ({ id: row.id, title: row.title }));
|
|
630
|
+
if (!apply) {
|
|
631
|
+
return { applied: false, count: candidates.length, sample };
|
|
632
|
+
}
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
const actorText = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
635
|
+
const relabeledIds = [];
|
|
636
|
+
for (const row of candidates) {
|
|
637
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
638
|
+
metadata.relabeled_from_status = 'failed';
|
|
639
|
+
metadata.relabeled_at = new Date(now).toISOString();
|
|
640
|
+
metadata.relabeled_by = actorText;
|
|
641
|
+
metadata.relabel_reason = 'obl_1622_june_10_backlog_reset_relabel';
|
|
642
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
643
|
+
UPDATE tasks
|
|
644
|
+
SET status = 'archived',
|
|
645
|
+
updated_at = ?,
|
|
646
|
+
metadata = ?
|
|
647
|
+
WHERE id = ?
|
|
648
|
+
AND status = 'failed'
|
|
649
|
+
`).run(now, JSON.stringify(metadata), row.id));
|
|
650
|
+
if (result.changes === 1) {
|
|
651
|
+
appendTaskEvent(db, {
|
|
652
|
+
taskId: row.id,
|
|
653
|
+
workspaceRoot: row.workspace_root,
|
|
654
|
+
actor: actorText,
|
|
655
|
+
eventType: 'relabeled_archived',
|
|
656
|
+
payload: { previous_status: 'failed', reason: 'obl_1622_june_10_backlog_reset_relabel' },
|
|
657
|
+
});
|
|
658
|
+
relabeledIds.push(row.id);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return { applied: true, count: relabeledIds.length, ids: relabeledIds, sample };
|
|
662
|
+
}
|
|
663
|
+
|
|
541
664
|
function cleanLandingText(value) {
|
|
542
665
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
543
666
|
return text || null;
|
|
544
667
|
}
|
|
545
668
|
|
|
669
|
+
// Record who delivered this review pass and answer whether certification is
|
|
670
|
+
// allowed: at least one recorded pass actor must differ from the builder.
|
|
671
|
+
// The first pass actor stamps built_by; judge != worker from then on.
|
|
672
|
+
function recordReviewPassActor(metadata, claimedBy, rawActor) {
|
|
673
|
+
const actorNorm = reviewIntegrity.normalizeActor(
|
|
674
|
+
rawActor || process.env.ATRIS_AGENT_ID || process.env.USER || ''
|
|
675
|
+
);
|
|
676
|
+
if (!metadata.built_by && actorNorm) metadata.built_by = actorNorm;
|
|
677
|
+
const actors = Array.isArray(metadata.agent_review_actors) ? [...metadata.agent_review_actors] : [];
|
|
678
|
+
if (actorNorm && !actors.includes(actorNorm)) actors.push(actorNorm);
|
|
679
|
+
metadata.agent_review_actors = actors;
|
|
680
|
+
const builder = reviewIntegrity.normalizeActor(metadata.built_by || claimedBy || '');
|
|
681
|
+
const independent = builder
|
|
682
|
+
? (actors.find((a) => a && a !== builder) || null)
|
|
683
|
+
: (actors.length >= 2 ? actors[1] : null);
|
|
684
|
+
return { builder: builder || null, independent };
|
|
685
|
+
}
|
|
686
|
+
|
|
546
687
|
function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resultTrace }) {
|
|
547
688
|
if (!id) throw new Error('id required');
|
|
548
689
|
const text = String(proof || '').trim();
|
|
@@ -567,11 +708,13 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
567
708
|
const cleaned = cleanLandingText(landingInput[key]);
|
|
568
709
|
if (cleaned) metadata[`landing_${key}`] = cleaned;
|
|
569
710
|
}
|
|
570
|
-
|
|
711
|
+
const passRecord = recordReviewPassActor(metadata, row.claimed_by, actor);
|
|
712
|
+
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && passRecord.independent) {
|
|
571
713
|
metadata.agent_certified = true;
|
|
572
714
|
metadata.agent_certified_at = new Date(now).toISOString();
|
|
573
715
|
metadata.agent_certified_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
574
716
|
metadata.agent_certification_policy = `${AGENT_CERTIFICATION_REVIEW_PASSES}_agent_review_passes`;
|
|
717
|
+
metadata.independent_review_by = passRecord.independent;
|
|
575
718
|
}
|
|
576
719
|
const result = withBusyRetry(() => db.prepare(`
|
|
577
720
|
UPDATE tasks
|
|
@@ -1170,6 +1313,61 @@ function noteTask(db, { id, actor, content }) {
|
|
|
1170
1313
|
return { noted: true, event };
|
|
1171
1314
|
}
|
|
1172
1315
|
|
|
1316
|
+
function normalizeTagToken(value) {
|
|
1317
|
+
return String(value == null ? '' : value).trim().toLowerCase();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Update a task's flag list (`metadata.tags`) on an already-created task.
|
|
1321
|
+
// Tags are only settable at creation elsewhere; this is the after-the-fact
|
|
1322
|
+
// escape hatch so an open task that becomes an owner decision can be marked
|
|
1323
|
+
// `needs-human` — which sweep and fleet staffing both honor — instead of
|
|
1324
|
+
// being restaffed forever (CLI-879). Every change appends a
|
|
1325
|
+
// `task_tags_updated` event so the trail shows who flagged it and when.
|
|
1326
|
+
function tagTask(db, { id, actor, add = [], remove = [] } = {}) {
|
|
1327
|
+
if (!id) throw new Error('id required');
|
|
1328
|
+
const addTags = [...new Set((Array.isArray(add) ? add : [add]).map(normalizeTagToken).filter(Boolean))];
|
|
1329
|
+
const removeSet = new Set((Array.isArray(remove) ? remove : [remove]).map(normalizeTagToken).filter(Boolean));
|
|
1330
|
+
if (!addTags.length && !removeSet.size) return { tagged: false, reason: 'no_tags' };
|
|
1331
|
+
const row = getTask(db, id);
|
|
1332
|
+
if (!row) return { tagged: false, reason: 'not_found' };
|
|
1333
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
1334
|
+
const before = [...new Set((Array.isArray(metadata.tags) ? metadata.tags : []).map(normalizeTagToken).filter(Boolean))];
|
|
1335
|
+
const removed = [];
|
|
1336
|
+
const next = before.filter((tag) => {
|
|
1337
|
+
if (removeSet.has(tag)) { removed.push(tag); return false; }
|
|
1338
|
+
return true;
|
|
1339
|
+
});
|
|
1340
|
+
const added = [];
|
|
1341
|
+
for (const tag of addTags) {
|
|
1342
|
+
// An explicit --remove of the same token wins over --add.
|
|
1343
|
+
if (removeSet.has(tag)) continue;
|
|
1344
|
+
if (!next.includes(tag)) { next.push(tag); added.push(tag); }
|
|
1345
|
+
}
|
|
1346
|
+
if (!added.length && !removed.length) {
|
|
1347
|
+
return { tagged: false, reason: 'no_changes', tags: before };
|
|
1348
|
+
}
|
|
1349
|
+
metadata.tags = next;
|
|
1350
|
+
const actorText = cleanStageText(actor) || null;
|
|
1351
|
+
const now = Math.max(Date.now(), Number(row.updated_at || 0) + 1);
|
|
1352
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
1353
|
+
UPDATE tasks
|
|
1354
|
+
SET metadata = ?,
|
|
1355
|
+
updated_at = ?
|
|
1356
|
+
WHERE id = ?
|
|
1357
|
+
AND updated_at = ?
|
|
1358
|
+
`).run(JSON.stringify(metadata), now, id, row.updated_at));
|
|
1359
|
+
if (result.changes !== 1) return { tagged: false, reason: 'stale_task_state' };
|
|
1360
|
+
const updated = getTask(db, id);
|
|
1361
|
+
const event = appendTaskEvent(db, {
|
|
1362
|
+
taskId: id,
|
|
1363
|
+
workspaceRoot: updated.workspace_root,
|
|
1364
|
+
actor: actorText,
|
|
1365
|
+
eventType: 'task_tags_updated',
|
|
1366
|
+
payload: { added, removed, tags: next, previous_tags: before },
|
|
1367
|
+
});
|
|
1368
|
+
return { tagged: true, event, row: updated, tags: next, added, removed };
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1173
1371
|
function taskChatPacketFromPayload(payload) {
|
|
1174
1372
|
const data = payload && typeof payload === 'object' ? payload : {};
|
|
1175
1373
|
const lines = ['TASK_CHAT_UPDATE'];
|
|
@@ -1235,7 +1433,7 @@ function chatTask(db, { id, actor, content, goal, summary }) {
|
|
|
1235
1433
|
};
|
|
1236
1434
|
}
|
|
1237
1435
|
|
|
1238
|
-
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, careerXpEligible = false, clearedFields = [], landing = {} }) {
|
|
1436
|
+
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, careerXpEligible = false, clearedFields = [], landing = {}, autoAccepted = false }) {
|
|
1239
1437
|
if (!id) throw new Error('id required');
|
|
1240
1438
|
const row = getTask(db, id);
|
|
1241
1439
|
if (!row) return { reviewed: false, reason: 'not_found' };
|
|
@@ -1243,6 +1441,14 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1243
1441
|
const now = Date.now();
|
|
1244
1442
|
const reviewer = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
1245
1443
|
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
1444
|
+
const reviewerNorm = reviewIntegrity.normalizeActor(reviewer);
|
|
1445
|
+
const builderNorm = reviewIntegrity.taskBuilder(row);
|
|
1446
|
+
// judge != worker applies pre-acceptance only: a builder cannot positive-
|
|
1447
|
+
// reward their own row while it still sits in review. Done rows are past
|
|
1448
|
+
// the human gate; finish and post-acceptance XP bookkeeping stay open.
|
|
1449
|
+
if (numericReward > 0 && row.status === 'review' && builderNorm && reviewerNorm && reviewerNorm === builderNorm) {
|
|
1450
|
+
return { reviewed: false, reason: 'judge_equals_worker', builder: builderNorm };
|
|
1451
|
+
}
|
|
1246
1452
|
const proofText = String(proof || '').trim();
|
|
1247
1453
|
const verifyText = cleanStageText(verify);
|
|
1248
1454
|
const lessonText = String(lesson || '').trim();
|
|
@@ -1284,11 +1490,13 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1284
1490
|
if (reviewingPendingProof) {
|
|
1285
1491
|
reviewPassCount += 1;
|
|
1286
1492
|
metadata.agent_review_pass_count = reviewPassCount;
|
|
1287
|
-
|
|
1493
|
+
const passRecord = recordReviewPassActor(metadata, row.claimed_by, reviewer);
|
|
1494
|
+
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && passRecord.independent) {
|
|
1288
1495
|
metadata.agent_certified = true;
|
|
1289
1496
|
metadata.agent_certified_at = new Date(now).toISOString();
|
|
1290
1497
|
metadata.agent_certified_by = reviewer;
|
|
1291
1498
|
metadata.agent_certification_policy = `${AGENT_CERTIFICATION_REVIEW_PASSES}_agent_review_passes`;
|
|
1499
|
+
metadata.independent_review_by = passRecord.independent;
|
|
1292
1500
|
}
|
|
1293
1501
|
}
|
|
1294
1502
|
if (numericReward > 0 && row.status === 'done') {
|
|
@@ -1312,6 +1520,7 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1312
1520
|
verify: verifyText || null,
|
|
1313
1521
|
career_xp_eligible: Boolean(careerXpEligible),
|
|
1314
1522
|
};
|
|
1523
|
+
if (autoAccepted) payload.auto_accepted = true;
|
|
1315
1524
|
if (reviewingPendingProof) {
|
|
1316
1525
|
payload.review_pass_count = reviewPassCount;
|
|
1317
1526
|
payload.agent_certified = metadata.agent_certified === true;
|
|
@@ -1762,11 +1971,15 @@ module.exports = {
|
|
|
1762
1971
|
backlogTask,
|
|
1763
1972
|
clearPlanTasks,
|
|
1764
1973
|
doneTask,
|
|
1974
|
+
archiveTask,
|
|
1975
|
+
relabelArchivedTasks,
|
|
1976
|
+
isJune10BacklogResetMarker,
|
|
1765
1977
|
readyTask,
|
|
1766
1978
|
reviseTask,
|
|
1767
1979
|
stageTask,
|
|
1768
1980
|
noteTask,
|
|
1769
1981
|
chatTask,
|
|
1982
|
+
tagTask,
|
|
1770
1983
|
reviewTask,
|
|
1771
1984
|
appendTaskEvent,
|
|
1772
1985
|
listTaskEvents,
|
package/lib/task-proof.js
CHANGED
|
@@ -9,6 +9,8 @@ const RECEIPT_OR_ARTIFACT_RE = /\b(?:receipt|artifact|screenshot|log|trace|path=
|
|
|
9
9
|
const RESULT_PAIR_RE = /\b(?:typecheck|build|smoke|test|pytest|verifier|validation|validated|verified|render|diff|sync|lineage|projection)\b.{0,80}\b(?:pass|passed|failed|green|ok|exit\s*0|reviewed)\b|\b(?:pass|passed|failed|green|ok|exit\s*0|reviewed)\b.{0,80}\b(?:typecheck|build|smoke|test|pytest|verifier|validation|validated|verified|render|diff|sync|lineage|projection)\b/i;
|
|
10
10
|
const HUMAN_PROOF_RE = /\b(?:team human approved|human approved|human approval|approved by|accepted by|reviewed by|customer replied|customer approved|customer accepted|replied)\b/i;
|
|
11
11
|
const FILE_ACTION_RE = /\b(?:changed|updated|edited|created|deleted|saved|wrote|patched|reviewed|verified|validated|opened|read)\b/i;
|
|
12
|
+
const VERIFIED_PROOF_RE = /^\[verified\]\s+`[^`]+`\s+passed\s+\(exit 0\)(?:\s|$)/i;
|
|
13
|
+
const SECOND_ACTOR_EXECUTED_PROOF_RE = /^Second-actor check:\s+`[^`]+`\s+re-run by [^,]+,\s+exited 0(?:[.\s]|$)/i;
|
|
12
14
|
|
|
13
15
|
function compactWhitespace(text) {
|
|
14
16
|
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
@@ -37,6 +39,22 @@ function taskProofLooksMeaningful(proof) {
|
|
|
37
39
|
return taskProofState(proof).ok;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function taskProofExecutionState(proof) {
|
|
43
|
+
const text = compactWhitespace(proof);
|
|
44
|
+
if (!text) return { ok: false, reason: 'proof_not_executed', detail: 'executed proof required' };
|
|
45
|
+
if (VERIFIED_PROOF_RE.test(text)) return { ok: true, reason: 'verified_proof' };
|
|
46
|
+
if (SECOND_ACTOR_EXECUTED_PROOF_RE.test(text)) return { ok: true, reason: 'second_actor_executed_proof' };
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
reason: 'proof_not_executed',
|
|
50
|
+
detail: 'proof must come from an executed verifier, not a free-text claim that tests passed',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function taskProofLooksExecuted(proof) {
|
|
55
|
+
return taskProofExecutionState(proof).ok;
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
function tailText(text, max = 400) {
|
|
41
59
|
const trimmed = String(text || '').trim();
|
|
42
60
|
if (trimmed.length <= max) return trimmed;
|
|
@@ -73,6 +91,8 @@ function buildVerifiedProof(verifyCmd, baseProof = '', runner, options = {}) {
|
|
|
73
91
|
|
|
74
92
|
module.exports = {
|
|
75
93
|
taskProofLooksMeaningful,
|
|
94
|
+
taskProofLooksExecuted,
|
|
76
95
|
taskProofState,
|
|
96
|
+
taskProofExecutionState,
|
|
77
97
|
buildVerifiedProof,
|
|
78
98
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Receipt writer for task proofs. Mirrors the mission receipt shape
|
|
4
|
+
// (commands/mission.js writeReceipt) so lib/receipt-evidence.js can validate
|
|
5
|
+
// paths named in a proof at the review gate: schema tag, `at` timestamp,
|
|
6
|
+
// result.passed. Every call writes a receipt file, pass or fail — a failing
|
|
7
|
+
// verifier still leaves a record, it just can't be cited to move a task to
|
|
8
|
+
// ready.
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
function tailText(text, max = 400) {
|
|
15
|
+
const trimmed = String(text || '').trim();
|
|
16
|
+
if (trimmed.length <= max) return trimmed;
|
|
17
|
+
return `...${trimmed.slice(-max)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function gitCommitHash(root) {
|
|
21
|
+
try {
|
|
22
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
23
|
+
if (result.status === 0) return String(result.stdout || '').trim() || null;
|
|
24
|
+
} catch {
|
|
25
|
+
// ignore — not every workspace is a git repo
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function runsDir(root) {
|
|
31
|
+
return path.join(root, 'atris', 'runs');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function slugifyTaskId(taskId) {
|
|
35
|
+
return String(taskId || 'task')
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
38
|
+
.replace(/^-+|-+$/g, '')
|
|
39
|
+
.slice(0, 40) || 'task';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Runs `command` for `taskId`, writes atris/runs/<date>-task-<id>-<time>.json,
|
|
43
|
+
// and returns { ok, passed, receiptPath, exit, signal, output }. `ok` mirrors
|
|
44
|
+
// `passed` — kept as a separate field so callers reading either name work.
|
|
45
|
+
function writeTaskReceipt({ taskId, command, root = process.cwd(), runner } = {}) {
|
|
46
|
+
const cmd = String(command || '').trim();
|
|
47
|
+
if (!taskId) return { ok: false, passed: false, reason: 'task_id_required' };
|
|
48
|
+
if (!cmd) return { ok: false, passed: false, reason: 'verify_command_required' };
|
|
49
|
+
const run = runner || spawnSync;
|
|
50
|
+
const result = run('bash', ['-lc', cmd], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
timeout: 120000,
|
|
54
|
+
});
|
|
55
|
+
if (result.error) {
|
|
56
|
+
return { ok: false, passed: false, reason: 'verifier_spawn_failed', error: result.error.message, cmd };
|
|
57
|
+
}
|
|
58
|
+
const output = tailText(`${result.stdout || ''}${result.stderr || ''}`);
|
|
59
|
+
const passed = result.status === 0;
|
|
60
|
+
const dir = runsDir(root);
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
const now = new Date();
|
|
63
|
+
const date = now.toISOString().slice(0, 10);
|
|
64
|
+
const safeTime = now.toISOString().replace(/[:.]/g, '-');
|
|
65
|
+
const receiptFile = path.join(dir, `${date}-task-${slugifyTaskId(taskId)}-${safeTime}.json`);
|
|
66
|
+
const receiptPath = path.relative(root, receiptFile);
|
|
67
|
+
const receipt = {
|
|
68
|
+
schema: 'atris.task_receipt.v1',
|
|
69
|
+
task_id: String(taskId),
|
|
70
|
+
command: cmd,
|
|
71
|
+
at: now.toISOString(),
|
|
72
|
+
commit: gitCommitHash(root),
|
|
73
|
+
result: {
|
|
74
|
+
passed,
|
|
75
|
+
exit: result.status,
|
|
76
|
+
signal: result.signal || null,
|
|
77
|
+
output,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
fs.writeFileSync(receiptFile, JSON.stringify(receipt, null, 2) + '\n', 'utf8');
|
|
81
|
+
return {
|
|
82
|
+
ok: passed,
|
|
83
|
+
passed,
|
|
84
|
+
receiptPath,
|
|
85
|
+
receiptFile,
|
|
86
|
+
exit: result.status,
|
|
87
|
+
signal: result.signal || null,
|
|
88
|
+
output,
|
|
89
|
+
cmd,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { writeTaskReceipt };
|
package/package.json
CHANGED
package/utils/update-check.js
CHANGED
|
@@ -5,6 +5,7 @@ const os = require('os');
|
|
|
5
5
|
const { spawn, spawnSync } = require('child_process');
|
|
6
6
|
|
|
7
7
|
const PACKAGE_NAME = 'atris';
|
|
8
|
+
const NPM_SELF_UPDATE_COMMAND = `npm install -g ${PACKAGE_NAME}@latest`;
|
|
8
9
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
9
10
|
const ATRIS_DIR = path.join(os.homedir(), '.atris');
|
|
10
11
|
const CACHE_FILE = path.join(ATRIS_DIR, '.update-check');
|
|
@@ -217,13 +218,31 @@ async function checkForUpdates(force = false) {
|
|
|
217
218
|
return null;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
function
|
|
221
|
+
function isGitCheckout(packageRoot = path.join(__dirname, '..')) {
|
|
222
|
+
return fs.existsSync(path.join(path.resolve(packageRoot), '.git'));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getNpmSelfUpdateCommand() {
|
|
226
|
+
return NPM_SELF_UPDATE_COMMAND;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getNpmSelfUpdateSpawnArgs() {
|
|
230
|
+
return {
|
|
231
|
+
command: 'npm',
|
|
232
|
+
args: ['install', '-g', `${PACKAGE_NAME}@latest`],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function showUpdateNotification(updateInfo, options = {}) {
|
|
221
237
|
if (!updateInfo || !updateInfo.needsUpdate) return;
|
|
222
238
|
|
|
223
|
-
|
|
239
|
+
const packageRoot = options.packageRoot || path.join(__dirname, '..');
|
|
224
240
|
const yellow = '\x1b[33m';
|
|
225
241
|
const reset = '\x1b[0m';
|
|
226
|
-
|
|
242
|
+
const installHint = isGitCheckout(packageRoot)
|
|
243
|
+
? 'run: atris upgrade'
|
|
244
|
+
: `run: ${NPM_SELF_UPDATE_COMMAND}`;
|
|
245
|
+
console.log(`${yellow}update available: ${updateInfo.installed} -> ${updateInfo.latest}. ${installHint}${reset}`);
|
|
227
246
|
}
|
|
228
247
|
|
|
229
248
|
function inspectInstallGitState(packageRoot = path.join(__dirname, '..')) {
|
|
@@ -288,9 +307,8 @@ function shouldAutoUpdate(updateInfo, state, env = process.env) {
|
|
|
288
307
|
if (mode === 'off') return false;
|
|
289
308
|
if (mode === 'force') return true;
|
|
290
309
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return !(state && state.isGitRepo);
|
|
310
|
+
const packageRoot = state && state.root ? state.root : path.join(__dirname, '..');
|
|
311
|
+
return !isGitCheckout(packageRoot);
|
|
294
312
|
}
|
|
295
313
|
|
|
296
314
|
function autoUpdate(updateInfo, options = {}) {
|
|
@@ -331,4 +349,7 @@ module.exports = {
|
|
|
331
349
|
shouldAutoUpdate,
|
|
332
350
|
inspectInstallGitState,
|
|
333
351
|
formatInstallGitWarning,
|
|
352
|
+
isGitCheckout,
|
|
353
|
+
getNpmSelfUpdateCommand,
|
|
354
|
+
getNpmSelfUpdateSpawnArgs,
|
|
334
355
|
};
|
package/atris/learnings.jsonl
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"ts":"2026-05-08T02:32:38.257Z","type":"pitfall","key":"narrow-grep-hides-matches","insight":"When operator says X exists in our code and a single-pattern grep returns 0 hits, do NOT conclude it does not exist. Run wider grep -rn first (whole backend/, not one subdir) before doubting. Tonight 2026-05-08: grepped \"gpt-5\\.5|gpt5\\.5|gpt-5p5\" in clients/ only, missed 20+ matches in routers/atris2_router.py + tests/, told operator gpt-5.5 was not wired. Operator was right.","confidence":5,"source":"observed","files":[]}
|