atris 3.58.6 → 3.58.8
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/GETTING_STARTED.md +2 -2
- package/PERSONA.md +11 -11
- package/README.md +9 -1
- package/atris/GEMINI.md +7 -6
- package/atris/PERSONA.md +4 -4
- package/atris/features/README.md +77 -6
- package/atris/policies/engineering-principles.md +129 -0
- package/atris/policies/genesis.md +112 -0
- package/atris/policies/product-design-principles.md +100 -0
- package/atris/skills/README.md +44 -15
- package/atris/skills/aeo/SKILL.md +10 -10
- package/atris/skills/atris/SKILL.md +1 -1
- package/atris/skills/design/SKILL.md +3 -1
- package/atris/skills/engines/SKILL.md +42 -50
- package/atris/skills/meta/SKILL.md +4 -4
- package/atris/skills/youtube/SKILL.md +11 -11
- package/bin/atris.js +60 -8
- package/commands/aeo.js +9 -12
- package/commands/analytics.js +8 -11
- package/commands/auth.js +8 -2
- package/commands/autopilot.js +4 -3
- package/commands/avail.js +1 -1
- package/commands/brain.js +4 -1
- package/commands/business.js +13 -8
- package/commands/close.js +2 -1
- package/commands/computer.js +15 -10
- package/commands/context-sync.js +3 -3
- package/commands/design.js +362 -0
- package/commands/doc-health.js +377 -0
- package/commands/drive.js +32 -0
- package/commands/experiments.js +13 -4
- package/commands/improve.js +68 -2
- package/commands/integrations.js +2 -5
- package/commands/land.js +144 -4
- package/commands/learn.js +7 -2
- package/commands/member.js +239 -58
- package/commands/mission.js +94 -18
- package/commands/pack.js +2 -2
- package/commands/probe.js +11 -5
- package/commands/pull.js +1 -1
- package/commands/pulse.js +38 -0
- package/commands/push.js +1 -1
- package/commands/rainmaker.js +3 -16
- package/commands/rsi.js +156 -0
- package/commands/spaceship.js +1 -1
- package/commands/stream.js +27 -2
- package/commands/study.js +15 -9
- package/commands/sync.js +1 -1
- package/commands/task.js +54 -3
- package/commands/workflow.js +17 -7
- package/commands/youtube.js +76 -8
- package/lib/apply-gate.js +22 -4
- package/lib/codex-flight.js +28 -5
- package/lib/daily-log.js +88 -0
- package/lib/design-api.js +130 -0
- package/lib/engine-ask.js +1 -1
- package/lib/first-minute.js +1 -6
- package/lib/fleet.js +4 -1
- package/lib/improve-vitals-html.js +3 -2
- package/lib/known-commands.js +3 -3
- package/lib/lesson-contradiction.js +2 -2
- package/lib/map-refs.js +183 -0
- package/lib/member-context.js +42 -0
- package/lib/mission-room.js +4 -4
- package/lib/noninteractive.js +15 -4
- package/lib/policy-lessons.js +1 -1
- package/lib/receipt-block.js +137 -28
- package/lib/rsi-record.js +335 -0
- package/lib/runner-command.js +1 -1
- package/lib/scorecard.js +4 -4
- package/lib/sleep-sync.js +11 -0
- package/lib/state-detection.js +36 -17
- package/lib/task-db.js +74 -53
- package/lib/task-list-keeper.js +192 -0
- package/lib/todo-fallback.js +10 -4
- package/lib/todo.js +22 -10
- package/lib/wish-delegate.js +19 -5
- package/mcp/atris-mcp/index.mjs +227 -0
- package/package.json +4 -2
- package/scripts/det/det.js +2 -2
- package/scripts/det/pr-description.js +2 -2
- package/scripts/det/test.js +2 -2
- package/scripts/det/ytnotes +43 -8
- package/utils/api.js +1 -1
- package/utils/auth.js +79 -14
- package/utils/backend-root.js +31 -0
- package/utils/config.js +5 -4
- package/utils/owner-identity.js +61 -0
- package/atris/policies/LESSONS.md +0 -13
- package/atris/skills/autopilot/atris-autopilot.md +0 -71
package/commands/stream.js
CHANGED
|
@@ -293,6 +293,31 @@ function pushEvent(events, input) {
|
|
|
293
293
|
if (event) events.push({ ...event, key: eventKey(event) });
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// When timestamps match, state snapshots precede receipts, then landing summaries.
|
|
297
|
+
const STREAM_SOURCE_RANK = {
|
|
298
|
+
task: 20,
|
|
299
|
+
task_episode: 30,
|
|
300
|
+
scorecard: 40,
|
|
301
|
+
mission_event: 50,
|
|
302
|
+
mission_state: 60,
|
|
303
|
+
mission_receipt: 70,
|
|
304
|
+
mission_landing: 80,
|
|
305
|
+
worktree: 85,
|
|
306
|
+
landing: 90,
|
|
307
|
+
state: 10,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function compareStreamEvents(a, b) {
|
|
311
|
+
const byMs = a.ms - b.ms;
|
|
312
|
+
if (byMs) return byMs;
|
|
313
|
+
const rankA = STREAM_SOURCE_RANK[a.source] ?? 55;
|
|
314
|
+
const rankB = STREAM_SOURCE_RANK[b.source] ?? 55;
|
|
315
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
316
|
+
const byKey = String(a.key || '').localeCompare(String(b.key || ''));
|
|
317
|
+
if (byKey) return byKey;
|
|
318
|
+
return String(a.event || '').localeCompare(String(b.event || ''));
|
|
319
|
+
}
|
|
320
|
+
|
|
296
321
|
function statePaths(root) {
|
|
297
322
|
const stateDir = path.join(root, '.atris', 'state');
|
|
298
323
|
return {
|
|
@@ -455,7 +480,6 @@ function collectRunReceiptEvents(root, deps, events) {
|
|
|
455
480
|
|
|
456
481
|
function candidateWorktreeDirs(root, deps) {
|
|
457
482
|
const dirs = new Set([
|
|
458
|
-
path.resolve(root, '..', '.agent-worktrees'),
|
|
459
483
|
path.join(path.dirname(root), '.agent-worktrees', path.basename(root)),
|
|
460
484
|
]);
|
|
461
485
|
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
@@ -606,7 +630,7 @@ function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', no
|
|
|
606
630
|
if (wantedAgent && event.agent.toLowerCase() !== wantedAgent) continue;
|
|
607
631
|
byKey.set(event.key, event);
|
|
608
632
|
}
|
|
609
|
-
return [...byKey.values()].sort(
|
|
633
|
+
return [...byKey.values()].sort(compareStreamEvents);
|
|
610
634
|
}
|
|
611
635
|
|
|
612
636
|
function waitingOnOperator(tasks) {
|
|
@@ -841,6 +865,7 @@ function streamCommand(args = [], deps = defaultDeps()) {
|
|
|
841
865
|
module.exports = {
|
|
842
866
|
collectSnapshot,
|
|
843
867
|
collectStreamEvents,
|
|
868
|
+
compareStreamEvents,
|
|
844
869
|
createStreamState,
|
|
845
870
|
parseArgs,
|
|
846
871
|
parseSince,
|
package/commands/study.js
CHANGED
|
@@ -15,6 +15,7 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const http = require('http');
|
|
17
17
|
const { spawn, spawnSync } = require('child_process');
|
|
18
|
+
const { resolveBackendRoot } = require('../utils/backend-root');
|
|
18
19
|
|
|
19
20
|
const FEED_URL = 'http://localhost:8777';
|
|
20
21
|
const FEED_STATS_URL = `${FEED_URL}/api/stats`;
|
|
@@ -91,15 +92,19 @@ function parseArgs(argv = []) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function resolveStudyRoot(cwd = process.cwd()) {
|
|
94
|
-
|
|
95
|
-
process.env.ATRIS_STUDY_ROOT
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
if (fs.existsSync(
|
|
102
|
-
}
|
|
95
|
+
if (process.env.ATRIS_STUDY_ROOT) {
|
|
96
|
+
const explicit = path.resolve(process.env.ATRIS_STUDY_ROOT);
|
|
97
|
+
if (fs.existsSync(explicit) && fs.statSync(explicit).isDirectory()) return explicit;
|
|
98
|
+
}
|
|
99
|
+
// Same order as before: an explicitly set backend root wins over the local folder.
|
|
100
|
+
if (process.env.ATRIS_BACKEND_ROOT) {
|
|
101
|
+
const envBackend = path.resolve(process.env.ATRIS_BACKEND_ROOT);
|
|
102
|
+
if (fs.existsSync(envBackend)) return envBackend;
|
|
103
|
+
}
|
|
104
|
+
const local = path.join(path.resolve(cwd), 'atris', 'study');
|
|
105
|
+
if (fs.existsSync(local) && fs.statSync(local).isDirectory()) return local;
|
|
106
|
+
const backend = resolveBackendRoot(cwd);
|
|
107
|
+
if (backend) return backend;
|
|
103
108
|
return null;
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -697,6 +702,7 @@ async function run(argv = []) {
|
|
|
697
702
|
|
|
698
703
|
module.exports = {
|
|
699
704
|
run,
|
|
705
|
+
resolveStudyRoot,
|
|
700
706
|
parseArgs,
|
|
701
707
|
showHelp,
|
|
702
708
|
wrapText,
|
package/commands/sync.js
CHANGED
|
@@ -615,7 +615,7 @@ Detect: Project has \`atris/\` folder with MAP.md, TODO.md, PERSONA.md
|
|
|
615
615
|
Commands: plan → do → review
|
|
616
616
|
|
|
617
617
|
Key behaviors:
|
|
618
|
-
- Read PERSONA.md (
|
|
618
|
+
- Read PERSONA.md (MAP.md first, plain words, lead with the outcome)
|
|
619
619
|
- Check MAP.md for file:line refs
|
|
620
620
|
- Use \`atris task\` for claims, proof, ready, and accept
|
|
621
621
|
- Give every task a plain first layer: what changes, why it matters, and what
|
package/commands/task.js
CHANGED
|
@@ -253,6 +253,7 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
253
253
|
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
254
254
|
--from-failed opts in to relabel a fail-closed row (never done)
|
|
255
255
|
atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
|
|
256
|
+
atris task keep [--json] Put away finished and untouched work, then refresh the list
|
|
256
257
|
atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
|
|
257
258
|
atris task relabel-archived [--dry-run|--apply]
|
|
258
259
|
One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
|
|
@@ -633,6 +634,12 @@ function goldenPathMissionXpTask(task) {
|
|
|
633
634
|
&& /\b(?:golden[- ]path|zero[- ]knowledge|zero[- ]papercuts?|fresh[- ](?:laptop|environment|install|home)|self[- ]landed)\b/i.test(text);
|
|
634
635
|
}
|
|
635
636
|
|
|
637
|
+
// A receipt that cannot be read drops out of review evidence; name it under
|
|
638
|
+
// ATRIS_DEBUG so the gap is findable, matching lib/todo.js.
|
|
639
|
+
function debugUnreadableReceipt(rel, err) {
|
|
640
|
+
if (process.env.ATRIS_DEBUG) console.error(`[task] receipt unreadable, skipped: ${rel}: ${err && err.message}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
636
643
|
function receiptTextForProof(proof, root = process.cwd()) {
|
|
637
644
|
const chunks = [];
|
|
638
645
|
const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
|
|
@@ -651,7 +658,9 @@ function receiptTextForProof(proof, root = process.cwd()) {
|
|
|
651
658
|
last_landing: parsed.last_landing || null,
|
|
652
659
|
summary: parsed.summary || null,
|
|
653
660
|
}).slice(0, 12000));
|
|
654
|
-
} catch {
|
|
661
|
+
} catch (err) {
|
|
662
|
+
debugUnreadableReceipt(rel, err);
|
|
663
|
+
}
|
|
655
664
|
}
|
|
656
665
|
return chunks.join(' ');
|
|
657
666
|
}
|
|
@@ -671,7 +680,10 @@ function missionReceiptResultForProof(task, proof, root = process.cwd()) {
|
|
|
671
680
|
try {
|
|
672
681
|
const parsed = JSON.parse(fs.readFileSync(path.resolve(root, rel), 'utf8'));
|
|
673
682
|
landing = parsed?.result?.landing || parsed?.landing || parsed?.last_landing || null;
|
|
674
|
-
} catch {
|
|
683
|
+
} catch (err) {
|
|
684
|
+
debugUnreadableReceipt(rel, err);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
675
687
|
const changed = String(landing?.changed || landing?.happened || '').replace(/\s+/g, ' ').trim();
|
|
676
688
|
if (!changed) continue;
|
|
677
689
|
if (/\brecorded tick \d+\.?$/i.test(changed) || /^recorded a proof heartbeat\b/i.test(changed)) continue;
|
|
@@ -6416,7 +6428,7 @@ function compactTechnicalDetails(task, formatTitle = value => value) {
|
|
|
6416
6428
|
}
|
|
6417
6429
|
|
|
6418
6430
|
function taskDayGroups(tasks, { now = Date.now() } = {}) {
|
|
6419
|
-
const active = tasks.filter(task => task.status !== 'done');
|
|
6431
|
+
const active = tasks.filter(task => task.status !== 'done' && task.status !== 'archived');
|
|
6420
6432
|
const staleFailed = [];
|
|
6421
6433
|
const visible = [];
|
|
6422
6434
|
for (const task of active) {
|
|
@@ -6446,7 +6458,43 @@ function taskDayGroups(tasks, { now = Date.now() } = {}) {
|
|
|
6446
6458
|
return { groups: grouped, staleFailed };
|
|
6447
6459
|
}
|
|
6448
6460
|
|
|
6461
|
+
function refreshKeptTaskList(cwd = process.cwd()) {
|
|
6462
|
+
const taskDb = getTaskDb();
|
|
6463
|
+
const db = taskDb.open();
|
|
6464
|
+
writeDefaultProjection(taskDb, db);
|
|
6465
|
+
autoRenderTodoFromDb(cwd);
|
|
6466
|
+
}
|
|
6467
|
+
|
|
6468
|
+
function keptCount(result) {
|
|
6469
|
+
return (result && result.put_away ? result.put_away.length : 0)
|
|
6470
|
+
+ (result && result.reaped ? result.reaped.length : 0);
|
|
6471
|
+
}
|
|
6472
|
+
|
|
6473
|
+
function cmdKeep(args) {
|
|
6474
|
+
const result = require('../lib/task-list-keeper').keepWorkspaceTaskList(process.cwd());
|
|
6475
|
+
if (keptCount(result)) refreshKeptTaskList(process.cwd());
|
|
6476
|
+
const count = keptCount(result);
|
|
6477
|
+
if (wantsJson(args)) {
|
|
6478
|
+
printJson({
|
|
6479
|
+
ok: true,
|
|
6480
|
+
action: 'keep',
|
|
6481
|
+
count,
|
|
6482
|
+
put_away: result.put_away,
|
|
6483
|
+
reaped: result.reaped,
|
|
6484
|
+
});
|
|
6485
|
+
return;
|
|
6486
|
+
}
|
|
6487
|
+
if (!count) {
|
|
6488
|
+
console.log('task list is current. nothing to put away.');
|
|
6489
|
+
return;
|
|
6490
|
+
}
|
|
6491
|
+
const noun = count === 1 ? 'item' : 'items';
|
|
6492
|
+
console.log(`put away ${count} ${noun} that were finished or sitting still.`);
|
|
6493
|
+
}
|
|
6494
|
+
|
|
6449
6495
|
function cmdDay(args) {
|
|
6496
|
+
const kept = require('../lib/task-list-keeper').keepWorkspaceTaskList(process.cwd());
|
|
6497
|
+
if (keptCount(kept)) autoRenderTodoFromDb(process.cwd());
|
|
6450
6498
|
const all = hasFlag(args, '--all');
|
|
6451
6499
|
const full = hasFlag(args, '--full');
|
|
6452
6500
|
const everywhere = taskScopeEverywhere(args);
|
|
@@ -13321,6 +13369,7 @@ async function runTaskCommand(args) {
|
|
|
13321
13369
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
13322
13370
|
case 'archive': return cmdArchive(rest);
|
|
13323
13371
|
case 'clear-done': return cmdClearDone(rest);
|
|
13372
|
+
case 'keep': return cmdKeep(rest);
|
|
13324
13373
|
case 'reap-mission-blockers':
|
|
13325
13374
|
case 'reap-blockers':
|
|
13326
13375
|
return cmdReapMissionBlockers(rest);
|
|
@@ -13425,6 +13474,8 @@ module.exports = {
|
|
|
13425
13474
|
delegateTask,
|
|
13426
13475
|
AGENT_ENV_MARKERS,
|
|
13427
13476
|
autoRenderTodoFromDb,
|
|
13477
|
+
refreshKeptTaskList,
|
|
13478
|
+
keptCount,
|
|
13428
13479
|
projectionMissions,
|
|
13429
13480
|
projectionWishes,
|
|
13430
13481
|
taskBoardViewModel,
|
package/commands/workflow.js
CHANGED
|
@@ -21,6 +21,19 @@ const { loadContext } = require('../lib/state-detection');
|
|
|
21
21
|
const { buildToolResultBody } = require('../lib/tool-result-encode');
|
|
22
22
|
const { commitReviewLearning } = require('./learn');
|
|
23
23
|
|
|
24
|
+
function reviewLearningLine(timestamp, answer) {
|
|
25
|
+
return `- ${timestamp} - ${String(answer).trim().replace(/\s*\u2014\s*/g, ', ')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function appendReviewLearningToJournal(journalContent, learning) {
|
|
29
|
+
const text = String(journalContent || '').replace(/\r\n/g, '\n');
|
|
30
|
+
const note = String(learning || '');
|
|
31
|
+
if (text.includes('## Notes')) {
|
|
32
|
+
return text.replace(/## Notes\n/, `## Notes\n${note}\n`);
|
|
33
|
+
}
|
|
34
|
+
return `${text}\n## Notes\n${note}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
function wrapWorkflowText(text, width = 76) {
|
|
25
38
|
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
26
39
|
if (!normalized) return [''];
|
|
@@ -1773,14 +1786,9 @@ async function reviewAtris() {
|
|
|
1773
1786
|
if (fs.existsSync(logFile)) {
|
|
1774
1787
|
let journalContent = fs.readFileSync(logFile, 'utf8');
|
|
1775
1788
|
const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
1776
|
-
const learning =
|
|
1789
|
+
const learning = reviewLearningLine(timestamp, answer);
|
|
1777
1790
|
|
|
1778
|
-
|
|
1779
|
-
if (journalContent.includes('## Notes')) {
|
|
1780
|
-
journalContent = journalContent.replace(/## Notes\n/, `## Notes\n${learning}\n`);
|
|
1781
|
-
} else {
|
|
1782
|
-
journalContent += `\n## Notes\n${learning}\n`;
|
|
1783
|
-
}
|
|
1791
|
+
journalContent = appendReviewLearningToJournal(journalContent, learning);
|
|
1784
1792
|
|
|
1785
1793
|
fs.writeFileSync(logFile, journalContent);
|
|
1786
1794
|
console.log('');
|
|
@@ -1893,6 +1901,8 @@ module.exports = {
|
|
|
1893
1901
|
planAtris,
|
|
1894
1902
|
doAtris,
|
|
1895
1903
|
reviewAtris,
|
|
1904
|
+
appendReviewLearningToJournal,
|
|
1905
|
+
reviewLearningLine,
|
|
1896
1906
|
renderReviewMinute,
|
|
1897
1907
|
executorAgentPrompt,
|
|
1898
1908
|
executorDispatchForTask,
|
package/commands/youtube.js
CHANGED
|
@@ -283,6 +283,51 @@ function parseVttTimestampMs(value) {
|
|
|
283
283
|
return ((hours * 3600) + (minutes * 60) + seconds) * 1000 + millis;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
function parseCleanTimestampMs(value) {
|
|
287
|
+
const match = String(value || '').trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
|
|
288
|
+
if (!match) return null;
|
|
289
|
+
const hours = Number(match[1] || 0);
|
|
290
|
+
const minutes = Number(match[2] || 0);
|
|
291
|
+
const seconds = Number(match[3] || 0);
|
|
292
|
+
if (![hours, minutes, seconds].every(Number.isFinite)) return null;
|
|
293
|
+
if (minutes > 59 || seconds > 59) return null;
|
|
294
|
+
return ((hours * 3600) + (minutes * 60) + seconds) * 1000;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseCleanTranscriptCues(raw) {
|
|
298
|
+
const cues = [];
|
|
299
|
+
let startMs = 0;
|
|
300
|
+
let sawStamp = false;
|
|
301
|
+
const pending = [];
|
|
302
|
+
const flush = () => {
|
|
303
|
+
const text = pending.join(' ').replace(/\s+/g, ' ').trim();
|
|
304
|
+
pending.length = 0;
|
|
305
|
+
if (!text) return;
|
|
306
|
+
const cue = { startMs, text };
|
|
307
|
+
if (cues.length && cues[cues.length - 1].text === cue.text && cues[cues.length - 1].startMs === cue.startMs) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
cues.push(cue);
|
|
311
|
+
};
|
|
312
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
313
|
+
const stripped = line.trim();
|
|
314
|
+
if (!stripped) continue;
|
|
315
|
+
const stamp = stripped.match(/^\[(\d{1,2}:\d{2}(?::\d{2})?)\]$/);
|
|
316
|
+
if (stamp) {
|
|
317
|
+
flush();
|
|
318
|
+
const parsed = parseCleanTimestampMs(stamp[1]);
|
|
319
|
+
if (parsed != null) {
|
|
320
|
+
startMs = parsed;
|
|
321
|
+
sawStamp = true;
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
pending.push(stripped.replace(/<[^>]+>/g, ''));
|
|
326
|
+
}
|
|
327
|
+
flush();
|
|
328
|
+
return sawStamp && cues.length ? cues : [];
|
|
329
|
+
}
|
|
330
|
+
|
|
286
331
|
function fetchCaptionText(urlString, redirects = 0) {
|
|
287
332
|
if (!captionHostAllowed(urlString)) {
|
|
288
333
|
return Promise.resolve(null);
|
|
@@ -380,7 +425,7 @@ function parseCaptionCues(raw) {
|
|
|
380
425
|
return cues;
|
|
381
426
|
}
|
|
382
427
|
|
|
383
|
-
return
|
|
428
|
+
return parseCleanTranscriptCues(raw);
|
|
384
429
|
}
|
|
385
430
|
|
|
386
431
|
function parseCaptionText(raw) {
|
|
@@ -415,7 +460,14 @@ function parseCaptionText(raw) {
|
|
|
415
460
|
}
|
|
416
461
|
|
|
417
462
|
function parseYtDlpInfoJson(result) {
|
|
418
|
-
const
|
|
463
|
+
const kept = [];
|
|
464
|
+
for (const line of String((result && result.stdout) || '').split(/\r?\n/)) {
|
|
465
|
+
const trimmed = line.trim();
|
|
466
|
+
if (!trimmed) continue;
|
|
467
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
468
|
+
kept.push(trimmed);
|
|
469
|
+
}
|
|
470
|
+
const raw = kept.join('\n').trim();
|
|
419
471
|
if (!raw) return null;
|
|
420
472
|
try {
|
|
421
473
|
const info = JSON.parse(raw);
|
|
@@ -426,7 +478,7 @@ function parseYtDlpInfoJson(result) {
|
|
|
426
478
|
}
|
|
427
479
|
|
|
428
480
|
function localCaptionNames(id) {
|
|
429
|
-
// scripts/det/ytnotes keeps
|
|
481
|
+
// scripts/det/ytnotes keeps these VTT names plus leftover yt_<id>.clean.txt.
|
|
430
482
|
return [
|
|
431
483
|
`yt_${id}.en.vtt`,
|
|
432
484
|
`yt_${id}.en-orig.vtt`,
|
|
@@ -765,6 +817,10 @@ function applySidecarRel(id) {
|
|
|
765
817
|
return applyGate.applySidecarRel('youtube', id);
|
|
766
818
|
}
|
|
767
819
|
|
|
820
|
+
function notesApplyRel(id) {
|
|
821
|
+
return applyGate.applySidecarRel('notes', experimentIdToken(id));
|
|
822
|
+
}
|
|
823
|
+
|
|
768
824
|
function notesExperimentSlug(id) {
|
|
769
825
|
return `notes-${experimentIdToken(id)}`;
|
|
770
826
|
}
|
|
@@ -814,7 +870,7 @@ function saveRichNotes(url, deps = {}) {
|
|
|
814
870
|
url,
|
|
815
871
|
lesson,
|
|
816
872
|
slug: id ? notesExperimentSlug(id) : null,
|
|
817
|
-
applyRel: id ?
|
|
873
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
818
874
|
});
|
|
819
875
|
return { thin: false, brief, packRel, lesson };
|
|
820
876
|
}
|
|
@@ -826,7 +882,7 @@ function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
|
|
|
826
882
|
return applyGate.ensureApply({
|
|
827
883
|
cwd,
|
|
828
884
|
source: url,
|
|
829
|
-
rel: id ?
|
|
885
|
+
rel: id ? notesApplyRel(id) : null,
|
|
830
886
|
now,
|
|
831
887
|
output,
|
|
832
888
|
incompleteMessage: slug
|
|
@@ -907,6 +963,7 @@ function unsaveYoutubeNotes(target, deps = {}) {
|
|
|
907
963
|
};
|
|
908
964
|
add(briefRel);
|
|
909
965
|
add(applyRel);
|
|
966
|
+
add(notesApplyRel(id));
|
|
910
967
|
for (const section of sections) {
|
|
911
968
|
add(teachBriefRel(id, section));
|
|
912
969
|
add(applySidecarRel(`${id}-s${section}`));
|
|
@@ -959,6 +1016,7 @@ function ensureProcessApply({ cwd, url, now, output } = {}) {
|
|
|
959
1016
|
output,
|
|
960
1017
|
incompleteMessage: PROCESS_APPLY_MESSAGE,
|
|
961
1018
|
required: true,
|
|
1019
|
+
human: true,
|
|
962
1020
|
});
|
|
963
1021
|
}
|
|
964
1022
|
|
|
@@ -1455,15 +1513,22 @@ function channelVideosUrl(channel) {
|
|
|
1455
1513
|
return `${base}/videos`;
|
|
1456
1514
|
}
|
|
1457
1515
|
|
|
1516
|
+
function looksLikeFlatVideoId(id) {
|
|
1517
|
+
const text = String(id || '');
|
|
1518
|
+
if (/^(NA|None)$/i.test(text)) return false;
|
|
1519
|
+
return /^[A-Za-z0-9_-]+$/.test(text);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1458
1522
|
function parseFlatPlaylist(stdout) {
|
|
1459
1523
|
const videos = [];
|
|
1460
1524
|
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
1461
1525
|
const trimmed = line.trim();
|
|
1462
1526
|
if (!trimmed || !trimmed.includes('|')) continue;
|
|
1527
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
1463
1528
|
const idx = trimmed.indexOf('|');
|
|
1464
1529
|
const id = trimmed.slice(0, idx).trim();
|
|
1465
1530
|
const title = trimmed.slice(idx + 1).trim();
|
|
1466
|
-
if (id
|
|
1531
|
+
if (looksLikeFlatVideoId(id)) videos.push({ id, title });
|
|
1467
1532
|
}
|
|
1468
1533
|
return videos;
|
|
1469
1534
|
}
|
|
@@ -1744,6 +1809,7 @@ function defaultPlaylistExpander(playlistUrl, deps = {}) {
|
|
|
1744
1809
|
const result = spawn('yt-dlp', [
|
|
1745
1810
|
'--no-update',
|
|
1746
1811
|
'--flat-playlist',
|
|
1812
|
+
'--no-warnings',
|
|
1747
1813
|
'--print',
|
|
1748
1814
|
'%(id)s|%(title)s',
|
|
1749
1815
|
playlistUrl,
|
|
@@ -1938,7 +2004,7 @@ function runYoutubeNotesBatch({ urls, engine, save, json } = {}, deps = {}) {
|
|
|
1938
2004
|
}));
|
|
1939
2005
|
const baseline = proveSavedLearnerBaseline({
|
|
1940
2006
|
cwd: deps.cwd || process.cwd(),
|
|
1941
|
-
applyRel: id ?
|
|
2007
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
1942
2008
|
lesson,
|
|
1943
2009
|
output,
|
|
1944
2010
|
json: asJson,
|
|
@@ -1989,7 +2055,7 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1989
2055
|
const id = videoIdFromUrl(url);
|
|
1990
2056
|
const baseline = proveSavedLearnerBaseline({
|
|
1991
2057
|
cwd,
|
|
1992
|
-
applyRel: id ?
|
|
2058
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
1993
2059
|
lesson: saved.lesson,
|
|
1994
2060
|
output,
|
|
1995
2061
|
json: deps.json === true,
|
|
@@ -2129,10 +2195,12 @@ function parseSearchStdout(stdout = '') {
|
|
|
2129
2195
|
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
2130
2196
|
const trimmed = line.trim();
|
|
2131
2197
|
if (!trimmed || !trimmed.includes('|')) continue;
|
|
2198
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
2132
2199
|
const parts = trimmed.split(/\s*\|\s*/).map((part) => part.trim());
|
|
2133
2200
|
if (parts.length < 5) continue;
|
|
2134
2201
|
const url = parts[parts.length - 1];
|
|
2135
2202
|
if (!/^https?:\/\/(?:www\.)?(?:youtube\.com\/|youtu\.be\/)/i.test(url)) continue;
|
|
2203
|
+
if (!looksLikeFlatVideoId(videoIdFromUrl(url))) continue;
|
|
2136
2204
|
const row = {
|
|
2137
2205
|
title: parts[0] || '',
|
|
2138
2206
|
channel: parts[1] || '',
|
package/lib/apply-gate.js
CHANGED
|
@@ -26,6 +26,17 @@ function isFilledApply(fields) {
|
|
|
26
26
|
return !empty(fields.change) && !empty(fields.receipt);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function isLearnerKeepApply(fields) {
|
|
30
|
+
const change = String((fields && fields.change) || '');
|
|
31
|
+
const receipt = String((fields && fields.receipt) || '');
|
|
32
|
+
return /^apply\s+atris\/experiments\//i.test(change)
|
|
33
|
+
|| /keep only if measure\.py moves 0/.test(receipt);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isFilledHumanApply(fields) {
|
|
37
|
+
return isFilledApply(fields) && !isLearnerKeepApply(fields);
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
function applySlug(text) {
|
|
30
41
|
const slug = String(text || '')
|
|
31
42
|
.toLowerCase()
|
|
@@ -46,7 +57,7 @@ function readApplyReceipt({ cwd, rel } = {}) {
|
|
|
46
57
|
return { rel, text: fs.readFileSync(abs, 'utf8') };
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine } = {}) {
|
|
60
|
+
function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine, force } = {}) {
|
|
50
61
|
try {
|
|
51
62
|
if (!rel || !cwd) return null;
|
|
52
63
|
const wikiDir = path.join(cwd, 'atris', 'wiki');
|
|
@@ -56,7 +67,7 @@ function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine }
|
|
|
56
67
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
57
68
|
const changeText = change || 'fill this';
|
|
58
69
|
const receiptText = receipt || 'fill this';
|
|
59
|
-
let shouldWrite = !fs.existsSync(abs);
|
|
70
|
+
let shouldWrite = force === true || !fs.existsSync(abs);
|
|
60
71
|
if (!shouldWrite && change && receipt) {
|
|
61
72
|
shouldWrite = !isFilledApply(parseApplyFields(fs.readFileSync(abs, 'utf8')));
|
|
62
73
|
}
|
|
@@ -86,11 +97,16 @@ function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine }
|
|
|
86
97
|
|
|
87
98
|
function ensureApply({
|
|
88
99
|
cwd, source, rel, now, output, incompleteMessage, required = true, change, receipt, journalLine,
|
|
100
|
+
human = false,
|
|
89
101
|
} = {}) {
|
|
90
102
|
const print = typeof output === 'function' ? output : (line = '') => console.error(line);
|
|
91
103
|
const existing = readApplyReceipt({ cwd, rel });
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
const fields = existing ? parseApplyFields(existing.text) : null;
|
|
105
|
+
const keepSidecar = Boolean(human && fields && isLearnerKeepApply(fields));
|
|
106
|
+
if (existing && isFilledApply(fields) && !keepSidecar) return 0;
|
|
107
|
+
if (!(required && existing) || keepSidecar) {
|
|
108
|
+
writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine, force: keepSidecar });
|
|
109
|
+
}
|
|
94
110
|
print(incompleteMessage);
|
|
95
111
|
return required ? 2 : 0;
|
|
96
112
|
}
|
|
@@ -112,4 +128,6 @@ module.exports = {
|
|
|
112
128
|
ensureApply,
|
|
113
129
|
ephemeralApplyMessage,
|
|
114
130
|
hintEphemeralApply,
|
|
131
|
+
isFilledHumanApply,
|
|
132
|
+
isLearnerKeepApply,
|
|
115
133
|
};
|
package/lib/codex-flight.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const { spawn, spawnSync } = require('child_process');
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// Worktree flights need full file access to edit and ship the requested repo.
|
|
9
|
+
const FLIGHT_SANDBOX = 'danger-full-access';
|
|
8
10
|
const CONTRACT = "CRITICAL: complete the entire job in this single run; never end your reply on a plan. Do NOT run any atris boot/brain/state commands; the only atris commands allowed are 'atris worktree guard' and 'atris worktree ship'.";
|
|
9
11
|
const COAUTHOR = 'Co-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>';
|
|
10
12
|
|
|
@@ -96,12 +98,30 @@ function parseTaskId(stdout) {
|
|
|
96
98
|
return asMatch ? asMatch[1] : '';
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
function
|
|
101
|
+
function resolveCodexCompanion() {
|
|
102
|
+
if (process.env.ATRIS_CODEX_COMPANION) {
|
|
103
|
+
const override = path.resolve(process.env.ATRIS_CODEX_COMPANION);
|
|
104
|
+
return fs.existsSync(override) && fs.statSync(override).isFile() ? override : null;
|
|
105
|
+
}
|
|
106
|
+
const cache = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'openai-codex', 'codex');
|
|
107
|
+
if (!fs.existsSync(cache)) return null;
|
|
108
|
+
const versions = fs.readdirSync(cache, { withFileTypes: true })
|
|
109
|
+
.filter((entry) => entry.isDirectory())
|
|
110
|
+
.map((entry) => entry.name)
|
|
111
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
112
|
+
for (const version of versions) {
|
|
113
|
+
const script = path.join(cache, version, 'scripts', 'codex-companion.mjs');
|
|
114
|
+
if (fs.existsSync(script) && fs.statSync(script).isFile()) return script;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function spawnCodex(worktreePath, prompt, companion) {
|
|
100
120
|
return new Promise((resolve, reject) => {
|
|
101
|
-
const argv = [
|
|
121
|
+
const argv = [companion, 'task', '--background', '--write', prompt];
|
|
102
122
|
const child = spawn('node', argv, {
|
|
103
123
|
cwd: worktreePath,
|
|
104
|
-
env: { ...process.env, CODEX_COMPANION_SANDBOX:
|
|
124
|
+
env: { ...process.env, CODEX_COMPANION_SANDBOX: FLIGHT_SANDBOX },
|
|
105
125
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
126
|
});
|
|
107
127
|
let stdout = '';
|
|
@@ -143,12 +163,15 @@ async function dispatchCodexFlight({ repoPath, slug, brief, verifyCmd, dryRun =
|
|
|
143
163
|
return { dryRun: true, prompt };
|
|
144
164
|
}
|
|
145
165
|
|
|
166
|
+
const companion = resolveCodexCompanion();
|
|
167
|
+
if (!companion) throw new Error('codex companion not found. set ATRIS_CODEX_COMPANION to its script path.');
|
|
146
168
|
runWorktreeAdd(repo.repoPath, branch, worktreePath);
|
|
147
|
-
const task = await spawnCodex(worktreePath, prompt);
|
|
169
|
+
const task = await spawnCodex(worktreePath, prompt, companion);
|
|
148
170
|
return { taskId: task.taskId, worktreePath, branch };
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
module.exports = {
|
|
152
174
|
dispatchCodexFlight,
|
|
153
175
|
buildPrompt,
|
|
176
|
+
resolveCodexCompanion,
|
|
154
177
|
};
|
package/lib/daily-log.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Two-level daily logging. Member logs (atris/team/<member>/logs/<date>.md)
|
|
4
|
+
// keep the detailed workstream: claims, notes, result receipts, completions.
|
|
5
|
+
// The master journal (atris/logs/<YYYY>/<date>.md) stays curated and receives
|
|
6
|
+
// only consequential entries: terminal task events and explicitly major
|
|
7
|
+
// updates such as decisions, handoffs, and shipped work.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function todayLogName(now = new Date()) {
|
|
13
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function logStamp(now = new Date()) {
|
|
17
|
+
return now.toTimeString().slice(0, 5);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compactLogText(value, max = 240) {
|
|
21
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
22
|
+
if (!text) return '';
|
|
23
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function logFieldRows(fields) {
|
|
27
|
+
return Object.entries(fields)
|
|
28
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
29
|
+
.map(([key, value]) => `- ${key}: ${compactLogText(value, 500)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function appendDailyEntry(logPath, title, fields) {
|
|
33
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
34
|
+
fs.appendFileSync(logPath, [
|
|
35
|
+
`## ${logStamp()} · ${title}`,
|
|
36
|
+
...logFieldRows(fields),
|
|
37
|
+
'',
|
|
38
|
+
].join('\n'), 'utf8');
|
|
39
|
+
return logPath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function memberSlug(root, member) {
|
|
43
|
+
const slug = String(member || '').trim();
|
|
44
|
+
if (!/^[A-Za-z0-9._-]+$/.test(slug)) return null;
|
|
45
|
+
if (!fs.existsSync(path.join(root, 'atris', 'team', slug, 'MEMBER.md'))) return null;
|
|
46
|
+
return slug;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendMemberDailyEntry(root, member, title, fields) {
|
|
50
|
+
const slug = memberSlug(root, member);
|
|
51
|
+
if (!slug || !fs.existsSync(path.join(root, 'atris'))) return null;
|
|
52
|
+
const logPath = path.join(root, 'atris', 'team', slug, 'logs', todayLogName());
|
|
53
|
+
appendDailyEntry(logPath, title, { ...fields, member: slug });
|
|
54
|
+
return logPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function appendMasterDailyEntry(root, title, fields) {
|
|
58
|
+
if (!fs.existsSync(path.join(root, 'atris'))) return null;
|
|
59
|
+
const logName = todayLogName();
|
|
60
|
+
const logPath = path.join(root, 'atris', 'logs', logName.slice(0, 4), logName);
|
|
61
|
+
appendDailyEntry(logPath, title, fields);
|
|
62
|
+
return logPath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Notes a person deliberately escalates to the day record. Anything else stays
|
|
66
|
+
// in the member's own log so the master journal does not become a transcript.
|
|
67
|
+
const MASTER_NOTE = /^(decision|decided|milestone|handoff|shipped|launched|landed|blocked)\b\s*[:\-]/i;
|
|
68
|
+
|
|
69
|
+
function noteTitle(content) {
|
|
70
|
+
const key = String(content || '').trim().match(/^([a-z]+)\s*[:\-]/i);
|
|
71
|
+
const word = key ? key[1].toLowerCase() : '';
|
|
72
|
+
if (word === 'decision' || word === 'decided') return 'Task decision';
|
|
73
|
+
if (word === 'handoff') return 'Task handoff';
|
|
74
|
+
if (word === 'shipped' || word === 'launched' || word === 'landed') return 'Task shipped';
|
|
75
|
+
if (word === 'blocked') return 'Task blocked';
|
|
76
|
+
if (word === 'milestone') return 'Task milestone';
|
|
77
|
+
return 'Task update';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
todayLogName,
|
|
82
|
+
compactLogText,
|
|
83
|
+
memberSlug,
|
|
84
|
+
appendMemberDailyEntry,
|
|
85
|
+
appendMasterDailyEntry,
|
|
86
|
+
MASTER_NOTE,
|
|
87
|
+
noteTitle,
|
|
88
|
+
};
|