easy-coding-harness 0.1.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +109 -135
  3. package/dist/cli.js +1218 -282
  4. package/dist/cli.js.map +1 -1
  5. package/package.json +10 -1
  6. package/templates/claude/agents/ec-implementer.md +1 -1
  7. package/templates/claude/agents/ec-reviewer.md +1 -1
  8. package/templates/common/bundled-skills/ec-init/SKILL.md +174 -0
  9. package/templates/common/bundled-skills/ec-init/references/memory-migration.md +87 -0
  10. package/templates/common/bundled-skills/ec-meta/SKILL.md +5 -2
  11. package/templates/common/bundled-skills/ec-meta/references/customize-local/README.md +2 -1
  12. package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +14 -11
  13. package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +8 -4
  14. package/templates/common/skills/ec-analysis/SKILL.md +111 -130
  15. package/templates/common/skills/ec-brainstorming/SKILL.md +1 -1
  16. package/templates/common/skills/ec-git/SKILL.md +10 -10
  17. package/templates/common/skills/ec-implementing/SKILL.md +31 -24
  18. package/templates/common/skills/ec-memory/SKILL.md +41 -16
  19. package/templates/common/skills/ec-reviewing/SKILL.md +1 -1
  20. package/templates/common/skills/ec-task-close/SKILL.md +4 -3
  21. package/templates/common/skills/ec-task-management/SKILL.md +9 -24
  22. package/templates/common/skills/ec-verification/SKILL.md +10 -4
  23. package/templates/common/skills/ec-workflow/SKILL.md +48 -17
  24. package/templates/main-constraint/AGENTS.md.tpl +16 -3
  25. package/templates/main-constraint/CLAUDE.md.tpl +16 -3
  26. package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +75 -0
  27. package/templates/runtime/memory/long/BUSINESS.md +42 -9
  28. package/templates/runtime/memory/long/MEMORY.md +37 -2
  29. package/templates/runtime/memory/long/TECHNICAL.md +45 -1
  30. package/templates/runtime/templates/dev-spec-skeleton.md +80 -0
  31. package/templates/shared-hooks/easy_coding_state.py +610 -0
  32. package/templates/shared-hooks/easy_coding_status.py +72 -81
  33. package/templates/shared-hooks/inject-subagent-context.py +5 -12
  34. package/templates/shared-hooks/inject-workflow-state.py +2 -1
  35. package/templates/shared-hooks/session-start.py +2 -1
  36. package/templates/common/skills/ec-init/SKILL.md +0 -96
package/dist/cli.js CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import chalk6 from "chalk";
4
+ import chalk8 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/add-agent.ts
8
- import path8 from "path";
8
+ import path13 from "path";
9
9
  import { outro } from "@clack/prompts";
10
10
  import chalk2 from "chalk";
11
11
 
12
12
  // src/configurators/claude.ts
13
- import path4 from "path";
13
+ import path5 from "path";
14
14
 
15
15
  // src/types/platform.ts
16
16
  var pythonCmd = process.platform === "win32" ? "python" : "python3";
@@ -88,13 +88,15 @@ function isAgentPlatform(value) {
88
88
  return Object.hasOwn(PLATFORM_META, value);
89
89
  }
90
90
 
91
- // src/configurators/shared.ts
92
- import { readdir, stat as stat2 } from "fs/promises";
93
- import path3 from "path";
91
+ // src/utils/install-manifest.ts
92
+ import { createHash } from "crypto";
93
+ import { readFile as readFile2 } from "fs/promises";
94
+ import path2 from "path";
94
95
 
95
96
  // src/constants/paths.ts
96
97
  var EASY_CODING_DIR = ".easy-coding";
97
98
  var CONFIG_FILE = "config.yaml";
99
+ var INSTALL_MANIFEST_FILE = "install-manifest.json";
98
100
  var SESSIONS_DIR = "sessions";
99
101
  var TASKS_DIR = "tasks";
100
102
  var PROJECT_INIT_TASK_ID = "project-init";
@@ -102,6 +104,7 @@ var MEMORY_DIR = "memory";
102
104
  var SPEC_DIR = "spec";
103
105
  var MAIN_SPEC_DIR = "main";
104
106
  var DEV_SPEC_DIR = "dev";
107
+ var TEMPLATES_DIR = "templates";
105
108
  var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
106
109
  var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
107
110
  var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness generated \u2550\u2550\u2550 -->";
@@ -143,6 +146,196 @@ async function isDirectory(filePath) {
143
146
  }
144
147
  }
145
148
 
149
+ // src/utils/install-manifest.ts
150
+ function fileArtifact(filePath, kind, platform) {
151
+ return { type: "file", kind, filePath, platform };
152
+ }
153
+ function constraintRegionArtifact(filePath, platform) {
154
+ return { type: "constraint-region", filePath, platform };
155
+ }
156
+ async function hookRegistrationArtifacts(configPath, platform) {
157
+ const content = await readTextIfExists(configPath);
158
+ if (content === null) {
159
+ return [];
160
+ }
161
+ let parsed;
162
+ try {
163
+ parsed = JSON.parse(content);
164
+ } catch {
165
+ return [];
166
+ }
167
+ return collectHookCommands(parsed.hooks).map((command) => ({
168
+ type: "hook-registration",
169
+ configPath,
170
+ command,
171
+ hookPath: extractHookPathFromCommand(command),
172
+ platform
173
+ }));
174
+ }
175
+ async function writeInstallManifest(cwd, params) {
176
+ const existing = params.mode === "merge" ? await readInstallManifest(cwd) : null;
177
+ const files = /* @__PURE__ */ new Map();
178
+ const hookRegistrations = /* @__PURE__ */ new Map();
179
+ const constraintRegions = /* @__PURE__ */ new Map();
180
+ if (existing) {
181
+ for (const file of existing.files) {
182
+ files.set(file.path, file);
183
+ }
184
+ for (const registration of existing.hook_registrations) {
185
+ hookRegistrations.set(
186
+ `${registration.config_path}\0${normalizeCommand(registration.command)}`,
187
+ registration
188
+ );
189
+ }
190
+ for (const region of existing.constraint_regions) {
191
+ constraintRegions.set(region.path, region);
192
+ }
193
+ }
194
+ for (const artifact of params.artifacts) {
195
+ if (artifact.type === "file") {
196
+ if (!await pathExists(artifact.filePath)) {
197
+ continue;
198
+ }
199
+ const relPath2 = toProjectPath(cwd, artifact.filePath);
200
+ files.set(relPath2, {
201
+ path: relPath2,
202
+ kind: artifact.kind,
203
+ platform: artifact.platform,
204
+ sha256: await sha256File(artifact.filePath)
205
+ });
206
+ continue;
207
+ }
208
+ if (artifact.type === "hook-registration") {
209
+ const configPath = toProjectPath(cwd, artifact.configPath);
210
+ const registration = {
211
+ config_path: configPath,
212
+ command: artifact.command,
213
+ hook_path: artifact.hookPath,
214
+ platform: artifact.platform
215
+ };
216
+ hookRegistrations.set(`${configPath}\0${normalizeCommand(artifact.command)}`, registration);
217
+ continue;
218
+ }
219
+ const relPath = toProjectPath(cwd, artifact.filePath);
220
+ constraintRegions.set(relPath, {
221
+ path: relPath,
222
+ platform: artifact.platform
223
+ });
224
+ }
225
+ const manifest = {
226
+ schema_version: 1,
227
+ harness_version: params.harnessVersion,
228
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
229
+ agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...params.agents])],
230
+ files: [...files.values()].sort(byPath),
231
+ hook_registrations: [...hookRegistrations.values()].sort(
232
+ (a, b) => `${a.config_path}\0${a.command}`.localeCompare(`${b.config_path}\0${b.command}`)
233
+ ),
234
+ constraint_regions: [...constraintRegions.values()].sort(byPath)
235
+ };
236
+ await writeTextFile(
237
+ path2.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
238
+ JSON.stringify(manifest, null, 2)
239
+ );
240
+ }
241
+ async function readInstallManifest(cwd) {
242
+ const manifestPath2 = path2.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
243
+ const content = await readTextIfExists(manifestPath2);
244
+ if (content === null) {
245
+ return null;
246
+ }
247
+ try {
248
+ const parsed = JSON.parse(content);
249
+ if (parsed.schema_version !== 1 || !Array.isArray(parsed.files) || !Array.isArray(parsed.hook_registrations) || !Array.isArray(parsed.constraint_regions)) {
250
+ return null;
251
+ }
252
+ const agents = Array.isArray(parsed.agents) ? parsed.agents.filter((agent) => isAgentPlatform(agent)) : [];
253
+ return { ...parsed, agents };
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+ async function manifestFileMatches(filePath, sha256) {
259
+ if (!await pathExists(filePath)) {
260
+ return false;
261
+ }
262
+ return await sha256File(filePath) === sha256;
263
+ }
264
+ function manifestPath(cwd, projectPath) {
265
+ return resolveProjectPath(cwd, projectPath);
266
+ }
267
+ function toProjectPath(cwd, filePath) {
268
+ const root = path2.resolve(cwd);
269
+ const resolved = path2.resolve(filePath);
270
+ assertPathInsideProject(root, resolved, filePath);
271
+ return path2.relative(root, resolved).split(path2.sep).join("/");
272
+ }
273
+ function normalizeCommand(command) {
274
+ return command.replace(/\\/g, "/").trim().replace(/\s+/g, " ");
275
+ }
276
+ async function sha256File(filePath) {
277
+ const content = await readFile2(filePath);
278
+ return createHash("sha256").update(content).digest("hex");
279
+ }
280
+ function resolveProjectPath(cwd, projectPath) {
281
+ const normalized = projectPath.replace(/\\/g, "/");
282
+ 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 === "..")) {
284
+ throw new Error(`Unsafe install manifest path: ${projectPath}`);
285
+ }
286
+ const root = path2.resolve(cwd);
287
+ const resolved = path2.resolve(root, normalized);
288
+ assertPathInsideProject(root, resolved, projectPath);
289
+ return resolved;
290
+ }
291
+ function assertPathInsideProject(root, resolvedPath, sourcePath) {
292
+ const relative = path2.relative(root, resolvedPath);
293
+ if (!relative || relative.startsWith("..") || path2.isAbsolute(relative)) {
294
+ throw new Error(`Unsafe install manifest path: ${sourcePath}`);
295
+ }
296
+ }
297
+ function collectHookCommands(hooks) {
298
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
299
+ return [];
300
+ }
301
+ const commands = [];
302
+ for (const value of Object.values(hooks)) {
303
+ if (!Array.isArray(value)) {
304
+ continue;
305
+ }
306
+ for (const group of value) {
307
+ if (!group || typeof group !== "object") {
308
+ continue;
309
+ }
310
+ const hookItems = group.hooks;
311
+ if (!Array.isArray(hookItems)) {
312
+ continue;
313
+ }
314
+ for (const hook of hookItems) {
315
+ if (!hook || typeof hook !== "object") {
316
+ continue;
317
+ }
318
+ const command = hook.command;
319
+ if (typeof command === "string") {
320
+ commands.push(command);
321
+ }
322
+ }
323
+ }
324
+ }
325
+ return commands;
326
+ }
327
+ function extractHookPathFromCommand(command) {
328
+ const match = normalizeCommand(command).match(/(?:^|\s)(\S+\/hooks\/\S+\.py)(?:\s|$)/);
329
+ return match?.[1] ?? null;
330
+ }
331
+ function byPath(a, b) {
332
+ return a.path.localeCompare(b.path);
333
+ }
334
+
335
+ // src/configurators/shared.ts
336
+ import { readdir, stat as stat2 } from "fs/promises";
337
+ import path4 from "path";
338
+
146
339
  // src/utils/marked-region.ts
147
340
  var MarkedRegionError = class extends Error {
148
341
  constructor(message) {
@@ -170,18 +363,27 @@ ${existing.trimStart()}`.trimEnd();
170
363
  }
171
364
  return existing.replace(currentRegion, generatedRegion.trim());
172
365
  }
366
+ function removeMarkedRegion(content) {
367
+ const region = extractMarkedRegion(content);
368
+ if (!region) {
369
+ return content;
370
+ }
371
+ const remaining = content.replace(region, "").replace(/\n{3,}/g, "\n\n").trim();
372
+ return remaining ? `${remaining}
373
+ ` : "";
374
+ }
173
375
 
174
376
  // src/utils/template-paths.ts
175
377
  import { existsSync } from "fs";
176
- import path2 from "path";
378
+ import path3 from "path";
177
379
  import { fileURLToPath } from "url";
178
380
  function getTemplateRoot() {
179
- const here = path2.dirname(fileURLToPath(import.meta.url));
381
+ const here = path3.dirname(fileURLToPath(import.meta.url));
180
382
  const candidates = [
181
- path2.resolve(here, "../templates"),
182
- path2.resolve(here, "../../templates"),
183
- path2.resolve(process.cwd(), "src/templates"),
184
- path2.resolve(process.cwd(), "templates")
383
+ path3.resolve(here, "../templates"),
384
+ path3.resolve(here, "../../templates"),
385
+ path3.resolve(process.cwd(), "src/templates"),
386
+ path3.resolve(process.cwd(), "templates")
185
387
  ];
186
388
  const found = candidates.find((candidate) => existsSync(candidate));
187
389
  if (!found) {
@@ -190,7 +392,7 @@ function getTemplateRoot() {
190
392
  return found;
191
393
  }
192
394
  function getTemplatePath(...segments) {
193
- return path2.join(getTemplateRoot(), ...segments);
395
+ return path3.join(getTemplateRoot(), ...segments);
194
396
  }
195
397
 
196
398
  // src/configurators/shared.ts
@@ -217,11 +419,11 @@ async function resolveSkills(ctx) {
217
419
  const entries = await readdir(skillsRoot);
218
420
  const skills = [];
219
421
  for (const entry of entries.sort()) {
220
- const skillDir = path3.join(skillsRoot, entry);
422
+ const skillDir = path4.join(skillsRoot, entry);
221
423
  if (!await isDirectory(skillDir)) {
222
424
  continue;
223
425
  }
224
- const content = await readTextFile(path3.join(skillDir, "SKILL.md"));
426
+ const content = await readTextFile(path4.join(skillDir, "SKILL.md"));
225
427
  skills.push({
226
428
  name: entry,
227
429
  content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
@@ -237,7 +439,7 @@ async function resolveBundledSkills(ctx) {
237
439
  const entries = await readdir(bundledRoot);
238
440
  const bundled = [];
239
441
  for (const entry of entries.sort()) {
240
- const sourceDir = path3.join(bundledRoot, entry);
442
+ const sourceDir = path4.join(bundledRoot, entry);
241
443
  if (await isDirectory(sourceDir)) {
242
444
  bundled.push({ name: entry, sourceDir, context: ctx });
243
445
  }
@@ -246,23 +448,30 @@ async function resolveBundledSkills(ctx) {
246
448
  }
247
449
  async function writeSkills(dir, skills, bundled) {
248
450
  await ensureDir(dir);
451
+ const written = [];
249
452
  for (const skill of skills) {
250
- await writeTextFile(path3.join(dir, skill.name, "SKILL.md"), skill.content);
453
+ const destination = path4.join(dir, skill.name, "SKILL.md");
454
+ await writeTextFile(destination, skill.content);
455
+ written.push(destination);
251
456
  }
252
457
  for (const skill of bundled) {
253
- await copyTemplateDirectory(skill.sourceDir, path3.join(dir, skill.name), skill.context);
458
+ written.push(
459
+ ...await copyTemplateDirectory(skill.sourceDir, path4.join(dir, skill.name), skill.context)
460
+ );
254
461
  }
462
+ return written;
255
463
  }
256
464
  async function writeSharedHooks(dir, platform, opts = {}) {
257
465
  const hooksRoot = getTemplatePath("shared-hooks");
258
466
  const ctx = PLATFORM_META[platform].templateContext;
259
467
  const entries = await readdir(hooksRoot);
260
468
  await ensureDir(dir);
469
+ const written = [];
261
470
  for (const entry of entries.sort()) {
262
471
  if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
263
472
  continue;
264
473
  }
265
- const sourcePath = path3.join(hooksRoot, entry);
474
+ const sourcePath = path4.join(hooksRoot, entry);
266
475
  if ((await stat2(sourcePath)).isDirectory()) {
267
476
  continue;
268
477
  }
@@ -271,14 +480,16 @@ async function writeSharedHooks(dir, platform, opts = {}) {
271
480
  ctx,
272
481
  `shared-hooks/${entry}`
273
482
  );
274
- const destination = path3.join(dir, entry);
483
+ const destination = path4.join(dir, entry);
275
484
  await writeTextFile(destination, content);
276
485
  await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
486
+ written.push(destination);
277
487
  }
488
+ return written;
278
489
  }
279
490
  async function copyPlatformTemplates(platformTemplateDir, destination, skipDirs, ctx) {
280
491
  const source = getTemplatePath(platformTemplateDir);
281
- await copyTemplateDirectory(source, destination, ctx, new Set(skipDirs));
492
+ return copyTemplateDirectory(source, destination, ctx, new Set(skipDirs));
282
493
  }
283
494
  async function writeMainConstraint(cwd, platform) {
284
495
  const meta = PLATFORM_META[platform];
@@ -289,7 +500,7 @@ async function writeMainConstraint(cwd, platform) {
289
500
  if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
290
501
  throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
291
502
  }
292
- const destination = path3.join(cwd, meta.mainConstraint);
503
+ const destination = path4.join(cwd, meta.mainConstraint);
293
504
  const current = await readTextIfExists(destination);
294
505
  if (current) {
295
506
  const markerContent = extractMarkedRegion(generated);
@@ -300,23 +511,27 @@ async function writeMainConstraint(cwd, platform) {
300
511
  } else {
301
512
  await writeTextFile(destination, generated);
302
513
  }
514
+ return destination;
303
515
  }
304
516
  async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
305
517
  await ensureDir(destination);
518
+ const written = [];
306
519
  for (const entry of (await readdir(source)).sort()) {
307
520
  if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
308
521
  continue;
309
522
  }
310
- const sourcePath = path3.join(source, entry);
311
- const destinationPath = path3.join(destination, stripTemplateExtension(entry));
523
+ const sourcePath = path4.join(source, entry);
524
+ const destinationPath = path4.join(destination, stripTemplateExtension(entry));
312
525
  const sourceStat = await stat2(sourcePath);
313
526
  if (sourceStat.isDirectory()) {
314
- await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs);
527
+ written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
315
528
  continue;
316
529
  }
317
530
  const content = resolvePlaceholders(await readTextFile(sourcePath), ctx, sourcePath);
318
531
  await writeTextFile(destinationPath, content);
532
+ written.push(destinationPath);
319
533
  }
534
+ return written;
320
535
  }
321
536
  function shouldSkipTemplateEntry(entry) {
322
537
  return entry === ".DS_Store" || entry === "__pycache__" || entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".map");
@@ -330,69 +545,194 @@ async function configureClaude(cwd) {
330
545
  const platform = "claude-code";
331
546
  const meta = PLATFORM_META[platform];
332
547
  const ctx = meta.templateContext;
333
- const dest = path4.join(cwd, ".claude");
334
- await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
335
- await writeSharedHooks(path4.join(dest, "hooks"), platform);
336
- await writeSkills(
337
- path4.join(cwd, meta.skillsDir),
338
- await resolveSkills(ctx),
339
- await resolveBundledSkills(ctx)
548
+ const dest = path5.join(cwd, ".claude");
549
+ const hookConfigPath = path5.join(cwd, meta.hookConfigFile);
550
+ const artifacts = [];
551
+ const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
552
+ artifacts.push(
553
+ ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
554
+ (filePath) => fileArtifact(
555
+ filePath,
556
+ filePath.startsWith(path5.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
557
+ platform
558
+ )
559
+ )
560
+ );
561
+ artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
562
+ artifacts.push(
563
+ ...(await writeSharedHooks(path5.join(dest, "hooks"), platform)).map(
564
+ (filePath) => fileArtifact(filePath, "hook", platform)
565
+ )
566
+ );
567
+ artifacts.push(
568
+ ...(await writeSkills(
569
+ path5.join(cwd, meta.skillsDir),
570
+ await resolveSkills(ctx),
571
+ await resolveBundledSkills(ctx)
572
+ )).map((filePath) => fileArtifact(filePath, "skill", platform))
340
573
  );
341
- await writeMainConstraint(cwd, platform);
574
+ artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
575
+ return artifacts;
342
576
  }
343
577
 
344
578
  // src/configurators/codex.ts
345
- import path5 from "path";
579
+ import path6 from "path";
346
580
  async function configureCodex(cwd) {
347
581
  const platform = "codex";
348
582
  const meta = PLATFORM_META[platform];
349
583
  const ctx = meta.templateContext;
350
- await writeSkills(
351
- path5.join(cwd, meta.skillsDir),
352
- await resolveSkills(ctx),
353
- await resolveBundledSkills(ctx)
584
+ const hookConfigPath = path6.join(cwd, meta.hookConfigFile);
585
+ const artifacts = [];
586
+ artifacts.push(
587
+ ...(await writeSkills(
588
+ path6.join(cwd, meta.skillsDir),
589
+ await resolveSkills(ctx),
590
+ await resolveBundledSkills(ctx)
591
+ )).map((filePath) => fileArtifact(filePath, "skill", platform))
354
592
  );
355
- await writeSharedHooks(path5.join(cwd, meta.hooksDir), platform, { skipSubagentContext: true });
356
- await copyPlatformTemplates("codex", path5.join(cwd, ".codex"), ["hooks"], ctx);
357
- await writeMainConstraint(cwd, platform);
593
+ artifacts.push(
594
+ ...(await writeSharedHooks(path6.join(cwd, meta.hooksDir), platform, {
595
+ skipSubagentContext: true
596
+ })).map((filePath) => fileArtifact(filePath, "hook", platform))
597
+ );
598
+ const platformFiles = await copyPlatformTemplates(
599
+ "codex",
600
+ path6.join(cwd, ".codex"),
601
+ ["hooks"],
602
+ ctx
603
+ );
604
+ artifacts.push(
605
+ ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
606
+ (filePath) => fileArtifact(
607
+ filePath,
608
+ filePath.startsWith(path6.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
609
+ platform
610
+ )
611
+ )
612
+ );
613
+ artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
614
+ artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
615
+ return artifacts;
358
616
  }
359
617
 
360
618
  // src/configurators/qoder.ts
361
- import { existsSync as existsSync2, readdirSync } from "fs";
362
- import path6 from "path";
363
- function claudeSkillsExist(cwd) {
364
- const claudeSkillsDir = path6.join(cwd, ".claude", "skills");
365
- if (!existsSync2(claudeSkillsDir)) return false;
619
+ import { readdir as readdir2 } from "fs/promises";
620
+ import path8 from "path";
621
+
622
+ // src/utils/platform-paths.ts
623
+ import { existsSync as existsSync2 } from "fs";
624
+ import path7 from "path";
625
+ function detectQoderCnVariant(cwd) {
626
+ if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
627
+ return true;
628
+ }
629
+ return existsSync2(path7.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
630
+ }
631
+ function resolvePlatformMeta(cwd, platform) {
632
+ const meta = PLATFORM_META[platform];
633
+ if (platform !== "qoder" || !detectQoderCnVariant(cwd)) {
634
+ return meta;
635
+ }
636
+ return resolveQoderMetaForBaseDir(meta.cnVariant ?? ".qodercn");
637
+ }
638
+ function resolveQoderVariantMetas() {
639
+ const standardDir = PLATFORM_META.qoder.templateContext.platform_config_dir;
640
+ const cnDir = PLATFORM_META.qoder.cnVariant ?? ".qodercn";
641
+ return [.../* @__PURE__ */ new Set([standardDir, cnDir])].map(resolveQoderMetaForBaseDir);
642
+ }
643
+ function resolveQoderMetaForBaseDir(baseDir) {
644
+ const meta = PLATFORM_META.qoder;
645
+ return {
646
+ ...meta,
647
+ skillsDir: `${baseDir}/skills`,
648
+ hooksDir: `${baseDir}/hooks`,
649
+ hookConfigFile: `${baseDir}/settings.json`,
650
+ agentsDir: `${baseDir}/agents`,
651
+ templateContext: {
652
+ ...meta.templateContext,
653
+ platform_config_dir: baseDir
654
+ }
655
+ };
656
+ }
657
+
658
+ // src/configurators/qoder.ts
659
+ async function claudeHarnessSkillsExist(cwd) {
660
+ return pathExists(path8.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
661
+ }
662
+ async function listFilesRecursive(dir) {
663
+ let entries;
366
664
  try {
367
- return readdirSync(claudeSkillsDir).some((d) => d.startsWith("ec-"));
368
- } catch {
369
- return false;
665
+ entries = await readdir2(dir, { withFileTypes: true });
666
+ } catch (error) {
667
+ if (error.code === "ENOENT") {
668
+ return [];
669
+ }
670
+ throw error;
370
671
  }
672
+ const files = [];
673
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
674
+ const entryPath = path8.join(dir, entry.name);
675
+ if (entry.isDirectory()) {
676
+ files.push(...await listFilesRecursive(entryPath));
677
+ continue;
678
+ }
679
+ if (entry.isFile()) {
680
+ files.push(entryPath);
681
+ }
682
+ }
683
+ return files;
684
+ }
685
+ async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
686
+ const artifacts = [];
687
+ for (const skillName of skillNames) {
688
+ for (const filePath of await listFilesRecursive(path8.join(skillsDir, skillName))) {
689
+ artifacts.push(fileArtifact(filePath, "skill", platform));
690
+ }
691
+ }
692
+ return artifacts;
371
693
  }
372
694
  async function configureQoder(cwd) {
373
695
  const platform = "qoder";
374
- const baseDir = detectQoderCnVariant(cwd) ? ".qodercn" : ".qoder";
375
- const ctx = {
376
- ...PLATFORM_META[platform].templateContext,
377
- platform_config_dir: baseDir
378
- };
379
- const dest = path6.join(cwd, baseDir);
380
- await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
381
- await writeSharedHooks(path6.join(dest, "hooks"), platform);
382
- if (!claudeSkillsExist(cwd)) {
383
- await writeSkills(
384
- path6.join(dest, "skills"),
385
- await resolveSkills(ctx),
386
- await resolveBundledSkills(ctx)
696
+ const meta = resolvePlatformMeta(cwd, platform);
697
+ const ctx = meta.templateContext;
698
+ const dest = path8.join(cwd, ctx.platform_config_dir);
699
+ const hookConfigPath = path8.join(cwd, meta.hookConfigFile);
700
+ const artifacts = [];
701
+ const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
702
+ artifacts.push(
703
+ ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
704
+ (filePath) => fileArtifact(
705
+ filePath,
706
+ filePath.startsWith(path8.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
707
+ platform
708
+ )
709
+ )
710
+ );
711
+ artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
712
+ artifacts.push(
713
+ ...(await writeSharedHooks(path8.join(dest, "hooks"), platform)).map(
714
+ (filePath) => fileArtifact(filePath, "hook", platform)
715
+ )
716
+ );
717
+ const skills = await resolveSkills(ctx);
718
+ const bundledSkills = await resolveBundledSkills(ctx);
719
+ if (!await claudeHarnessSkillsExist(cwd)) {
720
+ artifacts.push(
721
+ ...(await writeSkills(path8.join(dest, "skills"), skills, bundledSkills)).map(
722
+ (filePath) => fileArtifact(filePath, "skill", platform)
723
+ )
724
+ );
725
+ } else {
726
+ artifacts.push(
727
+ ...await existingManagedSkillArtifacts(
728
+ path8.join(dest, "skills"),
729
+ [...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
730
+ platform
731
+ )
387
732
  );
388
733
  }
389
- await writeMainConstraint(cwd, platform);
390
- }
391
- function detectQoderCnVariant(cwd) {
392
- if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
393
- return true;
394
- }
395
- return existsSync2(path6.join(cwd, ".qodercn"));
734
+ artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
735
+ return artifacts;
396
736
  }
397
737
 
398
738
  // src/configurators/index.ts
@@ -402,23 +742,19 @@ var CONFIGURATORS = {
402
742
  qoder: configureQoder
403
743
  };
404
744
 
405
- // src/ui/banner.ts
406
- import chalk from "chalk";
407
- import figlet from "figlet";
408
-
409
745
  // src/constants/version.ts
410
746
  import { readFileSync } from "fs";
411
- import path7 from "path";
747
+ import path9 from "path";
412
748
  import { fileURLToPath as fileURLToPath2 } from "url";
413
749
  var packageMetadata = readPackageMetadata();
414
750
  var PACKAGE_NAME = packageMetadata.name;
415
751
  var VERSION = packageMetadata.version;
416
752
  function readPackageMetadata() {
417
- const here = path7.dirname(fileURLToPath2(import.meta.url));
753
+ const here = path9.dirname(fileURLToPath2(import.meta.url));
418
754
  const candidates = [
419
- path7.resolve(here, "../../package.json"),
420
- path7.resolve(here, "../package.json"),
421
- path7.resolve(process.cwd(), "package.json")
755
+ path9.resolve(here, "../../package.json"),
756
+ path9.resolve(here, "../package.json"),
757
+ path9.resolve(process.cwd(), "package.json")
422
758
  ];
423
759
  for (const candidate of candidates) {
424
760
  try {
@@ -433,6 +769,8 @@ function readPackageMetadata() {
433
769
  }
434
770
 
435
771
  // src/ui/banner.ts
772
+ import chalk from "chalk";
773
+ import figlet from "figlet";
436
774
  var WIDE_TERMINAL_WIDTH = 88;
437
775
  var MEDIUM_TERMINAL_WIDTH = 72;
438
776
  function renderBanner() {
@@ -457,7 +795,7 @@ function colorizeBanner(art) {
457
795
  }
458
796
 
459
797
  // src/utils/config-yaml.ts
460
- import { readFile as readFile2 } from "fs/promises";
798
+ import { readFile as readFile3 } from "fs/promises";
461
799
  import YAML, { isScalar, isSeq, parseDocument } from "yaml";
462
800
  function createDefaultConfig(params) {
463
801
  return {
@@ -465,18 +803,13 @@ function createDefaultConfig(params) {
465
803
  harness_version: params.harnessVersion,
466
804
  agents: params.agents,
467
805
  project: {
468
- name: params.projectName,
469
- mode: "auto"
806
+ name: params.projectName
470
807
  },
471
808
  memory: {
472
809
  short_term_max: 10,
473
810
  short_term_keep: 5,
474
811
  schema_version: 2
475
812
  },
476
- test: {
477
- framework: "auto",
478
- command: ""
479
- },
480
813
  tasks: {
481
814
  auto_archive_days: 30
482
815
  },
@@ -493,11 +826,11 @@ async function writeConfigYaml(filePath, config) {
493
826
  await writeTextFile(filePath, stringifyConfig(config));
494
827
  }
495
828
  async function readConfigYaml(filePath) {
496
- const content = await readFile2(filePath, "utf8");
829
+ const content = await readFile3(filePath, "utf8");
497
830
  return YAML.parse(content);
498
831
  }
499
832
  async function updateConfigYaml(filePath, updater) {
500
- const content = await readFile2(filePath, "utf8");
833
+ const content = await readFile3(filePath, "utf8");
501
834
  const document = parseDocument(content);
502
835
  const config = document.toJSON();
503
836
  updater(config);
@@ -519,87 +852,10 @@ async function updateHarnessVersion(filePath, version) {
519
852
  });
520
853
  }
521
854
 
522
- // src/commands/platforms.ts
523
- import { cancel, confirm, multiselect } from "@clack/prompts";
524
- function parseAgentList(agentList) {
525
- const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
526
- if (values.length === 0) {
527
- throw new Error("No agent platform specified.");
528
- }
529
- const invalid = values.filter((value) => !isAgentPlatform(value));
530
- if (invalid.length > 0) {
531
- throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
532
- }
533
- return [...new Set(values)];
534
- }
535
- async function resolvePlatforms(opts, defaults = ["claude-code"]) {
536
- if (opts.agent) {
537
- return parseAgentList(opts.agent);
538
- }
539
- if (opts.yes) {
540
- return defaults;
541
- }
542
- let selectedDefaults = defaults;
543
- while (true) {
544
- const result = await multiselect({
545
- message: "Select agent platforms (Space to toggle, Enter to review)",
546
- options: AGENT_PLATFORMS.map((platform) => ({
547
- label: PLATFORM_META[platform].label,
548
- value: platform
549
- })),
550
- initialValues: selectedDefaults,
551
- required: true
552
- });
553
- if (typeof result === "symbol") {
554
- cancel("Platform selection cancelled.");
555
- process.exit(1);
556
- }
557
- const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
558
- const shouldInstall = await confirm({
559
- message: `Install Easy Coding for: ${labels}?`,
560
- initialValue: true
561
- });
562
- if (typeof shouldInstall === "symbol") {
563
- cancel("Platform selection cancelled.");
564
- process.exit(1);
565
- }
566
- if (shouldInstall) {
567
- return result;
568
- }
569
- selectedDefaults = result;
570
- }
571
- }
572
-
573
- // src/commands/add-agent.ts
574
- async function addAgent(opts) {
575
- renderBanner();
576
- const cwd = process.cwd();
577
- const configPath = path8.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
578
- if (!await pathExists(configPath)) {
579
- throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
580
- }
581
- const config = await readConfigYaml(configPath);
582
- const platforms = await resolvePlatforms(opts, ["claude-code"]);
583
- const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
584
- if (toInstall.length === 0) {
585
- outro(chalk2.yellow("All selected agent platforms are already installed."));
586
- return;
587
- }
588
- for (const platform of toInstall) {
589
- await CONFIGURATORS[platform](cwd);
590
- }
591
- await addAgentsToConfig(configPath, toInstall);
592
- outro(chalk2.green(`Added agent platforms: ${toInstall.join(", ")}`));
593
- }
594
-
595
- // src/commands/init.ts
596
- import { note, outro as outro2 } from "@clack/prompts";
597
- import chalk3 from "chalk";
598
-
599
855
  // src/utils/gitignore.ts
600
- import path9 from "path";
856
+ import path10 from "path";
601
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") {
602
- const gitignorePath = path9.join(cwd, ".gitignore");
858
+ const gitignorePath = path10.join(cwd, ".gitignore");
603
859
  const current = await readTextIfExists(gitignorePath) ?? "";
604
860
  const lines = current.split(/\r?\n/).map((line) => line.trim());
605
861
  if (lines.includes(entry)) {
@@ -614,90 +870,32 @@ async function ensureEasyCodingSessionsIgnored(cwd) {
614
870
  return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
615
871
  }
616
872
 
617
- // src/utils/install-state.ts
618
- import { readdir as readdir2 } from "fs/promises";
619
- import path10 from "path";
620
- var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
621
- async function detectEasyCodingInstallState(cwd) {
622
- const easyCodingDir = path10.join(cwd, EASY_CODING_DIR);
623
- if (!await pathExists(easyCodingDir)) {
624
- return { kind: "fresh", easyCodingDir };
625
- }
626
- const configPath = path10.join(easyCodingDir, CONFIG_FILE);
627
- if (await pathExists(configPath)) {
628
- return { kind: "installed", easyCodingDir, configPath };
629
- }
630
- const legacyAssets = await detectLegacyAssets(easyCodingDir);
631
- if (legacyAssets.length === 0) {
632
- return { kind: "unknown", easyCodingDir };
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
+ );
633
885
  }
634
- return {
635
- kind: "legacy",
636
- easyCodingDir,
637
- legacyAssets,
638
- missingHarnessFiles: [relativeConfigPath(), relativeProjectInitTaskPath()]
639
- };
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);
640
892
  }
641
- async function detectLegacyAssets(easyCodingDir) {
642
- const assets = [];
643
- for (const file of LEGACY_ROOT_FILES) {
644
- if (await pathExists(path10.join(easyCodingDir, file))) {
645
- assets.push(relativeEasyCodingPath(file));
646
- }
647
- }
648
- if (await pathExists(path10.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
649
- assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
650
- }
651
- const shortMemoryFiles = await listMarkdownFiles(path10.join(easyCodingDir, "memory", "short"));
652
- assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
653
- for (const dir of ["spec", "prototype"]) {
654
- if (await hasAnyDirectoryEntry(path10.join(easyCodingDir, dir))) {
655
- assets.push(relativeEasyCodingPath(dir));
656
- }
657
- }
658
- return assets.sort();
659
- }
660
- async function listMarkdownFiles(dir) {
661
- if (!await isDirectory(dir)) {
662
- return [];
663
- }
664
- const entries = await readdir2(dir, { withFileTypes: true });
665
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
666
- }
667
- async function hasAnyDirectoryEntry(dir) {
668
- if (!await isDirectory(dir)) {
669
- return false;
670
- }
671
- return (await readdir2(dir)).length > 0;
672
- }
673
- function relativeEasyCodingPath(...segments) {
674
- return path10.posix.join(EASY_CODING_DIR, ...segments);
675
- }
676
- function relativeConfigPath() {
677
- return path10.posix.join(EASY_CODING_DIR, CONFIG_FILE);
678
- }
679
- function relativeProjectInitTaskPath() {
680
- return path10.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
681
- }
682
-
683
- // src/utils/runtime-scaffold.ts
684
- import path11 from "path";
685
- async function writeRuntimeScaffold(cwd, agents) {
686
- const easyCodingDir = path11.join(cwd, EASY_CODING_DIR);
687
- await ensureDir(easyCodingDir);
688
- const configPath = path11.join(easyCodingDir, CONFIG_FILE);
689
- if (!await pathExists(configPath)) {
690
- const projectName = path11.basename(cwd);
691
- await writeConfigYaml(
692
- configPath,
693
- createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
694
- );
695
- }
696
- await ensureDir(path11.join(easyCodingDir, "tasks"));
697
- await ensureDir(path11.join(easyCodingDir, SESSIONS_DIR));
698
- await ensureDir(path11.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
699
- await ensureDir(path11.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
700
- await writeMemoryScaffold(easyCodingDir);
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));
701
899
  }
702
900
  async function writeMemoryScaffold(easyCodingDir) {
703
901
  const memoryDir = path11.join(easyCodingDir, MEMORY_DIR);
@@ -711,6 +909,11 @@ async function writeMemoryScaffold(easyCodingDir) {
711
909
  const templatePath = getTemplatePath("runtime", "memory", "long", file);
712
910
  await writeTextFile(destination, await readTextFile(templatePath));
713
911
  }
912
+ const shortTemplateDest = path11.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
913
+ if (!await pathExists(shortTemplateDest)) {
914
+ const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
915
+ await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
916
+ }
714
917
  }
715
918
 
716
919
  // src/utils/task-json.ts
@@ -756,6 +959,14 @@ async function writeProjectInitTask(cwd, agents, options = {}) {
756
959
  })
757
960
  );
758
961
  }
962
+ async function setPendingInitSince(cwd, version) {
963
+ const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
964
+ if (!await pathExists(filePath)) return;
965
+ const task = await readTaskJson(filePath);
966
+ if (task.status !== "COMPLETE") return;
967
+ task.pending_init_since = version;
968
+ await writeTaskJson(filePath, task);
969
+ }
759
970
  async function listTasks(cwd) {
760
971
  const tasksDir = path12.join(cwd, EASY_CODING_DIR, TASKS_DIR);
761
972
  if (!await pathExists(tasksDir)) {
@@ -788,6 +999,672 @@ function isActiveTask(task) {
788
999
  return task.status !== "COMPLETE" && task.status !== "CLOSED";
789
1000
  }
790
1001
 
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(", ")}`);
1012
+ }
1013
+ return [...new Set(values)];
1014
+ }
1015
+ async function resolvePlatforms(opts, defaults = ["claude-code"]) {
1016
+ if (opts.agent) {
1017
+ return parseAgentList(opts.agent);
1018
+ }
1019
+ if (opts.yes) {
1020
+ return defaults;
1021
+ }
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);
1036
+ }
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);
1045
+ }
1046
+ if (shouldInstall) {
1047
+ return result;
1048
+ }
1049
+ selectedDefaults = result;
1050
+ }
1051
+ }
1052
+
1053
+ // src/commands/add-agent.ts
1054
+ async function addAgent(opts) {
1055
+ renderBanner();
1056
+ const cwd = process.cwd();
1057
+ const configPath = path13.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1058
+ if (!await pathExists(configPath)) {
1059
+ throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
1060
+ }
1061
+ const config = await readConfigYaml(configPath);
1062
+ const platforms = await resolvePlatforms(opts, ["claude-code"]);
1063
+ const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
1064
+ if (toInstall.length === 0) {
1065
+ outro(chalk2.yellow("All selected agent platforms are already installed."));
1066
+ return;
1067
+ }
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(", ")}`));
1083
+ }
1084
+
1085
+ // src/commands/clear.ts
1086
+ 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";
1089
+ import chalk3 from "chalk";
1090
+ var PLATFORM_TEMPLATE_DIR = {
1091
+ "claude-code": "claude",
1092
+ codex: "codex",
1093
+ qoder: "qoder"
1094
+ };
1095
+ async function clear(opts) {
1096
+ renderBanner();
1097
+ const cwd = process.cwd();
1098
+ const easyCodingDir = path14.join(cwd, EASY_CODING_DIR);
1099
+ if (!await pathExists(easyCodingDir)) {
1100
+ throw new Error("No .easy-coding directory found in this project \u2014 nothing to clear.");
1101
+ }
1102
+ const agents = await resolveInstalledAgents(cwd);
1103
+ const plan = await buildClearPlan(cwd, agents);
1104
+ console.log(renderPlan(cwd, agents, plan));
1105
+ if (opts.dryRun) {
1106
+ return;
1107
+ }
1108
+ if (!opts.yes) {
1109
+ const ok = await confirm2({
1110
+ message: "Remove these harness files? Tasks, spec, memory, and knowledge files are kept.",
1111
+ initialValue: false
1112
+ });
1113
+ if (typeof ok === "symbol" || !ok) {
1114
+ cancel2("Clear cancelled.");
1115
+ return;
1116
+ }
1117
+ }
1118
+ await executeClearPlan(plan);
1119
+ outro2(chalk3.green("Easy Coding harness removed. Run easy-coding init to reinstall."));
1120
+ }
1121
+ async function resolveInstalledAgents(cwd) {
1122
+ const configPath = path14.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1123
+ if (await pathExists(configPath)) {
1124
+ try {
1125
+ const config = await readConfigYaml(configPath);
1126
+ const listed = Array.isArray(config.agents) ? config.agents.filter(isAgentPlatform) : [];
1127
+ if (listed.length > 0) {
1128
+ return listed;
1129
+ }
1130
+ } catch {
1131
+ }
1132
+ }
1133
+ const detected = [];
1134
+ for (const platform of AGENT_PLATFORMS) {
1135
+ if (await hasPlatformInstall(cwd, platform)) {
1136
+ detected.push(platform);
1137
+ }
1138
+ }
1139
+ return detected;
1140
+ }
1141
+ async function hasPlatformInstall(cwd, platform) {
1142
+ const metas = platform === "qoder" ? resolveQoderVariantMetas() : [resolvePlatformMeta(cwd, platform)];
1143
+ for (const meta of metas) {
1144
+ if (await hasInstallMarkers(cwd, meta)) {
1145
+ return true;
1146
+ }
1147
+ }
1148
+ return false;
1149
+ }
1150
+ async function hasInstallMarkers(cwd, meta) {
1151
+ const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
1152
+ for (const marker of markers) {
1153
+ if (await pathExists(path14.join(cwd, marker))) {
1154
+ return true;
1155
+ }
1156
+ }
1157
+ return false;
1158
+ }
1159
+ async function resolveClearPlatformMetas(cwd, platform) {
1160
+ if (platform !== "qoder") {
1161
+ return [resolvePlatformMeta(cwd, platform)];
1162
+ }
1163
+ const installed = [];
1164
+ for (const meta of resolveQoderVariantMetas()) {
1165
+ if (await hasInstallMarkers(cwd, meta)) {
1166
+ installed.push(meta);
1167
+ }
1168
+ }
1169
+ return installed.length > 0 ? installed : [resolvePlatformMeta(cwd, platform)];
1170
+ }
1171
+ async function buildClearPlan(cwd, agents) {
1172
+ const manifest = await readInstallManifest(cwd);
1173
+ if (manifest) {
1174
+ const plan2 = await buildManifestClearPlan(cwd, agents, manifest);
1175
+ const manifestAgents = new Set(manifest.agents.filter(isAgentPlatform));
1176
+ const missingAgents = agents.filter((agent) => !manifestAgents.has(agent));
1177
+ if (missingAgents.length > 0) {
1178
+ mergeClearPlan(plan2, await buildTemplateClearPlan(cwd, missingAgents));
1179
+ }
1180
+ if (manifestAgents.has("qoder") && agents.includes("qoder")) {
1181
+ const uncoveredQoderMetas = await resolveManifestUncoveredQoderMetas(cwd, manifest);
1182
+ if (uncoveredQoderMetas.length > 0) {
1183
+ mergeClearPlan(
1184
+ plan2,
1185
+ await buildTemplateClearPlanForMetas(cwd, "qoder", uncoveredQoderMetas)
1186
+ );
1187
+ }
1188
+ }
1189
+ addRuntimeClearEntries(plan2, cwd);
1190
+ return plan2;
1191
+ }
1192
+ const plan = await buildTemplateClearPlan(cwd, agents);
1193
+ addRuntimeClearEntries(plan, cwd);
1194
+ return plan;
1195
+ }
1196
+ async function buildManifestClearPlan(cwd, agents, manifest) {
1197
+ const plan = createClearPlan();
1198
+ const agentSet = new Set(agents);
1199
+ for (const file of manifest.files) {
1200
+ if (!agentSet.has(file.platform)) {
1201
+ continue;
1202
+ }
1203
+ const filePath = manifestPath(cwd, file.path);
1204
+ if (!await pathExists(filePath)) {
1205
+ continue;
1206
+ }
1207
+ if (await manifestFileMatches(filePath, file.sha256)) {
1208
+ plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
1209
+ addEmptyDirChain(plan.emptyDirs, path14.dirname(filePath), cwd);
1210
+ } else {
1211
+ plan.skippedModified.push(filePath);
1212
+ }
1213
+ }
1214
+ for (const registration of manifest.hook_registrations) {
1215
+ if (!agentSet.has(registration.platform)) {
1216
+ continue;
1217
+ }
1218
+ addHookConfigPrune(
1219
+ plan.pruneHookConfigs,
1220
+ plan.pruneHookCommands,
1221
+ manifestPath(cwd, registration.config_path),
1222
+ registration.hook_path ? [registration.hook_path] : [],
1223
+ [registration.command]
1224
+ );
1225
+ }
1226
+ for (const region of manifest.constraint_regions) {
1227
+ if (!agentSet.has(region.platform)) {
1228
+ continue;
1229
+ }
1230
+ plan.constraints.add(manifestPath(cwd, region.path));
1231
+ }
1232
+ return finalizeClearPlan(plan);
1233
+ }
1234
+ async function buildTemplateClearPlan(cwd, agents) {
1235
+ const plan = createClearPlan();
1236
+ const managedSkills = await listDirNames(getTemplatePath("common", "skills"));
1237
+ managedSkills.push(...await listDirNames(getTemplatePath("common", "bundled-skills")));
1238
+ for (const platform of agents) {
1239
+ const metas = await resolveClearPlatformMetas(cwd, platform);
1240
+ await addTemplateClearEntries(plan, cwd, platform, metas, managedSkills);
1241
+ }
1242
+ return finalizeClearPlan(plan);
1243
+ }
1244
+ async function buildTemplateClearPlanForMetas(cwd, platform, metas) {
1245
+ const plan = createClearPlan();
1246
+ const managedSkills = await listDirNames(getTemplatePath("common", "skills"));
1247
+ managedSkills.push(...await listDirNames(getTemplatePath("common", "bundled-skills")));
1248
+ await addTemplateClearEntries(plan, cwd, platform, metas, managedSkills);
1249
+ return finalizeClearPlan(plan);
1250
+ }
1251
+ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills) {
1252
+ const hookFileNames = await listSharedHookNamesForPlatform(platform);
1253
+ const agentFileNames = await listFileNames(
1254
+ getTemplatePath(PLATFORM_TEMPLATE_DIR[platform], "agents")
1255
+ );
1256
+ for (const meta of metas) {
1257
+ for (const name of managedSkills) {
1258
+ plan.remove.add(path14.join(cwd, meta.skillsDir, name));
1259
+ }
1260
+ for (const name of hookFileNames) {
1261
+ plan.remove.add(path14.join(cwd, meta.hooksDir, name));
1262
+ }
1263
+ for (const name of agentFileNames) {
1264
+ plan.remove.add(path14.join(cwd, meta.agentsDir, name));
1265
+ }
1266
+ addHookConfigPrune(
1267
+ plan.pruneHookConfigs,
1268
+ plan.pruneHookCommands,
1269
+ path14.join(cwd, meta.hookConfigFile),
1270
+ hookFileNames.map((name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`),
1271
+ []
1272
+ );
1273
+ plan.constraints.add(path14.join(cwd, meta.mainConstraint));
1274
+ if (platform === "codex") {
1275
+ plan.remove.add(path14.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
1276
+ }
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));
1280
+ }
1281
+ }
1282
+ async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
1283
+ const coveredBaseDirs = manifestCoveredQoderBaseDirs(manifest);
1284
+ const metas = [];
1285
+ for (const meta of resolveQoderVariantMetas()) {
1286
+ const baseDir = meta.templateContext.platform_config_dir;
1287
+ if (coveredBaseDirs.has(baseDir)) {
1288
+ continue;
1289
+ }
1290
+ if (await hasInstallMarkers(cwd, meta)) {
1291
+ metas.push(meta);
1292
+ }
1293
+ }
1294
+ return metas;
1295
+ }
1296
+ function manifestCoveredQoderBaseDirs(manifest) {
1297
+ const covered = /* @__PURE__ */ new Set();
1298
+ for (const meta of resolveQoderVariantMetas()) {
1299
+ const baseDir = meta.templateContext.platform_config_dir.replace(/\\/g, "/");
1300
+ const prefix = `${baseDir}/`;
1301
+ if (manifest.files.some((file) => file.platform === "qoder" && file.path.startsWith(prefix)) || manifest.hook_registrations.some(
1302
+ (registration) => registration.platform === "qoder" && (registration.config_path === meta.hookConfigFile || registration.config_path.startsWith(prefix) || Boolean(registration.hook_path?.startsWith(prefix)))
1303
+ )) {
1304
+ covered.add(baseDir);
1305
+ }
1306
+ }
1307
+ return covered;
1308
+ }
1309
+ function addHookConfigPrune(pruneHookConfigs, pruneHookCommands, filePath, managedHookPaths, managedHookCommands) {
1310
+ const existingPaths = pruneHookConfigs.get(filePath) ?? /* @__PURE__ */ new Set();
1311
+ for (const managedHookPath of managedHookPaths) {
1312
+ existingPaths.add(managedHookPath);
1313
+ }
1314
+ pruneHookConfigs.set(filePath, existingPaths);
1315
+ const existingCommands = pruneHookCommands.get(filePath) ?? /* @__PURE__ */ new Set();
1316
+ for (const managedHookCommand of managedHookCommands) {
1317
+ existingCommands.add(managedHookCommand);
1318
+ }
1319
+ pruneHookCommands.set(filePath, existingCommands);
1320
+ }
1321
+ function createClearPlan() {
1322
+ return {
1323
+ remove: /* @__PURE__ */ new Set(),
1324
+ removeFiles: [],
1325
+ skippedModified: [],
1326
+ pruneHookConfigs: /* @__PURE__ */ new Map(),
1327
+ pruneHookCommands: /* @__PURE__ */ new Map(),
1328
+ constraints: /* @__PURE__ */ new Set(),
1329
+ emptyDirs: /* @__PURE__ */ new Set()
1330
+ };
1331
+ }
1332
+ function finalizeClearPlan(plan) {
1333
+ return {
1334
+ remove: [...plan.remove],
1335
+ removeFiles: plan.removeFiles,
1336
+ skippedModified: plan.skippedModified,
1337
+ pruneHookConfigs: mergeHookConfigPrunes(plan.pruneHookConfigs, plan.pruneHookCommands),
1338
+ constraints: [...plan.constraints],
1339
+ emptyDirs: [...plan.emptyDirs]
1340
+ };
1341
+ }
1342
+ function mergeHookConfigPrunes(pruneHookConfigs, pruneHookCommands) {
1343
+ const filePaths = /* @__PURE__ */ new Set([...pruneHookConfigs.keys(), ...pruneHookCommands.keys()]);
1344
+ return [...filePaths].map((filePath) => ({
1345
+ filePath,
1346
+ managedHookPaths: [...pruneHookConfigs.get(filePath) ?? /* @__PURE__ */ new Set()],
1347
+ managedHookCommands: [...pruneHookCommands.get(filePath) ?? /* @__PURE__ */ new Set()]
1348
+ }));
1349
+ }
1350
+ function mergeClearPlan(target, source) {
1351
+ target.remove = [.../* @__PURE__ */ new Set([...target.remove, ...source.remove])];
1352
+ target.removeFiles = dedupeFileRemovals([...target.removeFiles, ...source.removeFiles]);
1353
+ target.skippedModified = [.../* @__PURE__ */ new Set([...target.skippedModified, ...source.skippedModified])];
1354
+ target.constraints = [.../* @__PURE__ */ new Set([...target.constraints, ...source.constraints])];
1355
+ target.emptyDirs = [.../* @__PURE__ */ new Set([...target.emptyDirs, ...source.emptyDirs])];
1356
+ for (const sourcePrune of source.pruneHookConfigs) {
1357
+ const existing = target.pruneHookConfigs.find(
1358
+ (prune) => prune.filePath === sourcePrune.filePath
1359
+ );
1360
+ if (!existing) {
1361
+ target.pruneHookConfigs.push(sourcePrune);
1362
+ continue;
1363
+ }
1364
+ existing.managedHookPaths = [
1365
+ .../* @__PURE__ */ new Set([...existing.managedHookPaths, ...sourcePrune.managedHookPaths])
1366
+ ];
1367
+ existing.managedHookCommands = [
1368
+ .../* @__PURE__ */ new Set([...existing.managedHookCommands, ...sourcePrune.managedHookCommands])
1369
+ ];
1370
+ }
1371
+ }
1372
+ function dedupeFileRemovals(removals) {
1373
+ const byPath2 = /* @__PURE__ */ new Map();
1374
+ for (const removal of removals) {
1375
+ byPath2.set(removal.filePath, removal);
1376
+ }
1377
+ return [...byPath2.values()];
1378
+ }
1379
+ function addRuntimeClearEntries(plan, cwd) {
1380
+ plan.remove = [
1381
+ .../* @__PURE__ */ new Set([
1382
+ ...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)
1387
+ ])
1388
+ ];
1389
+ }
1390
+ function addEmptyDirChain(emptyDirs, startDir, cwd) {
1391
+ let current = startDir;
1392
+ while (current !== cwd && isInsideDirectory(cwd, current)) {
1393
+ emptyDirs.add(current);
1394
+ const parent = path14.dirname(current);
1395
+ if (parent === current) {
1396
+ break;
1397
+ }
1398
+ current = parent;
1399
+ }
1400
+ }
1401
+ function isInsideDirectory(parent, child) {
1402
+ const relative = path14.relative(parent, child);
1403
+ return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
1404
+ }
1405
+ async function listSharedHookNamesForPlatform(platform) {
1406
+ const names = await listFileNames(getTemplatePath("shared-hooks"));
1407
+ if (PLATFORM_META[platform].hasSubagentContext) {
1408
+ return names;
1409
+ }
1410
+ return names.filter((name) => name !== "inject-subagent-context.py");
1411
+ }
1412
+ async function executeClearPlan(plan) {
1413
+ for (const target of plan.removeFiles) {
1414
+ if (await manifestFileMatches(target.filePath, target.expectedSha256)) {
1415
+ await rm(target.filePath, { force: true });
1416
+ }
1417
+ }
1418
+ for (const target of plan.remove) {
1419
+ await rm(target, { recursive: true, force: true });
1420
+ }
1421
+ for (const config of plan.pruneHookConfigs) {
1422
+ await pruneHookConfig(config);
1423
+ }
1424
+ for (const constraint of plan.constraints) {
1425
+ await stripConstraintRegion(constraint);
1426
+ }
1427
+ for (const dir of sortDirsDeepestFirst(plan.emptyDirs)) {
1428
+ await removeIfEmpty(dir);
1429
+ }
1430
+ }
1431
+ async function pruneHookConfig(config) {
1432
+ const { filePath, managedHookCommands, managedHookPaths } = config;
1433
+ const content = await readTextIfExists(filePath);
1434
+ if (content === null) {
1435
+ return;
1436
+ }
1437
+ let parsed;
1438
+ try {
1439
+ parsed = JSON.parse(content);
1440
+ } catch {
1441
+ return;
1442
+ }
1443
+ const hooks = parsed.hooks;
1444
+ if (hooks && typeof hooks === "object" && !Array.isArray(hooks)) {
1445
+ const cleaned = pruneHookEvents(
1446
+ hooks,
1447
+ managedHookPaths,
1448
+ managedHookCommands
1449
+ );
1450
+ if (cleaned === null) {
1451
+ parsed = Object.fromEntries(Object.entries(parsed).filter(([key]) => key !== "hooks"));
1452
+ } else {
1453
+ parsed.hooks = cleaned;
1454
+ }
1455
+ }
1456
+ if (Object.keys(parsed).length === 0) {
1457
+ await rm(filePath, { force: true });
1458
+ return;
1459
+ }
1460
+ await writeFile(filePath, `${JSON.stringify(parsed, null, 2)}
1461
+ `, "utf8");
1462
+ }
1463
+ function pruneHookEvents(hooks, managedHookPaths, managedHookCommands) {
1464
+ const result = {};
1465
+ for (const [event, value] of Object.entries(hooks)) {
1466
+ if (!Array.isArray(value)) {
1467
+ result[event] = value;
1468
+ continue;
1469
+ }
1470
+ const keptGroups = value.map((group) => pruneHookGroup(group, managedHookPaths, managedHookCommands)).filter((group) => group !== null);
1471
+ if (keptGroups.length > 0) {
1472
+ result[event] = keptGroups;
1473
+ }
1474
+ }
1475
+ return Object.keys(result).length === 0 ? null : result;
1476
+ }
1477
+ function pruneHookGroup(group, managedHookPaths, managedHookCommands) {
1478
+ if (!group || typeof group !== "object") {
1479
+ return group;
1480
+ }
1481
+ const entry = group;
1482
+ if (!Array.isArray(entry.hooks)) {
1483
+ return group;
1484
+ }
1485
+ const keptHooks = entry.hooks.filter(
1486
+ (hook) => !isManagedHook(hook, managedHookPaths, managedHookCommands)
1487
+ );
1488
+ if (keptHooks.length === 0) {
1489
+ return null;
1490
+ }
1491
+ return { ...group, hooks: keptHooks };
1492
+ }
1493
+ function isManagedHook(hook, managedHookPaths, managedHookCommands) {
1494
+ if (!hook || typeof hook !== "object") {
1495
+ return false;
1496
+ }
1497
+ const command = hook.command;
1498
+ if (typeof command !== "string") {
1499
+ return false;
1500
+ }
1501
+ const normalizedManagedCommands = managedHookCommands.map(normalizeCommand);
1502
+ if (normalizedManagedCommands.includes(normalizeCommand(command))) {
1503
+ return true;
1504
+ }
1505
+ const normalizedCommand = command.replace(/\\/g, "/");
1506
+ return managedHookPaths.some(
1507
+ (hookPath) => commandContainsPathToken(normalizedCommand, hookPath.replace(/\\/g, "/"))
1508
+ );
1509
+ }
1510
+ function commandContainsPathToken(command, targetPath) {
1511
+ const escaped = targetPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1512
+ return new RegExp(`(^|\\s|["'])${escaped}($|\\s|["'])`).test(command);
1513
+ }
1514
+ async function stripConstraintRegion(filePath) {
1515
+ const content = await readTextIfExists(filePath);
1516
+ if (content === null) {
1517
+ return;
1518
+ }
1519
+ const stripped = removeMarkedRegion(content);
1520
+ if (stripped !== content) {
1521
+ await writeFile(filePath, stripped, "utf8");
1522
+ }
1523
+ }
1524
+ async function removeIfEmpty(dir) {
1525
+ try {
1526
+ const entries = await readdir4(dir);
1527
+ if (entries.length === 0) {
1528
+ await rm(dir, { recursive: true, force: true });
1529
+ }
1530
+ } catch {
1531
+ }
1532
+ }
1533
+ function sortDirsDeepestFirst(dirs) {
1534
+ return [...dirs].sort((left, right) => {
1535
+ const depthDiff = pathDepth(right) - pathDepth(left);
1536
+ return depthDiff === 0 ? left.localeCompare(right) : depthDiff;
1537
+ });
1538
+ }
1539
+ function pathDepth(dir) {
1540
+ return path14.normalize(dir).split(path14.sep).filter(Boolean).length;
1541
+ }
1542
+ async function listDirNames(dir) {
1543
+ try {
1544
+ const entries = await readdir4(dir, { withFileTypes: true });
1545
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1546
+ } catch {
1547
+ return [];
1548
+ }
1549
+ }
1550
+ async function listFileNames(dir) {
1551
+ try {
1552
+ const entries = await readdir4(dir, { withFileTypes: true });
1553
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
1554
+ } catch {
1555
+ return [];
1556
+ }
1557
+ }
1558
+ function renderPlan(cwd, agents, plan) {
1559
+ const rel = (target) => path14.relative(cwd, target) || target;
1560
+ const lines = [];
1561
+ lines.push(chalk3.bold("easy-coding clear"));
1562
+ lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
1563
+ lines.push("");
1564
+ lines.push("Will remove:");
1565
+ for (const target of plan.removeFiles) {
1566
+ lines.push(` - ${rel(target.filePath)}`);
1567
+ }
1568
+ for (const target of plan.remove) {
1569
+ lines.push(` - ${rel(target)}`);
1570
+ }
1571
+ if (plan.skippedModified.length > 0) {
1572
+ lines.push("Will keep modified manifest files:");
1573
+ for (const target of plan.skippedModified) {
1574
+ lines.push(` - ${rel(target)}`);
1575
+ }
1576
+ }
1577
+ if (plan.constraints.length > 0) {
1578
+ lines.push("Will strip the generated region from (file kept):");
1579
+ for (const target of plan.constraints) {
1580
+ lines.push(` - ${rel(target)}`);
1581
+ }
1582
+ }
1583
+ if (plan.pruneHookConfigs.length > 0) {
1584
+ lines.push("Will prune harness hooks from (file kept if other config remains):");
1585
+ for (const target of plan.pruneHookConfigs) {
1586
+ lines.push(` - ${rel(target.filePath)}`);
1587
+ }
1588
+ }
1589
+ lines.push("");
1590
+ lines.push(
1591
+ chalk3.dim(
1592
+ "Kept: .easy-coding/tasks, spec, memory, project.yaml, and knowledge files (SOUL/RULES/ABSTRACT/TEST_STRATEGY)."
1593
+ )
1594
+ );
1595
+ return lines.join("\n");
1596
+ }
1597
+
1598
+ // src/commands/init.ts
1599
+ import { note, outro as outro3 } from "@clack/prompts";
1600
+ import chalk4 from "chalk";
1601
+
1602
+ // src/utils/install-state.ts
1603
+ import { readdir as readdir5 } from "fs/promises";
1604
+ import path15 from "path";
1605
+ var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
1606
+ async function detectEasyCodingInstallState(cwd) {
1607
+ const easyCodingDir = path15.join(cwd, EASY_CODING_DIR);
1608
+ if (!await pathExists(easyCodingDir)) {
1609
+ return { kind: "fresh", easyCodingDir };
1610
+ }
1611
+ const configPath = path15.join(easyCodingDir, CONFIG_FILE);
1612
+ if (await pathExists(configPath)) {
1613
+ return { kind: "installed", easyCodingDir, configPath };
1614
+ }
1615
+ const legacyAssets = await detectLegacyAssets(easyCodingDir);
1616
+ if (legacyAssets.length === 0) {
1617
+ return { kind: "unknown", easyCodingDir };
1618
+ }
1619
+ return {
1620
+ kind: "legacy",
1621
+ easyCodingDir,
1622
+ legacyAssets,
1623
+ missingHarnessFiles: [relativeConfigPath(), relativeProjectInitTaskPath()]
1624
+ };
1625
+ }
1626
+ async function detectLegacyAssets(easyCodingDir) {
1627
+ const assets = [];
1628
+ for (const file of LEGACY_ROOT_FILES) {
1629
+ if (await pathExists(path15.join(easyCodingDir, file))) {
1630
+ assets.push(relativeEasyCodingPath(file));
1631
+ }
1632
+ }
1633
+ if (await pathExists(path15.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
1634
+ assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
1635
+ }
1636
+ const shortMemoryFiles = await listMarkdownFiles(path15.join(easyCodingDir, "memory", "short"));
1637
+ assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
1638
+ for (const dir of ["spec", "prototype"]) {
1639
+ if (await hasAnyDirectoryEntry(path15.join(easyCodingDir, dir))) {
1640
+ assets.push(relativeEasyCodingPath(dir));
1641
+ }
1642
+ }
1643
+ return assets.sort();
1644
+ }
1645
+ async function listMarkdownFiles(dir) {
1646
+ if (!await isDirectory(dir)) {
1647
+ return [];
1648
+ }
1649
+ const entries = await readdir5(dir, { withFileTypes: true });
1650
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
1651
+ }
1652
+ async function hasAnyDirectoryEntry(dir) {
1653
+ if (!await isDirectory(dir)) {
1654
+ return false;
1655
+ }
1656
+ return (await readdir5(dir)).length > 0;
1657
+ }
1658
+ function relativeEasyCodingPath(...segments) {
1659
+ return path15.posix.join(EASY_CODING_DIR, ...segments);
1660
+ }
1661
+ function relativeConfigPath() {
1662
+ return path15.posix.join(EASY_CODING_DIR, CONFIG_FILE);
1663
+ }
1664
+ function relativeProjectInitTaskPath() {
1665
+ return path15.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
1666
+ }
1667
+
791
1668
  // src/commands/init.ts
792
1669
  async function init(opts) {
793
1670
  renderBanner();
@@ -804,10 +1681,16 @@ async function init(opts) {
804
1681
  );
805
1682
  }
806
1683
  const platforms = await resolvePlatforms(opts, ["claude-code"]);
1684
+ const artifacts = [];
807
1685
  for (const platform of platforms) {
808
- await CONFIGURATORS[platform](cwd);
1686
+ artifacts.push(...await CONFIGURATORS[platform](cwd));
809
1687
  }
810
1688
  await writeRuntimeScaffold(cwd, platforms);
1689
+ await writeInstallManifest(cwd, {
1690
+ harnessVersion: VERSION,
1691
+ agents: platforms,
1692
+ artifacts
1693
+ });
811
1694
  await writeProjectInitTask(cwd, platforms, {
812
1695
  initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
813
1696
  legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
@@ -818,17 +1701,17 @@ async function init(opts) {
818
1701
  (platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
819
1702
  ).join("\n");
820
1703
  note(triggers, "Next step");
821
- outro2(chalk3.green("easy-coding harness installed. Open your agent and run ec-init."));
1704
+ outro3(chalk4.green("easy-coding harness installed. Open your agent and run ec-init."));
822
1705
  }
823
1706
 
824
1707
  // src/commands/status.ts
825
- import path15 from "path";
826
- import chalk4 from "chalk";
1708
+ import path18 from "path";
1709
+ import chalk5 from "chalk";
827
1710
 
828
1711
  // src/utils/compare-versions.ts
829
- import path13 from "path";
1712
+ import path16 from "path";
830
1713
  function normalize(version) {
831
- const [core] = version.split("-");
1714
+ const [core] = String(version ?? "").split("-");
832
1715
  return core.split(".").map((part) => {
833
1716
  const parsed = Number.parseInt(part, 10);
834
1717
  return Number.isNaN(parsed) ? 0 : parsed;
@@ -850,11 +1733,16 @@ function isVersionBehind(installed, current = VERSION) {
850
1733
  return compareVersions(installed, current) === -1;
851
1734
  }
852
1735
  async function checkForUpgrade(cwd) {
853
- const configPath = path13.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1736
+ const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
854
1737
  if (!await pathExists(configPath)) {
855
1738
  return;
856
1739
  }
857
- const config = await readConfigYaml(configPath);
1740
+ let config;
1741
+ try {
1742
+ config = await readConfigYaml(configPath);
1743
+ } catch {
1744
+ return;
1745
+ }
858
1746
  const installed = String(config.harness_version ?? "");
859
1747
  if (installed && isVersionBehind(installed)) {
860
1748
  process.stderr.write(
@@ -865,15 +1753,15 @@ async function checkForUpgrade(cwd) {
865
1753
  }
866
1754
 
867
1755
  // src/utils/session.ts
868
- import { readdir as readdir4 } from "fs/promises";
869
- import path14 from "path";
1756
+ import { readdir as readdir6 } from "fs/promises";
1757
+ import path17 from "path";
870
1758
  var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
871
1759
  function getSessionDir(cwd) {
872
- return path14.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
1760
+ return path17.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
873
1761
  }
874
1762
  function getSessionFilePath(cwd, ppid) {
875
1763
  const pid = ppid ?? process.ppid;
876
- return path14.join(getSessionDir(cwd), `${pid}.json`);
1764
+ return path17.join(getSessionDir(cwd), `${pid}.json`);
877
1765
  }
878
1766
  async function readSessionFile(cwd, ppid) {
879
1767
  const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
@@ -887,7 +1775,7 @@ async function readSessionFile(cwd, ppid) {
887
1775
  async function status() {
888
1776
  renderBanner();
889
1777
  const cwd = process.cwd();
890
- const configPath = path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1778
+ const configPath = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
891
1779
  if (!await pathExists(configPath)) {
892
1780
  throw new Error("No easy-coding harness found in this project.");
893
1781
  }
@@ -897,20 +1785,20 @@ async function status() {
897
1785
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
898
1786
  const session = await readSessionFile(cwd);
899
1787
  const versionRelation = compareVersions(config.harness_version, VERSION);
900
- console.log(chalk4.bold("Harness"));
1788
+ console.log(chalk5.bold("Harness"));
901
1789
  console.log(` version: ${config.harness_version}`);
902
1790
  console.log(` cli: ${VERSION}`);
903
1791
  if (versionRelation === -1) {
904
- console.log(chalk4.yellow(" upgrade: available"));
1792
+ console.log(chalk5.yellow(" upgrade: available"));
905
1793
  } else if (versionRelation === 1) {
906
- console.log(chalk4.red(" upgrade: CLI is older than project harness"));
1794
+ console.log(chalk5.red(" upgrade: CLI is older than project harness"));
907
1795
  } else {
908
1796
  console.log(" upgrade: up to date");
909
1797
  }
910
1798
  console.log(` agents: ${config.agents.join(", ") || "(none)"}`);
911
- console.log(` project: ${config.project.name} (${config.project.mode})`);
1799
+ console.log(` project: ${config.project.name}`);
912
1800
  console.log("");
913
- console.log(chalk4.bold("Session"));
1801
+ console.log(chalk5.bold("Session"));
914
1802
  if (session?.current_task) {
915
1803
  const taskPath = getTaskJsonPath(cwd, session.current_task);
916
1804
  if (await pathExists(taskPath)) {
@@ -925,7 +1813,7 @@ async function status() {
925
1813
  console.log(" no active session");
926
1814
  }
927
1815
  console.log("");
928
- console.log(chalk4.bold("Tasks"));
1816
+ console.log(chalk5.bold("Tasks"));
929
1817
  console.log(` total: ${tasks.length}`);
930
1818
  for (const [taskStatus, count] of Object.entries(taskCounts)) {
931
1819
  console.log(` ${taskStatus}: ${count}`);
@@ -939,54 +1827,100 @@ async function status() {
939
1827
  }
940
1828
  }
941
1829
 
1830
+ // src/commands/update.ts
1831
+ import { execFileSync } from "child_process";
1832
+ import { cancel as cancel3, confirm as confirm3, outro as outro4 } from "@clack/prompts";
1833
+ import chalk6 from "chalk";
1834
+ async function update(opts) {
1835
+ renderBanner();
1836
+ const tag = opts.tag ?? "latest";
1837
+ const spec = `${PACKAGE_NAME}@${tag}`;
1838
+ const plan = ["update runs one step:", ` refresh the global CLI: npm install -g ${spec}`].join(
1839
+ "\n"
1840
+ );
1841
+ if (opts.dryRun) {
1842
+ console.log(plan);
1843
+ return;
1844
+ }
1845
+ if (!opts.yes) {
1846
+ const confirmed = await confirm3({
1847
+ message: `Update the global CLI to ${spec}?`,
1848
+ initialValue: true
1849
+ });
1850
+ if (typeof confirmed === "symbol" || !confirmed) {
1851
+ cancel3("Update cancelled.");
1852
+ return;
1853
+ }
1854
+ }
1855
+ console.log(chalk6.cyan(`\u2192 npm install -g ${spec}`));
1856
+ execFileSync("npm", ["install", "-g", spec], { stdio: "inherit" });
1857
+ outro4(chalk6.green("Global CLI updated."));
1858
+ }
1859
+
942
1860
  // src/commands/upgrade.ts
943
- import path16 from "path";
944
- import { cancel as cancel2, confirm as confirm2, outro as outro3 } from "@clack/prompts";
945
- import chalk5 from "chalk";
1861
+ import path19 from "path";
1862
+ import { cancel as cancel4, confirm as confirm4, outro as outro5 } from "@clack/prompts";
1863
+ import chalk7 from "chalk";
946
1864
  async function upgrade(opts) {
947
1865
  renderBanner();
948
1866
  const cwd = process.cwd();
949
- const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1867
+ const configPath = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
950
1868
  if (!await pathExists(configPath)) {
951
1869
  throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
952
1870
  }
953
1871
  const config = await readConfigYaml(configPath);
954
- const relation = compareVersions(config.harness_version, VERSION);
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);
955
1880
  if (relation === 0) {
956
- outro3(chalk5.green(`easy-coding harness is already up to date (${VERSION}).`));
1881
+ outro5(chalk7.green(`easy-coding harness is already up to date (${VERSION}).`));
957
1882
  return;
958
1883
  }
959
1884
  if (relation === 1) {
960
1885
  throw new Error(
961
- `Project harness version ${config.harness_version} is newer than CLI ${VERSION}. Update the CLI first.`
1886
+ `Project harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
962
1887
  );
963
1888
  }
964
1889
  const summary = [
965
- `Upgrade: ${config.harness_version} -> ${VERSION}`,
1890
+ `Upgrade: ${installedVersion || "unknown"} -> ${VERSION}`,
966
1891
  `Installed agents: ${config.agents.join(", ")}`,
967
- "Will overwrite managed skills, hooks, agents, and generated main-constraint regions.",
968
- "Will not touch tasks, memory, state, spec, or project knowledge files."
1892
+ "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
1893
+ "Will update project-init task to recommend ec-init re-run for version adaptation.",
1894
+ "Will not touch memory, state, spec, or project knowledge files."
969
1895
  ].join("\n");
970
1896
  if (opts.dryRun) {
971
1897
  console.log(summary);
972
1898
  return;
973
1899
  }
974
1900
  if (!opts.yes) {
975
- const shouldUpgrade = await confirm2({
1901
+ const shouldUpgrade = await confirm4({
976
1902
  message: "Apply this harness upgrade?",
977
1903
  initialValue: true
978
1904
  });
979
1905
  if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
980
- cancel2("Upgrade cancelled.");
1906
+ cancel4("Upgrade cancelled.");
981
1907
  return;
982
1908
  }
983
1909
  }
1910
+ const artifacts = [];
984
1911
  for (const platform of config.agents) {
985
- await CONFIGURATORS[platform](cwd);
1912
+ artifacts.push(...await CONFIGURATORS[platform](cwd));
986
1913
  }
1914
+ await writeRuntimeScaffold(cwd, config.agents);
1915
+ await writeInstallManifest(cwd, {
1916
+ harnessVersion: VERSION,
1917
+ agents: config.agents,
1918
+ artifacts
1919
+ });
987
1920
  await ensureEasyCodingSessionsIgnored(cwd);
988
1921
  await updateHarnessVersion(configPath, VERSION);
989
- outro3(chalk5.green(`easy-coding harness upgraded to ${VERSION}.`));
1922
+ await setPendingInitSince(cwd, VERSION);
1923
+ outro5(chalk7.green(`easy-coding harness upgraded to ${VERSION}.`));
990
1924
  }
991
1925
 
992
1926
  // src/cli.ts
@@ -995,7 +1929,7 @@ function withErrorHandling(fn) {
995
1929
  try {
996
1930
  await fn(opts);
997
1931
  } catch (error) {
998
- console.error(chalk6.red("Error:"), error instanceof Error ? error.message : error);
1932
+ console.error(chalk8.red("Error:"), error instanceof Error ? error.message : error);
999
1933
  if (process.env.EC_DEBUG && error instanceof Error) {
1000
1934
  console.error(error.stack);
1001
1935
  }
@@ -1009,6 +1943,8 @@ program.name("easy-coding").description(PACKAGE_NAME).version(VERSION, "-v, --ve
1009
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));
1010
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));
1011
1945
  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
+ 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));
1012
1947
  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));
1013
1949
  program.parse();
1014
1950
  //# sourceMappingURL=cli.js.map