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.
- package/AGENTS.md +2 -2
- package/GETTING_STARTED.md +1 -1
- package/PERSONA.md +4 -4
- package/README.md +11 -11
- package/atris/skills/copy-editor/SKILL.md +30 -4
- package/atris/skills/improve/SKILL.md +18 -20
- package/atris/wiki/concepts/agent-activation-contract.md +5 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
- package/atris/wiki/index.md +1 -0
- package/ax +522 -73
- package/bin/atris.js +31 -30
- package/commands/align.js +0 -14
- package/commands/apps.js +102 -1
- package/commands/autopilot.js +197 -22
- package/commands/brain.js +219 -34
- package/commands/brainstorm.js +0 -829
- package/commands/computer.js +0 -60
- package/commands/improve.js +501 -0
- package/commands/integrations.js +233 -71
- package/commands/lesson.js +44 -0
- package/commands/member.js +4498 -226
- package/commands/mission.js +302 -27
- package/commands/now.js +89 -1
- package/commands/radar.js +181 -56
- package/commands/soul.js +0 -4
- package/commands/task.js +5582 -517
- package/commands/terminal.js +14 -10
- package/commands/wiki.js +87 -1
- package/commands/workflow.js +288 -73
- package/commands/worktree.js +52 -15
- package/commands/xp.js +6 -65
- package/lib/auto-accept-certified.js +294 -0
- package/lib/file-ops.js +0 -184
- package/lib/member-alive.js +232 -0
- package/lib/policy-lessons.js +280 -0
- package/lib/receipt-evidence.js +64 -0
- package/lib/state-detection.js +34 -0
- package/lib/task-db.js +568 -16
- package/lib/task-proof.js +43 -0
- package/package.json +1 -1
- package/utils/auth.js +13 -4
- package/commands/research.js +0 -52
- 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
|
|
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:
|
|
605
|
-
next_task:
|
|
606
|
-
proof:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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,
|