atris 3.15.37 → 3.15.39
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/ax +190 -8
- package/commands/mission.js +21 -11
- package/commands/task.js +46 -23
- package/lib/todo-fallback.js +2 -2
- package/package.json +1 -1
package/ax
CHANGED
|
@@ -107,6 +107,54 @@ function formatUsage() {
|
|
|
107
107
|
].join('\n');
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
function timestampForFile(date = new Date()) {
|
|
111
|
+
return date.toISOString().replace(/[:.]/g, '-');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stripAnsi(value) {
|
|
115
|
+
return String(value || '').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', output = process.stdout } = {}) {
|
|
119
|
+
if (process.env.AX_AUTO_LOG === '0') return null;
|
|
120
|
+
if (output !== process.stdout && output !== process.stderr && !output.isTTY) return null;
|
|
121
|
+
|
|
122
|
+
const baseDir = fs.existsSync(path.join(cwd, 'atris'))
|
|
123
|
+
? path.join(cwd, 'atris', 'runs')
|
|
124
|
+
: path.join(os.homedir(), '.atris', 'runs');
|
|
125
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
126
|
+
const logPath = path.join(baseDir, `ax-${kind}-${timestampForFile()}.log`);
|
|
127
|
+
fs.appendFileSync(logPath, [
|
|
128
|
+
`command: ${process.argv.join(' ')}`,
|
|
129
|
+
`cwd: ${cwd}`,
|
|
130
|
+
`mode: ${mode}`,
|
|
131
|
+
`started_at: ${new Date().toISOString()}`,
|
|
132
|
+
'',
|
|
133
|
+
].join('\n'));
|
|
134
|
+
|
|
135
|
+
const writeLog = (chunk) => {
|
|
136
|
+
fs.appendFileSync(logPath, stripAnsi(chunk));
|
|
137
|
+
};
|
|
138
|
+
const teeOutput = {
|
|
139
|
+
isTTY: Boolean(output && output.isTTY),
|
|
140
|
+
write(chunk) {
|
|
141
|
+
const text = String(chunk || '');
|
|
142
|
+
if (output && typeof output.write === 'function') output.write(text);
|
|
143
|
+
writeLog(text);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
path: logPath,
|
|
150
|
+
output: teeOutput,
|
|
151
|
+
write: writeLog,
|
|
152
|
+
close(exitCode = 0) {
|
|
153
|
+
writeLog(`\nexit_code: ${exitCode}\nfinished_at: ${new Date().toISOString()}\n`);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
110
158
|
function backendBaseUrl() {
|
|
111
159
|
return (process.env.AX_BACKEND_URL
|
|
112
160
|
|| process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL
|
|
@@ -339,9 +387,16 @@ function workspaceIntent(message) {
|
|
|
339
387
|
return /\b(files?|folders?|repo|workspace|project|directory|tree|read|open|inspect|search|grep|find|locate|where|edit|write|change|modify|patch|fix|test|tests?|build|src|source|code|diff|git|backend|frontend|atris task|atris xp|xp game|career xp|agentxp|todo|map)\b/i.test(message || '');
|
|
340
388
|
}
|
|
341
389
|
|
|
390
|
+
function githubWorkspaceIntent(message) {
|
|
391
|
+
const text = String(message || '');
|
|
392
|
+
if (!/\bgithub\b/i.test(text)) return false;
|
|
393
|
+
return /\b(push|commit|commits?|branch|branches|checkout|merge|rebase|tag|release|pr|pull request|pull-request|repo change|code change|small change)\b/i.test(text);
|
|
394
|
+
}
|
|
395
|
+
|
|
342
396
|
function resolveRoute(message, options = {}) {
|
|
343
397
|
if (options.route === 'local' || options.forceLocal) return 'local';
|
|
344
398
|
if (options.route === 'cloud' || options.forceCloud) return 'cloud';
|
|
399
|
+
if (githubWorkspaceIntent(message)) return 'local';
|
|
345
400
|
if (mentionsConnector(message) && !workspaceIntent(message)) return 'cloud';
|
|
346
401
|
return 'local';
|
|
347
402
|
}
|
|
@@ -370,6 +425,112 @@ function paint(text, codes, options = {}) {
|
|
|
370
425
|
return `${codes.join('')}${text}${ANSI.reset}`;
|
|
371
426
|
}
|
|
372
427
|
|
|
428
|
+
function renderTerminalMarkdown(text, options = {}) {
|
|
429
|
+
let rendered = String(text || '');
|
|
430
|
+
const codeSpans = [];
|
|
431
|
+
rendered = rendered.replace(/`([^`\n]+)`/g, (_, code) => {
|
|
432
|
+
const token = `\u0000CODE${codeSpans.length}\u0000`;
|
|
433
|
+
codeSpans.push(code);
|
|
434
|
+
return token;
|
|
435
|
+
});
|
|
436
|
+
rendered = rendered.replace(/^#{1,6}\s+(.+)$/gm, (_, title) => paint(title, [ANSI.bold], options));
|
|
437
|
+
rendered = rendered.replace(/\*\*([^*\n]+)\*\*/g, (_, value) => paint(value, [ANSI.bold], options));
|
|
438
|
+
rendered = rendered.replace(/__([^_\n]+)__/g, (_, value) => paint(value, [ANSI.bold], options));
|
|
439
|
+
rendered = rendered.replace(/\u0000CODE(\d+)\u0000/g, (_, index) => paint(codeSpans[Number(index)] || '', [ANSI.accent], options));
|
|
440
|
+
return rendered;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function resetMarkdownState(state) {
|
|
444
|
+
state.markdownMode = 'normal';
|
|
445
|
+
state.markdownBuffer = '';
|
|
446
|
+
state.markdownCarry = '';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function ensureMarkdownState(state) {
|
|
450
|
+
if (!state.markdownMode) state.markdownMode = 'normal';
|
|
451
|
+
if (typeof state.markdownBuffer !== 'string') state.markdownBuffer = '';
|
|
452
|
+
if (typeof state.markdownCarry !== 'string') state.markdownCarry = '';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function renderStreamingMarkdown(state, text, options = {}) {
|
|
456
|
+
ensureMarkdownState(state);
|
|
457
|
+
const input = `${state.markdownCarry || ''}${String(text || '')}`;
|
|
458
|
+
state.markdownCarry = '';
|
|
459
|
+
let out = '';
|
|
460
|
+
|
|
461
|
+
for (let i = 0; i < input.length;) {
|
|
462
|
+
const char = input[i];
|
|
463
|
+
const next = input[i + 1];
|
|
464
|
+
|
|
465
|
+
if (state.markdownMode === 'normal') {
|
|
466
|
+
if (char === '*' && next === undefined) {
|
|
467
|
+
state.markdownCarry = '*';
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
if (char === '*' && next === '*') {
|
|
471
|
+
state.markdownMode = 'bold';
|
|
472
|
+
state.markdownBuffer = '';
|
|
473
|
+
i += 2;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (char === '`') {
|
|
477
|
+
state.markdownMode = 'code';
|
|
478
|
+
state.markdownBuffer = '';
|
|
479
|
+
i += 1;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
out += char;
|
|
483
|
+
i += 1;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (state.markdownMode === 'bold') {
|
|
488
|
+
if (char === '*' && next === undefined) {
|
|
489
|
+
state.markdownCarry = '*';
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (char === '*' && next === '*') {
|
|
493
|
+
out += paint(state.markdownBuffer, [ANSI.bold], options);
|
|
494
|
+
state.markdownMode = 'normal';
|
|
495
|
+
state.markdownBuffer = '';
|
|
496
|
+
i += 2;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
state.markdownBuffer += char;
|
|
500
|
+
i += 1;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (state.markdownMode === 'code') {
|
|
505
|
+
if (char === '`') {
|
|
506
|
+
out += paint(state.markdownBuffer, [ANSI.accent], options);
|
|
507
|
+
state.markdownMode = 'normal';
|
|
508
|
+
state.markdownBuffer = '';
|
|
509
|
+
i += 1;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
state.markdownBuffer += char;
|
|
513
|
+
i += 1;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function flushStreamingMarkdown(state, output) {
|
|
522
|
+
ensureMarkdownState(state);
|
|
523
|
+
let out = state.markdownCarry || '';
|
|
524
|
+
if (state.markdownMode === 'bold' && state.markdownBuffer) {
|
|
525
|
+
out += paint(state.markdownBuffer, [ANSI.bold], output);
|
|
526
|
+
} else if (state.markdownMode === 'code' && state.markdownBuffer) {
|
|
527
|
+
out += paint(state.markdownBuffer, [ANSI.accent], output);
|
|
528
|
+
}
|
|
529
|
+
resetMarkdownState(state);
|
|
530
|
+
if (out) output.write(out);
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
|
|
373
534
|
function truncateMiddle(value, limit = 120) {
|
|
374
535
|
const text = String(value || '');
|
|
375
536
|
if (text.length <= limit) return text;
|
|
@@ -523,6 +684,7 @@ function clearRetriedText(state) {
|
|
|
523
684
|
state.wroteText = false;
|
|
524
685
|
state.lastChar = '\n';
|
|
525
686
|
state.inAuxBlock = false;
|
|
687
|
+
resetMarkdownState(state);
|
|
526
688
|
}
|
|
527
689
|
|
|
528
690
|
function stopProgress(state) {
|
|
@@ -532,13 +694,20 @@ function stopProgress(state) {
|
|
|
532
694
|
}
|
|
533
695
|
|
|
534
696
|
function flushPendingText(state, output) {
|
|
535
|
-
|
|
697
|
+
const hasMarkdownRemainder = output && output.isTTY && (
|
|
698
|
+
state.markdownCarry
|
|
699
|
+
|| state.markdownBuffer
|
|
700
|
+
|| (state.markdownMode && state.markdownMode !== 'normal')
|
|
701
|
+
);
|
|
702
|
+
if (!state.pendingText && !hasMarkdownRemainder) return;
|
|
536
703
|
stopProgress(state);
|
|
537
704
|
if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
|
|
538
|
-
output.write(state.pendingText);
|
|
705
|
+
if (state.pendingText) output.write(output && output.isTTY ? renderTerminalMarkdown(state.pendingText, output) : state.pendingText);
|
|
706
|
+
const flushedMarkdown = hasMarkdownRemainder ? flushStreamingMarkdown(state, output) : '';
|
|
539
707
|
state.wroteText = true;
|
|
540
708
|
state.wroteActivity = true;
|
|
541
|
-
|
|
709
|
+
const written = `${state.pendingText || ''}${flushedMarkdown || ''}`;
|
|
710
|
+
state.lastChar = written.slice(-1);
|
|
542
711
|
state.pendingText = '';
|
|
543
712
|
state.inAuxBlock = false;
|
|
544
713
|
}
|
|
@@ -547,10 +716,11 @@ function writeStreamingText(state, output, content) {
|
|
|
547
716
|
if (!content) return;
|
|
548
717
|
stopProgress(state);
|
|
549
718
|
if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
|
|
550
|
-
|
|
719
|
+
const rendered = renderStreamingMarkdown(state, content, output);
|
|
720
|
+
if (rendered) output.write(rendered);
|
|
551
721
|
state.wroteText = true;
|
|
552
722
|
state.wroteActivity = true;
|
|
553
|
-
state.lastChar = String(content).slice(-1);
|
|
723
|
+
state.lastChar = String(rendered || content).slice(-1);
|
|
554
724
|
state.inAuxBlock = false;
|
|
555
725
|
}
|
|
556
726
|
|
|
@@ -691,7 +861,10 @@ async function postTurn(message, options = {}) {
|
|
|
691
861
|
durationMs: 0,
|
|
692
862
|
lastChar: '\n',
|
|
693
863
|
progress: null,
|
|
694
|
-
inAuxBlock: false
|
|
864
|
+
inAuxBlock: false,
|
|
865
|
+
markdownMode: 'normal',
|
|
866
|
+
markdownBuffer: '',
|
|
867
|
+
markdownCarry: ''
|
|
695
868
|
};
|
|
696
869
|
|
|
697
870
|
return new Promise((resolve, reject) => {
|
|
@@ -789,16 +962,20 @@ async function chat(options = {}) {
|
|
|
789
962
|
const mode = options.mode === 'fast' ? 'fast' : 'pro';
|
|
790
963
|
const cwd = options.cwd || process.cwd();
|
|
791
964
|
const input = options.input || process.stdin;
|
|
792
|
-
const
|
|
965
|
+
const baseOutput = options.output || process.stdout;
|
|
966
|
+
const logger = createRunLogger({ cwd, mode, kind: 'play', output: baseOutput });
|
|
967
|
+
const output = logger ? logger.output : baseOutput;
|
|
793
968
|
const history = [];
|
|
794
969
|
|
|
795
970
|
output.write(`${formatHeader({ mode, cwd, chat: true })}\n\n`);
|
|
971
|
+
if (logger) output.write(`${formatAuxRow('log', formatPathSubject(logger.path, output), output)}\n\n`);
|
|
796
972
|
|
|
797
973
|
const runLine = async (line) => {
|
|
798
974
|
const trimmed = String(line || '').trim();
|
|
799
975
|
if (!trimmed) return false;
|
|
800
976
|
if (EXIT_WORDS.has(trimmed.toLowerCase())) return true;
|
|
801
977
|
|
|
978
|
+
if (logger) logger.write(`${formatPrompt(mode)}${trimmed}\n`);
|
|
802
979
|
output.write('\n');
|
|
803
980
|
const result = await postTurn(trimmed, { mode, cwd, history, output });
|
|
804
981
|
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
@@ -813,10 +990,11 @@ async function chat(options = {}) {
|
|
|
813
990
|
for await (const line of rl) {
|
|
814
991
|
if (await runLine(line)) break;
|
|
815
992
|
}
|
|
993
|
+
if (logger) logger.close(0);
|
|
816
994
|
return;
|
|
817
995
|
}
|
|
818
996
|
|
|
819
|
-
const rl = readline.createInterface({ input, output });
|
|
997
|
+
const rl = readline.createInterface({ input, output: baseOutput });
|
|
820
998
|
const ask = () => new Promise(resolve => rl.question(formatPrompt(mode), resolve));
|
|
821
999
|
while (true) {
|
|
822
1000
|
const line = await ask();
|
|
@@ -825,6 +1003,7 @@ async function chat(options = {}) {
|
|
|
825
1003
|
break;
|
|
826
1004
|
}
|
|
827
1005
|
}
|
|
1006
|
+
if (logger) logger.close(0);
|
|
828
1007
|
}
|
|
829
1008
|
|
|
830
1009
|
function printBackendHint() {
|
|
@@ -1061,6 +1240,7 @@ module.exports = {
|
|
|
1061
1240
|
buildConnectionContext,
|
|
1062
1241
|
buildRunProfile,
|
|
1063
1242
|
chat,
|
|
1243
|
+
createRunLogger,
|
|
1064
1244
|
createProgressReporter,
|
|
1065
1245
|
formatDoneLine,
|
|
1066
1246
|
formatDuration,
|
|
@@ -1076,6 +1256,8 @@ module.exports = {
|
|
|
1076
1256
|
modelForMode,
|
|
1077
1257
|
parseSseBlock,
|
|
1078
1258
|
postTurn,
|
|
1259
|
+
renderStreamingMarkdown,
|
|
1260
|
+
renderTerminalMarkdown,
|
|
1079
1261
|
resolveRoute,
|
|
1080
1262
|
runBenchmark,
|
|
1081
1263
|
summarizeToolInput,
|
package/commands/mission.js
CHANGED
|
@@ -747,27 +747,36 @@ function secondsUntilMissionDue(mission, now = new Date()) {
|
|
|
747
747
|
return Math.max(0, Math.ceil((dueAt - now.getTime()) / 1000));
|
|
748
748
|
}
|
|
749
749
|
|
|
750
|
+
function missionIsRunnable(mission) {
|
|
751
|
+
return mission && !TERMINAL_STATUSES.has(mission.status) && mission.status !== 'paused';
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function missionSortTime(mission) {
|
|
755
|
+
return Date.parse(mission?.updated_at || mission?.created_at || '') || 0;
|
|
756
|
+
}
|
|
757
|
+
|
|
750
758
|
function selectDueMission(root = process.cwd(), now = new Date()) {
|
|
751
759
|
const candidates = listMissions(root)
|
|
752
|
-
.filter(
|
|
760
|
+
.filter(missionIsRunnable)
|
|
753
761
|
.filter((mission) => mission.verifier)
|
|
754
762
|
.filter((mission) => mission.always_on || !missionVerifierPassed(mission))
|
|
755
763
|
.filter((mission) => missionDueAt(mission, now));
|
|
756
764
|
|
|
757
765
|
candidates.sort((a, b) => {
|
|
758
|
-
const
|
|
759
|
-
const
|
|
760
|
-
return
|
|
766
|
+
const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
|
|
767
|
+
const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
|
|
768
|
+
if (aCaller !== bCaller) return bCaller - aCaller;
|
|
769
|
+
|
|
770
|
+
const aTime = missionSortTime(a);
|
|
771
|
+
const bTime = missionSortTime(b);
|
|
772
|
+
return bTime - aTime;
|
|
761
773
|
});
|
|
762
774
|
return candidates[0] || null;
|
|
763
775
|
}
|
|
764
776
|
|
|
765
777
|
function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
|
|
766
|
-
const due = selectDueMission(root, now);
|
|
767
|
-
if (due) return { mission: due, reason: 'due' };
|
|
768
|
-
|
|
769
778
|
const candidates = listMissions(root)
|
|
770
|
-
.filter(
|
|
779
|
+
.filter(missionIsRunnable);
|
|
771
780
|
|
|
772
781
|
candidates.sort((a, b) => {
|
|
773
782
|
const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
|
|
@@ -778,14 +787,15 @@ function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
|
|
|
778
787
|
const bVerifier = b.verifier ? 1 : 0;
|
|
779
788
|
if (aVerifier !== bVerifier) return bVerifier - aVerifier;
|
|
780
789
|
|
|
781
|
-
const aTime =
|
|
782
|
-
const bTime =
|
|
790
|
+
const aTime = missionSortTime(a);
|
|
791
|
+
const bTime = missionSortTime(b);
|
|
783
792
|
return bTime - aTime;
|
|
784
793
|
});
|
|
785
794
|
|
|
786
795
|
const mission = candidates[0] || null;
|
|
787
796
|
if (!mission) return null;
|
|
788
|
-
|
|
797
|
+
const due = mission.verifier && missionDueAt(mission, now);
|
|
798
|
+
return { mission, reason: due ? 'due' : 'active' };
|
|
789
799
|
}
|
|
790
800
|
|
|
791
801
|
function codexGoalObjective(mission) {
|
package/commands/task.js
CHANGED
|
@@ -795,6 +795,15 @@ function taskStatusSummary(projection, { history = false } = {}) {
|
|
|
795
795
|
done: tasks.filter(task => taskColumn(task) === 'done'),
|
|
796
796
|
};
|
|
797
797
|
const active = [...columns.do, ...columns.review, ...columns.plan];
|
|
798
|
+
const reviewNeedingAgentAction = columns.review.filter(task => {
|
|
799
|
+
const handoff = reviewHandoffForTask(task);
|
|
800
|
+
return handoff && handoff.next_action === 'agent_review_again';
|
|
801
|
+
});
|
|
802
|
+
const reviewAgentCertified = columns.review.filter(task => {
|
|
803
|
+
const handoff = reviewHandoffForTask(task);
|
|
804
|
+
return handoff && handoff.next_action === 'continue_work';
|
|
805
|
+
}).length;
|
|
806
|
+
const blocked = columns.review.filter(task => taskColumn(task) === 'blocked').length;
|
|
798
807
|
const lastUpdated = tasks.reduce((max, task) => Math.max(max, Number(task.updated_at || 0)), 0);
|
|
799
808
|
const swarloFeed = history ? tasks
|
|
800
809
|
.flatMap(task => (task.events || []).map(event => ({
|
|
@@ -830,14 +839,17 @@ function taskStatusSummary(projection, { history = false } = {}) {
|
|
|
830
839
|
goals: projection.goals || { source_path: null, items: [] },
|
|
831
840
|
counts: {
|
|
832
841
|
total: fullTaskCount,
|
|
833
|
-
active: columns.plan.length + columns.do.length +
|
|
842
|
+
active: columns.plan.length + columns.do.length + reviewNeedingAgentAction.length,
|
|
834
843
|
backlog: columns.backlog.length,
|
|
835
844
|
plan: columns.plan.length,
|
|
836
845
|
do: columns.do.length,
|
|
837
846
|
review: columns.review.length,
|
|
847
|
+
review_blocking: reviewNeedingAgentAction.length,
|
|
848
|
+
review_certified: reviewAgentCertified,
|
|
849
|
+
blocked,
|
|
838
850
|
done: tasks.filter(task => task.status === 'done' || (task.status === 'failed' && taskHasReview(task))).length + hiddenDoneCount,
|
|
839
851
|
},
|
|
840
|
-
current: compactTaskForStatus(columns.do[0] ||
|
|
852
|
+
current: compactTaskForStatus(columns.do[0] || reviewNeedingAgentAction[0] || null),
|
|
841
853
|
next: compactTaskForStatus(columns.plan[0] || null),
|
|
842
854
|
needs_review: columns.review.slice(0, 5).map(compactTaskForStatus),
|
|
843
855
|
streams: (projection.streams || []).slice(0, 8).map(stream => ({
|
|
@@ -1299,25 +1311,6 @@ function cmdNext(args) {
|
|
|
1299
1311
|
const reviewTasks = (reviewProjection.projection.tasks || [])
|
|
1300
1312
|
.map(compactTaskForStatus)
|
|
1301
1313
|
.filter(task => task && task.review && task.review.handoff);
|
|
1302
|
-
const secondReviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again');
|
|
1303
|
-
if (secondReviewTask) {
|
|
1304
|
-
const handoff = secondReviewTask.review.handoff;
|
|
1305
|
-
if (wantsJson(args)) {
|
|
1306
|
-
printJson({
|
|
1307
|
-
ok: true,
|
|
1308
|
-
action: handoff.next_action,
|
|
1309
|
-
task_id: secondReviewTask.id,
|
|
1310
|
-
owner: String(owner),
|
|
1311
|
-
projection_path: reviewProjection.outPath,
|
|
1312
|
-
handoff,
|
|
1313
|
-
review_task: secondReviewTask,
|
|
1314
|
-
});
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
console.log(`${taskRef(secondReviewTask)} needs one more agent review before continuation.`);
|
|
1318
|
-
console.log('Review this task again before claiming new work.');
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
1314
|
const open = taskDb.listTasks(db, {
|
|
1322
1315
|
workspaceRoot: taskDb.workspaceRoot(),
|
|
1323
1316
|
status: 'open',
|
|
@@ -1325,7 +1318,8 @@ function cmdNext(args) {
|
|
|
1325
1318
|
});
|
|
1326
1319
|
if (!open.length) {
|
|
1327
1320
|
const { projection, outPath } = reviewProjection;
|
|
1328
|
-
const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === '
|
|
1321
|
+
const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again')
|
|
1322
|
+
|| reviewTasks.find(task => task.review.handoff.next_action === 'continue_work');
|
|
1329
1323
|
if (reviewTask) {
|
|
1330
1324
|
const handoff = reviewTask.review.handoff;
|
|
1331
1325
|
if (wantsJson(args)) {
|
|
@@ -2013,11 +2007,34 @@ function extractTodoSectionMarkdown(content, sectionName) {
|
|
|
2013
2007
|
return match ? match[1].trimEnd() : null;
|
|
2014
2008
|
}
|
|
2015
2009
|
|
|
2010
|
+
function normalizeRenderedTaskRef(value) {
|
|
2011
|
+
return String(value || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function renderedTaskRefSet(taskDb, rows, refRows) {
|
|
2015
|
+
const byId = new Map();
|
|
2016
|
+
for (const row of [...(Array.isArray(rows) ? rows : []), ...(Array.isArray(refRows) ? refRows : [])]) {
|
|
2017
|
+
if (row && row.id && !byId.has(row.id)) byId.set(row.id, row);
|
|
2018
|
+
}
|
|
2019
|
+
const displayRows = taskDb.withTaskDisplayRefs([...byId.values()]);
|
|
2020
|
+
const refs = new Set();
|
|
2021
|
+
for (const row of displayRows) {
|
|
2022
|
+
for (const value of [row.id, row.display_id, row.legacy_ref]) {
|
|
2023
|
+
const ref = normalizeRenderedTaskRef(value);
|
|
2024
|
+
if (ref) refs.add(ref);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return refs;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2016
2030
|
function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows) {
|
|
2017
2031
|
if (!existingTodoPath || !fs.existsSync(existingTodoPath)) return [];
|
|
2018
2032
|
const { parseTodoFile } = require('../lib/todo-fallback');
|
|
2033
|
+
const existingTodo = fs.readFileSync(existingTodoPath, 'utf8');
|
|
2034
|
+
const generatedTodo = existingTodo.includes('Regenerated from durable Atris task state');
|
|
2019
2035
|
const parsed = parseTodoFile(existingTodoPath);
|
|
2020
2036
|
const ws = taskDb.workspaceRoot();
|
|
2037
|
+
const existingRefs = renderedTaskRefSet(taskDb, rows, refRows);
|
|
2021
2038
|
const existingSourceKeys = new Set(
|
|
2022
2039
|
(Array.isArray(refRows) ? refRows : [])
|
|
2023
2040
|
.map(row => row && row.source_key)
|
|
@@ -2041,7 +2058,13 @@ function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows) {
|
|
|
2041
2058
|
if (!task.title) continue;
|
|
2042
2059
|
const sk = taskDb.sourceKey(existingTodoPath, task.title);
|
|
2043
2060
|
const normalizedTitle = taskDb.normalizeTitle(task.title);
|
|
2044
|
-
|
|
2061
|
+
const renderedRef = normalizeRenderedTaskRef(task.id);
|
|
2062
|
+
if (
|
|
2063
|
+
(renderedRef && existingRefs.has(renderedRef)) ||
|
|
2064
|
+
(sk && existingSourceKeys.has(sk)) ||
|
|
2065
|
+
existingTitles.has(normalizedTitle) ||
|
|
2066
|
+
generatedTodo
|
|
2067
|
+
) continue;
|
|
2045
2068
|
out.push({
|
|
2046
2069
|
id: `markdown:${status}:${task.id || index}:${sk ? sk.slice(0, 10) : index}`,
|
|
2047
2070
|
title: task.title,
|
package/lib/todo-fallback.js
CHANGED
|
@@ -21,7 +21,7 @@ function parseTodoFile(todoPath) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function tagsFromText(text) {
|
|
24
|
-
const allTags = [...String(text || '').matchAll(/\[(\w+)\]/g)].map(m => m[1]);
|
|
24
|
+
const allTags = [...String(text || '').matchAll(/\[([\w-]+)\]/g)].map(m => m[1]);
|
|
25
25
|
return {
|
|
26
26
|
allTags,
|
|
27
27
|
tag: allTags.includes('endgame') ? 'endgame' : (allTags[0] || null),
|
|
@@ -30,7 +30,7 @@ function tagsFromText(text) {
|
|
|
30
30
|
|
|
31
31
|
function cleanTaskTitle(text) {
|
|
32
32
|
const raw = String(text || '').trim();
|
|
33
|
-
const withoutTags = raw.replace(/\s*\[\w+\]/g, '').trim();
|
|
33
|
+
const withoutTags = raw.replace(/\s*\[[\w-]+\]/g, '').trim();
|
|
34
34
|
const bold = withoutTags.match(/^\*\*(.+?)\*\*\s*(?:[—-]\s*)?(.*)$/);
|
|
35
35
|
if (!bold) return withoutTags;
|
|
36
36
|
return [bold[1], bold[2]].filter(Boolean).join(' — ').trim();
|