atris 3.15.57 → 3.16.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.
Files changed (43) hide show
  1. package/AGENTS.md +2 -2
  2. package/GETTING_STARTED.md +1 -1
  3. package/PERSONA.md +4 -4
  4. package/README.md +11 -11
  5. package/atris/skills/copy-editor/SKILL.md +30 -4
  6. package/atris/skills/improve/SKILL.md +18 -20
  7. package/atris/wiki/concepts/agent-activation-contract.md +5 -3
  8. package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
  9. package/atris/wiki/index.md +1 -0
  10. package/ax +522 -73
  11. package/bin/atris.js +31 -30
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +197 -22
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/computer.js +0 -60
  18. package/commands/improve.js +501 -0
  19. package/commands/integrations.js +233 -71
  20. package/commands/lesson.js +44 -0
  21. package/commands/member.js +4498 -226
  22. package/commands/mission.js +302 -27
  23. package/commands/now.js +89 -1
  24. package/commands/radar.js +181 -56
  25. package/commands/soul.js +0 -4
  26. package/commands/task.js +5582 -517
  27. package/commands/terminal.js +14 -10
  28. package/commands/wiki.js +87 -1
  29. package/commands/workflow.js +288 -73
  30. package/commands/worktree.js +52 -15
  31. package/commands/xp.js +6 -65
  32. package/lib/auto-accept-certified.js +294 -0
  33. package/lib/file-ops.js +0 -184
  34. package/lib/member-alive.js +232 -0
  35. package/lib/policy-lessons.js +280 -0
  36. package/lib/receipt-evidence.js +64 -0
  37. package/lib/state-detection.js +34 -0
  38. package/lib/task-db.js +568 -16
  39. package/lib/task-proof.js +43 -0
  40. package/package.json +1 -1
  41. package/utils/auth.js +13 -4
  42. package/commands/research.js +0 -52
  43. package/lib/section-merge.js +0 -196
package/lib/task-db.js CHANGED
@@ -31,12 +31,33 @@ const { DatabaseSync } = require('node:sqlite');
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
33
  const TODO_RENDER_DONE_LIMIT = 8;
34
+ const TODO_RENDER_FAILED_LIMIT = 12;
34
35
  const PROJECTION_DONE_LIMIT = 8;
35
36
  const PROJECTION_EVENT_LIMIT = 8;
36
37
  const PROJECTION_MESSAGE_LIMIT = 6;
37
38
  const PROJECTION_PAYLOAD_TEXT_LIMIT = 1000;
38
39
  const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
39
40
  const TASK_REF_GENERIC_TOKENS = new Set(['app', 'atris', 'atrisos', 'project', 'repo', 'workspace']);
41
+ const TASK_PLAN_TAGS = new Set([
42
+ 'agent',
43
+ 'autopilot',
44
+ 'cron',
45
+ 'endgame',
46
+ 'execute',
47
+ 'explore',
48
+ 'feature',
49
+ 'goal',
50
+ 'goal-step',
51
+ 'loop',
52
+ 'plan',
53
+ 'planned',
54
+ 'schedule',
55
+ 'scheduled',
56
+ 'shape',
57
+ 'shaping',
58
+ 'ui',
59
+ 'ux',
60
+ ]);
40
61
 
41
62
  const SCHEMA = `
42
63
  CREATE TABLE IF NOT EXISTS tasks (
@@ -494,6 +515,431 @@ function reviseTask(db, { id, actor, note }) {
494
515
  return { revised: true, event, row: updated };
495
516
  }
496
517
 
518
+ function cleanStageText(value) {
519
+ return String(value || '').replace(/\s+/g, ' ').trim();
520
+ }
521
+
522
+ function normalizedTaskPart(value) {
523
+ return cleanStageText(value).toLowerCase().replace(/\s+/g, '-');
524
+ }
525
+
526
+ function backlogTag(value) {
527
+ const requested = cleanStageText(value) || 'capture';
528
+ return TASK_PLAN_TAGS.has(normalizedTaskPart(requested)) ? 'capture' : requested;
529
+ }
530
+
531
+ function taskHasPlanSignal(row) {
532
+ const metadata = row && row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
533
+ const tag = normalizedTaskPart(row && row.tag);
534
+ const stage = normalizedTaskPart(metadata.stage);
535
+ return TASK_PLAN_TAGS.has(tag)
536
+ || TASK_PLAN_TAGS.has(stage)
537
+ || Boolean(
538
+ metadata.planned_at
539
+ || metadata.stage_plan_recorded_at
540
+ || metadata.verify
541
+ || metadata.proof_needed
542
+ || metadata.exit_condition
543
+ || metadata.stage_goal
544
+ || metadata.stage_owner
545
+ || metadata.goal
546
+ || metadata.loop
547
+ || metadata.cron
548
+ || metadata.next_run_at
549
+ );
550
+ }
551
+
552
+ function clearPlanMetadata(metadata, { actor, reason, previousTag } = {}) {
553
+ const next = metadata && typeof metadata === 'object' ? { ...metadata } : {};
554
+ const clearedKeys = [];
555
+ const taskGoal = cleanStageText(next.task_goal || next.goal_objective || next.objective || next.stage_goal);
556
+ if (taskGoal && !next.task_goal) next.task_goal = taskGoal;
557
+ for (const key of [
558
+ 'stage',
559
+ 'stage_goal',
560
+ 'stage_summary',
561
+ 'exit_condition',
562
+ 'verify',
563
+ 'proof_needed',
564
+ 'first_move',
565
+ 'next_button',
566
+ 'stage_updated_at',
567
+ 'stage_updated_by',
568
+ 'stage_owner',
569
+ 'stage_confidence',
570
+ 'goal_objective',
571
+ 'objective',
572
+ 'goal',
573
+ 'loop',
574
+ 'cron',
575
+ 'next_run_at',
576
+ 'planned_at',
577
+ 'planned_by',
578
+ 'stage_plan_recorded_at',
579
+ ]) {
580
+ if (next[key] !== undefined) {
581
+ delete next[key];
582
+ clearedKeys.push(key);
583
+ }
584
+ }
585
+ const delegatedAssignment = Boolean(metadata.delegate_via || metadata.swarlo_channel || metadata.created_for_day);
586
+ if (
587
+ next.assigned_to
588
+ && !delegatedAssignment
589
+ && (next.assigned_to === metadata.stage_owner || next.assigned_to === metadata.planned_by)
590
+ ) {
591
+ delete next.assigned_to;
592
+ clearedKeys.push('assigned_to');
593
+ }
594
+ next.backlogged_at = new Date().toISOString();
595
+ next.backlogged_by = cleanStageText(actor) || null;
596
+ next.backlog_reason = cleanStageText(reason) || 'clear_plan';
597
+ if (previousTag) next.backlog_previous_tag = previousTag;
598
+ return { metadata: next, clearedKeys };
599
+ }
600
+
601
+ function backlogTask(db, { id, actor, reason, tag = 'capture' }) {
602
+ if (!id) throw new Error('id required');
603
+ const row = getTask(db, id);
604
+ if (!row) return { backlogged: false, reason: 'not_found' };
605
+ if (row.status !== 'open') {
606
+ return { backlogged: false, reason: `already_${row.status}`, claimed_by: row.claimed_by || null };
607
+ }
608
+ if (!taskHasPlanSignal(row)) return { backlogged: false, reason: 'not_planned' };
609
+
610
+ const actorText = cleanStageText(actor) || null;
611
+ const previousTag = row.tag || null;
612
+ const nextTag = backlogTag(tag);
613
+ const { metadata, clearedKeys } = clearPlanMetadata(row.metadata || {}, {
614
+ actor: actorText,
615
+ reason,
616
+ previousTag: previousTag && normalizedTaskPart(previousTag) !== normalizedTaskPart(nextTag) ? previousTag : null,
617
+ });
618
+ const now = Math.max(Date.now(), Number(row.updated_at || 0) + 1);
619
+ const result = withBusyRetry(() => db.prepare(`
620
+ UPDATE tasks
621
+ SET tag = ?,
622
+ metadata = ?,
623
+ updated_at = ?
624
+ WHERE id = ?
625
+ AND status = 'open'
626
+ AND updated_at = ?
627
+ `).run(nextTag, JSON.stringify(metadata), now, id, row.updated_at));
628
+ if (result.changes !== 1) return { backlogged: false, reason: 'stale_task_state' };
629
+ const updated = getTask(db, id);
630
+ const event = appendTaskEvent(db, {
631
+ taskId: id,
632
+ workspaceRoot: updated.workspace_root,
633
+ actor: actorText,
634
+ eventType: 'task_backlogged',
635
+ payload: {
636
+ reason: cleanStageText(reason) || 'clear_plan',
637
+ previous_tag: previousTag,
638
+ tag: nextTag,
639
+ cleared_keys: clearedKeys,
640
+ },
641
+ });
642
+ return { backlogged: true, event, row: updated, cleared_keys: clearedKeys };
643
+ }
644
+
645
+ function clearPlanTasks(db, { workspaceRoot: ws, actor, reason, tag = 'capture', limit } = {}) {
646
+ const rows = listTasks(db, { workspaceRoot: ws || null, status: 'open', limit }).filter(taskHasPlanSignal);
647
+ const cleared = [];
648
+ const skipped = [];
649
+ for (const row of rows) {
650
+ const result = backlogTask(db, {
651
+ id: row.id,
652
+ actor,
653
+ reason: reason || 'clear_plan_bulk',
654
+ tag,
655
+ });
656
+ if (result.backlogged) cleared.push(result.row);
657
+ else skipped.push({ id: row.id, reason: result.reason });
658
+ }
659
+ return { cleared, skipped };
660
+ }
661
+
662
+ function stagePacketFromPayload(payload) {
663
+ const data = payload && typeof payload === 'object' ? payload : {};
664
+ const lines = [
665
+ 'TASK_STAGE_UPDATE',
666
+ `stage: ${cleanStageText(data.stage)}`,
667
+ ];
668
+ if (data.confidence !== undefined && data.confidence !== null) lines.push(`confidence: ${data.confidence}`);
669
+ if (data.summary) lines.push(`summary: ${data.summary}`);
670
+ if (data.owner) lines.push(`owner: ${data.owner}`);
671
+ if (data.goal) lines.push(`goal: ${data.goal}`);
672
+ if (data.exit) lines.push(`exit: ${data.exit}`);
673
+ if (data.proof_needed) lines.push(`proof_needed: ${data.proof_needed}`);
674
+ if (data.first_move) lines.push(`first_move: ${data.first_move}`);
675
+ if (data.next_button) lines.push(`next_button: ${data.next_button}`);
676
+ return lines.filter(Boolean).join('\n');
677
+ }
678
+
679
+ function backlogPacketFromPayload(payload) {
680
+ const data = payload && typeof payload === 'object' ? payload : {};
681
+ const lines = [
682
+ 'TASK_BACKLOG_UPDATE',
683
+ `reason: ${cleanStageText(data.reason || 'clear_plan')}`,
684
+ ];
685
+ if (data.previous_tag) lines.push(`previous_tag: ${cleanStageText(data.previous_tag)}`);
686
+ if (data.tag) lines.push(`tag: ${cleanStageText(data.tag)}`);
687
+ if (Array.isArray(data.cleared_keys) && data.cleared_keys.length) lines.push(`cleared_keys: ${data.cleared_keys.join(', ')}`);
688
+ return lines.filter(Boolean).join('\n');
689
+ }
690
+
691
+ function stageTask(db, {
692
+ id,
693
+ actor,
694
+ stage,
695
+ goal,
696
+ summary,
697
+ owner,
698
+ exit,
699
+ proofNeeded,
700
+ firstMove,
701
+ nextButton,
702
+ confidence,
703
+ }) {
704
+ if (!id) throw new Error('id required');
705
+ const targetStage = cleanStageText(stage).toLowerCase();
706
+ if (!['plan', 'do'].includes(targetStage)) throw new Error('stage must be plan|do');
707
+ const row = getTask(db, id);
708
+ if (!row) return { staged: false, reason: 'not_found' };
709
+ if (['done', 'failed'].includes(row.status)) return { staged: false, reason: `already_${row.status}` };
710
+ if (row.status === 'review') return { staged: false, reason: 'not_reviewable_use_revise' };
711
+
712
+ const requestedOwner = cleanStageText(owner);
713
+ const requestedGoal = cleanStageText(goal);
714
+ const requestedProof = cleanStageText(proofNeeded);
715
+ const requestedExit = cleanStageText(exit);
716
+ const actorText = cleanStageText(actor);
717
+ let metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
718
+ let stageOwner = requestedOwner || actorText || cleanStageText(row.claimed_by) || null;
719
+ let goalText = requestedGoal || cleanStageText(metadata.task_goal || metadata.goal_objective || metadata.objective || metadata.stage_goal);
720
+ let proofText = requestedProof || cleanStageText(metadata.verify || metadata.proof_needed);
721
+ let exitText = requestedExit || cleanStageText(metadata.exit_condition);
722
+
723
+ function recordedDoPlan(currentRow, { claimedOwnerWins = false } = {}) {
724
+ const currentMetadata = currentRow && currentRow.metadata && typeof currentRow.metadata === 'object'
725
+ ? { ...currentRow.metadata }
726
+ : {};
727
+ const claimedOwner = cleanStageText(currentRow && currentRow.claimed_by);
728
+ const planOwner = cleanStageText(currentMetadata.stage_owner || currentMetadata.assigned_to);
729
+ const recordedOwner = claimedOwnerWins && claimedOwner ? claimedOwner : (claimedOwner || planOwner);
730
+ const caller = actorText || requestedOwner || null;
731
+ const recordedGoal = cleanStageText(currentMetadata.goal_objective || currentMetadata.objective || currentMetadata.stage_goal || currentMetadata.task_goal);
732
+ const recordedProof = cleanStageText(currentMetadata.verify || currentMetadata.proof_needed);
733
+ const recordedExit = cleanStageText(currentMetadata.exit_condition);
734
+ const planRecorded = cleanStageText(currentMetadata.planned_at)
735
+ || cleanStageText(currentMetadata.stage) === 'plan'
736
+ || cleanStageText(currentMetadata.stage_plan_recorded_at);
737
+ if (!planRecorded) {
738
+ return { ok: false, reason: requestedGoal || requestedProof || requestedExit ? 'plan_required' : 'goal_required' };
739
+ }
740
+ if (!recordedGoal) return { ok: false, reason: 'goal_required' };
741
+ if (!recordedExit) return { ok: false, reason: 'exit_required' };
742
+ if (!recordedProof) return { ok: false, reason: 'proof_needed_required' };
743
+ if (requestedGoal && requestedGoal !== recordedGoal) return { ok: false, reason: 'plan_goal_mismatch' };
744
+ if (requestedProof && requestedProof !== recordedProof) return { ok: false, reason: 'plan_proof_mismatch' };
745
+ if (requestedExit && requestedExit !== recordedExit) return { ok: false, reason: 'plan_exit_mismatch' };
746
+ if (claimedOwner && planOwner && claimedOwner !== planOwner && !claimedOwnerWins) {
747
+ return { ok: false, reason: 'claimed_by_other', claimed_by: planOwner };
748
+ }
749
+ if (recordedOwner && caller && recordedOwner !== caller) {
750
+ return { ok: false, reason: 'claimed_by_other', claimed_by: recordedOwner };
751
+ }
752
+ return {
753
+ ok: true,
754
+ metadata: currentMetadata,
755
+ stageOwner: recordedOwner || requestedOwner || actorText || null,
756
+ goalText: recordedGoal,
757
+ proofText: recordedProof,
758
+ exitText: recordedExit,
759
+ };
760
+ }
761
+
762
+ if (targetStage === 'plan') {
763
+ if (!goalText) return { staged: false, reason: 'goal_required' };
764
+ if (!exitText || !proofText) {
765
+ return { staged: false, reason: !exitText ? 'exit_required' : 'proof_needed_required' };
766
+ }
767
+ const claimedBy = cleanStageText(row.claimed_by);
768
+ if (row.status === 'claimed' && claimedBy) {
769
+ if ((actorText && actorText !== claimedBy) || (requestedOwner && requestedOwner !== claimedBy)) {
770
+ return { staged: false, reason: 'claimed_by_other', claimed_by: claimedBy };
771
+ }
772
+ stageOwner = claimedBy;
773
+ }
774
+ } else {
775
+ const plan = recordedDoPlan(row, { claimedOwnerWins: row.status === 'claimed' });
776
+ if (!plan.ok) return { staged: false, reason: plan.reason, claimed_by: plan.claimed_by || null };
777
+ metadata = plan.metadata;
778
+ stageOwner = plan.stageOwner;
779
+ goalText = plan.goalText;
780
+ proofText = plan.proofText;
781
+ exitText = plan.exitText;
782
+ }
783
+
784
+ const hasConfidence = confidence !== undefined && confidence !== null && confidence !== '';
785
+ const confidenceValue = hasConfidence ? Number(confidence) : NaN;
786
+
787
+ let workingRow = row;
788
+ let claimedFromOpen = false;
789
+ let claimedFromUnowned = false;
790
+ let claimedWorker = null;
791
+ function rollbackOpenDoClaim(reasonRow) {
792
+ if (!claimedFromOpen || !claimedWorker || !reasonRow) return;
793
+ const rollbackTime = Math.max(Date.now(), Number(reasonRow.updated_at || 0) + 1);
794
+ withBusyRetry(() => db.prepare(`
795
+ UPDATE tasks
796
+ SET status = 'open',
797
+ claimed_by = NULL,
798
+ claimed_at = NULL,
799
+ updated_at = ?
800
+ WHERE id = ?
801
+ AND status = 'claimed'
802
+ AND claimed_by IS ?
803
+ AND updated_at = ?
804
+ `).run(rollbackTime, id, claimedWorker, reasonRow.updated_at));
805
+ }
806
+ if (targetStage === 'do' && workingRow.status === 'open') {
807
+ claimedWorker = stageOwner || cleanStageText(actor) || process.env.ATRIS_AGENT_ID || process.env.USER || 'unknown';
808
+ const claimTime = Math.max(Date.now(), Number(workingRow.updated_at || 0) + 1);
809
+ const claimed = withBusyRetry(() => db.prepare(`
810
+ UPDATE tasks
811
+ SET status = 'claimed',
812
+ claimed_by = ?,
813
+ claimed_at = ?,
814
+ updated_at = ?
815
+ WHERE id = ?
816
+ AND status = 'open'
817
+ AND updated_at = ?
818
+ `).run(claimedWorker, claimTime, claimTime, id, workingRow.updated_at));
819
+ if (claimed.changes !== 1) return { staged: false, reason: 'stale_task_state' };
820
+ claimedFromOpen = true;
821
+ workingRow = getTask(db, id);
822
+ if (!workingRow) return { staged: false, reason: 'not_found' };
823
+ const plan = recordedDoPlan(workingRow);
824
+ if (!plan.ok) {
825
+ rollbackOpenDoClaim(workingRow);
826
+ return { staged: false, reason: plan.reason, claimed_by: plan.claimed_by || null };
827
+ }
828
+ metadata = plan.metadata;
829
+ stageOwner = plan.stageOwner;
830
+ goalText = plan.goalText;
831
+ proofText = plan.proofText;
832
+ exitText = plan.exitText;
833
+ }
834
+
835
+ const updateTime = Math.max(Date.now(), Number(workingRow && workingRow.updated_at || row.updated_at || 0) + 1);
836
+ const updateIso = new Date(updateTime).toISOString();
837
+ metadata.stage = targetStage;
838
+ metadata.task_goal = goalText;
839
+ metadata.goal_objective = goalText;
840
+ metadata.stage_goal = goalText;
841
+ if (targetStage === 'plan') {
842
+ metadata.planned_at = updateIso;
843
+ metadata.planned_by = cleanStageText(actor) || null;
844
+ metadata.stage_plan_recorded_at = metadata.planned_at;
845
+ }
846
+ metadata.stage_summary = cleanStageText(summary) || metadata.stage_summary || null;
847
+ metadata.exit_condition = exitText || metadata.exit_condition || null;
848
+ metadata.verify = proofText || metadata.verify || null;
849
+ metadata.proof_needed = proofText || metadata.proof_needed || null;
850
+ metadata.first_move = cleanStageText(firstMove) || metadata.first_move || null;
851
+ metadata.next_button = cleanStageText(nextButton) || (targetStage === 'plan' ? 'Start do' : 'Move to review');
852
+ metadata.stage_updated_at = updateIso;
853
+ metadata.stage_updated_by = cleanStageText(actor) || null;
854
+ if (stageOwner) {
855
+ metadata.stage_owner = stageOwner;
856
+ metadata.assigned_to = targetStage === 'do' ? stageOwner : (metadata.assigned_to || stageOwner);
857
+ }
858
+ if (hasConfidence && Number.isFinite(confidenceValue)) metadata.stage_confidence = Math.max(0, Math.min(1, confidenceValue));
859
+
860
+ let result;
861
+ if (targetStage === 'plan') {
862
+ const whereClaim = row.status === 'claimed'
863
+ ? 'AND status = ? AND claimed_by IS ? AND updated_at = ?'
864
+ : 'AND status = ? AND updated_at = ?';
865
+ const params = row.status === 'claimed'
866
+ ? [updateTime, JSON.stringify(metadata), id, row.status, row.claimed_by || null, row.updated_at]
867
+ : [updateTime, JSON.stringify(metadata), id, row.status, row.updated_at];
868
+ result = withBusyRetry(() => db.prepare(`
869
+ UPDATE tasks
870
+ SET done_at = NULL,
871
+ updated_at = ?,
872
+ metadata = ?
873
+ WHERE id = ?
874
+ ${whereClaim}
875
+ `).run(...params));
876
+ } else {
877
+ const worker = workingRow && workingRow.claimed_by || stageOwner || cleanStageText(actor) || null;
878
+ if (workingRow && workingRow.status === 'claimed' && !workingRow.claimed_by && worker) {
879
+ result = withBusyRetry(() => db.prepare(`
880
+ UPDATE tasks
881
+ SET done_at = NULL,
882
+ claimed_by = ?,
883
+ claimed_at = ?,
884
+ updated_at = ?,
885
+ metadata = ?
886
+ WHERE id = ?
887
+ AND status = 'claimed'
888
+ AND claimed_by IS NULL
889
+ AND updated_at = ?
890
+ `).run(worker, updateTime, updateTime, JSON.stringify(metadata), id, workingRow.updated_at));
891
+ claimedFromUnowned = result.changes === 1;
892
+ claimedWorker = worker;
893
+ } else {
894
+ result = withBusyRetry(() => db.prepare(`
895
+ UPDATE tasks
896
+ SET done_at = NULL,
897
+ updated_at = ?,
898
+ metadata = ?
899
+ WHERE id = ?
900
+ AND status = 'claimed'
901
+ AND claimed_by IS ?
902
+ AND updated_at = ?
903
+ `).run(updateTime, JSON.stringify(metadata), id, worker, workingRow.updated_at));
904
+ }
905
+ }
906
+ if (result.changes !== 1) {
907
+ rollbackOpenDoClaim(workingRow);
908
+ return { staged: false, reason: 'stale_task_state' };
909
+ }
910
+
911
+ const payload = {
912
+ stage: targetStage,
913
+ goal: goalText,
914
+ summary: cleanStageText(summary) || null,
915
+ owner: stageOwner,
916
+ exit: exitText || null,
917
+ proof_needed: proofText || null,
918
+ first_move: cleanStageText(firstMove) || null,
919
+ next_button: metadata.next_button || null,
920
+ confidence: Number.isFinite(confidenceValue) ? metadata.stage_confidence : null,
921
+ };
922
+ payload.stage_packet = stagePacketFromPayload(payload);
923
+ const updated = getTask(db, id);
924
+ if ((claimedFromOpen || claimedFromUnowned) && targetStage === 'do') {
925
+ appendTaskEvent(db, {
926
+ taskId: id,
927
+ workspaceRoot: updated.workspace_root,
928
+ actor: claimedWorker,
929
+ eventType: 'claimed',
930
+ payload: { claimed_by: claimedWorker },
931
+ });
932
+ }
933
+ const event = appendTaskEvent(db, {
934
+ taskId: id,
935
+ workspaceRoot: updated.workspace_root,
936
+ actor: cleanStageText(actor) || null,
937
+ eventType: targetStage === 'plan' ? 'task_planned' : 'work_started',
938
+ payload,
939
+ });
940
+ return { staged: true, event, row: updated, stage_packet: payload.stage_packet };
941
+ }
942
+
497
943
  function appendTaskEvent(db, { taskId, workspaceRoot: ws, actor, eventType, payload }) {
498
944
  if (!taskId) throw new Error('taskId required');
499
945
  if (!ws) throw new Error('workspaceRoot required');
@@ -562,7 +1008,72 @@ function noteTask(db, { id, actor, content }) {
562
1008
  return { noted: true, event };
563
1009
  }
564
1010
 
565
- function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, careerXpEligible = false, clearedFields = [] }) {
1011
+ function taskChatPacketFromPayload(payload) {
1012
+ const data = payload && typeof payload === 'object' ? payload : {};
1013
+ const lines = ['TASK_CHAT_UPDATE'];
1014
+ if (data.goal) lines.push(`goal: ${data.goal}`);
1015
+ if (data.summary) lines.push(`summary: ${data.summary}`);
1016
+ if (data.content) lines.push(`message: ${data.content}`);
1017
+ return lines.join('\n');
1018
+ }
1019
+
1020
+ function chatTask(db, { id, actor, content, goal, summary }) {
1021
+ if (!id) throw new Error('id required');
1022
+ const text = cleanStageText(content);
1023
+ const goalText = cleanStageText(goal);
1024
+ const summaryText = cleanStageText(summary);
1025
+ if (!text && !goalText && !summaryText) return { chatted: false, reason: 'content_required' };
1026
+ const row = getTask(db, id);
1027
+ if (!row) return { chatted: false, reason: 'not_found' };
1028
+ if (['done', 'failed'].includes(row.status)) return { chatted: false, reason: `already_${row.status}` };
1029
+
1030
+ const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
1031
+ const previousGoal = cleanStageText(metadata.task_goal || metadata.goal_objective || metadata.objective || metadata.stage_goal);
1032
+ const actorText = cleanStageText(actor) || null;
1033
+ const now = Math.max(Date.now(), Number(row.updated_at || 0) + 1);
1034
+ const payload = {
1035
+ content: text || null,
1036
+ goal: goalText || null,
1037
+ summary: summaryText || null,
1038
+ previous_goal: goalText && previousGoal && previousGoal !== goalText ? previousGoal : null,
1039
+ };
1040
+ payload.chat_packet = taskChatPacketFromPayload(payload);
1041
+
1042
+ if (goalText || summaryText) {
1043
+ metadata.task_goal = goalText || metadata.task_goal || null;
1044
+ if (goalText && !metadata.goal_objective) metadata.goal_objective = goalText;
1045
+ if (goalText && !metadata.objective) metadata.objective = goalText;
1046
+ if (summaryText) metadata.task_summary = summaryText;
1047
+ metadata.task_refined_at = new Date(now).toISOString();
1048
+ metadata.task_refined_by = actorText;
1049
+ const updated = withBusyRetry(() => db.prepare(`
1050
+ UPDATE tasks
1051
+ SET metadata = ?,
1052
+ updated_at = ?
1053
+ WHERE id = ?
1054
+ AND updated_at = ?
1055
+ `).run(JSON.stringify(metadata), now, id, row.updated_at));
1056
+ if (updated.changes !== 1) return { chatted: false, reason: 'stale_task_state' };
1057
+ }
1058
+
1059
+ const latest = goalText || summaryText ? getTask(db, id) : row;
1060
+ const event = appendTaskEvent(db, {
1061
+ taskId: id,
1062
+ workspaceRoot: latest.workspace_root,
1063
+ actor: actorText,
1064
+ eventType: 'task_chat',
1065
+ payload,
1066
+ });
1067
+ return {
1068
+ chatted: true,
1069
+ event,
1070
+ row: latest,
1071
+ goal_changed: Boolean(goalText && goalText !== previousGoal),
1072
+ chat_packet: payload.chat_packet,
1073
+ };
1074
+ }
1075
+
1076
+ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, careerXpEligible = false, clearedFields = [] }) {
566
1077
  if (!id) throw new Error('id required');
567
1078
  const row = getTask(db, id);
568
1079
  if (!row) return { reviewed: false, reason: 'not_found' };
@@ -570,15 +1081,38 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, careerXpEl
570
1081
  const now = Date.now();
571
1082
  const reviewer = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
572
1083
  const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
1084
+ const proofText = String(proof || '').trim();
1085
+ const verifyText = cleanStageText(verify);
1086
+ const lessonText = String(lesson || '').trim();
1087
+ const nextTaskText = String(nextTask || '').trim();
1088
+ const clearedReviewFields = Array.isArray(clearedFields)
1089
+ ? Array.from(new Set(clearedFields.filter(field => field === 'lesson' || field === 'next_task')))
1090
+ : [];
573
1091
  const reviewingPendingProof = row.status === 'review'
574
1092
  && metadata.approval_status === 'pending'
1093
+ && numericReward <= 0
575
1094
  && metadata.agent_certified !== true;
1095
+ const updatingPendingReviewProof = row.status === 'review'
1096
+ && metadata.approval_status === 'pending'
1097
+ && numericReward <= 0
1098
+ && Boolean(proofText || verifyText || lessonText || nextTaskText || clearedReviewFields.length);
576
1099
  let reviewPassCount = Number(metadata.agent_review_pass_count || 0);
1100
+ if (reviewingPendingProof || updatingPendingReviewProof) {
1101
+ metadata.agent_reviewed_at = new Date(now).toISOString();
1102
+ metadata.agent_reviewed_by = reviewer;
1103
+ if (proofText) metadata.latest_agent_proof = proofText;
1104
+ if (verifyText) {
1105
+ metadata.verify = verifyText;
1106
+ metadata.latest_agent_verify = verifyText;
1107
+ }
1108
+ if (clearedReviewFields.includes('lesson')) metadata.latest_agent_lesson = null;
1109
+ else if (lessonText) metadata.latest_agent_lesson = lessonText;
1110
+ if (clearedReviewFields.includes('next_task')) metadata.latest_agent_next_task = null;
1111
+ else if (nextTaskText) metadata.latest_agent_next_task = nextTaskText;
1112
+ }
577
1113
  if (reviewingPendingProof) {
578
1114
  reviewPassCount += 1;
579
1115
  metadata.agent_review_pass_count = reviewPassCount;
580
- metadata.agent_reviewed_at = new Date(now).toISOString();
581
- metadata.agent_reviewed_by = reviewer;
582
1116
  if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES) {
583
1117
  metadata.agent_certified = true;
584
1118
  metadata.agent_certified_at = new Date(now).toISOString();
@@ -591,7 +1125,7 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, careerXpEl
591
1125
  metadata.accepted_at = new Date().toISOString();
592
1126
  metadata.accepted_by = reviewer;
593
1127
  }
594
- if (reviewingPendingProof || (numericReward > 0 && row.status === 'done')) {
1128
+ if (reviewingPendingProof || updatingPendingReviewProof || (numericReward > 0 && row.status === 'done')) {
595
1129
  withBusyRetry(() => db.prepare(`
596
1130
  UPDATE tasks
597
1131
  SET metadata = ?,
@@ -601,9 +1135,10 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, careerXpEl
601
1135
  }
602
1136
  const payload = {
603
1137
  reward: numericReward,
604
- lesson: String(lesson || '').trim(),
605
- next_task: String(nextTask || '').trim() || null,
606
- proof: String(proof || '').trim() || null,
1138
+ lesson: lessonText,
1139
+ next_task: nextTaskText || null,
1140
+ proof: proofText || null,
1141
+ verify: verifyText || null,
607
1142
  career_xp_eligible: Boolean(careerXpEligible),
608
1143
  };
609
1144
  if (reviewingPendingProof) {
@@ -611,9 +1146,6 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, careerXpEl
611
1146
  payload.agent_certified = metadata.agent_certified === true;
612
1147
  payload.agent_certification_policy = metadata.agent_certification_policy || null;
613
1148
  }
614
- const clearedReviewFields = Array.isArray(clearedFields)
615
- ? Array.from(new Set(clearedFields.filter(field => field === 'lesson' || field === 'next_task')))
616
- : [];
617
1149
  if (clearedReviewFields.length) payload.cleared_review_fields = clearedReviewFields;
618
1150
  const event = appendTaskEvent(db, {
619
1151
  taskId: id,
@@ -636,7 +1168,7 @@ function compactEpisodeText(value, max = 240) {
636
1168
  function goalSignalFromTaskMetadata(metadata) {
637
1169
  const goalId = compactEpisodeText(metadata.goal_id || metadata.goalId || metadata.goal?.id || '', 120);
638
1170
  const objective = compactEpisodeText(
639
- metadata.goal_objective || metadata.goalObjective || metadata.goal?.objective || metadata.goal || '',
1171
+ metadata.task_goal || metadata.goal_objective || metadata.goalObjective || metadata.goal?.objective || metadata.goal || '',
640
1172
  240,
641
1173
  );
642
1174
  if (!goalId && !objective) return null;
@@ -813,14 +1345,25 @@ function taskProjection(db, {
813
1345
  const taskEvents = byTask.get(row.id) || [];
814
1346
  const latest = taskEvents.length ? taskEvents[taskEvents.length - 1] : null;
815
1347
  const allMessages = taskEvents
816
- .filter(e => e.event_type === 'message')
1348
+ .filter(e => e.event_type === 'message' || e.event_type === 'task_chat' || e.event_type === 'task_planned' || e.event_type === 'work_started' || e.event_type === 'task_backlogged')
817
1349
  .map(e => ({
818
1350
  version: e.version,
819
1351
  actor: e.actor,
820
- content: clipProjectionText(e.payload && e.payload.content || ''),
1352
+ content: e.event_type === 'message'
1353
+ ? (e.payload && e.payload.content || '')
1354
+ : e.event_type === 'task_chat'
1355
+ ? (e.payload && (e.payload.chat_packet || taskChatPacketFromPayload(e.payload)) || '')
1356
+ : e.event_type === 'task_backlogged'
1357
+ ? (e.payload && backlogPacketFromPayload(e.payload) || '')
1358
+ : (e.payload && (e.payload.stage_packet || stagePacketFromPayload(e.payload)) || ''),
821
1359
  created_at: e.created_at,
822
1360
  }));
823
- const visibleMessages = includeHistory ? allMessages : allMessages.slice(-Math.max(0, Number(messageLimit) || 0));
1361
+ const visibleMessages = includeHistory
1362
+ ? allMessages
1363
+ : allMessages.slice(-Math.max(0, Number(messageLimit) || 0)).map(message => ({
1364
+ ...message,
1365
+ content: clipProjectionText(message.content),
1366
+ }));
824
1367
  const visibleEvents = includeHistory ? taskEvents : taskEvents.slice(-Math.max(0, Number(eventLimit) || 0)).map(compactProjectionEvent);
825
1368
  return {
826
1369
  id: row.id,
@@ -851,7 +1394,7 @@ function taskProjection(db, {
851
1394
  };
852
1395
  }
853
1396
 
854
- function renderTodoMarkdown(rows, { title = 'TODO.md', doneLimit = TODO_RENDER_DONE_LIMIT, refRows = rows, preservedSections = [] } = {}) {
1397
+ function renderTodoMarkdown(rows, { title = 'TODO.md', doneLimit = TODO_RENDER_DONE_LIMIT, failedLimit = TODO_RENDER_FAILED_LIMIT, refRows = rows, preservedSections = [] } = {}) {
855
1398
  const displayRows = withTaskDisplayRefs(rows, refRows);
856
1399
  const buckets = {
857
1400
  open: displayRows.filter(r => r.status === 'open'),
@@ -869,7 +1412,12 @@ function renderTodoMarkdown(rows, { title = 'TODO.md', doneLimit = TODO_RENDER_D
869
1412
  appendSection(lines, 'Backlog', buckets.open);
870
1413
  appendSection(lines, 'In Progress', buckets.claimed);
871
1414
  appendSection(lines, 'Review', buckets.review);
872
- appendSection(lines, 'Blocked', buckets.failed);
1415
+ const renderedFailed = buckets.failed.slice(0, Math.max(0, Number(failedLimit) || 0));
1416
+ appendSection(lines, 'Blocked', renderedFailed);
1417
+ const archivedFailed = Math.max(0, buckets.failed.length - renderedFailed.length);
1418
+ if (archivedFailed > 0) {
1419
+ lines.push(`(${archivedFailed} older blocked task${archivedFailed === 1 ? '' : 's'} archived in \`atris task list --status failed\` and \`atris task events\`.)`, '');
1420
+ }
873
1421
  const renderedDone = buckets.done.slice(0, Math.max(0, Number(doneLimit) || 0));
874
1422
  appendSection(lines, 'Completed', renderedDone);
875
1423
  const archivedDone = Math.max(0, buckets.done.length - renderedDone.length);
@@ -942,10 +1490,14 @@ module.exports = {
942
1490
  getTask,
943
1491
  listTasks,
944
1492
  claimTask,
1493
+ backlogTask,
1494
+ clearPlanTasks,
945
1495
  doneTask,
946
1496
  readyTask,
947
1497
  reviseTask,
1498
+ stageTask,
948
1499
  noteTask,
1500
+ chatTask,
949
1501
  reviewTask,
950
1502
  appendTaskEvent,
951
1503
  listTaskEvents,