atris 3.34.0 → 3.36.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 +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- 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/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- 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,
|
|
@@ -236,22 +240,43 @@ function newId() {
|
|
|
236
240
|
return head + tail;
|
|
237
241
|
}
|
|
238
242
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
+
// Resolve the canonical workspace root so `atris task` from any subdirectory
|
|
244
|
+
// keys the SAME store as the repo root. Precedence (nearest ancestor wins per
|
|
245
|
+
// pass):
|
|
246
|
+
// 1. A deliberate workspace spine — .atris/business.json (a bound customer
|
|
247
|
+
// sub-workspace) or atris/atris.md (the framework protocol file). These are
|
|
248
|
+
// intentional nested workspaces INSIDE a repo and must stay isolated.
|
|
249
|
+
// 2. The git toplevel (.git). One git repo == one workspace; this rescues a
|
|
250
|
+
// subdirectory whose only markers are a bare atris/ or .atris/ dir.
|
|
251
|
+
// 3. Legacy fallback for non-git trees: a bare atris/ or .atris/ dir.
|
|
252
|
+
//
|
|
253
|
+
// Before this, a bare atris/ or .atris/ dir short-circuited ahead of the git
|
|
254
|
+
// root, so a subdir like backend/ (which has an atris/ folder plus a stray
|
|
255
|
+
// .atris) hijacked itself as a second workspace root and split task/usage state
|
|
256
|
+
// into backend/.atris (proven footgun 2026-07-16). Checking .git only at the
|
|
257
|
+
// same level meant the walk returned before ever reaching the real repo root.
|
|
243
258
|
function findWorkspaceRoot(start) {
|
|
244
|
-
|
|
245
|
-
// Cap
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
const origin = path.resolve(start || process.cwd());
|
|
260
|
+
// Cap each walk at 32 levels to avoid pathological symlink loops.
|
|
261
|
+
const walkUp = (test) => {
|
|
262
|
+
let cur = origin;
|
|
263
|
+
for (let i = 0; i < 32; i++) {
|
|
264
|
+
if (test(cur)) return cur;
|
|
265
|
+
const parent = path.dirname(cur);
|
|
266
|
+
if (parent === cur) break;
|
|
267
|
+
cur = parent;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
};
|
|
271
|
+
const spine = walkUp((dir) => fs.existsSync(path.join(dir, '.atris', 'business.json'))
|
|
272
|
+
|| fs.existsSync(path.join(dir, 'atris', 'atris.md')));
|
|
273
|
+
if (spine) return spine;
|
|
274
|
+
const gitRoot = walkUp((dir) => fs.existsSync(path.join(dir, '.git')));
|
|
275
|
+
if (gitRoot) return gitRoot;
|
|
276
|
+
const legacy = walkUp((dir) => fs.existsSync(path.join(dir, 'atris'))
|
|
277
|
+
|| fs.existsSync(path.join(dir, '.atris')));
|
|
278
|
+
if (legacy) return legacy;
|
|
279
|
+
return origin;
|
|
255
280
|
}
|
|
256
281
|
|
|
257
282
|
function workspaceRoot(cwd) {
|
|
@@ -413,7 +438,7 @@ function listTasks(db, { workspaceRoot: ws, status, claimedBy, limit }) {
|
|
|
413
438
|
FROM tasks
|
|
414
439
|
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
415
440
|
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
|
|
441
|
+
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
442
|
CASE WHEN tag='endgame' THEN 0 ELSE 1 END,
|
|
418
443
|
created_at DESC
|
|
419
444
|
${limit ? 'LIMIT ' + Number(limit) : ''}
|
|
@@ -506,10 +531,13 @@ function releaseTask(db, { id, actor }) {
|
|
|
506
531
|
return { released: true, row: updated, event };
|
|
507
532
|
}
|
|
508
533
|
|
|
509
|
-
function doneTask(db, { id, status, actor, allowReview = false, action, proof } = {}) {
|
|
534
|
+
function doneTask(db, { id, status, actor, allowReview = false, action, proof, autoAccepted = false } = {}) {
|
|
510
535
|
if (!id) throw new Error('id required');
|
|
511
536
|
const final = status || 'done';
|
|
512
537
|
if (!['done', 'failed'].includes(final)) throw new Error('status must be done|failed');
|
|
538
|
+
// 'archived' is a distinct terminal status written by archiveTask(), never
|
|
539
|
+
// by this function — a bulk sweep of duplicate/off-roadmap tasks must not
|
|
540
|
+
// read as a real failure. See archiveTask() below.
|
|
513
541
|
const now = Date.now();
|
|
514
542
|
const allowedStatuses = allowReview ? "'open', 'claimed', 'review'" : "'open', 'claimed'";
|
|
515
543
|
const result = withBusyRetry(() => db.prepare(`
|
|
@@ -525,7 +553,11 @@ function doneTask(db, { id, status, actor, allowReview = false, action, proof }
|
|
|
525
553
|
workspaceRoot: row.workspace_root,
|
|
526
554
|
actor: actor || process.env.ATRIS_AGENT_ID || process.env.USER || null,
|
|
527
555
|
eventType: final === 'done' ? 'completed' : 'blocked',
|
|
528
|
-
payload: {
|
|
556
|
+
payload: {
|
|
557
|
+
status: final,
|
|
558
|
+
action: action || final,
|
|
559
|
+
...(autoAccepted ? { auto_accepted: true } : {}),
|
|
560
|
+
},
|
|
529
561
|
});
|
|
530
562
|
const logs = appendTaskCompletionLogs(db, row, {
|
|
531
563
|
status: final,
|
|
@@ -538,12 +570,147 @@ function doneTask(db, { id, status, actor, allowReview = false, action, proof }
|
|
|
538
570
|
return { updated: false };
|
|
539
571
|
}
|
|
540
572
|
|
|
573
|
+
// Distinct terminal status for housekeeping sweeps (duplicate loop-ticks,
|
|
574
|
+
// off-roadmap backlog resets, synthetic-test cleanup). Never conflate this
|
|
575
|
+
// with `failed` — 'failed' means the work itself did not succeed, 'archived'
|
|
576
|
+
// means the work was swept off the board for reasons unrelated to whether it
|
|
577
|
+
// succeeded. Reward/RSI signal readers must treat the two differently.
|
|
578
|
+
// `fromFailed: true` is an explicit opt-in for sanctioned historical cleanup
|
|
579
|
+
// (e.g. the 109 duplicate "Loop tick:" orphans fail-closed before 'archived'
|
|
580
|
+
// existed — cluster 1 of failed-tasks-analysis-2026-07-03). It permits the
|
|
581
|
+
// failed→archived transition and records the prior status in
|
|
582
|
+
// metadata.archived_from. Individual archive commands never archive 'done'
|
|
583
|
+
// rows. The clear-done sweep opts in through fromDone after selecting only the
|
|
584
|
+
// same 'done' rows counted by `atris status`.
|
|
585
|
+
function archiveTask(db, { id, actor, reason, fromFailed = false, fromDone = false } = {}) {
|
|
586
|
+
if (!id) throw new Error('id required');
|
|
587
|
+
const reasonText = String(reason || '').trim();
|
|
588
|
+
if (!reasonText) throw new Error('reason required');
|
|
589
|
+
const row = getTask(db, id);
|
|
590
|
+
if (!row) return { archived: false, reason: 'not_found' };
|
|
591
|
+
const allowedStatuses = ['open', 'claimed', 'review'];
|
|
592
|
+
if (fromFailed) allowedStatuses.push('failed');
|
|
593
|
+
if (fromDone) allowedStatuses.push('done');
|
|
594
|
+
if (!allowedStatuses.includes(row.status)) {
|
|
595
|
+
return { archived: false, reason: `already_${row.status}` };
|
|
596
|
+
}
|
|
597
|
+
const now = Date.now();
|
|
598
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
599
|
+
metadata.archived_reason = reasonText;
|
|
600
|
+
metadata.archived_at = new Date(now).toISOString();
|
|
601
|
+
metadata.archived_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
602
|
+
metadata.approval_status = 'archived';
|
|
603
|
+
if (row.status === 'failed' || row.status === 'done') metadata.archived_from = row.status;
|
|
604
|
+
const terminalAt = row.status === 'done' && row.done_at ? row.done_at : now;
|
|
605
|
+
const statusPlaceholders = allowedStatuses.map(() => '?').join(', ');
|
|
606
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
607
|
+
UPDATE tasks
|
|
608
|
+
SET status = 'archived',
|
|
609
|
+
done_at = ?,
|
|
610
|
+
updated_at = ?,
|
|
611
|
+
metadata = ?
|
|
612
|
+
WHERE id = ?
|
|
613
|
+
AND status IN (${statusPlaceholders})
|
|
614
|
+
`).run(terminalAt, now, JSON.stringify(metadata), id, ...allowedStatuses));
|
|
615
|
+
if (result.changes !== 1) return { archived: false, reason: 'stale_task_state' };
|
|
616
|
+
const updated = getTask(db, id);
|
|
617
|
+
const event = appendTaskEvent(db, {
|
|
618
|
+
taskId: id,
|
|
619
|
+
workspaceRoot: updated.workspace_root,
|
|
620
|
+
actor: metadata.archived_by,
|
|
621
|
+
eventType: 'archived',
|
|
622
|
+
payload: {
|
|
623
|
+
reason: reasonText,
|
|
624
|
+
...(['failed', 'done'].includes(row.status) ? { previous_status: row.status } : {}),
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
const logs = appendTaskCompletionLogs(db, updated, {
|
|
628
|
+
status: 'archived',
|
|
629
|
+
actor: metadata.archived_by,
|
|
630
|
+
action: 'archived',
|
|
631
|
+
proof: reasonText,
|
|
632
|
+
});
|
|
633
|
+
return { archived: true, event, row: updated, logs };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Marks metadata written by the 2026-06-10 "first-principles backlog reset"
|
|
637
|
+
// (see atris/logs/2026/2026-06-10.md) — a bulk pass that closed off-roadmap
|
|
638
|
+
// tasks, including certified/proof-backed done work, by writing status
|
|
639
|
+
// 'failed' because no distinct 'archived' status existed yet. This
|
|
640
|
+
// identifies exactly those rows so relabelArchivedTasks() can fix the label
|
|
641
|
+
// without touching any task that failed for a real reason.
|
|
642
|
+
function isJune10BacklogResetMarker(metadata) {
|
|
643
|
+
const m = metadata && typeof metadata === 'object' ? metadata : {};
|
|
644
|
+
if (!m.archived_at) return false;
|
|
645
|
+
return /first-principles backlog reset 2026-06-10/i.test(String(m.archive_reason || ''));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// One-time migration (OBL-1622): relabel the June-10 mislabeled rows from
|
|
649
|
+
// 'failed' to 'archived'. Dry-run only counts/samples; --apply performs the
|
|
650
|
+
// same status write path as archiveTask() (direct projection UPDATE +
|
|
651
|
+
// appendTaskEvent), never a raw projection-JSON edit.
|
|
652
|
+
function relabelArchivedTasks(db, { workspaceRoot: ws, apply = false, actor, limit = 5000 } = {}) {
|
|
653
|
+
const candidates = listTasks(db, { workspaceRoot: ws || null, status: 'failed', limit })
|
|
654
|
+
.filter(row => isJune10BacklogResetMarker(row.metadata));
|
|
655
|
+
const sample = candidates.slice(0, 10).map(row => ({ id: row.id, title: row.title }));
|
|
656
|
+
if (!apply) {
|
|
657
|
+
return { applied: false, count: candidates.length, sample };
|
|
658
|
+
}
|
|
659
|
+
const now = Date.now();
|
|
660
|
+
const actorText = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
661
|
+
const relabeledIds = [];
|
|
662
|
+
for (const row of candidates) {
|
|
663
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
664
|
+
metadata.relabeled_from_status = 'failed';
|
|
665
|
+
metadata.relabeled_at = new Date(now).toISOString();
|
|
666
|
+
metadata.relabeled_by = actorText;
|
|
667
|
+
metadata.relabel_reason = 'obl_1622_june_10_backlog_reset_relabel';
|
|
668
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
669
|
+
UPDATE tasks
|
|
670
|
+
SET status = 'archived',
|
|
671
|
+
updated_at = ?,
|
|
672
|
+
metadata = ?
|
|
673
|
+
WHERE id = ?
|
|
674
|
+
AND status = 'failed'
|
|
675
|
+
`).run(now, JSON.stringify(metadata), row.id));
|
|
676
|
+
if (result.changes === 1) {
|
|
677
|
+
appendTaskEvent(db, {
|
|
678
|
+
taskId: row.id,
|
|
679
|
+
workspaceRoot: row.workspace_root,
|
|
680
|
+
actor: actorText,
|
|
681
|
+
eventType: 'relabeled_archived',
|
|
682
|
+
payload: { previous_status: 'failed', reason: 'obl_1622_june_10_backlog_reset_relabel' },
|
|
683
|
+
});
|
|
684
|
+
relabeledIds.push(row.id);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return { applied: true, count: relabeledIds.length, ids: relabeledIds, sample };
|
|
688
|
+
}
|
|
689
|
+
|
|
541
690
|
function cleanLandingText(value) {
|
|
542
691
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
543
692
|
return text || null;
|
|
544
693
|
}
|
|
545
694
|
|
|
546
|
-
|
|
695
|
+
// Record who delivered this review pass and answer whether certification is
|
|
696
|
+
// allowed: at least one recorded pass actor must differ from the builder.
|
|
697
|
+
// The first pass actor stamps built_by; judge != worker from then on.
|
|
698
|
+
function recordReviewPassActor(metadata, claimedBy, rawActor) {
|
|
699
|
+
const actorNorm = reviewIntegrity.normalizeActor(
|
|
700
|
+
rawActor || process.env.ATRIS_AGENT_ID || process.env.USER || ''
|
|
701
|
+
);
|
|
702
|
+
if (!metadata.built_by && actorNorm) metadata.built_by = actorNorm;
|
|
703
|
+
const actors = Array.isArray(metadata.agent_review_actors) ? [...metadata.agent_review_actors] : [];
|
|
704
|
+
if (actorNorm && !actors.includes(actorNorm)) actors.push(actorNorm);
|
|
705
|
+
metadata.agent_review_actors = actors;
|
|
706
|
+
const builder = reviewIntegrity.normalizeActor(metadata.built_by || claimedBy || '');
|
|
707
|
+
const independent = builder
|
|
708
|
+
? (actors.find((a) => a && a !== builder) || null)
|
|
709
|
+
: (actors.length >= 2 ? actors[1] : null);
|
|
710
|
+
return { builder: builder || null, independent };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resultTrace, result: resultSentence, reason }) {
|
|
547
714
|
if (!id) throw new Error('id required');
|
|
548
715
|
const text = String(proof || '').trim();
|
|
549
716
|
if (!text) throw new Error('proof required');
|
|
@@ -562,16 +729,22 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
562
729
|
metadata.latest_agent_proof = text;
|
|
563
730
|
metadata.latest_agent_lesson = String(lesson || '').trim() || null;
|
|
564
731
|
metadata.latest_agent_next_task = String(nextTask || '').trim() || null;
|
|
732
|
+
const resultText = cleanLandingText(resultSentence);
|
|
733
|
+
if (resultText) metadata.result = resultText;
|
|
734
|
+
const reasonText = cleanLandingText(reason);
|
|
735
|
+
if (reasonText) metadata.result_reason = reasonText;
|
|
565
736
|
const landingInput = landing && typeof landing === 'object' ? landing : {};
|
|
566
737
|
for (const key of ['happened', 'checked', 'tested', 'decision']) {
|
|
567
738
|
const cleaned = cleanLandingText(landingInput[key]);
|
|
568
739
|
if (cleaned) metadata[`landing_${key}`] = cleaned;
|
|
569
740
|
}
|
|
570
|
-
|
|
741
|
+
const passRecord = recordReviewPassActor(metadata, row.claimed_by, actor);
|
|
742
|
+
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && passRecord.independent) {
|
|
571
743
|
metadata.agent_certified = true;
|
|
572
744
|
metadata.agent_certified_at = new Date(now).toISOString();
|
|
573
745
|
metadata.agent_certified_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
574
746
|
metadata.agent_certification_policy = `${AGENT_CERTIFICATION_REVIEW_PASSES}_agent_review_passes`;
|
|
747
|
+
metadata.independent_review_by = passRecord.independent;
|
|
575
748
|
}
|
|
576
749
|
const result = withBusyRetry(() => db.prepare(`
|
|
577
750
|
UPDATE tasks
|
|
@@ -592,6 +765,7 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
592
765
|
review_pass_count: reviewPassCount,
|
|
593
766
|
agent_certified: metadata.agent_certified === true,
|
|
594
767
|
agent_certification_policy: metadata.agent_certification_policy || null,
|
|
768
|
+
result: metadata.result || null,
|
|
595
769
|
};
|
|
596
770
|
if (resultTrace && typeof resultTrace === 'object') {
|
|
597
771
|
payload.result_trace = resultTrace;
|
|
@@ -613,13 +787,46 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
613
787
|
return { ready: true, event, row: updated };
|
|
614
788
|
}
|
|
615
789
|
|
|
616
|
-
function
|
|
790
|
+
function setTaskResult(db, { id, actor, result }) {
|
|
791
|
+
if (!id) throw new Error('id required');
|
|
792
|
+
const text = cleanLandingText(result);
|
|
793
|
+
if (!text) throw new Error('result required');
|
|
794
|
+
const row = getTask(db, id);
|
|
795
|
+
if (!row) return { saved: false, reason: 'not_found' };
|
|
796
|
+
const now = Date.now();
|
|
797
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
798
|
+
metadata.result = text;
|
|
799
|
+
metadata.result_at = new Date(now).toISOString();
|
|
800
|
+
metadata.result_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
801
|
+
const updated = withBusyRetry(() => db.prepare(`
|
|
802
|
+
UPDATE tasks
|
|
803
|
+
SET metadata = ?,
|
|
804
|
+
updated_at = ?
|
|
805
|
+
WHERE id = ?
|
|
806
|
+
`).run(JSON.stringify(metadata), now, id));
|
|
807
|
+
if (updated.changes !== 1) return { saved: false, reason: 'stale_task_state' };
|
|
808
|
+
const latest = getTask(db, id);
|
|
809
|
+
const event = appendTaskEvent(db, {
|
|
810
|
+
taskId: id,
|
|
811
|
+
workspaceRoot: latest.workspace_root,
|
|
812
|
+
actor: metadata.result_by,
|
|
813
|
+
eventType: 'result_set',
|
|
814
|
+
payload: { result: text },
|
|
815
|
+
});
|
|
816
|
+
return { saved: true, event, row: latest };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function reviseTask(db, { id, actor, note, allowDone = false }) {
|
|
617
820
|
if (!id) throw new Error('id required');
|
|
618
821
|
const text = String(note || '').trim();
|
|
619
822
|
if (!text) throw new Error('note required');
|
|
620
823
|
const row = getTask(db, id);
|
|
621
824
|
if (!row) return { revised: false, reason: 'not_found' };
|
|
622
|
-
|
|
825
|
+
const acceptedDone = allowDone
|
|
826
|
+
&& row.status === 'done'
|
|
827
|
+
&& row.metadata
|
|
828
|
+
&& row.metadata.approval_status === 'accepted';
|
|
829
|
+
if (row.status !== 'review' && !acceptedDone) {
|
|
623
830
|
return { revised: false, reason: `not_reviewable_${row.status}` };
|
|
624
831
|
}
|
|
625
832
|
const now = Date.now();
|
|
@@ -644,7 +851,19 @@ function reviseTask(db, { id, actor, note }) {
|
|
|
644
851
|
metadata.human_revision_at = new Date(now).toISOString();
|
|
645
852
|
metadata.human_revision_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
646
853
|
metadata.human_revision_note = text;
|
|
854
|
+
if (acceptedDone) {
|
|
855
|
+
for (const key of [
|
|
856
|
+
'accepted_at',
|
|
857
|
+
'accepted_by',
|
|
858
|
+
'auto_accepted_at',
|
|
859
|
+
'auto_accepted_by',
|
|
860
|
+
'auto_accept_policy',
|
|
861
|
+
]) {
|
|
862
|
+
delete metadata[key];
|
|
863
|
+
}
|
|
864
|
+
}
|
|
647
865
|
const revisedStatus = row.claimed_by ? 'claimed' : 'open';
|
|
866
|
+
const expectedStatus = acceptedDone ? 'done' : 'review';
|
|
648
867
|
const result = withBusyRetry(() => db.prepare(`
|
|
649
868
|
UPDATE tasks
|
|
650
869
|
SET status = ?,
|
|
@@ -652,8 +871,8 @@ function reviseTask(db, { id, actor, note }) {
|
|
|
652
871
|
updated_at = ?,
|
|
653
872
|
metadata = ?
|
|
654
873
|
WHERE id = ?
|
|
655
|
-
AND status =
|
|
656
|
-
`).run(revisedStatus, now, JSON.stringify(metadata), id));
|
|
874
|
+
AND status = ?
|
|
875
|
+
`).run(revisedStatus, now, JSON.stringify(metadata), id, expectedStatus));
|
|
657
876
|
if (result.changes !== 1) return { revised: false, reason: 'not_updated' };
|
|
658
877
|
const updated = getTask(db, id);
|
|
659
878
|
const event = appendTaskEvent(db, {
|
|
@@ -665,6 +884,7 @@ function reviseTask(db, { id, actor, note }) {
|
|
|
665
884
|
note: text,
|
|
666
885
|
approval_status: 'revise',
|
|
667
886
|
revision_count: revisionCount,
|
|
887
|
+
...(acceptedDone ? { previous_status: row.status } : {}),
|
|
668
888
|
},
|
|
669
889
|
});
|
|
670
890
|
const episode = taskEpisodeFromRevision(updated, event, event.payload);
|
|
@@ -1170,6 +1390,61 @@ function noteTask(db, { id, actor, content }) {
|
|
|
1170
1390
|
return { noted: true, event };
|
|
1171
1391
|
}
|
|
1172
1392
|
|
|
1393
|
+
function normalizeTagToken(value) {
|
|
1394
|
+
return String(value == null ? '' : value).trim().toLowerCase();
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Update a task's flag list (`metadata.tags`) on an already-created task.
|
|
1398
|
+
// Tags are only settable at creation elsewhere; this is the after-the-fact
|
|
1399
|
+
// escape hatch so an open task that becomes an owner decision can be marked
|
|
1400
|
+
// `needs-human` — which sweep and fleet staffing both honor — instead of
|
|
1401
|
+
// being restaffed forever (CLI-879). Every change appends a
|
|
1402
|
+
// `task_tags_updated` event so the trail shows who flagged it and when.
|
|
1403
|
+
function tagTask(db, { id, actor, add = [], remove = [] } = {}) {
|
|
1404
|
+
if (!id) throw new Error('id required');
|
|
1405
|
+
const addTags = [...new Set((Array.isArray(add) ? add : [add]).map(normalizeTagToken).filter(Boolean))];
|
|
1406
|
+
const removeSet = new Set((Array.isArray(remove) ? remove : [remove]).map(normalizeTagToken).filter(Boolean));
|
|
1407
|
+
if (!addTags.length && !removeSet.size) return { tagged: false, reason: 'no_tags' };
|
|
1408
|
+
const row = getTask(db, id);
|
|
1409
|
+
if (!row) return { tagged: false, reason: 'not_found' };
|
|
1410
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
1411
|
+
const before = [...new Set((Array.isArray(metadata.tags) ? metadata.tags : []).map(normalizeTagToken).filter(Boolean))];
|
|
1412
|
+
const removed = [];
|
|
1413
|
+
const next = before.filter((tag) => {
|
|
1414
|
+
if (removeSet.has(tag)) { removed.push(tag); return false; }
|
|
1415
|
+
return true;
|
|
1416
|
+
});
|
|
1417
|
+
const added = [];
|
|
1418
|
+
for (const tag of addTags) {
|
|
1419
|
+
// An explicit --remove of the same token wins over --add.
|
|
1420
|
+
if (removeSet.has(tag)) continue;
|
|
1421
|
+
if (!next.includes(tag)) { next.push(tag); added.push(tag); }
|
|
1422
|
+
}
|
|
1423
|
+
if (!added.length && !removed.length) {
|
|
1424
|
+
return { tagged: false, reason: 'no_changes', tags: before };
|
|
1425
|
+
}
|
|
1426
|
+
metadata.tags = next;
|
|
1427
|
+
const actorText = cleanStageText(actor) || null;
|
|
1428
|
+
const now = Math.max(Date.now(), Number(row.updated_at || 0) + 1);
|
|
1429
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
1430
|
+
UPDATE tasks
|
|
1431
|
+
SET metadata = ?,
|
|
1432
|
+
updated_at = ?
|
|
1433
|
+
WHERE id = ?
|
|
1434
|
+
AND updated_at = ?
|
|
1435
|
+
`).run(JSON.stringify(metadata), now, id, row.updated_at));
|
|
1436
|
+
if (result.changes !== 1) return { tagged: false, reason: 'stale_task_state' };
|
|
1437
|
+
const updated = getTask(db, id);
|
|
1438
|
+
const event = appendTaskEvent(db, {
|
|
1439
|
+
taskId: id,
|
|
1440
|
+
workspaceRoot: updated.workspace_root,
|
|
1441
|
+
actor: actorText,
|
|
1442
|
+
eventType: 'task_tags_updated',
|
|
1443
|
+
payload: { added, removed, tags: next, previous_tags: before },
|
|
1444
|
+
});
|
|
1445
|
+
return { tagged: true, event, row: updated, tags: next, added, removed };
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1173
1448
|
function taskChatPacketFromPayload(payload) {
|
|
1174
1449
|
const data = payload && typeof payload === 'object' ? payload : {};
|
|
1175
1450
|
const lines = ['TASK_CHAT_UPDATE'];
|
|
@@ -1235,7 +1510,7 @@ function chatTask(db, { id, actor, content, goal, summary }) {
|
|
|
1235
1510
|
};
|
|
1236
1511
|
}
|
|
1237
1512
|
|
|
1238
|
-
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, careerXpEligible = false, clearedFields = [], landing = {} }) {
|
|
1513
|
+
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, careerXpEligible = false, clearedFields = [], landing = {}, autoAccepted = false }) {
|
|
1239
1514
|
if (!id) throw new Error('id required');
|
|
1240
1515
|
const row = getTask(db, id);
|
|
1241
1516
|
if (!row) return { reviewed: false, reason: 'not_found' };
|
|
@@ -1243,6 +1518,14 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1243
1518
|
const now = Date.now();
|
|
1244
1519
|
const reviewer = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
1245
1520
|
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
1521
|
+
const reviewerNorm = reviewIntegrity.normalizeActor(reviewer);
|
|
1522
|
+
const builderNorm = reviewIntegrity.taskBuilder(row);
|
|
1523
|
+
// judge != worker applies pre-acceptance only: a builder cannot positive-
|
|
1524
|
+
// reward their own row while it still sits in review. Done rows are past
|
|
1525
|
+
// the human gate; finish and post-acceptance XP bookkeeping stay open.
|
|
1526
|
+
if (numericReward > 0 && row.status === 'review' && builderNorm && reviewerNorm && reviewerNorm === builderNorm) {
|
|
1527
|
+
return { reviewed: false, reason: 'judge_equals_worker', builder: builderNorm };
|
|
1528
|
+
}
|
|
1246
1529
|
const proofText = String(proof || '').trim();
|
|
1247
1530
|
const verifyText = cleanStageText(verify);
|
|
1248
1531
|
const lessonText = String(lesson || '').trim();
|
|
@@ -1284,11 +1567,13 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1284
1567
|
if (reviewingPendingProof) {
|
|
1285
1568
|
reviewPassCount += 1;
|
|
1286
1569
|
metadata.agent_review_pass_count = reviewPassCount;
|
|
1287
|
-
|
|
1570
|
+
const passRecord = recordReviewPassActor(metadata, row.claimed_by, reviewer);
|
|
1571
|
+
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && passRecord.independent) {
|
|
1288
1572
|
metadata.agent_certified = true;
|
|
1289
1573
|
metadata.agent_certified_at = new Date(now).toISOString();
|
|
1290
1574
|
metadata.agent_certified_by = reviewer;
|
|
1291
1575
|
metadata.agent_certification_policy = `${AGENT_CERTIFICATION_REVIEW_PASSES}_agent_review_passes`;
|
|
1576
|
+
metadata.independent_review_by = passRecord.independent;
|
|
1292
1577
|
}
|
|
1293
1578
|
}
|
|
1294
1579
|
if (numericReward > 0 && row.status === 'done') {
|
|
@@ -1312,6 +1597,7 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1312
1597
|
verify: verifyText || null,
|
|
1313
1598
|
career_xp_eligible: Boolean(careerXpEligible),
|
|
1314
1599
|
};
|
|
1600
|
+
if (autoAccepted) payload.auto_accepted = true;
|
|
1315
1601
|
if (reviewingPendingProof) {
|
|
1316
1602
|
payload.review_pass_count = reviewPassCount;
|
|
1317
1603
|
payload.agent_certified = metadata.agent_certified === true;
|
|
@@ -1637,6 +1923,7 @@ function taskProjection(db, {
|
|
|
1637
1923
|
id: row.id,
|
|
1638
1924
|
...(refById.get(row.id) || {}),
|
|
1639
1925
|
title: row.title,
|
|
1926
|
+
result: row.metadata && row.metadata.result || null,
|
|
1640
1927
|
status: row.status,
|
|
1641
1928
|
tag: row.tag,
|
|
1642
1929
|
workspace_root: row.workspace_root,
|
|
@@ -1762,11 +2049,16 @@ module.exports = {
|
|
|
1762
2049
|
backlogTask,
|
|
1763
2050
|
clearPlanTasks,
|
|
1764
2051
|
doneTask,
|
|
2052
|
+
archiveTask,
|
|
2053
|
+
relabelArchivedTasks,
|
|
2054
|
+
isJune10BacklogResetMarker,
|
|
1765
2055
|
readyTask,
|
|
2056
|
+
setTaskResult,
|
|
1766
2057
|
reviseTask,
|
|
1767
2058
|
stageTask,
|
|
1768
2059
|
noteTask,
|
|
1769
2060
|
chatTask,
|
|
2061
|
+
tagTask,
|
|
1770
2062
|
reviewTask,
|
|
1771
2063
|
appendTaskEvent,
|
|
1772
2064
|
listTaskEvents,
|
package/lib/task-proof.js
CHANGED
|
@@ -7,19 +7,43 @@ const FILE_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|src\/|scripts\/|a
|
|
|
7
7
|
const PATH_ONLY_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|\/private\/|\/var\/|atris\/runs\/|\.atris\/state\/)[^\s'"`,;)]+(?:[.](?:json|jsonl|md|log|txt|png|jpg|jpeg|pdf))?/i;
|
|
8
8
|
const RECEIPT_OR_ARTIFACT_RE = /\b(?:receipt|artifact|screenshot|log|trace|path=|file=|bytes=|model=|opened=|https?:\/\/)\b/i;
|
|
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
|
+
const SUITE_GREEN_CLAIM_RE = /\b(?:tests|test\s+suite|suite)\s+(?:is|are|was|were)?\s*(?:all\s+)?(?:green|pass(?:es|ed)?|ok)\b|\b(?:all|full|whole|entire)\s+(?:tests?|test\s+suite|suite)\s+(?:is|are|was|were)?\s*(?:green|pass(?:es|ed)?|ok)\b|\ball\s+green\b|\b(?:npm|pnpm|yarn)\s+(?:run\s+)?test\b\x60?(?:\s+(?:\d+\s*\/\s*\d+|all|suite))?\s+(?:is\s+)?(?:green|pass(?:es|ed)?|ok)\b/i;
|
|
11
|
+
const CI_RUN_CITATION_RE = /https?:\/\/github[.]com\/[^/\s]+\/[^/\s]+\/actions\/runs\/\d+\b|\brun_id\s*=\s*\d+\b|\brun(?:\s+id)?\s*(?:=|:)?\s*\d+\b/i;
|
|
12
|
+
const COMMIT_PINNED_LOCAL_VERIFY_RE = /\bcommit\s+[0-9a-f]{7,40}\b/i;
|
|
13
|
+
const EXIT_ZERO_RE = /\bexit(?:ed)?(?:\s+code)?\s*0\b/i;
|
|
14
|
+
const SUITE_GREEN_CITATION_REASON = 'cite the CI run URL, run id, or commit-pinned verify command with exit 0 before claiming the suite is green';
|
|
10
15
|
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
16
|
const FILE_ACTION_RE = /\b(?:changed|updated|edited|created|deleted|saved|wrote|patched|reviewed|verified|validated|opened|read)\b/i;
|
|
17
|
+
const VERIFIED_PROOF_RE = /^\[verified\]\s+`[^`]+`\s+passed\s+\(exit 0\)(?:\s|$)/i;
|
|
18
|
+
const SECOND_ACTOR_EXECUTED_PROOF_RE = /^Second-actor check:\s+`[^`]+`\s+re-run by [^,]+,\s+exited 0(?:[.\s]|$)/i;
|
|
12
19
|
|
|
13
20
|
function compactWhitespace(text) {
|
|
14
21
|
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
function hasSuiteGreenCitation(text) {
|
|
25
|
+
return CI_RUN_CITATION_RE.test(text)
|
|
26
|
+
|| (COMMIT_PINNED_LOCAL_VERIFY_RE.test(text)
|
|
27
|
+
&& COMMAND_PROOF_RE.test(text)
|
|
28
|
+
&& EXIT_ZERO_RE.test(text));
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
function taskProofState(proof) {
|
|
18
32
|
const text = compactWhitespace(proof);
|
|
19
33
|
if (!text) return { ok: false, reason: 'proof required' };
|
|
20
34
|
if (GENERIC_COMPLETION_PROOF_RE.test(text)) {
|
|
21
35
|
return { ok: false, reason: 'proof must name what was verified, changed, approved, or produced' };
|
|
22
36
|
}
|
|
37
|
+
if (SUITE_GREEN_CLAIM_RE.test(text)) {
|
|
38
|
+
if (!hasSuiteGreenCitation(text)) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
code: 'suite_green_citation_required',
|
|
42
|
+
reason: SUITE_GREEN_CITATION_REASON,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, reason: 'proof cites a CI run or commit-pinned local verify' };
|
|
46
|
+
}
|
|
23
47
|
if (COMMAND_PROOF_RE.test(text)) return { ok: true, reason: 'proof names a command' };
|
|
24
48
|
if (HUMAN_PROOF_RE.test(text)) return { ok: true, reason: 'proof names human/customer approval' };
|
|
25
49
|
if (RESULT_PAIR_RE.test(text)) return { ok: true, reason: 'proof names a verifier result' };
|
|
@@ -37,6 +61,22 @@ function taskProofLooksMeaningful(proof) {
|
|
|
37
61
|
return taskProofState(proof).ok;
|
|
38
62
|
}
|
|
39
63
|
|
|
64
|
+
function taskProofExecutionState(proof) {
|
|
65
|
+
const text = compactWhitespace(proof);
|
|
66
|
+
if (!text) return { ok: false, reason: 'proof_not_executed', detail: 'executed proof required' };
|
|
67
|
+
if (VERIFIED_PROOF_RE.test(text)) return { ok: true, reason: 'verified_proof' };
|
|
68
|
+
if (SECOND_ACTOR_EXECUTED_PROOF_RE.test(text)) return { ok: true, reason: 'second_actor_executed_proof' };
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
reason: 'proof_not_executed',
|
|
72
|
+
detail: 'proof must come from an executed verifier, not a free-text claim that tests passed',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function taskProofLooksExecuted(proof) {
|
|
77
|
+
return taskProofExecutionState(proof).ok;
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
function tailText(text, max = 400) {
|
|
41
81
|
const trimmed = String(text || '').trim();
|
|
42
82
|
if (trimmed.length <= max) return trimmed;
|
|
@@ -48,7 +88,7 @@ function tailText(text, max = 400) {
|
|
|
48
88
|
// happily pattern-matches even if nothing ran) and a VERIFIED proof: the command
|
|
49
89
|
// actually executed and exited 0. On failure it returns ok:false so the caller
|
|
50
90
|
// can refuse to mark the task ready. The returned proof string is prefixed
|
|
51
|
-
//
|
|
91
|
+
// It names the command and exit; suite-green claims still need a citation.
|
|
52
92
|
function buildVerifiedProof(verifyCmd, baseProof = '', runner, options = {}) {
|
|
53
93
|
const cmd = String(verifyCmd || '').trim();
|
|
54
94
|
if (!cmd) return { ok: false, reason: 'verify_command_required' };
|
|
@@ -73,6 +113,8 @@ function buildVerifiedProof(verifyCmd, baseProof = '', runner, options = {}) {
|
|
|
73
113
|
|
|
74
114
|
module.exports = {
|
|
75
115
|
taskProofLooksMeaningful,
|
|
116
|
+
taskProofLooksExecuted,
|
|
76
117
|
taskProofState,
|
|
118
|
+
taskProofExecutionState,
|
|
77
119
|
buildVerifiedProof,
|
|
78
120
|
};
|