@pleri/olam-cli 0.1.159 → 0.1.161

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 (88) hide show
  1. package/README.md +11 -0
  2. package/dist/agent-stream/agent-sdk-to-chunks.js +3 -0
  3. package/dist/agent-stream/driver-runner.js +9 -4
  4. package/dist/agent-stream/host-driver-launch.js +48 -0
  5. package/dist/commands/bootstrap.d.ts +15 -0
  6. package/dist/commands/bootstrap.d.ts.map +1 -1
  7. package/dist/commands/bootstrap.js +30 -1
  8. package/dist/commands/bootstrap.js.map +1 -1
  9. package/dist/commands/flywheel/check-persona-skeleton.d.ts +30 -2
  10. package/dist/commands/flywheel/check-persona-skeleton.d.ts.map +1 -1
  11. package/dist/commands/flywheel/check-persona-skeleton.js +143 -6
  12. package/dist/commands/flywheel/check-persona-skeleton.js.map +1 -1
  13. package/dist/commands/flywheel/diversity-check.d.ts +12 -2
  14. package/dist/commands/flywheel/diversity-check.d.ts.map +1 -1
  15. package/dist/commands/flywheel/diversity-check.js +56 -6
  16. package/dist/commands/flywheel/diversity-check.js.map +1 -1
  17. package/dist/commands/flywheel/index.d.ts.map +1 -1
  18. package/dist/commands/flywheel/index.js +2 -0
  19. package/dist/commands/flywheel/index.js.map +1 -1
  20. package/dist/commands/flywheel/install-shims.d.ts +36 -3
  21. package/dist/commands/flywheel/install-shims.d.ts.map +1 -1
  22. package/dist/commands/flywheel/install-shims.js +118 -7
  23. package/dist/commands/flywheel/install-shims.js.map +1 -1
  24. package/dist/commands/flywheel/k10-measure.d.ts +12 -2
  25. package/dist/commands/flywheel/k10-measure.d.ts.map +1 -1
  26. package/dist/commands/flywheel/k10-measure.js +55 -6
  27. package/dist/commands/flywheel/k10-measure.js.map +1 -1
  28. package/dist/commands/flywheel/migrate-overlays.d.ts +115 -0
  29. package/dist/commands/flywheel/migrate-overlays.d.ts.map +1 -0
  30. package/dist/commands/flywheel/migrate-overlays.js +766 -0
  31. package/dist/commands/flywheel/migrate-overlays.js.map +1 -0
  32. package/dist/commands/flywheel/sanitize-persona-output.d.ts +33 -2
  33. package/dist/commands/flywheel/sanitize-persona-output.d.ts.map +1 -1
  34. package/dist/commands/flywheel/sanitize-persona-output.js +94 -6
  35. package/dist/commands/flywheel/sanitize-persona-output.js.map +1 -1
  36. package/dist/commands/memory/index.d.ts.map +1 -1
  37. package/dist/commands/memory/index.js +2 -0
  38. package/dist/commands/memory/index.js.map +1 -1
  39. package/dist/commands/memory/install-hooks.d.ts +22 -0
  40. package/dist/commands/memory/install-hooks.d.ts.map +1 -0
  41. package/dist/commands/memory/install-hooks.js +156 -0
  42. package/dist/commands/memory/install-hooks.js.map +1 -0
  43. package/dist/commands/skills-doctor.js +2 -2
  44. package/dist/commands/skills-doctor.js.map +1 -1
  45. package/dist/commands/skills-source.d.ts.map +1 -1
  46. package/dist/commands/skills-source.js +10 -0
  47. package/dist/commands/skills-source.js.map +1 -1
  48. package/dist/commands/skills.d.ts.map +1 -1
  49. package/dist/commands/skills.js +169 -1
  50. package/dist/commands/skills.js.map +1 -1
  51. package/dist/image-digests.json +7 -7
  52. package/dist/index.js +4361 -1768
  53. package/dist/lib/bootstrap-kubernetes.d.ts +42 -0
  54. package/dist/lib/bootstrap-kubernetes.d.ts.map +1 -0
  55. package/dist/lib/bootstrap-kubernetes.js +287 -0
  56. package/dist/lib/bootstrap-kubernetes.js.map +1 -0
  57. package/dist/lib/config.d.ts.map +1 -1
  58. package/dist/lib/config.js +6 -1
  59. package/dist/lib/config.js.map +1 -1
  60. package/dist/lib/flywheel-probes.d.ts +58 -0
  61. package/dist/lib/flywheel-probes.d.ts.map +1 -0
  62. package/dist/lib/flywheel-probes.js +163 -0
  63. package/dist/lib/flywheel-probes.js.map +1 -0
  64. package/dist/lib/shim-generator.d.ts +51 -0
  65. package/dist/lib/shim-generator.d.ts.map +1 -0
  66. package/dist/lib/shim-generator.js +88 -0
  67. package/dist/lib/shim-generator.js.map +1 -0
  68. package/dist/lib/skills-apply-overlays.d.ts +35 -0
  69. package/dist/lib/skills-apply-overlays.d.ts.map +1 -0
  70. package/dist/lib/skills-apply-overlays.js +243 -0
  71. package/dist/lib/skills-apply-overlays.js.map +1 -0
  72. package/dist/mcp-server.js +1106 -453
  73. package/hermes-bundle/version.json +1 -1
  74. package/host-cp/k8s/manifests/50-deployment.yaml +1 -1
  75. package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +1 -1
  76. package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
  77. package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +1 -1
  78. package/host-cp/k8s/manifests/memory-service/30-configmap.yaml +11 -0
  79. package/host-cp/k8s/manifests/memory-service/35-configmap-iii-config.yaml +76 -0
  80. package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +11 -1
  81. package/host-cp/observability/grafana-port-forward.sh +273 -0
  82. package/host-cp/observability/kyverno-cardinality-mutate.sh +452 -0
  83. package/host-cp/observability/loki-ingest.sh +243 -0
  84. package/host-cp/observability/prom-no-double-grafana.sh +301 -0
  85. package/host-cp/src/crystallize-planning.mjs +261 -0
  86. package/host-cp/src/plan-chat-service.mjs +84 -2
  87. package/host-cp/src/planning-sessions.mjs +270 -0
  88. package/package.json +1 -1
@@ -0,0 +1,766 @@
1
+ /**
2
+ * `olam flywheel migrate-overlays` — operator-local overlay-layer migration.
3
+ *
4
+ * Scans `~/.claude/{skills,agents}.overrides/` recursively for hardcoded
5
+ * `~/.claude/scripts/<name>` references matching the SHIM_TARGETS registry;
6
+ * sed-replaces each match with the corresponding `olam flywheel <subcmd>`
7
+ * invocation. Each operator runs this once to migrate THEIR layer 3
8
+ * (per-user overlay files) from the legacy script-path contract to the new
9
+ * flywheel CLI surface.
10
+ *
11
+ * Closes K1 OQ23 (pass-3 resolution): the migration helper that scales
12
+ * the namespace migration beyond just this operator's overlays. Each
13
+ * operator with their own `.overrides/` tree runs this command once.
14
+ *
15
+ * Replaces:
16
+ * `bash ~/.claude/scripts/emit-breadcrumb.sh ...` → `olam flywheel emit-breadcrumb ...`
17
+ * `python3 ~/.claude/scripts/sanitize-persona-output.py ...` → `olam flywheel sanitize-persona-output ...`
18
+ * `~/.claude/scripts/k5-rubric-helper.py` → `olam flywheel k5-validate`
19
+ * etc. (full set per SHIM_TARGETS registry at @olam/core/lib/shim-targets)
20
+ *
21
+ * Flags:
22
+ * --dry-run — preview changes without writing
23
+ * --target-dir <path> — override the search root (default: ~/.claude/); for tests
24
+ * --json — emit summary as JSON for tooling
25
+ *
26
+ * --push — PUSH mode: copy overlays into atlas-toolbox clone
27
+ * --target <name> — required with --push; name of a registered skill source
28
+ *
29
+ * Idempotent: re-runs against already-migrated overlays produce no diff.
30
+ *
31
+ * Plan reference: docs/plans/olam-flywheel-cli-namespace/phase-c-tasks.md § C7
32
+ * Phase B: docs/plans/member-overlays-sync/phase-b-tasks.md
33
+ */
34
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from 'node:fs';
35
+ import { spawnSync } from 'node:child_process';
36
+ import { homedir } from 'node:os';
37
+ import { dirname, join, relative } from 'node:path';
38
+ import { SHIM_TARGETS } from '@olam/core/src/lib/shim-targets.js';
39
+ import { listSkillSources, skillSourceClonePath, } from '@olam/core/src/skill-sources/index.js';
40
+ // ---------------------------------------------------------------------------
41
+ // Rewrite-mode helpers (Phase C7 — unchanged)
42
+ // ---------------------------------------------------------------------------
43
+ function escapeRegex(s) {
44
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
45
+ }
46
+ function applyReplacements(content) {
47
+ let working = content;
48
+ const replacements = [];
49
+ for (const target of SHIM_TARGETS) {
50
+ const escaped = escapeRegex(target.basename);
51
+ const patterns = [
52
+ { re: new RegExp(`(?:bash |python3 )?~/\\.claude/scripts/${escaped}`, 'g'), label: 'tilde' },
53
+ { re: new RegExp(`(?:bash |python3 )?\\$HOME/\\.claude/scripts/${escaped}`, 'g'), label: 'home' },
54
+ { re: new RegExp(`(?:bash |python3 )?/Users/[\\w.-]+/\\.claude/scripts/${escaped}`, 'g'), label: 'absolute' },
55
+ ];
56
+ let count = 0;
57
+ for (const { re } of patterns) {
58
+ const matches = working.match(re);
59
+ if (matches !== null) {
60
+ count += matches.length;
61
+ working = working.replace(re, target.replacedBy);
62
+ }
63
+ }
64
+ if (count > 0) {
65
+ replacements.push({ basename: target.basename, count, replacedBy: target.replacedBy });
66
+ }
67
+ }
68
+ return { newContent: working, replacements };
69
+ }
70
+ function walkOverlayFiles(dir) {
71
+ const found = [];
72
+ let entries;
73
+ try {
74
+ entries = readdirSync(dir);
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ for (const entry of entries) {
80
+ const fullPath = join(dir, entry);
81
+ let stat;
82
+ try {
83
+ stat = statSync(fullPath);
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ if (stat.isDirectory()) {
89
+ found.push(...walkOverlayFiles(fullPath));
90
+ }
91
+ else if (stat.isFile() && /\.(md|json|sh)$/.test(entry)) {
92
+ found.push(fullPath);
93
+ }
94
+ }
95
+ return found;
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Push-mode helpers (Phase B)
99
+ // ---------------------------------------------------------------------------
100
+ /**
101
+ * Error thrown by push-mode operations. Carries an exit code so callers
102
+ * can distinguish between different failure scenarios.
103
+ */
104
+ export class PushError extends Error {
105
+ exitCode;
106
+ constructor(exitCode, message) {
107
+ super(message);
108
+ this.exitCode = exitCode;
109
+ this.name = 'PushError';
110
+ }
111
+ }
112
+ const ATLAS_USER_RE = /^[a-z0-9][a-z0-9_-]{0,38}$/;
113
+ /**
114
+ * Validate that `value` matches the GitHub login shape used as atlas-user.
115
+ * Throws PushError on mismatch so callers handle it via the standard error path.
116
+ */
117
+ function assertValidAtlasUser(value) {
118
+ if (!ATLAS_USER_RE.test(value)) {
119
+ throw new PushError(1, `invalid atlasUser "${value}" in ~/.claude/.atlas-user — must match /^[a-z0-9][a-z0-9_-]{0,38}$/`);
120
+ }
121
+ }
122
+ /**
123
+ * Read atlas-user from ~/.claude/.atlas-user (or OLAM_CLAUDE_DIR override).
124
+ */
125
+ function resolveAtlasUser(opts) {
126
+ if (opts._testAtlasUser !== undefined) {
127
+ const v = opts._testAtlasUser;
128
+ if (!v)
129
+ return null;
130
+ assertValidAtlasUser(v);
131
+ return v;
132
+ }
133
+ const claudeDir = opts._testClaudeDir ?? process.env['OLAM_CLAUDE_DIR'] ?? join(homedir(), '.claude');
134
+ const f = join(claudeDir, '.atlas-user');
135
+ if (existsSync(f)) {
136
+ const v = readFileSync(f, 'utf-8').trim();
137
+ if (v.length === 0)
138
+ return null;
139
+ assertValidAtlasUser(v);
140
+ return v;
141
+ }
142
+ return null;
143
+ }
144
+ /**
145
+ * Run a git command, returning stdout. Throws on non-zero exit.
146
+ */
147
+ function runGit(args, cwd) {
148
+ const result = spawnSync('git', args, {
149
+ cwd,
150
+ encoding: 'utf-8',
151
+ stdio: ['ignore', 'pipe', 'pipe'],
152
+ });
153
+ if (result.status !== 0) {
154
+ const errMsg = (result.stderr ?? '').trim() || `git ${args[0]} failed`;
155
+ throw new Error(errMsg);
156
+ }
157
+ return (result.stdout ?? '').trim();
158
+ }
159
+ /**
160
+ * Walk overlay source directories for push-mode.
161
+ * Returns array of { srcFile, overlayKind: 'skills' | 'agents', relPath }.
162
+ * Skips paths matching *.overrides.local/ (Decision #4).
163
+ */
164
+ function walkPushSourceFiles(claudeDir) {
165
+ const result = [];
166
+ const roots = [
167
+ { dir: join(claudeDir, 'skills.overrides'), overlayKind: 'skills' },
168
+ { dir: join(claudeDir, 'agents.overrides'), overlayKind: 'agents' },
169
+ ];
170
+ for (const { dir, overlayKind } of roots) {
171
+ const files = walkOverlayFiles(dir);
172
+ for (const f of files) {
173
+ // Defensive skip for .overrides.local/ paths (Decision #4)
174
+ if (f.includes('.overrides.local'))
175
+ continue;
176
+ const relPath = relative(dir, f);
177
+ result.push({ srcFile: f, overlayKind, relPath });
178
+ }
179
+ }
180
+ return result;
181
+ }
182
+ /**
183
+ * Generate branch name. If exists on origin, auto-suffix -2, -3, etc.
184
+ */
185
+ function resolveBranchName(clonePath, atlasUser) {
186
+ const now = new Date();
187
+ const pad = (n) => String(n).padStart(2, '0');
188
+ const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
189
+ const base = `feat/${atlasUser}/overlay-update-${ts}`;
190
+ // Check if candidate exists on origin OR locally; auto-suffix if so.
191
+ let candidate = base;
192
+ let suffix = 2;
193
+ while (true) {
194
+ let existsOnOrigin = false;
195
+ let existsLocally = false;
196
+ try {
197
+ const out = runGit(['ls-remote', 'origin', candidate], clonePath);
198
+ existsOnOrigin = out.length > 0;
199
+ }
200
+ catch {
201
+ // ls-remote failure (offline, etc.) — assume name is free on origin
202
+ }
203
+ if (!existsOnOrigin) {
204
+ try {
205
+ runGit(['rev-parse', '--verify', `refs/heads/${candidate}`], clonePath);
206
+ existsLocally = true;
207
+ }
208
+ catch {
209
+ // non-zero exit means branch does not exist locally
210
+ }
211
+ }
212
+ if (!existsOnOrigin && !existsLocally)
213
+ break;
214
+ candidate = `${base}-${suffix}`;
215
+ suffix += 1;
216
+ if (suffix > 100)
217
+ break; // safety ceiling
218
+ }
219
+ return candidate;
220
+ }
221
+ /**
222
+ * Run the pre-flight gate against the clone.
223
+ *
224
+ * When `dryRunWarnOnly` is true, blocking conditions emit stderr WARNINGs
225
+ * instead of returning error strings — the dry-run path collects warnings
226
+ * so the operator can see what would block a real run. The returned
227
+ * `preflightWarnings` array contains any warnings emitted.
228
+ *
229
+ * When `dryRunWarnOnly` is false (default / real-run), the first blocking
230
+ * condition returns immediately with `error` set.
231
+ */
232
+ function runPreFlight(clonePath, forceCurrentBranch, dryRunWarnOnly = false) {
233
+ const warnings = [];
234
+ const warn = (msg) => {
235
+ process.stderr.write(`[dry-run] pre-flight WARNING: ${msg}\n`);
236
+ warnings.push(msg);
237
+ };
238
+ // 1. Dirty working tree
239
+ let statusOut;
240
+ try {
241
+ statusOut = runGit(['status', '--porcelain'], clonePath);
242
+ }
243
+ catch (err) {
244
+ const msg = `pre-flight: failed to check clone status: ${err instanceof Error ? err.message : String(err)}`;
245
+ return { error: msg, preflightWarnings: warnings };
246
+ }
247
+ if (statusOut.length > 0) {
248
+ const msg = [
249
+ `pre-flight: clone has uncommitted changes at "${clonePath}".`,
250
+ ` Resolve before pushing overlays.`,
251
+ ` Actions: [Stash and continue] / [Abort]`,
252
+ ].join('\n');
253
+ if (dryRunWarnOnly) {
254
+ warn(msg);
255
+ }
256
+ else {
257
+ return { error: msg, preflightWarnings: warnings };
258
+ }
259
+ }
260
+ // 2. Not on main branch
261
+ let currentBranch;
262
+ try {
263
+ currentBranch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], clonePath);
264
+ }
265
+ catch (err) {
266
+ const msg = `pre-flight: failed to determine current branch: ${err instanceof Error ? err.message : String(err)}`;
267
+ return { error: msg, preflightWarnings: warnings };
268
+ }
269
+ if (currentBranch !== 'main' && !forceCurrentBranch) {
270
+ const msg = [
271
+ `pre-flight: clone is on branch "${currentBranch}" (expected "main").`,
272
+ ` Switch to main first, or use --force-current-branch to continue on "${currentBranch}".`,
273
+ ` Actions: [Switch to main first] / [Continue on current branch]`,
274
+ ].join('\n');
275
+ if (dryRunWarnOnly) {
276
+ warn(msg);
277
+ }
278
+ else {
279
+ return { error: msg, preflightWarnings: warnings };
280
+ }
281
+ }
282
+ // 3. Clone behind origin/main — warn but allow continue (both modes)
283
+ try {
284
+ runGit(['fetch', 'origin'], clonePath);
285
+ const behindCount = runGit(['rev-list', '--count', 'HEAD..origin/main'], clonePath);
286
+ const n = parseInt(behindCount, 10);
287
+ if (!isNaN(n) && n > 0) {
288
+ process.stderr.write(`warning: clone is ${n} commit(s) behind origin/main.\n` +
289
+ ` Run \`git -C "${clonePath}" pull\` first, or continue with stale base.\n` +
290
+ ` Actions: [Pull first] / [Continue]\n`);
291
+ }
292
+ }
293
+ catch {
294
+ // fetch failure is non-fatal (offline); proceed
295
+ }
296
+ return { error: null, preflightWarnings: warnings };
297
+ }
298
+ /**
299
+ * Core push-overlays implementation (Phase B).
300
+ * Returns PushSummary. Throws PushError on validation/pre-flight failures
301
+ * (lets callers handle process.exit vs test assertions cleanly).
302
+ */
303
+ export function pushOverlays(opts) {
304
+ // B2 — Resolve atlas-user
305
+ const atlasUser = resolveAtlasUser(opts);
306
+ if (atlasUser === null) {
307
+ throw new PushError(1, `~/.claude/.atlas-user not set.\n` +
308
+ ` Run \`olam skills atlas-user set <name>\` to configure your atlas user.`);
309
+ }
310
+ // B2 — Resolve skill source (test injection or registry lookup)
311
+ let clonePath;
312
+ if (opts._testClonePath !== undefined) {
313
+ clonePath = opts._testClonePath;
314
+ }
315
+ else {
316
+ // Resolve from registry (or test-injected sources)
317
+ const sources = opts._testSkillSources ?? listSkillSources();
318
+ const source = sources.find((s) => s.name === opts.target) ?? null;
319
+ if (source === null) {
320
+ const available = sources.map((s) => s.name).join(', ') || '(none)';
321
+ throw new PushError(1, `target source "${opts.target}" not registered.\n` +
322
+ ` Available sources: ${available}\n` +
323
+ ` Run \`olam skills source add\` to register one.\n` +
324
+ ` Actions: [List sources] / [Cancel]`);
325
+ }
326
+ clonePath = skillSourceClonePath(source.id);
327
+ }
328
+ // Determine source overlay directory
329
+ const claudeDir = opts._testClaudeDir ?? opts.targetDir ?? process.env['OLAM_CLAUDE_DIR'] ?? join(homedir(), '.claude');
330
+ const sourceFiles = walkPushSourceFiles(claudeDir);
331
+ // Dry-run: surface pre-flight warnings, then preview copy actions (no clone mutation)
332
+ if (opts.dryRun === true) {
333
+ const { preflightWarnings } = runPreFlight(clonePath, opts.forceCurrentBranch === true, true);
334
+ if (preflightWarnings.length > 0) {
335
+ process.stderr.write(`[dry-run] pre-flight warnings (would BLOCK in real run):\n` +
336
+ preflightWarnings.map((w) => ` - ${w.split('\n')[0]}`).join('\n') +
337
+ '\n');
338
+ }
339
+ const membersBase = join(clonePath, 'members', atlasUser);
340
+ let wouldCopy = 0;
341
+ let wouldUnchange = 0;
342
+ let wouldCreate = 0;
343
+ for (const { srcFile, overlayKind, relPath } of sourceFiles) {
344
+ const targetDir = join(membersBase, `${overlayKind}.overrides`);
345
+ const targetFile = join(targetDir, relPath);
346
+ let srcBuf;
347
+ try {
348
+ srcBuf = readFileSync(srcFile);
349
+ }
350
+ catch {
351
+ continue;
352
+ }
353
+ if (existsSync(targetFile)) {
354
+ const dstBuf = readFileSync(targetFile);
355
+ if (srcBuf.equals(dstBuf)) {
356
+ wouldUnchange += 1;
357
+ process.stdout.write(` [skip] ${overlayKind}.overrides/${relPath} (unchanged)\n`);
358
+ continue;
359
+ }
360
+ wouldCopy += 1;
361
+ process.stdout.write(` [copy] ${overlayKind}.overrides/${relPath} (changed)\n`);
362
+ }
363
+ else {
364
+ wouldCreate += 1;
365
+ process.stdout.write(` [new] ${overlayKind}.overrides/${relPath}\n`);
366
+ }
367
+ }
368
+ process.stdout.write(`[dry-run] would copy ${wouldCopy + wouldCreate} file(s), ${wouldUnchange} unchanged.\n`);
369
+ return {
370
+ filesCopied: wouldCopy,
371
+ filesUnchanged: wouldUnchange,
372
+ filesCreated: wouldCreate,
373
+ branchName: '',
374
+ nextStepGhCommand: '',
375
+ };
376
+ }
377
+ // B4 — Pre-flight gate (real run — fail fast)
378
+ const { error: preFlightError } = runPreFlight(clonePath, opts.forceCurrentBranch === true, false);
379
+ if (preFlightError !== null) {
380
+ throw new PushError(1, preFlightError);
381
+ }
382
+ // B4 — Generate and create branch
383
+ const branchName = resolveBranchName(clonePath, atlasUser);
384
+ try {
385
+ runGit(['checkout', '-b', branchName], clonePath);
386
+ }
387
+ catch (err) {
388
+ throw new PushError(1, `failed to create branch "${branchName}": ${err instanceof Error ? err.message : String(err)}`);
389
+ }
390
+ // B3 — Copy files (atomic: rollback branch on any copy failure)
391
+ const membersBase = join(clonePath, 'members', atlasUser);
392
+ let filesCopied = 0;
393
+ let filesUnchanged = 0;
394
+ let filesCreated = 0;
395
+ try {
396
+ for (const { srcFile, overlayKind, relPath } of sourceFiles) {
397
+ const targetDir = join(membersBase, `${overlayKind}.overrides`, dirname(relPath));
398
+ const targetFile = join(membersBase, `${overlayKind}.overrides`, relPath);
399
+ let srcBuf;
400
+ try {
401
+ srcBuf = readFileSync(srcFile);
402
+ }
403
+ catch {
404
+ continue;
405
+ }
406
+ const targetExists = existsSync(targetFile);
407
+ if (targetExists) {
408
+ const dstBuf = readFileSync(targetFile);
409
+ if (srcBuf.equals(dstBuf)) {
410
+ filesUnchanged += 1;
411
+ continue;
412
+ }
413
+ // Content differs — overwrite
414
+ mkdirSync(targetDir, { recursive: true });
415
+ copyFileSync(srcFile, targetFile);
416
+ filesCopied += 1;
417
+ }
418
+ else {
419
+ // New file
420
+ mkdirSync(targetDir, { recursive: true });
421
+ copyFileSync(srcFile, targetFile);
422
+ filesCreated += 1;
423
+ }
424
+ }
425
+ }
426
+ catch (copyErr) {
427
+ // Rollback: return clone to main and delete the new branch
428
+ try {
429
+ runGit(['checkout', 'main'], clonePath);
430
+ }
431
+ catch {
432
+ // best-effort
433
+ }
434
+ try {
435
+ runGit(['branch', '-D', branchName], clonePath);
436
+ }
437
+ catch {
438
+ // best-effort
439
+ }
440
+ const origMsg = copyErr instanceof Error ? copyErr.message : String(copyErr);
441
+ throw new PushError(1, `Copy failed partway through — clone reverted to main. Original: ${origMsg}`);
442
+ }
443
+ // B4 — Compose next-steps copy-paste block
444
+ const memberSubdir = `members/${atlasUser}/`;
445
+ const autoSubject = `feat: push operator overlays for ${atlasUser}`;
446
+ const nextStepGhCommand = `gh pr create --base main --head ${branchName} --title "${autoSubject}" --body "Operator overlay sync for ${atlasUser}."`;
447
+ const copyPasteBlock = [
448
+ `\nNext steps (run in ${clonePath}):`,
449
+ ` git -C "${clonePath}" add ${memberSubdir}`,
450
+ ` git -C "${clonePath}" commit -m '${autoSubject}'`,
451
+ ` git -C "${clonePath}" push -u origin ${branchName}`,
452
+ ` ${nextStepGhCommand}`,
453
+ '',
454
+ ].join('\n');
455
+ return {
456
+ filesCopied,
457
+ filesUnchanged,
458
+ filesCreated,
459
+ branchName,
460
+ nextStepGhCommand,
461
+ _copyPasteBlock: copyPasteBlock,
462
+ };
463
+ }
464
+ // ---------------------------------------------------------------------------
465
+ // Init-member mode (Phase C C3)
466
+ // ---------------------------------------------------------------------------
467
+ /**
468
+ * Scaffold members/<name>/{skills,agents}.overrides/.gitkeep + README.md
469
+ * in the atlas-toolbox clone. Leaves the working tree dirty.
470
+ * Throws PushError on validation / pre-flight / idempotency failures.
471
+ */
472
+ export function initMember(opts) {
473
+ const memberName = opts.initMember;
474
+ // Validate member name shape (same regex as ATLAS_USER_RE).
475
+ if (!ATLAS_USER_RE.test(memberName)) {
476
+ throw new PushError(1, `invalid member name "${memberName}" — must match /^[a-z0-9][a-z0-9_-]{0,38}$/`);
477
+ }
478
+ // Resolve target source (reuse same path as pushOverlays).
479
+ let clonePath;
480
+ if (opts._testClonePath !== undefined) {
481
+ clonePath = opts._testClonePath;
482
+ }
483
+ else {
484
+ const sources = opts._testSkillSources ?? listSkillSources();
485
+ const source = sources.find((s) => s.name === opts.target) ?? null;
486
+ if (source === null) {
487
+ const available = sources.map((s) => s.name).join(', ') || '(none)';
488
+ throw new PushError(1, `target source "${opts.target}" not registered.\n` +
489
+ ` Available sources: ${available}\n` +
490
+ ` Run \`olam skills source add\` to register one.\n` +
491
+ ` Actions: [List sources] / [Cancel]`);
492
+ }
493
+ clonePath = skillSourceClonePath(source.id);
494
+ }
495
+ // Resolve atlas-user for branch name.
496
+ const atlasUser = resolveAtlasUser(opts) ?? memberName;
497
+ const memberDir = join(clonePath, 'members', memberName);
498
+ // Idempotency: if members/<name>/ already exists, refuse.
499
+ if (existsSync(memberDir)) {
500
+ throw new PushError(1, `members/${memberName}/ already exists; use --push to update; or remove manually first\n` +
501
+ ` Actions: [Show existing] / [Cancel]`);
502
+ }
503
+ const wouldCreate = [
504
+ join(memberDir, 'skills.overrides', '.gitkeep'),
505
+ join(memberDir, 'agents.overrides', '.gitkeep'),
506
+ join(memberDir, 'README.md'),
507
+ ];
508
+ // Dry-run: preview only.
509
+ if (opts.dryRun === true) {
510
+ const { preflightWarnings } = runPreFlight(clonePath, opts.forceCurrentBranch === true, true);
511
+ if (preflightWarnings.length > 0) {
512
+ process.stderr.write(`[dry-run] pre-flight warnings (would BLOCK in real run):\n` +
513
+ preflightWarnings.map((w) => ` - ${w.split('\n')[0]}`).join('\n') +
514
+ '\n');
515
+ }
516
+ for (const p of wouldCreate) {
517
+ process.stdout.write(` [new] ${p.replace(clonePath + '/', '')}\n`);
518
+ }
519
+ process.stdout.write(`[dry-run] would create ${wouldCreate.length} path(s) in members/${memberName}/\n`);
520
+ return {
521
+ memberName,
522
+ branchName: '',
523
+ nextStepGhCommand: '',
524
+ createdPaths: wouldCreate,
525
+ };
526
+ }
527
+ // Real run: pre-flight gate.
528
+ const { error: preFlightError } = runPreFlight(clonePath, opts.forceCurrentBranch === true, false);
529
+ if (preFlightError !== null) {
530
+ throw new PushError(1, preFlightError);
531
+ }
532
+ // Generate and create branch.
533
+ const now = new Date();
534
+ const pad = (n) => String(n).padStart(2, '0');
535
+ const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
536
+ const branchName = `feat/${atlasUser}/init-member-${memberName}-${ts}`;
537
+ let resolvedBranch = branchName;
538
+ {
539
+ let suffix = 2;
540
+ while (true) {
541
+ let existsLocally = false;
542
+ try {
543
+ runGit(['rev-parse', '--verify', `refs/heads/${resolvedBranch}`], clonePath);
544
+ existsLocally = true;
545
+ }
546
+ catch {
547
+ // doesn't exist
548
+ }
549
+ if (!existsLocally)
550
+ break;
551
+ resolvedBranch = `${branchName}-${suffix}`;
552
+ suffix += 1;
553
+ if (suffix > 100)
554
+ break;
555
+ }
556
+ }
557
+ try {
558
+ runGit(['checkout', '-b', resolvedBranch], clonePath);
559
+ }
560
+ catch (err) {
561
+ throw new PushError(1, `failed to create branch "${resolvedBranch}": ${err instanceof Error ? err.message : String(err)}`);
562
+ }
563
+ // CP3 ADV-C-2 fix: atomic copy rollback. If mkdirSync/writeFileSync
564
+ // fails partway through, leave the clone on the new branch with partial
565
+ // files — next invocation refuses the idempotency check + accumulates
566
+ // orphan branches. Mirror the pushOverlays pattern: try/catch wrapped
567
+ // around all create ops + best-effort cleanup (checkout main + branch -D)
568
+ // before re-throwing as PushError with the "reverted to main" prefix.
569
+ try {
570
+ // Create directory tree.
571
+ mkdirSync(join(memberDir, 'skills.overrides'), { recursive: true });
572
+ writeFileSync(join(memberDir, 'skills.overrides', '.gitkeep'), '');
573
+ mkdirSync(join(memberDir, 'agents.overrides'), { recursive: true });
574
+ writeFileSync(join(memberDir, 'agents.overrides', '.gitkeep'), '');
575
+ writeFileSync(join(memberDir, 'README.md'), `# Member overlays for \`${memberName}\`\n\nSee docs/plans/member-overlays-sync/ for layer architecture.\n`);
576
+ }
577
+ catch (err) {
578
+ const msg = err instanceof Error ? err.message : String(err);
579
+ // Best-effort cleanup: switch back to main + delete the orphan branch.
580
+ try {
581
+ runGit(['checkout', 'main'], clonePath);
582
+ }
583
+ catch {
584
+ // ignore; operator can recover manually
585
+ }
586
+ try {
587
+ runGit(['branch', '-D', resolvedBranch], clonePath);
588
+ }
589
+ catch {
590
+ // ignore
591
+ }
592
+ throw new PushError(1, `Scaffold failed partway through — clone reverted to main. Original: ${msg}`);
593
+ }
594
+ // Compose copy-paste block.
595
+ const autoSubject = `feat: scaffold members/${memberName}/ for atlas-toolbox-tracked overlays`;
596
+ const nextStepGhCommand = `gh pr create --base main --head ${resolvedBranch} --title "${autoSubject}" --body "Scaffold member overlay directory for ${memberName}."`;
597
+ const copyPasteBlock = [
598
+ `\nNext steps (run in ${clonePath}):`,
599
+ ` git -C "${clonePath}" add members/${memberName}/`,
600
+ ` git -C "${clonePath}" commit -m '${autoSubject}'`,
601
+ ` git -C "${clonePath}" push -u origin ${resolvedBranch}`,
602
+ ` ${nextStepGhCommand}`,
603
+ '',
604
+ ].join('\n');
605
+ return {
606
+ memberName,
607
+ branchName: resolvedBranch,
608
+ nextStepGhCommand,
609
+ createdPaths: wouldCreate,
610
+ _copyPasteBlock: copyPasteBlock,
611
+ };
612
+ }
613
+ // ---------------------------------------------------------------------------
614
+ // Rewrite-mode entry (Phase C7 — unchanged)
615
+ // ---------------------------------------------------------------------------
616
+ export function migrateOverlays(opts = {}) {
617
+ const root = opts.targetDir ?? join(homedir(), '.claude');
618
+ const overrideRoots = [
619
+ join(root, 'skills.overrides'),
620
+ join(root, 'agents.overrides'),
621
+ ];
622
+ const allFiles = [];
623
+ for (const overrideRoot of overrideRoots) {
624
+ allFiles.push(...walkOverlayFiles(overrideRoot));
625
+ }
626
+ const summary = {
627
+ scanned: allFiles.length,
628
+ modified: 0,
629
+ unchanged: 0,
630
+ totalReplacements: 0,
631
+ perFile: [],
632
+ };
633
+ for (const filePath of allFiles) {
634
+ let original;
635
+ try {
636
+ original = readFileSync(filePath, 'utf8');
637
+ }
638
+ catch {
639
+ continue;
640
+ }
641
+ const { newContent, replacements } = applyReplacements(original);
642
+ const totalReplacements = replacements.reduce((s, r) => s + r.count, 0);
643
+ if (totalReplacements === 0) {
644
+ summary.unchanged += 1;
645
+ continue;
646
+ }
647
+ if (opts.dryRun !== true) {
648
+ writeFileSync(filePath, newContent, 'utf8');
649
+ }
650
+ summary.modified += 1;
651
+ summary.totalReplacements += totalReplacements;
652
+ summary.perFile.push({ path: filePath, replacements, totalReplacements });
653
+ }
654
+ return summary;
655
+ }
656
+ // ---------------------------------------------------------------------------
657
+ // Commander registration
658
+ // ---------------------------------------------------------------------------
659
+ export function registerFlywheelMigrateOverlays(parent) {
660
+ parent
661
+ .command('migrate-overlays')
662
+ .description('Migrate operator-local overlay files (~/.claude/{skills,agents}.overrides/) from hardcoded ~/.claude/scripts/<name> references to olam flywheel <subcmd> invocations. With --push, copy overlays into atlas-toolbox clone.')
663
+ .option('--dry-run', 'preview changes without modifying files')
664
+ .option('--target-dir <path>', 'override ~/.claude/ root (default: $HOME/.claude); for tests')
665
+ .option('--json', 'emit summary as JSON instead of human-readable')
666
+ .option('--push', 'PUSH mode: copy overlays into atlas-toolbox clone')
667
+ .option('--target <name>', 'skill-source name to push into (required with --push or --init-member)')
668
+ .option('--force-current-branch', 'Allow --push from non-main branches without pre-flight refusal')
669
+ .option('--init-member <name>', 'Scaffold members/<name>/{skills,agents}.overrides/ on the atlas-toolbox clone')
670
+ .action((opts) => {
671
+ // T6 mode-overloading guard: --push requires --target
672
+ if (opts.push === true && !opts.target) {
673
+ process.stderr.write(`Error: --push requires --target=<skill-source-name>.\n` +
674
+ ` Actions: [Add --target=atlas-toolbox] / [Cancel]\n`);
675
+ process.exit(1);
676
+ }
677
+ // C3 mode-overloading guard: --init-member requires --target
678
+ if (opts.initMember !== undefined && !opts.target) {
679
+ process.stderr.write(`Error: --init-member requires --target=<skill-source-name>.\n` +
680
+ ` Actions: [Add --target=atlas-toolbox] / [Cancel]\n`);
681
+ process.exit(1);
682
+ }
683
+ if (opts.initMember !== undefined) {
684
+ let summary;
685
+ try {
686
+ summary = initMember(opts);
687
+ }
688
+ catch (err) {
689
+ if (err instanceof PushError) {
690
+ process.stderr.write(`Error: ${err.message}\n`);
691
+ process.exit(err.exitCode);
692
+ }
693
+ throw err;
694
+ }
695
+ if (opts.json === true) {
696
+ process.stdout.write(JSON.stringify(summary) + '\n');
697
+ return;
698
+ }
699
+ const dryRunLabel = opts.dryRun === true ? ' (dry-run)' : '';
700
+ process.stdout.write(`olam flywheel migrate-overlays --init-member ${opts.initMember} → ${opts.target}${dryRunLabel}\n`);
701
+ if (opts.dryRun !== true) {
702
+ process.stdout.write(` branch: ${summary.branchName}\n`);
703
+ process.stdout.write(` created: ${summary.createdPaths.length} path(s)\n`);
704
+ if (summary._copyPasteBlock !== undefined) {
705
+ process.stdout.write(summary._copyPasteBlock);
706
+ }
707
+ }
708
+ return;
709
+ }
710
+ if (opts.push === true) {
711
+ let summary;
712
+ try {
713
+ summary = pushOverlays(opts);
714
+ }
715
+ catch (err) {
716
+ if (err instanceof PushError) {
717
+ process.stderr.write(`Error: ${err.message}\n`);
718
+ process.exit(err.exitCode);
719
+ }
720
+ throw err;
721
+ }
722
+ if (opts.json === true) {
723
+ process.stdout.write(JSON.stringify(summary) + '\n');
724
+ return;
725
+ }
726
+ const dryRunLabel = opts.dryRun === true ? ' (dry-run)' : '';
727
+ process.stdout.write(`olam flywheel migrate-overlays --push → ${opts.target}${dryRunLabel}\n`);
728
+ if (opts.dryRun !== true) {
729
+ process.stdout.write(` branch: ${summary.branchName}\n`);
730
+ process.stdout.write(` copied: ${summary.filesCopied}\n`);
731
+ process.stdout.write(` created: ${summary.filesCreated}\n`);
732
+ process.stdout.write(` unchanged: ${summary.filesUnchanged}\n`);
733
+ if (summary._copyPasteBlock !== undefined) {
734
+ process.stdout.write(summary._copyPasteBlock);
735
+ }
736
+ }
737
+ return;
738
+ }
739
+ // Rewrite mode
740
+ const root = opts.targetDir ?? join(homedir(), '.claude');
741
+ const summary = migrateOverlays(opts);
742
+ if (opts.json === true) {
743
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
744
+ return;
745
+ }
746
+ const dryRunSuffix = opts.dryRun === true ? ' (dry-run)' : '';
747
+ process.stdout.write(`olam flywheel migrate-overlays → ${root}${dryRunSuffix}\n`);
748
+ process.stdout.write(` scanned: ${summary.scanned} file(s)\n`);
749
+ process.stdout.write(` modified: ${summary.modified}\n`);
750
+ process.stdout.write(` unchanged: ${summary.unchanged}\n`);
751
+ process.stdout.write(` total replacements: ${summary.totalReplacements}\n`);
752
+ if (summary.perFile.length > 0) {
753
+ process.stdout.write(`\nPer-file changes:\n`);
754
+ for (const file of summary.perFile) {
755
+ process.stdout.write(` ${relative(root, file.path)}${dryRunSuffix}\n`);
756
+ for (const r of file.replacements) {
757
+ process.stdout.write(` ${r.count}× ${r.basename} → ${r.replacedBy}\n`);
758
+ }
759
+ }
760
+ }
761
+ if (summary.modified === 0) {
762
+ process.stdout.write(`\n[migrate-overlays] no changes needed — all overlays already migrated or no shim-target references found.\n`);
763
+ }
764
+ });
765
+ }
766
+ //# sourceMappingURL=migrate-overlays.js.map