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.
Files changed (133) hide show
  1. package/AGENTS.md +37 -0
  2. package/README.md +5 -3
  3. package/atris/GETTING_STARTED.md +1 -1
  4. package/atris/atris.md +3 -0
  5. package/atris/policies/day-loop-voice.md +102 -0
  6. package/atris/policies/outbound-artifact-gate.md +2 -0
  7. package/atris/skills/design/SKILL.md +56 -32
  8. package/atris/skills/endgame/SKILL.md +12 -6
  9. package/atris/skills/engines/SKILL.md +22 -4
  10. package/atris/skills/fable-method/SKILL.md +66 -0
  11. package/atris/skills/improve/SKILL.md +65 -45
  12. package/atris/skills/youtube/SKILL.md +10 -1
  13. package/atris.md +2 -0
  14. package/ax +147 -19
  15. package/bin/atris.js +576 -267
  16. package/commands/activate.js +194 -88
  17. package/commands/agents.js +166 -0
  18. package/commands/autoland.js +459 -107
  19. package/commands/autopilot-front.js +20 -2
  20. package/commands/autopilot.js +118 -2
  21. package/commands/avail.js +407 -0
  22. package/commands/bench.js +188 -0
  23. package/commands/brain.js +3 -0
  24. package/commands/brief.js +651 -0
  25. package/commands/business-sync.js +192 -6
  26. package/commands/clean.js +50 -24
  27. package/commands/close.js +1083 -0
  28. package/commands/cloud.js +245 -0
  29. package/commands/compile.js +292 -1
  30. package/commands/computer.js +150 -3
  31. package/commands/dream.js +365 -0
  32. package/commands/drill.js +371 -0
  33. package/commands/engine.js +993 -32
  34. package/commands/experiments.js +28 -0
  35. package/commands/feedback.js +34 -12
  36. package/commands/fleet-report.js +206 -0
  37. package/commands/gm.js +23 -0
  38. package/commands/goal.js +247 -0
  39. package/commands/improve.js +642 -26
  40. package/commands/init.js +72 -44
  41. package/commands/interview.js +67 -1
  42. package/commands/land.js +152 -52
  43. package/commands/lifecycle.js +39 -3
  44. package/commands/log.js +84 -1
  45. package/commands/loops.js +220 -16
  46. package/commands/meet.js +220 -0
  47. package/commands/member.js +511 -34
  48. package/commands/mission.js +3144 -338
  49. package/commands/next.js +137 -0
  50. package/commands/now.js +220 -25
  51. package/commands/one-lap.js +776 -0
  52. package/commands/orb.js +314 -0
  53. package/commands/pack-craft.js +179 -0
  54. package/commands/pack.js +823 -0
  55. package/commands/play.js +3 -2
  56. package/commands/probe.js +30 -3
  57. package/commands/pulse.js +241 -46
  58. package/commands/push.js +260 -82
  59. package/commands/rainmaker.js +49 -0
  60. package/commands/report.js +415 -0
  61. package/commands/scout.js +147 -0
  62. package/commands/search.js +363 -0
  63. package/commands/skill.js +47 -3
  64. package/commands/slop.js +50 -2
  65. package/commands/soul.js +1 -1
  66. package/commands/stream.js +861 -0
  67. package/commands/study.js +693 -0
  68. package/commands/sync.js +67 -54
  69. package/commands/task.js +1346 -117
  70. package/commands/team.js +73 -0
  71. package/commands/verify.js +96 -0
  72. package/commands/watch.js +303 -0
  73. package/commands/wish.js +500 -0
  74. package/commands/workflow.js +11 -5
  75. package/commands/worktree.js +234 -13
  76. package/commands/xp.js +29 -11
  77. package/lib/auto-accept-certified.js +331 -34
  78. package/lib/autoland.js +319 -54
  79. package/lib/ax-auto-lane.js +96 -0
  80. package/lib/bench/context.js +147 -0
  81. package/lib/bench/engines.js +141 -0
  82. package/lib/bench/report.js +140 -0
  83. package/lib/bench/runner.js +512 -0
  84. package/lib/brief-ledger.js +350 -0
  85. package/lib/cloud-mission.js +259 -0
  86. package/lib/codex-flight.js +154 -0
  87. package/lib/default-runner.js +45 -0
  88. package/lib/default-verifier.js +70 -0
  89. package/lib/engine-registry.js +232 -0
  90. package/lib/experiments/daily.js +640 -0
  91. package/lib/fleet.js +2219 -67
  92. package/lib/improve-vitals-html.js +171 -0
  93. package/lib/known-commands.js +58 -0
  94. package/lib/loop-doctor.js +416 -0
  95. package/lib/member-switches.js +144 -0
  96. package/lib/mission-room.js +1 -0
  97. package/lib/mission-root.js +52 -0
  98. package/lib/next-moves.js +327 -10
  99. package/lib/one-lap-validator.js +60 -0
  100. package/lib/orb-context.js +477 -0
  101. package/lib/orb-scorecard.js +224 -0
  102. package/lib/policy-lessons.js +52 -1
  103. package/lib/pulse.js +277 -3
  104. package/lib/receipt-block.js +168 -0
  105. package/lib/receipt-evidence.js +65 -4
  106. package/lib/router-brain.js +352 -0
  107. package/lib/runner-command.js +10 -0
  108. package/lib/self-drive.js +258 -0
  109. package/lib/short-name.js +103 -0
  110. package/lib/spawn-env.js +18 -0
  111. package/lib/state-detection.js +56 -1
  112. package/lib/sync-status.js +59 -0
  113. package/lib/task-db.js +108 -29
  114. package/lib/task-proof.js +23 -1
  115. package/lib/team-presence.js +260 -0
  116. package/lib/tool-result-encode.js +7 -0
  117. package/lib/trust-tiers.js +90 -0
  118. package/lib/usage.js +107 -0
  119. package/lib/voice-gate.js +163 -0
  120. package/lib/wish-audit.js +1368 -0
  121. package/lib/wish-delegate.js +1840 -0
  122. package/lib/wish-design.js +110 -0
  123. package/lib/wish-stats.js +183 -0
  124. package/lib/wish-store.js +354 -0
  125. package/lib/zip.js +221 -0
  126. package/package.json +3 -1
  127. package/templates/loops/atris/loops/LOOPS.md +55 -0
  128. package/templates/loops/atris/loops/TICK.md +24 -0
  129. package/templates/loops/atris/loops/feedback.md +22 -0
  130. package/templates/loops/atris/loops/quality.md +22 -0
  131. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  132. package/utils/api.js +5 -1
  133. package/utils/auth.js +57 -21
@@ -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, { cwd, encoding: 'utf8', shell: true, stdio: 'pipe' });
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 defaultStartBase(root) {
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
- const result = runGit(['status', '--porcelain'], { cwd: root, check: false });
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
- const checkoutBase = normalizeTargetRef(root, baseOverride || defaultStartBase(root));
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
- const branch = currentBranch(root);
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 || branch === 'master' || branch === 'main') {
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 counts = statusCounts(root) || { staged: 0, unstaged: 0, untracked: 0 };
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
- const counts = statusCounts(root);
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 && episode?.rl?.label === 'accepted')
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 || !episode?.episode_id) return null;
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:${episode.episode_id}`,
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: episode.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', process.cwd()));
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', process.cwd()));
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());