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
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) {
|
|
@@ -232,7 +365,11 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
232
365
|
const branch = branchOverride || branchName(owner, task, now);
|
|
233
366
|
const target = path.resolve(pathOverride || defaultWorktreePath(root, owner, task, now));
|
|
234
367
|
const explicitBase = Boolean(baseOverride);
|
|
235
|
-
|
|
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));
|
|
236
373
|
const shipBase = explicitBase ? checkoutBase : normalizeTargetRef(root, 'origin/master');
|
|
237
374
|
if (fs.existsSync(target)) throw new Error(`worktree path already exists: ${target}`);
|
|
238
375
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -353,9 +490,12 @@ function prMergeRef(prOutput) {
|
|
|
353
490
|
}
|
|
354
491
|
|
|
355
492
|
function shipHelp() {
|
|
356
|
-
console.log('Usage: atris worktree ship --message "<commit>" --verify "<cmd>" [--merge] [--target <ref>]');
|
|
493
|
+
console.log('Usage: atris worktree ship --message "<commit>" --verify "<cmd>" [--merge] [--target <ref>] [--local]');
|
|
357
494
|
console.log('');
|
|
358
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>');
|
|
359
499
|
}
|
|
360
500
|
|
|
361
501
|
function shipWorktree(args) {
|
|
@@ -366,15 +506,24 @@ function shipWorktree(args) {
|
|
|
366
506
|
const root = repoRoot();
|
|
367
507
|
const dryRun = hasFlag(args, '--dry-run');
|
|
368
508
|
const noPr = hasFlag(args, '--no-pr');
|
|
509
|
+
const localMode = hasFlag(args, '--local') || hasFlag(args, '--local-merge');
|
|
369
510
|
const merge = hasFlag(args, '--merge');
|
|
370
511
|
const message = readFlag(args, '--message') || readFlag(args, '-m');
|
|
371
512
|
const verify = readFlag(args, '--verify');
|
|
372
|
-
|
|
513
|
+
let branch = currentBranch(root);
|
|
373
514
|
const worktrees = listWorktrees(root);
|
|
374
515
|
const primary = worktrees[0]?.path;
|
|
375
516
|
const targetRef = normalizeTargetRef(root, readFlag(args, '--target') || defaultShipTarget(root));
|
|
376
517
|
|
|
377
|
-
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') {
|
|
378
527
|
console.error('blocked: ship from a feature worktree branch, not master/main');
|
|
379
528
|
return 2;
|
|
380
529
|
}
|
|
@@ -383,8 +532,27 @@ function shipWorktree(args) {
|
|
|
383
532
|
return 2;
|
|
384
533
|
}
|
|
385
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
|
+
|
|
386
552
|
refreshRemoteRef(root, targetRef);
|
|
387
|
-
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 };
|
|
388
556
|
const dirty = counts.staged || counts.unstaged || counts.untracked;
|
|
389
557
|
if (dirty && !message) {
|
|
390
558
|
console.error('blocked: --message is required when there are local changes to commit');
|
|
@@ -395,23 +563,33 @@ function shipWorktree(args) {
|
|
|
395
563
|
console.log(`commit: ${message}`);
|
|
396
564
|
if (!dryRun) {
|
|
397
565
|
runGit(['add', '-A'], { cwd: root });
|
|
566
|
+
if (!proofTreeMatches('index')) return 3;
|
|
398
567
|
runGit(['commit', '-m', message], { cwd: root });
|
|
568
|
+
if (!proofTreeMatches('head')) return 3;
|
|
399
569
|
}
|
|
400
570
|
} else {
|
|
401
571
|
console.log('commit: skipped (no local changes)');
|
|
572
|
+
if (!dryRun && !proofTreeMatches('head')) return 3;
|
|
402
573
|
}
|
|
403
574
|
|
|
404
575
|
if (verify) {
|
|
576
|
+
const beforeVerify = dryRun ? [] : statusSnapshot(root);
|
|
405
577
|
console.log(`verify: ${verify}`);
|
|
406
578
|
if (!dryRun) runCommand(verify, { cwd: root });
|
|
579
|
+
const afterVerifyRows = dryRun ? [] : statusSnapshot(root);
|
|
580
|
+
const verifyDelta = newStatusRows(beforeVerify, afterVerifyRows);
|
|
407
581
|
const afterVerify = dryRun ? { staged: 0, unstaged: 0, untracked: 0 } : statusCounts(root) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
408
582
|
if (!dryRun && (afterVerify.staged || afterVerify.unstaged || afterVerify.untracked)) {
|
|
409
583
|
console.error(
|
|
410
584
|
`blocked: verifier left checkout dirty staged=${afterVerify.staged} ` +
|
|
411
585
|
`unstaged=${afterVerify.unstaged} untracked=${afterVerify.untracked}`
|
|
412
586
|
);
|
|
587
|
+
if (verifyDelta.length) {
|
|
588
|
+
console.error(`new after verify: ${verifyDelta.slice(0, 8).join(', ')}`);
|
|
589
|
+
}
|
|
413
590
|
return 3;
|
|
414
591
|
}
|
|
592
|
+
if (!dryRun && !proofTreeMatches('head')) return 3;
|
|
415
593
|
} else {
|
|
416
594
|
console.log('verify: skipped (no --verify)');
|
|
417
595
|
}
|
|
@@ -426,6 +604,35 @@ function shipWorktree(args) {
|
|
|
426
604
|
}
|
|
427
605
|
console.log(`merge_check: ${targetRef} clean`);
|
|
428
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
|
+
|
|
429
636
|
console.log(`push: origin ${branch}`);
|
|
430
637
|
if (!dryRun) runGit(['push', '-u', 'origin', branch], { cwd: root });
|
|
431
638
|
|
|
@@ -461,6 +668,12 @@ function shipWorktree(args) {
|
|
|
461
668
|
console.log('pr: skipped (--no-pr)');
|
|
462
669
|
}
|
|
463
670
|
|
|
671
|
+
try {
|
|
672
|
+
stampLatestOpenBriefForWorktree(root, {
|
|
673
|
+
result: 'pass',
|
|
674
|
+
note: `worktree ship completed into ${targetRef}`,
|
|
675
|
+
}, { worktree: root });
|
|
676
|
+
} catch {}
|
|
464
677
|
console.log('done: worktree shipped');
|
|
465
678
|
return 0;
|
|
466
679
|
}
|
|
@@ -474,7 +687,8 @@ function guard(args) {
|
|
|
474
687
|
console.error('run: atris worktree start --member <member>|--agent <name> --task "<short task>" --claim');
|
|
475
688
|
return 2;
|
|
476
689
|
}
|
|
477
|
-
|
|
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']) });
|
|
478
692
|
if (counts && (counts.staged || counts.unstaged || counts.untracked) && !hasFlag(args, '--allow-dirty')) {
|
|
479
693
|
console.error(`blocked: checkout is dirty staged=${counts.staged} unstaged=${counts.unstaged} untracked=${counts.untracked}`);
|
|
480
694
|
return 3;
|
|
@@ -617,6 +831,7 @@ function guide() {
|
|
|
617
831
|
console.log('4. Ship only from the isolated worktree:');
|
|
618
832
|
console.log(' atris worktree ship --message "<commit>" --verify "<cmd>" --merge [--target <ref>]');
|
|
619
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>');
|
|
620
835
|
console.log('');
|
|
621
836
|
console.log('5. Clean merged worktrees:');
|
|
622
837
|
console.log(' atris worktree cleanup');
|
|
@@ -632,8 +847,10 @@ function help() {
|
|
|
632
847
|
console.log('');
|
|
633
848
|
console.log(' atris worktree guide');
|
|
634
849
|
console.log(' atris worktree start --member <member>|--agent <name> --task "<task>" [--claim]');
|
|
635
|
-
console.log(' atris worktree ship --message "<commit>" --verify "<cmd>" [--merge] [--target <ref>]');
|
|
850
|
+
console.log(' atris worktree ship --message "<commit>" --verify "<cmd>" [--merge] [--target <ref>] [--local]');
|
|
636
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>');
|
|
637
854
|
console.log(' atris worktree status');
|
|
638
855
|
console.log(' atris worktree guard [--allow-primary] [--allow-dirty]');
|
|
639
856
|
console.log(' atris worktree prune [--apply]');
|
|
@@ -663,6 +880,8 @@ function worktreeCommand(args = []) {
|
|
|
663
880
|
|
|
664
881
|
module.exports = {
|
|
665
882
|
branchName,
|
|
883
|
+
statusSnapshot,
|
|
884
|
+
newStatusRows,
|
|
666
885
|
createAgentWorktree,
|
|
667
886
|
createOrFindPr,
|
|
668
887
|
cleanupWorktrees,
|
|
@@ -671,7 +890,9 @@ module.exports = {
|
|
|
671
890
|
defaultWorktreePath,
|
|
672
891
|
listWorktrees,
|
|
673
892
|
parseWorktrees,
|
|
893
|
+
REGEN_ADAPTER_FILES,
|
|
674
894
|
normalizeTargetRef,
|
|
895
|
+
restoreRegeneratedAdapterChurn,
|
|
675
896
|
prMergeRef,
|
|
676
897
|
slugify,
|
|
677
898
|
statusCounts,
|
package/commands/xp.js
CHANGED
|
@@ -18,6 +18,21 @@ const CAREER_XP_CURSOR_FILE = path.join('.atris', 'state', 'career_xp.cursor.jso
|
|
|
18
18
|
const CAREER_XP_SESSIONS_DIR = path.join('.atris', 'state', 'career_xp_sessions');
|
|
19
19
|
const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
|
|
20
20
|
const CODEX_STATE_FILE = path.join(os.homedir(), '.codex', 'state_5.sqlite');
|
|
21
|
+
|
|
22
|
+
// Default single-workspace XP root. Local XP reads task_episodes.jsonl (written
|
|
23
|
+
// by task-db at the resolved workspace root) and writes its projection beside
|
|
24
|
+
// it, so the `--workspace` default must resolve the same shared root — otherwise
|
|
25
|
+
// `xp --local` from a subdir (e.g. backend/) read the wrong/empty episodes and
|
|
26
|
+
// split the projection into a nested .atris. Explicit --workspace still wins;
|
|
27
|
+
// the multi-root roster path (discoverCareerXpWorkspaces) is untouched. Falls
|
|
28
|
+
// back to raw cwd if the resolver can't load.
|
|
29
|
+
function defaultXpWorkspace() {
|
|
30
|
+
try {
|
|
31
|
+
return require('../lib/mission-root').resolveWorkspaceRoot(process.cwd());
|
|
32
|
+
} catch {
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
// Codex moved native goals to goals_1.sqlite; thread metadata (cwd/title) stayed in state_5.sqlite.
|
|
22
37
|
const CODEX_GOALS_FILE = path.join(os.homedir(), '.codex', 'goals_1.sqlite');
|
|
23
38
|
const AGENT_XP_LABEL = 'AgentXP';
|
|
@@ -890,30 +905,33 @@ function readTaskEpisodeTail(episodePath, cursorPath, options = {}) {
|
|
|
890
905
|
}
|
|
891
906
|
|
|
892
907
|
function receiptFromTaskEpisode(episode) {
|
|
908
|
+
const episodeId = episode?.episode_id;
|
|
909
|
+
const label = episode?.rl?.label;
|
|
910
|
+
const rejected = label === 'rework_requested' || label === 'rejected';
|
|
893
911
|
const doneForXp = String(episode?.state?.status || '').toLowerCase() === 'done';
|
|
894
912
|
const hasExplicitCareerXp = episode?.career_xp && typeof episode.career_xp === 'object';
|
|
895
913
|
const eligible = doneForXp && (
|
|
896
914
|
episode?.career_xp?.eligible === true
|
|
897
|
-
|| (!hasExplicitCareerXp &&
|
|
915
|
+
|| (!hasExplicitCareerXp && label === 'accepted')
|
|
898
916
|
);
|
|
899
917
|
const proof = String(episode?.proof || '').trim();
|
|
900
918
|
const reward = asNumber(episode?.career_xp?.reward ?? episode?.reward?.value, 0);
|
|
901
|
-
if (!eligible || !proof || reward <= 0
|
|
919
|
+
if (!episodeId || (!rejected && (!eligible || !proof || reward <= 0))) return null;
|
|
902
920
|
|
|
903
921
|
return {
|
|
904
922
|
schema: 'atris.career_xp_receipt.v1',
|
|
905
|
-
receipt_id: `task_review:${
|
|
923
|
+
receipt_id: `task_review:${episodeId}${rejected ? ':rejected' : ''}`,
|
|
906
924
|
source: 'atris-cli',
|
|
907
925
|
source_type: 'task_review',
|
|
908
926
|
source_task_id: episode.task_id || null,
|
|
909
|
-
source_episode_id:
|
|
927
|
+
source_episode_id: episodeId,
|
|
910
928
|
workspace_root: episode.workspace_root || null,
|
|
911
929
|
actor: episode.action?.actor || null,
|
|
912
|
-
outcome: 'accepted',
|
|
913
|
-
xp: reward,
|
|
914
|
-
reward,
|
|
915
|
-
proof,
|
|
916
|
-
proof_ref: proof,
|
|
930
|
+
outcome: rejected ? 'rejected' : 'accepted',
|
|
931
|
+
xp: rejected ? 0 : reward,
|
|
932
|
+
reward: rejected ? 0 : reward,
|
|
933
|
+
proof: proof || null,
|
|
934
|
+
proof_ref: proof || null,
|
|
917
935
|
source_episode_hash: hashPayload(episode),
|
|
918
936
|
title: episode.state?.title || null,
|
|
919
937
|
goal: episode.goal || null,
|
|
@@ -1022,7 +1040,7 @@ function buildCareerXpProjection(receipts, workspace, integrity = {}) {
|
|
|
1022
1040
|
}
|
|
1023
1041
|
|
|
1024
1042
|
function collectLocalXpProjectionState(args = [], { write = true } = {}) {
|
|
1025
|
-
const workspace = path.resolve(readFlag(args, '--workspace',
|
|
1043
|
+
const workspace = path.resolve(readFlag(args, '--workspace', defaultXpWorkspace()));
|
|
1026
1044
|
const episodePath = path.join(workspace, TASK_EPISODES_FILE);
|
|
1027
1045
|
const receiptsPath = path.join(workspace, CAREER_XP_RECEIPTS_FILE);
|
|
1028
1046
|
const projectionPath = path.join(workspace, CAREER_XP_PROJECTION_FILE);
|
|
@@ -1614,7 +1632,7 @@ LIMIT 25
|
|
|
1614
1632
|
}
|
|
1615
1633
|
|
|
1616
1634
|
function buildCareerXpSessionCapsule(args = []) {
|
|
1617
|
-
const workspace = path.resolve(readFlag(args, '--workspace',
|
|
1635
|
+
const workspace = path.resolve(readFlag(args, '--workspace', defaultXpWorkspace()));
|
|
1618
1636
|
const sinceInput = readFlag(args, '--since', 'today');
|
|
1619
1637
|
const untilInput = readFlag(args, '--until', null);
|
|
1620
1638
|
const since = parseSessionBoundary(sinceInput, startOfToday());
|