easy-coding-harness 0.4.0 → 0.5.1

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.
package/dist/cli.js CHANGED
@@ -5,12 +5,207 @@ import chalk8 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/add-agent.ts
8
- import path13 from "path";
9
8
  import { outro } from "@clack/prompts";
10
9
  import chalk2 from "chalk";
11
10
 
12
- // src/configurators/claude.ts
13
- import path5 from "path";
11
+ // src/constants/version.ts
12
+ import { readFileSync } from "fs";
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+ var packageMetadata = readPackageMetadata();
16
+ var PACKAGE_NAME = packageMetadata.name;
17
+ var VERSION = packageMetadata.version;
18
+ function readPackageMetadata() {
19
+ const here = path.dirname(fileURLToPath(import.meta.url));
20
+ const candidates = [
21
+ path.resolve(here, "../../package.json"),
22
+ path.resolve(here, "../package.json"),
23
+ path.resolve(process.cwd(), "package.json")
24
+ ];
25
+ for (const candidate of candidates) {
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
28
+ if (parsed.name && parsed.version) {
29
+ return { name: parsed.name, version: parsed.version };
30
+ }
31
+ } catch {
32
+ }
33
+ }
34
+ throw new Error(`Unable to locate package metadata. Tried: ${candidates.join(", ")}`);
35
+ }
36
+
37
+ // src/ui/banner.ts
38
+ import chalk from "chalk";
39
+ import figlet from "figlet";
40
+ var WIDE_TERMINAL_WIDTH = 88;
41
+ var MEDIUM_TERMINAL_WIDTH = 72;
42
+ function renderBanner() {
43
+ const terminalWidth = process.stdout.columns ?? 120;
44
+ const art = figlet.textSync("Easy Coding", selectBannerPreset(terminalWidth));
45
+ console.log(colorizeBanner(art));
46
+ console.log(chalk.bold.white(` Easy Coding Harness ${chalk.cyan(`v${VERSION}`)}
47
+ `));
48
+ }
49
+ function selectBannerPreset(width) {
50
+ if (width >= WIDE_TERMINAL_WIDTH) {
51
+ return { font: "ANSI Shadow", horizontalLayout: "full" };
52
+ }
53
+ if (width >= MEDIUM_TERMINAL_WIDTH) {
54
+ return { font: "Small Shadow", horizontalLayout: "full" };
55
+ }
56
+ return { font: "Small Slant" };
57
+ }
58
+ function colorizeBanner(art) {
59
+ const colors = [chalk.cyanBright, chalk.cyan, chalk.blueBright, chalk.blue];
60
+ return art.split("\n").map((line, index) => colors[index % colors.length](line)).join("\n");
61
+ }
62
+
63
+ // src/utils/config-yaml.ts
64
+ import { readFile as readFile2 } from "fs/promises";
65
+ import YAML, { isScalar, isSeq, parseDocument } from "yaml";
66
+
67
+ // src/utils/file-writer.ts
68
+ import { constants } from "fs";
69
+ import { access, cp, writeFile as fsWriteFile, mkdir, readFile, stat } from "fs/promises";
70
+ import path2 from "path";
71
+ async function pathExists(filePath) {
72
+ try {
73
+ await access(filePath, constants.F_OK);
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+ async function ensureDir(dir) {
80
+ await mkdir(dir, { recursive: true });
81
+ }
82
+ async function writeTextFile(filePath, content) {
83
+ await ensureDir(path2.dirname(filePath));
84
+ await fsWriteFile(filePath, content.endsWith("\n") ? content : `${content}
85
+ `, "utf8");
86
+ }
87
+ async function readTextFile(filePath) {
88
+ return readFile(filePath, "utf8");
89
+ }
90
+ async function readTextIfExists(filePath) {
91
+ if (!await pathExists(filePath)) {
92
+ return null;
93
+ }
94
+ return readTextFile(filePath);
95
+ }
96
+ async function isDirectory(filePath) {
97
+ try {
98
+ return (await stat(filePath)).isDirectory();
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ // src/utils/config-yaml.ts
105
+ function createDefaultConfig(params) {
106
+ const config = {
107
+ version: 1,
108
+ harness_version: params.harnessVersion,
109
+ agents: params.agents,
110
+ project: {
111
+ name: params.projectName
112
+ },
113
+ memory: {
114
+ short_term_max: 10,
115
+ short_term_keep: 5,
116
+ schema_version: 2
117
+ },
118
+ tasks: {
119
+ auto_archive_days: 30
120
+ },
121
+ behavior: {
122
+ strict_confirm: true,
123
+ auto_mode: false
124
+ }
125
+ };
126
+ if (params.supermodule) {
127
+ config.supermodule = params.supermodule;
128
+ }
129
+ return config;
130
+ }
131
+ function stringifyConfig(config) {
132
+ return YAML.stringify(config);
133
+ }
134
+ async function writeConfigYaml(filePath, config) {
135
+ await writeTextFile(filePath, stringifyConfig(config));
136
+ }
137
+ async function readConfigYaml(filePath) {
138
+ const content = await readFile2(filePath, "utf8");
139
+ return YAML.parse(content);
140
+ }
141
+ async function updateConfigYaml(filePath, updater) {
142
+ const content = await readFile2(filePath, "utf8");
143
+ const document = parseDocument(content);
144
+ const config = document.toJSON();
145
+ updater(config);
146
+ for (const [key, value] of Object.entries(config)) {
147
+ document.set(key, value);
148
+ }
149
+ await writeTextFile(filePath, document.toString());
150
+ return config;
151
+ }
152
+ async function addAgentsToConfig(filePath, agents) {
153
+ return updateConfigYaml(filePath, (config) => {
154
+ const merged = /* @__PURE__ */ new Set([...config.agents ?? [], ...agents]);
155
+ config.agents = [...merged];
156
+ });
157
+ }
158
+ async function updateHarnessVersion(filePath, version) {
159
+ return updateConfigYaml(filePath, (config) => {
160
+ config.harness_version = version;
161
+ });
162
+ }
163
+ async function updateSupermoduleConfig(filePath, supermodule) {
164
+ return updateConfigYaml(filePath, (config) => {
165
+ config.supermodule = supermodule;
166
+ });
167
+ }
168
+
169
+ // src/utils/gitignore.ts
170
+ import path3 from "path";
171
+
172
+ // src/constants/paths.ts
173
+ var EASY_CODING_DIR = ".easy-coding";
174
+ var CONFIG_FILE = "config.yaml";
175
+ var INSTALL_MANIFEST_FILE = "install-manifest.json";
176
+ var SESSIONS_DIR = "sessions";
177
+ var TASKS_DIR = "tasks";
178
+ var PROJECT_INIT_TASK_ID = "project-init";
179
+ var MEMORY_DIR = "memory";
180
+ var SPEC_DIR = "spec";
181
+ var MAIN_SPEC_DIR = "main";
182
+ var DEV_SPEC_DIR = "dev";
183
+ var TEMPLATES_DIR = "templates";
184
+ var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
185
+ var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
186
+ var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness generated \u2550\u2550\u2550 -->";
187
+
188
+ // src/utils/gitignore.ts
189
+ async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Personal runtime state; do not commit") {
190
+ const gitignorePath = path3.join(cwd, ".gitignore");
191
+ const current = await readTextIfExists(gitignorePath) ?? "";
192
+ const lines = current.split(/\r?\n/).map((line) => line.trim());
193
+ if (lines.includes(entry)) {
194
+ return false;
195
+ }
196
+ const prefix = current.trimEnd();
197
+ const next = [prefix, heading, entry].filter(Boolean).join("\n");
198
+ await writeTextFile(gitignorePath, next);
199
+ return true;
200
+ }
201
+ async function ensureEasyCodingSessionsIgnored(cwd) {
202
+ return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
203
+ }
204
+
205
+ // src/utils/install-manifest.ts
206
+ import { createHash } from "crypto";
207
+ import { readFile as readFile3 } from "fs/promises";
208
+ import path4 from "path";
14
209
 
15
210
  // src/types/platform.ts
16
211
  var pythonCmd = process.platform === "win32" ? "python" : "python3";
@@ -34,7 +229,8 @@ var PLATFORM_META = {
34
229
  workflow_state_path: ".easy-coding/sessions/",
35
230
  main_constraint_file: "CLAUDE.md",
36
231
  python_cmd: pythonCmd,
37
- platform_config_dir: ".claude"
232
+ platform_config_dir: ".claude",
233
+ supermodule_boundary: ""
38
234
  }
39
235
  },
40
236
  codex: {
@@ -56,7 +252,8 @@ var PLATFORM_META = {
56
252
  workflow_state_path: ".easy-coding/sessions/",
57
253
  main_constraint_file: "AGENTS.md",
58
254
  python_cmd: pythonCmd,
59
- platform_config_dir: ".codex"
255
+ platform_config_dir: ".codex",
256
+ supermodule_boundary: ""
60
257
  }
61
258
  },
62
259
  qoder: {
@@ -79,7 +276,8 @@ var PLATFORM_META = {
79
276
  workflow_state_path: ".easy-coding/sessions/",
80
277
  main_constraint_file: "AGENTS.md",
81
278
  python_cmd: pythonCmd,
82
- platform_config_dir: ".qoder"
279
+ platform_config_dir: ".qoder",
280
+ supermodule_boundary: ""
83
281
  }
84
282
  }
85
283
  };
@@ -88,64 +286,6 @@ function isAgentPlatform(value) {
88
286
  return Object.hasOwn(PLATFORM_META, value);
89
287
  }
90
288
 
91
- // src/utils/install-manifest.ts
92
- import { createHash } from "crypto";
93
- import { readFile as readFile2 } from "fs/promises";
94
- import path2 from "path";
95
-
96
- // src/constants/paths.ts
97
- var EASY_CODING_DIR = ".easy-coding";
98
- var CONFIG_FILE = "config.yaml";
99
- var INSTALL_MANIFEST_FILE = "install-manifest.json";
100
- var SESSIONS_DIR = "sessions";
101
- var TASKS_DIR = "tasks";
102
- var PROJECT_INIT_TASK_ID = "project-init";
103
- var MEMORY_DIR = "memory";
104
- var SPEC_DIR = "spec";
105
- var MAIN_SPEC_DIR = "main";
106
- var DEV_SPEC_DIR = "dev";
107
- var TEMPLATES_DIR = "templates";
108
- var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
109
- var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
110
- var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness generated \u2550\u2550\u2550 -->";
111
-
112
- // src/utils/file-writer.ts
113
- import { constants } from "fs";
114
- import { access, cp, writeFile as fsWriteFile, mkdir, readFile, stat } from "fs/promises";
115
- import path from "path";
116
- async function pathExists(filePath) {
117
- try {
118
- await access(filePath, constants.F_OK);
119
- return true;
120
- } catch {
121
- return false;
122
- }
123
- }
124
- async function ensureDir(dir) {
125
- await mkdir(dir, { recursive: true });
126
- }
127
- async function writeTextFile(filePath, content) {
128
- await ensureDir(path.dirname(filePath));
129
- await fsWriteFile(filePath, content.endsWith("\n") ? content : `${content}
130
- `, "utf8");
131
- }
132
- async function readTextFile(filePath) {
133
- return readFile(filePath, "utf8");
134
- }
135
- async function readTextIfExists(filePath) {
136
- if (!await pathExists(filePath)) {
137
- return null;
138
- }
139
- return readTextFile(filePath);
140
- }
141
- async function isDirectory(filePath) {
142
- try {
143
- return (await stat(filePath)).isDirectory();
144
- } catch {
145
- return false;
146
- }
147
- }
148
-
149
289
  // src/utils/install-manifest.ts
150
290
  function fileArtifact(filePath, kind, platform) {
151
291
  return { type: "file", kind, filePath, platform };
@@ -153,8 +293,8 @@ function fileArtifact(filePath, kind, platform) {
153
293
  function constraintRegionArtifact(filePath, platform) {
154
294
  return { type: "constraint-region", filePath, platform };
155
295
  }
156
- async function hookRegistrationArtifacts(configPath, platform) {
157
- const content = await readTextIfExists(configPath);
296
+ async function hookRegistrationArtifacts(configPath2, platform) {
297
+ const content = await readTextIfExists(configPath2);
158
298
  if (content === null) {
159
299
  return [];
160
300
  }
@@ -166,7 +306,7 @@ async function hookRegistrationArtifacts(configPath, platform) {
166
306
  }
167
307
  return collectHookCommands(parsed.hooks).map((command) => ({
168
308
  type: "hook-registration",
169
- configPath,
309
+ configPath: configPath2,
170
310
  command,
171
311
  hookPath: extractHookPathFromCommand(command),
172
312
  platform
@@ -206,14 +346,14 @@ async function writeInstallManifest(cwd, params) {
206
346
  continue;
207
347
  }
208
348
  if (artifact.type === "hook-registration") {
209
- const configPath = toProjectPath(cwd, artifact.configPath);
349
+ const configPath2 = toProjectPath(cwd, artifact.configPath);
210
350
  const registration = {
211
- config_path: configPath,
351
+ config_path: configPath2,
212
352
  command: artifact.command,
213
- hook_path: artifact.hookPath,
353
+ hook_path: artifact.hookPath ? normalizeHookPath(cwd, artifact.hookPath) : null,
214
354
  platform: artifact.platform
215
355
  };
216
- hookRegistrations.set(`${configPath}\0${normalizeCommand(artifact.command)}`, registration);
356
+ hookRegistrations.set(`${configPath2}\0${normalizeCommand(artifact.command)}`, registration);
217
357
  continue;
218
358
  }
219
359
  const relPath = toProjectPath(cwd, artifact.filePath);
@@ -234,12 +374,12 @@ async function writeInstallManifest(cwd, params) {
234
374
  constraint_regions: [...constraintRegions.values()].sort(byPath)
235
375
  };
236
376
  await writeTextFile(
237
- path2.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
377
+ path4.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
238
378
  JSON.stringify(manifest, null, 2)
239
379
  );
240
380
  }
241
381
  async function readInstallManifest(cwd) {
242
- const manifestPath2 = path2.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
382
+ const manifestPath2 = path4.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
243
383
  const content = await readTextIfExists(manifestPath2);
244
384
  if (content === null) {
245
385
  return null;
@@ -265,32 +405,32 @@ function manifestPath(cwd, projectPath) {
265
405
  return resolveProjectPath(cwd, projectPath);
266
406
  }
267
407
  function toProjectPath(cwd, filePath) {
268
- const root = path2.resolve(cwd);
269
- const resolved = path2.resolve(filePath);
408
+ const root = path4.resolve(cwd);
409
+ const resolved = path4.resolve(filePath);
270
410
  assertPathInsideProject(root, resolved, filePath);
271
- return path2.relative(root, resolved).split(path2.sep).join("/");
411
+ return path4.relative(root, resolved).split(path4.sep).join("/");
272
412
  }
273
413
  function normalizeCommand(command) {
274
414
  return command.replace(/\\/g, "/").trim().replace(/\s+/g, " ");
275
415
  }
276
416
  async function sha256File(filePath) {
277
- const content = await readFile2(filePath);
417
+ const content = await readFile3(filePath);
278
418
  return createHash("sha256").update(content).digest("hex");
279
419
  }
280
420
  function resolveProjectPath(cwd, projectPath) {
281
421
  const normalized = projectPath.replace(/\\/g, "/");
282
422
  const parts = normalized.split("/");
283
- if (normalized.trim() === "" || path2.isAbsolute(projectPath) || path2.posix.isAbsolute(normalized) || path2.win32.isAbsolute(projectPath) || /^[A-Za-z]:/.test(projectPath) || parts.some((part) => part === "..")) {
423
+ if (normalized.trim() === "" || path4.isAbsolute(projectPath) || path4.posix.isAbsolute(normalized) || path4.win32.isAbsolute(projectPath) || /^[A-Za-z]:/.test(projectPath) || parts.some((part) => part === "..")) {
284
424
  throw new Error(`Unsafe install manifest path: ${projectPath}`);
285
425
  }
286
- const root = path2.resolve(cwd);
287
- const resolved = path2.resolve(root, normalized);
426
+ const root = path4.resolve(cwd);
427
+ const resolved = path4.resolve(root, normalized);
288
428
  assertPathInsideProject(root, resolved, projectPath);
289
429
  return resolved;
290
430
  }
291
431
  function assertPathInsideProject(root, resolvedPath, sourcePath) {
292
- const relative = path2.relative(root, resolvedPath);
293
- if (!relative || relative.startsWith("..") || path2.isAbsolute(relative)) {
432
+ const relative = path4.relative(root, resolvedPath);
433
+ if (!relative || relative.startsWith("..") || path4.isAbsolute(relative)) {
294
434
  throw new Error(`Unsafe install manifest path: ${sourcePath}`);
295
435
  }
296
436
  }
@@ -325,20 +465,250 @@ function collectHookCommands(hooks) {
325
465
  return commands;
326
466
  }
327
467
  function extractHookPathFromCommand(command) {
328
- const match = normalizeCommand(command).match(/(?:^|\s)(\S+\/hooks\/\S+\.py)(?:\s|$)/);
329
- return match?.[1] ?? null;
330
- }
331
- function byPath(a, b) {
332
- return a.path.localeCompare(b.path);
468
+ return extractQuotedHookPath(command) ?? extractUnquotedHookPath(command);
333
469
  }
334
-
335
- // src/configurators/shared.ts
336
- import { readdir, stat as stat2 } from "fs/promises";
337
- import path4 from "path";
338
-
339
- // src/utils/marked-region.ts
340
- var MarkedRegionError = class extends Error {
341
- constructor(message) {
470
+ function extractQuotedHookPath(command) {
471
+ for (let index = 0; index < command.length; index += 1) {
472
+ const quote = command[index];
473
+ if (quote !== '"' && quote !== "'") {
474
+ continue;
475
+ }
476
+ if (index > 0 && !/\s/.test(command[index - 1])) {
477
+ continue;
478
+ }
479
+ const parsed = parseQuotedToken(command, index, quote);
480
+ if (!parsed) {
481
+ continue;
482
+ }
483
+ const suffix = readPathSuffix(command, parsed.endIndex);
484
+ for (const candidate of [parsed.value + suffix, parsed.value]) {
485
+ if (isHookPythonPath(candidate)) {
486
+ return candidate;
487
+ }
488
+ }
489
+ index = parsed.endIndex;
490
+ }
491
+ return null;
492
+ }
493
+ function parseQuotedToken(command, startIndex, quote) {
494
+ let value = "";
495
+ for (let index = startIndex + 1; index < command.length; index += 1) {
496
+ const char = command[index];
497
+ if (quote === '"' && char === "\\" && index + 1 < command.length) {
498
+ value += command[index + 1];
499
+ index += 1;
500
+ continue;
501
+ }
502
+ if (char === quote) {
503
+ return { value, endIndex: index + 1 };
504
+ }
505
+ value += char;
506
+ }
507
+ return null;
508
+ }
509
+ function readPathSuffix(command, startIndex) {
510
+ if (command[startIndex] !== "/") {
511
+ return "";
512
+ }
513
+ let suffix = "";
514
+ for (let index = startIndex; index < command.length; index += 1) {
515
+ const char = command[index];
516
+ if (/\s/.test(char)) {
517
+ break;
518
+ }
519
+ suffix += char;
520
+ }
521
+ return suffix;
522
+ }
523
+ function extractUnquotedHookPath(command) {
524
+ const match = command.match(/(?:^|\s)(\S+\/hooks\/\S+\.py)(?:\s|$)/);
525
+ return match?.[1] ?? null;
526
+ }
527
+ function isHookPythonPath(candidate) {
528
+ return /\/hooks\/\S+\.py$/.test(candidate);
529
+ }
530
+ function normalizeHookPath(cwd, hookPath) {
531
+ const normalized = hookPath.replace(/\\/g, "/").replace(/^\.\//, "");
532
+ if (path4.isAbsolute(hookPath) || path4.posix.isAbsolute(normalized) || path4.win32.isAbsolute(hookPath)) {
533
+ return toProjectPath(cwd, hookPath);
534
+ }
535
+ return normalized;
536
+ }
537
+ function byPath(a, b) {
538
+ return a.path.localeCompare(b.path);
539
+ }
540
+
541
+ // src/utils/runtime-scaffold.ts
542
+ import path6 from "path";
543
+
544
+ // src/utils/template-paths.ts
545
+ import { existsSync } from "fs";
546
+ import path5 from "path";
547
+ import { fileURLToPath as fileURLToPath2 } from "url";
548
+ function getTemplateRoot() {
549
+ const here = path5.dirname(fileURLToPath2(import.meta.url));
550
+ const candidates = [
551
+ path5.resolve(here, "../templates"),
552
+ path5.resolve(here, "../../templates"),
553
+ path5.resolve(process.cwd(), "src/templates"),
554
+ path5.resolve(process.cwd(), "templates")
555
+ ];
556
+ const found = candidates.find((candidate) => existsSync(candidate));
557
+ if (!found) {
558
+ throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
559
+ }
560
+ return found;
561
+ }
562
+ function getTemplatePath(...segments) {
563
+ return path5.join(getTemplateRoot(), ...segments);
564
+ }
565
+
566
+ // src/utils/runtime-scaffold.ts
567
+ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
568
+ const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
569
+ await ensureDir(easyCodingDir);
570
+ const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
571
+ if (!await pathExists(configPath2)) {
572
+ const projectName = path6.basename(cwd);
573
+ await writeConfigYaml(
574
+ configPath2,
575
+ createDefaultConfig({
576
+ projectName,
577
+ harnessVersion: VERSION,
578
+ agents,
579
+ supermodule: opts.supermodule
580
+ })
581
+ );
582
+ }
583
+ await ensureDir(path6.join(easyCodingDir, "tasks"));
584
+ await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
585
+ await ensureDir(path6.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
586
+ await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
587
+ await writeMemoryScaffold(easyCodingDir);
588
+ await writeTemplatesScaffold(easyCodingDir);
589
+ }
590
+ async function writeTemplatesScaffold(easyCodingDir) {
591
+ const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
592
+ await ensureDir(templatesDir);
593
+ const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
594
+ const dest = path6.join(templatesDir, "dev-spec-skeleton.md");
595
+ await writeTextFile(dest, await readTextFile(src));
596
+ }
597
+ async function writeMemoryScaffold(easyCodingDir) {
598
+ const memoryDir = path6.join(easyCodingDir, MEMORY_DIR);
599
+ await ensureDir(path6.join(memoryDir, "short"));
600
+ await ensureDir(path6.join(memoryDir, "long"));
601
+ for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
602
+ const destination = path6.join(memoryDir, "long", file);
603
+ if (await pathExists(destination)) {
604
+ continue;
605
+ }
606
+ const templatePath = getTemplatePath("runtime", "memory", "long", file);
607
+ await writeTextFile(destination, await readTextFile(templatePath));
608
+ }
609
+ const shortTemplateDest = path6.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
610
+ if (!await pathExists(shortTemplateDest)) {
611
+ const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
612
+ await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
613
+ }
614
+ }
615
+
616
+ // src/utils/task-json.ts
617
+ import { readdir } from "fs/promises";
618
+ import path7 from "path";
619
+ function createProjectInitTask(params) {
620
+ return {
621
+ type: "project-init",
622
+ status: "PENDING",
623
+ created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
624
+ created_by: "cli-init",
625
+ last_agent: "cli",
626
+ stage_history: [],
627
+ context: {
628
+ agents_installed: params.agents,
629
+ cli_version: VERSION,
630
+ project_path: params.cwd,
631
+ init_source: params.initSource ?? "fresh",
632
+ ...params.legacyAssets ? { legacy_assets: params.legacyAssets } : {},
633
+ ...params.legacyMissingHarnessFiles ? { legacy_missing_harness_files: params.legacyMissingHarnessFiles } : {}
634
+ },
635
+ init_log: []
636
+ };
637
+ }
638
+ function getTaskJsonPath(cwd, taskId) {
639
+ return path7.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
640
+ }
641
+ async function writeTaskJson(filePath, task) {
642
+ await writeTextFile(filePath, JSON.stringify(task, null, 2));
643
+ }
644
+ async function readTaskJson(filePath) {
645
+ return JSON.parse(await readTextFile(filePath));
646
+ }
647
+ async function writeProjectInitTask(cwd, agents, options = {}) {
648
+ await writeTaskJson(
649
+ getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID),
650
+ createProjectInitTask({
651
+ cwd,
652
+ agents,
653
+ initSource: options.initSource,
654
+ legacyAssets: options.legacyAssets,
655
+ legacyMissingHarnessFiles: options.legacyMissingHarnessFiles
656
+ })
657
+ );
658
+ }
659
+ async function setPendingInitSince(cwd, version) {
660
+ const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
661
+ if (!await pathExists(filePath)) return;
662
+ const task = await readTaskJson(filePath);
663
+ if (task.status !== "COMPLETE") return;
664
+ task.pending_init_since = version;
665
+ await writeTaskJson(filePath, task);
666
+ }
667
+ async function listTasks(cwd) {
668
+ const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
669
+ if (!await pathExists(tasksDir)) {
670
+ return [];
671
+ }
672
+ const entries = await readdir(tasksDir, { withFileTypes: true });
673
+ const tasks = [];
674
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
675
+ if (!entry.isDirectory()) {
676
+ continue;
677
+ }
678
+ const taskPath = getTaskJsonPath(cwd, entry.name);
679
+ if (!await pathExists(taskPath)) {
680
+ continue;
681
+ }
682
+ tasks.push({ id: entry.name, task: await readTaskJson(taskPath) });
683
+ }
684
+ return tasks;
685
+ }
686
+ function summarizeTaskStatuses(tasks) {
687
+ return tasks.reduce(
688
+ (summary, item) => {
689
+ summary[item.task.status] = (summary[item.task.status] ?? 0) + 1;
690
+ return summary;
691
+ },
692
+ {}
693
+ );
694
+ }
695
+ function isActiveTask(task) {
696
+ return task.status !== "COMPLETE" && task.status !== "CLOSED";
697
+ }
698
+
699
+ // src/commands/install-harness.ts
700
+ import path13 from "path";
701
+
702
+ // src/configurators/claude.ts
703
+ import path9 from "path";
704
+
705
+ // src/configurators/shared.ts
706
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
707
+ import path8 from "path";
708
+
709
+ // src/utils/marked-region.ts
710
+ var MarkedRegionError = class extends Error {
711
+ constructor(message) {
342
712
  super(message);
343
713
  this.name = "MarkedRegionError";
344
714
  }
@@ -373,28 +743,6 @@ function removeMarkedRegion(content) {
373
743
  ` : "";
374
744
  }
375
745
 
376
- // src/utils/template-paths.ts
377
- import { existsSync } from "fs";
378
- import path3 from "path";
379
- import { fileURLToPath } from "url";
380
- function getTemplateRoot() {
381
- const here = path3.dirname(fileURLToPath(import.meta.url));
382
- const candidates = [
383
- path3.resolve(here, "../templates"),
384
- path3.resolve(here, "../../templates"),
385
- path3.resolve(process.cwd(), "src/templates"),
386
- path3.resolve(process.cwd(), "templates")
387
- ];
388
- const found = candidates.find((candidate) => existsSync(candidate));
389
- if (!found) {
390
- throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
391
- }
392
- return found;
393
- }
394
- function getTemplatePath(...segments) {
395
- return path3.join(getTemplateRoot(), ...segments);
396
- }
397
-
398
746
  // src/configurators/shared.ts
399
747
  var UnresolvedPlaceholderError = class extends Error {
400
748
  constructor(contentName, placeholders) {
@@ -414,16 +762,43 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
414
762
  }
415
763
  return resolved;
416
764
  }
765
+ function withInstallPaths(cwd, ctx) {
766
+ const hooksDir = toPosixPath(path8.resolve(cwd, ctx.platform_config_dir, "hooks"));
767
+ return {
768
+ ...ctx,
769
+ platform_hooks_dir_abs: hooksDir,
770
+ platform_hooks_dir_shell: jsonStringContent(shellDoubleQuoteArg(hooksDir))
771
+ };
772
+ }
773
+ function renderHookCommand(cwd, ctx, scriptName, platform = process.platform) {
774
+ const hooksDir = toPosixPath(path8.resolve(cwd, ctx.platform_config_dir, "hooks"));
775
+ return `${ctx.python_cmd} ${shellDoubleQuoteArg(hooksDir, platform)}/${scriptName}`;
776
+ }
777
+ function shellDoubleQuoteArg(value, platform = process.platform) {
778
+ if (platform === "win32") {
779
+ if (value.includes('"')) {
780
+ throw new Error("Windows hook paths cannot contain double quotes.");
781
+ }
782
+ return `"${value}"`;
783
+ }
784
+ return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
785
+ }
786
+ function toPosixPath(filePath) {
787
+ return filePath.split(path8.sep).join("/");
788
+ }
789
+ function jsonStringContent(value) {
790
+ return JSON.stringify(value).slice(1, -1);
791
+ }
417
792
  async function resolveSkills(ctx) {
418
793
  const skillsRoot = getTemplatePath("common", "skills");
419
- const entries = await readdir(skillsRoot);
794
+ const entries = await readdir2(skillsRoot);
420
795
  const skills = [];
421
796
  for (const entry of entries.sort()) {
422
- const skillDir = path4.join(skillsRoot, entry);
797
+ const skillDir = path8.join(skillsRoot, entry);
423
798
  if (!await isDirectory(skillDir)) {
424
799
  continue;
425
800
  }
426
- const content = await readTextFile(path4.join(skillDir, "SKILL.md"));
801
+ const content = await readTextFile(path8.join(skillDir, "SKILL.md"));
427
802
  skills.push({
428
803
  name: entry,
429
804
  content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
@@ -436,10 +811,10 @@ async function resolveBundledSkills(ctx) {
436
811
  if (!await pathExists(bundledRoot)) {
437
812
  return [];
438
813
  }
439
- const entries = await readdir(bundledRoot);
814
+ const entries = await readdir2(bundledRoot);
440
815
  const bundled = [];
441
816
  for (const entry of entries.sort()) {
442
- const sourceDir = path4.join(bundledRoot, entry);
817
+ const sourceDir = path8.join(bundledRoot, entry);
443
818
  if (await isDirectory(sourceDir)) {
444
819
  bundled.push({ name: entry, sourceDir, context: ctx });
445
820
  }
@@ -450,13 +825,13 @@ async function writeSkills(dir, skills, bundled) {
450
825
  await ensureDir(dir);
451
826
  const written = [];
452
827
  for (const skill of skills) {
453
- const destination = path4.join(dir, skill.name, "SKILL.md");
828
+ const destination = path8.join(dir, skill.name, "SKILL.md");
454
829
  await writeTextFile(destination, skill.content);
455
830
  written.push(destination);
456
831
  }
457
832
  for (const skill of bundled) {
458
833
  written.push(
459
- ...await copyTemplateDirectory(skill.sourceDir, path4.join(dir, skill.name), skill.context)
834
+ ...await copyTemplateDirectory(skill.sourceDir, path8.join(dir, skill.name), skill.context)
460
835
  );
461
836
  }
462
837
  return written;
@@ -464,14 +839,14 @@ async function writeSkills(dir, skills, bundled) {
464
839
  async function writeSharedHooks(dir, platform, opts = {}) {
465
840
  const hooksRoot = getTemplatePath("shared-hooks");
466
841
  const ctx = PLATFORM_META[platform].templateContext;
467
- const entries = await readdir(hooksRoot);
842
+ const entries = await readdir2(hooksRoot);
468
843
  await ensureDir(dir);
469
844
  const written = [];
470
845
  for (const entry of entries.sort()) {
471
846
  if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
472
847
  continue;
473
848
  }
474
- const sourcePath = path4.join(hooksRoot, entry);
849
+ const sourcePath = path8.join(hooksRoot, entry);
475
850
  if ((await stat2(sourcePath)).isDirectory()) {
476
851
  continue;
477
852
  }
@@ -480,7 +855,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
480
855
  ctx,
481
856
  `shared-hooks/${entry}`
482
857
  );
483
- const destination = path4.join(dir, entry);
858
+ const destination = path8.join(dir, entry);
484
859
  await writeTextFile(destination, content);
485
860
  await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
486
861
  written.push(destination);
@@ -491,16 +866,19 @@ async function copyPlatformTemplates(platformTemplateDir, destination, skipDirs,
491
866
  const source = getTemplatePath(platformTemplateDir);
492
867
  return copyTemplateDirectory(source, destination, ctx, new Set(skipDirs));
493
868
  }
494
- async function writeMainConstraint(cwd, platform) {
869
+ async function writeMainConstraint(cwd, platform, opts = {}) {
495
870
  const meta = PLATFORM_META[platform];
496
- const ctx = meta.templateContext;
871
+ const ctx = {
872
+ ...meta.templateContext,
873
+ supermodule_boundary: renderSupermoduleBoundary(opts.supermodule)
874
+ };
497
875
  const templateName = `${meta.mainConstraint}.tpl`;
498
876
  const template = await readTextFile(getTemplatePath("main-constraint", templateName));
499
877
  const generated = resolvePlaceholders(template, ctx, `main-constraint/${templateName}`);
500
878
  if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
501
879
  throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
502
880
  }
503
- const destination = path4.join(cwd, meta.mainConstraint);
881
+ const destination = path8.join(cwd, meta.mainConstraint);
504
882
  const current = await readTextIfExists(destination);
505
883
  if (current) {
506
884
  const markerContent = extractMarkedRegion(generated);
@@ -513,15 +891,37 @@ async function writeMainConstraint(cwd, platform) {
513
891
  }
514
892
  return destination;
515
893
  }
894
+ function renderSupermoduleBoundary(boundary) {
895
+ if (!boundary || boundary.submodulePaths.length === 0) {
896
+ return "";
897
+ }
898
+ const submodules = boundary.submodulePaths.map((submodulePath) => `- \`${submodulePath}\``);
899
+ return [
900
+ "",
901
+ "## Supermodule Boundary",
902
+ "",
903
+ "This directory is a git supermodule root. The following subdirectories are independent",
904
+ "submodule harness roots with their own git boundaries and Easy Coding runtime:",
905
+ "",
906
+ ...submodules,
907
+ "",
908
+ "- Cross-repo work launched from this root uses the parent `.easy-coding` task, session, state, and spec as the source of truth.",
909
+ "- Child `.easy-coding` directories are not parent workflow state sources.",
910
+ "- During memory archive, technical memory that belongs to a child repo is written only to that child's `.easy-coding/memory`; do not write child task/session/state files from the parent.",
911
+ "- Commit submodule changes in two steps: push each child repo first, then commit and push the parent gitlink update.",
912
+ "- Parent memory should keep cross-repo context; child technical details should follow the owning child repo.",
913
+ ""
914
+ ].join("\n");
915
+ }
516
916
  async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
517
917
  await ensureDir(destination);
518
918
  const written = [];
519
- for (const entry of (await readdir(source)).sort()) {
919
+ for (const entry of (await readdir2(source)).sort()) {
520
920
  if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
521
921
  continue;
522
922
  }
523
- const sourcePath = path4.join(source, entry);
524
- const destinationPath = path4.join(destination, stripTemplateExtension(entry));
923
+ const sourcePath = path8.join(source, entry);
924
+ const destinationPath = path8.join(destination, stripTemplateExtension(entry));
525
925
  const sourceStat = await stat2(sourcePath);
526
926
  if (sourceStat.isDirectory()) {
527
927
  written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
@@ -541,63 +941,65 @@ function stripTemplateExtension(fileName) {
541
941
  }
542
942
 
543
943
  // src/configurators/claude.ts
544
- async function configureClaude(cwd) {
944
+ async function configureClaude(cwd, opts = {}) {
545
945
  const platform = "claude-code";
546
946
  const meta = PLATFORM_META[platform];
547
- const ctx = meta.templateContext;
548
- const dest = path5.join(cwd, ".claude");
549
- const hookConfigPath = path5.join(cwd, meta.hookConfigFile);
947
+ const ctx = withInstallPaths(cwd, meta.templateContext);
948
+ const dest = path9.join(cwd, ".claude");
949
+ const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
550
950
  const artifacts = [];
551
951
  const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
552
952
  artifacts.push(
553
953
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
554
954
  (filePath) => fileArtifact(
555
955
  filePath,
556
- filePath.startsWith(path5.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
956
+ filePath.startsWith(path9.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
557
957
  platform
558
958
  )
559
959
  )
560
960
  );
561
961
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
562
962
  artifacts.push(
563
- ...(await writeSharedHooks(path5.join(dest, "hooks"), platform)).map(
963
+ ...(await writeSharedHooks(path9.join(dest, "hooks"), platform)).map(
564
964
  (filePath) => fileArtifact(filePath, "hook", platform)
565
965
  )
566
966
  );
567
967
  artifacts.push(
568
968
  ...(await writeSkills(
569
- path5.join(cwd, meta.skillsDir),
969
+ path9.join(cwd, meta.skillsDir),
570
970
  await resolveSkills(ctx),
571
971
  await resolveBundledSkills(ctx)
572
972
  )).map((filePath) => fileArtifact(filePath, "skill", platform))
573
973
  );
574
- artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
974
+ artifacts.push(
975
+ constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
976
+ );
575
977
  return artifacts;
576
978
  }
577
979
 
578
980
  // src/configurators/codex.ts
579
- import path6 from "path";
580
- async function configureCodex(cwd) {
981
+ import path10 from "path";
982
+ async function configureCodex(cwd, opts = {}) {
581
983
  const platform = "codex";
582
984
  const meta = PLATFORM_META[platform];
583
- const ctx = meta.templateContext;
584
- const hookConfigPath = path6.join(cwd, meta.hookConfigFile);
985
+ const ctx = withInstallPaths(cwd, meta.templateContext);
986
+ const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
585
987
  const artifacts = [];
586
988
  artifacts.push(
587
989
  ...(await writeSkills(
588
- path6.join(cwd, meta.skillsDir),
990
+ path10.join(cwd, meta.skillsDir),
589
991
  await resolveSkills(ctx),
590
992
  await resolveBundledSkills(ctx)
591
993
  )).map((filePath) => fileArtifact(filePath, "skill", platform))
592
994
  );
593
995
  artifacts.push(
594
- ...(await writeSharedHooks(path6.join(cwd, meta.hooksDir), platform, {
996
+ ...(await writeSharedHooks(path10.join(cwd, meta.hooksDir), platform, {
595
997
  skipSubagentContext: true
596
998
  })).map((filePath) => fileArtifact(filePath, "hook", platform))
597
999
  );
598
1000
  const platformFiles = await copyPlatformTemplates(
599
1001
  "codex",
600
- path6.join(cwd, ".codex"),
1002
+ path10.join(cwd, ".codex"),
601
1003
  ["hooks"],
602
1004
  ctx
603
1005
  );
@@ -605,28 +1007,30 @@ async function configureCodex(cwd) {
605
1007
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
606
1008
  (filePath) => fileArtifact(
607
1009
  filePath,
608
- filePath.startsWith(path6.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1010
+ filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
609
1011
  platform
610
1012
  )
611
1013
  )
612
1014
  );
613
1015
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
614
- artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
1016
+ artifacts.push(
1017
+ constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
1018
+ );
615
1019
  return artifacts;
616
1020
  }
617
1021
 
618
1022
  // src/configurators/qoder.ts
619
- import { readdir as readdir2 } from "fs/promises";
620
- import path8 from "path";
1023
+ import { readdir as readdir3 } from "fs/promises";
1024
+ import path12 from "path";
621
1025
 
622
1026
  // src/utils/platform-paths.ts
623
1027
  import { existsSync as existsSync2 } from "fs";
624
- import path7 from "path";
1028
+ import path11 from "path";
625
1029
  function detectQoderCnVariant(cwd) {
626
1030
  if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
627
1031
  return true;
628
1032
  }
629
- return existsSync2(path7.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
1033
+ return existsSync2(path11.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
630
1034
  }
631
1035
  function resolvePlatformMeta(cwd, platform) {
632
1036
  const meta = PLATFORM_META[platform];
@@ -657,12 +1061,12 @@ function resolveQoderMetaForBaseDir(baseDir) {
657
1061
 
658
1062
  // src/configurators/qoder.ts
659
1063
  async function claudeHarnessSkillsExist(cwd) {
660
- return pathExists(path8.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
1064
+ return pathExists(path12.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
661
1065
  }
662
1066
  async function listFilesRecursive(dir) {
663
1067
  let entries;
664
1068
  try {
665
- entries = await readdir2(dir, { withFileTypes: true });
1069
+ entries = await readdir3(dir, { withFileTypes: true });
666
1070
  } catch (error) {
667
1071
  if (error.code === "ENOENT") {
668
1072
  return [];
@@ -671,7 +1075,7 @@ async function listFilesRecursive(dir) {
671
1075
  }
672
1076
  const files = [];
673
1077
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
674
- const entryPath = path8.join(dir, entry.name);
1078
+ const entryPath = path12.join(dir, entry.name);
675
1079
  if (entry.isDirectory()) {
676
1080
  files.push(...await listFilesRecursive(entryPath));
677
1081
  continue;
@@ -685,32 +1089,32 @@ async function listFilesRecursive(dir) {
685
1089
  async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
686
1090
  const artifacts = [];
687
1091
  for (const skillName of skillNames) {
688
- for (const filePath of await listFilesRecursive(path8.join(skillsDir, skillName))) {
1092
+ for (const filePath of await listFilesRecursive(path12.join(skillsDir, skillName))) {
689
1093
  artifacts.push(fileArtifact(filePath, "skill", platform));
690
1094
  }
691
1095
  }
692
1096
  return artifacts;
693
1097
  }
694
- async function configureQoder(cwd) {
1098
+ async function configureQoder(cwd, opts = {}) {
695
1099
  const platform = "qoder";
696
1100
  const meta = resolvePlatformMeta(cwd, platform);
697
- const ctx = meta.templateContext;
698
- const dest = path8.join(cwd, ctx.platform_config_dir);
699
- const hookConfigPath = path8.join(cwd, meta.hookConfigFile);
1101
+ const ctx = withInstallPaths(cwd, meta.templateContext);
1102
+ const dest = path12.join(cwd, ctx.platform_config_dir);
1103
+ const hookConfigPath = path12.join(cwd, meta.hookConfigFile);
700
1104
  const artifacts = [];
701
1105
  const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
702
1106
  artifacts.push(
703
1107
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
704
1108
  (filePath) => fileArtifact(
705
1109
  filePath,
706
- filePath.startsWith(path8.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1110
+ filePath.startsWith(path12.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
707
1111
  platform
708
1112
  )
709
1113
  )
710
1114
  );
711
1115
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
712
1116
  artifacts.push(
713
- ...(await writeSharedHooks(path8.join(dest, "hooks"), platform)).map(
1117
+ ...(await writeSharedHooks(path12.join(dest, "hooks"), platform)).map(
714
1118
  (filePath) => fileArtifact(filePath, "hook", platform)
715
1119
  )
716
1120
  );
@@ -718,20 +1122,22 @@ async function configureQoder(cwd) {
718
1122
  const bundledSkills = await resolveBundledSkills(ctx);
719
1123
  if (!await claudeHarnessSkillsExist(cwd)) {
720
1124
  artifacts.push(
721
- ...(await writeSkills(path8.join(dest, "skills"), skills, bundledSkills)).map(
1125
+ ...(await writeSkills(path12.join(dest, "skills"), skills, bundledSkills)).map(
722
1126
  (filePath) => fileArtifact(filePath, "skill", platform)
723
1127
  )
724
1128
  );
725
1129
  } else {
726
1130
  artifacts.push(
727
1131
  ...await existingManagedSkillArtifacts(
728
- path8.join(dest, "skills"),
1132
+ path12.join(dest, "skills"),
729
1133
  [...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
730
1134
  platform
731
1135
  )
732
1136
  );
733
1137
  }
734
- artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
1138
+ artifacts.push(
1139
+ constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
1140
+ );
735
1141
  return artifacts;
736
1142
  }
737
1143
 
@@ -742,261 +1148,64 @@ var CONFIGURATORS = {
742
1148
  qoder: configureQoder
743
1149
  };
744
1150
 
745
- // src/constants/version.ts
746
- import { readFileSync } from "fs";
747
- import path9 from "path";
748
- import { fileURLToPath as fileURLToPath2 } from "url";
749
- var packageMetadata = readPackageMetadata();
750
- var PACKAGE_NAME = packageMetadata.name;
751
- var VERSION = packageMetadata.version;
752
- function readPackageMetadata() {
753
- const here = path9.dirname(fileURLToPath2(import.meta.url));
754
- const candidates = [
755
- path9.resolve(here, "../../package.json"),
756
- path9.resolve(here, "../package.json"),
757
- path9.resolve(process.cwd(), "package.json")
758
- ];
759
- for (const candidate of candidates) {
760
- try {
761
- const parsed = JSON.parse(readFileSync(candidate, "utf8"));
762
- if (parsed.name && parsed.version) {
763
- return { name: parsed.name, version: parsed.version };
764
- }
765
- } catch {
766
- }
1151
+ // src/commands/install-harness.ts
1152
+ async function configurePlatformsForDir(targetDir, platforms, boundary) {
1153
+ const artifacts = [];
1154
+ for (const platform of platforms) {
1155
+ artifacts.push(...await CONFIGURATORS[platform](targetDir, { supermodule: boundary }));
767
1156
  }
768
- throw new Error(`Unable to locate package metadata. Tried: ${candidates.join(", ")}`);
1157
+ return artifacts;
769
1158
  }
770
-
771
- // src/ui/banner.ts
772
- import chalk from "chalk";
773
- import figlet from "figlet";
774
- var WIDE_TERMINAL_WIDTH = 88;
775
- var MEDIUM_TERMINAL_WIDTH = 72;
776
- function renderBanner() {
777
- const terminalWidth = process.stdout.columns ?? 120;
778
- const art = figlet.textSync("Easy Coding", selectBannerPreset(terminalWidth));
779
- console.log(colorizeBanner(art));
780
- console.log(chalk.bold.white(` Easy Coding Harness ${chalk.cyan(`v${VERSION}`)}
781
- `));
1159
+ async function installHarnessToDir(targetDir, platforms, ctx) {
1160
+ const artifacts = await configurePlatformsForDir(targetDir, platforms, boundaryFromContext(ctx));
1161
+ await writeRuntimeScaffold(targetDir, platforms, {
1162
+ supermodule: supermoduleConfigFromContext(ctx)
1163
+ });
1164
+ await writeInstallManifest(targetDir, {
1165
+ harnessVersion: VERSION,
1166
+ agents: platforms,
1167
+ artifacts
1168
+ });
1169
+ await writeProjectInitTask(targetDir, platforms, {
1170
+ initSource: ctx.initSource,
1171
+ legacyAssets: ctx.legacyAssets,
1172
+ legacyMissingHarnessFiles: ctx.legacyMissingHarnessFiles
1173
+ });
1174
+ await ensureEasyCodingSessionsIgnored(targetDir);
1175
+ return artifacts;
782
1176
  }
783
- function selectBannerPreset(width) {
784
- if (width >= WIDE_TERMINAL_WIDTH) {
785
- return { font: "ANSI Shadow", horizontalLayout: "full" };
1177
+ function supermoduleConfigFromContext(ctx) {
1178
+ if (ctx.role === "super-parent") {
1179
+ return {
1180
+ role: "super-parent",
1181
+ submodules: ctx.submodulePaths ?? []
1182
+ };
1183
+ }
1184
+ if (ctx.role === "submodule-child") {
1185
+ return {
1186
+ role: "submodule-child",
1187
+ parent: ctx.parent
1188
+ };
1189
+ }
1190
+ return { role: "standalone" };
1191
+ }
1192
+ async function refreshSupermoduleParent(targetDir, platforms, submodulePaths) {
1193
+ const supermodule = {
1194
+ role: "super-parent",
1195
+ submodules: submodulePaths
1196
+ };
1197
+ await updateSupermoduleConfig(path13.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
1198
+ for (const platform of platforms) {
1199
+ await writeMainConstraint(targetDir, platform, {
1200
+ supermodule: { submodulePaths }
1201
+ });
786
1202
  }
787
- if (width >= MEDIUM_TERMINAL_WIDTH) {
788
- return { font: "Small Shadow", horizontalLayout: "full" };
1203
+ }
1204
+ function boundaryFromContext(ctx) {
1205
+ if (ctx.role !== "super-parent") {
1206
+ return void 0;
789
1207
  }
790
- return { font: "Small Slant" };
791
- }
792
- function colorizeBanner(art) {
793
- const colors = [chalk.cyanBright, chalk.cyan, chalk.blueBright, chalk.blue];
794
- return art.split("\n").map((line, index) => colors[index % colors.length](line)).join("\n");
795
- }
796
-
797
- // src/utils/config-yaml.ts
798
- import { readFile as readFile3 } from "fs/promises";
799
- import YAML, { isScalar, isSeq, parseDocument } from "yaml";
800
- function createDefaultConfig(params) {
801
- return {
802
- version: 1,
803
- harness_version: params.harnessVersion,
804
- agents: params.agents,
805
- project: {
806
- name: params.projectName
807
- },
808
- memory: {
809
- short_term_max: 10,
810
- short_term_keep: 5,
811
- schema_version: 2
812
- },
813
- tasks: {
814
- auto_archive_days: 30
815
- },
816
- behavior: {
817
- strict_confirm: true,
818
- auto_mode: false
819
- }
820
- };
821
- }
822
- function stringifyConfig(config) {
823
- return YAML.stringify(config);
824
- }
825
- async function writeConfigYaml(filePath, config) {
826
- await writeTextFile(filePath, stringifyConfig(config));
827
- }
828
- async function readConfigYaml(filePath) {
829
- const content = await readFile3(filePath, "utf8");
830
- return YAML.parse(content);
831
- }
832
- async function updateConfigYaml(filePath, updater) {
833
- const content = await readFile3(filePath, "utf8");
834
- const document = parseDocument(content);
835
- const config = document.toJSON();
836
- updater(config);
837
- for (const [key, value] of Object.entries(config)) {
838
- document.set(key, value);
839
- }
840
- await writeTextFile(filePath, document.toString());
841
- return config;
842
- }
843
- async function addAgentsToConfig(filePath, agents) {
844
- return updateConfigYaml(filePath, (config) => {
845
- const merged = /* @__PURE__ */ new Set([...config.agents ?? [], ...agents]);
846
- config.agents = [...merged];
847
- });
848
- }
849
- async function updateHarnessVersion(filePath, version) {
850
- return updateConfigYaml(filePath, (config) => {
851
- config.harness_version = version;
852
- });
853
- }
854
-
855
- // src/utils/gitignore.ts
856
- import path10 from "path";
857
- async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Personal runtime state; do not commit") {
858
- const gitignorePath = path10.join(cwd, ".gitignore");
859
- const current = await readTextIfExists(gitignorePath) ?? "";
860
- const lines = current.split(/\r?\n/).map((line) => line.trim());
861
- if (lines.includes(entry)) {
862
- return false;
863
- }
864
- const prefix = current.trimEnd();
865
- const next = [prefix, heading, entry].filter(Boolean).join("\n");
866
- await writeTextFile(gitignorePath, next);
867
- return true;
868
- }
869
- async function ensureEasyCodingSessionsIgnored(cwd) {
870
- return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
871
- }
872
-
873
- // src/utils/runtime-scaffold.ts
874
- import path11 from "path";
875
- async function writeRuntimeScaffold(cwd, agents) {
876
- const easyCodingDir = path11.join(cwd, EASY_CODING_DIR);
877
- await ensureDir(easyCodingDir);
878
- const configPath = path11.join(easyCodingDir, CONFIG_FILE);
879
- if (!await pathExists(configPath)) {
880
- const projectName = path11.basename(cwd);
881
- await writeConfigYaml(
882
- configPath,
883
- createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
884
- );
885
- }
886
- await ensureDir(path11.join(easyCodingDir, "tasks"));
887
- await ensureDir(path11.join(easyCodingDir, SESSIONS_DIR));
888
- await ensureDir(path11.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
889
- await ensureDir(path11.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
890
- await writeMemoryScaffold(easyCodingDir);
891
- await writeTemplatesScaffold(easyCodingDir);
892
- }
893
- async function writeTemplatesScaffold(easyCodingDir) {
894
- const templatesDir = path11.join(easyCodingDir, TEMPLATES_DIR);
895
- await ensureDir(templatesDir);
896
- const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
897
- const dest = path11.join(templatesDir, "dev-spec-skeleton.md");
898
- await writeTextFile(dest, await readTextFile(src));
899
- }
900
- async function writeMemoryScaffold(easyCodingDir) {
901
- const memoryDir = path11.join(easyCodingDir, MEMORY_DIR);
902
- await ensureDir(path11.join(memoryDir, "short"));
903
- await ensureDir(path11.join(memoryDir, "long"));
904
- for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
905
- const destination = path11.join(memoryDir, "long", file);
906
- if (await pathExists(destination)) {
907
- continue;
908
- }
909
- const templatePath = getTemplatePath("runtime", "memory", "long", file);
910
- await writeTextFile(destination, await readTextFile(templatePath));
911
- }
912
- const shortTemplateDest = path11.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
913
- if (!await pathExists(shortTemplateDest)) {
914
- const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
915
- await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
916
- }
917
- }
918
-
919
- // src/utils/task-json.ts
920
- import { readdir as readdir3 } from "fs/promises";
921
- import path12 from "path";
922
- function createProjectInitTask(params) {
923
- return {
924
- type: "project-init",
925
- status: "PENDING",
926
- created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
927
- created_by: "cli-init",
928
- last_agent: "cli",
929
- stage_history: [],
930
- context: {
931
- agents_installed: params.agents,
932
- cli_version: VERSION,
933
- project_path: params.cwd,
934
- init_source: params.initSource ?? "fresh",
935
- ...params.legacyAssets ? { legacy_assets: params.legacyAssets } : {},
936
- ...params.legacyMissingHarnessFiles ? { legacy_missing_harness_files: params.legacyMissingHarnessFiles } : {}
937
- },
938
- init_log: []
939
- };
940
- }
941
- function getTaskJsonPath(cwd, taskId) {
942
- return path12.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
943
- }
944
- async function writeTaskJson(filePath, task) {
945
- await writeTextFile(filePath, JSON.stringify(task, null, 2));
946
- }
947
- async function readTaskJson(filePath) {
948
- return JSON.parse(await readTextFile(filePath));
949
- }
950
- async function writeProjectInitTask(cwd, agents, options = {}) {
951
- await writeTaskJson(
952
- getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID),
953
- createProjectInitTask({
954
- cwd,
955
- agents,
956
- initSource: options.initSource,
957
- legacyAssets: options.legacyAssets,
958
- legacyMissingHarnessFiles: options.legacyMissingHarnessFiles
959
- })
960
- );
961
- }
962
- async function setPendingInitSince(cwd, version) {
963
- const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
964
- if (!await pathExists(filePath)) return;
965
- const task = await readTaskJson(filePath);
966
- if (task.status !== "COMPLETE") return;
967
- task.pending_init_since = version;
968
- await writeTaskJson(filePath, task);
969
- }
970
- async function listTasks(cwd) {
971
- const tasksDir = path12.join(cwd, EASY_CODING_DIR, TASKS_DIR);
972
- if (!await pathExists(tasksDir)) {
973
- return [];
974
- }
975
- const entries = await readdir3(tasksDir, { withFileTypes: true });
976
- const tasks = [];
977
- for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
978
- if (!entry.isDirectory()) {
979
- continue;
980
- }
981
- const taskPath = getTaskJsonPath(cwd, entry.name);
982
- if (!await pathExists(taskPath)) {
983
- continue;
984
- }
985
- tasks.push({ id: entry.name, task: await readTaskJson(taskPath) });
986
- }
987
- return tasks;
988
- }
989
- function summarizeTaskStatuses(tasks) {
990
- return tasks.reduce(
991
- (summary, item) => {
992
- summary[item.task.status] = (summary[item.task.status] ?? 0) + 1;
993
- return summary;
994
- },
995
- {}
996
- );
997
- }
998
- function isActiveTask(task) {
999
- return task.status !== "COMPLETE" && task.status !== "CLOSED";
1208
+ return { submodulePaths: ctx.submodulePaths ?? [] };
1000
1209
  }
1001
1210
 
1002
1211
  // src/commands/platforms.ts
@@ -1043,49 +1252,453 @@ async function resolvePlatforms(opts, defaults = ["claude-code"]) {
1043
1252
  cancel("Platform selection cancelled.");
1044
1253
  process.exit(1);
1045
1254
  }
1046
- if (shouldInstall) {
1047
- return result;
1255
+ if (shouldInstall) {
1256
+ return result;
1257
+ }
1258
+ selectedDefaults = result;
1259
+ }
1260
+ }
1261
+ function parseSubmoduleList(submoduleList, available) {
1262
+ const requested = submoduleList.split(",").map((value) => value.trim()).filter(Boolean);
1263
+ if (requested.length === 0) {
1264
+ throw new Error("No submodule specified.");
1265
+ }
1266
+ const byPathOrName = /* @__PURE__ */ new Map();
1267
+ for (const submodule of available) {
1268
+ byPathOrName.set(submodule.path, submodule);
1269
+ byPathOrName.set(submodule.name, submodule);
1270
+ }
1271
+ const selected = [];
1272
+ const invalid = [];
1273
+ for (const value of requested) {
1274
+ const submodule = byPathOrName.get(value);
1275
+ if (!submodule) {
1276
+ invalid.push(value);
1277
+ continue;
1278
+ }
1279
+ if (!selected.some((item) => item.path === submodule.path)) {
1280
+ selected.push(submodule);
1281
+ }
1282
+ }
1283
+ if (invalid.length > 0) {
1284
+ throw new Error(`Unknown or unavailable submodule: ${invalid.join(", ")}`);
1285
+ }
1286
+ return selected.sort((a, b) => a.path.localeCompare(b.path));
1287
+ }
1288
+ async function resolveSubmodules(opts, available, defaultSelection = available) {
1289
+ if (opts.submodules === false) {
1290
+ return [];
1291
+ }
1292
+ if (typeof opts.submodules === "string") {
1293
+ return parseSubmoduleList(opts.submodules, available);
1294
+ }
1295
+ if (available.length === 0) {
1296
+ return [];
1297
+ }
1298
+ if (opts.yes) {
1299
+ return defaultSelection;
1300
+ }
1301
+ const result = await multiselect({
1302
+ message: "Select checked-out submodules to initialize (Space to toggle, Enter to confirm)",
1303
+ options: available.map((submodule) => ({
1304
+ label: `${submodule.path} (${submodule.name})`,
1305
+ value: submodule.path
1306
+ })),
1307
+ initialValues: defaultSelection.map((submodule) => submodule.path),
1308
+ required: false
1309
+ });
1310
+ if (typeof result === "symbol") {
1311
+ cancel("Submodule selection cancelled.");
1312
+ process.exit(1);
1313
+ }
1314
+ const selected = new Set(result);
1315
+ return available.filter((submodule) => selected.has(submodule.path));
1316
+ }
1317
+
1318
+ // src/commands/supermodule-targets.ts
1319
+ import path15 from "path";
1320
+ import { cancel as cancel2, multiselect as multiselect2 } from "@clack/prompts";
1321
+
1322
+ // src/utils/gitmodules.ts
1323
+ import { lstat, realpath } from "fs/promises";
1324
+ import path14 from "path";
1325
+ async function parseGitmodules(rootDir) {
1326
+ const content = await readTextIfExists(path14.join(rootDir, ".gitmodules"));
1327
+ if (content === null) {
1328
+ return [];
1329
+ }
1330
+ const entries = [];
1331
+ let current = null;
1332
+ const flushCurrent = () => {
1333
+ if (current?.name && current.path) {
1334
+ entries.push({
1335
+ name: current.name,
1336
+ path: current.path,
1337
+ url: current.url ?? ""
1338
+ });
1339
+ }
1340
+ };
1341
+ for (const rawLine of content.split(/\r?\n/)) {
1342
+ const line = stripGitConfigInlineComment(rawLine).trim();
1343
+ if (!line || line.startsWith("#") || line.startsWith(";")) {
1344
+ continue;
1345
+ }
1346
+ const section = line.match(/^\[submodule\s+"(.+)"\]$/i);
1347
+ if (section) {
1348
+ flushCurrent();
1349
+ current = { name: section[1] };
1350
+ continue;
1351
+ }
1352
+ if (!current) {
1353
+ continue;
1354
+ }
1355
+ const keyValue = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/);
1356
+ if (!keyValue) {
1357
+ continue;
1358
+ }
1359
+ const key = keyValue[1].toLowerCase();
1360
+ const value = unquote(keyValue[2].trim());
1361
+ if (key === "path") {
1362
+ current.path = normalizeSubmodulePath(value);
1363
+ continue;
1364
+ }
1365
+ if (key === "url") {
1366
+ current.url = value;
1367
+ }
1368
+ }
1369
+ flushCurrent();
1370
+ return entries.sort((a, b) => a.path.localeCompare(b.path));
1371
+ }
1372
+ async function listInstallableSubmodules(rootDir) {
1373
+ const entries = await parseGitmodules(rootDir);
1374
+ const installable = [];
1375
+ for (const entry of entries) {
1376
+ if (await isSubmoduleWorktree(rootDir, entry.path)) {
1377
+ installable.push(entry);
1378
+ }
1379
+ }
1380
+ return installable;
1381
+ }
1382
+ function normalizeSubmodulePath(value) {
1383
+ const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
1384
+ const parts = normalized.split("/").filter(Boolean);
1385
+ if (normalized === "" || path14.posix.isAbsolute(normalized) || path14.win32.isAbsolute(value) || parts.some((part) => part === "..")) {
1386
+ throw new Error(`Unsafe submodule path in .gitmodules: ${value}`);
1387
+ }
1388
+ return parts.join("/");
1389
+ }
1390
+ function unquote(value) {
1391
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1392
+ return value.slice(1, -1);
1393
+ }
1394
+ return value;
1395
+ }
1396
+ function stripGitConfigInlineComment(value) {
1397
+ let inQuotes = false;
1398
+ let escaped = false;
1399
+ for (let index = 0; index < value.length; index += 1) {
1400
+ const char = value[index];
1401
+ if (escaped) {
1402
+ escaped = false;
1403
+ continue;
1404
+ }
1405
+ if (inQuotes && char === "\\") {
1406
+ escaped = true;
1407
+ continue;
1408
+ }
1409
+ if (char === '"') {
1410
+ inQuotes = !inQuotes;
1411
+ continue;
1412
+ }
1413
+ if (!inQuotes && (char === "#" || char === ";") && isCommentStart(value, index)) {
1414
+ return value.slice(0, index);
1415
+ }
1416
+ }
1417
+ return value;
1418
+ }
1419
+ function isCommentStart(value, index) {
1420
+ return index === 0 || /\s/.test(value[index - 1]);
1421
+ }
1422
+ async function isSubmoduleWorktree(rootDir, submodulePath) {
1423
+ const dir = path14.join(rootDir, submodulePath);
1424
+ try {
1425
+ if (!await pathHasNoSymlinkSegments(rootDir, submodulePath)) {
1426
+ return false;
1427
+ }
1428
+ const rootRealPath = await realpath(rootDir);
1429
+ const dirRealPath = await realpath(dir);
1430
+ if (!isInsideDirectory(rootRealPath, dirRealPath)) {
1431
+ return false;
1432
+ }
1433
+ const dirStat = await lstat(dir);
1434
+ if (!dirStat.isDirectory()) {
1435
+ return false;
1436
+ }
1437
+ const gitMarkerStat = await lstat(path14.join(dir, ".git"));
1438
+ return !gitMarkerStat.isSymbolicLink() && (gitMarkerStat.isFile() || gitMarkerStat.isDirectory());
1439
+ } catch (error) {
1440
+ if (error.code === "ENOENT") {
1441
+ return false;
1442
+ }
1443
+ throw error;
1444
+ }
1445
+ }
1446
+ async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
1447
+ let current = rootDir;
1448
+ for (const part of submodulePath.split("/")) {
1449
+ current = path14.join(current, part);
1450
+ const partStat = await lstat(current);
1451
+ if (partStat.isSymbolicLink()) {
1452
+ return false;
1453
+ }
1454
+ }
1455
+ return true;
1456
+ }
1457
+ function isInsideDirectory(parent, child) {
1458
+ const relative = path14.relative(parent, child);
1459
+ return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
1460
+ }
1461
+
1462
+ // src/commands/supermodule-targets.ts
1463
+ function rejectSubmoduleListWithoutGitmodules(opts) {
1464
+ if (typeof opts.submodules === "string") {
1465
+ throw new Error("--submodules can only be used in a repository with .gitmodules.");
1466
+ }
1467
+ }
1468
+ async function resolveAddAgentTargets(cwd, opts) {
1469
+ const installedSubmodules = await listInstalledSubmodules(cwd);
1470
+ if ((await parseGitmodules(cwd)).length === 0) {
1471
+ rejectSubmoduleListWithoutGitmodules(opts);
1472
+ return [standaloneTarget(cwd)];
1473
+ }
1474
+ const selected = await resolveSubmodules(opts, installedSubmodules);
1475
+ const parentManagedSubmodulePaths = await listParentManagedSubmodulePaths(cwd);
1476
+ const parentSubmodulePaths = [
1477
+ .../* @__PURE__ */ new Set([...parentManagedSubmodulePaths, ...selected.map((entry) => entry.path)])
1478
+ ].sort();
1479
+ return [
1480
+ parentTargetFromPaths(cwd, parentSubmodulePaths),
1481
+ ...selected.map((entry) => childTarget(cwd, entry))
1482
+ ];
1483
+ }
1484
+ async function resolveUpgradeTargets(cwd) {
1485
+ const installedSubmodules = await listInstalledSubmodules(cwd);
1486
+ if ((await parseGitmodules(cwd)).length === 0) {
1487
+ return [standaloneTarget(cwd)];
1488
+ }
1489
+ return [
1490
+ parentTarget(cwd, installedSubmodules),
1491
+ ...installedSubmodules.map((entry) => childTarget(cwd, entry))
1492
+ ];
1493
+ }
1494
+ async function resolveInitSubmoduleSelection(cwd, opts) {
1495
+ const installable = await listInstallableSubmodules(cwd);
1496
+ const installed = await listInstalledSubmodules(cwd);
1497
+ const parentManagedSubmodulePaths = await listParentManagedSubmodulePaths(cwd);
1498
+ const defaultSelection = installable;
1499
+ const selected = await resolveSubmodules(opts, installable, defaultSelection);
1500
+ const parentSubmodulePaths = [
1501
+ .../* @__PURE__ */ new Set([...parentManagedSubmodulePaths, ...selected.map((entry) => entry.path)])
1502
+ ].sort();
1503
+ return { installable, installed, selected, parentSubmodulePaths };
1504
+ }
1505
+ async function resolveClearTargets(cwd, opts) {
1506
+ if ((await parseGitmodules(cwd)).length === 0) {
1507
+ rejectSubmoduleListWithoutGitmodules(opts);
1508
+ return [standaloneTarget(cwd)];
1509
+ }
1510
+ const installedSubmodules = await listInstalledSubmodules(cwd);
1511
+ const parent = await pathExists(path15.join(cwd, EASY_CODING_DIR)) ? parentTarget(cwd, installedSubmodules) : null;
1512
+ const children = installedSubmodules.map((entry) => childTarget(cwd, entry));
1513
+ if (opts.submodules === false) {
1514
+ return parent ? [parent] : [];
1515
+ }
1516
+ if (typeof opts.submodules === "string") {
1517
+ return [
1518
+ ...parent ? [parent] : [],
1519
+ ...parseSubmoduleSelection(opts.submodules, installedSubmodules).map(
1520
+ (entry) => childTarget(cwd, entry)
1521
+ )
1522
+ ];
1523
+ }
1524
+ if (opts.yes) {
1525
+ return parent ? [parent] : [];
1526
+ }
1527
+ const candidates = [...parent ? [parent] : [], ...children];
1528
+ if (candidates.length === 0) {
1529
+ return [];
1530
+ }
1531
+ const result = await multiselect2({
1532
+ message: "Select Easy Coding harness targets to clear",
1533
+ options: candidates.map((target) => ({
1534
+ label: target.label === "." ? "parent repository" : `submodule: ${target.label}`,
1535
+ value: target.label
1536
+ })),
1537
+ initialValues: parent ? [parent.label] : [],
1538
+ required: true
1539
+ });
1540
+ if (typeof result === "symbol") {
1541
+ cancel2("Clear target selection cancelled.");
1542
+ process.exit(1);
1543
+ }
1544
+ const selected = new Set(result);
1545
+ return candidates.filter((target) => selected.has(target.label));
1546
+ }
1547
+ async function listInstalledSubmodules(cwd) {
1548
+ const entries = await listInstallableSubmodules(cwd);
1549
+ const installed = [];
1550
+ for (const entry of entries) {
1551
+ if (await pathExists(configPath(path15.join(cwd, entry.path)))) {
1552
+ installed.push(entry);
1553
+ }
1554
+ }
1555
+ return installed;
1556
+ }
1557
+ async function listParentManagedSubmodulePaths(cwd) {
1558
+ const parentConfigPath = configPath(cwd);
1559
+ if (!await pathExists(parentConfigPath)) {
1560
+ return [];
1561
+ }
1562
+ try {
1563
+ const config = await readConfigYaml(parentConfigPath);
1564
+ if (config.supermodule?.role !== "super-parent" || !Array.isArray(config.supermodule.submodules)) {
1565
+ return [];
1566
+ }
1567
+ return config.supermodule.submodules.filter((submodulePath) => typeof submodulePath === "string").sort();
1568
+ } catch {
1569
+ return [];
1570
+ }
1571
+ }
1572
+ function standaloneTarget(cwd) {
1573
+ return {
1574
+ dir: cwd,
1575
+ label: ".",
1576
+ configPath: configPath(cwd),
1577
+ supermodule: { role: "standalone" }
1578
+ };
1579
+ }
1580
+ function parentTarget(cwd, installedSubmodules) {
1581
+ const submodulePaths = installedSubmodules.map((entry) => entry.path);
1582
+ return parentTargetFromPaths(cwd, submodulePaths);
1583
+ }
1584
+ function parentTargetFromPaths(cwd, submodulePaths) {
1585
+ return {
1586
+ dir: cwd,
1587
+ label: ".",
1588
+ configPath: configPath(cwd),
1589
+ supermodule: { role: "super-parent", submodules: submodulePaths },
1590
+ boundary: { submodulePaths }
1591
+ };
1592
+ }
1593
+ function childTarget(cwd, entry) {
1594
+ const dir = path15.join(cwd, entry.path);
1595
+ return {
1596
+ dir,
1597
+ label: entry.path,
1598
+ configPath: configPath(dir),
1599
+ supermodule: { role: "submodule-child", parent: toPosixRelative(dir, cwd) }
1600
+ };
1601
+ }
1602
+ function parseSubmoduleSelection(submoduleList, available) {
1603
+ const requested = submoduleList.split(",").map((value) => value.trim()).filter(Boolean);
1604
+ if (requested.length === 0) {
1605
+ throw new Error("No submodule specified.");
1606
+ }
1607
+ const byPathOrName = /* @__PURE__ */ new Map();
1608
+ for (const submodule of available) {
1609
+ byPathOrName.set(submodule.path, submodule);
1610
+ byPathOrName.set(submodule.name, submodule);
1611
+ }
1612
+ const selected = [];
1613
+ const invalid = [];
1614
+ for (const value of requested) {
1615
+ const submodule = byPathOrName.get(value);
1616
+ if (!submodule) {
1617
+ invalid.push(value);
1618
+ continue;
1619
+ }
1620
+ if (!selected.some((item) => item.path === submodule.path)) {
1621
+ selected.push(submodule);
1048
1622
  }
1049
- selectedDefaults = result;
1050
1623
  }
1624
+ if (invalid.length > 0) {
1625
+ throw new Error(`Unknown or unavailable initialized submodule: ${invalid.join(", ")}`);
1626
+ }
1627
+ return selected.sort((a, b) => a.path.localeCompare(b.path));
1628
+ }
1629
+ function configPath(cwd) {
1630
+ return path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1631
+ }
1632
+ function toPosixRelative(from, to) {
1633
+ const relative = path15.relative(from, to);
1634
+ return relative ? relative.split(path15.sep).join("/") : ".";
1051
1635
  }
1052
1636
 
1053
1637
  // src/commands/add-agent.ts
1054
1638
  async function addAgent(opts) {
1055
1639
  renderBanner();
1056
1640
  const cwd = process.cwd();
1057
- const configPath = path13.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1058
- if (!await pathExists(configPath)) {
1641
+ const targets = await resolveAddAgentTargets(cwd, opts);
1642
+ if (!await pathExists(targets[0].configPath)) {
1059
1643
  throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
1060
1644
  }
1061
- const config = await readConfigYaml(configPath);
1062
1645
  const platforms = await resolvePlatforms(opts, ["claude-code"]);
1063
- const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
1064
- if (toInstall.length === 0) {
1646
+ const installedLabels = [];
1647
+ const refreshedLabels = [];
1648
+ for (const target of targets) {
1649
+ if (!await pathExists(target.configPath)) {
1650
+ continue;
1651
+ }
1652
+ const config = await readConfigYaml(target.configPath);
1653
+ const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
1654
+ const agents = [...config.agents, ...toInstall];
1655
+ if (toInstall.length > 0) {
1656
+ const artifacts = await configurePlatformsForDir(
1657
+ target.dir,
1658
+ toInstall,
1659
+ target.boundary
1660
+ );
1661
+ await writeRuntimeScaffold(target.dir, agents, {
1662
+ supermodule: target.supermodule
1663
+ });
1664
+ await writeInstallManifest(target.dir, {
1665
+ harnessVersion: VERSION,
1666
+ agents: toInstall,
1667
+ artifacts,
1668
+ mode: "merge"
1669
+ });
1670
+ await ensureEasyCodingSessionsIgnored(target.dir);
1671
+ await addAgentsToConfig(target.configPath, toInstall);
1672
+ await setPendingInitSince(target.dir, VERSION);
1673
+ installedLabels.push(`${target.label}: ${toInstall.join(", ")}`);
1674
+ }
1675
+ if (target.supermodule.role === "super-parent") {
1676
+ await refreshSupermoduleParent(target.dir, agents, target.supermodule.submodules ?? []);
1677
+ refreshedLabels.push(target.label);
1678
+ continue;
1679
+ }
1680
+ if (target.supermodule.role === "submodule-child") {
1681
+ await updateSupermoduleConfig(target.configPath, target.supermodule);
1682
+ refreshedLabels.push(target.label);
1683
+ }
1684
+ }
1685
+ if (installedLabels.length === 0) {
1686
+ if (refreshedLabels.length > 0) {
1687
+ outro(chalk2.green(`Supermodule topology refreshed:
1688
+ ${refreshedLabels.join("\n")}`));
1689
+ return;
1690
+ }
1065
1691
  outro(chalk2.yellow("All selected agent platforms are already installed."));
1066
1692
  return;
1067
1693
  }
1068
- const artifacts = [];
1069
- for (const platform of toInstall) {
1070
- artifacts.push(...await CONFIGURATORS[platform](cwd));
1071
- }
1072
- await writeRuntimeScaffold(cwd, [...config.agents, ...toInstall]);
1073
- await writeInstallManifest(cwd, {
1074
- harnessVersion: VERSION,
1075
- agents: toInstall,
1076
- artifacts,
1077
- mode: "merge"
1078
- });
1079
- await ensureEasyCodingSessionsIgnored(cwd);
1080
- await addAgentsToConfig(configPath, toInstall);
1081
- await setPendingInitSince(cwd, VERSION);
1082
- outro(chalk2.green(`Added agent platforms: ${toInstall.join(", ")}`));
1694
+ outro(chalk2.green(`Added agent platforms:
1695
+ ${installedLabels.join("\n")}`));
1083
1696
  }
1084
1697
 
1085
1698
  // src/commands/clear.ts
1086
1699
  import { readdir as readdir4, rm, writeFile } from "fs/promises";
1087
- import path14 from "path";
1088
- import { cancel as cancel2, confirm as confirm2, outro as outro2 } from "@clack/prompts";
1700
+ import path16 from "path";
1701
+ import { cancel as cancel3, confirm as confirm2, outro as outro2 } from "@clack/prompts";
1089
1702
  import chalk3 from "chalk";
1090
1703
  var PLATFORM_TEMPLATE_DIR = {
1091
1704
  "claude-code": "claude",
@@ -1095,13 +1708,12 @@ var PLATFORM_TEMPLATE_DIR = {
1095
1708
  async function clear(opts) {
1096
1709
  renderBanner();
1097
1710
  const cwd = process.cwd();
1098
- const easyCodingDir = path14.join(cwd, EASY_CODING_DIR);
1099
- if (!await pathExists(easyCodingDir)) {
1711
+ const targets = await resolveClearTargets(cwd, opts);
1712
+ const targetPlans = await buildTargetClearPlans(targets);
1713
+ if (targetPlans.length === 0) {
1100
1714
  throw new Error("No .easy-coding directory found in this project \u2014 nothing to clear.");
1101
1715
  }
1102
- const agents = await resolveInstalledAgents(cwd);
1103
- const plan = await buildClearPlan(cwd, agents);
1104
- console.log(renderPlan(cwd, agents, plan));
1716
+ console.log(renderTargetPlans(targetPlans));
1105
1717
  if (opts.dryRun) {
1106
1718
  return;
1107
1719
  }
@@ -1111,18 +1723,54 @@ async function clear(opts) {
1111
1723
  initialValue: false
1112
1724
  });
1113
1725
  if (typeof ok === "symbol" || !ok) {
1114
- cancel2("Clear cancelled.");
1726
+ cancel3("Clear cancelled.");
1115
1727
  return;
1116
1728
  }
1117
1729
  }
1118
- await executeClearPlan(plan);
1730
+ for (const targetPlan of targetPlans) {
1731
+ await executeClearPlan(targetPlan.plan);
1732
+ }
1733
+ await refreshParentAfterChildClear(cwd, targetPlans);
1119
1734
  outro2(chalk3.green("Easy Coding harness removed. Run easy-coding init to reinstall."));
1120
1735
  }
1736
+ async function buildTargetClearPlans(targets) {
1737
+ const plans = [];
1738
+ for (const target of targets) {
1739
+ const easyCodingDir = path16.join(target.dir, EASY_CODING_DIR);
1740
+ if (!await pathExists(easyCodingDir)) {
1741
+ continue;
1742
+ }
1743
+ const agents = await resolveInstalledAgents(target.dir);
1744
+ plans.push({
1745
+ target,
1746
+ agents,
1747
+ plan: await buildClearPlan(target.dir, agents)
1748
+ });
1749
+ }
1750
+ return plans;
1751
+ }
1752
+ async function refreshParentAfterChildClear(cwd, targetPlans) {
1753
+ if (targetPlans.some((targetPlan) => targetPlan.target.label === ".")) {
1754
+ return;
1755
+ }
1756
+ if (!await pathExists(path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
1757
+ return;
1758
+ }
1759
+ const [parent] = await resolveUpgradeTargets(cwd);
1760
+ if (!parent || parent.label !== ".") {
1761
+ return;
1762
+ }
1763
+ const config = await readConfigYaml(parent.configPath);
1764
+ if (!Array.isArray(config.agents) || config.agents.length === 0) {
1765
+ return;
1766
+ }
1767
+ await refreshSupermoduleParent(cwd, config.agents, parent.supermodule.submodules ?? []);
1768
+ }
1121
1769
  async function resolveInstalledAgents(cwd) {
1122
- const configPath = path14.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1123
- if (await pathExists(configPath)) {
1770
+ const configPath2 = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1771
+ if (await pathExists(configPath2)) {
1124
1772
  try {
1125
- const config = await readConfigYaml(configPath);
1773
+ const config = await readConfigYaml(configPath2);
1126
1774
  const listed = Array.isArray(config.agents) ? config.agents.filter(isAgentPlatform) : [];
1127
1775
  if (listed.length > 0) {
1128
1776
  return listed;
@@ -1150,7 +1798,7 @@ async function hasPlatformInstall(cwd, platform) {
1150
1798
  async function hasInstallMarkers(cwd, meta) {
1151
1799
  const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
1152
1800
  for (const marker of markers) {
1153
- if (await pathExists(path14.join(cwd, marker))) {
1801
+ if (await pathExists(path16.join(cwd, marker))) {
1154
1802
  return true;
1155
1803
  }
1156
1804
  }
@@ -1206,7 +1854,7 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
1206
1854
  }
1207
1855
  if (await manifestFileMatches(filePath, file.sha256)) {
1208
1856
  plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
1209
- addEmptyDirChain(plan.emptyDirs, path14.dirname(filePath), cwd);
1857
+ addEmptyDirChain(plan.emptyDirs, path16.dirname(filePath), cwd);
1210
1858
  } else {
1211
1859
  plan.skippedModified.push(filePath);
1212
1860
  }
@@ -1215,11 +1863,12 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
1215
1863
  if (!agentSet.has(registration.platform)) {
1216
1864
  continue;
1217
1865
  }
1866
+ const managedHookPaths = registration.hook_path ? managedHookPathsForRegistration(cwd, registration.hook_path) : [];
1218
1867
  addHookConfigPrune(
1219
1868
  plan.pruneHookConfigs,
1220
1869
  plan.pruneHookCommands,
1221
1870
  manifestPath(cwd, registration.config_path),
1222
- registration.hook_path ? [registration.hook_path] : [],
1871
+ managedHookPaths,
1223
1872
  [registration.command]
1224
1873
  );
1225
1874
  }
@@ -1255,28 +1904,28 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
1255
1904
  );
1256
1905
  for (const meta of metas) {
1257
1906
  for (const name of managedSkills) {
1258
- plan.remove.add(path14.join(cwd, meta.skillsDir, name));
1907
+ plan.remove.add(path16.join(cwd, meta.skillsDir, name));
1259
1908
  }
1260
1909
  for (const name of hookFileNames) {
1261
- plan.remove.add(path14.join(cwd, meta.hooksDir, name));
1910
+ plan.remove.add(path16.join(cwd, meta.hooksDir, name));
1262
1911
  }
1263
1912
  for (const name of agentFileNames) {
1264
- plan.remove.add(path14.join(cwd, meta.agentsDir, name));
1913
+ plan.remove.add(path16.join(cwd, meta.agentsDir, name));
1265
1914
  }
1266
1915
  addHookConfigPrune(
1267
1916
  plan.pruneHookConfigs,
1268
1917
  plan.pruneHookCommands,
1269
- path14.join(cwd, meta.hookConfigFile),
1270
- hookFileNames.map((name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`),
1918
+ path16.join(cwd, meta.hookConfigFile),
1919
+ managedHookPathsForTemplate(cwd, meta, hookFileNames),
1271
1920
  []
1272
1921
  );
1273
- plan.constraints.add(path14.join(cwd, meta.mainConstraint));
1922
+ plan.constraints.add(path16.join(cwd, meta.mainConstraint));
1274
1923
  if (platform === "codex") {
1275
- plan.remove.add(path14.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
1924
+ plan.remove.add(path16.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
1276
1925
  }
1277
- plan.emptyDirs.add(path14.join(cwd, meta.skillsDir));
1278
- plan.emptyDirs.add(path14.join(cwd, meta.hooksDir));
1279
- plan.emptyDirs.add(path14.join(cwd, meta.agentsDir));
1926
+ plan.emptyDirs.add(path16.join(cwd, meta.skillsDir));
1927
+ plan.emptyDirs.add(path16.join(cwd, meta.hooksDir));
1928
+ plan.emptyDirs.add(path16.join(cwd, meta.agentsDir));
1280
1929
  }
1281
1930
  }
1282
1931
  async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
@@ -1318,6 +1967,51 @@ function addHookConfigPrune(pruneHookConfigs, pruneHookCommands, filePath, manag
1318
1967
  }
1319
1968
  pruneHookCommands.set(filePath, existingCommands);
1320
1969
  }
1970
+ function managedHookPathsForRegistration(cwd, hookPath) {
1971
+ const paths = [hookPath];
1972
+ try {
1973
+ paths.push(...managedHookPathTokens(manifestPath(cwd, hookPath)));
1974
+ } catch {
1975
+ if (path16.isAbsolute(hookPath)) {
1976
+ paths.push(...managedHookPathTokens(hookPath));
1977
+ }
1978
+ }
1979
+ return paths;
1980
+ }
1981
+ function managedHookPathsForTemplate(cwd, meta, hookFileNames) {
1982
+ const relativePaths = hookFileNames.map(
1983
+ (name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`
1984
+ );
1985
+ const absolutePaths = hookFileNames.flatMap(
1986
+ (name) => managedHookPathTokens(path16.join(cwd, meta.hooksDir, name))
1987
+ );
1988
+ return [...relativePaths, ...absolutePaths];
1989
+ }
1990
+ function managedHookPathTokens(filePath) {
1991
+ const tokens = /* @__PURE__ */ new Set();
1992
+ for (const equivalentPath of equivalentAbsolutePaths(filePath)) {
1993
+ const normalized = equivalentPath.replace(/\\/g, "/");
1994
+ const dir = path16.posix.dirname(normalized);
1995
+ const basename = path16.posix.basename(normalized);
1996
+ tokens.add(normalized);
1997
+ tokens.add(shellDoubleQuoteArg2(normalized));
1998
+ tokens.add(`${shellDoubleQuoteArg2(dir)}/${basename}`);
1999
+ }
2000
+ return [...tokens];
2001
+ }
2002
+ function equivalentAbsolutePaths(filePath) {
2003
+ const normalized = path16.resolve(filePath).replace(/\\/g, "/");
2004
+ const equivalents = [normalized];
2005
+ if (normalized.startsWith("/private/var/")) {
2006
+ equivalents.push(normalized.replace(/^\/private\/var\//, "/var/"));
2007
+ } else if (normalized.startsWith("/var/")) {
2008
+ equivalents.push(normalized.replace(/^\/var\//, "/private/var/"));
2009
+ }
2010
+ return equivalents;
2011
+ }
2012
+ function shellDoubleQuoteArg2(value) {
2013
+ return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
2014
+ }
1321
2015
  function createClearPlan() {
1322
2016
  return {
1323
2017
  remove: /* @__PURE__ */ new Set(),
@@ -1380,27 +2074,27 @@ function addRuntimeClearEntries(plan, cwd) {
1380
2074
  plan.remove = [
1381
2075
  .../* @__PURE__ */ new Set([
1382
2076
  ...plan.remove,
1383
- path14.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
1384
- path14.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
1385
- path14.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
1386
- path14.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
2077
+ path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
2078
+ path16.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
2079
+ path16.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
2080
+ path16.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
1387
2081
  ])
1388
2082
  ];
1389
2083
  }
1390
2084
  function addEmptyDirChain(emptyDirs, startDir, cwd) {
1391
2085
  let current = startDir;
1392
- while (current !== cwd && isInsideDirectory(cwd, current)) {
2086
+ while (current !== cwd && isInsideDirectory2(cwd, current)) {
1393
2087
  emptyDirs.add(current);
1394
- const parent = path14.dirname(current);
2088
+ const parent = path16.dirname(current);
1395
2089
  if (parent === current) {
1396
2090
  break;
1397
2091
  }
1398
2092
  current = parent;
1399
2093
  }
1400
2094
  }
1401
- function isInsideDirectory(parent, child) {
1402
- const relative = path14.relative(parent, child);
1403
- return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
2095
+ function isInsideDirectory2(parent, child) {
2096
+ const relative = path16.relative(parent, child);
2097
+ return Boolean(relative) && !relative.startsWith("..") && !path16.isAbsolute(relative);
1404
2098
  }
1405
2099
  async function listSharedHookNamesForPlatform(platform) {
1406
2100
  const names = await listFileNames(getTemplatePath("shared-hooks"));
@@ -1502,7 +2196,7 @@ function isManagedHook(hook, managedHookPaths, managedHookCommands) {
1502
2196
  if (normalizedManagedCommands.includes(normalizeCommand(command))) {
1503
2197
  return true;
1504
2198
  }
1505
- const normalizedCommand = command.replace(/\\/g, "/");
2199
+ const normalizedCommand = command;
1506
2200
  return managedHookPaths.some(
1507
2201
  (hookPath) => commandContainsPathToken(normalizedCommand, hookPath.replace(/\\/g, "/"))
1508
2202
  );
@@ -1537,7 +2231,7 @@ function sortDirsDeepestFirst(dirs) {
1537
2231
  });
1538
2232
  }
1539
2233
  function pathDepth(dir) {
1540
- return path14.normalize(dir).split(path14.sep).filter(Boolean).length;
2234
+ return path16.normalize(dir).split(path16.sep).filter(Boolean).length;
1541
2235
  }
1542
2236
  async function listDirNames(dir) {
1543
2237
  try {
@@ -1556,7 +2250,7 @@ async function listFileNames(dir) {
1556
2250
  }
1557
2251
  }
1558
2252
  function renderPlan(cwd, agents, plan) {
1559
- const rel = (target) => path14.relative(cwd, target) || target;
2253
+ const rel = (target) => path16.relative(cwd, target) || target;
1560
2254
  const lines = [];
1561
2255
  lines.push(chalk3.bold("easy-coding clear"));
1562
2256
  lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
@@ -1594,23 +2288,33 @@ function renderPlan(cwd, agents, plan) {
1594
2288
  );
1595
2289
  return lines.join("\n");
1596
2290
  }
2291
+ function renderTargetPlans(targetPlans) {
2292
+ if (targetPlans.length === 1) {
2293
+ const [{ target, agents, plan }] = targetPlans;
2294
+ return [`Target: ${target.label}`, renderPlan(target.dir, agents, plan)].join("\n");
2295
+ }
2296
+ return targetPlans.map(
2297
+ ({ target, agents, plan }) => [`Target: ${target.label}`, renderPlan(target.dir, agents, plan)].join("\n")
2298
+ ).join("\n\n");
2299
+ }
1597
2300
 
1598
2301
  // src/commands/init.ts
2302
+ import path18 from "path";
1599
2303
  import { note, outro as outro3 } from "@clack/prompts";
1600
2304
  import chalk4 from "chalk";
1601
2305
 
1602
2306
  // src/utils/install-state.ts
1603
2307
  import { readdir as readdir5 } from "fs/promises";
1604
- import path15 from "path";
2308
+ import path17 from "path";
1605
2309
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
1606
2310
  async function detectEasyCodingInstallState(cwd) {
1607
- const easyCodingDir = path15.join(cwd, EASY_CODING_DIR);
2311
+ const easyCodingDir = path17.join(cwd, EASY_CODING_DIR);
1608
2312
  if (!await pathExists(easyCodingDir)) {
1609
2313
  return { kind: "fresh", easyCodingDir };
1610
2314
  }
1611
- const configPath = path15.join(easyCodingDir, CONFIG_FILE);
1612
- if (await pathExists(configPath)) {
1613
- return { kind: "installed", easyCodingDir, configPath };
2315
+ const configPath2 = path17.join(easyCodingDir, CONFIG_FILE);
2316
+ if (await pathExists(configPath2)) {
2317
+ return { kind: "installed", easyCodingDir, configPath: configPath2 };
1614
2318
  }
1615
2319
  const legacyAssets = await detectLegacyAssets(easyCodingDir);
1616
2320
  if (legacyAssets.length === 0) {
@@ -1626,17 +2330,17 @@ async function detectEasyCodingInstallState(cwd) {
1626
2330
  async function detectLegacyAssets(easyCodingDir) {
1627
2331
  const assets = [];
1628
2332
  for (const file of LEGACY_ROOT_FILES) {
1629
- if (await pathExists(path15.join(easyCodingDir, file))) {
2333
+ if (await pathExists(path17.join(easyCodingDir, file))) {
1630
2334
  assets.push(relativeEasyCodingPath(file));
1631
2335
  }
1632
2336
  }
1633
- if (await pathExists(path15.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
2337
+ if (await pathExists(path17.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
1634
2338
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
1635
2339
  }
1636
- const shortMemoryFiles = await listMarkdownFiles(path15.join(easyCodingDir, "memory", "short"));
2340
+ const shortMemoryFiles = await listMarkdownFiles(path17.join(easyCodingDir, "memory", "short"));
1637
2341
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
1638
2342
  for (const dir of ["spec", "prototype"]) {
1639
- if (await hasAnyDirectoryEntry(path15.join(easyCodingDir, dir))) {
2343
+ if (await hasAnyDirectoryEntry(path17.join(easyCodingDir, dir))) {
1640
2344
  assets.push(relativeEasyCodingPath(dir));
1641
2345
  }
1642
2346
  }
@@ -1656,19 +2360,54 @@ async function hasAnyDirectoryEntry(dir) {
1656
2360
  return (await readdir5(dir)).length > 0;
1657
2361
  }
1658
2362
  function relativeEasyCodingPath(...segments) {
1659
- return path15.posix.join(EASY_CODING_DIR, ...segments);
2363
+ return path17.posix.join(EASY_CODING_DIR, ...segments);
1660
2364
  }
1661
2365
  function relativeConfigPath() {
1662
- return path15.posix.join(EASY_CODING_DIR, CONFIG_FILE);
2366
+ return path17.posix.join(EASY_CODING_DIR, CONFIG_FILE);
1663
2367
  }
1664
2368
  function relativeProjectInitTaskPath() {
1665
- return path15.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
2369
+ return path17.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
1666
2370
  }
1667
2371
 
1668
2372
  // src/commands/init.ts
1669
2373
  async function init(opts) {
1670
2374
  renderBanner();
1671
2375
  const cwd = process.cwd();
2376
+ const submodules = await parseGitmodules(cwd);
2377
+ if (submodules.length === 0) {
2378
+ rejectSubmoduleListWithoutGitmodules(opts);
2379
+ }
2380
+ const targets = submodules.length === 0 ? [await standaloneTarget2(cwd)] : await supermoduleTargets(cwd, opts, submodules);
2381
+ const installableTargets = targets.filter((target) => !target.installed);
2382
+ const parentTarget2 = targets.find((target) => target.label === ".");
2383
+ if (installableTargets.length === 0) {
2384
+ await refreshInstalledChildTopologies(targets);
2385
+ if (submodules.length > 0 && parentTarget2) {
2386
+ const platforms2 = await resolveInitPlatforms(cwd, opts, true);
2387
+ await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms2);
2388
+ }
2389
+ outro3(chalk4.yellow("All selected easy-coding harness targets are already installed."));
2390
+ return;
2391
+ }
2392
+ const platforms = await resolveInitPlatforms(cwd, opts, Boolean(parentTarget2?.installed));
2393
+ for (const target of installableTargets) {
2394
+ await installHarnessToDir(target.dir, platforms, target.context);
2395
+ }
2396
+ await refreshInstalledChildTopologies(targets);
2397
+ if (submodules.length > 0 && parentTarget2) {
2398
+ await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms);
2399
+ }
2400
+ const triggers = platforms.map(
2401
+ (platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
2402
+ ).join("\n");
2403
+ note(triggers, "Next step");
2404
+ outro3(
2405
+ chalk4.green(
2406
+ `easy-coding harness installed in ${installableTargets.map((target) => target.label).join(", ")}. Open your agent and run ec-init.`
2407
+ )
2408
+ );
2409
+ }
2410
+ async function standaloneTarget2(cwd) {
1672
2411
  const installState = await detectEasyCodingInstallState(cwd);
1673
2412
  if (installState.kind === "installed") {
1674
2413
  throw new Error(
@@ -1680,36 +2419,121 @@ async function init(opts) {
1680
2419
  ".easy-coding exists but is not recognized as an easy-coding harness or legacy easy-coding skill project. Please inspect it manually before running init."
1681
2420
  );
1682
2421
  }
1683
- const platforms = await resolvePlatforms(opts, ["claude-code"]);
1684
- const artifacts = [];
1685
- for (const platform of platforms) {
1686
- artifacts.push(...await CONFIGURATORS[platform](cwd));
2422
+ return {
2423
+ dir: cwd,
2424
+ label: ".",
2425
+ context: contextFromState("standalone", installState),
2426
+ installed: false
2427
+ };
2428
+ }
2429
+ async function supermoduleTargets(cwd, opts, submodules) {
2430
+ const { installable, parentSubmodulePaths } = await resolveInitSubmoduleSelection(cwd, opts);
2431
+ const skipped = submodules.filter(
2432
+ (entry) => !installable.some((installableEntry) => installableEntry.path === entry.path)
2433
+ );
2434
+ if (skipped.length > 0) {
2435
+ note(
2436
+ skipped.map((entry) => `${entry.path} (${entry.name})`).join("\n"),
2437
+ "Skipped unchecked-out submodules"
2438
+ );
1687
2439
  }
1688
- await writeRuntimeScaffold(cwd, platforms);
1689
- await writeInstallManifest(cwd, {
1690
- harnessVersion: VERSION,
1691
- agents: platforms,
1692
- artifacts
1693
- });
1694
- await writeProjectInitTask(cwd, platforms, {
2440
+ const targets = [
2441
+ await targetFromState(cwd, ".", "super-parent", {
2442
+ submodulePaths: parentSubmodulePaths
2443
+ })
2444
+ ];
2445
+ const targetSubmodulePaths = opts.submodules === false ? /* @__PURE__ */ new Set() : new Set(parentSubmodulePaths);
2446
+ for (const entry of installable) {
2447
+ if (!targetSubmodulePaths.has(entry.path)) {
2448
+ continue;
2449
+ }
2450
+ const dir = path18.join(cwd, entry.path);
2451
+ targets.push(
2452
+ await targetFromState(dir, entry.path, "submodule-child", {
2453
+ parent: toPosixRelative2(dir, cwd)
2454
+ })
2455
+ );
2456
+ }
2457
+ return targets;
2458
+ }
2459
+ async function targetFromState(targetDir, label, role, extraContext = {}) {
2460
+ const installState = await detectEasyCodingInstallState(targetDir);
2461
+ if (installState.kind === "unknown") {
2462
+ throw new Error(
2463
+ `${targetDir}/.easy-coding exists but is not recognized as an easy-coding harness or legacy easy-coding skill project. Please inspect it manually before running init.`
2464
+ );
2465
+ }
2466
+ if (installState.kind === "installed") {
2467
+ return {
2468
+ dir: targetDir,
2469
+ label,
2470
+ installed: true,
2471
+ context: {
2472
+ role,
2473
+ initSource: "fresh",
2474
+ ...extraContext
2475
+ }
2476
+ };
2477
+ }
2478
+ return {
2479
+ dir: targetDir,
2480
+ label,
2481
+ installed: false,
2482
+ context: {
2483
+ ...contextFromState(role, installState),
2484
+ ...extraContext
2485
+ }
2486
+ };
2487
+ }
2488
+ function contextFromState(role, installState) {
2489
+ return {
2490
+ role,
1695
2491
  initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
1696
2492
  legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
1697
2493
  legacyMissingHarnessFiles: installState.kind === "legacy" ? installState.missingHarnessFiles : void 0
1698
- });
1699
- await ensureEasyCodingSessionsIgnored(cwd);
1700
- const triggers = platforms.map(
1701
- (platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
1702
- ).join("\n");
1703
- note(triggers, "Next step");
1704
- outro3(chalk4.green("easy-coding harness installed. Open your agent and run ec-init."));
2494
+ };
2495
+ }
2496
+ function toPosixRelative2(from, to) {
2497
+ const relative = path18.relative(from, to);
2498
+ return relative ? relative.split(path18.sep).join("/") : ".";
2499
+ }
2500
+ async function resolveInitPlatforms(cwd, opts, parentInstalled) {
2501
+ if (opts.agent || !parentInstalled) {
2502
+ return resolvePlatforms(opts, ["claude-code"]);
2503
+ }
2504
+ const config = await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
2505
+ if (Array.isArray(config.agents) && config.agents.length > 0) {
2506
+ return config.agents;
2507
+ }
2508
+ return resolvePlatforms(opts, ["claude-code"]);
2509
+ }
2510
+ async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
2511
+ if (!await pathExists(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
2512
+ return;
2513
+ }
2514
+ const config = parentTarget2.installed ? await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
2515
+ const platforms = Array.isArray(config.agents) && config.agents.length > 0 ? config.agents : installPlatforms;
2516
+ await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
2517
+ }
2518
+ async function refreshInstalledChildTopologies(targets) {
2519
+ for (const target of targets) {
2520
+ if (!target.installed || target.context.role !== "submodule-child") {
2521
+ continue;
2522
+ }
2523
+ const configPath2 = path18.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
2524
+ if (!await pathExists(configPath2)) {
2525
+ continue;
2526
+ }
2527
+ await updateSupermoduleConfig(configPath2, supermoduleConfigFromContext(target.context));
2528
+ }
1705
2529
  }
1706
2530
 
1707
2531
  // src/commands/status.ts
1708
- import path18 from "path";
2532
+ import path21 from "path";
1709
2533
  import chalk5 from "chalk";
1710
2534
 
1711
2535
  // src/utils/compare-versions.ts
1712
- import path16 from "path";
2536
+ import path19 from "path";
1713
2537
  function normalize(version) {
1714
2538
  const [core] = String(version ?? "").split("-");
1715
2539
  return core.split(".").map((part) => {
@@ -1733,13 +2557,13 @@ function isVersionBehind(installed, current = VERSION) {
1733
2557
  return compareVersions(installed, current) === -1;
1734
2558
  }
1735
2559
  async function checkForUpgrade(cwd) {
1736
- const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1737
- if (!await pathExists(configPath)) {
2560
+ const configPath2 = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2561
+ if (!await pathExists(configPath2)) {
1738
2562
  return;
1739
2563
  }
1740
2564
  let config;
1741
2565
  try {
1742
- config = await readConfigYaml(configPath);
2566
+ config = await readConfigYaml(configPath2);
1743
2567
  } catch {
1744
2568
  return;
1745
2569
  }
@@ -1754,14 +2578,14 @@ async function checkForUpgrade(cwd) {
1754
2578
 
1755
2579
  // src/utils/session.ts
1756
2580
  import { readdir as readdir6 } from "fs/promises";
1757
- import path17 from "path";
2581
+ import path20 from "path";
1758
2582
  var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
1759
2583
  function getSessionDir(cwd) {
1760
- return path17.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
2584
+ return path20.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
1761
2585
  }
1762
2586
  function getSessionFilePath(cwd, ppid) {
1763
2587
  const pid = ppid ?? process.ppid;
1764
- return path17.join(getSessionDir(cwd), `${pid}.json`);
2588
+ return path20.join(getSessionDir(cwd), `${pid}.json`);
1765
2589
  }
1766
2590
  async function readSessionFile(cwd, ppid) {
1767
2591
  const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
@@ -1775,11 +2599,11 @@ async function readSessionFile(cwd, ppid) {
1775
2599
  async function status() {
1776
2600
  renderBanner();
1777
2601
  const cwd = process.cwd();
1778
- const configPath = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1779
- if (!await pathExists(configPath)) {
2602
+ const configPath2 = path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2603
+ if (!await pathExists(configPath2)) {
1780
2604
  throw new Error("No easy-coding harness found in this project.");
1781
2605
  }
1782
- const config = await readConfigYaml(configPath);
2606
+ const config = await readConfigYaml(configPath2);
1783
2607
  const tasks = await listTasks(cwd);
1784
2608
  const taskCounts = summarizeTaskStatuses(tasks);
1785
2609
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
@@ -1829,7 +2653,7 @@ async function status() {
1829
2653
 
1830
2654
  // src/commands/update.ts
1831
2655
  import { execFileSync } from "child_process";
1832
- import { cancel as cancel3, confirm as confirm3, outro as outro4 } from "@clack/prompts";
2656
+ import { cancel as cancel4, confirm as confirm3, outro as outro4 } from "@clack/prompts";
1833
2657
  import chalk6 from "chalk";
1834
2658
  async function update(opts) {
1835
2659
  renderBanner();
@@ -1848,7 +2672,7 @@ async function update(opts) {
1848
2672
  initialValue: true
1849
2673
  });
1850
2674
  if (typeof confirmed === "symbol" || !confirmed) {
1851
- cancel3("Update cancelled.");
2675
+ cancel4("Update cancelled.");
1852
2676
  return;
1853
2677
  }
1854
2678
  }
@@ -1858,37 +2682,62 @@ async function update(opts) {
1858
2682
  }
1859
2683
 
1860
2684
  // src/commands/upgrade.ts
1861
- import path19 from "path";
1862
- import { cancel as cancel4, confirm as confirm4, outro as outro5 } from "@clack/prompts";
2685
+ import path22 from "path";
2686
+ import { cancel as cancel5, confirm as confirm4, outro as outro5 } from "@clack/prompts";
1863
2687
  import chalk7 from "chalk";
2688
+ var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
2689
+ "claude-code": [
2690
+ { event: "SessionStart", scriptName: "session-start.py" },
2691
+ { event: "UserPromptSubmit", scriptName: "session-start.py" },
2692
+ { event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" },
2693
+ { event: "PreToolUse", scriptName: "inject-subagent-context.py" }
2694
+ ],
2695
+ codex: [
2696
+ { event: "UserPromptSubmit", scriptName: "session-start.py" },
2697
+ { event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" }
2698
+ ],
2699
+ qoder: [
2700
+ { event: "UserPromptSubmit", scriptName: "session-start.py" },
2701
+ { event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" },
2702
+ { event: "PreToolUse", scriptName: "inject-subagent-context.py" }
2703
+ ]
2704
+ };
2705
+ var MANAGED_HOOK_SCRIPT_NAMES = {
2706
+ "claude-code": [
2707
+ ...new Set(
2708
+ EXPECTED_HOOK_REGISTRATION_SCRIPTS["claude-code"].map(({ scriptName }) => scriptName)
2709
+ )
2710
+ ],
2711
+ codex: [...new Set(EXPECTED_HOOK_REGISTRATION_SCRIPTS.codex.map(({ scriptName }) => scriptName))],
2712
+ qoder: [...new Set(EXPECTED_HOOK_REGISTRATION_SCRIPTS.qoder.map(({ scriptName }) => scriptName))]
2713
+ };
1864
2714
  async function upgrade(opts) {
1865
2715
  renderBanner();
1866
2716
  const cwd = process.cwd();
1867
- const configPath = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1868
- if (!await pathExists(configPath)) {
2717
+ const targets = await resolveUpgradeTargets(cwd);
2718
+ if (!await pathExists(targets[0].configPath)) {
1869
2719
  throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
1870
2720
  }
1871
- const config = await readConfigYaml(configPath);
1872
- const installedVersion = String(config.harness_version ?? "");
1873
- const hasAgents = Array.isArray(config.agents) && config.agents.length > 0;
1874
- if (!installedVersion || !hasAgents) {
1875
- throw new Error(
1876
- "config.yaml is missing required harness fields (harness_version / agents). This project predates the current config layout. Run `easy-coding clear` then `easy-coding init` to migrate \u2014 your tasks, spec, and memory are preserved."
1877
- );
1878
- }
1879
- const relation = compareVersions(installedVersion, VERSION);
1880
- if (relation === 0) {
2721
+ const pending = await resolvePendingUpgradeTargets(targets);
2722
+ const parentTopologyRefresh = await resolveParentTopologyRefresh(targets, pending);
2723
+ const childTopologyRefreshes = await resolveChildTopologyRefreshes(targets, pending);
2724
+ if (pending.length === 0 && !parentTopologyRefresh && childTopologyRefreshes.length === 0) {
1881
2725
  outro5(chalk7.green(`easy-coding harness is already up to date (${VERSION}).`));
1882
2726
  return;
1883
2727
  }
1884
- if (relation === 1) {
1885
- throw new Error(
1886
- `Project harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
1887
- );
1888
- }
1889
2728
  const summary = [
1890
- `Upgrade: ${installedVersion || "unknown"} -> ${VERSION}`,
1891
- `Installed agents: ${config.agents.join(", ")}`,
2729
+ "Upgrade targets:",
2730
+ ...pending.length > 0 ? pending.map(
2731
+ ({ target, config }) => `- ${target.label}: ${String(config.harness_version ?? "unknown")} -> ${VERSION}; agents: ${config.agents.join(", ")}`
2732
+ ) : ["- (none)"],
2733
+ ...parentTopologyRefresh ? [
2734
+ "Supermodule topology refresh:",
2735
+ `- ${parentTopologyRefresh.target.label}: submodules: ${parentTopologyRefresh.submodulePaths.length > 0 ? parentTopologyRefresh.submodulePaths.join(", ") : "(none)"}`
2736
+ ] : [],
2737
+ ...childTopologyRefreshes.length > 0 ? [
2738
+ parentTopologyRefresh ? "" : "Supermodule topology refresh:",
2739
+ ...childTopologyRefreshes.map(({ target }) => `- ${target.label}: role: submodule-child`)
2740
+ ].filter(Boolean) : [],
1892
2741
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
1893
2742
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
1894
2743
  "Will not touch memory, state, spec, or project knowledge files."
@@ -1903,24 +2752,256 @@ async function upgrade(opts) {
1903
2752
  initialValue: true
1904
2753
  });
1905
2754
  if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
1906
- cancel4("Upgrade cancelled.");
2755
+ cancel5("Upgrade cancelled.");
1907
2756
  return;
1908
2757
  }
1909
2758
  }
1910
- const artifacts = [];
1911
- for (const platform of config.agents) {
1912
- artifacts.push(...await CONFIGURATORS[platform](cwd));
2759
+ for (const { target, config } of pending) {
2760
+ const artifacts = await configurePlatformsForDir(
2761
+ target.dir,
2762
+ config.agents,
2763
+ target.boundary
2764
+ );
2765
+ await writeRuntimeScaffold(target.dir, config.agents, {
2766
+ supermodule: target.supermodule
2767
+ });
2768
+ await writeInstallManifest(target.dir, {
2769
+ harnessVersion: VERSION,
2770
+ agents: config.agents,
2771
+ artifacts
2772
+ });
2773
+ await ensureEasyCodingSessionsIgnored(target.dir);
2774
+ await updateHarnessVersion(target.configPath, VERSION);
2775
+ await updateSupermoduleConfig(target.configPath, target.supermodule);
2776
+ await setPendingInitSince(target.dir, VERSION);
2777
+ }
2778
+ if (parentTopologyRefresh) {
2779
+ await refreshSupermoduleParent(
2780
+ parentTopologyRefresh.target.dir,
2781
+ parentTopologyRefresh.agents,
2782
+ parentTopologyRefresh.submodulePaths
2783
+ );
1913
2784
  }
1914
- await writeRuntimeScaffold(cwd, config.agents);
1915
- await writeInstallManifest(cwd, {
1916
- harnessVersion: VERSION,
1917
- agents: config.agents,
1918
- artifacts
2785
+ for (const { target } of childTopologyRefreshes) {
2786
+ await updateSupermoduleConfig(target.configPath, target.supermodule);
2787
+ }
2788
+ outro5(
2789
+ chalk7.green(
2790
+ pending.length > 0 ? `easy-coding harness upgraded to ${VERSION}.` : "easy-coding supermodule topology refreshed."
2791
+ )
2792
+ );
2793
+ }
2794
+ async function resolvePendingUpgradeTargets(targets) {
2795
+ const pending = [];
2796
+ for (const target of targets) {
2797
+ if (!await pathExists(target.configPath)) {
2798
+ continue;
2799
+ }
2800
+ const config = await readConfigYaml(target.configPath);
2801
+ const installedVersion = String(config.harness_version ?? "");
2802
+ const hasAgents = Array.isArray(config.agents) && config.agents.length > 0;
2803
+ if (!installedVersion || !hasAgents) {
2804
+ throw new Error(
2805
+ `${target.label} config.yaml is missing required harness fields (harness_version / agents). This project predates the current config layout. Run \`easy-coding clear\` then \`easy-coding init\` to migrate \u2014 your tasks, spec, and memory are preserved.`
2806
+ );
2807
+ }
2808
+ const relation = compareVersions(installedVersion, VERSION);
2809
+ if (relation === 1) {
2810
+ throw new Error(
2811
+ `${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
2812
+ );
2813
+ }
2814
+ if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config.agents)) {
2815
+ pending.push({ target, config });
2816
+ }
2817
+ }
2818
+ return pending;
2819
+ }
2820
+ async function needsHookConfigRefresh(target, agents) {
2821
+ const manifest = await readInstallManifest(target.dir);
2822
+ for (const agent of agents) {
2823
+ const meta = resolvePlatformMeta(target.dir, agent);
2824
+ const configPath2 = path22.join(target.dir, meta.hookConfigFile);
2825
+ const content = await readTextIfExists(configPath2);
2826
+ if (content === null) {
2827
+ return true;
2828
+ }
2829
+ let parsed;
2830
+ try {
2831
+ parsed = JSON.parse(content);
2832
+ } catch {
2833
+ return true;
2834
+ }
2835
+ const commandsByEvent = collectHookCommandsByEvent(parsed.hooks);
2836
+ const commands = [...commandsByEvent.values()].flat();
2837
+ const expectedRegistrations = expectedHookRegistrations(target.dir, meta, agent);
2838
+ const expectedCommandSet = new Set(
2839
+ expectedRegistrations.map((registration) => registration.command)
2840
+ );
2841
+ if (!hasExpectedHookRegistrations(commandsByEvent, expectedRegistrations)) {
2842
+ return true;
2843
+ }
2844
+ const manifestManagedCommands = new Set(
2845
+ manifest?.hook_registrations.filter((registration) => registration.platform === agent).map((registration) => registration.command) ?? []
2846
+ );
2847
+ if (commands.some(
2848
+ (command) => isUnexpectedManagedHookCommand(
2849
+ command,
2850
+ expectedCommandSet,
2851
+ target.dir,
2852
+ meta,
2853
+ agent,
2854
+ manifestManagedCommands
2855
+ )
2856
+ )) {
2857
+ return true;
2858
+ }
2859
+ }
2860
+ return false;
2861
+ }
2862
+ function expectedHookRegistrations(cwd, meta, platform) {
2863
+ return EXPECTED_HOOK_REGISTRATION_SCRIPTS[platform].map(({ event, scriptName }) => ({
2864
+ event,
2865
+ command: renderHookCommand(cwd, meta.templateContext, scriptName)
2866
+ }));
2867
+ }
2868
+ function hasExpectedHookRegistrations(commandsByEvent, expectedRegistrations) {
2869
+ const actualCounts = countRegistrations(
2870
+ [...commandsByEvent.entries()].flatMap(
2871
+ ([event, commands]) => commands.map((command) => ({ event, command }))
2872
+ )
2873
+ );
2874
+ const expectedCounts = countRegistrations(expectedRegistrations);
2875
+ return [...expectedCounts.entries()].every(
2876
+ ([registrationKey, expectedCount]) => (actualCounts.get(registrationKey) ?? 0) >= expectedCount
2877
+ );
2878
+ }
2879
+ function countRegistrations(registrations) {
2880
+ const counts = /* @__PURE__ */ new Map();
2881
+ for (const registration of registrations) {
2882
+ const key = hookRegistrationKey(registration);
2883
+ counts.set(key, (counts.get(key) ?? 0) + 1);
2884
+ }
2885
+ return counts;
2886
+ }
2887
+ function hookRegistrationKey(registration) {
2888
+ return `${registration.event}\0${registration.command}`;
2889
+ }
2890
+ function collectHookCommandsByEvent(hooks) {
2891
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
2892
+ return /* @__PURE__ */ new Map();
2893
+ }
2894
+ const commandsByEvent = /* @__PURE__ */ new Map();
2895
+ for (const [event, value] of Object.entries(hooks)) {
2896
+ if (!Array.isArray(value)) {
2897
+ continue;
2898
+ }
2899
+ for (const group of value) {
2900
+ if (!group || typeof group !== "object") {
2901
+ continue;
2902
+ }
2903
+ const hookItems = group.hooks;
2904
+ if (!Array.isArray(hookItems)) {
2905
+ continue;
2906
+ }
2907
+ for (const hook of hookItems) {
2908
+ if (!hook || typeof hook !== "object") {
2909
+ continue;
2910
+ }
2911
+ const command = hook.command;
2912
+ if (typeof command === "string") {
2913
+ const eventCommands = commandsByEvent.get(event) ?? [];
2914
+ eventCommands.push(command);
2915
+ commandsByEvent.set(event, eventCommands);
2916
+ }
2917
+ }
2918
+ }
2919
+ }
2920
+ return commandsByEvent;
2921
+ }
2922
+ function isUnexpectedManagedHookCommand(command, expectedCommands, cwd, meta, platform, manifestManagedCommands) {
2923
+ if (expectedCommands.has(command)) {
2924
+ return false;
2925
+ }
2926
+ if (manifestManagedCommands.has(command)) {
2927
+ return true;
2928
+ }
2929
+ const hookPath = extractHookPathFromCommand(command);
2930
+ return hookPath === null ? false : isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform);
2931
+ }
2932
+ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
2933
+ const normalizedHookPath = normalizePathForHookComparison(hookPath);
2934
+ const configDir = normalizePathForHookComparison(meta.templateContext.platform_config_dir);
2935
+ return MANAGED_HOOK_SCRIPT_NAMES[platform].some((scriptName) => {
2936
+ const relativeHookPath = `${configDir}/hooks/${scriptName}`;
2937
+ if (normalizedHookPath === relativeHookPath) {
2938
+ return true;
2939
+ }
2940
+ return pathAliases(
2941
+ normalizePathForHookComparison(path22.resolve(cwd, relativeHookPath))
2942
+ ).includes(normalizedHookPath);
1919
2943
  });
1920
- await ensureEasyCodingSessionsIgnored(cwd);
1921
- await updateHarnessVersion(configPath, VERSION);
1922
- await setPendingInitSince(cwd, VERSION);
1923
- outro5(chalk7.green(`easy-coding harness upgraded to ${VERSION}.`));
2944
+ }
2945
+ function normalizePathForHookComparison(value) {
2946
+ return value.replace(/\\/g, "/").replace(/^\.\//, "");
2947
+ }
2948
+ function pathAliases(value) {
2949
+ const aliases = /* @__PURE__ */ new Set([value]);
2950
+ if (value.startsWith("/private/var/")) {
2951
+ aliases.add(value.replace(/^\/private\/var\//, "/var/"));
2952
+ } else if (value.startsWith("/var/")) {
2953
+ aliases.add(`/private${value}`);
2954
+ }
2955
+ return [...aliases];
2956
+ }
2957
+ async function resolveParentTopologyRefresh(targets, pending) {
2958
+ const parent = targets.find(
2959
+ (target) => target.label === "." && target.supermodule.role === "super-parent"
2960
+ );
2961
+ if (!parent || pending.some(({ target }) => target.label === ".")) {
2962
+ return null;
2963
+ }
2964
+ if (!await pathExists(parent.configPath)) {
2965
+ return null;
2966
+ }
2967
+ const config = await readConfigYaml(parent.configPath);
2968
+ if (!Array.isArray(config.agents) || config.agents.length === 0) {
2969
+ return null;
2970
+ }
2971
+ const submodulePaths = parent.supermodule.submodules ?? [];
2972
+ const configuredSubmodules = Array.isArray(config.supermodule?.submodules) ? config.supermodule.submodules : [];
2973
+ const needsRefresh = config.supermodule?.role !== "super-parent" || !sameStringList(configuredSubmodules, submodulePaths);
2974
+ if (!needsRefresh) {
2975
+ return null;
2976
+ }
2977
+ return {
2978
+ target: parent,
2979
+ agents: config.agents,
2980
+ submodulePaths
2981
+ };
2982
+ }
2983
+ async function resolveChildTopologyRefreshes(targets, pending) {
2984
+ const pendingLabels = new Set(pending.map(({ target }) => target.label));
2985
+ const refreshes = [];
2986
+ for (const target of targets) {
2987
+ if (target.supermodule.role !== "submodule-child" || pendingLabels.has(target.label) || !await pathExists(target.configPath)) {
2988
+ continue;
2989
+ }
2990
+ const config = await readConfigYaml(target.configPath);
2991
+ if (config.supermodule?.role === "submodule-child" && config.supermodule.parent === target.supermodule.parent) {
2992
+ continue;
2993
+ }
2994
+ refreshes.push({ target });
2995
+ }
2996
+ return refreshes;
2997
+ }
2998
+ function sameStringList(left, right) {
2999
+ if (left.length !== right.length) {
3000
+ return false;
3001
+ }
3002
+ const sortedLeft = [...left].sort();
3003
+ const sortedRight = [...right].sort();
3004
+ return sortedLeft.every((value, index) => value === sortedRight[index]);
1924
3005
  }
1925
3006
 
1926
3007
  // src/cli.ts
@@ -1940,11 +3021,14 @@ function withErrorHandling(fn) {
1940
3021
  await checkForUpgrade(process.cwd());
1941
3022
  var program = new Command();
1942
3023
  program.name("easy-coding").description(PACKAGE_NAME).version(VERSION, "-v, --version");
1943
- program.command("init").description("Initialize easy-coding harness in current project").option("--agent <list>", "Comma-separated platforms: claude-code,codex,qoder").option("-y, --yes", "Skip prompts, use defaults").action(withErrorHandling(init));
1944
- program.command("add-agent").description("Add agent platform support to an existing project").option("--agent <list>", "Comma-separated platforms to add").action(withErrorHandling(addAgent));
3024
+ program.command("init").description("Initialize easy-coding harness in current project").option("--agent <list>", "Comma-separated platforms: claude-code,codex,qoder").option(
3025
+ "--submodules <list>",
3026
+ "Comma-separated checked-out submodule paths or names to initialize"
3027
+ ).option("--no-submodules", "Initialize only the current directory when .gitmodules exists").option("-y, --yes", "Skip prompts, use defaults").action(withErrorHandling(init));
3028
+ program.command("add-agent").description("Add agent platform support to an existing project").option("--agent <list>", "Comma-separated platforms to add").option("--submodules <list>", "Comma-separated initialized submodule paths or names to update").option("--no-submodules", "Add the agent only to the current directory").action(withErrorHandling(addAgent));
1945
3029
  program.command("upgrade").description("Upgrade harness files to current CLI version").option("--dry-run", "Preview changes without applying").option("-y, --yes", "Skip confirmation").action(withErrorHandling(upgrade));
1946
3030
  program.command("update").description("Refresh the global CLI to the latest published version").option("--tag <tag>", "npm dist-tag or version to install", "latest").option("--dry-run", "Preview the install command without running it").option("-y, --yes", "Skip confirmation").action(withErrorHandling(update));
1947
3031
  program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
1948
- program.command("clear").description("Remove installed harness files (skills, hooks, config); keep tasks, spec, memory").option("--dry-run", "Preview what would be removed without deleting").option("-y, --yes", "Skip confirmation").action(withErrorHandling(clear));
3032
+ program.command("clear").description("Remove installed harness files (skills, hooks, config); keep tasks, spec, memory").option("--submodules <list>", "Comma-separated initialized submodule paths or names to clear").option("--no-submodules", "Clear only the current directory").option("--dry-run", "Preview what would be removed without deleting").option("-y, --yes", "Skip confirmation").action(withErrorHandling(clear));
1949
3033
  program.parse();
1950
3034
  //# sourceMappingURL=cli.js.map