atris 3.74.3 → 3.74.5

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 (117) hide show
  1. package/FOR_AGENTS.md +12 -12
  2. package/atris/AGENTS.md +7 -7
  3. package/atris/CLAUDE.md +10 -10
  4. package/atris/policies/atris-backend.md +1 -1
  5. package/atris/policies/atris-design.md +71 -71
  6. package/atris/policies/design-seed.md +28 -28
  7. package/atris/policies/writing.md +1 -1
  8. package/atris/skills/apps/SKILL.md +3 -3
  9. package/atris/skills/atris-feedback/SKILL.md +2 -2
  10. package/atris/skills/autopilot/SKILL.md +10 -10
  11. package/atris/skills/blocks/SKILL.md +16 -16
  12. package/atris/skills/calendar/SKILL.md +1 -1
  13. package/atris/skills/clawhub/atris/SKILL.md +4 -4
  14. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +7 -7
  15. package/atris/skills/create-app/SKILL.md +18 -18
  16. package/atris/skills/create-member/SKILL.md +11 -11
  17. package/atris/skills/design/SKILL.md +1 -1
  18. package/atris/skills/drive/SKILL.md +25 -25
  19. package/atris/skills/email-agent/SKILL.md +11 -11
  20. package/atris/skills/endgame/SKILL.md +33 -33
  21. package/atris/skills/magic-inbox/SKILL.md +15 -15
  22. package/atris/skills/notion/SKILL.md +1 -1
  23. package/atris/skills/ramp/SKILL.md +7 -7
  24. package/atris/skills/research-search/SKILL.md +7 -7
  25. package/atris/skills/slides/SKILL.md +7 -7
  26. package/atris/skills/tidy/SKILL.md +11 -11
  27. package/atris/skills/wake/SKILL.md +11 -11
  28. package/atris/skills/writing/SKILL.md +5 -5
  29. package/atris/team/_template/SOUL.md +3 -3
  30. package/atris/team/brainstormer/MEMBER.md +2 -2
  31. package/atris/team/launcher/MEMBER.md +2 -2
  32. package/atris/team/mission-lead/MISSION.md +7 -7
  33. package/atris/team/opus-overnight/MISSION.md +16 -16
  34. package/atris/team/opus-overnight/STEERING.md +5 -5
  35. package/atris.md +9 -0
  36. package/bin/atris.js +16 -3
  37. package/commands/campaign.js +17 -2
  38. package/commands/card.js +19 -12
  39. package/commands/engine.js +92 -17
  40. package/commands/gm.js +30 -0
  41. package/commands/idea.js +263 -0
  42. package/commands/init.js +11 -2
  43. package/commands/land.js +39 -18
  44. package/commands/mission.js +4 -3
  45. package/commands/ninety-nine.js +288 -0
  46. package/commands/one-lap.js +46 -27
  47. package/commands/reel.js +12 -7
  48. package/commands/sync.js +10 -2
  49. package/lib/campaign-keep.js +23 -8
  50. package/lib/campaign-loop.js +24 -5
  51. package/lib/claude-boot-block.js +7 -4
  52. package/lib/engine-registry.js +139 -1
  53. package/lib/fleet.js +11 -2
  54. package/lib/gm-bet.js +338 -34
  55. package/lib/gm-phone-page.js +325 -33
  56. package/lib/gm-phone.js +5 -1
  57. package/lib/gm-season.js +14 -0
  58. package/lib/headless-chrome.js +39 -0
  59. package/lib/idea-gate.js +33 -0
  60. package/lib/idea-inbox.js +220 -0
  61. package/lib/idea-ledger.js +196 -0
  62. package/lib/idea-tick.js +384 -0
  63. package/lib/known-commands.js +1 -1
  64. package/lib/member-engine.js +1 -0
  65. package/lib/ninety-nine-sync.js +156 -0
  66. package/lib/ninety-nine.js +247 -0
  67. package/lib/opening-lines.js +94 -0
  68. package/lib/roster-markdown.js +39 -1
  69. package/lib/roster-models.js +1 -1
  70. package/lib/roster-swap.js +353 -0
  71. package/lib/spawn-env.js +29 -2
  72. package/lib/wish-audit.js +14 -0
  73. package/lib/yes-lock.js +358 -241
  74. package/lib/zen-apply.js +5 -3
  75. package/lib/zen.js +13 -1
  76. package/package.json +1 -1
  77. package/templates/business-starter/CLAUDE.md +7 -7
  78. package/templates/business-starter/MAP.md +1 -1
  79. package/templates/business-starter/TODO.md +1 -1
  80. package/templates/business-starter/atris.md +2 -2
  81. package/templates/business-starter/context/README.md +6 -6
  82. package/templates/business-starter/context/live-workspace.md +1 -1
  83. package/templates/business-starter/goals.md +1 -1
  84. package/templates/business-starter/instructions.md +1 -1
  85. package/templates/business-starter/memory.md +4 -4
  86. package/templates/business-starter/persona.md +3 -3
  87. package/templates/business-starter/policies/LESSONS.md +1 -1
  88. package/templates/business-starter/reports/README.md +1 -1
  89. package/templates/business-starter/skills/README.md +1 -1
  90. package/templates/business-starter/team/README.md +1 -1
  91. package/templates/business-starter/team/START_HERE.md +1 -1
  92. package/templates/business-starter/team/_template/SOUL.md +3 -3
  93. package/templates/business-starter/team/comms/SOUL.md +2 -2
  94. package/templates/business-starter/team/ops/SOUL.md +4 -4
  95. package/templates/business-starter/team/research/SOUL.md +3 -3
  96. package/templates/business-starter/wiki/index.md +1 -1
  97. package/templates/research-canonical/CLAUDE.md +7 -7
  98. package/templates/research-canonical/MAP.md +1 -1
  99. package/templates/research-canonical/TODO.md +1 -1
  100. package/templates/research-canonical/atris.md +2 -2
  101. package/templates/research-canonical/context/README.md +8 -8
  102. package/templates/research-canonical/context/live-workspace.md +1 -1
  103. package/templates/research-canonical/goals.md +2 -2
  104. package/templates/research-canonical/instructions.md +1 -1
  105. package/templates/research-canonical/memory.md +4 -4
  106. package/templates/research-canonical/persona.md +1 -1
  107. package/templates/research-canonical/policies/LESSONS.md +1 -1
  108. package/templates/research-canonical/reports/README.md +1 -1
  109. package/templates/research-canonical/skills/README.md +1 -1
  110. package/templates/research-canonical/team/README.md +1 -1
  111. package/templates/research-canonical/team/eval/SOUL.md +1 -1
  112. package/templates/research-canonical/team/experiment/SOUL.md +2 -2
  113. package/templates/research-canonical/team/hypothesis/SOUL.md +3 -3
  114. package/templates/research-canonical/team/literature/SOUL.md +2 -2
  115. package/templates/research-canonical/wiki/briefs/research-program.md +1 -1
  116. package/templates/research-canonical/wiki/index.md +1 -1
  117. package/utils/auth.js +1 -0
@@ -0,0 +1,263 @@
1
+ 'use strict';
2
+
3
+ // atris idea: feed ideas, Atris delegates.
4
+ // atris idea "<sentence>" [--repo <path>] [--verify "<cmd>"] save one idea
5
+ // atris idea [--repo <path>] [--json] list ideas
6
+ // atris idea retry <n> [--repo <path>] put one back in line
7
+ // atris idea tick [--repo <path>] [--max-minutes <n>] build the oldest waiting idea
8
+ // atris idea on|off [--repo <path>] [--dry-run] tick every 10 minutes, or stop
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const { spawnSync } = require('child_process');
14
+ const { argsWantHelp } = require('../lib/noninteractive');
15
+ const {
16
+ addIdea,
17
+ findIdea,
18
+ ideaRefusal,
19
+ oneLine,
20
+ readIdeas,
21
+ updateIdea,
22
+ } = require('../lib/idea-inbox');
23
+ const { ideaScore, readOutcomes, scoreLine } = require('../lib/idea-ledger');
24
+
25
+ const VALUE_FLAGS = new Set(['--repo', '--verify', '--max-minutes']);
26
+
27
+ function ideaUsage() {
28
+ return [
29
+ 'Usage: atris idea "<sentence>" [--repo <path>] [--verify "<cmd>"]',
30
+ ' atris idea [--repo <path>] [--json]',
31
+ ' atris idea retry <n> [--repo <path>] [--verify "<cmd>"]',
32
+ ' atris idea tick [--repo <path>] [--max-minutes <n>]',
33
+ ' atris idea on|off [--repo <path>] [--dry-run]',
34
+ ].join('\n');
35
+ }
36
+
37
+ function parseArgs(args = []) {
38
+ const list = Array.isArray(args) ? args.map(String) : [];
39
+ const flags = {};
40
+ const words = [];
41
+ for (let i = 0; i < list.length; i += 1) {
42
+ const arg = list[i];
43
+ const eq = arg.match(/^(--[a-z-]+)=(.*)$/);
44
+ if (eq && VALUE_FLAGS.has(eq[1])) {
45
+ flags[eq[1].slice(2)] = eq[2];
46
+ } else if (VALUE_FLAGS.has(arg)) {
47
+ flags[arg.slice(2)] = list[i + 1] || '';
48
+ i += 1;
49
+ } else if (arg === '--json') {
50
+ flags.json = true;
51
+ } else if (arg === '--dry-run') {
52
+ flags.dryRun = true;
53
+ } else if (!arg.startsWith('--')) {
54
+ words.push(arg);
55
+ }
56
+ }
57
+ return { flags, words };
58
+ }
59
+
60
+ // The repo an idea belongs to: --repo when given, else the git top of the
61
+ // current folder, else the folder itself.
62
+ function resolveRepo(repo, cwd = process.cwd()) {
63
+ const start = path.resolve(cwd, repo || '.');
64
+ const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: start, encoding: 'utf8' });
65
+ return top.status === 0 && String(top.stdout || '').trim() ? String(top.stdout).trim() : start;
66
+ }
67
+
68
+ const TICK_EVERY_SECONDS = 600;
69
+
70
+ // The timer that runs `atris idea tick` for one repo: launchd on macOS, cron
71
+ // elsewhere, through the same planner `atris campaign keep` uses.
72
+ function ideaTimerPlan(repo, keepDeps = {}) {
73
+ const { planKeep } = require('../lib/campaign-keep');
74
+ return planKeep({
75
+ root: repo,
76
+ home: path.join(os.homedir(), '.atris', 'overnight'),
77
+ atrisBin: path.resolve(__dirname, '..', 'bin', 'atris.js'),
78
+ job: {
79
+ name: 'idea-tick',
80
+ args: ['idea', 'tick'],
81
+ every_seconds: TICK_EVERY_SECONDS,
82
+ marker: 'ATRIS_IDEA_TICK',
83
+ log: `idea-tick-${path.basename(repo)}.log`,
84
+ },
85
+ ...(keepDeps.plan || {}),
86
+ });
87
+ }
88
+
89
+ function ideaTimerOn(plan, keepDeps = {}) {
90
+ if (plan.kind === 'launchd') return fs.existsSync(plan.file);
91
+ const spawn = keepDeps.spawn || spawnSync;
92
+ const listed = spawn('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
93
+ return listed.status === 0 && String(listed.stdout || '').includes(plan.marker);
94
+ }
95
+
96
+ function switchTimer(turnOn, { repo, dryRun, json, log, keepDeps }) {
97
+ const { installKeep, removeKeep } = require('../lib/campaign-keep');
98
+ const plan = ideaTimerPlan(repo, keepDeps);
99
+ const where = plan.kind === 'launchd' ? plan.file : 'your crontab';
100
+ if (dryRun) {
101
+ if (json) log(JSON.stringify({ schema: 'atris.idea_timer.v1', ok: true, on: turnOn, dry_run: true, plan }, null, 2));
102
+ else log(`would ${turnOn ? 'start' : 'stop'} a ${plan.kind} timer in ${where}:\n every ${plan.every_seconds / 60} minutes: ${plan.command}\n in ${repo}\n log ${plan.log_path}`);
103
+ return 0;
104
+ }
105
+ const result = (turnOn ? installKeep : removeKeep)(plan, keepDeps);
106
+ if (json) log(JSON.stringify({ schema: 'atris.idea_timer.v1', on: turnOn, ...result, plan }, null, 2));
107
+ if (!result.ok) {
108
+ if (!json) log(`could not ${turnOn ? 'start' : 'stop'} the timer: ${result.error}`);
109
+ return 1;
110
+ }
111
+ if (json) return 0;
112
+ if (turnOn) {
113
+ log(`on. every ${plan.every_seconds / 60} minutes the oldest idea in ${path.basename(repo)} gets built and checked.`);
114
+ log(`log: ${plan.log_path}`);
115
+ log('stop it: atris idea off');
116
+ } else {
117
+ log(result.removed === false ? 'the idea timer was not on here.' : 'off. ideas stay saved; nothing builds them until you turn it back on.');
118
+ }
119
+ return 0;
120
+ }
121
+
122
+ function ideaJson(idea) {
123
+ return {
124
+ n: idea.n,
125
+ id: idea.id,
126
+ text: idea.text,
127
+ status: idea.status,
128
+ reason: idea.reason || null,
129
+ task_id: idea.task_id || null,
130
+ link: idea.link || null,
131
+ at: idea.at,
132
+ tag: idea.tag || null,
133
+ };
134
+ }
135
+
136
+ // Ideas the loop files about itself land in the loop's own code, so they go
137
+ // to this atris checkout when it is a git checkout, else to the same repo.
138
+ function loopHome(repo) {
139
+ const home = path.resolve(__dirname, '..');
140
+ return fs.existsSync(path.join(home, '.git')) ? home : repo;
141
+ }
142
+
143
+ function renderIdeaList(ideas, { repo, timerOn = null, score = null } = {}) {
144
+ const lines = [];
145
+ if (score && score.builds) {
146
+ lines.push(scoreLine(score));
147
+ lines.push('');
148
+ }
149
+ if (!ideas.length) {
150
+ lines.push('no ideas here yet.');
151
+ lines.push('');
152
+ lines.push('drop one: atris idea "make the inbox search faster"');
153
+ return lines.join('\n');
154
+ }
155
+ lines.push(`ideas in ${path.basename(repo || '.')}: ${ideas.length}`);
156
+ lines.push('');
157
+ const width = String(ideas.length).length;
158
+ const pad = ' '.repeat(width + 4 + 10);
159
+ for (const idea of ideas) {
160
+ const tag = idea.tag === 'loop' ? '[loop] ' : '';
161
+ lines.push(` ${String(idea.n).padStart(width)}. ${idea.status.padEnd(9)} ${tag}${oneLine(idea.text, 80)}`);
162
+ if (idea.status === 'landed' && idea.link) lines.push(`${pad}${idea.link}`);
163
+ if (idea.status === 'refused' && idea.reason) lines.push(`${pad}${oneLine(idea.reason, 100)}`);
164
+ if (idea.status === 'building' && idea.task_id) lines.push(`${pad}task ${idea.task_id}`);
165
+ }
166
+ const waiting = ideas.filter((idea) => idea.status === 'waiting').length;
167
+ if (waiting) {
168
+ lines.push('');
169
+ lines.push(`${waiting} waiting. they build one at a time, oldest first.`);
170
+ if (timerOn === true) lines.push('the next one starts within 10 minutes.');
171
+ if (timerOn === false) lines.push('nothing builds them yet. start it: atris idea on');
172
+ }
173
+ return lines.join('\n');
174
+ }
175
+
176
+ function saveIdea(sentence, { repo, verify, json, log }) {
177
+ const idea = addIdea(repo, sentence, { verify });
178
+ if (json) {
179
+ log(JSON.stringify({ schema: 'atris.idea.v1', ok: true, repo, idea: ideaJson(idea) }, null, 2));
180
+ return 0;
181
+ }
182
+ if (idea.status === 'refused') {
183
+ log(`saved idea ${idea.n}, but I will not build it alone: ${idea.reason}.`);
184
+ log('');
185
+ log('it stays on the list so you can pick it up yourself.');
186
+ return 0;
187
+ }
188
+ log(`saved idea ${idea.n}: ${oneLine(idea.text, 80)}`);
189
+ log('');
190
+ log('it gets built and checked when the idea tick runs, one idea at a time.');
191
+ log('see where it is: atris idea');
192
+ return 0;
193
+ }
194
+
195
+ function retryIdea(ref, { repo, verify = '', json, log }) {
196
+ const idea = findIdea(readIdeas(repo), ref);
197
+ if (!idea) {
198
+ log(`no idea ${ref || '(missing number)'} here. see the list: atris idea`);
199
+ return 2;
200
+ }
201
+ if (idea.status === 'building' || idea.status === 'landed') {
202
+ log(`idea ${idea.n} is ${idea.status}; nothing to retry.`);
203
+ return 2;
204
+ }
205
+ const refusal = ideaRefusal(idea.text);
206
+ if (refusal) {
207
+ log(`idea ${idea.n} still ${refusal.replace(/, needs owner$/, '')}. it needs you, not a retry.`);
208
+ return 2;
209
+ }
210
+ const next = updateIdea(repo, idea.id, { status: 'waiting', reason: '', attempts: 0, ...(verify ? { verify } : {}) });
211
+ if (json) log(JSON.stringify({ schema: 'atris.idea.v1', ok: true, repo, idea: ideaJson(next) }, null, 2));
212
+ else log(`idea ${next.n} is back in line.`);
213
+ return 0;
214
+ }
215
+
216
+ function ideaAtris(args = process.argv.slice(3), { cwd = process.cwd(), log = console.log, keepDeps = {} } = {}) {
217
+ const list = Array.isArray(args) ? args : [];
218
+ if (argsWantHelp(list)) {
219
+ log(ideaUsage());
220
+ return 0;
221
+ }
222
+ const { flags, words } = parseArgs(list);
223
+ const repo = resolveRepo(flags.repo, cwd);
224
+ const json = flags.json === true;
225
+ const verb = words[0];
226
+
227
+ if (verb === 'retry' && words.length <= 2) return retryIdea(words[1], { repo, verify: flags.verify || '', json, log });
228
+ if ((verb === 'on' || verb === 'off') && words.length === 1) {
229
+ return switchTimer(verb === 'on', { repo, dryRun: flags.dryRun === true, json, log, keepDeps });
230
+ }
231
+ if (verb === 'tick' && words.length === 1) {
232
+ const minutes = Number(flags['max-minutes']);
233
+ const { runIdeaTick, DEFAULT_BUILD_SECONDS } = require('../lib/idea-tick');
234
+ return runIdeaTick({
235
+ root: repo,
236
+ log: json ? () => {} : log,
237
+ loopRepo: loopHome(repo),
238
+ maxSeconds: Number.isFinite(minutes) && minutes > 0 ? Math.floor(minutes * 60) : DEFAULT_BUILD_SECONDS,
239
+ }).then((outcome) => {
240
+ if (json) log(JSON.stringify({ schema: 'atris.idea_tick.v1', repo, ...outcome }, null, 2));
241
+ return 0;
242
+ });
243
+ }
244
+ if (verb === 'list' && words.length === 1) words.length = 0;
245
+
246
+ const sentence = words.join(' ').trim();
247
+ if (sentence) return saveIdea(sentence, { repo, verify: flags.verify || '', json, log });
248
+
249
+ const ideas = readIdeas(repo);
250
+ const score = ideaScore(readOutcomes(repo));
251
+ if (json) {
252
+ log(JSON.stringify({ schema: 'atris.idea_list.v1', repo, score: { ...score, line: scoreLine(score) }, ideas: ideas.map(ideaJson) }, null, 2));
253
+ return 0;
254
+ }
255
+ let timerOn = null;
256
+ try { timerOn = ideaTimerOn(ideaTimerPlan(repo, keepDeps), keepDeps); } catch {}
257
+ log(renderIdeaList(ideas, { repo, timerOn, score }));
258
+ return 0;
259
+ }
260
+
261
+ module.exports = {
262
+ ideaAtris,
263
+ };
package/commands/init.js CHANGED
@@ -4,6 +4,7 @@ const { ensureMemberBundle } = require('../lib/member-scaffold');
4
4
  const { ensureExperimentsFramework } = require('./experiments');
5
5
  const { ensureWorkspaceBrain } = require('../lib/workspace-scaffold');
6
6
  const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
7
+ const { openingLinesForRoot, renderAgentsOpeningBlock, upsertAgentsOpeningLines } = require('../lib/opening-lines');
7
8
  const { ensureRunIgnore } = require('../lib/gitignore');
8
9
  const { snapshotBeforeInit, commitInitFiles } = require('../lib/init-commit');
9
10
  const { INTENTS } = require('../lib/intents');
@@ -648,6 +649,8 @@ tools: []
648
649
  // Create agent instruction files for different tools
649
650
  if (withAdapters) {
650
651
  const intentTable = renderIntentTable();
652
+ const openingLines = openingLinesForRoot(process.cwd());
653
+ const agentsOpeningBlock = renderAgentsOpeningBlock(openingLines);
651
654
  const agentInstructions = `# AGENTS.md: Universal Agent Instructions
652
655
 
653
656
  > Works with: Claude Code, Cursor, Codex, OpenClaw, Windsurf, and any AI coding agent.
@@ -666,6 +669,8 @@ atris atris.md
666
669
 
667
670
  This is the Atris boot sequence. Show the output to the user, then respond naturally.
668
671
 
672
+ ${agentsOpeningBlock}
673
+
669
674
  ## You translate
670
675
 
671
676
  The person you work for does not know atris words and never has to learn them. Never tell them to run an atris command or learn a term.
@@ -870,6 +875,10 @@ member -> mission start --verify -> status --status active -> one bounded step -
870
875
  if (agentsVoiceResult.action !== 'unchanged') {
871
876
  markReady('adapters', 'AGENTS.md voice card', '✓ Pinned the Atris voice card in AGENTS.md');
872
877
  }
878
+ const agentsOpeningResult = upsertAgentsOpeningLines(agentsMdFile, openingLines);
879
+ if (agentsOpeningResult.action !== 'unchanged') {
880
+ markReady('adapters', 'AGENTS.md operating rules', '✓ Added the atris.md operating rules to AGENTS.md');
881
+ }
873
882
 
874
883
  // .devin/config.local.json for Devin for Terminal
875
884
  const devinConfigDir = path.join(process.cwd(), '.devin');
@@ -1100,7 +1109,7 @@ Read atris/MAP.md. Begin iteration 1.`;
1100
1109
  const rootClaudeMd = path.join(process.cwd(), 'CLAUDE.md');
1101
1110
  if (fs.existsSync(rootClaudeMd)) {
1102
1111
  const source = fs.readFileSync(rootClaudeMd, 'utf8');
1103
- const result = upsertAtrisClaudeBootBlock(source);
1112
+ const result = upsertAtrisClaudeBootBlock(source, { openingLines: openingLinesForRoot(process.cwd()) });
1104
1113
  fs.writeFileSync(rootClaudeMd, result.content);
1105
1114
  if (result.action === 'repaired') {
1106
1115
  markReady('adapters', 'CLAUDE.md', '✓ repaired atris block in CLAUDE.md (missing end marker)');
@@ -1111,7 +1120,7 @@ Read atris/MAP.md. Begin iteration 1.`;
1111
1120
  }
1112
1121
  } else {
1113
1122
  // Create new CLAUDE.md with just Atris block
1114
- fs.writeFileSync(rootClaudeMd, upsertAtrisClaudeBootBlock().content);
1123
+ fs.writeFileSync(rootClaudeMd, upsertAtrisClaudeBootBlock('', { openingLines: openingLinesForRoot(process.cwd()) }).content);
1115
1124
  markReady('adapters', 'CLAUDE.md', '✓ created CLAUDE.md with atris block');
1116
1125
  }
1117
1126
  }
package/commands/land.js CHANGED
@@ -14,7 +14,8 @@ function runGit(args, opts) {
14
14
  return gitSpawn.runGit(args, opts);
15
15
  }
16
16
 
17
- const CHERRY_CACHE_FILE = path.join('.atris', 'state', 'cherry-cache.json');
17
+ const CHERRY_CACHE_FILE = path.join('atris', 'cherry-cache.json');
18
+ const LEGACY_CHERRY_CACHE_FILE = path.join('.atris', 'state', 'cherry-cache.json');
18
19
  const CHERRY_CACHE_MAX = 2000;
19
20
 
20
21
  // Pluralize a count + noun ("1 change", "2 changes").
@@ -218,29 +219,49 @@ function cherryStats(root, base, ref) {
218
219
  }
219
220
 
220
221
  // `git cherry <base> <ref>` depends only on the two commit shas, so its
221
- // counts are cached per (base sha, ref sha) under .atris/state. The cache is
222
- // only written where .atris already exists; a missing or corrupt file means
223
- // recompute. Entries for refs not seen this run are dropped on save.
222
+ // counts are cached per (base sha, ref sha). The file lives in the shared git
223
+ // dir, so a fresh worktree (which has no .atris yet) reuses what any other
224
+ // worktree of the repo already computed. Only repos that use atris (.atris or
225
+ // atris/ at the root) get a cache; a missing or corrupt file means recompute.
226
+ // Entries for refs not seen this run are dropped on save.
224
227
  function cherryCachePath(root) {
225
- return path.join(root, CHERRY_CACHE_FILE);
228
+ const result = runGit(['rev-parse', '--git-common-dir'], { cwd: root, check: false });
229
+ if (result.status !== 0 || !result.stdout.trim()) return '';
230
+ return path.join(path.resolve(root, result.stdout.trim()), CHERRY_CACHE_FILE);
226
231
  }
227
232
 
228
- function loadCherryCache(root) {
229
- const cache = { file: cherryCachePath(root), entries: {}, used: {}, dirty: false, writable: false };
230
- try {
231
- cache.writable = fs.statSync(path.join(root, '.atris')).isDirectory();
232
- } catch {
233
- cache.writable = false;
234
- }
235
- if (!cache.writable) return cache;
236
- try {
237
- const parsed = JSON.parse(fs.readFileSync(cache.file, 'utf8'));
238
- if (parsed && parsed.version === 1 && parsed.entries && typeof parsed.entries === 'object') {
239
- cache.entries = parsed.entries;
233
+ function usesAtris(root) {
234
+ return ['.atris', 'atris'].some((name) => {
235
+ try {
236
+ return fs.statSync(path.join(root, name)).isDirectory();
237
+ } catch {
238
+ return false;
240
239
  }
240
+ });
241
+ }
242
+
243
+ function readCherryEntries(file) {
244
+ try {
245
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
246
+ if (parsed && parsed.version === 1 && parsed.entries && typeof parsed.entries === 'object') return parsed.entries;
241
247
  } catch {
242
- cache.entries = {};
248
+ // missing or corrupt: recompute
243
249
  }
250
+ return null;
251
+ }
252
+
253
+ function loadCherryCache(root) {
254
+ const cache = { file: '', entries: {}, used: {}, dirty: false, writable: false };
255
+ if (!usesAtris(root)) return cache;
256
+ cache.file = cherryCachePath(root);
257
+ if (!cache.file) return cache;
258
+ cache.writable = true;
259
+ // Older versions kept the cache under .atris/state; read it once so an
260
+ // upgrade does not pay for a cold board.
261
+ const current = readCherryEntries(cache.file);
262
+ const legacy = current ? null : readCherryEntries(path.join(root, LEGACY_CHERRY_CACHE_FILE));
263
+ cache.entries = current || legacy || {};
264
+ cache.dirty = Boolean(legacy);
244
265
  return cache;
245
266
  }
246
267
 
@@ -4814,11 +4814,12 @@ function startMission(args, options = {}) {
4814
4814
  lane: saved.lane,
4815
4815
  verifier: saved.verifier,
4816
4816
  });
4817
- if (saved.yes_lock_parent && saved.yes_locks && saved.yes_locks.steps) {
4818
- // the lock record names the job it came from, before any command is held
4817
+ if ((saved.yes_lock_parent || process.env.ATRIS_HANDOFF_YES_LOCK) && saved.yes_locks && saved.yes_locks.steps) {
4818
+ // the lock record names the member and the job it came from, before any
4819
+ // command is held, so a yes card can say who is asking
4819
4820
  try {
4820
4821
  require('../lib/yes-lock').writeYesLocks(path.join(require('../lib/state-home').atrisHome(), 'state'), saved.id,
4821
- { seal: saved.yes_locks.steps, parentMissionId: saved.yes_lock_parent });
4822
+ { seal: saved.yes_locks.steps, parentMissionId: saved.yes_lock_parent || null, owner: saved.owner || null });
4822
4823
  } catch {}
4823
4824
  }
4824
4825
  const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd(), codeBaseline);