atris 3.35.0 → 3.37.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 +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { historicalLandingText } = require('./autoland');
|
|
4
|
+
|
|
5
|
+
function compactLine(value) {
|
|
6
|
+
return String(value == null ? '' : value)
|
|
7
|
+
.replace(/\s+/g, ' ')
|
|
8
|
+
.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function asciiLine(value) {
|
|
12
|
+
return compactLine(value)
|
|
13
|
+
.replace(/[\u2013\u2014]/g, '-')
|
|
14
|
+
.replace(/[^\S\r\n]+/g, ' ');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function lowerLine(value) {
|
|
18
|
+
return asciiLine(value).toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function trimPunctuation(value) {
|
|
22
|
+
return asciiLine(value).replace(/[.!?:;,]+$/g, '').trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function truncateLine(value, max = 180) {
|
|
26
|
+
const text = asciiLine(value);
|
|
27
|
+
if (text.length <= max) return text;
|
|
28
|
+
return `${text.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function landing(receipt) {
|
|
32
|
+
const value = receipt && receipt.result && receipt.result.landing;
|
|
33
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function result(receipt) {
|
|
37
|
+
const value = receipt && receipt.result;
|
|
38
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function verifierResult(receipt) {
|
|
42
|
+
const res = result(receipt);
|
|
43
|
+
const value = res.verifier_result || res.tick?.verifier_result || null;
|
|
44
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function changedText(receipt) {
|
|
48
|
+
const land = landing(receipt);
|
|
49
|
+
const res = result(receipt);
|
|
50
|
+
const text = land.changed
|
|
51
|
+
|| land.happened
|
|
52
|
+
|| res.tick?.summary
|
|
53
|
+
|| res.tick?.claude?.summary
|
|
54
|
+
|| res.tick?.atris2?.receipt_text
|
|
55
|
+
|| res.summary
|
|
56
|
+
|| receipt?.objective
|
|
57
|
+
|| 'mission receipt recorded';
|
|
58
|
+
return lowerLine(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function statusText(receipt) {
|
|
62
|
+
const land = landing(receipt);
|
|
63
|
+
const res = result(receipt);
|
|
64
|
+
const status = land.status || res.status || res.tick?.status || (res.passed === true ? 'proof_ready' : '');
|
|
65
|
+
return lowerLine(status || 'recorded');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scaleText(receipt) {
|
|
69
|
+
const res = result(receipt);
|
|
70
|
+
const worktree = res.worktree || res.tick?.worktree || {};
|
|
71
|
+
const changedCount = Number(
|
|
72
|
+
worktree.new_since_baseline_count
|
|
73
|
+
|| worktree.new_dirty_count
|
|
74
|
+
|| worktree.dirty_count
|
|
75
|
+
|| 0,
|
|
76
|
+
);
|
|
77
|
+
if (Number.isFinite(changedCount) && changedCount > 0) {
|
|
78
|
+
return changedCount === 1 ? '1 changed file' : `${changedCount} changed files`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tickCount = Number(res.tick_count || 0);
|
|
82
|
+
if (Number.isFinite(tickCount) && tickCount > 0) {
|
|
83
|
+
return tickCount === 1 ? '1 tick' : `${tickCount} ticks`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ranTicks = Number(res.ran_ticks || 0);
|
|
87
|
+
if (Number.isFinite(ranTicks) && ranTicks > 0) {
|
|
88
|
+
return ranTicks === 1 ? '1 ran tick' : `${ranTicks} ran ticks`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (res.tick || res.kind === 'mission_tick' || res.kind === 'mission_run_tick') return '1 tick';
|
|
92
|
+
return '1 receipt';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function directProofText(value) {
|
|
96
|
+
const text = lowerLine(value);
|
|
97
|
+
const ran = trimPunctuation(text).match(/^i ran (.+)$/);
|
|
98
|
+
return ran ? `${ran[1]} passed` : text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function proofText(receipt) {
|
|
102
|
+
const land = landing(receipt);
|
|
103
|
+
const verifier = verifierResult(receipt);
|
|
104
|
+
if (land.checked) return directProofText(land.checked);
|
|
105
|
+
if (land.tested) return directProofText(land.tested);
|
|
106
|
+
if (verifier) {
|
|
107
|
+
const command = lowerLine(verifier.command || receipt?.verifier || 'configured verifier');
|
|
108
|
+
return `${command} ${verifier.passed ? 'passed' : 'failed'}`;
|
|
109
|
+
}
|
|
110
|
+
if (result(receipt).passed === true) return 'receipt says verifier passed';
|
|
111
|
+
if (receipt?.at) return `receipt saved at ${lowerLine(receipt.at)}`;
|
|
112
|
+
return 'receipt is present';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function nextText(receipt) {
|
|
116
|
+
const text = landing(receipt).next || result(receipt).next || '';
|
|
117
|
+
return lowerLine(text);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderEmailLine(receipt) {
|
|
121
|
+
const what = historicalLandingText(trimPunctuation(changedText(receipt)), 180);
|
|
122
|
+
const scale = scaleText(receipt);
|
|
123
|
+
const proof = historicalLandingText(trimPunctuation(proofText(receipt)), 90);
|
|
124
|
+
return `${what}; ${scale}; we know because ${proof}.`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderMorningCardRow(receipt) {
|
|
128
|
+
return `- mission: ${renderEmailLine(receipt).replace(/\.$/, '')}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderPageSection(receipt) {
|
|
132
|
+
const next = nextText(receipt);
|
|
133
|
+
const lines = [
|
|
134
|
+
'## mission receipt',
|
|
135
|
+
'',
|
|
136
|
+
`- what: ${truncateLine(trimPunctuation(changedText(receipt)), 160)}`,
|
|
137
|
+
`- how big: ${scaleText(receipt)}`,
|
|
138
|
+
`- how we know: ${truncateLine(trimPunctuation(proofText(receipt)), 160)}`,
|
|
139
|
+
`- status: ${statusText(receipt)}`,
|
|
140
|
+
];
|
|
141
|
+
if (receipt?.mission_id) lines.push(`- mission: ${lowerLine(receipt.mission_id)}`);
|
|
142
|
+
if (next) lines.push(`- next: ${truncateLine(trimPunctuation(next), 160)}`);
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderCard(receipt) {
|
|
147
|
+
const status = statusText(receipt);
|
|
148
|
+
const what = truncateLine(trimPunctuation(changedText(receipt)), 64);
|
|
149
|
+
const scale = scaleText(receipt);
|
|
150
|
+
const proof = truncateLine(trimPunctuation(proofText(receipt)), 88);
|
|
151
|
+
return {
|
|
152
|
+
kind: 'statement',
|
|
153
|
+
headline: what,
|
|
154
|
+
text: what,
|
|
155
|
+
kicker: `${status} mission receipt`,
|
|
156
|
+
sub: `${scale}; ${proof}`,
|
|
157
|
+
brand: 'atris',
|
|
158
|
+
size: 'og',
|
|
159
|
+
theme: 'atris',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
renderCard,
|
|
165
|
+
renderPageSection,
|
|
166
|
+
renderEmailLine,
|
|
167
|
+
renderMorningCardRow,
|
|
168
|
+
};
|
package/lib/receipt-evidence.js
CHANGED
|
@@ -16,11 +16,65 @@ const MAX_RECEIPTS = 5;
|
|
|
16
16
|
function receiptVerifierPassed(receipt) {
|
|
17
17
|
const result = receipt?.result;
|
|
18
18
|
if (!result || typeof result !== 'object') return null;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const oneLapReview = receipt?.schema === 'atris.dispatch_receipt.v1'
|
|
20
|
+
&& receipt?.review_only === true
|
|
21
|
+
&& receipt?.context?.source === 'one_lap';
|
|
22
|
+
if (oneLapReview) {
|
|
23
|
+
const validator = result.validator_result;
|
|
24
|
+
const validatorEngine = String(validator?.engine || '').trim();
|
|
25
|
+
const executorEngine = String(validator?.executor_engine || '').trim();
|
|
26
|
+
return result.passed === true
|
|
27
|
+
&& result.master_boundary_enforced === true
|
|
28
|
+
&& result.master_unchanged === true
|
|
29
|
+
&& validator?.passed === true
|
|
30
|
+
&& validator?.independent === true
|
|
31
|
+
&& validator?.worktree_unchanged === true
|
|
32
|
+
&& Boolean(validatorEngine)
|
|
33
|
+
&& Boolean(executorEngine)
|
|
34
|
+
&& validatorEngine !== executorEngine;
|
|
35
|
+
}
|
|
36
|
+
// The receipt's aggregate result is authoritative. A nested verifier can
|
|
37
|
+
// explain one green check inside a failed flight, but it cannot turn that
|
|
38
|
+
// failed flight into passing evidence.
|
|
39
|
+
if (typeof result.passed === 'boolean') return result.passed;
|
|
40
|
+
const nested = [result.verifier_result?.passed, result.tick?.verifier_passed];
|
|
41
|
+
if (nested.includes(false)) return false;
|
|
42
|
+
if (nested.includes(true)) return true;
|
|
21
43
|
return null;
|
|
22
44
|
}
|
|
23
45
|
|
|
46
|
+
// A forced completion means the mission's own gate said no and an operator
|
|
47
|
+
// overrode it. The receipt can still show a passing verifier, so the forced
|
|
48
|
+
// flag lives in mission state (`completion_gate.forced`); read it back here
|
|
49
|
+
// so the review queue shows forced work as forced, not as passing evidence.
|
|
50
|
+
function loadForcedMissionIds(root) {
|
|
51
|
+
const forced = new Map();
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = fs.readFileSync(path.resolve(root, '.atris/state/missions.jsonl'), 'utf8');
|
|
55
|
+
} catch {
|
|
56
|
+
return forced;
|
|
57
|
+
}
|
|
58
|
+
for (const line of raw.split('\n')) {
|
|
59
|
+
if (!line.trim()) continue;
|
|
60
|
+
let parsed = null;
|
|
61
|
+
try { parsed = JSON.parse(line); } catch { continue; }
|
|
62
|
+
const mission = parsed?.mission && typeof parsed.mission === 'object' ? parsed.mission : parsed;
|
|
63
|
+
const id = mission?.id || parsed?.mission_id;
|
|
64
|
+
if (!id) continue;
|
|
65
|
+
// Last record for a mission wins: an honest re-completion clears the flag.
|
|
66
|
+
forced.set(id, mission?.completion_gate?.forced === true);
|
|
67
|
+
}
|
|
68
|
+
return forced;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function receiptForced(receipt, forcedMissionIds) {
|
|
72
|
+
if (receipt?.completion_gate?.forced === true) return true;
|
|
73
|
+
if (receipt?.mission?.completion_gate?.forced === true) return true;
|
|
74
|
+
const missionId = receipt?.mission_id || receipt?.mission?.id;
|
|
75
|
+
return Boolean(missionId && forcedMissionIds.get(missionId) === true);
|
|
76
|
+
}
|
|
77
|
+
|
|
24
78
|
function extractReceiptEvidence(proofText, root = process.cwd()) {
|
|
25
79
|
const text = String(proofText || '');
|
|
26
80
|
const found = [];
|
|
@@ -37,6 +91,7 @@ function extractReceiptEvidence(proofText, root = process.cwd()) {
|
|
|
37
91
|
|
|
38
92
|
const receipts = [];
|
|
39
93
|
const missing = [];
|
|
94
|
+
let forcedMissionIds = null;
|
|
40
95
|
for (const rel of found) {
|
|
41
96
|
let raw;
|
|
42
97
|
try {
|
|
@@ -51,13 +106,19 @@ function extractReceiptEvidence(proofText, root = process.cwd()) {
|
|
|
51
106
|
const passed = receiptVerifierPassed(parsed);
|
|
52
107
|
if (passed !== null) entry.verifier_passed = passed;
|
|
53
108
|
if (parsed?.mission_id) entry.mission_id = parsed.mission_id;
|
|
109
|
+
if (forcedMissionIds === null) forcedMissionIds = loadForcedMissionIds(root);
|
|
110
|
+
if (receiptForced(parsed, forcedMissionIds)) entry.forced = true;
|
|
54
111
|
receipts.push(entry);
|
|
55
112
|
}
|
|
113
|
+
const anyForced = receipts.some((entry) => entry.forced === true);
|
|
56
114
|
return {
|
|
57
115
|
receipts,
|
|
58
116
|
missing,
|
|
59
|
-
|
|
60
|
-
|
|
117
|
+
any_forced: anyForced,
|
|
118
|
+
// Unknown verifier state is not proof, and a forced completion is not
|
|
119
|
+
// passing evidence: every named receipt must explicitly pass, unforced.
|
|
120
|
+
all_passing: receipts.length > 0 && !missing.length && !anyForced
|
|
121
|
+
&& receipts.every((entry) => entry.verifier_passed === true),
|
|
61
122
|
};
|
|
62
123
|
}
|
|
63
124
|
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MIN_RECEIPTS = 3;
|
|
7
|
+
const DEFAULT_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
8
|
+
const DURATION_SCALE_MS = 60 * 1000;
|
|
9
|
+
|
|
10
|
+
function objectValue(value) {
|
|
11
|
+
return value && typeof value === 'object' ? value : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function recordContainers(record) {
|
|
15
|
+
const row = objectValue(record);
|
|
16
|
+
if (!row) return [];
|
|
17
|
+
return [
|
|
18
|
+
row,
|
|
19
|
+
objectValue(row.payload),
|
|
20
|
+
objectValue(row.result),
|
|
21
|
+
objectValue(row.tick),
|
|
22
|
+
objectValue(row.result?.tick),
|
|
23
|
+
objectValue(row.payload?.tick),
|
|
24
|
+
objectValue(row.verifier_result),
|
|
25
|
+
objectValue(row.result?.verifier_result),
|
|
26
|
+
objectValue(row.payload?.verifier_result),
|
|
27
|
+
objectValue(row.mission),
|
|
28
|
+
objectValue(row.context),
|
|
29
|
+
].filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function firstField(containers, names, accept = (value) => value != null) {
|
|
33
|
+
for (const container of containers) {
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
const value = container[name];
|
|
36
|
+
if (accept(value)) return value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizedLabel(value) {
|
|
43
|
+
return String(value || '').trim().toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function finiteNonnegative(value) {
|
|
47
|
+
const number = Number(value);
|
|
48
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function recordDuration(containers) {
|
|
52
|
+
const direct = firstField(
|
|
53
|
+
containers,
|
|
54
|
+
['duration_ms', 'durationMs', 'elapsed_ms', 'elapsedMs'],
|
|
55
|
+
(value) => finiteNonnegative(value) !== null,
|
|
56
|
+
);
|
|
57
|
+
if (direct !== null) return finiteNonnegative(direct);
|
|
58
|
+
for (const container of containers) {
|
|
59
|
+
const started = Date.parse(String(container.started_at || container.startedAt || ''));
|
|
60
|
+
const finished = Date.parse(String(container.finished_at || container.finishedAt || ''));
|
|
61
|
+
if (Number.isFinite(started) && Number.isFinite(finished) && finished >= started) {
|
|
62
|
+
return finished - started;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function recordTimestamp(containers, fallbackMs) {
|
|
69
|
+
const raw = firstField(containers, [
|
|
70
|
+
'at',
|
|
71
|
+
'ts',
|
|
72
|
+
'finished_at',
|
|
73
|
+
'finishedAt',
|
|
74
|
+
'created_at',
|
|
75
|
+
'createdAt',
|
|
76
|
+
'updated_at',
|
|
77
|
+
'updatedAt',
|
|
78
|
+
'started_at',
|
|
79
|
+
'startedAt',
|
|
80
|
+
]);
|
|
81
|
+
const parsed = Date.parse(String(raw || ''));
|
|
82
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
83
|
+
return Number.isFinite(fallbackMs) ? fallbackMs : Date.now();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function observationFromRecord(record, source, fallbackMs) {
|
|
87
|
+
const containers = recordContainers(record);
|
|
88
|
+
const engine = normalizedLabel(firstField(containers, [
|
|
89
|
+
'engine_id',
|
|
90
|
+
'engine',
|
|
91
|
+
'selected_engine',
|
|
92
|
+
'executor_engine',
|
|
93
|
+
'runner',
|
|
94
|
+
]));
|
|
95
|
+
const taskType = normalizedLabel(firstField(containers, [
|
|
96
|
+
'task_type',
|
|
97
|
+
'taskType',
|
|
98
|
+
'job_type',
|
|
99
|
+
'jobType',
|
|
100
|
+
'role',
|
|
101
|
+
'kind',
|
|
102
|
+
]));
|
|
103
|
+
const passed = firstField(
|
|
104
|
+
containers,
|
|
105
|
+
['verified_passed', 'verifier_passed', 'passed', 'verified'],
|
|
106
|
+
(value) => typeof value === 'boolean',
|
|
107
|
+
);
|
|
108
|
+
if (!engine || !taskType || typeof passed !== 'boolean') return null;
|
|
109
|
+
return {
|
|
110
|
+
engine,
|
|
111
|
+
task_type: taskType,
|
|
112
|
+
verified_passed: passed,
|
|
113
|
+
duration_ms: recordDuration(containers),
|
|
114
|
+
at_ms: recordTimestamp(containers, fallbackMs),
|
|
115
|
+
source,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readTaskReceiptObservations(root) {
|
|
120
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = fs.readdirSync(runsDir, { withFileTypes: true });
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const observations = [];
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (!entry.isFile() || !entry.name.endsWith('.json') || !entry.name.includes('task-')) continue;
|
|
130
|
+
const file = path.join(runsDir, entry.name);
|
|
131
|
+
try {
|
|
132
|
+
const stat = fs.statSync(file);
|
|
133
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
134
|
+
const observation = observationFromRecord(parsed, path.relative(root, file), stat.mtimeMs);
|
|
135
|
+
if (observation) observations.push(observation);
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
return observations;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readMissionEventObservations(root) {
|
|
142
|
+
const file = path.join(root, '.atris', 'state', 'mission_events.jsonl');
|
|
143
|
+
let raw;
|
|
144
|
+
let fallbackMs = Date.now();
|
|
145
|
+
try {
|
|
146
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
147
|
+
fallbackMs = fs.statSync(file).mtimeMs;
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const observations = [];
|
|
152
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
153
|
+
if (!line.trim()) continue;
|
|
154
|
+
try {
|
|
155
|
+
const observation = observationFromRecord(JSON.parse(line), path.relative(root, file), fallbackMs);
|
|
156
|
+
if (observation) observations.push(observation);
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
return observations;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readDispatchReceiptObservations(root) {
|
|
163
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
164
|
+
let entries;
|
|
165
|
+
try {
|
|
166
|
+
entries = fs.readdirSync(runsDir, { withFileTypes: true });
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const observations = [];
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (!entry.isFile() || !entry.name.startsWith('dispatch-') || !entry.name.endsWith('.json')) continue;
|
|
173
|
+
const file = path.join(runsDir, entry.name);
|
|
174
|
+
try {
|
|
175
|
+
const stat = fs.statSync(file);
|
|
176
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
177
|
+
const results = Array.isArray(parsed?.results) ? parsed.results : [];
|
|
178
|
+
for (const [index, row] of results.entries()) {
|
|
179
|
+
if (typeof row?.verified_passed !== 'boolean') continue;
|
|
180
|
+
const source = `${path.relative(root, file)}#results[${index}]`;
|
|
181
|
+
const observation = observationFromRecord(row, source, stat.mtimeMs);
|
|
182
|
+
if (observation) observations.push(observation);
|
|
183
|
+
}
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
return observations;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function loadRouterHistory(root = process.cwd()) {
|
|
190
|
+
return [
|
|
191
|
+
...readTaskReceiptObservations(root),
|
|
192
|
+
...readMissionEventObservations(root),
|
|
193
|
+
...readDispatchReceiptObservations(root),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function median(values) {
|
|
198
|
+
const sorted = values
|
|
199
|
+
.map(finiteNonnegative)
|
|
200
|
+
.filter((value) => value !== null)
|
|
201
|
+
.sort((left, right) => left - right);
|
|
202
|
+
if (!sorted.length) return null;
|
|
203
|
+
const middle = Math.floor(sorted.length / 2);
|
|
204
|
+
return sorted.length % 2
|
|
205
|
+
? sorted[middle]
|
|
206
|
+
: (sorted[middle - 1] + sorted[middle]) / 2;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function historyKey(engine, taskType) {
|
|
210
|
+
return `${normalizedLabel(engine)}\u0000${normalizedLabel(taskType)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function computeEngineTaskStats(observations, options = {}) {
|
|
214
|
+
const nowValue = options.now instanceof Date ? options.now.getTime() : Number(options.now ?? Date.now());
|
|
215
|
+
const nowMs = Number.isFinite(nowValue) ? nowValue : Date.now();
|
|
216
|
+
const halfLifeValue = Number(options.halfLifeMs ?? DEFAULT_HALF_LIFE_MS);
|
|
217
|
+
const halfLifeMs = Number.isFinite(halfLifeValue) && halfLifeValue > 0
|
|
218
|
+
? halfLifeValue
|
|
219
|
+
: DEFAULT_HALF_LIFE_MS;
|
|
220
|
+
const groups = new Map();
|
|
221
|
+
for (const row of Array.isArray(observations) ? observations : []) {
|
|
222
|
+
const engine = normalizedLabel(row?.engine);
|
|
223
|
+
const taskType = normalizedLabel(row?.task_type || row?.taskType);
|
|
224
|
+
if (!engine || !taskType || typeof row?.verified_passed !== 'boolean') continue;
|
|
225
|
+
const key = historyKey(engine, taskType);
|
|
226
|
+
const group = groups.get(key) || { engine, task_type: taskType, receipts: [] };
|
|
227
|
+
group.receipts.push(row);
|
|
228
|
+
groups.set(key, group);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return [...groups.values()].map((group) => {
|
|
232
|
+
let weightedPasses = 0;
|
|
233
|
+
let totalWeight = 0;
|
|
234
|
+
let verifiedPasses = 0;
|
|
235
|
+
for (const receipt of group.receipts) {
|
|
236
|
+
const atMs = Number(receipt.at_ms);
|
|
237
|
+
const ageMs = Number.isFinite(atMs) ? Math.max(0, nowMs - atMs) : halfLifeMs;
|
|
238
|
+
const weight = Math.pow(0.5, ageMs / halfLifeMs);
|
|
239
|
+
totalWeight += weight;
|
|
240
|
+
if (receipt.verified_passed) {
|
|
241
|
+
verifiedPasses += 1;
|
|
242
|
+
weightedPasses += weight;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const receiptCount = group.receipts.length;
|
|
246
|
+
const passRate = verifiedPasses / receiptCount;
|
|
247
|
+
const weightedPassRate = totalWeight > 0 ? weightedPasses / totalWeight : passRate;
|
|
248
|
+
const medianDurationMs = median(group.receipts.map((receipt) => receipt.duration_ms));
|
|
249
|
+
const durationScore = medianDurationMs === null
|
|
250
|
+
? 0
|
|
251
|
+
: 1 / (1 + (medianDurationMs / DURATION_SCALE_MS));
|
|
252
|
+
return {
|
|
253
|
+
engine: group.engine,
|
|
254
|
+
task_type: group.task_type,
|
|
255
|
+
receipt_count: receiptCount,
|
|
256
|
+
verified_passes: verifiedPasses,
|
|
257
|
+
verified_failures: receiptCount - verifiedPasses,
|
|
258
|
+
verified_pass_rate: passRate,
|
|
259
|
+
median_duration_ms: medianDurationMs,
|
|
260
|
+
recency_weighted_score: (weightedPassRate * 0.85) + (durationScore * 0.15),
|
|
261
|
+
};
|
|
262
|
+
}).sort((left, right) => left.engine.localeCompare(right.engine)
|
|
263
|
+
|| left.task_type.localeCompare(right.task_type));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function candidateId(candidate) {
|
|
267
|
+
return normalizedLabel(typeof candidate === 'string' ? candidate : candidate?.id || candidate?.name);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function fallbackOrder(candidate, index) {
|
|
271
|
+
const value = Number(candidate && typeof candidate === 'object' ? candidate.fallback_order : NaN);
|
|
272
|
+
return Number.isFinite(value) ? value : index;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function legacyRank(candidates) {
|
|
276
|
+
return candidates
|
|
277
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
278
|
+
.sort((left, right) => fallbackOrder(left.candidate, left.index) - fallbackOrder(right.candidate, right.index)
|
|
279
|
+
|| candidateId(left.candidate).localeCompare(candidateId(right.candidate)))
|
|
280
|
+
.map((entry) => entry.candidate);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function rankEnginesDetailed(candidates, options = {}) {
|
|
284
|
+
const input = Array.isArray(candidates) ? candidates : [];
|
|
285
|
+
const legacy = legacyRank(input);
|
|
286
|
+
const taskType = normalizedLabel(options.taskType || options.task_type);
|
|
287
|
+
const minValue = Number(options.minReceipts ?? DEFAULT_MIN_RECEIPTS);
|
|
288
|
+
const minReceipts = Number.isInteger(minValue) && minValue > 0 ? minValue : DEFAULT_MIN_RECEIPTS;
|
|
289
|
+
const observations = Array.isArray(options.observations)
|
|
290
|
+
? options.observations
|
|
291
|
+
: loadRouterHistory(options.root || process.cwd());
|
|
292
|
+
const stats = computeEngineTaskStats(observations, options);
|
|
293
|
+
const statsByEngine = new Map(stats
|
|
294
|
+
.filter((row) => row.task_type === taskType)
|
|
295
|
+
.map((row) => [row.engine, row]));
|
|
296
|
+
const thinEngines = legacy
|
|
297
|
+
.map(candidateId)
|
|
298
|
+
.filter((engine) => !statsByEngine.has(engine) || statsByEngine.get(engine).receipt_count < minReceipts);
|
|
299
|
+
|
|
300
|
+
if (!taskType || thinEngines.length) {
|
|
301
|
+
return {
|
|
302
|
+
candidates: legacy,
|
|
303
|
+
stats,
|
|
304
|
+
task_type: taskType,
|
|
305
|
+
used_track_record: false,
|
|
306
|
+
thin_engines: thinEngines,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const ranked = legacy
|
|
311
|
+
.map((candidate, index) => ({ candidate, index, stats: statsByEngine.get(candidateId(candidate)) }))
|
|
312
|
+
.sort((left, right) => right.stats.recency_weighted_score - left.stats.recency_weighted_score
|
|
313
|
+
|| fallbackOrder(left.candidate, left.index) - fallbackOrder(right.candidate, right.index)
|
|
314
|
+
|| candidateId(left.candidate).localeCompare(candidateId(right.candidate)))
|
|
315
|
+
.map((entry) => entry.candidate);
|
|
316
|
+
return {
|
|
317
|
+
candidates: ranked,
|
|
318
|
+
stats,
|
|
319
|
+
task_type: taskType,
|
|
320
|
+
used_track_record: true,
|
|
321
|
+
thin_engines: [],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function rankEngines(candidates, options = {}) {
|
|
326
|
+
return rankEnginesDetailed(candidates, options).candidates;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function routerPickExplanation(decision) {
|
|
330
|
+
const winner = decision?.candidates?.[0];
|
|
331
|
+
const engine = candidateId(winner);
|
|
332
|
+
if (!engine) return '';
|
|
333
|
+
const taskType = normalizedLabel(decision.task_type) || 'work';
|
|
334
|
+
if (!decision.used_track_record) {
|
|
335
|
+
return `router picked ${engine} because ${taskType} track records are thin, so fallback order applies.`;
|
|
336
|
+
}
|
|
337
|
+
const stats = decision.stats.find((row) => row.engine === engine && row.task_type === taskType);
|
|
338
|
+
if (!stats) return `router picked ${engine} because it has the strongest ${taskType} track record.`;
|
|
339
|
+
const passRate = (stats.verified_pass_rate * 100).toFixed(1);
|
|
340
|
+
const duration = stats.median_duration_ms === null ? 'no duration' : `${Math.round(stats.median_duration_ms)} ms median`;
|
|
341
|
+
return `router picked ${engine} because ${stats.receipt_count} ${taskType} receipts scored ${stats.recency_weighted_score.toFixed(3)} with a ${passRate}% verified pass rate and ${duration}.`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = {
|
|
345
|
+
DEFAULT_MIN_RECEIPTS,
|
|
346
|
+
DEFAULT_HALF_LIFE_MS,
|
|
347
|
+
loadRouterHistory,
|
|
348
|
+
computeEngineTaskStats,
|
|
349
|
+
rankEngines,
|
|
350
|
+
rankEnginesDetailed,
|
|
351
|
+
routerPickExplanation,
|
|
352
|
+
};
|
package/lib/runner-command.js
CHANGED
|
@@ -62,11 +62,21 @@ const RUNNER_PROFILE_DEFS = Object.freeze({
|
|
|
62
62
|
model: '',
|
|
63
63
|
commandTemplate: '{bin} -p -- {prompt}',
|
|
64
64
|
}),
|
|
65
|
+
grok: Object.freeze({
|
|
66
|
+
bin: 'grok',
|
|
67
|
+
model: 'grok-4.5',
|
|
68
|
+
commandTemplate: '{bin} --always-approve -p {prompt}',
|
|
69
|
+
}),
|
|
65
70
|
hermes: Object.freeze({
|
|
66
71
|
bin: 'hermes',
|
|
67
72
|
model: '',
|
|
68
73
|
commandTemplate: '{bin} -p -- {prompt}',
|
|
69
74
|
}),
|
|
75
|
+
droid: Object.freeze({
|
|
76
|
+
bin: 'droid',
|
|
77
|
+
model: '',
|
|
78
|
+
commandTemplate: '{bin} exec {prompt}',
|
|
79
|
+
}),
|
|
70
80
|
});
|
|
71
81
|
|
|
72
82
|
// Alias -> canonical profile name. Every alias resolves to the same config as
|