atris 3.34.0 → 3.36.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 +35 -0
- package/FOR_AGENTS.md +5 -3
- 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 +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- 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/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- 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/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- 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/radar.js +259 -14
- 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/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -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 +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -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 +2431 -38
- 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/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -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/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -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 +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- 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
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/commands/worktree.js
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
|
+
const { stampLatestOpenBriefForWorktree } = require('../lib/brief-ledger');
|
|
7
|
+
|
|
8
|
+
const REGEN_ADAPTER_FILES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
|
|
9
|
+
const COMMAND_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
const GIT_OID_PATTERN = /^[0-9a-f]{40,64}$/;
|
|
11
|
+
const ONE_LAP_PROOF_REF_PATTERN = /^refs\/atris\/one-lap\/([0-9a-f]{40,64})$/;
|
|
6
12
|
|
|
7
13
|
function runGit(args, { cwd = process.cwd(), check = true } = {}) {
|
|
8
14
|
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
@@ -14,7 +20,14 @@ function runGit(args, { cwd = process.cwd(), check = true } = {}) {
|
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
function runCommand(command, { cwd = process.cwd(), check = true } = {}) {
|
|
17
|
-
const result = spawnSync(command, {
|
|
23
|
+
const result = spawnSync(command, {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
shell: true,
|
|
27
|
+
stdio: 'pipe',
|
|
28
|
+
maxBuffer: COMMAND_MAX_BUFFER_BYTES,
|
|
29
|
+
});
|
|
30
|
+
if (result.error) throw result.error;
|
|
18
31
|
if (check && result.status !== 0) {
|
|
19
32
|
const msg = (result.stderr || result.stdout || `${command} failed`).trim();
|
|
20
33
|
throw new Error(msg);
|
|
@@ -22,6 +35,67 @@ function runCommand(command, { cwd = process.cwd(), check = true } = {}) {
|
|
|
22
35
|
return result;
|
|
23
36
|
}
|
|
24
37
|
|
|
38
|
+
function statusSnapshot(root) {
|
|
39
|
+
const result = runGit(['status', '--porcelain=v1', '--untracked-files=all'], { cwd: root, check: false });
|
|
40
|
+
if (result.status !== 0) return [];
|
|
41
|
+
return result.stdout
|
|
42
|
+
.split(/\r?\n/)
|
|
43
|
+
.map((line) => line.trimEnd())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function newStatusRows(before, after) {
|
|
48
|
+
const seen = new Set(before);
|
|
49
|
+
return after.filter((row) => !seen.has(row));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readOneLapProofBinding(root, branch) {
|
|
53
|
+
const sidecarPath = path.join(root, '.atris', 'agent-worktree.json');
|
|
54
|
+
let sidecar = null;
|
|
55
|
+
try {
|
|
56
|
+
sidecar = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error && error.code !== 'ENOENT') throw new Error('one-lap proof metadata is unreadable');
|
|
59
|
+
}
|
|
60
|
+
const metadata = sidecar && sidecar.one_lap_proof;
|
|
61
|
+
const configured = runGit(['config', '--get', `branch.${branch}.atris-proof-ref`], { cwd: root, check: false });
|
|
62
|
+
const configuredRef = configured.status === 0 ? String(configured.stdout || '').trim() : '';
|
|
63
|
+
if (!metadata && !configuredRef) return null;
|
|
64
|
+
if (!metadata || !configuredRef) throw new Error('one-lap proof binding is incomplete');
|
|
65
|
+
|
|
66
|
+
const proofRef = String(metadata.ref || '');
|
|
67
|
+
const sourceCommit = String(metadata.commit || '');
|
|
68
|
+
const sourceTree = String(metadata.tree || '');
|
|
69
|
+
const refMatch = proofRef.match(ONE_LAP_PROOF_REF_PATTERN);
|
|
70
|
+
if (!refMatch || proofRef !== configuredRef || !GIT_OID_PATTERN.test(sourceCommit) || !GIT_OID_PATTERN.test(sourceTree)) {
|
|
71
|
+
throw new Error('one-lap proof binding is invalid');
|
|
72
|
+
}
|
|
73
|
+
const resolvedCommit = runGit(['rev-parse', '--verify', `${proofRef}^{commit}`], { cwd: root, check: false });
|
|
74
|
+
const resolvedTree = runGit(['rev-parse', '--verify', `${proofRef}^{tree}`], { cwd: root, check: false });
|
|
75
|
+
if (resolvedCommit.status !== 0 || String(resolvedCommit.stdout || '').trim() !== sourceCommit || refMatch[1] !== sourceCommit) {
|
|
76
|
+
throw new Error('one-lap proof commit no longer matches its retained ref');
|
|
77
|
+
}
|
|
78
|
+
if (resolvedTree.status !== 0 || String(resolvedTree.stdout || '').trim() !== sourceTree) {
|
|
79
|
+
throw new Error('one-lap proof tree no longer matches its retained ref');
|
|
80
|
+
}
|
|
81
|
+
return { ref: proofRef, commit: sourceCommit, tree: sourceTree };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function worktreeTree(root, source) {
|
|
85
|
+
const result = runGit(source === 'index'
|
|
86
|
+
? ['write-tree']
|
|
87
|
+
: ['rev-parse', '--verify', 'HEAD^{tree}'], { cwd: root, check: false });
|
|
88
|
+
if (result.status !== 0) throw new Error(`could not resolve the ${source} tree for one-lap proof`);
|
|
89
|
+
return String(result.stdout || '').trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertOneLapProofTree(root, binding, source) {
|
|
93
|
+
if (!binding) return;
|
|
94
|
+
if (worktreeTree(root, source) !== binding.tree) {
|
|
95
|
+
throw new Error(`one-lap ${source} tree differs from the independently verified proof tree`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
25
99
|
function repoRoot(cwd = process.cwd()) {
|
|
26
100
|
return runGit(['rev-parse', '--show-toplevel'], { cwd }).stdout.trim();
|
|
27
101
|
}
|
|
@@ -89,6 +163,25 @@ function currentBranch(root = repoRoot()) {
|
|
|
89
163
|
return runGit(['branch', '--show-current'], { cwd: root, check: false }).stdout.trim();
|
|
90
164
|
}
|
|
91
165
|
|
|
166
|
+
function detachedShipBranch(root) {
|
|
167
|
+
return `codex/${path.basename(root)}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function createDetachedShipBranch(root, branch) {
|
|
171
|
+
if (refExists(root, `refs/heads/${branch}`) || refExists(root, `refs/remotes/origin/${branch}`)) {
|
|
172
|
+
console.error(`blocked: detached head branch already exists: ${branch}`);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const created = runGit(['switch', '-c', branch], { cwd: root, check: false });
|
|
176
|
+
if (created.status !== 0) {
|
|
177
|
+
console.error(`blocked: detached head branch create failed: ${branch}`);
|
|
178
|
+
console.error((created.stderr || created.stdout || '').trim());
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
console.log(`ship: detached head, created branch ${branch}`);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
92
185
|
function currentUpstream(root) {
|
|
93
186
|
const result = runGit(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], { cwd: root, check: false });
|
|
94
187
|
return result.status === 0 ? result.stdout.trim() : '';
|
|
@@ -118,9 +211,7 @@ function normalizeTargetRef(root, target) {
|
|
|
118
211
|
return value;
|
|
119
212
|
}
|
|
120
213
|
|
|
121
|
-
function
|
|
122
|
-
const upstream = currentUpstream(root);
|
|
123
|
-
if (upstream && refExists(root, upstream)) return upstream;
|
|
214
|
+
function defaultMainlineBase(root) {
|
|
124
215
|
const head = remoteHead(root);
|
|
125
216
|
if (head && refExists(root, head)) return head;
|
|
126
217
|
for (const candidate of ['origin/master', 'origin/main']) {
|
|
@@ -129,6 +220,23 @@ function defaultStartBase(root) {
|
|
|
129
220
|
return 'HEAD';
|
|
130
221
|
}
|
|
131
222
|
|
|
223
|
+
function defaultStartBase(root) {
|
|
224
|
+
const upstream = currentUpstream(root);
|
|
225
|
+
const mainline = defaultMainlineBase(root);
|
|
226
|
+
// Always refresh before cutting a worktree: a stale origin/master base sent
|
|
227
|
+
// an engine off to rebuild work that had already landed (2026-07-09, packs
|
|
228
|
+
// one-click flight rebuilt the stars backend from a base two PRs behind).
|
|
229
|
+
refreshRemoteRef(root, mainline);
|
|
230
|
+
if (!upstream || !refExists(root, upstream)) return mainline;
|
|
231
|
+
if (mainline === 'HEAD' || mainline === upstream) return upstream;
|
|
232
|
+
// A launcher upstream that is behind the mainline is a stale cut point
|
|
233
|
+
// (2026-07-05: a stale origin/task branch sent two flights into rebase
|
|
234
|
+
// conflicts). Start from the mainline unless the upstream carries new work.
|
|
235
|
+
refreshRemoteRef(root, upstream);
|
|
236
|
+
const behind = runGit(['merge-base', '--is-ancestor', upstream, mainline], { cwd: root, check: false });
|
|
237
|
+
return behind.status === 0 ? mainline : upstream;
|
|
238
|
+
}
|
|
239
|
+
|
|
132
240
|
function defaultShipTarget(root) {
|
|
133
241
|
const branch = currentBranch(root);
|
|
134
242
|
if (branch) {
|
|
@@ -153,15 +261,21 @@ function baseBranchName(ref) {
|
|
|
153
261
|
return String(ref || '').replace(/^refs\/heads\//, '').replace(/^origin\//, '');
|
|
154
262
|
}
|
|
155
263
|
|
|
156
|
-
function statusCounts(root) {
|
|
264
|
+
function statusCounts(root, { ignoredUnstagedFiles = new Set(), ignoredUntrackedFiles = new Set() } = {}) {
|
|
157
265
|
if (!fs.existsSync(root)) return null;
|
|
158
|
-
|
|
266
|
+
// -uall expands untracked directories to individual files so ignoredUntrackedFiles
|
|
267
|
+
// can match exact paths (plain porcelain collapses them to "?? dir/").
|
|
268
|
+
const statusArgs = ignoredUntrackedFiles.size ? ['status', '--porcelain', '-uall'] : ['status', '--porcelain'];
|
|
269
|
+
const result = runGit(statusArgs, { cwd: root, check: false });
|
|
159
270
|
if (result.status !== 0) return null;
|
|
160
271
|
let staged = 0;
|
|
161
272
|
let unstaged = 0;
|
|
162
273
|
let untracked = 0;
|
|
163
274
|
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
275
|
+
const file = line.slice(3);
|
|
276
|
+
if (ignoredUnstagedFiles.has(file) && line[0] === ' ' && line[1] !== ' ') continue;
|
|
164
277
|
if (line.startsWith('??')) {
|
|
278
|
+
if (ignoredUntrackedFiles.has(file)) continue;
|
|
165
279
|
untracked += 1;
|
|
166
280
|
continue;
|
|
167
281
|
}
|
|
@@ -171,6 +285,25 @@ function statusCounts(root) {
|
|
|
171
285
|
return { staged, unstaged, untracked };
|
|
172
286
|
}
|
|
173
287
|
|
|
288
|
+
function changedFiles(root, args) {
|
|
289
|
+
const result = runGit([...args, '--', ...REGEN_ADAPTER_FILES], { cwd: root, check: false });
|
|
290
|
+
if (result.status !== 0) return new Set();
|
|
291
|
+
return new Set(result.stdout.split(/\r?\n/).filter(Boolean));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function restoreRegeneratedAdapterChurn(root, message, { dryRun = false } = {}) {
|
|
295
|
+
const unstaged = changedFiles(root, ['diff', '--name-only']);
|
|
296
|
+
const staged = changedFiles(root, ['diff', '--cached', '--name-only']);
|
|
297
|
+
const commitMessage = String(message || '');
|
|
298
|
+
const skipped = REGEN_ADAPTER_FILES.filter(
|
|
299
|
+
(file) => unstaged.has(file) && !staged.has(file) && !commitMessage.includes(file)
|
|
300
|
+
);
|
|
301
|
+
if (!skipped.length) return [];
|
|
302
|
+
console.log(`ship: skipped regenerated adapter churn: ${skipped.join(', ')}`);
|
|
303
|
+
if (!dryRun) runGit(['checkout', '--', ...skipped], { cwd: root });
|
|
304
|
+
return skipped;
|
|
305
|
+
}
|
|
306
|
+
|
|
174
307
|
function readFlag(args, name, fallback = '') {
|
|
175
308
|
const prefix = `${name}=`;
|
|
176
309
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -215,17 +348,34 @@ function printStatus() {
|
|
|
215
348
|
// Programmatic core shared by `atris worktree start` and `atris mission start
|
|
216
349
|
// --worktree`: creates the branch + isolated checkout + identity sidecar and
|
|
217
350
|
// returns the facts. Throws on failure; callers own messaging and next steps.
|
|
351
|
+
//
|
|
352
|
+
// checkoutBase (what the branch is cut from) and shipBase (what `atris
|
|
353
|
+
// worktree ship` targets, recorded as branch.<branch>.atris-base) are kept
|
|
354
|
+
// separate on purpose. checkoutBase defaults to the launcher's own
|
|
355
|
+
// upstream/default remote base, so the agent starts from current work.
|
|
356
|
+
// shipBase defaults to origin/master for agent/member worktrees regardless of
|
|
357
|
+
// what branch the launcher happened to have checked out. recording the
|
|
358
|
+
// launcher's own feature branch as the ship target false-landed two PRs on
|
|
359
|
+
// 2026-07-04 (ship merged into the launcher's branch, never reached master).
|
|
360
|
+
// --base/--target still overrides both when the caller wants a different
|
|
361
|
+
// checkout point and ship target on purpose.
|
|
218
362
|
function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task, branch: branchOverride, path: pathOverride, base: baseOverride, now = new Date() } = {}) {
|
|
219
363
|
const owner = member || agent;
|
|
220
364
|
if (!owner || !task) throw new Error('createAgentWorktree: owner (member/agent) and task required');
|
|
221
365
|
const branch = branchOverride || branchName(owner, task, now);
|
|
222
366
|
const target = path.resolve(pathOverride || defaultWorktreePath(root, owner, task, now));
|
|
223
|
-
const
|
|
367
|
+
const explicitBase = Boolean(baseOverride);
|
|
368
|
+
// Agent/member worktrees always cut from the mainline: honoring the
|
|
369
|
+
// launcher checkout's upstream let a dispatch inherit another session's
|
|
370
|
+
// in-progress feature branch (2026-07-09: an auth fix arrived carrying 13
|
|
371
|
+
// unpushed commits from a parallel session and could not merge).
|
|
372
|
+
const checkoutBase = normalizeTargetRef(root, baseOverride || defaultMainlineBase(root));
|
|
373
|
+
const shipBase = explicitBase ? checkoutBase : normalizeTargetRef(root, 'origin/master');
|
|
224
374
|
if (fs.existsSync(target)) throw new Error(`worktree path already exists: ${target}`);
|
|
225
375
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
226
|
-
refreshRemoteRef(root,
|
|
227
|
-
runGit(['worktree', 'add', '-b', branch, target,
|
|
228
|
-
runGit(['config', `branch.${branch}.atris-base`,
|
|
376
|
+
refreshRemoteRef(root, checkoutBase);
|
|
377
|
+
runGit(['worktree', 'add', '-b', branch, target, checkoutBase], { cwd: root });
|
|
378
|
+
runGit(['config', `branch.${branch}.atris-base`, shipBase], { cwd: target, check: false });
|
|
229
379
|
runGit(['config', `branch.${branch}.atris-owner`, owner], { cwd: target, check: false });
|
|
230
380
|
runGit(['config', `branch.${branch}.atris-task`, task], { cwd: target, check: false });
|
|
231
381
|
fs.mkdirSync(path.join(target, '.atris'), { recursive: true });
|
|
@@ -237,13 +387,14 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
237
387
|
owner,
|
|
238
388
|
task,
|
|
239
389
|
branch,
|
|
240
|
-
base,
|
|
390
|
+
base: shipBase,
|
|
391
|
+
checkout_base: checkoutBase,
|
|
241
392
|
workspace_root: root,
|
|
242
393
|
created_at: now.toISOString(),
|
|
243
394
|
}, null, 2) + '\n',
|
|
244
395
|
'utf8'
|
|
245
396
|
);
|
|
246
|
-
return { path: target, branch, base, owner };
|
|
397
|
+
return { path: target, branch, base: shipBase, checkoutBase, owner };
|
|
247
398
|
}
|
|
248
399
|
|
|
249
400
|
function startWorktree(args) {
|
|
@@ -298,12 +449,21 @@ function startWorktree(args) {
|
|
|
298
449
|
|
|
299
450
|
function createOrFindPr(root, branch, targetRef, title, dryRun) {
|
|
300
451
|
const targetBranch = baseBranchName(targetRef);
|
|
301
|
-
|
|
452
|
+
// `gh pr view` (no --state filter) returns ANY PR for this branch name,
|
|
453
|
+
// including one already MERGED or CLOSED. A worktree branch that ships
|
|
454
|
+
// more than once (this same branch, more commits, another `worktree ship`)
|
|
455
|
+
// would then reuse the dead PR number: `gh pr merge <dead pr> --merge`
|
|
456
|
+
// exits 0 with just a warning (never fails), so the new commits silently
|
|
457
|
+
// never land while the ship command still reports success. Only reuse a
|
|
458
|
+
// PR that is still OPEN; anything else gets a fresh one.
|
|
459
|
+
const existing = spawnSync('gh', ['pr', 'view', '--json', 'number,url,state', '--jq', '"\\(.number) \\(.url) \\(.state)"'], {
|
|
302
460
|
cwd: root,
|
|
303
461
|
encoding: 'utf8',
|
|
304
462
|
});
|
|
305
463
|
if (existing.status === 0 && existing.stdout.trim()) {
|
|
306
|
-
|
|
464
|
+
const parts = existing.stdout.trim().split(/\s+/);
|
|
465
|
+
const state = parts.pop();
|
|
466
|
+
if (state === 'OPEN') return parts.join(' ');
|
|
307
467
|
}
|
|
308
468
|
const body = [
|
|
309
469
|
'Automated Atris worktree ship.',
|
|
@@ -329,19 +489,41 @@ function prMergeRef(prOutput) {
|
|
|
329
489
|
return text.split(/\s+/)[0];
|
|
330
490
|
}
|
|
331
491
|
|
|
492
|
+
function shipHelp() {
|
|
493
|
+
console.log('Usage: atris worktree ship --message "<commit>" --verify "<cmd>" [--merge] [--target <ref>] [--local]');
|
|
494
|
+
console.log('');
|
|
495
|
+
console.log(' --target <ref> override the default landing target (default: branch atris-base, else origin default branch)');
|
|
496
|
+
console.log(' --local merge into the local primary checkout instead of pushing and opening a PR');
|
|
497
|
+
console.log(' unstaged regenerated adapter files are skipped unless staged first or named in --message');
|
|
498
|
+
console.log(' recommended flight verify: npm run test:fast && node --test <focused files>');
|
|
499
|
+
}
|
|
500
|
+
|
|
332
501
|
function shipWorktree(args) {
|
|
502
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
|
|
503
|
+
shipHelp();
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
333
506
|
const root = repoRoot();
|
|
334
507
|
const dryRun = hasFlag(args, '--dry-run');
|
|
335
508
|
const noPr = hasFlag(args, '--no-pr');
|
|
509
|
+
const localMode = hasFlag(args, '--local') || hasFlag(args, '--local-merge');
|
|
336
510
|
const merge = hasFlag(args, '--merge');
|
|
337
511
|
const message = readFlag(args, '--message') || readFlag(args, '-m');
|
|
338
512
|
const verify = readFlag(args, '--verify');
|
|
339
|
-
|
|
513
|
+
let branch = currentBranch(root);
|
|
340
514
|
const worktrees = listWorktrees(root);
|
|
341
515
|
const primary = worktrees[0]?.path;
|
|
342
516
|
const targetRef = normalizeTargetRef(root, readFlag(args, '--target') || defaultShipTarget(root));
|
|
343
517
|
|
|
344
|
-
if (!branch
|
|
518
|
+
if (!branch && path.resolve(root) === path.resolve(primary)) {
|
|
519
|
+
console.error('blocked: detached head on the primary checkout');
|
|
520
|
+
return 2;
|
|
521
|
+
}
|
|
522
|
+
if (!branch) {
|
|
523
|
+
branch = detachedShipBranch(root);
|
|
524
|
+
if (!createDetachedShipBranch(root, branch)) return 2;
|
|
525
|
+
}
|
|
526
|
+
if (branch === 'master' || branch === 'main') {
|
|
345
527
|
console.error('blocked: ship from a feature worktree branch, not master/main');
|
|
346
528
|
return 2;
|
|
347
529
|
}
|
|
@@ -350,8 +532,27 @@ function shipWorktree(args) {
|
|
|
350
532
|
return 2;
|
|
351
533
|
}
|
|
352
534
|
|
|
535
|
+
let oneLapProof = null;
|
|
536
|
+
try {
|
|
537
|
+
oneLapProof = readOneLapProofBinding(root, branch);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.error(`blocked: ${error.message}`);
|
|
540
|
+
return 3;
|
|
541
|
+
}
|
|
542
|
+
const proofTreeMatches = (source) => {
|
|
543
|
+
try {
|
|
544
|
+
assertOneLapProofTree(root, oneLapProof, source);
|
|
545
|
+
return true;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.error(`blocked: ${error.message}`);
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
353
552
|
refreshRemoteRef(root, targetRef);
|
|
354
|
-
const
|
|
553
|
+
const skippedAdapters = restoreRegeneratedAdapterChurn(root, message, { dryRun });
|
|
554
|
+
const ignoredUnstagedFiles = dryRun ? new Set(skippedAdapters) : new Set();
|
|
555
|
+
const counts = statusCounts(root, { ignoredUnstagedFiles }) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
355
556
|
const dirty = counts.staged || counts.unstaged || counts.untracked;
|
|
356
557
|
if (dirty && !message) {
|
|
357
558
|
console.error('blocked: --message is required when there are local changes to commit');
|
|
@@ -362,23 +563,33 @@ function shipWorktree(args) {
|
|
|
362
563
|
console.log(`commit: ${message}`);
|
|
363
564
|
if (!dryRun) {
|
|
364
565
|
runGit(['add', '-A'], { cwd: root });
|
|
566
|
+
if (!proofTreeMatches('index')) return 3;
|
|
365
567
|
runGit(['commit', '-m', message], { cwd: root });
|
|
568
|
+
if (!proofTreeMatches('head')) return 3;
|
|
366
569
|
}
|
|
367
570
|
} else {
|
|
368
571
|
console.log('commit: skipped (no local changes)');
|
|
572
|
+
if (!dryRun && !proofTreeMatches('head')) return 3;
|
|
369
573
|
}
|
|
370
574
|
|
|
371
575
|
if (verify) {
|
|
576
|
+
const beforeVerify = dryRun ? [] : statusSnapshot(root);
|
|
372
577
|
console.log(`verify: ${verify}`);
|
|
373
578
|
if (!dryRun) runCommand(verify, { cwd: root });
|
|
579
|
+
const afterVerifyRows = dryRun ? [] : statusSnapshot(root);
|
|
580
|
+
const verifyDelta = newStatusRows(beforeVerify, afterVerifyRows);
|
|
374
581
|
const afterVerify = dryRun ? { staged: 0, unstaged: 0, untracked: 0 } : statusCounts(root) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
375
582
|
if (!dryRun && (afterVerify.staged || afterVerify.unstaged || afterVerify.untracked)) {
|
|
376
583
|
console.error(
|
|
377
584
|
`blocked: verifier left checkout dirty staged=${afterVerify.staged} ` +
|
|
378
585
|
`unstaged=${afterVerify.unstaged} untracked=${afterVerify.untracked}`
|
|
379
586
|
);
|
|
587
|
+
if (verifyDelta.length) {
|
|
588
|
+
console.error(`new after verify: ${verifyDelta.slice(0, 8).join(', ')}`);
|
|
589
|
+
}
|
|
380
590
|
return 3;
|
|
381
591
|
}
|
|
592
|
+
if (!dryRun && !proofTreeMatches('head')) return 3;
|
|
382
593
|
} else {
|
|
383
594
|
console.log('verify: skipped (no --verify)');
|
|
384
595
|
}
|
|
@@ -393,6 +604,35 @@ function shipWorktree(args) {
|
|
|
393
604
|
}
|
|
394
605
|
console.log(`merge_check: ${targetRef} clean`);
|
|
395
606
|
|
|
607
|
+
if (localMode) {
|
|
608
|
+
console.log('push: skipped (local mode)');
|
|
609
|
+
if (merge) {
|
|
610
|
+
console.log('merge: requested (local mode)');
|
|
611
|
+
if (!dryRun) {
|
|
612
|
+
const targetBranch = baseBranchName(targetRef);
|
|
613
|
+
const primaryBranch = currentBranch(primary);
|
|
614
|
+
if (primaryBranch !== targetBranch) {
|
|
615
|
+
const switched = runGit(['switch', targetBranch], { cwd: primary, check: false });
|
|
616
|
+
if (switched.status !== 0) {
|
|
617
|
+
throw new Error((switched.stderr || switched.stdout || `git switch ${targetBranch} failed`).trim());
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const primaryCounts = statusCounts(primary) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
621
|
+
if (primaryCounts.staged || primaryCounts.unstaged || primaryCounts.untracked) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`primary checkout dirty staged=${primaryCounts.staged} ` +
|
|
624
|
+
`unstaged=${primaryCounts.unstaged} untracked=${primaryCounts.untracked}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
runGit(['merge', '--no-ff', branch, '-m', `Merge ${branch}`], { cwd: primary });
|
|
628
|
+
console.log('merge: merged (local mode)');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
console.log('pr: skipped (local mode)');
|
|
632
|
+
console.log('done: worktree shipped');
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
635
|
+
|
|
396
636
|
console.log(`push: origin ${branch}`);
|
|
397
637
|
if (!dryRun) runGit(['push', '-u', 'origin', branch], { cwd: root });
|
|
398
638
|
|
|
@@ -409,6 +649,11 @@ function shipWorktree(args) {
|
|
|
409
649
|
mergeArgs.push('--merge');
|
|
410
650
|
const merged = spawnSync('gh', mergeArgs, { cwd: root, encoding: 'utf8' });
|
|
411
651
|
if (merged.status !== 0) throw new Error((merged.stderr || merged.stdout || 'gh pr merge failed').trim());
|
|
652
|
+
// `gh pr merge` on an already-merged PR exits 0 with a warning
|
|
653
|
+
// instead of failing, which would otherwise report a false landing.
|
|
654
|
+
if (/already merged/i.test(`${merged.stdout}${merged.stderr}`)) {
|
|
655
|
+
throw new Error(`gh pr merge reported the PR was already merged (stale PR reused): ${(merged.stdout || merged.stderr).trim()}`);
|
|
656
|
+
}
|
|
412
657
|
console.log('merge: merged');
|
|
413
658
|
const deleted = runGit(['push', 'origin', '--delete', branch], { cwd: root, check: false });
|
|
414
659
|
if (deleted.status === 0) {
|
|
@@ -423,6 +668,12 @@ function shipWorktree(args) {
|
|
|
423
668
|
console.log('pr: skipped (--no-pr)');
|
|
424
669
|
}
|
|
425
670
|
|
|
671
|
+
try {
|
|
672
|
+
stampLatestOpenBriefForWorktree(root, {
|
|
673
|
+
result: 'pass',
|
|
674
|
+
note: `worktree ship completed into ${targetRef}`,
|
|
675
|
+
}, { worktree: root });
|
|
676
|
+
} catch {}
|
|
426
677
|
console.log('done: worktree shipped');
|
|
427
678
|
return 0;
|
|
428
679
|
}
|
|
@@ -436,7 +687,8 @@ function guard(args) {
|
|
|
436
687
|
console.error('run: atris worktree start --member <member>|--agent <name> --task "<short task>" --claim');
|
|
437
688
|
return 2;
|
|
438
689
|
}
|
|
439
|
-
|
|
690
|
+
// The CLI's own worktree metadata (written by `worktree start`) must not count as dirt.
|
|
691
|
+
const counts = statusCounts(root, { ignoredUntrackedFiles: new Set(['.atris/agent-worktree.json']) });
|
|
440
692
|
if (counts && (counts.staged || counts.unstaged || counts.untracked) && !hasFlag(args, '--allow-dirty')) {
|
|
441
693
|
console.error(`blocked: checkout is dirty staged=${counts.staged} unstaged=${counts.unstaged} untracked=${counts.untracked}`);
|
|
442
694
|
return 3;
|
|
@@ -456,6 +708,19 @@ function prune(args) {
|
|
|
456
708
|
|
|
457
709
|
const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
458
710
|
|
|
711
|
+
// A worktree fresh off `worktree start` is merged into base by definition
|
|
712
|
+
// (zero commits yet), so without a grace window the janitor reaps it while
|
|
713
|
+
// the engine that requested it is still booting inside.
|
|
714
|
+
const WORKTREE_REAP_GRACE_MS = 60 * 60 * 1000;
|
|
715
|
+
|
|
716
|
+
function worktreeWithinReapGrace(worktreePath, now = Date.now()) {
|
|
717
|
+
try {
|
|
718
|
+
return now - fs.statSync(worktreePath).mtimeMs < WORKTREE_REAP_GRACE_MS;
|
|
719
|
+
} catch {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
459
724
|
function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply = false } = {}) {
|
|
460
725
|
const worktrees = listWorktrees(root);
|
|
461
726
|
const primary = worktrees[0]?.path ? path.resolve(worktrees[0].path) : '';
|
|
@@ -503,6 +768,10 @@ function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply =
|
|
|
503
768
|
kept.push({ ...item, reason: 'unmerged' });
|
|
504
769
|
continue;
|
|
505
770
|
}
|
|
771
|
+
if (worktreeWithinReapGrace(wtPath)) {
|
|
772
|
+
kept.push({ ...item, reason: 'fresh_worktree_grace' });
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
506
775
|
const candidate = { ...item, reason: 'merged_into_base' };
|
|
507
776
|
candidates.push(candidate);
|
|
508
777
|
if (!apply) continue;
|
|
@@ -560,7 +829,9 @@ function guide() {
|
|
|
560
829
|
console.log(' atris mission tick <id> --verify --complete-on-pass');
|
|
561
830
|
console.log('');
|
|
562
831
|
console.log('4. Ship only from the isolated worktree:');
|
|
563
|
-
console.log(' atris worktree ship --message "<commit>" --verify "<cmd>" --merge');
|
|
832
|
+
console.log(' atris worktree ship --message "<commit>" --verify "<cmd>" --merge [--target <ref>]');
|
|
833
|
+
console.log(' --target <ref> overrides the default landing target (default: branch atris-base, else origin default branch)');
|
|
834
|
+
console.log(' recommended verify: npm run test:fast && node --test <focused files>');
|
|
564
835
|
console.log('');
|
|
565
836
|
console.log('5. Clean merged worktrees:');
|
|
566
837
|
console.log(' atris worktree cleanup');
|
|
@@ -576,7 +847,10 @@ function help() {
|
|
|
576
847
|
console.log('');
|
|
577
848
|
console.log(' atris worktree guide');
|
|
578
849
|
console.log(' atris worktree start --member <member>|--agent <name> --task "<task>" [--claim]');
|
|
579
|
-
console.log(' atris worktree ship --message "<commit>" --verify "<cmd>" [--merge]');
|
|
850
|
+
console.log(' atris worktree ship --message "<commit>" --verify "<cmd>" [--merge] [--target <ref>] [--local]');
|
|
851
|
+
console.log(' --target <ref> override the default landing target (default: branch atris-base, else origin default branch)');
|
|
852
|
+
console.log(' --local merge into the local primary checkout instead of pushing and opening a PR');
|
|
853
|
+
console.log(' recommended verify: npm run test:fast && node --test <focused files>');
|
|
580
854
|
console.log(' atris worktree status');
|
|
581
855
|
console.log(' atris worktree guard [--allow-primary] [--allow-dirty]');
|
|
582
856
|
console.log(' atris worktree prune [--apply]');
|
|
@@ -606,14 +880,19 @@ function worktreeCommand(args = []) {
|
|
|
606
880
|
|
|
607
881
|
module.exports = {
|
|
608
882
|
branchName,
|
|
883
|
+
statusSnapshot,
|
|
884
|
+
newStatusRows,
|
|
609
885
|
createAgentWorktree,
|
|
886
|
+
createOrFindPr,
|
|
610
887
|
cleanupWorktrees,
|
|
611
888
|
defaultShipTarget,
|
|
612
889
|
defaultStartBase,
|
|
613
890
|
defaultWorktreePath,
|
|
614
891
|
listWorktrees,
|
|
615
892
|
parseWorktrees,
|
|
893
|
+
REGEN_ADAPTER_FILES,
|
|
616
894
|
normalizeTargetRef,
|
|
895
|
+
restoreRegeneratedAdapterChurn,
|
|
617
896
|
prMergeRef,
|
|
618
897
|
slugify,
|
|
619
898
|
statusCounts,
|