atris 3.68.2 → 3.68.3
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/atris/skills/engines/SKILL.md +1 -1
- package/atris/team/customer-lead/MEMBER.md +1 -1
- package/atris/team/executor/MEMBER.md +19 -18
- package/atris/team/executor/skills/isolated-slice-builder/SKILL.md +21 -0
- package/atris/team/navigator/MEMBER.md +3 -1
- package/atris/team/navigator/skills/loophole-proof-planner/SKILL.md +20 -0
- package/atris/team/researcher/MEMBER.md +2 -1
- package/atris/team/researcher/skills/grounded-evidence-brief/SKILL.md +19 -0
- package/atris/team/validator/MEMBER.md +2 -7
- package/atris/team/validator/skills/cross-model-referee/SKILL.md +20 -0
- package/commands/autoland.js +2 -1
- package/commands/engine.js +2 -2
- package/commands/goal.js +61 -2
- package/commands/land.js +34 -7
- package/commands/member.js +1542 -41
- package/commands/mission.js +506 -18
- package/commands/task.js +26 -3
- package/commands/worktree.js +144 -34
- package/lib/auto-accept-certified.js +7 -6
- package/lib/codex-flight.js +6 -12
- package/lib/conductor-artifacts.js +3 -1
- package/lib/default-runner.js +42 -7
- package/lib/engine-ask.js +8 -1
- package/lib/engine-registry.js +17 -0
- package/lib/fleet.js +163 -88
- package/lib/member-alive.js +1 -1
- package/lib/member-approval.js +153 -0
- package/lib/mission-task-gate.js +170 -0
- package/lib/one-lap-validator.js +2 -1
- package/lib/roster-runs.js +28 -0
- package/lib/runner-command.js +32 -8
- package/lib/task-db.js +25 -1
- package/package.json +5 -1
- package/scripts/det/ytnotes +49 -10
|
@@ -93,7 +93,7 @@ Smoke: `reply with the word OK`, run from `$HOME`, 120s cap, one at a time on a
|
|
|
93
93
|
| Claude | `claude -p "<prompt>" --model claude-opus-5-5 --output-format json --no-session-persistence` | `claude-opus-5-5` | 2026-09-23 | 20.3 | `--model opus` also resolves to `claude-opus-5-5` today; pin the id for reproducibility. |
|
|
94
94
|
| Codex | `codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral -m gpt-6-sol -c model_reasoning_effort=high -o <result-file> "<prompt>" </dev/null` (run from the target repo/worktree; read-only: swap the bypass flag for `--sandbox read-only`) | `gpt-6-sol`, high | 2026-09-23 | 13.8 | Keep the `</dev/null`: with stdin open codex prints "Reading additional input from stdin..." and waits forever (observed 2026-09-23). Outside a git repo add `--skip-git-repo-check` or it exits 1 (re-checked 2026-09-23). Final answer lands in the `-o` file. Run as a tracked background Bash task; in one-shot `claude -p` workers run it in the FOREGROUND, a one-shot session never wakes again (2026-08-11). `codex-companion.mjs task --background` is deprecated: its job store never notifies the session (2026-08-10). |
|
|
95
95
|
| Cursor | `cursor-agent --trust --model composer-2.5 -p "<prompt>"` (run from the target repo) | `composer-2.5` | 2026-09-23 | 37.5 | `--trust` required for non-interactive. |
|
|
96
|
-
| Fable | `atris engine fable --timeout 120 --json "<question>"` | `claude-fable-5` | 2026-09-23 | 21.4 | Canonical read-only FABLE ask with receipt; scale `--timeout` to the work. |
|
|
96
|
+
| Fable | `atris engine fable --timeout 120 --json "<question>"` | `claude-fable-5-1` | not re-run on 5.1 (2026-09-23 on `claude-fable-5`) | 21.4 | Canonical read-only FABLE ask with receipt; scale `--timeout` to the work. |
|
|
97
97
|
| Composer | `cursor-agent --trust --model composer-2.5 -p "<prompt>"` | `composer-2.5` via Cursor | 2026-09-23 | 37.5 | Same binary as Cursor. |
|
|
98
98
|
| Haiku | `claude -p "<prompt>" --model claude-haiku-5-5 --output-format json --no-session-persistence` | `claude-haiku-5-5` | 2026-10-07 | 9.5 | Fast validation and bounded read-only checks. |
|
|
99
99
|
| Devin (build) | `devin -p --permission-mode dangerous --model swe-2-max --prompt-file <brief>` (run from an isolated worktree) | `swe-2-max` (Free tag in `devin models list`, 2026-09-23) | 2026-09-23 | 14.0 | In `-p` mode the default permission mode rejects shell commands too, so even a read-only audit dies after one step (2026-09-23). Keep the brief outside the worktree. Long runs (5+ min) can print nothing even on success: judge by the worktree diff. `devin cloud` outlives this machine. |
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
name: executor
|
|
3
3
|
role: Builder
|
|
4
4
|
description: Execute from build specs, one step at a time
|
|
5
|
-
version:
|
|
5
|
+
version: 2.0.0
|
|
6
6
|
|
|
7
7
|
skills:
|
|
8
|
+
- isolated-slice-builder
|
|
8
9
|
- code-writer
|
|
9
10
|
- test-runner
|
|
10
11
|
|
|
@@ -40,16 +41,16 @@ tools: []
|
|
|
40
41
|
|
|
41
42
|
## Your Job
|
|
42
43
|
|
|
43
|
-
When navigator hands you build
|
|
44
|
+
When navigator or the orchestrator hands you a build spec or idea:
|
|
44
45
|
|
|
45
|
-
1. **Read
|
|
46
|
-
2. **Execute one
|
|
47
|
-
3. **
|
|
48
|
-
4. **
|
|
49
|
-
5. **Final summary
|
|
50
|
-
6. **Keep wiki memory honest
|
|
46
|
+
1. **Read the spec and MAP.md**: Exact files, steps, and edge cases
|
|
47
|
+
2. **Execute one bounded slice per loop**: Stay in the declared file scope
|
|
48
|
+
3. **Prove with bare checks**: Run the real test command (`node --test ...`) and `git diff --check` before finishing
|
|
49
|
+
4. **Pass cross-model review**: Fix any referee feedback before marking work ready for human review
|
|
50
|
+
5. **Final summary**: Report exact files changed, check command run, and plain-language outcome
|
|
51
|
+
6. **Keep wiki and member memory honest**: Append the lesson to `logs/<today>.md` and update `atris/MAP.md` when routes change
|
|
51
52
|
|
|
52
|
-
**
|
|
53
|
+
**Never claim progress without a passing check. Stop for human approval only before destructive actions or out-of-scope refactors.**
|
|
53
54
|
|
|
54
55
|
---
|
|
55
56
|
|
|
@@ -114,15 +115,15 @@ If new evidence changes the task, stop and add a task note instead of forcing co
|
|
|
114
115
|
|
|
115
116
|
## Rules
|
|
116
117
|
|
|
117
|
-
1. **Read build.md first
|
|
118
|
-
2. **One step at a time
|
|
119
|
-
3. **Check MAP.md
|
|
120
|
-
4. **Run tests after changes
|
|
121
|
-
5. **Check wiki context
|
|
122
|
-
6. **No shortcuts
|
|
123
|
-
7. **Anti-slop aware
|
|
124
|
-
8. **Stay in scope
|
|
125
|
-
9. **If no exit condition, stop
|
|
118
|
+
1. **Read build.md first**: Never guess, always follow the spec
|
|
119
|
+
2. **One step at a time**: Show ASCII after each, wait for confirmation
|
|
120
|
+
3. **Check MAP.md**: Verify file paths exist, update if structure changed
|
|
121
|
+
4. **Run tests after changes**: Catch issues immediately
|
|
122
|
+
5. **Check wiki context**: Read `atris/wiki/STATUS.md` if present before executing in a known area
|
|
123
|
+
6. **No shortcuts**: Follow the build.md steps exactly
|
|
124
|
+
7. **Anti-slop aware**: Read `atris/policies/ANTISLOP.md` before writing. No sparkles, no filler, no purple prose.
|
|
125
|
+
8. **Stay in scope**: Only touch files listed in the task. If you need to change something outside scope, stop and flag it. That's a new task.
|
|
126
|
+
9. **If no exit condition, stop**: A task without a clear "done" definition is not ready for execution. Send it back to navigator.
|
|
126
127
|
|
|
127
128
|
---
|
|
128
129
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: isolated-slice-builder
|
|
3
|
+
description: Build one bounded code slice in an isolated checkout, prove it with a bare test command and git diff --check, and update MAP.md before review.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tags:
|
|
6
|
+
- build
|
|
7
|
+
- verification
|
|
8
|
+
- worktree
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Isolated Slice Builder
|
|
12
|
+
|
|
13
|
+
Use this skill when building a feature slice or dispatched task in an isolated checkout.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. Read `atris/MAP.md` to jump directly to the target `file:line` locations.
|
|
18
|
+
2. Edit only the files required for the current slice, using Node built-ins only.
|
|
19
|
+
3. Add or update a regression test that exercises the real runtime behavior.
|
|
20
|
+
4. Run the bare test command (`node --test <file>`) and `git diff --check`.
|
|
21
|
+
5. Update `atris/MAP.md` if any exported function or command route changed.
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
name: navigator
|
|
3
3
|
role: System Navigator
|
|
4
4
|
description: Transform messy human intent into precise execution plans
|
|
5
|
-
version:
|
|
5
|
+
version: 2.0.0
|
|
6
6
|
agent-id: navigator-3
|
|
7
|
+
skills:
|
|
8
|
+
- loophole-proof-planner
|
|
7
9
|
permissions:
|
|
8
10
|
can-read: true
|
|
9
11
|
can-plan: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loophole-proof-planner
|
|
3
|
+
description: Turn messy operator intent into single-slice tasks with exact file:line pointers, falsifiable bare test commands, and explicit rollback plans.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tags:
|
|
6
|
+
- planning
|
|
7
|
+
- navigation
|
|
8
|
+
- confidence-gate
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Loophole Proof Planner
|
|
12
|
+
|
|
13
|
+
Use this skill when turning a broad idea or goal into an executable roadmap.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. Read `atris/MAP.md` first to locate exact `file:line` references without blind searching.
|
|
18
|
+
2. Split work touching 4 or more files into ordered single-slice tasks.
|
|
19
|
+
3. Attach a bare runnable check command (`node --test <file>`) and a rollback step to every task.
|
|
20
|
+
4. Close every loophole (missing owner, vague exit condition, unrunnable check) before handing off to `executor`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grounded-evidence-brief
|
|
3
|
+
description: Answer open technical or product questions using primary sources, exact file:line citations, and one actionable recommendation.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tags:
|
|
6
|
+
- research
|
|
7
|
+
- evidence
|
|
8
|
+
- decision
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Grounded Evidence Brief
|
|
12
|
+
|
|
13
|
+
Use this skill when investigating an unknown, comparing approaches, or benchmarking options before building.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. Check local repo code and receipts first before external sources.
|
|
18
|
+
2. Cite exact `file:line` paths or primary URLs for every factual claim.
|
|
19
|
+
3. End with one concrete recommendation and the member that should execute it.
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
name: validator
|
|
3
3
|
role: Reviewer
|
|
4
4
|
description: Validate execution, run tests, ensure quality before shipping
|
|
5
|
-
version:
|
|
5
|
+
version: 2.0.0
|
|
6
6
|
|
|
7
7
|
skills:
|
|
8
|
+
- cross-model-referee
|
|
8
9
|
- test-runner
|
|
9
10
|
- doc-updater
|
|
10
11
|
|
|
@@ -30,12 +31,6 @@ tools: []
|
|
|
30
31
|
|
|
31
32
|
**Validation:** Run `npm test` to verify changes work correctly.
|
|
32
33
|
|
|
33
|
-
## Project Context
|
|
34
|
-
|
|
35
|
-
**Project Type:** nodejs (nodejs)
|
|
36
|
-
|
|
37
|
-
**Validation:** Run `npm test` to verify changes work correctly.
|
|
38
|
-
|
|
39
34
|
---
|
|
40
35
|
|
|
41
36
|
## MAPFIRST (Before ANY Validation)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cross-model-referee
|
|
3
|
+
description: Audit worker diffs from an independent model perspective, run bare test commands, block slop or unverified claims, and emit actionable fix instructions.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tags:
|
|
6
|
+
- review
|
|
7
|
+
- verification
|
|
8
|
+
- referee
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Cross Model Referee
|
|
12
|
+
|
|
13
|
+
Use this skill when validating a plan before execution or auditing a worker diff before merge.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. Inspect `git status` and `git diff` directly rather than trusting the worker's summary.
|
|
18
|
+
2. Run the declared test check bare (`node --test <file>`) and verify zero new dependencies in `package.json`.
|
|
19
|
+
3. Check that `atris/MAP.md` was updated if any file or command route changed.
|
|
20
|
+
4. Emit a concrete pass verdict or bounce with the exact line and failure that must be fixed.
|
package/commands/autoland.js
CHANGED
|
@@ -379,6 +379,7 @@ function sweepLanding(root, { ttlDays, staleHours, now = Date.now() } = {}) {
|
|
|
379
379
|
remote: false,
|
|
380
380
|
includeDetached: false,
|
|
381
381
|
now,
|
|
382
|
+
by: 'atris autoland',
|
|
382
383
|
});
|
|
383
384
|
const afterBoard = collectBoard(root, { ttlDays, staleHours, now, light: true });
|
|
384
385
|
const human = [];
|
|
@@ -926,7 +927,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
926
927
|
}
|
|
927
928
|
try {
|
|
928
929
|
const { cleanupWorktrees } = require('./worktree');
|
|
929
|
-
const cleaned = cleanupWorktrees({ root, apply: true });
|
|
930
|
+
const cleaned = cleanupWorktrees({ root, apply: true, by: 'atris autoland' });
|
|
930
931
|
receipt.worktrees_reaped = cleaned.removed.length;
|
|
931
932
|
if (cleaned.removed.length > 0) receipt.worktrees_reaped_paths = cleaned.removed.map((w) => w.path);
|
|
932
933
|
} catch (err) {
|
package/commands/engine.js
CHANGED
|
@@ -38,7 +38,7 @@ const { teamRosterView } = require('../lib/member-engine');
|
|
|
38
38
|
const { engineModelText, engineRunsView, availableModels, modelLabel } = require('../lib/roster-models');
|
|
39
39
|
const { renderTable } = require('../lib/text-table');
|
|
40
40
|
const { TEAM_FOOTER } = require('../lib/team-lineup');
|
|
41
|
-
const { readRosterRuns, workerRuns, summarizeRuns, recentRuns, renderRunLine, DEFAULT_DAYS: RUN_DAYS } = require('../lib/roster-runs');
|
|
41
|
+
const { readRosterRuns, workerRuns, summarizeRuns, recentRuns, renderRunLine, runView, DEFAULT_DAYS: RUN_DAYS } = require('../lib/roster-runs');
|
|
42
42
|
const { attachSuggestions } = require('../lib/roster-suggest');
|
|
43
43
|
const { PIN_MAX_AGE_DAYS: PIN_STALE_DAYS, attachStaleModels, modelCatalog } = require('../lib/roster-stale');
|
|
44
44
|
const {
|
|
@@ -1507,7 +1507,7 @@ const RUNS_LIST_LIMIT = 20;
|
|
|
1507
1507
|
function runRosterRunsCommand(job, json, root, now) {
|
|
1508
1508
|
const runs = recentRuns(readRosterRuns(root, { now, days: RUNS_LIST_DAYS }), { job, limit: RUNS_LIST_LIMIT });
|
|
1509
1509
|
if (json) {
|
|
1510
|
-
console.log(JSON.stringify({ job: job || null, days: RUNS_LIST_DAYS, runs }, null, 2));
|
|
1510
|
+
console.log(JSON.stringify({ job: job || null, days: RUNS_LIST_DAYS, runs: runs.map(runView) }, null, 2));
|
|
1511
1511
|
return 0;
|
|
1512
1512
|
}
|
|
1513
1513
|
if (!runs.length) {
|
package/commands/goal.js
CHANGED
|
@@ -168,9 +168,65 @@ function renderGoalView(view) {
|
|
|
168
168
|
return lines.join('\n');
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// A goal that never becomes a mission run is a wish. `goal set` therefore also
|
|
172
|
+
// files the sentence through the normal wish capture path (capture only: no
|
|
173
|
+
// mission, no driver), so the goal -> wish -> mission -> run chain starts here.
|
|
174
|
+
// Wish capture is best-effort: it must never fail or undo the goal itself.
|
|
175
|
+
function wishNextStep(wish, goal) {
|
|
176
|
+
return wish.status === 'needs_input'
|
|
177
|
+
? 'atris wish answer "your words"'
|
|
178
|
+
: `atris wish again ${wish.id} "${goal}"`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function captureGoalWish(sentence, root) {
|
|
182
|
+
const goal = String(sentence || '').replace(/\s+/g, ' ').trim();
|
|
183
|
+
const { isFreshWorkspace } = require('../lib/first-minute');
|
|
184
|
+
if (isFreshWorkspace(root)) {
|
|
185
|
+
console.error('note: wish not filed, no atris/ workspace here. run: atris init');
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const previous = process.cwd();
|
|
189
|
+
let wish = null;
|
|
190
|
+
try {
|
|
191
|
+
if (previous !== root) process.chdir(root);
|
|
192
|
+
// Re-setting the same goal must not stack duplicate wishes.
|
|
193
|
+
const { readWishes } = require('../lib/wish-store');
|
|
194
|
+
const open = readWishes(root).find((w) => (
|
|
195
|
+
normalizeText(w.text) === normalizeText(goal)
|
|
196
|
+
&& ['captured', 'captured_no_mission', 'needs_input'].includes(w.status)
|
|
197
|
+
));
|
|
198
|
+
if (open) {
|
|
199
|
+
console.log(`wish already filed: ${open.id}`);
|
|
200
|
+
console.log(`next ${wishNextStep(open, goal)}`);
|
|
201
|
+
return open;
|
|
202
|
+
}
|
|
203
|
+
const { runCapturedWish } = require('../lib/wish-delegate');
|
|
204
|
+
runCapturedWish(goal, root, {
|
|
205
|
+
noMission: true,
|
|
206
|
+
afterCapture: (captured) => { wish = captured; },
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(`warning: goal set, but wish capture failed: ${normalizeText(error?.message || error)}`);
|
|
210
|
+
return null;
|
|
211
|
+
} finally {
|
|
212
|
+
if (process.cwd() !== previous) process.chdir(previous);
|
|
213
|
+
}
|
|
214
|
+
if (!wish) return null;
|
|
215
|
+
let latest = wish;
|
|
216
|
+
try {
|
|
217
|
+
const { readWishes } = require('../lib/wish-store');
|
|
218
|
+
latest = readWishes(root).find((w) => w.id === wish.id) || wish;
|
|
219
|
+
} catch {
|
|
220
|
+
latest = wish;
|
|
221
|
+
}
|
|
222
|
+
console.log(`wish id ${wish.id}`);
|
|
223
|
+
console.log(`next ${wishNextStep(latest, goal)}`);
|
|
224
|
+
return latest;
|
|
225
|
+
}
|
|
226
|
+
|
|
171
227
|
function printHelp() {
|
|
172
228
|
console.log('usage: atris goal [--json]');
|
|
173
|
-
console.log(' atris goal set "<sentence>"');
|
|
229
|
+
console.log(' atris goal set "<sentence>" [--no-wish]');
|
|
174
230
|
console.log(' atris goal metric <name> <value>');
|
|
175
231
|
console.log(' atris goal metric <name> --rm');
|
|
176
232
|
}
|
|
@@ -187,7 +243,9 @@ function run(args = [], context = {}) {
|
|
|
187
243
|
}
|
|
188
244
|
|
|
189
245
|
if (command === 'set') {
|
|
190
|
-
const
|
|
246
|
+
const noWish = args.includes('--no-wish');
|
|
247
|
+
const sentence = args.slice(1).filter((arg) => arg !== '--no-wish').join(' ');
|
|
248
|
+
const goal = normalizeText(sentence);
|
|
191
249
|
if (!goal) {
|
|
192
250
|
console.error('error: goal sentence is required');
|
|
193
251
|
return 2;
|
|
@@ -195,6 +253,7 @@ function run(args = [], context = {}) {
|
|
|
195
253
|
const scoreboard = readScoreboard(root);
|
|
196
254
|
writeScoreboard({ ...scoreboard, goal }, root);
|
|
197
255
|
console.log('goal set.');
|
|
256
|
+
if (!noWish) captureGoalWish(sentence, root);
|
|
198
257
|
return 0;
|
|
199
258
|
}
|
|
200
259
|
|
package/commands/land.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const gitSpawn = require('../lib/git-spawn');
|
|
4
4
|
const { compactErrorPayload, printCliJson } = require('../lib/cli-json');
|
|
5
|
-
const { listWorktrees, statusCounts } = require('./worktree');
|
|
5
|
+
const { listWorktrees, statusCounts, defaultMainlineBase, listActiveProcessCwds, worktreeHasActiveProcess, recordWorktreeRemoval } = require('./worktree');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_TTL_DAYS = 7;
|
|
8
8
|
const DEFAULT_STALE_HOURS = 48;
|
|
@@ -38,10 +38,11 @@ function refExists(root, ref) {
|
|
|
38
38
|
|
|
39
39
|
function baseBranch(root, override = '') {
|
|
40
40
|
if (override && refExists(root, override)) return override;
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const mainline = defaultMainlineBase(root);
|
|
42
|
+
if (mainline !== 'HEAD') return mainline;
|
|
43
|
+
for (const candidate of ['main', 'master']) {
|
|
44
|
+
if (refExists(root, candidate)) return candidate;
|
|
45
|
+
}
|
|
45
46
|
return '';
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -309,7 +310,7 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
309
310
|
const cherryCache = loadCherryCache(root);
|
|
310
311
|
const baseSha = resolveSha(root, base);
|
|
311
312
|
for (const branch of listBranches(root, base)) {
|
|
312
|
-
if (PROTECTED_BRANCHES.has(branch.name)) continue;
|
|
313
|
+
if (PROTECTED_BRANCHES.has(branch.name) || branch.name === base.replace(/^origin\//, '')) continue;
|
|
313
314
|
const lastCommitMs = Number(branch.lastCommitUnix) * 1000;
|
|
314
315
|
if (Number.isFinite(lastCommitMs)) lastCommitMsByBranch.set(branch.name, lastCommitMs);
|
|
315
316
|
const ahead = branch.ahead !== undefined ? branch.ahead : aheadCount(root, base, branch.name);
|
|
@@ -527,6 +528,15 @@ function salvageWorktree(w, dir, receipt) {
|
|
|
527
528
|
}
|
|
528
529
|
}
|
|
529
530
|
|
|
531
|
+
function reapReason(w, board, ttlDays, force) {
|
|
532
|
+
const state = (board.branches.find((b) => b.name === w.branch) || {}).state;
|
|
533
|
+
if (state === 'landed') return 'branch_landed';
|
|
534
|
+
if (state === 'due') return 'branch_past_ttl';
|
|
535
|
+
if (w.state === 'detached') return 'detached_head';
|
|
536
|
+
if (typeof w.ageDays === 'number' && w.ageDays > ttlDays) return 'worktree_past_ttl';
|
|
537
|
+
return force ? 'forced' : 'unlanded_commits';
|
|
538
|
+
}
|
|
539
|
+
|
|
530
540
|
// Reap: salvage then delete everything landed (residue) or past TTL.
|
|
531
541
|
// Salvage first, always: unlanded commits go into a git bundle, dirty
|
|
532
542
|
// worktrees into patch files + untracked-file copies, so a reap is never
|
|
@@ -534,7 +544,9 @@ function salvageWorktree(w, dir, receipt) {
|
|
|
534
544
|
// includeDetached: detached-HEAD worktrees have no branch, so the salvage
|
|
535
545
|
// bundle cannot cover their commits, unattended reaps (autoland) pass false
|
|
536
546
|
// and leave them for a human reap.
|
|
537
|
-
function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true, protectCurrent = true, force = false, now = Date.now() } = {}) {
|
|
547
|
+
function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true, protectCurrent = true, force = false, now = Date.now(), activeCwds: activeCwdsOverride, by = 'atris land --reap' } = {}) {
|
|
548
|
+
const readActiveCwds = () => (Array.isArray(activeCwdsOverride) ? activeCwdsOverride : listActiveProcessCwds());
|
|
549
|
+
let activeCwds = null;
|
|
538
550
|
const board = collectBoard(root, { ttlDays, staleHours, base: baseOverride, now });
|
|
539
551
|
const targets = board.branches.filter((b) => b.state === 'landed' || b.state === 'due');
|
|
540
552
|
const targetNames = new Set(targets.map((b) => b.name));
|
|
@@ -554,6 +566,13 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
|
|
|
554
566
|
protectedWorktrees.push({ ...w, reason: 'current_checkout' });
|
|
555
567
|
continue;
|
|
556
568
|
}
|
|
569
|
+
// A run working inside the folder outranks --force: removing it pulls
|
|
570
|
+
// the floor out from under a live engine.
|
|
571
|
+
if (!activeCwds) activeCwds = readActiveCwds();
|
|
572
|
+
if (worktreeHasActiveProcess(w.path, activeCwds)) {
|
|
573
|
+
protectedWorktrees.push({ ...w, reason: 'active_process' });
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
557
576
|
if (worktreeWithinReapGrace(w.path, now)) {
|
|
558
577
|
protectedWorktrees.push({ ...w, reason: 'fresh_worktree_grace' });
|
|
559
578
|
continue;
|
|
@@ -642,7 +661,14 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
|
|
|
642
661
|
const survivingWorktreeTargets = worktreeTargets.filter(
|
|
643
662
|
(w) => targetNames.has(w.branch) || (includeDetached && w.state === 'detached')
|
|
644
663
|
);
|
|
664
|
+
const activeAtRemoval = survivingWorktreeTargets.length ? readActiveCwds() : [];
|
|
645
665
|
for (const w of survivingWorktreeTargets) {
|
|
666
|
+
// A run can start in the folder while the bundle is built; look again.
|
|
667
|
+
if (worktreeHasActiveProcess(w.path, activeAtRemoval)) {
|
|
668
|
+
receipt.keptWorktrees.push(`${w.path} (active_process)`);
|
|
669
|
+
if (w.branch) targetNames.delete(w.branch);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
646
672
|
// The grace check ran at scan time, and a bundle build can sit between it
|
|
647
673
|
// and removal — an engine that booted mid-sweep refreshes the directory
|
|
648
674
|
// mtime in that gap. Stat once more at removal time, the cheapest place
|
|
@@ -665,6 +691,7 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
|
|
|
665
691
|
const removed = runGit(['worktree', 'remove', '--force', w.path], { cwd: root, check: false });
|
|
666
692
|
if (removed.status === 0) {
|
|
667
693
|
receipt.removedWorktrees.push(w.path);
|
|
694
|
+
recordWorktreeRemoval(root, { path: w.path, branch: w.branch, by, reason: reapReason(w, board, ttlDays, force) });
|
|
668
695
|
} else {
|
|
669
696
|
// A locked worktree fails here and, unreported, reap re-salvages the
|
|
670
697
|
// same patches every pass forever. Say what blocked it so a human can
|