easy-coding-harness 0.3.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
353
  hook_path: artifact.hookPath,
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
  }
@@ -332,17 +472,181 @@ function byPath(a, b) {
332
472
  return a.path.localeCompare(b.path);
333
473
  }
334
474
 
335
- // src/configurators/shared.ts
336
- import { readdir, stat as stat2 } from "fs/promises";
337
- import path4 from "path";
475
+ // src/utils/runtime-scaffold.ts
476
+ import path6 from "path";
338
477
 
339
- // src/utils/marked-region.ts
340
- var MarkedRegionError = class extends Error {
341
- constructor(message) {
342
- super(message);
343
- this.name = "MarkedRegionError";
344
- }
345
- };
478
+ // src/utils/template-paths.ts
479
+ import { existsSync } from "fs";
480
+ import path5 from "path";
481
+ import { fileURLToPath as fileURLToPath2 } from "url";
482
+ function getTemplateRoot() {
483
+ const here = path5.dirname(fileURLToPath2(import.meta.url));
484
+ const candidates = [
485
+ path5.resolve(here, "../templates"),
486
+ path5.resolve(here, "../../templates"),
487
+ path5.resolve(process.cwd(), "src/templates"),
488
+ path5.resolve(process.cwd(), "templates")
489
+ ];
490
+ const found = candidates.find((candidate) => existsSync(candidate));
491
+ if (!found) {
492
+ throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
493
+ }
494
+ return found;
495
+ }
496
+ function getTemplatePath(...segments) {
497
+ return path5.join(getTemplateRoot(), ...segments);
498
+ }
499
+
500
+ // src/utils/runtime-scaffold.ts
501
+ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
502
+ const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
503
+ await ensureDir(easyCodingDir);
504
+ const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
505
+ if (!await pathExists(configPath2)) {
506
+ const projectName = path6.basename(cwd);
507
+ await writeConfigYaml(
508
+ configPath2,
509
+ createDefaultConfig({
510
+ projectName,
511
+ harnessVersion: VERSION,
512
+ agents,
513
+ supermodule: opts.supermodule
514
+ })
515
+ );
516
+ }
517
+ await ensureDir(path6.join(easyCodingDir, "tasks"));
518
+ await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
519
+ await ensureDir(path6.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
520
+ await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
521
+ await writeMemoryScaffold(easyCodingDir);
522
+ await writeTemplatesScaffold(easyCodingDir);
523
+ }
524
+ async function writeTemplatesScaffold(easyCodingDir) {
525
+ const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
526
+ await ensureDir(templatesDir);
527
+ const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
528
+ const dest = path6.join(templatesDir, "dev-spec-skeleton.md");
529
+ await writeTextFile(dest, await readTextFile(src));
530
+ }
531
+ async function writeMemoryScaffold(easyCodingDir) {
532
+ const memoryDir = path6.join(easyCodingDir, MEMORY_DIR);
533
+ await ensureDir(path6.join(memoryDir, "short"));
534
+ await ensureDir(path6.join(memoryDir, "long"));
535
+ for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
536
+ const destination = path6.join(memoryDir, "long", file);
537
+ if (await pathExists(destination)) {
538
+ continue;
539
+ }
540
+ const templatePath = getTemplatePath("runtime", "memory", "long", file);
541
+ await writeTextFile(destination, await readTextFile(templatePath));
542
+ }
543
+ const shortTemplateDest = path6.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
544
+ if (!await pathExists(shortTemplateDest)) {
545
+ const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
546
+ await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
547
+ }
548
+ }
549
+
550
+ // src/utils/task-json.ts
551
+ import { readdir } from "fs/promises";
552
+ import path7 from "path";
553
+ function createProjectInitTask(params) {
554
+ return {
555
+ type: "project-init",
556
+ status: "PENDING",
557
+ created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
558
+ created_by: "cli-init",
559
+ last_agent: "cli",
560
+ stage_history: [],
561
+ context: {
562
+ agents_installed: params.agents,
563
+ cli_version: VERSION,
564
+ project_path: params.cwd,
565
+ init_source: params.initSource ?? "fresh",
566
+ ...params.legacyAssets ? { legacy_assets: params.legacyAssets } : {},
567
+ ...params.legacyMissingHarnessFiles ? { legacy_missing_harness_files: params.legacyMissingHarnessFiles } : {}
568
+ },
569
+ init_log: []
570
+ };
571
+ }
572
+ function getTaskJsonPath(cwd, taskId) {
573
+ return path7.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
574
+ }
575
+ async function writeTaskJson(filePath, task) {
576
+ await writeTextFile(filePath, JSON.stringify(task, null, 2));
577
+ }
578
+ async function readTaskJson(filePath) {
579
+ return JSON.parse(await readTextFile(filePath));
580
+ }
581
+ async function writeProjectInitTask(cwd, agents, options = {}) {
582
+ await writeTaskJson(
583
+ getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID),
584
+ createProjectInitTask({
585
+ cwd,
586
+ agents,
587
+ initSource: options.initSource,
588
+ legacyAssets: options.legacyAssets,
589
+ legacyMissingHarnessFiles: options.legacyMissingHarnessFiles
590
+ })
591
+ );
592
+ }
593
+ async function setPendingInitSince(cwd, version) {
594
+ const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
595
+ if (!await pathExists(filePath)) return;
596
+ const task = await readTaskJson(filePath);
597
+ if (task.status !== "COMPLETE") return;
598
+ task.pending_init_since = version;
599
+ await writeTaskJson(filePath, task);
600
+ }
601
+ async function listTasks(cwd) {
602
+ const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
603
+ if (!await pathExists(tasksDir)) {
604
+ return [];
605
+ }
606
+ const entries = await readdir(tasksDir, { withFileTypes: true });
607
+ const tasks = [];
608
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
609
+ if (!entry.isDirectory()) {
610
+ continue;
611
+ }
612
+ const taskPath = getTaskJsonPath(cwd, entry.name);
613
+ if (!await pathExists(taskPath)) {
614
+ continue;
615
+ }
616
+ tasks.push({ id: entry.name, task: await readTaskJson(taskPath) });
617
+ }
618
+ return tasks;
619
+ }
620
+ function summarizeTaskStatuses(tasks) {
621
+ return tasks.reduce(
622
+ (summary, item) => {
623
+ summary[item.task.status] = (summary[item.task.status] ?? 0) + 1;
624
+ return summary;
625
+ },
626
+ {}
627
+ );
628
+ }
629
+ function isActiveTask(task) {
630
+ return task.status !== "COMPLETE" && task.status !== "CLOSED";
631
+ }
632
+
633
+ // src/commands/install-harness.ts
634
+ import path13 from "path";
635
+
636
+ // src/configurators/claude.ts
637
+ import path9 from "path";
638
+
639
+ // src/configurators/shared.ts
640
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
641
+ import path8 from "path";
642
+
643
+ // src/utils/marked-region.ts
644
+ var MarkedRegionError = class extends Error {
645
+ constructor(message) {
646
+ super(message);
647
+ this.name = "MarkedRegionError";
648
+ }
649
+ };
346
650
  function extractMarkedRegion(content) {
347
651
  const start = content.indexOf(GENERATED_REGION_START);
348
652
  const end = content.indexOf(GENERATED_REGION_END);
@@ -373,28 +677,6 @@ function removeMarkedRegion(content) {
373
677
  ` : "";
374
678
  }
375
679
 
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
680
  // src/configurators/shared.ts
399
681
  var UnresolvedPlaceholderError = class extends Error {
400
682
  constructor(contentName, placeholders) {
@@ -416,14 +698,14 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
416
698
  }
417
699
  async function resolveSkills(ctx) {
418
700
  const skillsRoot = getTemplatePath("common", "skills");
419
- const entries = await readdir(skillsRoot);
701
+ const entries = await readdir2(skillsRoot);
420
702
  const skills = [];
421
703
  for (const entry of entries.sort()) {
422
- const skillDir = path4.join(skillsRoot, entry);
704
+ const skillDir = path8.join(skillsRoot, entry);
423
705
  if (!await isDirectory(skillDir)) {
424
706
  continue;
425
707
  }
426
- const content = await readTextFile(path4.join(skillDir, "SKILL.md"));
708
+ const content = await readTextFile(path8.join(skillDir, "SKILL.md"));
427
709
  skills.push({
428
710
  name: entry,
429
711
  content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
@@ -436,10 +718,10 @@ async function resolveBundledSkills(ctx) {
436
718
  if (!await pathExists(bundledRoot)) {
437
719
  return [];
438
720
  }
439
- const entries = await readdir(bundledRoot);
721
+ const entries = await readdir2(bundledRoot);
440
722
  const bundled = [];
441
723
  for (const entry of entries.sort()) {
442
- const sourceDir = path4.join(bundledRoot, entry);
724
+ const sourceDir = path8.join(bundledRoot, entry);
443
725
  if (await isDirectory(sourceDir)) {
444
726
  bundled.push({ name: entry, sourceDir, context: ctx });
445
727
  }
@@ -450,13 +732,13 @@ async function writeSkills(dir, skills, bundled) {
450
732
  await ensureDir(dir);
451
733
  const written = [];
452
734
  for (const skill of skills) {
453
- const destination = path4.join(dir, skill.name, "SKILL.md");
735
+ const destination = path8.join(dir, skill.name, "SKILL.md");
454
736
  await writeTextFile(destination, skill.content);
455
737
  written.push(destination);
456
738
  }
457
739
  for (const skill of bundled) {
458
740
  written.push(
459
- ...await copyTemplateDirectory(skill.sourceDir, path4.join(dir, skill.name), skill.context)
741
+ ...await copyTemplateDirectory(skill.sourceDir, path8.join(dir, skill.name), skill.context)
460
742
  );
461
743
  }
462
744
  return written;
@@ -464,14 +746,14 @@ async function writeSkills(dir, skills, bundled) {
464
746
  async function writeSharedHooks(dir, platform, opts = {}) {
465
747
  const hooksRoot = getTemplatePath("shared-hooks");
466
748
  const ctx = PLATFORM_META[platform].templateContext;
467
- const entries = await readdir(hooksRoot);
749
+ const entries = await readdir2(hooksRoot);
468
750
  await ensureDir(dir);
469
751
  const written = [];
470
752
  for (const entry of entries.sort()) {
471
753
  if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
472
754
  continue;
473
755
  }
474
- const sourcePath = path4.join(hooksRoot, entry);
756
+ const sourcePath = path8.join(hooksRoot, entry);
475
757
  if ((await stat2(sourcePath)).isDirectory()) {
476
758
  continue;
477
759
  }
@@ -480,7 +762,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
480
762
  ctx,
481
763
  `shared-hooks/${entry}`
482
764
  );
483
- const destination = path4.join(dir, entry);
765
+ const destination = path8.join(dir, entry);
484
766
  await writeTextFile(destination, content);
485
767
  await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
486
768
  written.push(destination);
@@ -491,16 +773,19 @@ async function copyPlatformTemplates(platformTemplateDir, destination, skipDirs,
491
773
  const source = getTemplatePath(platformTemplateDir);
492
774
  return copyTemplateDirectory(source, destination, ctx, new Set(skipDirs));
493
775
  }
494
- async function writeMainConstraint(cwd, platform) {
776
+ async function writeMainConstraint(cwd, platform, opts = {}) {
495
777
  const meta = PLATFORM_META[platform];
496
- const ctx = meta.templateContext;
778
+ const ctx = {
779
+ ...meta.templateContext,
780
+ supermodule_boundary: renderSupermoduleBoundary(opts.supermodule)
781
+ };
497
782
  const templateName = `${meta.mainConstraint}.tpl`;
498
783
  const template = await readTextFile(getTemplatePath("main-constraint", templateName));
499
784
  const generated = resolvePlaceholders(template, ctx, `main-constraint/${templateName}`);
500
785
  if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
501
786
  throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
502
787
  }
503
- const destination = path4.join(cwd, meta.mainConstraint);
788
+ const destination = path8.join(cwd, meta.mainConstraint);
504
789
  const current = await readTextIfExists(destination);
505
790
  if (current) {
506
791
  const markerContent = extractMarkedRegion(generated);
@@ -513,15 +798,37 @@ async function writeMainConstraint(cwd, platform) {
513
798
  }
514
799
  return destination;
515
800
  }
801
+ function renderSupermoduleBoundary(boundary) {
802
+ if (!boundary || boundary.submodulePaths.length === 0) {
803
+ return "";
804
+ }
805
+ const submodules = boundary.submodulePaths.map((submodulePath) => `- \`${submodulePath}\``);
806
+ return [
807
+ "",
808
+ "## Supermodule Boundary",
809
+ "",
810
+ "This directory is a git supermodule root. The following subdirectories are independent",
811
+ "submodule harness roots with their own git boundaries and Easy Coding runtime:",
812
+ "",
813
+ ...submodules,
814
+ "",
815
+ "- Cross-repo work launched from this root uses the parent `.easy-coding` task, session, state, and spec as the source of truth.",
816
+ "- Child `.easy-coding` directories are not parent workflow state sources.",
817
+ "- 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.",
818
+ "- Commit submodule changes in two steps: push each child repo first, then commit and push the parent gitlink update.",
819
+ "- Parent memory should keep cross-repo context; child technical details should follow the owning child repo.",
820
+ ""
821
+ ].join("\n");
822
+ }
516
823
  async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
517
824
  await ensureDir(destination);
518
825
  const written = [];
519
- for (const entry of (await readdir(source)).sort()) {
826
+ for (const entry of (await readdir2(source)).sort()) {
520
827
  if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
521
828
  continue;
522
829
  }
523
- const sourcePath = path4.join(source, entry);
524
- const destinationPath = path4.join(destination, stripTemplateExtension(entry));
830
+ const sourcePath = path8.join(source, entry);
831
+ const destinationPath = path8.join(destination, stripTemplateExtension(entry));
525
832
  const sourceStat = await stat2(sourcePath);
526
833
  if (sourceStat.isDirectory()) {
527
834
  written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
@@ -541,63 +848,65 @@ function stripTemplateExtension(fileName) {
541
848
  }
542
849
 
543
850
  // src/configurators/claude.ts
544
- async function configureClaude(cwd) {
851
+ async function configureClaude(cwd, opts = {}) {
545
852
  const platform = "claude-code";
546
853
  const meta = PLATFORM_META[platform];
547
854
  const ctx = meta.templateContext;
548
- const dest = path5.join(cwd, ".claude");
549
- const hookConfigPath = path5.join(cwd, meta.hookConfigFile);
855
+ const dest = path9.join(cwd, ".claude");
856
+ const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
550
857
  const artifacts = [];
551
858
  const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
552
859
  artifacts.push(
553
860
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
554
861
  (filePath) => fileArtifact(
555
862
  filePath,
556
- filePath.startsWith(path5.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
863
+ filePath.startsWith(path9.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
557
864
  platform
558
865
  )
559
866
  )
560
867
  );
561
868
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
562
869
  artifacts.push(
563
- ...(await writeSharedHooks(path5.join(dest, "hooks"), platform)).map(
870
+ ...(await writeSharedHooks(path9.join(dest, "hooks"), platform)).map(
564
871
  (filePath) => fileArtifact(filePath, "hook", platform)
565
872
  )
566
873
  );
567
874
  artifacts.push(
568
875
  ...(await writeSkills(
569
- path5.join(cwd, meta.skillsDir),
876
+ path9.join(cwd, meta.skillsDir),
570
877
  await resolveSkills(ctx),
571
878
  await resolveBundledSkills(ctx)
572
879
  )).map((filePath) => fileArtifact(filePath, "skill", platform))
573
880
  );
574
- artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
881
+ artifacts.push(
882
+ constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
883
+ );
575
884
  return artifacts;
576
885
  }
577
886
 
578
887
  // src/configurators/codex.ts
579
- import path6 from "path";
580
- async function configureCodex(cwd) {
888
+ import path10 from "path";
889
+ async function configureCodex(cwd, opts = {}) {
581
890
  const platform = "codex";
582
891
  const meta = PLATFORM_META[platform];
583
892
  const ctx = meta.templateContext;
584
- const hookConfigPath = path6.join(cwd, meta.hookConfigFile);
893
+ const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
585
894
  const artifacts = [];
586
895
  artifacts.push(
587
896
  ...(await writeSkills(
588
- path6.join(cwd, meta.skillsDir),
897
+ path10.join(cwd, meta.skillsDir),
589
898
  await resolveSkills(ctx),
590
899
  await resolveBundledSkills(ctx)
591
900
  )).map((filePath) => fileArtifact(filePath, "skill", platform))
592
901
  );
593
902
  artifacts.push(
594
- ...(await writeSharedHooks(path6.join(cwd, meta.hooksDir), platform, {
903
+ ...(await writeSharedHooks(path10.join(cwd, meta.hooksDir), platform, {
595
904
  skipSubagentContext: true
596
905
  })).map((filePath) => fileArtifact(filePath, "hook", platform))
597
906
  );
598
907
  const platformFiles = await copyPlatformTemplates(
599
908
  "codex",
600
- path6.join(cwd, ".codex"),
909
+ path10.join(cwd, ".codex"),
601
910
  ["hooks"],
602
911
  ctx
603
912
  );
@@ -605,28 +914,30 @@ async function configureCodex(cwd) {
605
914
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
606
915
  (filePath) => fileArtifact(
607
916
  filePath,
608
- filePath.startsWith(path6.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
917
+ filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
609
918
  platform
610
919
  )
611
920
  )
612
921
  );
613
922
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
614
- artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
923
+ artifacts.push(
924
+ constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
925
+ );
615
926
  return artifacts;
616
927
  }
617
928
 
618
929
  // src/configurators/qoder.ts
619
- import { readdir as readdir2 } from "fs/promises";
620
- import path8 from "path";
930
+ import { readdir as readdir3 } from "fs/promises";
931
+ import path12 from "path";
621
932
 
622
933
  // src/utils/platform-paths.ts
623
934
  import { existsSync as existsSync2 } from "fs";
624
- import path7 from "path";
935
+ import path11 from "path";
625
936
  function detectQoderCnVariant(cwd) {
626
937
  if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
627
938
  return true;
628
939
  }
629
- return existsSync2(path7.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
940
+ return existsSync2(path11.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
630
941
  }
631
942
  function resolvePlatformMeta(cwd, platform) {
632
943
  const meta = PLATFORM_META[platform];
@@ -657,12 +968,12 @@ function resolveQoderMetaForBaseDir(baseDir) {
657
968
 
658
969
  // src/configurators/qoder.ts
659
970
  async function claudeHarnessSkillsExist(cwd) {
660
- return pathExists(path8.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
971
+ return pathExists(path12.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
661
972
  }
662
973
  async function listFilesRecursive(dir) {
663
974
  let entries;
664
975
  try {
665
- entries = await readdir2(dir, { withFileTypes: true });
976
+ entries = await readdir3(dir, { withFileTypes: true });
666
977
  } catch (error) {
667
978
  if (error.code === "ENOENT") {
668
979
  return [];
@@ -671,7 +982,7 @@ async function listFilesRecursive(dir) {
671
982
  }
672
983
  const files = [];
673
984
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
674
- const entryPath = path8.join(dir, entry.name);
985
+ const entryPath = path12.join(dir, entry.name);
675
986
  if (entry.isDirectory()) {
676
987
  files.push(...await listFilesRecursive(entryPath));
677
988
  continue;
@@ -685,32 +996,32 @@ async function listFilesRecursive(dir) {
685
996
  async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
686
997
  const artifacts = [];
687
998
  for (const skillName of skillNames) {
688
- for (const filePath of await listFilesRecursive(path8.join(skillsDir, skillName))) {
999
+ for (const filePath of await listFilesRecursive(path12.join(skillsDir, skillName))) {
689
1000
  artifacts.push(fileArtifact(filePath, "skill", platform));
690
1001
  }
691
1002
  }
692
1003
  return artifacts;
693
1004
  }
694
- async function configureQoder(cwd) {
1005
+ async function configureQoder(cwd, opts = {}) {
695
1006
  const platform = "qoder";
696
1007
  const meta = resolvePlatformMeta(cwd, platform);
697
1008
  const ctx = meta.templateContext;
698
- const dest = path8.join(cwd, ctx.platform_config_dir);
699
- const hookConfigPath = path8.join(cwd, meta.hookConfigFile);
1009
+ const dest = path12.join(cwd, ctx.platform_config_dir);
1010
+ const hookConfigPath = path12.join(cwd, meta.hookConfigFile);
700
1011
  const artifacts = [];
701
1012
  const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
702
1013
  artifacts.push(
703
1014
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
704
1015
  (filePath) => fileArtifact(
705
1016
  filePath,
706
- filePath.startsWith(path8.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1017
+ filePath.startsWith(path12.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
707
1018
  platform
708
1019
  )
709
1020
  )
710
1021
  );
711
1022
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
712
1023
  artifacts.push(
713
- ...(await writeSharedHooks(path8.join(dest, "hooks"), platform)).map(
1024
+ ...(await writeSharedHooks(path12.join(dest, "hooks"), platform)).map(
714
1025
  (filePath) => fileArtifact(filePath, "hook", platform)
715
1026
  )
716
1027
  );
@@ -718,20 +1029,22 @@ async function configureQoder(cwd) {
718
1029
  const bundledSkills = await resolveBundledSkills(ctx);
719
1030
  if (!await claudeHarnessSkillsExist(cwd)) {
720
1031
  artifacts.push(
721
- ...(await writeSkills(path8.join(dest, "skills"), skills, bundledSkills)).map(
1032
+ ...(await writeSkills(path12.join(dest, "skills"), skills, bundledSkills)).map(
722
1033
  (filePath) => fileArtifact(filePath, "skill", platform)
723
1034
  )
724
1035
  );
725
1036
  } else {
726
1037
  artifacts.push(
727
1038
  ...await existingManagedSkillArtifacts(
728
- path8.join(dest, "skills"),
1039
+ path12.join(dest, "skills"),
729
1040
  [...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
730
1041
  platform
731
1042
  )
732
1043
  );
733
1044
  }
734
- artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
1045
+ artifacts.push(
1046
+ constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
1047
+ );
735
1048
  return artifacts;
736
1049
  }
737
1050
 
@@ -742,350 +1055,557 @@ var CONFIGURATORS = {
742
1055
  qoder: configureQoder
743
1056
  };
744
1057
 
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
- }
767
- }
768
- throw new Error(`Unable to locate package metadata. Tried: ${candidates.join(", ")}`);
769
- }
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
- `));
782
- }
783
- function selectBannerPreset(width) {
784
- if (width >= WIDE_TERMINAL_WIDTH) {
785
- return { font: "ANSI Shadow", horizontalLayout: "full" };
786
- }
787
- if (width >= MEDIUM_TERMINAL_WIDTH) {
788
- return { font: "Small Shadow", horizontalLayout: "full" };
789
- }
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);
1058
+ // src/commands/install-harness.ts
1059
+ async function configurePlatformsForDir(targetDir, platforms, boundary) {
1060
+ const artifacts = [];
1061
+ for (const platform of platforms) {
1062
+ artifacts.push(...await CONFIGURATORS[platform](targetDir, { supermodule: boundary }));
839
1063
  }
840
- await writeTextFile(filePath, document.toString());
841
- return config;
1064
+ return artifacts;
842
1065
  }
843
- async function addAgentsToConfig(filePath, agents) {
844
- return updateConfigYaml(filePath, (config) => {
845
- const merged = /* @__PURE__ */ new Set([...config.agents ?? [], ...agents]);
846
- config.agents = [...merged];
1066
+ async function installHarnessToDir(targetDir, platforms, ctx) {
1067
+ const artifacts = await configurePlatformsForDir(targetDir, platforms, boundaryFromContext(ctx));
1068
+ await writeRuntimeScaffold(targetDir, platforms, {
1069
+ supermodule: supermoduleConfigFromContext(ctx)
847
1070
  });
848
- }
849
- async function updateHarnessVersion(filePath, version) {
850
- return updateConfigYaml(filePath, (config) => {
851
- config.harness_version = version;
1071
+ await writeInstallManifest(targetDir, {
1072
+ harnessVersion: VERSION,
1073
+ agents: platforms,
1074
+ artifacts
852
1075
  });
1076
+ await writeProjectInitTask(targetDir, platforms, {
1077
+ initSource: ctx.initSource,
1078
+ legacyAssets: ctx.legacyAssets,
1079
+ legacyMissingHarnessFiles: ctx.legacyMissingHarnessFiles
1080
+ });
1081
+ await ensureEasyCodingSessionsIgnored(targetDir);
1082
+ return artifacts;
853
1083
  }
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;
1084
+ function supermoduleConfigFromContext(ctx) {
1085
+ if (ctx.role === "super-parent") {
1086
+ return {
1087
+ role: "super-parent",
1088
+ submodules: ctx.submodulePaths ?? []
1089
+ };
1090
+ }
1091
+ if (ctx.role === "submodule-child") {
1092
+ return {
1093
+ role: "submodule-child",
1094
+ parent: ctx.parent
1095
+ };
1096
+ }
1097
+ return { role: "standalone" };
1098
+ }
1099
+ async function refreshSupermoduleParent(targetDir, platforms, submodulePaths) {
1100
+ const supermodule = {
1101
+ role: "super-parent",
1102
+ submodules: submodulePaths
1103
+ };
1104
+ await updateSupermoduleConfig(path13.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
1105
+ for (const platform of platforms) {
1106
+ await writeMainConstraint(targetDir, platform, {
1107
+ supermodule: { submodulePaths }
1108
+ });
863
1109
  }
864
- const prefix = current.trimEnd();
865
- const next = [prefix, heading, entry].filter(Boolean).join("\n");
866
- await writeTextFile(gitignorePath, next);
867
- return true;
868
1110
  }
869
- async function ensureEasyCodingSessionsIgnored(cwd) {
870
- return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
1111
+ function boundaryFromContext(ctx) {
1112
+ if (ctx.role !== "super-parent") {
1113
+ return void 0;
1114
+ }
1115
+ return { submodulePaths: ctx.submodulePaths ?? [] };
871
1116
  }
872
1117
 
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
- );
1118
+ // src/commands/platforms.ts
1119
+ import { cancel, confirm, multiselect } from "@clack/prompts";
1120
+ function parseAgentList(agentList) {
1121
+ const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
1122
+ if (values.length === 0) {
1123
+ throw new Error("No agent platform specified.");
885
1124
  }
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);
1125
+ const invalid = values.filter((value) => !isAgentPlatform(value));
1126
+ if (invalid.length > 0) {
1127
+ throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
1128
+ }
1129
+ return [...new Set(values)];
892
1130
  }
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));
1131
+ async function resolvePlatforms(opts, defaults = ["claude-code"]) {
1132
+ if (opts.agent) {
1133
+ return parseAgentList(opts.agent);
1134
+ }
1135
+ if (opts.yes) {
1136
+ return defaults;
1137
+ }
1138
+ let selectedDefaults = defaults;
1139
+ while (true) {
1140
+ const result = await multiselect({
1141
+ message: "Select agent platforms (Space to toggle, Enter to review)",
1142
+ options: AGENT_PLATFORMS.map((platform) => ({
1143
+ label: PLATFORM_META[platform].label,
1144
+ value: platform
1145
+ })),
1146
+ initialValues: selectedDefaults,
1147
+ required: true
1148
+ });
1149
+ if (typeof result === "symbol") {
1150
+ cancel("Platform selection cancelled.");
1151
+ process.exit(1);
1152
+ }
1153
+ const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
1154
+ const shouldInstall = await confirm({
1155
+ message: `Install Easy Coding for: ${labels}?`,
1156
+ initialValue: true
1157
+ });
1158
+ if (typeof shouldInstall === "symbol") {
1159
+ cancel("Platform selection cancelled.");
1160
+ process.exit(1);
1161
+ }
1162
+ if (shouldInstall) {
1163
+ return result;
1164
+ }
1165
+ selectedDefaults = result;
1166
+ }
899
1167
  }
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)) {
1168
+ function parseSubmoduleList(submoduleList, available) {
1169
+ const requested = submoduleList.split(",").map((value) => value.trim()).filter(Boolean);
1170
+ if (requested.length === 0) {
1171
+ throw new Error("No submodule specified.");
1172
+ }
1173
+ const byPathOrName = /* @__PURE__ */ new Map();
1174
+ for (const submodule of available) {
1175
+ byPathOrName.set(submodule.path, submodule);
1176
+ byPathOrName.set(submodule.name, submodule);
1177
+ }
1178
+ const selected = [];
1179
+ const invalid = [];
1180
+ for (const value of requested) {
1181
+ const submodule = byPathOrName.get(value);
1182
+ if (!submodule) {
1183
+ invalid.push(value);
907
1184
  continue;
908
1185
  }
909
- const templatePath = getTemplatePath("runtime", "memory", "long", file);
910
- await writeTextFile(destination, await readTextFile(templatePath));
1186
+ if (!selected.some((item) => item.path === submodule.path)) {
1187
+ selected.push(submodule);
1188
+ }
911
1189
  }
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));
1190
+ if (invalid.length > 0) {
1191
+ throw new Error(`Unknown or unavailable submodule: ${invalid.join(", ")}`);
1192
+ }
1193
+ return selected.sort((a, b) => a.path.localeCompare(b.path));
1194
+ }
1195
+ async function resolveSubmodules(opts, available, defaultSelection = available) {
1196
+ if (opts.submodules === false) {
1197
+ return [];
916
1198
  }
1199
+ if (typeof opts.submodules === "string") {
1200
+ return parseSubmoduleList(opts.submodules, available);
1201
+ }
1202
+ if (available.length === 0) {
1203
+ return [];
1204
+ }
1205
+ if (opts.yes) {
1206
+ return defaultSelection;
1207
+ }
1208
+ const result = await multiselect({
1209
+ message: "Select checked-out submodules to initialize (Space to toggle, Enter to confirm)",
1210
+ options: available.map((submodule) => ({
1211
+ label: `${submodule.path} (${submodule.name})`,
1212
+ value: submodule.path
1213
+ })),
1214
+ initialValues: defaultSelection.map((submodule) => submodule.path),
1215
+ required: false
1216
+ });
1217
+ if (typeof result === "symbol") {
1218
+ cancel("Submodule selection cancelled.");
1219
+ process.exit(1);
1220
+ }
1221
+ const selected = new Set(result);
1222
+ return available.filter((submodule) => selected.has(submodule.path));
917
1223
  }
918
1224
 
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: []
1225
+ // src/commands/supermodule-targets.ts
1226
+ import path15 from "path";
1227
+ import { cancel as cancel2, multiselect as multiselect2 } from "@clack/prompts";
1228
+
1229
+ // src/utils/gitmodules.ts
1230
+ import { lstat, realpath } from "fs/promises";
1231
+ import path14 from "path";
1232
+ async function parseGitmodules(rootDir) {
1233
+ const content = await readTextIfExists(path14.join(rootDir, ".gitmodules"));
1234
+ if (content === null) {
1235
+ return [];
1236
+ }
1237
+ const entries = [];
1238
+ let current = null;
1239
+ const flushCurrent = () => {
1240
+ if (current?.name && current.path) {
1241
+ entries.push({
1242
+ name: current.name,
1243
+ path: current.path,
1244
+ url: current.url ?? ""
1245
+ });
1246
+ }
939
1247
  };
1248
+ for (const rawLine of content.split(/\r?\n/)) {
1249
+ const line = stripGitConfigInlineComment(rawLine).trim();
1250
+ if (!line || line.startsWith("#") || line.startsWith(";")) {
1251
+ continue;
1252
+ }
1253
+ const section = line.match(/^\[submodule\s+"(.+)"\]$/i);
1254
+ if (section) {
1255
+ flushCurrent();
1256
+ current = { name: section[1] };
1257
+ continue;
1258
+ }
1259
+ if (!current) {
1260
+ continue;
1261
+ }
1262
+ const keyValue = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/);
1263
+ if (!keyValue) {
1264
+ continue;
1265
+ }
1266
+ const key = keyValue[1].toLowerCase();
1267
+ const value = unquote(keyValue[2].trim());
1268
+ if (key === "path") {
1269
+ current.path = normalizeSubmodulePath(value);
1270
+ continue;
1271
+ }
1272
+ if (key === "url") {
1273
+ current.url = value;
1274
+ }
1275
+ }
1276
+ flushCurrent();
1277
+ return entries.sort((a, b) => a.path.localeCompare(b.path));
940
1278
  }
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
- );
1279
+ async function listInstallableSubmodules(rootDir) {
1280
+ const entries = await parseGitmodules(rootDir);
1281
+ const installable = [];
1282
+ for (const entry of entries) {
1283
+ if (await isSubmoduleWorktree(rootDir, entry.path)) {
1284
+ installable.push(entry);
1285
+ }
1286
+ }
1287
+ return installable;
961
1288
  }
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);
1289
+ function normalizeSubmodulePath(value) {
1290
+ const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
1291
+ const parts = normalized.split("/").filter(Boolean);
1292
+ if (normalized === "" || path14.posix.isAbsolute(normalized) || path14.win32.isAbsolute(value) || parts.some((part) => part === "..")) {
1293
+ throw new Error(`Unsafe submodule path in .gitmodules: ${value}`);
1294
+ }
1295
+ return parts.join("/");
969
1296
  }
970
- async function listTasks(cwd) {
971
- const tasksDir = path12.join(cwd, EASY_CODING_DIR, TASKS_DIR);
972
- if (!await pathExists(tasksDir)) {
973
- return [];
1297
+ function unquote(value) {
1298
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1299
+ return value.slice(1, -1);
974
1300
  }
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()) {
1301
+ return value;
1302
+ }
1303
+ function stripGitConfigInlineComment(value) {
1304
+ let inQuotes = false;
1305
+ let escaped = false;
1306
+ for (let index = 0; index < value.length; index += 1) {
1307
+ const char = value[index];
1308
+ if (escaped) {
1309
+ escaped = false;
979
1310
  continue;
980
1311
  }
981
- const taskPath = getTaskJsonPath(cwd, entry.name);
982
- if (!await pathExists(taskPath)) {
1312
+ if (inQuotes && char === "\\") {
1313
+ escaped = true;
1314
+ continue;
1315
+ }
1316
+ if (char === '"') {
1317
+ inQuotes = !inQuotes;
983
1318
  continue;
984
1319
  }
985
- tasks.push({ id: entry.name, task: await readTaskJson(taskPath) });
1320
+ if (!inQuotes && (char === "#" || char === ";") && isCommentStart(value, index)) {
1321
+ return value.slice(0, index);
1322
+ }
1323
+ }
1324
+ return value;
1325
+ }
1326
+ function isCommentStart(value, index) {
1327
+ return index === 0 || /\s/.test(value[index - 1]);
1328
+ }
1329
+ async function isSubmoduleWorktree(rootDir, submodulePath) {
1330
+ const dir = path14.join(rootDir, submodulePath);
1331
+ try {
1332
+ if (!await pathHasNoSymlinkSegments(rootDir, submodulePath)) {
1333
+ return false;
1334
+ }
1335
+ const rootRealPath = await realpath(rootDir);
1336
+ const dirRealPath = await realpath(dir);
1337
+ if (!isInsideDirectory(rootRealPath, dirRealPath)) {
1338
+ return false;
1339
+ }
1340
+ const dirStat = await lstat(dir);
1341
+ if (!dirStat.isDirectory()) {
1342
+ return false;
1343
+ }
1344
+ const gitMarkerStat = await lstat(path14.join(dir, ".git"));
1345
+ return !gitMarkerStat.isSymbolicLink() && (gitMarkerStat.isFile() || gitMarkerStat.isDirectory());
1346
+ } catch (error) {
1347
+ if (error.code === "ENOENT") {
1348
+ return false;
1349
+ }
1350
+ throw error;
986
1351
  }
987
- return tasks;
988
1352
  }
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
- );
1353
+ async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
1354
+ let current = rootDir;
1355
+ for (const part of submodulePath.split("/")) {
1356
+ current = path14.join(current, part);
1357
+ const partStat = await lstat(current);
1358
+ if (partStat.isSymbolicLink()) {
1359
+ return false;
1360
+ }
1361
+ }
1362
+ return true;
997
1363
  }
998
- function isActiveTask(task) {
999
- return task.status !== "COMPLETE" && task.status !== "CLOSED";
1364
+ function isInsideDirectory(parent, child) {
1365
+ const relative = path14.relative(parent, child);
1366
+ return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
1000
1367
  }
1001
1368
 
1002
- // src/commands/platforms.ts
1003
- import { cancel, confirm, multiselect } from "@clack/prompts";
1004
- function parseAgentList(agentList) {
1005
- const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
1006
- if (values.length === 0) {
1007
- throw new Error("No agent platform specified.");
1008
- }
1009
- const invalid = values.filter((value) => !isAgentPlatform(value));
1010
- if (invalid.length > 0) {
1011
- throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
1369
+ // src/commands/supermodule-targets.ts
1370
+ function rejectSubmoduleListWithoutGitmodules(opts) {
1371
+ if (typeof opts.submodules === "string") {
1372
+ throw new Error("--submodules can only be used in a repository with .gitmodules.");
1373
+ }
1374
+ }
1375
+ async function resolveAddAgentTargets(cwd, opts) {
1376
+ const installedSubmodules = await listInstalledSubmodules(cwd);
1377
+ if ((await parseGitmodules(cwd)).length === 0) {
1378
+ rejectSubmoduleListWithoutGitmodules(opts);
1379
+ return [standaloneTarget(cwd)];
1380
+ }
1381
+ const selected = await resolveSubmodules(opts, installedSubmodules);
1382
+ const parentManagedSubmodulePaths = await listParentManagedSubmodulePaths(cwd);
1383
+ const parentSubmodulePaths = [
1384
+ .../* @__PURE__ */ new Set([...parentManagedSubmodulePaths, ...selected.map((entry) => entry.path)])
1385
+ ].sort();
1386
+ return [
1387
+ parentTargetFromPaths(cwd, parentSubmodulePaths),
1388
+ ...selected.map((entry) => childTarget(cwd, entry))
1389
+ ];
1390
+ }
1391
+ async function resolveUpgradeTargets(cwd) {
1392
+ const installedSubmodules = await listInstalledSubmodules(cwd);
1393
+ if ((await parseGitmodules(cwd)).length === 0) {
1394
+ return [standaloneTarget(cwd)];
1012
1395
  }
1013
- return [...new Set(values)];
1396
+ return [
1397
+ parentTarget(cwd, installedSubmodules),
1398
+ ...installedSubmodules.map((entry) => childTarget(cwd, entry))
1399
+ ];
1014
1400
  }
1015
- async function resolvePlatforms(opts, defaults = ["claude-code"]) {
1016
- if (opts.agent) {
1017
- return parseAgentList(opts.agent);
1401
+ async function resolveInitSubmoduleSelection(cwd, opts) {
1402
+ const installable = await listInstallableSubmodules(cwd);
1403
+ const installed = await listInstalledSubmodules(cwd);
1404
+ const parentManagedSubmodulePaths = await listParentManagedSubmodulePaths(cwd);
1405
+ const defaultSelection = installable;
1406
+ const selected = await resolveSubmodules(opts, installable, defaultSelection);
1407
+ const parentSubmodulePaths = [
1408
+ .../* @__PURE__ */ new Set([...parentManagedSubmodulePaths, ...selected.map((entry) => entry.path)])
1409
+ ].sort();
1410
+ return { installable, installed, selected, parentSubmodulePaths };
1411
+ }
1412
+ async function resolveClearTargets(cwd, opts) {
1413
+ if ((await parseGitmodules(cwd)).length === 0) {
1414
+ rejectSubmoduleListWithoutGitmodules(opts);
1415
+ return [standaloneTarget(cwd)];
1416
+ }
1417
+ const installedSubmodules = await listInstalledSubmodules(cwd);
1418
+ const parent = await pathExists(path15.join(cwd, EASY_CODING_DIR)) ? parentTarget(cwd, installedSubmodules) : null;
1419
+ const children = installedSubmodules.map((entry) => childTarget(cwd, entry));
1420
+ if (opts.submodules === false) {
1421
+ return parent ? [parent] : [];
1422
+ }
1423
+ if (typeof opts.submodules === "string") {
1424
+ return [
1425
+ ...parent ? [parent] : [],
1426
+ ...parseSubmoduleSelection(opts.submodules, installedSubmodules).map(
1427
+ (entry) => childTarget(cwd, entry)
1428
+ )
1429
+ ];
1018
1430
  }
1019
1431
  if (opts.yes) {
1020
- return defaults;
1432
+ return parent ? [parent] : [];
1021
1433
  }
1022
- let selectedDefaults = defaults;
1023
- while (true) {
1024
- const result = await multiselect({
1025
- message: "Select agent platforms (Space to toggle, Enter to review)",
1026
- options: AGENT_PLATFORMS.map((platform) => ({
1027
- label: PLATFORM_META[platform].label,
1028
- value: platform
1029
- })),
1030
- initialValues: selectedDefaults,
1031
- required: true
1032
- });
1033
- if (typeof result === "symbol") {
1034
- cancel("Platform selection cancelled.");
1035
- process.exit(1);
1434
+ const candidates = [...parent ? [parent] : [], ...children];
1435
+ if (candidates.length === 0) {
1436
+ return [];
1437
+ }
1438
+ const result = await multiselect2({
1439
+ message: "Select Easy Coding harness targets to clear",
1440
+ options: candidates.map((target) => ({
1441
+ label: target.label === "." ? "parent repository" : `submodule: ${target.label}`,
1442
+ value: target.label
1443
+ })),
1444
+ initialValues: parent ? [parent.label] : [],
1445
+ required: true
1446
+ });
1447
+ if (typeof result === "symbol") {
1448
+ cancel2("Clear target selection cancelled.");
1449
+ process.exit(1);
1450
+ }
1451
+ const selected = new Set(result);
1452
+ return candidates.filter((target) => selected.has(target.label));
1453
+ }
1454
+ async function listInstalledSubmodules(cwd) {
1455
+ const entries = await listInstallableSubmodules(cwd);
1456
+ const installed = [];
1457
+ for (const entry of entries) {
1458
+ if (await pathExists(configPath(path15.join(cwd, entry.path)))) {
1459
+ installed.push(entry);
1036
1460
  }
1037
- const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
1038
- const shouldInstall = await confirm({
1039
- message: `Install Easy Coding for: ${labels}?`,
1040
- initialValue: true
1041
- });
1042
- if (typeof shouldInstall === "symbol") {
1043
- cancel("Platform selection cancelled.");
1044
- process.exit(1);
1461
+ }
1462
+ return installed;
1463
+ }
1464
+ async function listParentManagedSubmodulePaths(cwd) {
1465
+ const parentConfigPath = configPath(cwd);
1466
+ if (!await pathExists(parentConfigPath)) {
1467
+ return [];
1468
+ }
1469
+ try {
1470
+ const config = await readConfigYaml(parentConfigPath);
1471
+ if (config.supermodule?.role !== "super-parent" || !Array.isArray(config.supermodule.submodules)) {
1472
+ return [];
1045
1473
  }
1046
- if (shouldInstall) {
1047
- return result;
1474
+ return config.supermodule.submodules.filter((submodulePath) => typeof submodulePath === "string").sort();
1475
+ } catch {
1476
+ return [];
1477
+ }
1478
+ }
1479
+ function standaloneTarget(cwd) {
1480
+ return {
1481
+ dir: cwd,
1482
+ label: ".",
1483
+ configPath: configPath(cwd),
1484
+ supermodule: { role: "standalone" }
1485
+ };
1486
+ }
1487
+ function parentTarget(cwd, installedSubmodules) {
1488
+ const submodulePaths = installedSubmodules.map((entry) => entry.path);
1489
+ return parentTargetFromPaths(cwd, submodulePaths);
1490
+ }
1491
+ function parentTargetFromPaths(cwd, submodulePaths) {
1492
+ return {
1493
+ dir: cwd,
1494
+ label: ".",
1495
+ configPath: configPath(cwd),
1496
+ supermodule: { role: "super-parent", submodules: submodulePaths },
1497
+ boundary: { submodulePaths }
1498
+ };
1499
+ }
1500
+ function childTarget(cwd, entry) {
1501
+ const dir = path15.join(cwd, entry.path);
1502
+ return {
1503
+ dir,
1504
+ label: entry.path,
1505
+ configPath: configPath(dir),
1506
+ supermodule: { role: "submodule-child", parent: toPosixRelative(dir, cwd) }
1507
+ };
1508
+ }
1509
+ function parseSubmoduleSelection(submoduleList, available) {
1510
+ const requested = submoduleList.split(",").map((value) => value.trim()).filter(Boolean);
1511
+ if (requested.length === 0) {
1512
+ throw new Error("No submodule specified.");
1513
+ }
1514
+ const byPathOrName = /* @__PURE__ */ new Map();
1515
+ for (const submodule of available) {
1516
+ byPathOrName.set(submodule.path, submodule);
1517
+ byPathOrName.set(submodule.name, submodule);
1518
+ }
1519
+ const selected = [];
1520
+ const invalid = [];
1521
+ for (const value of requested) {
1522
+ const submodule = byPathOrName.get(value);
1523
+ if (!submodule) {
1524
+ invalid.push(value);
1525
+ continue;
1526
+ }
1527
+ if (!selected.some((item) => item.path === submodule.path)) {
1528
+ selected.push(submodule);
1048
1529
  }
1049
- selectedDefaults = result;
1050
1530
  }
1531
+ if (invalid.length > 0) {
1532
+ throw new Error(`Unknown or unavailable initialized submodule: ${invalid.join(", ")}`);
1533
+ }
1534
+ return selected.sort((a, b) => a.path.localeCompare(b.path));
1535
+ }
1536
+ function configPath(cwd) {
1537
+ return path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1538
+ }
1539
+ function toPosixRelative(from, to) {
1540
+ const relative = path15.relative(from, to);
1541
+ return relative ? relative.split(path15.sep).join("/") : ".";
1051
1542
  }
1052
1543
 
1053
1544
  // src/commands/add-agent.ts
1054
1545
  async function addAgent(opts) {
1055
1546
  renderBanner();
1056
1547
  const cwd = process.cwd();
1057
- const configPath = path13.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1058
- if (!await pathExists(configPath)) {
1548
+ const targets = await resolveAddAgentTargets(cwd, opts);
1549
+ if (!await pathExists(targets[0].configPath)) {
1059
1550
  throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
1060
1551
  }
1061
- const config = await readConfigYaml(configPath);
1062
1552
  const platforms = await resolvePlatforms(opts, ["claude-code"]);
1063
- const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
1064
- if (toInstall.length === 0) {
1553
+ const installedLabels = [];
1554
+ const refreshedLabels = [];
1555
+ for (const target of targets) {
1556
+ if (!await pathExists(target.configPath)) {
1557
+ continue;
1558
+ }
1559
+ const config = await readConfigYaml(target.configPath);
1560
+ const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
1561
+ const agents = [...config.agents, ...toInstall];
1562
+ if (toInstall.length > 0) {
1563
+ const artifacts = await configurePlatformsForDir(
1564
+ target.dir,
1565
+ toInstall,
1566
+ target.boundary
1567
+ );
1568
+ await writeRuntimeScaffold(target.dir, agents, {
1569
+ supermodule: target.supermodule
1570
+ });
1571
+ await writeInstallManifest(target.dir, {
1572
+ harnessVersion: VERSION,
1573
+ agents: toInstall,
1574
+ artifacts,
1575
+ mode: "merge"
1576
+ });
1577
+ await ensureEasyCodingSessionsIgnored(target.dir);
1578
+ await addAgentsToConfig(target.configPath, toInstall);
1579
+ await setPendingInitSince(target.dir, VERSION);
1580
+ installedLabels.push(`${target.label}: ${toInstall.join(", ")}`);
1581
+ }
1582
+ if (target.supermodule.role === "super-parent") {
1583
+ await refreshSupermoduleParent(target.dir, agents, target.supermodule.submodules ?? []);
1584
+ refreshedLabels.push(target.label);
1585
+ continue;
1586
+ }
1587
+ if (target.supermodule.role === "submodule-child") {
1588
+ await updateSupermoduleConfig(target.configPath, target.supermodule);
1589
+ refreshedLabels.push(target.label);
1590
+ }
1591
+ }
1592
+ if (installedLabels.length === 0) {
1593
+ if (refreshedLabels.length > 0) {
1594
+ outro(chalk2.green(`Supermodule topology refreshed:
1595
+ ${refreshedLabels.join("\n")}`));
1596
+ return;
1597
+ }
1065
1598
  outro(chalk2.yellow("All selected agent platforms are already installed."));
1066
1599
  return;
1067
1600
  }
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(", ")}`));
1601
+ outro(chalk2.green(`Added agent platforms:
1602
+ ${installedLabels.join("\n")}`));
1083
1603
  }
1084
1604
 
1085
1605
  // src/commands/clear.ts
1086
1606
  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";
1607
+ import path16 from "path";
1608
+ import { cancel as cancel3, confirm as confirm2, outro as outro2 } from "@clack/prompts";
1089
1609
  import chalk3 from "chalk";
1090
1610
  var PLATFORM_TEMPLATE_DIR = {
1091
1611
  "claude-code": "claude",
@@ -1095,13 +1615,12 @@ var PLATFORM_TEMPLATE_DIR = {
1095
1615
  async function clear(opts) {
1096
1616
  renderBanner();
1097
1617
  const cwd = process.cwd();
1098
- const easyCodingDir = path14.join(cwd, EASY_CODING_DIR);
1099
- if (!await pathExists(easyCodingDir)) {
1618
+ const targets = await resolveClearTargets(cwd, opts);
1619
+ const targetPlans = await buildTargetClearPlans(targets);
1620
+ if (targetPlans.length === 0) {
1100
1621
  throw new Error("No .easy-coding directory found in this project \u2014 nothing to clear.");
1101
1622
  }
1102
- const agents = await resolveInstalledAgents(cwd);
1103
- const plan = await buildClearPlan(cwd, agents);
1104
- console.log(renderPlan(cwd, agents, plan));
1623
+ console.log(renderTargetPlans(targetPlans));
1105
1624
  if (opts.dryRun) {
1106
1625
  return;
1107
1626
  }
@@ -1111,18 +1630,54 @@ async function clear(opts) {
1111
1630
  initialValue: false
1112
1631
  });
1113
1632
  if (typeof ok === "symbol" || !ok) {
1114
- cancel2("Clear cancelled.");
1633
+ cancel3("Clear cancelled.");
1115
1634
  return;
1116
1635
  }
1117
1636
  }
1118
- await executeClearPlan(plan);
1637
+ for (const targetPlan of targetPlans) {
1638
+ await executeClearPlan(targetPlan.plan);
1639
+ }
1640
+ await refreshParentAfterChildClear(cwd, targetPlans);
1119
1641
  outro2(chalk3.green("Easy Coding harness removed. Run easy-coding init to reinstall."));
1120
1642
  }
1643
+ async function buildTargetClearPlans(targets) {
1644
+ const plans = [];
1645
+ for (const target of targets) {
1646
+ const easyCodingDir = path16.join(target.dir, EASY_CODING_DIR);
1647
+ if (!await pathExists(easyCodingDir)) {
1648
+ continue;
1649
+ }
1650
+ const agents = await resolveInstalledAgents(target.dir);
1651
+ plans.push({
1652
+ target,
1653
+ agents,
1654
+ plan: await buildClearPlan(target.dir, agents)
1655
+ });
1656
+ }
1657
+ return plans;
1658
+ }
1659
+ async function refreshParentAfterChildClear(cwd, targetPlans) {
1660
+ if (targetPlans.some((targetPlan) => targetPlan.target.label === ".")) {
1661
+ return;
1662
+ }
1663
+ if (!await pathExists(path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
1664
+ return;
1665
+ }
1666
+ const [parent] = await resolveUpgradeTargets(cwd);
1667
+ if (!parent || parent.label !== ".") {
1668
+ return;
1669
+ }
1670
+ const config = await readConfigYaml(parent.configPath);
1671
+ if (!Array.isArray(config.agents) || config.agents.length === 0) {
1672
+ return;
1673
+ }
1674
+ await refreshSupermoduleParent(cwd, config.agents, parent.supermodule.submodules ?? []);
1675
+ }
1121
1676
  async function resolveInstalledAgents(cwd) {
1122
- const configPath = path14.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1123
- if (await pathExists(configPath)) {
1677
+ const configPath2 = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1678
+ if (await pathExists(configPath2)) {
1124
1679
  try {
1125
- const config = await readConfigYaml(configPath);
1680
+ const config = await readConfigYaml(configPath2);
1126
1681
  const listed = Array.isArray(config.agents) ? config.agents.filter(isAgentPlatform) : [];
1127
1682
  if (listed.length > 0) {
1128
1683
  return listed;
@@ -1150,7 +1705,7 @@ async function hasPlatformInstall(cwd, platform) {
1150
1705
  async function hasInstallMarkers(cwd, meta) {
1151
1706
  const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
1152
1707
  for (const marker of markers) {
1153
- if (await pathExists(path14.join(cwd, marker))) {
1708
+ if (await pathExists(path16.join(cwd, marker))) {
1154
1709
  return true;
1155
1710
  }
1156
1711
  }
@@ -1206,7 +1761,7 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
1206
1761
  }
1207
1762
  if (await manifestFileMatches(filePath, file.sha256)) {
1208
1763
  plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
1209
- addEmptyDirChain(plan.emptyDirs, path14.dirname(filePath), cwd);
1764
+ addEmptyDirChain(plan.emptyDirs, path16.dirname(filePath), cwd);
1210
1765
  } else {
1211
1766
  plan.skippedModified.push(filePath);
1212
1767
  }
@@ -1255,28 +1810,28 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
1255
1810
  );
1256
1811
  for (const meta of metas) {
1257
1812
  for (const name of managedSkills) {
1258
- plan.remove.add(path14.join(cwd, meta.skillsDir, name));
1813
+ plan.remove.add(path16.join(cwd, meta.skillsDir, name));
1259
1814
  }
1260
1815
  for (const name of hookFileNames) {
1261
- plan.remove.add(path14.join(cwd, meta.hooksDir, name));
1816
+ plan.remove.add(path16.join(cwd, meta.hooksDir, name));
1262
1817
  }
1263
1818
  for (const name of agentFileNames) {
1264
- plan.remove.add(path14.join(cwd, meta.agentsDir, name));
1819
+ plan.remove.add(path16.join(cwd, meta.agentsDir, name));
1265
1820
  }
1266
1821
  addHookConfigPrune(
1267
1822
  plan.pruneHookConfigs,
1268
1823
  plan.pruneHookCommands,
1269
- path14.join(cwd, meta.hookConfigFile),
1824
+ path16.join(cwd, meta.hookConfigFile),
1270
1825
  hookFileNames.map((name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`),
1271
1826
  []
1272
1827
  );
1273
- plan.constraints.add(path14.join(cwd, meta.mainConstraint));
1828
+ plan.constraints.add(path16.join(cwd, meta.mainConstraint));
1274
1829
  if (platform === "codex") {
1275
- plan.remove.add(path14.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
1830
+ plan.remove.add(path16.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
1276
1831
  }
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));
1832
+ plan.emptyDirs.add(path16.join(cwd, meta.skillsDir));
1833
+ plan.emptyDirs.add(path16.join(cwd, meta.hooksDir));
1834
+ plan.emptyDirs.add(path16.join(cwd, meta.agentsDir));
1280
1835
  }
1281
1836
  }
1282
1837
  async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
@@ -1380,27 +1935,27 @@ function addRuntimeClearEntries(plan, cwd) {
1380
1935
  plan.remove = [
1381
1936
  .../* @__PURE__ */ new Set([
1382
1937
  ...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)
1938
+ path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
1939
+ path16.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
1940
+ path16.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
1941
+ path16.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
1387
1942
  ])
1388
1943
  ];
1389
1944
  }
1390
1945
  function addEmptyDirChain(emptyDirs, startDir, cwd) {
1391
1946
  let current = startDir;
1392
- while (current !== cwd && isInsideDirectory(cwd, current)) {
1947
+ while (current !== cwd && isInsideDirectory2(cwd, current)) {
1393
1948
  emptyDirs.add(current);
1394
- const parent = path14.dirname(current);
1949
+ const parent = path16.dirname(current);
1395
1950
  if (parent === current) {
1396
1951
  break;
1397
1952
  }
1398
1953
  current = parent;
1399
1954
  }
1400
1955
  }
1401
- function isInsideDirectory(parent, child) {
1402
- const relative = path14.relative(parent, child);
1403
- return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
1956
+ function isInsideDirectory2(parent, child) {
1957
+ const relative = path16.relative(parent, child);
1958
+ return Boolean(relative) && !relative.startsWith("..") && !path16.isAbsolute(relative);
1404
1959
  }
1405
1960
  async function listSharedHookNamesForPlatform(platform) {
1406
1961
  const names = await listFileNames(getTemplatePath("shared-hooks"));
@@ -1537,7 +2092,7 @@ function sortDirsDeepestFirst(dirs) {
1537
2092
  });
1538
2093
  }
1539
2094
  function pathDepth(dir) {
1540
- return path14.normalize(dir).split(path14.sep).filter(Boolean).length;
2095
+ return path16.normalize(dir).split(path16.sep).filter(Boolean).length;
1541
2096
  }
1542
2097
  async function listDirNames(dir) {
1543
2098
  try {
@@ -1556,7 +2111,7 @@ async function listFileNames(dir) {
1556
2111
  }
1557
2112
  }
1558
2113
  function renderPlan(cwd, agents, plan) {
1559
- const rel = (target) => path14.relative(cwd, target) || target;
2114
+ const rel = (target) => path16.relative(cwd, target) || target;
1560
2115
  const lines = [];
1561
2116
  lines.push(chalk3.bold("easy-coding clear"));
1562
2117
  lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
@@ -1594,23 +2149,33 @@ function renderPlan(cwd, agents, plan) {
1594
2149
  );
1595
2150
  return lines.join("\n");
1596
2151
  }
2152
+ function renderTargetPlans(targetPlans) {
2153
+ if (targetPlans.length === 1) {
2154
+ const [{ target, agents, plan }] = targetPlans;
2155
+ return [`Target: ${target.label}`, renderPlan(target.dir, agents, plan)].join("\n");
2156
+ }
2157
+ return targetPlans.map(
2158
+ ({ target, agents, plan }) => [`Target: ${target.label}`, renderPlan(target.dir, agents, plan)].join("\n")
2159
+ ).join("\n\n");
2160
+ }
1597
2161
 
1598
2162
  // src/commands/init.ts
2163
+ import path18 from "path";
1599
2164
  import { note, outro as outro3 } from "@clack/prompts";
1600
2165
  import chalk4 from "chalk";
1601
2166
 
1602
2167
  // src/utils/install-state.ts
1603
2168
  import { readdir as readdir5 } from "fs/promises";
1604
- import path15 from "path";
2169
+ import path17 from "path";
1605
2170
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
1606
2171
  async function detectEasyCodingInstallState(cwd) {
1607
- const easyCodingDir = path15.join(cwd, EASY_CODING_DIR);
2172
+ const easyCodingDir = path17.join(cwd, EASY_CODING_DIR);
1608
2173
  if (!await pathExists(easyCodingDir)) {
1609
2174
  return { kind: "fresh", easyCodingDir };
1610
2175
  }
1611
- const configPath = path15.join(easyCodingDir, CONFIG_FILE);
1612
- if (await pathExists(configPath)) {
1613
- return { kind: "installed", easyCodingDir, configPath };
2176
+ const configPath2 = path17.join(easyCodingDir, CONFIG_FILE);
2177
+ if (await pathExists(configPath2)) {
2178
+ return { kind: "installed", easyCodingDir, configPath: configPath2 };
1614
2179
  }
1615
2180
  const legacyAssets = await detectLegacyAssets(easyCodingDir);
1616
2181
  if (legacyAssets.length === 0) {
@@ -1626,17 +2191,17 @@ async function detectEasyCodingInstallState(cwd) {
1626
2191
  async function detectLegacyAssets(easyCodingDir) {
1627
2192
  const assets = [];
1628
2193
  for (const file of LEGACY_ROOT_FILES) {
1629
- if (await pathExists(path15.join(easyCodingDir, file))) {
2194
+ if (await pathExists(path17.join(easyCodingDir, file))) {
1630
2195
  assets.push(relativeEasyCodingPath(file));
1631
2196
  }
1632
2197
  }
1633
- if (await pathExists(path15.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
2198
+ if (await pathExists(path17.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
1634
2199
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
1635
2200
  }
1636
- const shortMemoryFiles = await listMarkdownFiles(path15.join(easyCodingDir, "memory", "short"));
2201
+ const shortMemoryFiles = await listMarkdownFiles(path17.join(easyCodingDir, "memory", "short"));
1637
2202
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
1638
2203
  for (const dir of ["spec", "prototype"]) {
1639
- if (await hasAnyDirectoryEntry(path15.join(easyCodingDir, dir))) {
2204
+ if (await hasAnyDirectoryEntry(path17.join(easyCodingDir, dir))) {
1640
2205
  assets.push(relativeEasyCodingPath(dir));
1641
2206
  }
1642
2207
  }
@@ -1656,19 +2221,54 @@ async function hasAnyDirectoryEntry(dir) {
1656
2221
  return (await readdir5(dir)).length > 0;
1657
2222
  }
1658
2223
  function relativeEasyCodingPath(...segments) {
1659
- return path15.posix.join(EASY_CODING_DIR, ...segments);
2224
+ return path17.posix.join(EASY_CODING_DIR, ...segments);
1660
2225
  }
1661
2226
  function relativeConfigPath() {
1662
- return path15.posix.join(EASY_CODING_DIR, CONFIG_FILE);
2227
+ return path17.posix.join(EASY_CODING_DIR, CONFIG_FILE);
1663
2228
  }
1664
2229
  function relativeProjectInitTaskPath() {
1665
- return path15.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
2230
+ return path17.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
1666
2231
  }
1667
2232
 
1668
2233
  // src/commands/init.ts
1669
2234
  async function init(opts) {
1670
2235
  renderBanner();
1671
2236
  const cwd = process.cwd();
2237
+ const submodules = await parseGitmodules(cwd);
2238
+ if (submodules.length === 0) {
2239
+ rejectSubmoduleListWithoutGitmodules(opts);
2240
+ }
2241
+ const targets = submodules.length === 0 ? [await standaloneTarget2(cwd)] : await supermoduleTargets(cwd, opts, submodules);
2242
+ const installableTargets = targets.filter((target) => !target.installed);
2243
+ const parentTarget2 = targets.find((target) => target.label === ".");
2244
+ if (installableTargets.length === 0) {
2245
+ await refreshInstalledChildTopologies(targets);
2246
+ if (submodules.length > 0 && parentTarget2) {
2247
+ const platforms2 = await resolveInitPlatforms(cwd, opts, true);
2248
+ await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms2);
2249
+ }
2250
+ outro3(chalk4.yellow("All selected easy-coding harness targets are already installed."));
2251
+ return;
2252
+ }
2253
+ const platforms = await resolveInitPlatforms(cwd, opts, Boolean(parentTarget2?.installed));
2254
+ for (const target of installableTargets) {
2255
+ await installHarnessToDir(target.dir, platforms, target.context);
2256
+ }
2257
+ await refreshInstalledChildTopologies(targets);
2258
+ if (submodules.length > 0 && parentTarget2) {
2259
+ await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms);
2260
+ }
2261
+ const triggers = platforms.map(
2262
+ (platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
2263
+ ).join("\n");
2264
+ note(triggers, "Next step");
2265
+ outro3(
2266
+ chalk4.green(
2267
+ `easy-coding harness installed in ${installableTargets.map((target) => target.label).join(", ")}. Open your agent and run ec-init.`
2268
+ )
2269
+ );
2270
+ }
2271
+ async function standaloneTarget2(cwd) {
1672
2272
  const installState = await detectEasyCodingInstallState(cwd);
1673
2273
  if (installState.kind === "installed") {
1674
2274
  throw new Error(
@@ -1680,36 +2280,121 @@ async function init(opts) {
1680
2280
  ".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
2281
  );
1682
2282
  }
1683
- const platforms = await resolvePlatforms(opts, ["claude-code"]);
1684
- const artifacts = [];
1685
- for (const platform of platforms) {
1686
- artifacts.push(...await CONFIGURATORS[platform](cwd));
2283
+ return {
2284
+ dir: cwd,
2285
+ label: ".",
2286
+ context: contextFromState("standalone", installState),
2287
+ installed: false
2288
+ };
2289
+ }
2290
+ async function supermoduleTargets(cwd, opts, submodules) {
2291
+ const { installable, parentSubmodulePaths } = await resolveInitSubmoduleSelection(cwd, opts);
2292
+ const skipped = submodules.filter(
2293
+ (entry) => !installable.some((installableEntry) => installableEntry.path === entry.path)
2294
+ );
2295
+ if (skipped.length > 0) {
2296
+ note(
2297
+ skipped.map((entry) => `${entry.path} (${entry.name})`).join("\n"),
2298
+ "Skipped unchecked-out submodules"
2299
+ );
1687
2300
  }
1688
- await writeRuntimeScaffold(cwd, platforms);
1689
- await writeInstallManifest(cwd, {
1690
- harnessVersion: VERSION,
1691
- agents: platforms,
1692
- artifacts
1693
- });
1694
- await writeProjectInitTask(cwd, platforms, {
2301
+ const targets = [
2302
+ await targetFromState(cwd, ".", "super-parent", {
2303
+ submodulePaths: parentSubmodulePaths
2304
+ })
2305
+ ];
2306
+ const targetSubmodulePaths = opts.submodules === false ? /* @__PURE__ */ new Set() : new Set(parentSubmodulePaths);
2307
+ for (const entry of installable) {
2308
+ if (!targetSubmodulePaths.has(entry.path)) {
2309
+ continue;
2310
+ }
2311
+ const dir = path18.join(cwd, entry.path);
2312
+ targets.push(
2313
+ await targetFromState(dir, entry.path, "submodule-child", {
2314
+ parent: toPosixRelative2(dir, cwd)
2315
+ })
2316
+ );
2317
+ }
2318
+ return targets;
2319
+ }
2320
+ async function targetFromState(targetDir, label, role, extraContext = {}) {
2321
+ const installState = await detectEasyCodingInstallState(targetDir);
2322
+ if (installState.kind === "unknown") {
2323
+ throw new Error(
2324
+ `${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.`
2325
+ );
2326
+ }
2327
+ if (installState.kind === "installed") {
2328
+ return {
2329
+ dir: targetDir,
2330
+ label,
2331
+ installed: true,
2332
+ context: {
2333
+ role,
2334
+ initSource: "fresh",
2335
+ ...extraContext
2336
+ }
2337
+ };
2338
+ }
2339
+ return {
2340
+ dir: targetDir,
2341
+ label,
2342
+ installed: false,
2343
+ context: {
2344
+ ...contextFromState(role, installState),
2345
+ ...extraContext
2346
+ }
2347
+ };
2348
+ }
2349
+ function contextFromState(role, installState) {
2350
+ return {
2351
+ role,
1695
2352
  initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
1696
2353
  legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
1697
2354
  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."));
2355
+ };
2356
+ }
2357
+ function toPosixRelative2(from, to) {
2358
+ const relative = path18.relative(from, to);
2359
+ return relative ? relative.split(path18.sep).join("/") : ".";
2360
+ }
2361
+ async function resolveInitPlatforms(cwd, opts, parentInstalled) {
2362
+ if (opts.agent || !parentInstalled) {
2363
+ return resolvePlatforms(opts, ["claude-code"]);
2364
+ }
2365
+ const config = await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
2366
+ if (Array.isArray(config.agents) && config.agents.length > 0) {
2367
+ return config.agents;
2368
+ }
2369
+ return resolvePlatforms(opts, ["claude-code"]);
2370
+ }
2371
+ async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
2372
+ if (!await pathExists(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
2373
+ return;
2374
+ }
2375
+ const config = parentTarget2.installed ? await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
2376
+ const platforms = Array.isArray(config.agents) && config.agents.length > 0 ? config.agents : installPlatforms;
2377
+ await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
2378
+ }
2379
+ async function refreshInstalledChildTopologies(targets) {
2380
+ for (const target of targets) {
2381
+ if (!target.installed || target.context.role !== "submodule-child") {
2382
+ continue;
2383
+ }
2384
+ const configPath2 = path18.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
2385
+ if (!await pathExists(configPath2)) {
2386
+ continue;
2387
+ }
2388
+ await updateSupermoduleConfig(configPath2, supermoduleConfigFromContext(target.context));
2389
+ }
1705
2390
  }
1706
2391
 
1707
2392
  // src/commands/status.ts
1708
- import path18 from "path";
2393
+ import path21 from "path";
1709
2394
  import chalk5 from "chalk";
1710
2395
 
1711
2396
  // src/utils/compare-versions.ts
1712
- import path16 from "path";
2397
+ import path19 from "path";
1713
2398
  function normalize(version) {
1714
2399
  const [core] = String(version ?? "").split("-");
1715
2400
  return core.split(".").map((part) => {
@@ -1733,13 +2418,13 @@ function isVersionBehind(installed, current = VERSION) {
1733
2418
  return compareVersions(installed, current) === -1;
1734
2419
  }
1735
2420
  async function checkForUpgrade(cwd) {
1736
- const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1737
- if (!await pathExists(configPath)) {
2421
+ const configPath2 = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2422
+ if (!await pathExists(configPath2)) {
1738
2423
  return;
1739
2424
  }
1740
2425
  let config;
1741
2426
  try {
1742
- config = await readConfigYaml(configPath);
2427
+ config = await readConfigYaml(configPath2);
1743
2428
  } catch {
1744
2429
  return;
1745
2430
  }
@@ -1754,14 +2439,14 @@ async function checkForUpgrade(cwd) {
1754
2439
 
1755
2440
  // src/utils/session.ts
1756
2441
  import { readdir as readdir6 } from "fs/promises";
1757
- import path17 from "path";
2442
+ import path20 from "path";
1758
2443
  var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
1759
2444
  function getSessionDir(cwd) {
1760
- return path17.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
2445
+ return path20.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
1761
2446
  }
1762
2447
  function getSessionFilePath(cwd, ppid) {
1763
2448
  const pid = ppid ?? process.ppid;
1764
- return path17.join(getSessionDir(cwd), `${pid}.json`);
2449
+ return path20.join(getSessionDir(cwd), `${pid}.json`);
1765
2450
  }
1766
2451
  async function readSessionFile(cwd, ppid) {
1767
2452
  const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
@@ -1775,11 +2460,11 @@ async function readSessionFile(cwd, ppid) {
1775
2460
  async function status() {
1776
2461
  renderBanner();
1777
2462
  const cwd = process.cwd();
1778
- const configPath = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1779
- if (!await pathExists(configPath)) {
2463
+ const configPath2 = path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2464
+ if (!await pathExists(configPath2)) {
1780
2465
  throw new Error("No easy-coding harness found in this project.");
1781
2466
  }
1782
- const config = await readConfigYaml(configPath);
2467
+ const config = await readConfigYaml(configPath2);
1783
2468
  const tasks = await listTasks(cwd);
1784
2469
  const taskCounts = summarizeTaskStatuses(tasks);
1785
2470
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
@@ -1829,7 +2514,7 @@ async function status() {
1829
2514
 
1830
2515
  // src/commands/update.ts
1831
2516
  import { execFileSync } from "child_process";
1832
- import { cancel as cancel3, confirm as confirm3, outro as outro4 } from "@clack/prompts";
2517
+ import { cancel as cancel4, confirm as confirm3, outro as outro4 } from "@clack/prompts";
1833
2518
  import chalk6 from "chalk";
1834
2519
  async function update(opts) {
1835
2520
  renderBanner();
@@ -1848,7 +2533,7 @@ async function update(opts) {
1848
2533
  initialValue: true
1849
2534
  });
1850
2535
  if (typeof confirmed === "symbol" || !confirmed) {
1851
- cancel3("Update cancelled.");
2536
+ cancel4("Update cancelled.");
1852
2537
  return;
1853
2538
  }
1854
2539
  }
@@ -1858,37 +2543,35 @@ async function update(opts) {
1858
2543
  }
1859
2544
 
1860
2545
  // src/commands/upgrade.ts
1861
- import path19 from "path";
1862
- import { cancel as cancel4, confirm as confirm4, outro as outro5 } from "@clack/prompts";
2546
+ import { cancel as cancel5, confirm as confirm4, outro as outro5 } from "@clack/prompts";
1863
2547
  import chalk7 from "chalk";
1864
2548
  async function upgrade(opts) {
1865
2549
  renderBanner();
1866
2550
  const cwd = process.cwd();
1867
- const configPath = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1868
- if (!await pathExists(configPath)) {
2551
+ const targets = await resolveUpgradeTargets(cwd);
2552
+ if (!await pathExists(targets[0].configPath)) {
1869
2553
  throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
1870
2554
  }
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) {
2555
+ const pending = await resolvePendingUpgradeTargets(targets);
2556
+ const parentTopologyRefresh = await resolveParentTopologyRefresh(targets, pending);
2557
+ const childTopologyRefreshes = await resolveChildTopologyRefreshes(targets, pending);
2558
+ if (pending.length === 0 && !parentTopologyRefresh && childTopologyRefreshes.length === 0) {
1881
2559
  outro5(chalk7.green(`easy-coding harness is already up to date (${VERSION}).`));
1882
2560
  return;
1883
2561
  }
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
2562
  const summary = [
1890
- `Upgrade: ${installedVersion || "unknown"} -> ${VERSION}`,
1891
- `Installed agents: ${config.agents.join(", ")}`,
2563
+ "Upgrade targets:",
2564
+ ...pending.length > 0 ? pending.map(
2565
+ ({ target, config }) => `- ${target.label}: ${String(config.harness_version ?? "unknown")} -> ${VERSION}; agents: ${config.agents.join(", ")}`
2566
+ ) : ["- (none)"],
2567
+ ...parentTopologyRefresh ? [
2568
+ "Supermodule topology refresh:",
2569
+ `- ${parentTopologyRefresh.target.label}: submodules: ${parentTopologyRefresh.submodulePaths.length > 0 ? parentTopologyRefresh.submodulePaths.join(", ") : "(none)"}`
2570
+ ] : [],
2571
+ ...childTopologyRefreshes.length > 0 ? [
2572
+ parentTopologyRefresh ? "" : "Supermodule topology refresh:",
2573
+ ...childTopologyRefreshes.map(({ target }) => `- ${target.label}: role: submodule-child`)
2574
+ ].filter(Boolean) : [],
1892
2575
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
1893
2576
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
1894
2577
  "Will not touch memory, state, spec, or project knowledge files."
@@ -1903,24 +2586,119 @@ async function upgrade(opts) {
1903
2586
  initialValue: true
1904
2587
  });
1905
2588
  if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
1906
- cancel4("Upgrade cancelled.");
2589
+ cancel5("Upgrade cancelled.");
1907
2590
  return;
1908
2591
  }
1909
2592
  }
1910
- const artifacts = [];
1911
- for (const platform of config.agents) {
1912
- artifacts.push(...await CONFIGURATORS[platform](cwd));
2593
+ for (const { target, config } of pending) {
2594
+ const artifacts = await configurePlatformsForDir(
2595
+ target.dir,
2596
+ config.agents,
2597
+ target.boundary
2598
+ );
2599
+ await writeRuntimeScaffold(target.dir, config.agents, {
2600
+ supermodule: target.supermodule
2601
+ });
2602
+ await writeInstallManifest(target.dir, {
2603
+ harnessVersion: VERSION,
2604
+ agents: config.agents,
2605
+ artifacts
2606
+ });
2607
+ await ensureEasyCodingSessionsIgnored(target.dir);
2608
+ await updateHarnessVersion(target.configPath, VERSION);
2609
+ await updateSupermoduleConfig(target.configPath, target.supermodule);
2610
+ await setPendingInitSince(target.dir, VERSION);
2611
+ }
2612
+ if (parentTopologyRefresh) {
2613
+ await refreshSupermoduleParent(
2614
+ parentTopologyRefresh.target.dir,
2615
+ parentTopologyRefresh.agents,
2616
+ parentTopologyRefresh.submodulePaths
2617
+ );
1913
2618
  }
1914
- await writeRuntimeScaffold(cwd, config.agents);
1915
- await writeInstallManifest(cwd, {
1916
- harnessVersion: VERSION,
2619
+ for (const { target } of childTopologyRefreshes) {
2620
+ await updateSupermoduleConfig(target.configPath, target.supermodule);
2621
+ }
2622
+ outro5(
2623
+ chalk7.green(
2624
+ pending.length > 0 ? `easy-coding harness upgraded to ${VERSION}.` : "easy-coding supermodule topology refreshed."
2625
+ )
2626
+ );
2627
+ }
2628
+ async function resolvePendingUpgradeTargets(targets) {
2629
+ const pending = [];
2630
+ for (const target of targets) {
2631
+ if (!await pathExists(target.configPath)) {
2632
+ continue;
2633
+ }
2634
+ const config = await readConfigYaml(target.configPath);
2635
+ const installedVersion = String(config.harness_version ?? "");
2636
+ const hasAgents = Array.isArray(config.agents) && config.agents.length > 0;
2637
+ if (!installedVersion || !hasAgents) {
2638
+ throw new Error(
2639
+ `${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.`
2640
+ );
2641
+ }
2642
+ const relation = compareVersions(installedVersion, VERSION);
2643
+ if (relation === 1) {
2644
+ throw new Error(
2645
+ `${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
2646
+ );
2647
+ }
2648
+ if (relation === -1) {
2649
+ pending.push({ target, config });
2650
+ }
2651
+ }
2652
+ return pending;
2653
+ }
2654
+ async function resolveParentTopologyRefresh(targets, pending) {
2655
+ const parent = targets.find(
2656
+ (target) => target.label === "." && target.supermodule.role === "super-parent"
2657
+ );
2658
+ if (!parent || pending.some(({ target }) => target.label === ".")) {
2659
+ return null;
2660
+ }
2661
+ if (!await pathExists(parent.configPath)) {
2662
+ return null;
2663
+ }
2664
+ const config = await readConfigYaml(parent.configPath);
2665
+ if (!Array.isArray(config.agents) || config.agents.length === 0) {
2666
+ return null;
2667
+ }
2668
+ const submodulePaths = parent.supermodule.submodules ?? [];
2669
+ const configuredSubmodules = Array.isArray(config.supermodule?.submodules) ? config.supermodule.submodules : [];
2670
+ const needsRefresh = config.supermodule?.role !== "super-parent" || !sameStringList(configuredSubmodules, submodulePaths);
2671
+ if (!needsRefresh) {
2672
+ return null;
2673
+ }
2674
+ return {
2675
+ target: parent,
1917
2676
  agents: config.agents,
1918
- artifacts
1919
- });
1920
- await ensureEasyCodingSessionsIgnored(cwd);
1921
- await updateHarnessVersion(configPath, VERSION);
1922
- await setPendingInitSince(cwd, VERSION);
1923
- outro5(chalk7.green(`easy-coding harness upgraded to ${VERSION}.`));
2677
+ submodulePaths
2678
+ };
2679
+ }
2680
+ async function resolveChildTopologyRefreshes(targets, pending) {
2681
+ const pendingLabels = new Set(pending.map(({ target }) => target.label));
2682
+ const refreshes = [];
2683
+ for (const target of targets) {
2684
+ if (target.supermodule.role !== "submodule-child" || pendingLabels.has(target.label) || !await pathExists(target.configPath)) {
2685
+ continue;
2686
+ }
2687
+ const config = await readConfigYaml(target.configPath);
2688
+ if (config.supermodule?.role === "submodule-child" && config.supermodule.parent === target.supermodule.parent) {
2689
+ continue;
2690
+ }
2691
+ refreshes.push({ target });
2692
+ }
2693
+ return refreshes;
2694
+ }
2695
+ function sameStringList(left, right) {
2696
+ if (left.length !== right.length) {
2697
+ return false;
2698
+ }
2699
+ const sortedLeft = [...left].sort();
2700
+ const sortedRight = [...right].sort();
2701
+ return sortedLeft.every((value, index) => value === sortedRight[index]);
1924
2702
  }
1925
2703
 
1926
2704
  // src/cli.ts
@@ -1940,11 +2718,14 @@ function withErrorHandling(fn) {
1940
2718
  await checkForUpgrade(process.cwd());
1941
2719
  var program = new Command();
1942
2720
  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));
2721
+ program.command("init").description("Initialize easy-coding harness in current project").option("--agent <list>", "Comma-separated platforms: claude-code,codex,qoder").option(
2722
+ "--submodules <list>",
2723
+ "Comma-separated checked-out submodule paths or names to initialize"
2724
+ ).option("--no-submodules", "Initialize only the current directory when .gitmodules exists").option("-y, --yes", "Skip prompts, use defaults").action(withErrorHandling(init));
2725
+ 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
2726
  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
2727
  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
2728
  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));
2729
+ 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
2730
  program.parse();
1950
2731
  //# sourceMappingURL=cli.js.map