atris 3.60.0 → 3.62.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/README.md +37 -5
- package/atris/skills/engines/SKILL.md +9 -9
- package/bin/atris.js +1 -0
- package/commands/autoland.js +36 -5
- package/commands/autopilot.js +105 -6
- package/commands/engine.js +96 -21
- package/commands/host.js +56 -4
- package/commands/land.js +129 -8
- package/commands/member.js +33 -8
- package/commands/mission.js +112 -3
- package/commands/one-lap.js +14 -10
- package/commands/stream.js +56 -22
- package/commands/team.js +82 -10
- package/commands/worktree.js +39 -0
- package/commands/youtube.js +36 -26
- package/lib/engine-ask.js +112 -9
- package/lib/engine-registry.js +283 -39
- package/lib/fleet.js +444 -26
- package/lib/functional-owner.js +15 -6
- package/lib/host.js +311 -175
- package/lib/member-engine.js +11 -1
- package/lib/member-park.js +138 -0
- package/lib/member-scaffold.js +37 -0
- package/lib/roster-markdown.js +1 -1
- package/lib/roster-prep.js +289 -0
- package/lib/roster-runs.js +389 -0
- package/lib/roster-suggest.js +145 -0
- package/lib/router-brain.js +111 -3
- package/lib/self-drive.js +26 -2
- package/lib/state-detection.js +3 -3
- package/lib/task-db.js +38 -3
- package/lib/team-lineup.js +7 -2
- package/lib/todo.js +1 -1
- package/lib/wish-audit.js +1 -1
- package/package.json +1 -1
- package/scripts/det/cpu-hotspots.js +216 -0
- package/scripts/det/ytnotes +105 -19
- package/scripts/det/ytquote-repair.js +18 -4
- package/templates/members/host/MEMBER.md +5 -3
- package/templates/members/host/MISSION.md +1 -1
- package/templates/members/host/skills/host/SKILL.md +61 -12
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ atris init && atris
|
|
|
24
24
|
|
|
25
25
|
## For Coding Agents
|
|
26
26
|
|
|
27
|
-
**Read [`FOR_AGENTS.md`](FOR_AGENTS.md) first
|
|
27
|
+
**Read [`FOR_AGENTS.md`](FOR_AGENTS.md) first**: why agents adopt Atris, 60-second start, spread-it instructions.
|
|
28
28
|
|
|
29
29
|
If you're an agent, run:
|
|
30
30
|
|
|
@@ -110,6 +110,17 @@ atris site publish dist --slug my-site --spa
|
|
|
110
110
|
atris site publish . --slug my-next-app --profile app --build # Next.js static export
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
### Bring people together with the Host
|
|
114
|
+
|
|
115
|
+
The Host is a team member that gets to know everyone in a room (a company, a club, your own network) through one fun question at a time, keeps a card for each person, and introduces two people only when both say yes. Then it offers to find a time and checks that it happened. It never reads anyone's messages and keeps no scores.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
atris member install host
|
|
119
|
+
atris host setup --room personal # your own network: it asks you, and drafts messages for you to send
|
|
120
|
+
atris host import people.jsonl # bulk add people and who already knows whom
|
|
121
|
+
atris host room # the host's weekly read
|
|
122
|
+
```
|
|
123
|
+
|
|
113
124
|
## Chat With Atris 2
|
|
114
125
|
|
|
115
126
|
`ax` is the Atris 2 chat and coding-agent CLI. It uses the hosted Atris cloud by default, streams text, shows tool activity, and keeps fresh installs away from local setup.
|
|
@@ -123,7 +134,7 @@ ax --max "reason through this hard decision"
|
|
|
123
134
|
ax --doctor
|
|
124
135
|
```
|
|
125
136
|
|
|
126
|
-
Use Fast for quick chat and low-latency Atris loops. Use Pro for deeper tool loops. Use Max for the hardest jobs
|
|
137
|
+
Use Fast for quick chat and low-latency Atris loops. Use Pro for deeper tool loops. Use Max for the hardest jobs; it runs the highest-reasoning model and takes longer per turn. Advanced local workspace mode is opt-in with `--local` plus `AX_BACKEND_URL`.
|
|
127
138
|
|
|
128
139
|
## Play AgentXP
|
|
129
140
|
|
|
@@ -267,9 +278,30 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
267
278
|
|
|
268
279
|
## Engines
|
|
269
280
|
|
|
270
|
-
An engine is the intelligence that builds a mission. Every installed headless coding CLI is a swappable worker behind one contract: a bounded prompt goes in, verified proof comes out, and engines never self-certify their own work. The house default is `atris-fast` (`ax`);
|
|
281
|
+
An engine is the intelligence that builds a mission. Every installed headless coding CLI is a swappable worker behind one contract: a bounded prompt goes in, verified proof comes out, and engines never self-certify their own work. The house default is `atris-fast` (`ax`); `claude`, `codex`, `cursor`, `devin`, `grok`, and others join when their CLIs are installed.
|
|
282
|
+
|
|
283
|
+
### Who does each job
|
|
284
|
+
|
|
285
|
+
`ROSTER.md` says which tool and model does each job. Edit it any time; every project, team member, and new chat follows it.
|
|
286
|
+
|
|
287
|
+
```markdown
|
|
288
|
+
## build
|
|
289
|
+
- claude code, model: opus 5.5
|
|
290
|
+
|
|
291
|
+
## review
|
|
292
|
+
- codex, model: gpt-6-astra, effort: medium, max: 20 min, prep: search
|
|
293
|
+
- claude code, model: opus 5.5
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The first ready worker leads and the rest are backups. `~/.atris/ROSTER.md` covers every project on the machine; `atris/ROSTER.md` overrides it for one project. A worker that stalls sits out 30 minutes and the next one takes over.
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
atris engine roster # who does what, with each worker's recent record
|
|
300
|
+
atris engine assign build claude --model "opus 5.5" # change a job's lead
|
|
301
|
+
atris engine roster --available # tools and models on this machine
|
|
302
|
+
```
|
|
271
303
|
|
|
272
|
-
Show
|
|
304
|
+
Show every installed engine and the current default:
|
|
273
305
|
|
|
274
306
|
```bash
|
|
275
307
|
atris engine
|
|
@@ -290,7 +322,7 @@ atris mission run "fix the flaky login test" --engine codex
|
|
|
290
322
|
|
|
291
323
|
### Fleet Flight
|
|
292
324
|
|
|
293
|
-
`atris mission run --fleet` staffs every idle installed engine on the board's claimable safe-lane tasks
|
|
325
|
+
`atris mission run --fleet` staffs every idle installed engine on the board's claimable safe-lane tasks: one mission per task, one worktree per engine. Builds run in parallel; arrivals land serially with rebase-before-ship, so a rebase conflict pauses that landing (never auto-resolved) and keeps its worktree instead of orphaning work. Each flight writes a receipt to `atris/runs/fleet-<stamp>.json`.
|
|
294
326
|
|
|
295
327
|
```bash
|
|
296
328
|
atris mission run --fleet --slots 3 # staff up to 3 engines
|
|
@@ -26,29 +26,29 @@ One contract, eleven live profiles. The orchestrator writes a bounded task promp
|
|
|
26
26
|
|
|
27
27
|
## Pick the job owner first
|
|
28
28
|
|
|
29
|
-
Before choosing an engine for search, build, or
|
|
29
|
+
Before choosing an engine for search, build, review, or any other job, run `atris engine roster` and use that job's lead worker. Only deviate when the user names an engine.
|
|
30
30
|
|
|
31
|
-
The
|
|
31
|
+
**The file.** `~/.atris/ROSTER.md` covers every project on the machine; `atris/ROSTER.md` overrides it job by job. Each `## job` lists workers in order, one per line, tool then model:
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
## review
|
|
35
|
-
- codex, model: gpt-6-astra, effort: medium, max: 20 min
|
|
35
|
+
- codex, model: gpt-6-astra, effort: medium, max: 20 min, prep: search
|
|
36
36
|
- claude code, model: opus 5.5
|
|
37
37
|
|
|
38
38
|
## small build
|
|
39
|
-
- devin, model: swe-2-max, until 2026-10-24
|
|
39
|
+
- devin, model: swe-2-max, max: 20 min, until 2026-10-24
|
|
40
40
|
- grok, model: grok 4.7 fast
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The first worker that is ready and not expired leads; the rest are
|
|
43
|
+
The first worker that is ready and not expired leads; the rest are backups in order, and the whole list is the job's team for parallel parts. Jobs are open-ended: add `## quick fixes (like build)` when the name does not say its kind. Use the small build lead for small, tightly specified slices.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
**Changing it.** Edit the file, or: `atris engine assign <job> <tool> [--model --effort --max --prep search --days N]` sets the lead and keeps the rest; `--add`, `--remove <tool>`, `--promote <tool>` (move an existing worker to the top, line unchanged), `--clear`; `--everywhere` writes the machine file; `--session` (with `ATRIS_ROSTER_SESSION=<name>`) changes this shell only. `atris engine roster --available` lists installed tools and models.
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
**What happens on its own.** A worker that stalls or loses its connection sits out 30 minutes ("cooling") and the next worker takes the job; a failed task (tests failed) is not handed over and does not bench anyone. Every run is recorded; `atris engine roster --runs` lists them and the roster shows each worker's recent record. When the record says the order is wrong, the roster prints one suggestion with the exact command. It never edits the file: run it only when the owner says so.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
**Heavy models.** Astra and Fable should judge, not explore. Give their line `prep: search`: the search lead writes a brief of at most 300 lines first, and if prep fails the worker runs as usual.
|
|
50
50
|
|
|
51
|
-
Team members
|
|
51
|
+
**Team members.** Navigators, researchers, and scouts do search; validators, reviewers, and judges do review; everyone else builds. A `## team` line overrides: `- researcher: search` or `- judge: claude code, model: opus 5.5`. `member run`, owned missions, and autopilot phases use the member's pick; `--engine` still wins. Parked members are skipped; `atris team park <name>` and `atris team unpark <name>` toggle it.
|
|
52
52
|
|
|
53
53
|
## three verbs
|
|
54
54
|
|
package/bin/atris.js
CHANGED
|
@@ -574,6 +574,7 @@ function showHelpAll() {
|
|
|
574
574
|
console.log(' who - Show local engines and team members working, waiting, done, or stale');
|
|
575
575
|
console.log(' stream - Watch the whole team work live in one terminal');
|
|
576
576
|
console.log(' team - One team view: members, roles, engine assignments (presence for live)');
|
|
577
|
+
console.log(' host - The Host: gets to know people, makes two-yes introductions (atris member install host)');
|
|
577
578
|
console.log(' watch - Turn one sentence into an always-on background watcher');
|
|
578
579
|
console.log(' ctop - Show a process-first live agent CPU/memory view');
|
|
579
580
|
console.log(' doctor - Node/task/auth/workspace readiness (--json for agents)');
|
package/commands/autoland.js
CHANGED
|
@@ -130,13 +130,38 @@ function flag(args, name, fallback = '') {
|
|
|
130
130
|
return args[idx + 1] || fallback;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
function runOwnCli(root, cliArgs) {
|
|
133
|
+
function runOwnCli(root, cliArgs, timeoutMs = 300000) {
|
|
134
134
|
// Always spawn the bin this module shipped with. Resolving through the
|
|
135
135
|
// target root or a global `atris` breaks whenever the tick runs a project
|
|
136
136
|
// that isn't the CLI repo on a machine without a global install (CI, cron).
|
|
137
137
|
const bin = path.resolve(__dirname, '..', 'bin', 'atris.js');
|
|
138
|
-
const result = spawnSync(process.execPath, [bin, ...cliArgs], { cwd: root, encoding: 'utf8', timeout:
|
|
139
|
-
return {
|
|
138
|
+
const result = spawnSync(process.execPath, [bin, ...cliArgs], { cwd: root, encoding: 'utf8', timeout: timeoutMs });
|
|
139
|
+
return {
|
|
140
|
+
status: result.status,
|
|
141
|
+
stdout: String(result.stdout || ''),
|
|
142
|
+
stderr: String(result.stderr || ''),
|
|
143
|
+
error_code: result.error && result.error.code ? result.error.code : null,
|
|
144
|
+
signal: result.signal || null,
|
|
145
|
+
timeout_ms: timeoutMs,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The accept sweep walks every pending-review row under --all, so a fixed
|
|
150
|
+
// 300s child timeout kills a real backlog mid-accept. Size the window from
|
|
151
|
+
// the queue: roughly 8s a row on top of a 60s floor, never under the old
|
|
152
|
+
// 300s default, capped at 25min.
|
|
153
|
+
function acceptSweepTimeoutMs(pendingRows) {
|
|
154
|
+
return Math.min(1500000, Math.max(300000, 60000 + pendingRows * 8000));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A killed child truncates stdout and often leaves stderr empty, so the
|
|
158
|
+
// spawn failure itself is the first thing to name. Without this a timed-out
|
|
159
|
+
// sweep reports "output unreadable" forever.
|
|
160
|
+
function describeAcceptKillCause(accept) {
|
|
161
|
+
if (accept.error_code === 'ETIMEDOUT') return `auto-accept timed out at ${accept.timeout_ms}ms`;
|
|
162
|
+
if (accept.error_code) return `auto-accept spawn failed: ${accept.error_code}`;
|
|
163
|
+
if (accept.signal) return `auto-accept killed by ${accept.signal}`;
|
|
164
|
+
return null;
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
function readProjection(root) {
|
|
@@ -759,7 +784,11 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
759
784
|
const cliArgs = ['task', 'auto-accept-certified', '--json', '--certify-first'];
|
|
760
785
|
if (policy.accept_all) cliArgs.push('--all');
|
|
761
786
|
else if (policy.strict_verify === false) cliArgs.push('--no-strict-verify');
|
|
762
|
-
const
|
|
787
|
+
const pendingReviewRows = readProjection(root)
|
|
788
|
+
.filter((task) => task && task.status === 'review')
|
|
789
|
+
.filter((task) => String(task.review?.approval_status || task.metadata?.approval_status || 'pending') === 'pending')
|
|
790
|
+
.length;
|
|
791
|
+
const accept = runOwnCli(root, cliArgs, acceptSweepTimeoutMs(pendingReviewRows));
|
|
763
792
|
try {
|
|
764
793
|
const parsed = JSON.parse(accept.stdout);
|
|
765
794
|
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
@@ -801,7 +830,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
801
830
|
receipt.skipped = parsed.skipped ?? null;
|
|
802
831
|
receipt.undercounted = Boolean(parsed.undercounted);
|
|
803
832
|
} catch (err) {
|
|
804
|
-
receipt.accept_error = accept.stderr.slice(0, 200) || 'auto-accept output unreadable';
|
|
833
|
+
receipt.accept_error = describeAcceptKillCause(accept) || accept.stderr.slice(0, 200) || 'auto-accept output unreadable';
|
|
805
834
|
}
|
|
806
835
|
|
|
807
836
|
// 2b. tell the operator the moment something lands: one text, one landing
|
|
@@ -1108,6 +1137,8 @@ function autolandCommand(args = []) {
|
|
|
1108
1137
|
module.exports = {
|
|
1109
1138
|
autolandCommand,
|
|
1110
1139
|
digestNextMoves,
|
|
1140
|
+
acceptSweepTimeoutMs,
|
|
1141
|
+
describeAcceptKillCause,
|
|
1111
1142
|
digestStoryRows,
|
|
1112
1143
|
digestTickStatus,
|
|
1113
1144
|
engineAnswerValidationLine,
|
package/commands/autopilot.js
CHANGED
|
@@ -31,6 +31,10 @@ const { ownerIdentity } = require('../utils/owner-identity');
|
|
|
31
31
|
const pkg = require('../package.json');
|
|
32
32
|
|
|
33
33
|
const PHASE_TIMEOUT = 600000; // 10 min per phase
|
|
34
|
+
// A phase's cap covers its prep pass too: prep's elapsed time comes out of
|
|
35
|
+
// the cap before the phase command starts. The floor keeps a slow prep from
|
|
36
|
+
// leaving the phase no room at all.
|
|
37
|
+
const PHASE_MIN_AFTER_PREP_MS = 30000;
|
|
34
38
|
|
|
35
39
|
function looksOwnerClaimed(claimed) {
|
|
36
40
|
const text = String(claimed || '').trim().toLowerCase();
|
|
@@ -521,7 +525,17 @@ function buildPhaseRunner(phase, promptFile, cwd = process.cwd(), allowedTools =
|
|
|
521
525
|
...(picked.engine.roster_effort ? { effort: picked.engine.roster_effort } : {}),
|
|
522
526
|
});
|
|
523
527
|
const maxSeconds = Number(picked.engine.roster_max_seconds) || 0;
|
|
524
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
command,
|
|
530
|
+
timeoutMs: maxSeconds > 0 ? maxSeconds * 1000 : null,
|
|
531
|
+
engineId: picked.engine.id,
|
|
532
|
+
maxSeconds,
|
|
533
|
+
model: picked.model || '',
|
|
534
|
+
effort: picked.engine.roster_effort || '',
|
|
535
|
+
job: picked.job || '',
|
|
536
|
+
member: AUTOPILOT_PHASE_MEMBERS[phase],
|
|
537
|
+
prep: picked.engine.roster_prep || '',
|
|
538
|
+
};
|
|
525
539
|
} finally {
|
|
526
540
|
if (previous === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
|
|
527
541
|
else process.env.ATRIS_RUNNER_PROFILE = previous;
|
|
@@ -532,6 +546,54 @@ function buildPhaseRunnerCommand(phase, promptFile, cwd = process.cwd()) {
|
|
|
532
546
|
return buildPhaseRunner(phase, promptFile, cwd).command;
|
|
533
547
|
}
|
|
534
548
|
|
|
549
|
+
// One run-record line for a phase a roster worker ran. Never throws.
|
|
550
|
+
function recordPhaseRosterRun(root, phase, runner, startedMs, outcome, err = null) {
|
|
551
|
+
if (!runner || !runner.engineId) return;
|
|
552
|
+
try {
|
|
553
|
+
const { appendRosterRun, parseRunUsage } = require('../lib/roster-runs');
|
|
554
|
+
const { engineRunsView } = require('../lib/roster-models');
|
|
555
|
+
const { prepRecordFields } = require('../lib/roster-prep');
|
|
556
|
+
appendRosterRun(root, {
|
|
557
|
+
at: new Date(startedMs).toISOString(),
|
|
558
|
+
job: runner.job || '',
|
|
559
|
+
member: runner.member || '',
|
|
560
|
+
engine: runner.engineId,
|
|
561
|
+
model: runner.model || engineRunsView(runner.engineId).model || '',
|
|
562
|
+
effort: runner.effort || '',
|
|
563
|
+
max_seconds: runner.maxSeconds || 0,
|
|
564
|
+
seconds: Math.max(0, (Date.now() - startedMs) / 1000),
|
|
565
|
+
task: `autopilot ${phase}`,
|
|
566
|
+
source: 'autopilot',
|
|
567
|
+
...prepRecordFields(runner.prepResult),
|
|
568
|
+
...parseRunUsage({ stdout: outcome.stdout || (err && err.stdout) || '', stderr: (err && err.stderr) || '' }),
|
|
569
|
+
outcome: outcome.outcome,
|
|
570
|
+
...(outcome.detail ? { detail: outcome.detail } : {}),
|
|
571
|
+
});
|
|
572
|
+
} catch { /* best effort */ }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// How a phase ended when its command threw: a timeout stalled, a signal or
|
|
576
|
+
// nonzero exit failed, a credit wall on stderr is credit out.
|
|
577
|
+
function phaseFailureOutcome(err, runner) {
|
|
578
|
+
if (isPhaseTimeoutError(err)) {
|
|
579
|
+
const cap = Number(runner && runner.maxSeconds) || 0;
|
|
580
|
+
return { outcome: 'stalled', detail: cap >= 60 ? `stalled at ${Math.round(cap / 60)} min` : cap > 0 ? `stalled at ${cap}s` : 'hit its time limit' };
|
|
581
|
+
}
|
|
582
|
+
if (isPhaseKillError(err)) return { outcome: 'failed', detail: `stopped by ${err.signal || 'a signal'}` };
|
|
583
|
+
const { engineFailureHealthStatus } = require('../lib/engine-registry');
|
|
584
|
+
const stderr = String(err && err.stderr || '');
|
|
585
|
+
if (engineFailureHealthStatus({ status: 'errored', stderr }) === 'credit_out') return { outcome: 'credit out', detail: 'the engine hit its usage limit' };
|
|
586
|
+
const line = stderr.trim().split('\n').pop() || String(err && err.message || '').split('\n')[0];
|
|
587
|
+
return { outcome: 'failed', detail: line || `exited ${err && err.status}` };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// What the prep worker reads as the task: the tick's task title.
|
|
591
|
+
function autopilotPhaseTaskTitle(context, phase) {
|
|
592
|
+
const task = context && context.task;
|
|
593
|
+
if (task && typeof task === 'object') return String(task.title || task.display_id || `autopilot ${phase}`);
|
|
594
|
+
return String(task || `autopilot ${phase}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
535
597
|
/**
|
|
536
598
|
* Run a phase via the configured runner subprocess.
|
|
537
599
|
*/
|
|
@@ -539,20 +601,43 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
539
601
|
const { verbose = false } = options;
|
|
540
602
|
let timeout = options.timeout || PHASE_TIMEOUT;
|
|
541
603
|
|
|
542
|
-
|
|
604
|
+
let prompt = buildPrompt(phase, context, options);
|
|
543
605
|
const tmpFile = path.join(process.cwd(), '.autopilot-prompt.tmp');
|
|
544
606
|
fs.writeFileSync(tmpFile, prompt);
|
|
545
607
|
|
|
608
|
+
let rosterRunner = null;
|
|
609
|
+
let startedMs = Date.now();
|
|
546
610
|
try {
|
|
547
611
|
let cmd = options.cmdOverride;
|
|
548
612
|
if (!cmd) {
|
|
549
613
|
const runner = buildPhaseRunner(phase, tmpFile);
|
|
550
|
-
|
|
551
|
-
// A roster time cap stops the phase at that time
|
|
614
|
+
if (runner.engineId) rosterRunner = runner;
|
|
615
|
+
// A roster time cap stops the phase at that time, and prep runs inside
|
|
616
|
+
// it: the phase gets what remains after the pass, floored so it can
|
|
617
|
+
// still try.
|
|
552
618
|
if (runner.timeoutMs) timeout = runner.timeoutMs;
|
|
619
|
+
// A worker whose roster line asks for prep gets its brief first, the
|
|
620
|
+
// same pass fleet runs; the launch reads the prompt file, brief added.
|
|
621
|
+
if (runner.engineId && runner.prep) {
|
|
622
|
+
const prepStartedMs = Date.now();
|
|
623
|
+
const { runPrepPassSync, withPrepBrief } = require('../lib/roster-prep');
|
|
624
|
+
runner.prepResult = runPrepPassSync({
|
|
625
|
+
prepJob: runner.prep,
|
|
626
|
+
forJob: runner.job || AUTOPILOT_PHASE_MEMBERS[phase],
|
|
627
|
+
task: { display_id: `autopilot ${phase}`, title: autopilotPhaseTaskTitle(context, phase) },
|
|
628
|
+
prompt,
|
|
629
|
+
root: process.cwd(),
|
|
630
|
+
record: { member: runner.member || '' },
|
|
631
|
+
});
|
|
632
|
+
prompt = withPrepBrief(prompt, runner.prepResult);
|
|
633
|
+
fs.writeFileSync(tmpFile, prompt);
|
|
634
|
+
timeout = Math.max(PHASE_MIN_AFTER_PREP_MS, timeout - (Date.now() - prepStartedMs));
|
|
635
|
+
}
|
|
636
|
+
cmd = runner.command;
|
|
553
637
|
}
|
|
554
638
|
const env = { ...process.env };
|
|
555
639
|
delete env.CLAUDECODE;
|
|
640
|
+
startedMs = Date.now();
|
|
556
641
|
const output = execPhaseCommandSync(cmd, {
|
|
557
642
|
cwd: process.cwd(),
|
|
558
643
|
encoding: 'utf8',
|
|
@@ -563,10 +648,19 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
563
648
|
});
|
|
564
649
|
|
|
565
650
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
651
|
+
recordPhaseRosterRun(process.cwd(), phase, rosterRunner, startedMs, { outcome: 'landed', stdout: output || '' });
|
|
566
652
|
return { prompt, output: output || '' };
|
|
567
653
|
} catch (err) {
|
|
568
654
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
655
|
+
recordPhaseRosterRun(process.cwd(), phase, rosterRunner, startedMs, phaseFailureOutcome(err, rosterRunner), err);
|
|
569
656
|
if (isPhaseTimeoutError(err)) {
|
|
657
|
+
// A roster worker that stalls sits out the cooldown, so the next
|
|
658
|
+
// phase run goes to the job's backup.
|
|
659
|
+
if (rosterRunner) {
|
|
660
|
+
try {
|
|
661
|
+
require('../lib/engine-registry').recordEngineRunHealth(rosterRunner.engineId, { timed_out: true, max_seconds: rosterRunner.maxSeconds }, process.cwd());
|
|
662
|
+
} catch { /* best effort */ }
|
|
663
|
+
}
|
|
570
664
|
throw new Error(`${phase} phase timed out after ${timeout / 1000}s (configured runner hit the wall; any work it committed survives, reconcile from pre-tick HEADs)`);
|
|
571
665
|
}
|
|
572
666
|
if (isPhaseKillError(err)) {
|
|
@@ -1419,7 +1513,7 @@ function parseProposedBlock(lines) {
|
|
|
1419
1513
|
// behind it, the configured runner runs exactly as before.
|
|
1420
1514
|
function buildPlanReviewRunner(promptFile, cwd = process.cwd(), timeout = 180000) {
|
|
1421
1515
|
const runner = buildPhaseRunner('review', promptFile, cwd, 'Bash,Read,Grep,Glob');
|
|
1422
|
-
return { command: runner.command, timeoutMs: runner.timeoutMs || timeout };
|
|
1516
|
+
return { ...runner, command: runner.command, timeoutMs: runner.timeoutMs || timeout };
|
|
1423
1517
|
}
|
|
1424
1518
|
|
|
1425
1519
|
/**
|
|
@@ -1429,12 +1523,15 @@ function buildPlanReviewRunner(promptFile, cwd = process.cwd(), timeout = 180000
|
|
|
1429
1523
|
function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
1430
1524
|
const tmpFile = path.join(cwd, '.autopilot-plan-review.tmp');
|
|
1431
1525
|
fs.writeFileSync(tmpFile, prompt);
|
|
1526
|
+
let runner = null;
|
|
1527
|
+
let startedMs = Date.now();
|
|
1432
1528
|
try {
|
|
1433
|
-
|
|
1529
|
+
runner = buildPlanReviewRunner(tmpFile, cwd, timeout);
|
|
1434
1530
|
const cmd = runner.command;
|
|
1435
1531
|
timeout = runner.timeoutMs;
|
|
1436
1532
|
const env = { ...process.env };
|
|
1437
1533
|
delete env.CLAUDECODE;
|
|
1534
|
+
startedMs = Date.now();
|
|
1438
1535
|
const output = execPhaseCommandSync(cmd, {
|
|
1439
1536
|
cwd,
|
|
1440
1537
|
encoding: 'utf8',
|
|
@@ -1443,8 +1540,10 @@ function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
|
1443
1540
|
maxBuffer: 10 * 1024 * 1024,
|
|
1444
1541
|
env,
|
|
1445
1542
|
});
|
|
1543
|
+
recordPhaseRosterRun(cwd, 'plan review', runner, startedMs, { outcome: 'landed', stdout: output || '' });
|
|
1446
1544
|
return output || '';
|
|
1447
1545
|
} catch (err) {
|
|
1546
|
+
recordPhaseRosterRun(cwd, 'plan review', runner, startedMs, phaseFailureOutcome(err, runner), err);
|
|
1448
1547
|
if (err.stdout) return err.stdout;
|
|
1449
1548
|
throw err;
|
|
1450
1549
|
} finally {
|