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
@@ -0,0 +1,314 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const readline = require('node:readline');
6
+ const { spawn } = require('node:child_process');
7
+ const { workspaceRoot } = require('../lib/task-db');
8
+ const { collectOrbContext, appendOrbChoice } = require('../lib/orb-context');
9
+ const {
10
+ readOrbScorecard,
11
+ renderOrbScorecard,
12
+ parseOrbScorecardDays,
13
+ } = require('../lib/orb-scorecard');
14
+
15
+ const MAX_VISIBLE_SUGGESTIONS = 9;
16
+ const MAX_CONCURRENT_JOBS = 3;
17
+ const VALID_ENGINES = new Set(['claude', 'codex', 'fast']);
18
+
19
+ function cleanLabel(value, max = 120) {
20
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
21
+ if (!text) return '';
22
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
23
+ }
24
+
25
+ function slugify(value) {
26
+ return String(value || '')
27
+ .toLowerCase()
28
+ .replace(/[^a-z0-9]+/g, '-')
29
+ .replace(/^-+|-+$/g, '')
30
+ .slice(0, 48) || 'run';
31
+ }
32
+
33
+ function parseEngine(args) {
34
+ const equalsArg = args.find((arg) => arg.startsWith('--engine='));
35
+ const flagIndex = args.indexOf('--engine');
36
+ const engine = equalsArg
37
+ ? equalsArg.slice('--engine='.length)
38
+ : flagIndex !== -1
39
+ ? args[flagIndex + 1]
40
+ : 'claude';
41
+ return String(engine || '').toLowerCase();
42
+ }
43
+
44
+ function engineInvocation(engine, prompt) {
45
+ if (engine === 'codex') return { command: 'codex', args: ['exec', prompt] };
46
+ if (engine === 'fast') return { command: 'ax', args: ['--fast', '--print', prompt] };
47
+ return { command: 'claude', args: ['-p', prompt] };
48
+ }
49
+
50
+ function findExecutableOnPath(command, pathValue = process.env.PATH) {
51
+ const candidates = String(command || '').includes(path.sep)
52
+ ? [command]
53
+ : String(pathValue || '').split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, command));
54
+ for (const candidate of candidates) {
55
+ try {
56
+ fs.accessSync(candidate, fs.constants.X_OK);
57
+ return candidate;
58
+ } catch {}
59
+ }
60
+ return null;
61
+ }
62
+
63
+ function formatJobNotification(job) {
64
+ const failed = Boolean(job && job.error) || Number(job && job.exitCode) !== 0;
65
+ return failed
66
+ ? `✗ failed: ${job.label} (o to open)`
67
+ : `✔ ready: ${job.label} (o to open)`;
68
+ }
69
+
70
+ function renderContext(context) {
71
+ console.log('');
72
+ for (const beat of context.beats || []) console.log(beat);
73
+ console.log('');
74
+ const suggestions = (context.suggestions || []).slice(0, MAX_VISIBLE_SUGGESTIONS);
75
+ for (const [index, suggestion] of suggestions.entries()) {
76
+ const side = suggestion.side ? ` · ${suggestion.side}` : '';
77
+ console.log(`${index + 1}. ${suggestion.label}${side}`);
78
+ if (suggestion.reason) console.log(` ${suggestion.reason}`);
79
+ }
80
+ console.log('');
81
+ return suggestions;
82
+ }
83
+
84
+ function renderHint() {
85
+ console.log('Pick 1-9, or type anything. Enter on empty = next; o = open a ready result; q = quit.');
86
+ }
87
+
88
+ function showHelp() {
89
+ console.log('Usage: atris orb [--once] [--engine claude|codex|fast]');
90
+ console.log(' atris orb scorecard [--days N]');
91
+ console.log('');
92
+ console.log('Choose a next move while background engine jobs keep working.');
93
+ }
94
+
95
+ function tailLines(text, count = 60) {
96
+ const lines = String(text || '').split(/\r?\n/);
97
+ if (lines[lines.length - 1] === '') lines.pop();
98
+ return lines.slice(-count).join('\n');
99
+ }
100
+
101
+ async function runOrb(args = []) {
102
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
103
+ showHelp();
104
+ return 0;
105
+ }
106
+
107
+ if (args[0] === 'scorecard') {
108
+ const parsed = parseOrbScorecardDays(args.slice(1));
109
+ if (!parsed.ok) {
110
+ console.error(`orb scorecard: ${parsed.error}`);
111
+ return 2;
112
+ }
113
+ const scorecard = readOrbScorecard(workspaceRoot(), { days: parsed.days });
114
+ console.log(renderOrbScorecard(scorecard));
115
+ return 0;
116
+ }
117
+
118
+ const engine = parseEngine(args);
119
+ if (!VALID_ENGINES.has(engine)) {
120
+ console.error(`Unknown orb engine: ${engine || '(empty)'}. Use claude, codex, or fast.`);
121
+ return 2;
122
+ }
123
+
124
+ const engineCommand = engineInvocation(engine, '').command;
125
+ if (!findExecutableOnPath(engineCommand)) {
126
+ console.error(`orb warning: engine binary "${engineCommand}" is missing from PATH; picks will fail until it is installed.`);
127
+ }
128
+
129
+ const root = workspaceRoot();
130
+ let context = await collectOrbContext(root, { threads: [] });
131
+ let visibleSuggestions = renderContext(context);
132
+ const once = args.includes('--once') || !process.stdin.isTTY || !process.stdout.isTTY;
133
+ if (once) return 0;
134
+
135
+ renderHint();
136
+ const runsDir = path.join(root, '.atris', 'state', 'orb-runs');
137
+ const indexPath = path.join(runsDir, 'index.jsonl');
138
+ const jobs = [];
139
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
140
+ let closing = false;
141
+
142
+ function prompt() {
143
+ if (!closing) rl.prompt();
144
+ }
145
+
146
+ function notifyReady(job) {
147
+ if (closing) return;
148
+ readline.clearLine(process.stdout, 0);
149
+ readline.cursorTo(process.stdout, 0);
150
+ console.log(formatJobNotification(job));
151
+ prompt();
152
+ }
153
+
154
+ function runningJobs() {
155
+ return jobs.filter((job) => job.status === 'running');
156
+ }
157
+
158
+ function duplicateInFlight(label) {
159
+ const normalized = cleanLabel(label).toLowerCase();
160
+ return runningJobs().some((job) => cleanLabel(job.label).toLowerCase() === normalized);
161
+ }
162
+
163
+ function finishJob(job, code, error) {
164
+ if (job.status !== 'running') return;
165
+ job.exitCode = Number.isInteger(code) ? code : 1;
166
+ job.error = error ? String(error.message || error) : null;
167
+ job.status = job.exitCode === 0 && !job.error ? 'ready' : 'failed';
168
+ job.unread = true;
169
+ job.durationMs = Date.now() - job.startedAtMs;
170
+ if (error) {
171
+ try {
172
+ fs.appendFileSync(job.logPath, `\nengine launch failed: ${error.message || error}\n`, 'utf8');
173
+ } catch {}
174
+ }
175
+ const record = {
176
+ status: job.status,
177
+ ts: new Date().toISOString(),
178
+ label: job.label,
179
+ kind: job.kind,
180
+ engine: job.engine,
181
+ exitCode: job.exitCode,
182
+ durationMs: job.durationMs,
183
+ logPath: path.relative(root, job.logPath),
184
+ error: job.error,
185
+ };
186
+ fs.promises.appendFile(indexPath, `${JSON.stringify(record)}\n`, 'utf8').catch(() => {});
187
+ notifyReady(job);
188
+ }
189
+
190
+ async function dispatch(choice) {
191
+ const label = cleanLabel(choice.label);
192
+ if (duplicateInFlight(label)) {
193
+ console.log('already working on that');
194
+ return;
195
+ }
196
+ const running = runningJobs().length;
197
+ if (running >= MAX_CONCURRENT_JOBS) {
198
+ console.log(`that will have to wait - ${running} jobs running`);
199
+ return;
200
+ }
201
+
202
+ const appended = await appendOrbChoice(root, label);
203
+ if (!appended.ok) {
204
+ console.error(`Could not update now.md: ${appended.error}`);
205
+ return;
206
+ }
207
+
208
+ fs.mkdirSync(runsDir, { recursive: true });
209
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
210
+ const logPath = path.join(runsDir, `${stamp}-${slugify(label)}.log`);
211
+ const logFd = fs.openSync(logPath, 'a');
212
+ const invocation = engineInvocation(engine, choice.prompt);
213
+ let child;
214
+ try {
215
+ child = spawn(invocation.command, invocation.args, {
216
+ cwd: root,
217
+ detached: true,
218
+ stdio: ['ignore', logFd, logFd],
219
+ });
220
+ } finally {
221
+ fs.closeSync(logFd);
222
+ }
223
+
224
+ const startedAt = new Date().toISOString();
225
+ const relativeLogPath = path.relative(root, logPath);
226
+ if (Number.isInteger(child.pid) && child.pid > 0) {
227
+ fs.appendFileSync(indexPath, `${JSON.stringify({
228
+ status: 'dispatched',
229
+ ts: startedAt,
230
+ label,
231
+ kind: choice.kind || 'freeform',
232
+ engine,
233
+ logPath: relativeLogPath,
234
+ pid: child.pid,
235
+ })}\n`, 'utf8');
236
+ }
237
+
238
+ const job = {
239
+ label,
240
+ kind: choice.kind || 'freeform',
241
+ engine,
242
+ startedAt,
243
+ startedAtMs: Date.now(),
244
+ logPath,
245
+ child,
246
+ status: 'running',
247
+ unread: false,
248
+ };
249
+ jobs.push(job);
250
+ child.once('error', (error) => finishJob(job, 1, error));
251
+ child.once('exit', (code) => finishJob(job, code));
252
+ child.unref();
253
+ console.log(`working: ${label}`);
254
+ }
255
+
256
+ async function refresh() {
257
+ context = await collectOrbContext(root, { threads: [] });
258
+ visibleSuggestions = renderContext(context);
259
+ renderHint();
260
+ }
261
+
262
+ async function openReady() {
263
+ const job = jobs.find((candidate) => ['ready', 'failed'].includes(candidate.status) && candidate.unread);
264
+ if (!job) {
265
+ console.log('No ready result yet.');
266
+ return;
267
+ }
268
+ job.unread = false;
269
+ try {
270
+ const output = tailLines(await fs.promises.readFile(job.logPath, 'utf8'));
271
+ console.log(`\n${job.label}`);
272
+ console.log(output || '(empty result)');
273
+ console.log('');
274
+ } catch (error) {
275
+ console.error(`Could not open result: ${error.message || error}`);
276
+ }
277
+ }
278
+
279
+ return await new Promise((resolve) => {
280
+ rl.setPrompt('> ');
281
+ prompt();
282
+ rl.on('line', async (line) => {
283
+ rl.pause();
284
+ const input = line.trim();
285
+ if (input.toLowerCase() === 'q') {
286
+ closing = true;
287
+ rl.close();
288
+ return;
289
+ }
290
+ if (!input) {
291
+ await refresh();
292
+ } else if (input.toLowerCase() === 'o') {
293
+ await openReady();
294
+ } else if (/^[1-9]$/.test(input) && visibleSuggestions[Number(input) - 1]) {
295
+ await dispatch(visibleSuggestions[Number(input) - 1]);
296
+ } else {
297
+ await dispatch({ label: input, kind: 'freeform', prompt: input });
298
+ }
299
+ rl.resume();
300
+ prompt();
301
+ });
302
+ rl.once('close', () => resolve(0));
303
+ });
304
+ }
305
+
306
+ module.exports = {
307
+ runOrb,
308
+ engineInvocation,
309
+ findExecutableOnPath,
310
+ formatJobNotification,
311
+ parseEngine,
312
+ renderContext,
313
+ tailLines,
314
+ };
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ function slugify(value, fallback = 'atris-pack') {
8
+ return String(value || fallback)
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9]+/g, '-')
11
+ .replace(/^-+|-+$/g, '') || fallback;
12
+ }
13
+
14
+ function titleFromSlug(slug) {
15
+ return String(slug || 'atris-pack')
16
+ .split('-')
17
+ .filter(Boolean)
18
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
19
+ .join(' ') || 'Atris Pack';
20
+ }
21
+
22
+ function takeValue(args, name) {
23
+ const prefix = `${name}=`;
24
+ for (let i = 0; i < args.length; i += 1) {
25
+ const arg = String(args[i]);
26
+ if (arg.startsWith(prefix)) {
27
+ args.splice(i, 1);
28
+ return arg.slice(prefix.length);
29
+ }
30
+ if (arg === name) {
31
+ const value = args[i + 1];
32
+ if (value === undefined || String(value).startsWith('--')) {
33
+ throw new Error(`${name} requires a value`);
34
+ }
35
+ args.splice(i, 2);
36
+ return value;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function takeFlag(args, name) {
43
+ const index = args.indexOf(name);
44
+ if (index === -1) return false;
45
+ args.splice(index, 1);
46
+ return true;
47
+ }
48
+
49
+ function shellQuote(value) {
50
+ const text = String(value || '.');
51
+ if (/^[A-Za-z0-9_./:-]+$/.test(text)) return text;
52
+ return `'${text.replace(/'/g, "'\\''")}'`;
53
+ }
54
+
55
+ function writeJson(filePath, value) {
56
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
57
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
58
+ }
59
+
60
+ function isNonEmptyTarget(targetDir) {
61
+ if (!fs.existsSync(targetDir)) return false;
62
+ const stat = fs.statSync(targetDir);
63
+ if (!stat.isDirectory()) return true;
64
+ return fs.readdirSync(targetDir).length > 0;
65
+ }
66
+
67
+ // the registry requires a non-empty author, so fall back through the local
68
+ // login and finally a placeholder the filler agent is told to replace
69
+ function defaultAuthor() {
70
+ try {
71
+ const creds = JSON.parse(
72
+ fs.readFileSync(path.join(os.homedir(), '.atris', 'credentials.json'), 'utf8')
73
+ );
74
+ if (creds && typeof creds.email === 'string' && creds.email.trim()) return creds.email.trim();
75
+ } catch {
76
+ // not logged in; use the placeholder
77
+ }
78
+ return 'unknown author';
79
+ }
80
+
81
+ function buildCraftManifest(topic, slug) {
82
+ const title = titleFromSlug(slug);
83
+ return {
84
+ name: slug,
85
+ slug,
86
+ title,
87
+ description: `research pack on ${topic}, in progress`,
88
+ author: defaultAuthor(),
89
+ tags: [],
90
+ version: '0.0.1',
91
+ versions: [],
92
+ };
93
+ }
94
+
95
+ function buildAtrisBoot(topic) {
96
+ return [
97
+ '# research pack boot',
98
+ '',
99
+ `you are filling a research pack on ${topic}.`,
100
+ '',
101
+ 'method:',
102
+ '- gather sources',
103
+ '- write wiki-style pages under wiki/ (one concept per file, sources cited inline as plain urls)',
104
+ "- keep atris/now.md updated with what is done and what is next (next = researchable questions, not tasks)",
105
+ '- never fabricate citations',
106
+ '- prefer primary sources; a secondary source is fine when the primary is unreachable, but say so inline next to the citation',
107
+ '- add a not-affiliated disclaimer in README.md if the topic is a person or company',
108
+ '',
109
+ 'when the pack answers its topic well, bump pack.json to 0.1.0 and it is ready to publish with:',
110
+ '',
111
+ ' atris pack publish --push',
112
+ '',
113
+ ].join('\n');
114
+ }
115
+
116
+ function buildNowMd(topic) {
117
+ return `nothing gathered yet. start with the 3 most load-bearing questions about ${topic}.\n`;
118
+ }
119
+
120
+ function buildReadme(topic, title) {
121
+ return [
122
+ `# ${title}`,
123
+ '',
124
+ `research pack on ${topic}.`,
125
+ '',
126
+ 'run your agent in this folder.',
127
+ '',
128
+ ].join('\n');
129
+ }
130
+
131
+ function buildWikiIndex() {
132
+ return [
133
+ '# wiki index',
134
+ '',
135
+ 'pages: none yet',
136
+ '',
137
+ ].join('\n');
138
+ }
139
+
140
+ function craftPack(rawArgs, cwd = process.cwd()) {
141
+ const args = [...rawArgs];
142
+ const topic = args.shift();
143
+ if (!topic || topic === 'help' || topic === '--help' || topic === '-h') {
144
+ return { needsHelp: true };
145
+ }
146
+ const targetArg = takeValue(args, '--dir');
147
+ const force = takeFlag(args, '--force');
148
+ if (args.length) throw new Error(`unknown pack craft argument: ${args.join(' ')}`);
149
+
150
+ const slug = slugify(topic);
151
+ const targetDir = path.resolve(cwd, targetArg || slug);
152
+ if (isNonEmptyTarget(targetDir) && !force) {
153
+ const displayTarget = path.relative(cwd, targetDir) || targetDir;
154
+ throw new Error(`target is not empty: ${displayTarget}. rerun with --force to overwrite.`);
155
+ }
156
+
157
+ const manifest = buildCraftManifest(topic, slug);
158
+ const title = manifest.title;
159
+
160
+ fs.mkdirSync(path.join(targetDir, 'atris'), { recursive: true });
161
+ fs.mkdirSync(path.join(targetDir, 'wiki'), { recursive: true });
162
+ writeJson(path.join(targetDir, 'pack.json'), manifest);
163
+ fs.writeFileSync(path.join(targetDir, 'atris', 'atris.md'), buildAtrisBoot(topic));
164
+ fs.writeFileSync(path.join(targetDir, 'atris', 'now.md'), buildNowMd(topic));
165
+ fs.writeFileSync(path.join(targetDir, 'README.md'), buildReadme(topic, title));
166
+ fs.writeFileSync(path.join(targetDir, 'wiki', 'index.md'), buildWikiIndex());
167
+
168
+ const displayTarget = path.relative(cwd, targetDir) || '.';
169
+ console.log(`created ${slug} -> ${displayTarget}`);
170
+ console.log(`cd ${shellQuote(displayTarget)} && claude`);
171
+ return 0;
172
+ }
173
+
174
+ module.exports = {
175
+ craftPack,
176
+ slugify,
177
+ isNonEmptyTarget,
178
+ buildCraftManifest,
179
+ };