docks-kit 0.17.1 → 0.17.3

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 (87) hide show
  1. package/AGENTS.md +41 -33
  2. package/cli/docs/omp-context.md +104 -0
  3. package/cli/docs/omp-models.md +106 -56
  4. package/cli/src/argv.ts +38 -469
  5. package/cli/src/argvSurface.ts +371 -0
  6. package/cli/src/argvValidate.ts +122 -0
  7. package/cli/src/commands/docs.ts +62 -42
  8. package/cli/src/commands/harnesses.ts +38 -45
  9. package/cli/src/commands/model.ts +48 -53
  10. package/cli/src/commands/models.ts +24 -26
  11. package/cli/src/commands/omp.ts +132 -112
  12. package/cli/src/commands/plugins.ts +20 -20
  13. package/cli/src/commands/skills.ts +22 -20
  14. package/cli/src/commands/status.ts +87 -80
  15. package/cli/src/commands/sync.ts +113 -87
  16. package/cli/src/commands/toolchain.ts +21 -21
  17. package/cli/src/commands/update.ts +139 -117
  18. package/cli/src/efforts.ts +35 -34
  19. package/cli/src/engine-native/DESIGN.md +30 -30
  20. package/cli/src/engine-native/bun.ts +77 -55
  21. package/cli/src/engine-native/claudeLsp.ts +154 -0
  22. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  23. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  24. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  25. package/cli/src/engine-native/claudeRetired.ts +4 -4
  26. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  27. package/cli/src/engine-native/claudeSettings.ts +174 -0
  28. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  29. package/cli/src/engine-native/claudeSync.ts +176 -456
  30. package/cli/src/engine-native/codexConfig.ts +158 -0
  31. package/cli/src/engine-native/codexHooks.ts +91 -0
  32. package/cli/src/engine-native/codexPlugins.ts +290 -0
  33. package/cli/src/engine-native/codexStatus.ts +24 -0
  34. package/cli/src/engine-native/codexSync.ts +110 -568
  35. package/cli/src/engine-native/codexToml.ts +140 -138
  36. package/cli/src/engine-native/deps.ts +144 -107
  37. package/cli/src/engine-native/engineCtx.ts +126 -0
  38. package/cli/src/engine-native/exec.ts +98 -87
  39. package/cli/src/engine-native/failures.ts +3 -3
  40. package/cli/src/engine-native/harnesses.ts +62 -72
  41. package/cli/src/engine-native/index.ts +34 -315
  42. package/cli/src/engine-native/jq.ts +25 -24
  43. package/cli/src/engine-native/logger.ts +98 -100
  44. package/cli/src/engine-native/models.ts +50 -45
  45. package/cli/src/engine-native/modes.ts +83 -80
  46. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  47. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  48. package/cli/src/engine-native/ompOverlay.ts +67 -64
  49. package/cli/src/engine-native/ompPaths.ts +49 -38
  50. package/cli/src/engine-native/ompPlugins.ts +170 -0
  51. package/cli/src/engine-native/ompRemovals.ts +159 -0
  52. package/cli/src/engine-native/ompSync.ts +69 -409
  53. package/cli/src/engine-native/ompYaml.ts +43 -41
  54. package/cli/src/engine-native/os/darwin.ts +5 -5
  55. package/cli/src/engine-native/os/index.ts +15 -15
  56. package/cli/src/engine-native/os/linux.ts +5 -5
  57. package/cli/src/engine-native/os/posix.ts +28 -24
  58. package/cli/src/engine-native/os/targets.ts +17 -17
  59. package/cli/src/engine-native/os/types.ts +31 -31
  60. package/cli/src/engine-native/os/windows.ts +61 -61
  61. package/cli/src/engine-native/parseArgs.ts +122 -372
  62. package/cli/src/engine-native/parseHelp.ts +66 -0
  63. package/cli/src/engine-native/parseModifiers.ts +222 -0
  64. package/cli/src/engine-native/services.ts +34 -34
  65. package/cli/src/engine-native/settings.ts +15 -15
  66. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  67. package/cli/src/engine-native/skillsInstall.ts +105 -0
  68. package/cli/src/engine-native/skillsLinks.ts +203 -0
  69. package/cli/src/engine-native/skillsManifest.ts +16 -0
  70. package/cli/src/engine-native/skillsPrune.ts +108 -0
  71. package/cli/src/engine-native/skillsSync.ts +28 -362
  72. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  73. package/cli/src/engine-native/syncDispatch.ts +141 -0
  74. package/cli/src/engine-native/toolchain.ts +65 -62
  75. package/cli/src/engine.ts +65 -57
  76. package/cli/src/generated/sotPayload.ts +7 -7
  77. package/cli/src/kitHome.ts +44 -40
  78. package/cli/src/main.ts +63 -56
  79. package/cli/src/manifests.ts +53 -54
  80. package/cli/src/md.d.ts +2 -2
  81. package/cli/src/payload.ts +9 -9
  82. package/cli/src/services.ts +21 -13
  83. package/cli/tsconfig.json +4 -1
  84. package/docks-kit +13 -2
  85. package/docks-kit.ps1 +21 -6
  86. package/package.json +10 -2
  87. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -2,384 +2,50 @@
2
2
  * EngineNative `sync agents` pipeline: universal-skill bootstrap
3
3
  * (`npx skills@<pin> add`), Claude symlink healing, --prune reconcile against
4
4
  * the kit-managed snapshot, and the snapshot write.
5
+ *
6
+ * Split: manifest parsing lives in `skillsManifest.ts`, link mechanics and
7
+ * healing in `skillsLinks.ts`, the install pass in `skillsInstall.ts`, and
8
+ * prune plus snapshot tracking in `skillsPrune.ts`. This module keeps the
9
+ * orchestration and re-exports the public surface so existing import paths
10
+ * keep working.
5
11
  */
6
- import {
7
- cpSync,
8
- existsSync,
9
- lstatSync,
10
- mkdirSync,
11
- readFileSync,
12
- readlinkSync,
13
- rmSync,
14
- statSync,
15
- symlinkSync,
16
- writeFileSync
17
- } from "node:fs"
18
- import { dirname, relative, resolve } from "node:path"
19
- import { payloadText } from "../payload"
20
- import { p, spawnProcess, writeTextIfChanged } from "./exec"
21
- import type { Ctx } from "./index"
22
- import { compareCodepoints } from "./jq"
23
- import { hostOs, type DirectoryLinkKind } from "./os"
24
- import { ExitError } from "./parseArgs"
25
- import type { EngineServices } from "./services"
26
- import { field } from "./toolchain"
12
+ import { mkdirSync } from "node:fs";
13
+ import { payloadText } from "../payload";
14
+ import { p } from "./exec";
15
+ import type { Ctx } from "./index";
16
+ import { syncUniversal, type SkillsState } from "./skillsInstall";
17
+ import { reconcileRemovals, updateSnapshot } from "./skillsPrune";
27
18
 
28
- export interface SkillsState {
29
- present: number
30
- }
31
-
32
- export type LinkOutcome = "symlink" | "junction" | "copy" | "failed"
33
-
34
- /** Lets heal and prune distinguish a kit-owned copy from a user's real directory. */
35
- export const COPY_MARKER = ".docks-kit-copied-skill"
19
+ export type { SkillsState } from "./skillsInstall";
20
+ export { COPY_MARKER, linkOrCopy, type LinkOutcome } from "./skillsLinks";
21
+ export { normalizeManifest } from "./skillsManifest";
36
22
 
37
23
  export async function skillsSync(ctx: Ctx): Promise<SkillsState> {
38
- const state: SkillsState = { present: 0 }
39
- const skillsDir = p(ctx.agentsDir, "skills")
40
- const manifest = payloadText("SoT/.agents/skills.txt")
41
- const snapshot = p(ctx.agentsDir, ".kit-managed-skills")
42
-
43
- if (!ctx.dryRun) mkdirSync(skillsDir, { recursive: true })
44
-
45
- await syncUniversal(ctx, state, skillsDir, manifest)
46
- const failedRemovals = ctx.prune ? await reconcileRemovals(ctx, manifest, snapshot) : []
47
- updateSnapshot(ctx, manifest, snapshot, failedRemovals)
48
- return state
49
- }
50
-
51
- /** skills::_skills_cli — the pinned npx package spec. */
52
- function skillsCli(ctx: Ctx): string {
53
- const version = field("skills-cli", "verified")
54
- if (version !== "") return `skills@${version}`
55
- ctx.services.logger.err("Universal skills sync aborted because SoT/toolchain.json has no verified skills-cli pin")
56
- throw new ExitError(1)
57
- }
58
-
59
- /** skills::_normalize_manifest — cleaned slugs, one per line. */
60
- export function normalizeManifest(content: string): Array<string> {
61
- const out: Array<string> = []
62
- for (const line of content.split("\n")) {
63
- if (/^[ \t]*#/.test(line)) continue
64
- if (/^[ \t]*$/.test(line)) continue
65
- const cleaned = line.replace(/[ \t]*#.*$/, "").replace(/[ \t\r]+/g, "")
66
- if (cleaned.length > 0) out.push(cleaned)
67
- }
68
- return out
69
- }
70
-
71
- function readSlugs(file: string): Array<string> {
72
- return existsSync(file) ? normalizeManifest(readFileSync(file, "utf8")) : []
73
- }
74
-
75
- async function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest: string): Promise<void> {
76
- const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
77
- if (ctx.services.deps.probe("npx").state === "missing") {
78
- ctx.services.deps.warnMissing("npx", ctx.services.logger, "skipping universal skills bootstrap")
79
- return
80
- }
81
-
82
- let added = 0
83
- let already = 0
84
- let failed = 0
85
- let healed = 0
86
-
87
- for (const slug of normalizeManifest(manifest)) {
88
- const base = slug.slice(slug.lastIndexOf("/") + 1)
89
-
90
- if (ctx.dryRun) {
91
- if (isDir(p(skillsDir, base))) {
92
- echo(`[dry-run] universal skill present: ${base}`)
93
- healClaudeSymlink(ctx, skillsDir, base)
94
- } else {
95
- echo(`[dry-run] npx ${skillsCli(ctx)} add ${slug} -g -y -a claude-code codex`)
96
- }
97
- continue
98
- }
99
-
100
- if (isDir(p(skillsDir, base))) {
101
- already++
102
- if (healClaudeSymlink(ctx, skillsDir, base)) healed++
103
- continue
104
- }
105
-
106
- progress(`Installing universal skill ${slug}...`)
107
- const res = await spawnProcess("npx", ["--yes", skillsCli(ctx), "add", slug, "-g", "-y", "-a", "claude-code", "codex"], {
108
- stdio: "ignore"
109
- })
110
- clearProgress()
111
- if (res.error === undefined && res.exitCode === 0) {
112
- added++
113
- } else {
114
- warn(`Failed to install universal skill: ${slug}`)
115
- failed++
116
- }
117
- }
118
-
119
- if (ctx.dryRun) return
120
-
121
- state.present = added + already
122
-
123
- if (added > 0) {
124
- change(`Universal skills synced (+${added} new, ${already} already present)`)
125
- ctx.nextStepTriggers.skillsRestart = true
126
- } else {
127
- verbose(`Universal skills already in sync (${already} present)`)
128
- }
129
- if (healed > 0) {
130
- change(`Claude per-tool symlinks healed (+${healed}) — canonical present, ~/.claude/skills/<name> was missing or broken`)
131
- ctx.nextStepTriggers.skillsRestart = true
132
- }
133
- if (failed > 0) {
134
- warn(`${failed} skill install(s) failed — re-run sync or install manually with: npx skills add <slug> -g -y -a claude-code codex`)
135
- }
136
- }
137
-
138
- function isDir(path: string): boolean {
139
- try {
140
- return statSync(path).isDirectory()
141
- } catch {
142
- return false
143
- }
144
- }
145
-
146
- /** skills::heal_claude_symlink — true when a heal occurred. */
147
- function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
148
- const { echo, warn } = ctx.services.logger
149
- const canonical = p(skillsDir, base)
150
- const claudeSkillsDir = p(ctx.home, ".claude", "skills")
151
- const claudeLink = p(claudeSkillsDir, base)
152
- const relTarget = relative(dirname(claudeLink), canonical)
153
-
154
- if (!isDir(canonical)) return false
155
-
156
- const linkStat = lstat(claudeLink)
157
- if (linkStat?.isSymbolicLink() === true) {
158
- const current = safeReadlink(claudeLink)
159
- if (current === relTarget) return false
160
- if (ctx.dryRun) {
161
- echo(`[dry-run] would replace stale Claude symlink: ~/.claude/skills/${base} -> ${current} (correct: ${relTarget})`)
162
- return true
163
- }
164
- if (!removeLink(claudeLink)) {
165
- warn(`could not remove stale link ~/.claude/skills/${base} — remove it manually, then re-run sync`)
166
- return false
167
- }
168
- } else if (linkStat !== undefined) {
169
- if (!isKitOwnedCopy(claudeLink)) {
170
- warn(`~/.claude/skills/${base} exists as a real path (not a symlink) — leaving alone; remove manually if it's stale`)
171
- return false
172
- }
173
- if (ctx.dryRun) {
174
- echo(`[dry-run] would replace kit-created Claude copy: ~/.claude/skills/${base} -> ${relTarget}`)
175
- return true
176
- }
177
- if (!removeKitOwnedCopy(claudeLink)) {
178
- warn(`could not remove kit-created copy ~/.claude/skills/${base} — remove it manually, then re-run sync`)
179
- return false
180
- }
181
- } else if (ctx.dryRun) {
182
- echo(`[dry-run] would create missing Claude symlink: ~/.claude/skills/${base} -> ${relTarget}`)
183
- return true
184
- }
185
-
186
- mkdirSync(claudeSkillsDir, { recursive: true })
187
- return linkOrCopyWithWarnings(relTarget, claudeLink, ctx.services) !== "failed"
188
- }
189
-
190
- function lstat(path: string): ReturnType<typeof lstatSync> | undefined {
191
- try {
192
- return lstatSync(path)
193
- } catch {
194
- return undefined
195
- }
196
- }
197
-
198
- /**
199
- * A link only counts when it RESOLVES to the skill directory. Windows picks a
200
- * symlink's file-or-directory type by autodetecting the target against the
201
- * process working directory, not the link's own directory, so a relative
202
- * target can yield a symlink that exists but resolves to nothing. Checking
203
- * resolution is what makes the next mechanism in the chain reachable.
204
- */
205
- function linksToDirectory(path: string): boolean {
206
- if (lstat(path)?.isSymbolicLink() !== true) return false
207
- try {
208
- return statSync(path).isDirectory()
209
- } catch {
210
- return false
211
- }
212
- }
213
-
214
- function isKitOwnedCopy(path: string): boolean {
215
- return lstat(path)?.isDirectory() === true && existsSync(p(path, COPY_MARKER))
216
- }
217
-
218
- function removeKitOwnedCopy(path: string): boolean {
219
- if (!isKitOwnedCopy(path)) return false
220
- try {
221
- rmSync(path, { recursive: true, force: true })
222
- return true
223
- } catch {
224
- return lstat(path) === undefined
225
- }
226
- }
227
-
228
- function safeReadlink(path: string): string {
229
- try {
230
- return readlinkSync(path)
231
- } catch {
232
- return ""
233
- }
234
- }
235
-
236
-
237
- /** Remove a symlink without touching a real directory. */
238
- function removeLink(path: string): boolean {
239
- try {
240
- rmSync(path, { force: true })
241
- return true
242
- } catch {
243
- return lstat(path) === undefined
244
- }
245
- }
246
-
247
- /** skills::_link_or_copy — try directory links in host order, then a marked copy. */
248
- export function linkOrCopy(
249
- target: string,
250
- link: string,
251
- kinds: ReadonlyArray<DirectoryLinkKind> = hostOs().directoryLinkKinds
252
- ): LinkOutcome {
253
- const resolvedLink = resolve(link)
254
- const absoluteTarget = resolve(dirname(resolvedLink), target)
255
- if (absoluteTarget === resolvedLink) return "symlink"
256
- removeLink(link)
257
-
258
- for (const kind of kinds) {
259
- try {
260
- if (kind === "symlink") {
261
- symlinkSync(target, link)
262
- } else {
263
- symlinkSync(absoluteTarget, link, "junction")
264
- }
265
- if (linksToDirectory(link)) return kind
266
- } catch {
267
- // The runtime decides whether each mechanism works; try the next one.
268
- }
269
- removeLink(link)
270
- }
271
-
272
- const copyDestinationWasAbsent = lstat(link) === undefined
273
- try {
274
- cpSync(absoluteTarget, link, { recursive: true })
275
- writeFileSync(p(link, COPY_MARKER), "")
276
- return "copy"
277
- } catch {
278
- if (copyDestinationWasAbsent) {
279
- try {
280
- rmSync(link, { recursive: true, force: true })
281
- } catch {
282
- // The outcome remains failed; a later sync can retry the destination.
283
- }
284
- }
285
- return "failed"
286
- }
287
- }
288
-
289
- function linkOrCopyWithWarnings(target: string, link: string, services: EngineServices): LinkOutcome {
290
- const outcome = linkOrCopy(target, link)
291
- if (outcome === "copy") {
292
- services.logger.warn(`created copy fallback ${link} because directory linking is unavailable — a later sync will restore a real link once linking works`)
293
- } else if (outcome === "failed") {
294
- services.logger.warn(`could not create symlink ${link}`)
295
- }
296
- return outcome
297
- }
298
-
299
- // ----------------------------------------------------- prune + snapshot ----
300
-
301
- async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): Promise<Array<string>> {
302
- const { change, clearProgress, echo, progress, warn } = ctx.services.logger
303
- if (!existsSync(snapshot)) {
304
- if (ctx.dryRun) {
305
- echo(
306
- `[dry-run] (--prune) no kit-managed-skills snapshot yet; first real sync writes ${snapshot}, then future --prune runs reconcile against it`
307
- )
308
- }
309
- return []
310
- }
311
-
312
- const current = normalizeManifest(manifest)
313
- const currentBases = new Set(current.map((slug) => slug.slice(slug.lastIndexOf("/") + 1)))
314
- let removed = 0
315
- let failed = 0
316
- const failedSlugs: Array<string> = []
317
- for (const slug of readSlugs(snapshot)) {
318
- if (current.includes(slug)) continue
319
- const base = slug.slice(slug.lastIndexOf("/") + 1)
320
- if (currentBases.has(base)) continue
321
- const claudeEntry = p(ctx.home, ".claude", "skills", base)
322
- const managedClaudeEntry = lstat(claudeEntry)?.isSymbolicLink() === true || isKitOwnedCopy(claudeEntry)
323
- if (ctx.dryRun) {
324
- echo(`[dry-run] kit-managed skill no longer in SoT — would remove: ${base}`)
325
- if (managedClaudeEntry) {
326
- echo(`[dry-run] kit-managed Claude skill entry — would remove: ~/.claude/skills/${base}`)
327
- }
328
- continue
329
- }
330
- progress(`Removing universal skill ${base}...`)
331
- const res = await spawnProcess("npx", ["--yes", skillsCli(ctx), "remove", "--global", base, "-y"], {
332
- stdio: "ignore"
333
- })
334
- clearProgress()
335
- if (res.error !== undefined || res.exitCode !== 0) {
336
- warn(`Failed to remove kit-managed skill: ${base}`)
337
- failed++
338
- failedSlugs.push(slug)
339
- continue
340
- }
341
- if (managedClaudeEntry) {
342
- const entryStat = lstat(claudeEntry)
343
- const removedClaudeEntry = entryStat === undefined
344
- || (entryStat.isSymbolicLink() ? removeLink(claudeEntry) : removeKitOwnedCopy(claudeEntry))
345
- if (!removedClaudeEntry) {
346
- warn(`Failed to remove kit-managed Claude skill entry: ${base}`)
347
- failed++
348
- failedSlugs.push(slug)
349
- continue
350
- }
351
- }
352
- removed++
353
- }
354
-
355
- if (removed > 0) {
356
- change(`Kit-managed skills removed (-${removed})`)
357
- ctx.nextStepTriggers.skillsRestart = true
358
- }
359
- if (failed > 0) warn(`${failed} skill remove(s) failed — re-run with --prune or run: npx skills remove --global <name> -y`)
360
- return failedSlugs
361
- }
24
+ const state: SkillsState = { present: 0 };
25
+ const skillsDir = p(ctx.agentsDir, "skills");
26
+ const manifest = payloadText("SoT/.agents/skills.txt");
27
+ const snapshot = p(ctx.agentsDir, ".kit-managed-skills");
362
28
 
363
- function updateSnapshot(ctx: Ctx, manifest: string, snapshot: string, failedRemovals: ReadonlyArray<string>): void {
364
- if (ctx.dryRun) return
29
+ if (!ctx.dryRun) mkdirSync(skillsDir, { recursive: true });
365
30
 
366
- mkdirSync(ctx.agentsDir, { recursive: true })
367
- const sorted = [...new Set([...normalizeManifest(manifest), ...failedRemovals])].sort(compareCodepoints)
368
- writeTextIfChanged(snapshot, sorted.length > 0 ? `${sorted.join("\n")}\n` : "")
31
+ await syncUniversal(ctx, state, skillsDir, manifest);
32
+ const failedRemovals = ctx.prune ? await reconcileRemovals(ctx, manifest, snapshot) : [];
33
+ updateSnapshot(ctx, manifest, snapshot, failedRemovals);
34
+ return state;
369
35
  }
370
36
 
371
37
  // -------------------------------------------------------------- summary ----
372
38
 
373
39
  export function skillsSummary(ctx: Ctx, state: SkillsState): void {
374
- const { echo } = ctx.services.logger
375
- echo(`Skills: ${p(ctx.agentsDir, "skills")}`)
40
+ const { echo } = ctx.services.logger;
41
+ echo(`Skills: ${p(ctx.agentsDir, "skills")}`);
376
42
  if (!ctx.dryRun) {
377
- echo(` ${state.present} universal skill(s) installed`)
43
+ echo(` ${state.present} universal skill(s) installed`);
378
44
  }
379
45
  }
380
46
 
381
47
  export function skillsNextSteps(ctx: Ctx): Array<string> {
382
48
  return ctx.verbose || ctx.nextStepTriggers.skillsRestart
383
49
  ? ["Restart Claude Code (and Codex) to discover newly installed universal skills."]
384
- : []
50
+ : [];
385
51
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * EngineNative sync scheduling: bounded-concurrency runner plus the
3
+ * manifest-driven serialization rule. Split from index.ts, which re-exports
4
+ * the public surface so existing import paths keep working.
5
+ */
6
+ import { normalizeManifest } from "./skillsManifest";
7
+
8
+ export type SyncConcurrency = 1 | 2 | 3;
9
+ export type SyncTask<T> = () => Promise<T>;
10
+
11
+ /**
12
+ * Run input-ordered tasks with bounded overlap. Once one task rejects, queued
13
+ * tasks stay queued while already-started tasks drain; the earliest rejection
14
+ * in input order is then propagated.
15
+ */
16
+ export async function runBounded<T>(
17
+ tasks: ReadonlyArray<SyncTask<T>>,
18
+ concurrency: SyncConcurrency,
19
+ ): Promise<Array<T>> {
20
+ const results = new Array<T>(tasks.length);
21
+ const failures = new Map<number, unknown>();
22
+ let nextIndex = 0;
23
+ let stopped = false;
24
+
25
+ const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, async () => {
26
+ for (;;) {
27
+ if (stopped || nextIndex >= tasks.length) return;
28
+ const index = nextIndex;
29
+ nextIndex += 1;
30
+ try {
31
+ results[index] = await tasks[index]!();
32
+ } catch (error) {
33
+ failures.set(index, error);
34
+ stopped = true;
35
+ }
36
+ }
37
+ });
38
+ await Promise.all(workers);
39
+
40
+ for (let index = 0; index < tasks.length; index += 1) {
41
+ if (failures.has(index)) throw failures.get(index);
42
+ }
43
+ return results;
44
+ }
45
+
46
+ export function syncConcurrencyForManifest(
47
+ configured: SyncConcurrency,
48
+ manifest: string,
49
+ claudeSelected: boolean,
50
+ skillsSelected: boolean,
51
+ ): SyncConcurrency {
52
+ if (!claudeSelected || !skillsSelected || normalizeManifest(manifest).length === 0)
53
+ return configured;
54
+ return 1;
55
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * EngineNative pipeline dispatch: select the requested harness pipelines,
3
+ * run them under the concurrency cap, then print the summary and advice.
4
+ * Split from index.ts; the `sync`/`model`/`toolchain` entry routing still
5
+ * lives there.
6
+ */
7
+ import { payloadText } from "../payload";
8
+ import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } from "./claudeSync";
9
+ import { codexNextSteps, codexSummary, codexSync } from "./codexSync";
10
+ import type { Ctx } from "./index";
11
+ import { ompNextSteps, ompSummary, ompSync, type OmpState } from "./ompSync";
12
+ import { skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync";
13
+ import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs";
14
+ import {
15
+ runBounded,
16
+ syncConcurrencyForManifest,
17
+ type SyncConcurrency,
18
+ type SyncTask,
19
+ } from "./syncConcurrency";
20
+
21
+ export async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
22
+ const { acquireTerminal, echo, err } = ctx.services.logger;
23
+ parseArgs(ctx, args);
24
+ validateModifierFlags(ctx);
25
+ const configuredConcurrency = process.env["DOCKS_KIT_SYNC_CONCURRENCY"];
26
+ if (configuredConcurrency === undefined || configuredConcurrency === "") {
27
+ ctx.syncConcurrency = 3;
28
+ } else if (
29
+ configuredConcurrency === "1" ||
30
+ configuredConcurrency === "2" ||
31
+ configuredConcurrency === "3"
32
+ ) {
33
+ ctx.syncConcurrency = Number(configuredConcurrency) as SyncConcurrency;
34
+ } else {
35
+ err("DOCKS_KIT_SYNC_CONCURRENCY must be 1, 2, or 3");
36
+ throw new ExitError(2);
37
+ }
38
+
39
+ type PipelineResult =
40
+ | { readonly kind: "claude"; readonly runtime: ClaudeRuntimeState }
41
+ | { readonly kind: "codex" }
42
+ | { readonly kind: "skills"; readonly state: SkillsState }
43
+ | { readonly kind: "omp"; readonly state: OmpState };
44
+ interface SelectedPipeline {
45
+ readonly name: string;
46
+ readonly run: SyncTask<PipelineResult>;
47
+ }
48
+
49
+ const selected: Array<SelectedPipeline> = [];
50
+ if (ctx.syncClaude) {
51
+ selected.push({
52
+ name: "Claude",
53
+ run: async () => ({ kind: "claude", runtime: await claudeSync(ctx) }),
54
+ });
55
+ }
56
+ if (ctx.syncCodex) {
57
+ selected.push({
58
+ name: "Codex",
59
+ run: async () => {
60
+ await codexSync(ctx);
61
+ return { kind: "codex" };
62
+ },
63
+ });
64
+ }
65
+ if (ctx.syncAgents) {
66
+ selected.push({
67
+ name: "skills",
68
+ run: async () => ({ kind: "skills", state: await skillsSync(ctx) }),
69
+ });
70
+ }
71
+ if (ctx.syncOmp) {
72
+ selected.push({
73
+ name: "omp",
74
+ run: async () => ({ kind: "omp", state: await ompSync(ctx) }),
75
+ });
76
+ }
77
+
78
+ // A populated skills manifest deploys with `-a claude-code codex`, and symlink healing also writes into Claude's tree.
79
+ ctx.syncConcurrency = syncConcurrencyForManifest(
80
+ ctx.syncConcurrency,
81
+ payloadText("SoT/.agents/skills.txt"),
82
+ ctx.syncClaude,
83
+ ctx.syncAgents,
84
+ );
85
+
86
+ const remaining = new Set(selected.map(({ name }) => name));
87
+ const lease = acquireTerminal(`Syncing ${[...remaining].join(", ")}...`);
88
+ ctx.terminalLease = lease;
89
+ let results: Array<PipelineResult>;
90
+ try {
91
+ const tasks = selected.map(({ name, run }): SyncTask<PipelineResult> => async () => {
92
+ try {
93
+ return await run();
94
+ } finally {
95
+ remaining.delete(name);
96
+ if (remaining.size > 0) lease.update(`Syncing ${[...remaining].join(", ")}...`);
97
+ }
98
+ });
99
+ results = await runBounded(tasks, ctx.syncConcurrency);
100
+ } finally {
101
+ lease.release();
102
+ ctx.terminalLease = undefined;
103
+ }
104
+
105
+ const claudeRan = ctx.syncClaude;
106
+ const codexRan = ctx.syncCodex;
107
+ let claudeRuntime: ClaudeRuntimeState | undefined;
108
+ let skillsState: SkillsState | undefined;
109
+ let ompState: OmpState | undefined;
110
+ for (const result of results) {
111
+ if (result.kind === "claude") claudeRuntime = result.runtime;
112
+ else if (result.kind === "skills") skillsState = result.state;
113
+ else if (result.kind === "omp") ompState = result.state;
114
+ }
115
+
116
+ echo("");
117
+ echo("--- Sync complete ---");
118
+ echo(`Repo: ${ctx.repoDir}`);
119
+ if (claudeRuntime !== undefined) claudeSummary(ctx, claudeRuntime);
120
+ if (codexRan) codexSummary(ctx);
121
+ if (skillsState !== undefined) skillsSummary(ctx, skillsState);
122
+ if (ompState !== undefined) ompSummary(ctx, ompState);
123
+
124
+ const advice = [
125
+ ...(claudeRan ? claudeNextSteps(ctx) : []),
126
+ ...(codexRan ? codexNextSteps(ctx) : []),
127
+ ...(skillsState !== undefined ? skillsNextSteps(ctx) : []),
128
+ ...(ompState !== undefined ? ompNextSteps(ctx) : []),
129
+ ];
130
+ if (advice.length > 0) {
131
+ echo("");
132
+ for (const line of advice) echo(line);
133
+ }
134
+ if (ctx.failures.length > 0) {
135
+ echo("");
136
+ echo("--- Failures ---");
137
+ for (const failure of ctx.failures) echo(`- ${failure}`);
138
+ return 1;
139
+ }
140
+ return 0;
141
+ }