atris 3.57.4 → 3.58.6

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 (175) hide show
  1. package/README.md +12 -1
  2. package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
  3. package/atris/skills/create-member/SKILL.md +16 -13
  4. package/atris/skills/design/SKILL.md +27 -4
  5. package/atris/skills/engines/SKILL.md +55 -12
  6. package/atris/skills/x-search/SKILL.md +8 -6
  7. package/atris/skills/youtube/SKILL.md +66 -21
  8. package/atris/team/brainstormer/MEMBER.md +2 -2
  9. package/atris/team/executor/MEMBER.md +3 -3
  10. package/atris/team/launcher/MEMBER.md +4 -4
  11. package/atris/team/navigator/MEMBER.md +3 -3
  12. package/atris/team/researcher/MEMBER.md +2 -2
  13. package/atris/team/validator/MEMBER.md +1 -1
  14. package/atris.md +14 -0
  15. package/ax +153 -23
  16. package/bin/atris.js +78 -30
  17. package/commands/aeo.js +4 -4
  18. package/commands/agents.js +1 -1
  19. package/commands/align.js +12 -12
  20. package/commands/analytics.js +3 -3
  21. package/commands/app.js +5 -5
  22. package/commands/ask.js +175 -0
  23. package/commands/auth.js +59 -31
  24. package/commands/autoland.js +29 -5
  25. package/commands/autopilot-front.js +6 -6
  26. package/commands/autopilot.js +90 -90
  27. package/commands/avail.js +5 -5
  28. package/commands/bench.js +63 -1
  29. package/commands/browse.js +2 -2
  30. package/commands/business.js +12 -12
  31. package/commands/card.js +2 -2
  32. package/commands/clean.js +13 -13
  33. package/commands/close.js +3 -3
  34. package/commands/compile.js +13 -13
  35. package/commands/computer.js +94 -27
  36. package/commands/console.js +5 -5
  37. package/commands/decide.js +1 -1
  38. package/commands/deck.js +9 -9
  39. package/commands/drive.js +4 -4
  40. package/commands/engine.js +173 -19
  41. package/commands/errors.js +3 -3
  42. package/commands/experiments.js +18 -1
  43. package/commands/feed.js +2 -2
  44. package/commands/feedback.js +4 -4
  45. package/commands/fleet.js +12 -12
  46. package/commands/game.js +1 -1
  47. package/commands/guide.js +49 -0
  48. package/commands/improve.js +19 -17
  49. package/commands/init.js +67 -19
  50. package/commands/interview.js +7 -7
  51. package/commands/land.js +40 -15
  52. package/commands/learn.js +216 -50
  53. package/commands/lesson.js +2 -2
  54. package/commands/log.js +1 -1
  55. package/commands/loops.js +4 -4
  56. package/commands/member.js +75 -50
  57. package/commands/mission.js +90 -82
  58. package/commands/one-lap.js +1 -1
  59. package/commands/pack.js +74 -8
  60. package/commands/plugin.js +6 -6
  61. package/commands/probe.js +9 -9
  62. package/commands/pull.js +35 -35
  63. package/commands/pulse.js +1 -1
  64. package/commands/push.js +21 -21
  65. package/commands/rainmaker.js +1 -1
  66. package/commands/recap.js +7 -7
  67. package/commands/reel.js +2 -2
  68. package/commands/release.js +3 -3
  69. package/commands/report.js +1 -1
  70. package/commands/review.js +5 -5
  71. package/commands/revisions.js +1 -1
  72. package/commands/run.js +1 -1
  73. package/commands/scout.js +3 -3
  74. package/commands/security-review.js +7 -7
  75. package/commands/serve.js +9 -9
  76. package/commands/setup.js +1 -1
  77. package/commands/sign.js +4 -4
  78. package/commands/signup.js +4 -4
  79. package/commands/site.js +8 -3
  80. package/commands/skill.js +6 -6
  81. package/commands/slop.js +18 -18
  82. package/commands/social.js +3 -3
  83. package/commands/soul.js +14 -14
  84. package/commands/spaceship.js +1 -1
  85. package/commands/status.js +2 -2
  86. package/commands/strings.js +13 -13
  87. package/commands/study.js +4 -4
  88. package/commands/sync.js +8 -8
  89. package/commands/task.js +50 -25
  90. package/commands/teach.js +2 -2
  91. package/commands/team.js +1 -1
  92. package/commands/theme.js +2 -2
  93. package/commands/tree.js +34 -0
  94. package/commands/truth.js +7 -7
  95. package/commands/verify.js +12 -12
  96. package/commands/visualize.js +2 -2
  97. package/commands/workflow.js +101 -48
  98. package/commands/worktree.js +1 -1
  99. package/commands/write.js +9 -9
  100. package/commands/x-search.js +93 -22
  101. package/commands/xp.js +6 -3
  102. package/commands/youtube.js +1019 -123
  103. package/lib/accept-verify-gate.js +3 -3
  104. package/lib/activity-stream.js +1 -1
  105. package/lib/auto-accept-certified.js +28 -12
  106. package/lib/autoland.js +6 -6
  107. package/lib/bench/engines.js +59 -8
  108. package/lib/bench/report.js +1 -1
  109. package/lib/bench/runner.js +204 -5
  110. package/lib/bench/tree-render.js +76 -0
  111. package/lib/card.js +1 -1
  112. package/lib/chat-log-scan.js +1 -1
  113. package/lib/claude-boot-block.js +2 -0
  114. package/lib/deck-compose.js +1 -1
  115. package/lib/deck-history.js +2 -2
  116. package/lib/deck-layout.js +1 -1
  117. package/lib/deck-review.js +5 -5
  118. package/lib/deck-schema.js +1 -1
  119. package/lib/default-verifier.js +1 -1
  120. package/lib/developer-api.js +3 -0
  121. package/lib/engine-ask.js +45 -7
  122. package/lib/engine-registry.js +15 -2
  123. package/lib/file-ops.js +1 -1
  124. package/lib/fleet.js +71 -35
  125. package/lib/html-render.js +1 -1
  126. package/lib/intents.js +172 -0
  127. package/lib/journal.js +1 -1
  128. package/lib/known-commands.js +2 -2
  129. package/lib/land-green.js +364 -0
  130. package/lib/lesson-contradiction.js +2 -2
  131. package/lib/lesson-preflight.js +1 -1
  132. package/lib/manifest.js +5 -5
  133. package/lib/member-alive.js +9 -5
  134. package/lib/memory-view.js +3 -3
  135. package/lib/mission-protected-lane.js +1 -1
  136. package/lib/mission-root.js +2 -2
  137. package/lib/orb-context.js +10 -10
  138. package/lib/pack-capabilities.js +861 -3
  139. package/lib/permission-grants.js +2 -2
  140. package/lib/policy-lessons.js +6 -6
  141. package/lib/pulse.js +8 -8
  142. package/lib/receipt-evidence.js +1 -1
  143. package/lib/reel.js +1 -1
  144. package/lib/review-integrity.js +1 -1
  145. package/lib/runner-command.js +35 -5
  146. package/lib/scorecard.js +4 -4
  147. package/lib/security-scan.js +5 -5
  148. package/lib/self-drive.js +1 -1
  149. package/lib/site-publish.js +372 -0
  150. package/lib/site.js +1 -1
  151. package/lib/slides-deck.js +2 -2
  152. package/lib/state-detection.js +2 -2
  153. package/lib/sync-telemetry.js +2 -2
  154. package/lib/task-db.js +17 -10
  155. package/lib/task-explanation.js +1 -1
  156. package/lib/task-proof.js +2 -2
  157. package/lib/task-receipt.js +3 -3
  158. package/lib/theme.js +12 -12
  159. package/lib/todo-fallback.js +3 -3
  160. package/lib/todo-sections.js +1 -1
  161. package/lib/tree-hash.js +79 -0
  162. package/lib/usage.js +1 -1
  163. package/lib/wish-delegate.js +1 -1
  164. package/lib/workspace-safety.js +4 -4
  165. package/lib/workspace-scaffold.js +3 -3
  166. package/package.json +1 -1
  167. package/scripts/det/concierge-bakeoff.sh +26 -0
  168. package/scripts/det/ytnotes +82 -5
  169. package/scripts/det/ytquote-repair.js +0 -1
  170. package/scripts/det/ytrail-race.js +0 -5
  171. package/scripts/member-operate.mjs +52 -14
  172. package/scripts/outbound-artifact-gate.js +0 -3
  173. package/utils/auth.js +47 -9
  174. package/utils/config.js +0 -1
  175. package/scripts/det/checklist-score.js +0 -191
@@ -0,0 +1,364 @@
1
+ 'use strict';
2
+
3
+ // Green landing for stale branches.
4
+ //
5
+ // The landing board already classifies every branch: landed, active, due.
6
+ // Before this module the loop had exactly two moves for finished work that
7
+ // nobody merged: carry it in the digest, then salvage-reap it after the TTL.
8
+ // Both of the branches that sat "in the air" on 2026-09-01 were green and
9
+ // merged cleanly; they waited four days for a human to run the suite by hand.
10
+ //
11
+ // This is the third move. A stale active branch that merges without conflict,
12
+ // touches no protected surface, and passes the full verify command in its own
13
+ // scratch checkout is fast-forwarded onto the base branch and pushed. A branch
14
+ // that fails opens one close flag naming the failing checks so the owner sees
15
+ // it the same day instead of on reap day. Everything else is left alone with
16
+ // a named reason. One branch per tick: the suite is minutes, not seconds.
17
+
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+ const { spawnSync } = require('child_process');
22
+ const { runGit } = require('./git-spawn');
23
+
24
+ const DEFAULT_VERIFY = 'npm test';
25
+ const DEFAULT_VERIFY_TIMEOUT_MS = 20 * 60 * 1000;
26
+ const FAILING_LINE = /^(?:✖|not ok)\s+(.+?)(?:\s+\(\d+(?:\.\d+)?ms\))?\s*$/;
27
+
28
+ function gitOut(args, cwd) {
29
+ const result = runGit(args, { cwd, check: false });
30
+ return { ok: result.status === 0, out: String(result.stdout || '').trim(), err: String(result.stderr || '').trim() };
31
+ }
32
+
33
+ function branchAuthor(root, branch) {
34
+ const result = gitOut(['log', '-1', '--format=%an', branch], root);
35
+ return result.ok && result.out ? result.out : 'operator';
36
+ }
37
+
38
+ function mergeConflicts(root, base, branch) {
39
+ // --write-tree exits 1 on conflict, 0 on clean, >1 on error (git >= 2.38).
40
+ const result = runGit(['merge-tree', '--write-tree', '--name-only', base, branch], { cwd: root, check: false });
41
+ if (result.status === 0) return { ok: true, conflicts: [] };
42
+ if (result.status === 1) {
43
+ // Output: tree id, conflicted paths, a blank line, then messages.
44
+ const block = String(result.stdout || '').split(/\r?\n\r?\n/)[0] || '';
45
+ const lines = block.split(/\r?\n/).filter(Boolean);
46
+ return { ok: true, conflicts: lines.slice(1) };
47
+ }
48
+ return { ok: false, error: String(result.stderr || result.stdout || 'merge-tree failed').trim() };
49
+ }
50
+
51
+ function protectedSurfaces(root, base, branch) {
52
+ let matcher = null;
53
+ try {
54
+ ({ matchProtectedMissionDiff: matcher } = require('./mission-protected-lane'));
55
+ } catch {
56
+ return { protected: false, surfaces: [] };
57
+ }
58
+ const diff = runGit(['diff', `${base}...${branch}`], { cwd: root, check: false, maxBuffer: 64 * 1024 * 1024 });
59
+ if (diff.status !== 0) return { protected: true, surfaces: ['unreadable diff'] };
60
+ const verdict = matcher(String(diff.stdout || ''));
61
+ return { protected: Boolean(verdict.protected), surfaces: verdict.surfaces || [] };
62
+ }
63
+
64
+ function dirtyCheckouts(root, branch) {
65
+ let listWorktrees;
66
+ let statusCounts;
67
+ try {
68
+ ({ listWorktrees, statusCounts } = require('../commands/worktree'));
69
+ } catch {
70
+ return [];
71
+ }
72
+ const dirty = [];
73
+ for (const wt of listWorktrees(root)) {
74
+ const name = String(wt.branch || '').replace(/^refs\/heads\//, '');
75
+ if (name !== branch) continue;
76
+ const counts = statusCounts(wt.path) || { staged: 0, unstaged: 0, untracked: 0 };
77
+ if (counts.staged + counts.unstaged + counts.untracked > 0) dirty.push(wt.path);
78
+ }
79
+ return dirty;
80
+ }
81
+
82
+ function failingLines(text, limit = 8) {
83
+ const seen = [];
84
+ for (const raw of String(text || '').split(/\r?\n/)) {
85
+ const match = raw.trim().match(FAILING_LINE);
86
+ if (!match) continue;
87
+ const name = match[1].trim();
88
+ if (/^failing tests:?$/i.test(name)) continue;
89
+ if (!seen.includes(name)) seen.push(name);
90
+ if (seen.length >= limit) break;
91
+ }
92
+ return seen;
93
+ }
94
+
95
+ function runVerify(command, cwd, { timeoutMs = DEFAULT_VERIFY_TIMEOUT_MS, env = process.env } = {}) {
96
+ const started = Date.now();
97
+ const result = spawnSync(command, {
98
+ cwd,
99
+ shell: true,
100
+ encoding: 'utf8',
101
+ timeout: timeoutMs,
102
+ maxBuffer: 64 * 1024 * 1024,
103
+ env: { ...env, ATRIS_SKIP_UPDATE_CHECK: '1' },
104
+ });
105
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
106
+ return {
107
+ command,
108
+ status: result.status,
109
+ timed_out: Boolean(result.error && result.error.code === 'ETIMEDOUT'),
110
+ duration_ms: Date.now() - started,
111
+ failing: failingLines(output),
112
+ tail: output.trim().split(/\r?\n/).slice(-12).join('\n'),
113
+ };
114
+ }
115
+
116
+ function baseCheckoutState(root, base) {
117
+ const head = gitOut(['symbolic-ref', '--quiet', '--short', 'HEAD'], root);
118
+ const onBase = head.ok && head.out === base;
119
+ if (!onBase) return { onBase: false, dirty: false };
120
+ const status = gitOut(['status', '--porcelain', '--untracked-files=no'], root);
121
+ return { onBase: true, dirty: status.ok && status.out.length > 0 };
122
+ }
123
+
124
+ // Move the base ref to `sha`, respecting a checked-out base worktree: when
125
+ // the root has base checked out and is clean, a fast-forward merge updates
126
+ // both the ref and the files; otherwise the ref moves alone.
127
+ function advanceBase(root, base, sha) {
128
+ const hasLocal = gitOut(['rev-parse', '--verify', '--quiet', `refs/heads/${base}`], root);
129
+ if (!hasLocal.ok) return { ok: true, skipped: 'no_local_branch' };
130
+ const state = baseCheckoutState(root, base);
131
+ if (state.onBase && state.dirty) return { ok: false, reason: 'base_checkout_dirty' };
132
+ if (state.onBase) {
133
+ const merged = gitOut(['merge', '--ff-only', sha], root);
134
+ if (!merged.ok) return { ok: false, reason: 'fast_forward_failed', detail: merged.err || merged.out };
135
+ return { ok: true };
136
+ }
137
+ const before = gitOut(['rev-parse', base], root);
138
+ const moved = gitOut(['update-ref', `refs/heads/${base}`, sha, before.ok ? before.out : ''], root);
139
+ if (!moved.ok) return { ok: false, reason: 'update_ref_failed', detail: moved.err || moved.out };
140
+ return { ok: true };
141
+ }
142
+
143
+ function pushBase(root, base, sha) {
144
+ const remote = gitOut(['remote', 'get-url', 'origin'], root);
145
+ if (!remote.ok) return { attempted: false, ok: false, reason: 'no_remote' };
146
+ const pushed = runGit(['push', 'origin', `${sha}:refs/heads/${base}`], { cwd: root, check: false });
147
+ return { attempted: true, ok: pushed.status === 0, detail: String(pushed.stderr || pushed.stdout || '').trim().slice(0, 300) };
148
+ }
149
+
150
+ // The board names the base as origin/master when a remote exists, so the
151
+ // merge target is the remote tip by construction. Refresh it first, and hold
152
+ // when the local branch carries commits origin lacks: pushing would fail and
153
+ // fast-forwarding the local branch would rewrite someone's unpushed work.
154
+ function localBaseName(base) {
155
+ return String(base).replace(/^origin\//, '');
156
+ }
157
+
158
+ function resolveMergeTarget(root, base) {
159
+ const local = localBaseName(base);
160
+ const remote = gitOut(['remote', 'get-url', 'origin'], root);
161
+ if (!remote.ok) return { ok: true, target: local, refreshed: false };
162
+ spawnSync('git', ['fetch', 'origin', local], { cwd: root, encoding: 'utf8', timeout: 30000 });
163
+ const remoteRef = `refs/remotes/origin/${local}`;
164
+ const exists = gitOut(['rev-parse', '--verify', '--quiet', remoteRef], root);
165
+ if (!exists.ok) return { ok: true, target: local, refreshed: true };
166
+ const hasLocal = gitOut(['rev-parse', '--verify', '--quiet', `refs/heads/${local}`], root);
167
+ if (hasLocal.ok) {
168
+ const counts = gitOut(['rev-list', '--left-right', '--count', `refs/heads/${local}...${remoteRef}`], root);
169
+ const [localOnly, remoteOnly] = counts.ok ? counts.out.split(/\s+/).map((n) => Number(n) || 0) : [0, 0];
170
+ if (localOnly > 0) {
171
+ return { ok: false, reason: 'base_diverged', detail: `local ${local} has ${localOnly} commit(s) origin lacks${remoteOnly ? ` and origin has ${remoteOnly} local lacks` : ''}` };
172
+ }
173
+ }
174
+ return { ok: true, target: remoteRef, refreshed: true };
175
+ }
176
+
177
+ function scratchCheckout(root, branch) {
178
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-land-green-'));
179
+ const added = gitOut(['worktree', 'add', '--detach', dir, branch], root);
180
+ if (!added.ok) {
181
+ fs.rmSync(dir, { recursive: true, force: true });
182
+ return { ok: false, error: added.err || added.out };
183
+ }
184
+ return { ok: true, dir };
185
+ }
186
+
187
+ function dropScratch(root, dir) {
188
+ if (!dir) return;
189
+ runGit(['worktree', 'remove', '--force', dir], { cwd: root, check: false });
190
+ fs.rmSync(dir, { recursive: true, force: true });
191
+ runGit(['worktree', 'prune'], { cwd: root, check: false });
192
+ }
193
+
194
+ function installDeps(dir) {
195
+ // Zero-dependency repos verify without install; only run it when a lockfile
196
+ // exists so the scratch checkout can resolve dev tooling.
197
+ if (!fs.existsSync(path.join(dir, 'package-lock.json'))) return { ran: false, ok: true };
198
+ const result = spawnSync('npm', ['ci', '--ignore-scripts', '--no-audit', '--no-fund'], {
199
+ cwd: dir, encoding: 'utf8', timeout: 5 * 60 * 1000, maxBuffer: 16 * 1024 * 1024,
200
+ });
201
+ return { ran: true, ok: result.status === 0, detail: String(result.stderr || '').trim().slice(0, 300) };
202
+ }
203
+
204
+ function verdictFor(branch, base, extra) {
205
+ return { branch, base, ...extra };
206
+ }
207
+
208
+ // Try to land exactly one branch. Returns a verdict with `action` in:
209
+ // landed, red, skipped, error
210
+ function landOneBranch(root, base, branch, {
211
+ verifyCommand = DEFAULT_VERIFY,
212
+ verifyTimeoutMs = DEFAULT_VERIFY_TIMEOUT_MS,
213
+ dryRun = false,
214
+ push = true,
215
+ runVerifyFn = runVerify,
216
+ env = process.env,
217
+ } = {}) {
218
+ const dirty = dirtyCheckouts(root, branch);
219
+ if (dirty.length) {
220
+ return verdictFor(branch, base, { action: 'skipped', reason: 'checkout_dirty', detail: dirty.join(', ') });
221
+ }
222
+ const conflicts = mergeConflicts(root, base, branch);
223
+ if (!conflicts.ok) return verdictFor(branch, base, { action: 'error', reason: 'merge_check_failed', detail: conflicts.error });
224
+ if (conflicts.conflicts.length) {
225
+ return verdictFor(branch, base, { action: 'skipped', reason: 'merge_conflict', detail: conflicts.conflicts.slice(0, 6).join(', ') });
226
+ }
227
+ const lane = protectedSurfaces(root, base, branch);
228
+ if (lane.protected) {
229
+ return verdictFor(branch, base, { action: 'skipped', reason: 'protected_surface', detail: lane.surfaces.join(', ') });
230
+ }
231
+ const target = resolveMergeTarget(root, base);
232
+ if (!target.ok) return verdictFor(branch, base, { action: 'skipped', reason: target.reason, detail: target.detail });
233
+ if (dryRun) return verdictFor(branch, base, { action: 'skipped', reason: 'dry_run', target: target.target });
234
+
235
+ const scratch = scratchCheckout(root, branch);
236
+ if (!scratch.ok) return verdictFor(branch, base, { action: 'error', reason: 'scratch_checkout_failed', detail: scratch.error });
237
+ try {
238
+ // Verify the merged state, not the branch tip: base may have moved since.
239
+ const merged = gitOut(['merge', '--no-edit', '--no-ff', target.target, '-m', `land: ${branch} onto ${base} after a green verify`], scratch.dir);
240
+ if (!merged.ok) return verdictFor(branch, base, { action: 'error', reason: 'merge_failed', detail: merged.err || merged.out });
241
+ const deps = installDeps(scratch.dir);
242
+ if (!deps.ok) return verdictFor(branch, base, { action: 'error', reason: 'install_failed', detail: deps.detail });
243
+ const verify = runVerifyFn(verifyCommand, scratch.dir, { timeoutMs: verifyTimeoutMs, env });
244
+ if (verify.status !== 0) {
245
+ return verdictFor(branch, base, {
246
+ action: 'red',
247
+ reason: verify.timed_out ? 'verify_timed_out' : 'verify_failed',
248
+ verify,
249
+ owner: branchAuthor(root, branch),
250
+ });
251
+ }
252
+ const sha = gitOut(['rev-parse', 'HEAD'], scratch.dir);
253
+ if (!sha.ok) return verdictFor(branch, base, { action: 'error', reason: 'merged_sha_unreadable' });
254
+ const local = localBaseName(base);
255
+ const pushed = push ? pushBase(root, local, sha.out) : { attempted: false, ok: false, reason: 'push_disabled' };
256
+ if (pushed.attempted && !pushed.ok) {
257
+ // A race with another landing: origin moved after the fetch. Leave the
258
+ // local branch alone so the next tick retries onto the new tip.
259
+ return verdictFor(branch, base, { action: 'skipped', reason: 'push_rejected', detail: pushed.detail, verify });
260
+ }
261
+ const advanced = advanceBase(root, local, sha.out);
262
+ if (!advanced.ok) return verdictFor(branch, base, { action: 'landed', sha: sha.out, verify, push: pushed, local_base: advanced.reason });
263
+ return verdictFor(branch, base, { action: 'landed', sha: sha.out, verify, push: pushed });
264
+ } finally {
265
+ dropScratch(root, scratch.dir);
266
+ }
267
+ }
268
+
269
+ function flagWhat(verdict) {
270
+ const names = (verdict.verify && verdict.verify.failing) || [];
271
+ const head = names.length ? names.slice(0, 3).join('; ') : (verdict.reason === 'verify_timed_out' ? 'the verify timed out' : 'the verify command failed');
272
+ return `branch ${verdict.branch} fails its checks and cannot land: ${head}`;
273
+ }
274
+
275
+ function openRedFlag(root, verdict, { runCli } = {}) {
276
+ const what = flagWhat(verdict);
277
+ const args = ['close', 'add', what, '--owner', verdict.owner || 'operator', '--lane', 'code', '--ttl', '3',
278
+ '--when', `${verdict.branch} passes ${verdict.verify ? verdict.verify.command : 'its checks'} and lands on ${verdict.base}`,
279
+ '--source', 'land-green'];
280
+ if (typeof runCli === 'function') return runCli(args);
281
+ const bin = path.join(__dirname, '..', 'bin', 'atris.js');
282
+ const result = spawnSync(process.execPath, [bin, ...args], { cwd: root, encoding: 'utf8', timeout: 30000 });
283
+ return { status: result.status, stdout: String(result.stdout || '').trim(), stderr: String(result.stderr || '').trim() };
284
+ }
285
+
286
+ // The tick entry point: pick the stale active branches the board already
287
+ // knows about, oldest activity first, and land at most `limit` of them.
288
+ function landStaleGreenBranches(root, {
289
+ board = null,
290
+ staleHours,
291
+ ttlDays,
292
+ base: baseOverride = '',
293
+ limit = 1,
294
+ now = Date.now(),
295
+ verifyCommand = DEFAULT_VERIFY,
296
+ verifyTimeoutMs = DEFAULT_VERIFY_TIMEOUT_MS,
297
+ dryRun = false,
298
+ push = true,
299
+ openFlags = true,
300
+ runVerifyFn = runVerify,
301
+ runCli,
302
+ env = process.env,
303
+ } = {}) {
304
+ const { collectBoard } = require('../commands/land');
305
+ const live = board || collectBoard(root, { staleHours, ttlDays, base: baseOverride, now, light: true });
306
+ const candidates = live.branches
307
+ .filter((b) => b.state === 'active' && b.stale)
308
+ .sort((a, b) => b.activityHours - a.activityHours);
309
+ const verdicts = [];
310
+ let attempts = 0;
311
+ for (const candidate of candidates) {
312
+ if (attempts >= limit) {
313
+ verdicts.push(verdictFor(candidate.name, live.base, { action: 'skipped', reason: 'per_tick_limit' }));
314
+ continue;
315
+ }
316
+ attempts += 1;
317
+ let verdict;
318
+ try {
319
+ verdict = landOneBranch(root, live.base, candidate.name, { verifyCommand, verifyTimeoutMs, dryRun, push, runVerifyFn, env });
320
+ } catch (err) {
321
+ verdict = verdictFor(candidate.name, live.base, { action: 'error', reason: 'exception', detail: String((err && err.message) || err).slice(0, 300) });
322
+ }
323
+ if (verdict.action === 'red' && openFlags) {
324
+ try {
325
+ const flagged = openRedFlag(root, verdict, { runCli });
326
+ verdict.flag = { status: flagged.status, out: String(flagged.stdout || '').slice(0, 200) };
327
+ } catch (err) {
328
+ verdict.flag = { status: 1, out: String((err && err.message) || err).slice(0, 200) };
329
+ }
330
+ }
331
+ verdicts.push(verdict);
332
+ }
333
+ return {
334
+ at: new Date(now).toISOString(),
335
+ base: live.base,
336
+ candidates: candidates.map((b) => b.name),
337
+ landed: verdicts.filter((v) => v.action === 'landed').map((v) => v.branch),
338
+ red: verdicts.filter((v) => v.action === 'red').map((v) => v.branch),
339
+ verdicts,
340
+ };
341
+ }
342
+
343
+ function summaryLine(result) {
344
+ if (!result || !result.candidates || result.candidates.length === 0) return 'no stale branches to land';
345
+ const parts = [];
346
+ if (result.landed.length) parts.push(`landed ${result.landed.join(', ')}`);
347
+ if (result.red.length) parts.push(`red: ${result.red.join(', ')}`);
348
+ for (const v of result.verdicts) {
349
+ if (v.action === 'skipped' && v.reason !== 'per_tick_limit') parts.push(`${v.branch} held (${v.reason.replace(/_/g, ' ')}${v.detail ? `: ${v.detail}` : ''})`);
350
+ if (v.action === 'error') parts.push(`${v.branch} error (${v.reason.replace(/_/g, ' ')})`);
351
+ }
352
+ const waiting = result.verdicts.filter((v) => v.reason === 'per_tick_limit').length;
353
+ if (waiting) parts.push(`${waiting} more waiting for the next tick`);
354
+ return parts.join('; ') || 'nothing landed';
355
+ }
356
+
357
+ module.exports = {
358
+ DEFAULT_VERIFY,
359
+ landStaleGreenBranches,
360
+
361
+ failingLines,
362
+ flagWhat,
363
+ summaryLine,
364
+ };
@@ -22,8 +22,8 @@ function detectLessonContradictions(cwd) {
22
22
  ? fs.readFileSync(lessonsMdPath, 'utf8')
23
23
  : '';
24
24
 
25
- // Format: - **[YYYY-MM-DD] slug** pass|fail prose
26
- const lessonRegex = /^-\s+\*\*\[(\d{4}-\d{2}-\d{2})\]\s+([a-z0-9-]+)\*\*\s*—\s*(pass|fail)\s*—\s*(\[resolved\])?\s*(.+)$/m;
25
+ // Format: - **[YYYY-MM-DD] slug**, pass|fail, prose
26
+ const lessonRegex = /^-\s+\*\*\[(\d{4}-\d{2}-\d{2})\]\s+([a-z0-9-]+)\*\*\s*\u2014\s*(pass|fail)\s*\u2014\s*(\[resolved\])?\s*(.+)$/m;
27
27
  const lines = lessonsMdContent.split('\n');
28
28
 
29
29
  const lessonsBySlug = new Map(); // slug -> [{ date, outcome, lineNum, resolved }]
@@ -28,7 +28,7 @@ function readLessonText(filePath) {
28
28
 
29
29
  const bySlug = new Map();
30
30
  for (const line of content.split(/\r?\n/)) {
31
- const match = line.match(/^\s*-\s*\*\*\[([^\]]+)\]\s*([a-z0-9][a-z0-9-]*)\*\*\s*[—:-]+\s*(?:pass|fail)?\s*[—:-]*\s*(.*)$/i);
31
+ const match = line.match(/^\s*-\s*\*\*\[([^\]]+)\]\s*([a-z0-9][a-z0-9-]*)\*\*\s*[\u2014:-]+\s*(?:pass|fail)?\s*[\u2014:-]*\s*(.*)$/i);
32
32
  if (!match || bySlug.has(match[2])) continue;
33
33
  const text = match[3].replace(/^\[resolved\]\s*/i, '').trim();
34
34
  if (text) bySlug.set(match[2], text);
package/lib/manifest.js CHANGED
@@ -152,14 +152,14 @@ function threeWayCompare(localFiles, remoteFiles, manifest) {
152
152
  newRemote: [],
153
153
  };
154
154
 
155
- // First sync no manifest
155
+ // First sync, no manifest
156
156
  if (!manifest || !manifest.files) {
157
157
  // Everything remote is "new from remote"
158
158
  for (const p of Object.keys(remoteFiles)) {
159
159
  if (localFiles[p] && localFiles[p].hash === remoteFiles[p].hash) {
160
160
  result.unchanged.push(p);
161
161
  } else if (localFiles[p]) {
162
- // Both exist with different hashes, no baseline treat as conflict
162
+ // Both exist with different hashes, no baseline, treat as conflict
163
163
  result.conflicts.push(p);
164
164
  } else {
165
165
  result.newRemote.push(p);
@@ -191,7 +191,7 @@ function threeWayCompare(localFiles, remoteFiles, manifest) {
191
191
  const baseHash = inBase ? base[p].hash : null;
192
192
 
193
193
  if (inLocal && inRemote && inBase) {
194
- // All three exist standard three-way
194
+ // All three exist, standard three-way
195
195
  const localChanged = localHash !== baseHash;
196
196
  const remoteChanged = remoteHash !== baseHash;
197
197
 
@@ -223,10 +223,10 @@ function threeWayCompare(localFiles, remoteFiles, manifest) {
223
223
  // Was in base, deleted locally, still remote
224
224
  result.deletedLocal.push(p);
225
225
  } else if (inBase && !inLocal && !inRemote) {
226
- // Deleted on both sides nothing to do
226
+ // Deleted on both sides, nothing to do
227
227
  // (don't add to any list)
228
228
  } else if (inLocal && inRemote && !inBase) {
229
- // Both sides have it but no base new on both
229
+ // Both sides have it but no base, new on both
230
230
  if (localHash === remoteHash) {
231
231
  result.unchanged.push(p);
232
232
  } else {
@@ -6,7 +6,7 @@ const { spawnSync } = require('child_process');
6
6
 
7
7
  const MEMBER_OPERATE_PROGRESS_PREFIX = 'ATRIS_MEMBER_OPERATE_PROGRESS\t';
8
8
 
9
- // Wake decisions where the member should NOT run operate work this tick it is waiting on a
9
+ // Wake decisions where the member should NOT run operate work this tick: it is waiting on a
10
10
  // human (ask), on a pending review or clean scope (wait), or has no active goal yet (stop).
11
11
  const NON_OPERATE_DECISIONS = new Set(['ask', 'wait', 'stop']);
12
12
 
@@ -65,6 +65,7 @@ function resolveMemberOperateScript(cwd) {
65
65
  const candidates = [
66
66
  path.join(cwd, 'scripts', 'member-operate.mjs'),
67
67
  path.join(cwd, 'scripts', 'member-operate.js'),
68
+ path.join(__dirname, '..', 'scripts', 'member-operate.mjs'),
68
69
  ];
69
70
  for (const candidate of candidates) {
70
71
  if (fs.existsSync(candidate)) return candidate;
@@ -87,6 +88,7 @@ function runMemberOperateScript(scriptPath, member, opts) {
87
88
  if (opts.agent) args.push('--agent', String(opts.agent));
88
89
  if (opts.model) args.push('--model', String(opts.model));
89
90
  if (opts.noPrime) args.push('--no-prime');
91
+ if (opts.sharedCheckout) args.push('--shared-checkout');
90
92
  // Inherit stderr so member-operate progress lines reach the parent alive/loop process live.
91
93
  const result = spawnSync(process.execPath, args, {
92
94
  cwd: opts.cwd || process.cwd(),
@@ -188,7 +190,7 @@ function runAliveTick(name, opts = {}) {
188
190
  return tick;
189
191
  }
190
192
 
191
- emitAliveProgress({ kind: 'phase', text: `Running ${name} tick streaming live below.` });
193
+ emitAliveProgress({ kind: 'phase', text: `Running ${name} tick, streaming live below.` });
192
194
  tick.operate = runMemberOperateScript(operateScript, name, {
193
195
  cwd,
194
196
  execute: true,
@@ -197,6 +199,7 @@ function runAliveTick(name, opts = {}) {
197
199
  agent: opts.agent || null,
198
200
  model: opts.model || null,
199
201
  noPrime: true,
202
+ sharedCheckout: opts.sharedCheckout,
200
203
  });
201
204
  tick.needs_user = tick.needs_user || tick.operate.needs_user === true;
202
205
 
@@ -210,17 +213,18 @@ function runAliveTick(name, opts = {}) {
210
213
  const operateOk = tick.operate.ok !== false;
211
214
  const acceptOk = tick.auto_accept.ok !== false;
212
215
  tick.ok = operateOk && acceptOk;
216
+ const planned = tick.ok && tick.operate.payload?.status === 'planned';
213
217
  tick.status = tick.needs_user
214
218
  ? 'needs_user'
215
219
  : tick.ok
216
- ? 'completed'
220
+ ? planned ? 'planned' : 'completed'
217
221
  : 'failed';
218
222
  tick.reason = tick.ok
219
- ? 'alive_tick_complete'
223
+ ? planned ? tick.operate.reason : 'alive_tick_complete'
220
224
  : tick.operate.reason || tick.auto_accept.json?.summary
221
225
  ? 'partial_failure'
222
226
  : 'alive_tick_failed';
223
- tick.receipt_path = tick.operate.receipt_path || wakeJson.receipt_path || null;
227
+ tick.receipt_path = tick.operate.receipt_path || null;
224
228
  tick.finished_at = stampIso();
225
229
  return tick;
226
230
  }
@@ -1,4 +1,4 @@
1
- // Memory view another way to look at memory updates. Reads the workspace's
1
+ // Memory view: another way to look at memory updates. Reads the workspace's
2
2
  // compounding memory (lessons.md, learnings.jsonl, task projection) and builds a
3
3
  // content spec rendered as a beautiful HTML page by lib/html-render.
4
4
  //
@@ -9,13 +9,13 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
- // "- **[2026-06-18] some-id** pass text..." -> { date, id, status, text, resolved }
12
+ // "- **[2026-06-18] some-id**, pass, text..." -> { date, id, status, text, resolved }
13
13
  // A leading "[resolved]" marker (written when a lesson's detector passes) is
14
14
  // stripped from the text and surfaced as `resolved` so the view can retire it.
15
15
  function parseLessons(text) {
16
16
  const out = [];
17
17
  for (const line of String(text || '').split('\n')) {
18
- const m = line.match(/^-\s*\*\*\[([^\]]+)\]\s*([^*]+?)\*\*\s*[—:-]*\s*(pass|fail)?\s*[—:-]*\s*(.*)$/i);
18
+ const m = line.match(/^-\s*\*\*\[([^\]]+)\]\s*([^*]+?)\*\*\s*[\u2014:-]*\s*(pass|fail)?\s*[\u2014:-]*\s*(.*)$/i);
19
19
  if (!m) continue;
20
20
  let body = m[4].trim();
21
21
  const resolved = /\[resolved\]/i.test(body);
@@ -51,7 +51,7 @@ function changedPathsFromDiff(diff) {
51
51
 
52
52
  // Prose is not a protected surface. Docs, judge cards, wikis, journals and run
53
53
  // receipts routinely *describe* CSP or force-dynamic without changing any
54
- // behaviour replaying this guard over 60 real commits, 5 of 6 hits were
54
+ // behaviour: replaying this guard over 60 real commits, 5 of 6 hits were
55
55
  // markdown that merely mentioned the words. A guard that pauses on every judge
56
56
  // card gets switched off, which is worse than no guard. Content signals
57
57
  // therefore apply to code only; PATH signals still apply to every file.
@@ -6,7 +6,7 @@ const { spawnSync } = require('child_process');
6
6
  // The mission store lives at <workspace>/.atris/state. Historically every
7
7
  // mission entry point defaulted its root to process.cwd(), so running any
8
8
  // `atris mission ...` verb from a subdirectory (e.g. atris/features/food-ordering)
9
- // silently created a NESTED .atris there a second, split-brain mission store
9
+ // silently created a NESTED .atris there: a second, split-brain mission store
10
10
  // the fleet never reads. Resolve to the workspace root so mission state always
11
11
  // lands there, whichever subdir the caller happened to be in.
12
12
  //
@@ -14,7 +14,7 @@ const { spawnSync } = require('child_process');
14
14
  // (lib/task-db findWorkspaceRoot): a bound customer sub-workspace spine
15
15
  // (.atris/business.json | atris/atris.md) wins, else the git toplevel, else the
16
16
  // caller's cwd. Sharing one resolver keeps mission state and task state in the
17
- // same root previously mission used only the git toplevel, so a mission
17
+ // same root. previously mission used only the git toplevel, so a mission
18
18
  // started inside a bound sub-workspace split away from that workspace's task
19
19
  // state. A git worktree reports its own toplevel (its .git marker), which is
20
20
  // the correct per-worktree store, so --worktree missions are unaffected. Walk-up
@@ -1,4 +1,4 @@
1
- // Orb context deterministic greeting + suggestion synthesis for the orb surface.
1
+ // Orb context: deterministic greeting + suggestion synthesis for the orb surface.
2
2
  // Pure synthesis lives in buildOrbContext (unit-testable, no fs); collectOrbContext
3
3
  // does the read-only fs aggregation from a project root. No model calls anywhere:
4
4
  // 0ms-to-greeting is the contract (atris/features/orb-desktop/PLAN.md).
@@ -65,7 +65,7 @@ function normalizeTasks(tasks) {
65
65
  }
66
66
 
67
67
  // Known suggestion action prefixes. Stripped repeatedly (front-anchored) so that
68
- // "review: review: X" or "pick up pick up X" both collapse to the bare title
68
+ // "review: review: X" or "pick up pick up X" both collapse to the bare title.
69
69
  // used both to keep labels single-prefixed and to compare titles across sources.
70
70
  // "atrisbox" is included because Atrisbox threads are titled "Atrisbox: <label>",
71
71
  // which otherwise shields inner accumulated prefixes from stripping (seen live
@@ -85,12 +85,12 @@ function stripActionPrefixes(value) {
85
85
  }
86
86
 
87
87
  // Lowercase, prefix-stripped, whitespace-collapsed form used only for equality/
88
- // prefix comparison across sources (tasks vs threads) never shown to the user.
88
+ // prefix comparison across sources (tasks vs threads), never shown to the user.
89
89
  function normalizeForMatch(value) {
90
90
  return stripActionPrefixes(String(value || '').toLowerCase().replace(/\s+/g, ' ').trim());
91
91
  }
92
92
 
93
- // True when two titles are "the same work" exact match, or one is a truncated
93
+ // True when two titles are "the same work": exact match, or one is a truncated
94
94
  // prefix of the other (titles get cut at different lengths per source, so a
95
95
  // straight equality check would miss the common case). Guarded by a minimum
96
96
  // needle length so short titles ("fix it") don't false-positive against unrelated
@@ -106,7 +106,7 @@ function titlesOverlap(a, b) {
106
106
  }
107
107
 
108
108
  // Background Atrisbox taps write a launch-receipt thread (desk_<project>_
109
- // atrisbox_bg_<slug>) — it's proof a turn ran, not a resumable conversation.
109
+ // atrisbox_bg_<slug>), proof a turn ran, not a resumable conversation.
110
110
  // They self-archive on delivery (App.tsx sendAtrisboxMessage) and main.js
111
111
  // sweeps stragglers, but excluding them from warm eligibility here too is
112
112
  // cheap (a string check, no I/O) and keeps the count honest even for threads
@@ -127,7 +127,7 @@ function normalizeThreads(threads) {
127
127
  .sort((a, b) => b.updatedAt - a.updatedAt);
128
128
  }
129
129
 
130
- // Dedupe by "same work" (titlesOverlap) keeping the newest of each cluster
130
+ // Dedupe by "same work" (titlesOverlap) keeping the newest of each cluster,
131
131
  // used before counting warm threads so the greeting number reflects distinct
132
132
  // resumable work, not the same thread title repeated under different ids.
133
133
  function dedupeByTitle(threads) {
@@ -218,7 +218,7 @@ function buildOrbContext({ nowMd = '', tasks = [], threads = [], memberLoops = [
218
218
  const suggestions = [];
219
219
  // Titles already spoken for by a suggestion, used to dedupe across sources
220
220
  // (review/resume/continue are built independently but can point at the same
221
- // underlying work e.g. a task's warm thread title vs the task's own title).
221
+ // underlying work, e.g. a task's warm thread title vs the task's own title).
222
222
  const usedTitles = [];
223
223
 
224
224
  function pushSuggestion(suggestion, title = '') {
@@ -268,7 +268,7 @@ function buildOrbContext({ nowMd = '', tasks = [], threads = [], memberLoops = [
268
268
  });
269
269
  }
270
270
 
271
- // Explicitly blocked/stalled statuses are their own lane distinct from the
271
+ // Explicitly blocked/stalled statuses are their own lane, distinct from the
272
272
  // stale-doing "finish" lane below, which infers staleness from the clock.
273
273
  const blockedTask = cleanTasks
274
274
  .filter((t) => t.status === 'blocked' || t.status === 'stalled')
@@ -310,7 +310,7 @@ function buildOrbContext({ nowMd = '', tasks = [], threads = [], memberLoops = [
310
310
  for (const thread of warmThreads) {
311
311
  if (usedTitles.some((title) => titlesOverlap(title, thread.title))) continue;
312
312
  // Thread titles inherit message text, so a picked-up thread can arrive as
313
- // "pick up X: pick up X..." strip the accumulated prefixes before reusing.
313
+ // "pick up X: pick up X...", strip the accumulated prefixes before reusing.
314
314
  const threadTitle = cleanTitle(stripActionPrefixes(String(thread.title || '')), 40);
315
315
  if (threadTitle.length < 3) continue;
316
316
  resumeThread = thread;
@@ -369,7 +369,7 @@ function buildOrbContext({ nowMd = '', tasks = [], threads = [], memberLoops = [
369
369
 
370
370
  // These stay at the tail even when the backlog is full, so orientation and
371
371
  // planning never disappear behind the deeper task-fed pool.
372
- // 'status' is answered locally by the view from gmSnapshot no model call.
372
+ // 'status' is answered locally by the view from gmSnapshot, no model call.
373
373
  const staticSuggestions = [{
374
374
  id: 'status',
375
375
  label: "what's moving",