easy-coding-harness 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +10 -65
  3. package/dist/cli.js +1227 -299
  4. package/dist/cli.js.map +1 -1
  5. package/package.json +2 -1
  6. package/templates/claude/agents/ec-fixer.md +36 -0
  7. package/templates/codex/agents/ec-fixer.toml +25 -0
  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 +104 -126
  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 +26 -3
  18. package/templates/common/skills/ec-memory/SKILL.md +56 -19
  19. package/templates/common/skills/ec-reviewing/SKILL.md +39 -10
  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 +22 -11
  23. package/templates/common/skills/ec-workflow/SKILL.md +51 -21
  24. package/templates/main-constraint/AGENTS.md.tpl +16 -3
  25. package/templates/main-constraint/CLAUDE.md.tpl +16 -3
  26. package/templates/qoder/agents/ec-fixer.md +36 -0
  27. package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +75 -0
  28. package/templates/runtime/memory/long/BUSINESS.md +42 -9
  29. package/templates/runtime/memory/long/MEMORY.md +37 -2
  30. package/templates/runtime/memory/long/TECHNICAL.md +45 -1
  31. package/templates/runtime/templates/dev-spec-skeleton.md +80 -0
  32. package/templates/shared-hooks/easy_coding_state.py +516 -0
  33. package/templates/shared-hooks/easy_coding_status.py +77 -38
  34. package/templates/shared-hooks/inject-subagent-context.py +4 -11
  35. package/templates/shared-hooks/inject-workflow-state.py +5 -14
  36. package/templates/shared-hooks/session-start.py +90 -16
  37. 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 chalk7 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";
@@ -31,7 +31,7 @@ var PLATFORM_META = {
31
31
  sub_agent_dispatch: "Agent tool",
32
32
  platform_spawn_instruction: 'Use the Agent tool with run_in_background when useful; use isolation: "worktree" for parallel file edits.',
33
33
  skill_trigger: "/",
34
- workflow_state_path: ".easy-coding/state.json",
34
+ workflow_state_path: ".easy-coding/sessions/",
35
35
  main_constraint_file: "CLAUDE.md",
36
36
  python_cmd: pythonCmd,
37
37
  platform_config_dir: ".claude"
@@ -53,7 +53,7 @@ var PLATFORM_META = {
53
53
  sub_agent_dispatch: "Codex sub-agent dispatch",
54
54
  platform_spawn_instruction: "Use Codex sub-agent delegation where available; pass the full task card in the prompt.",
55
55
  skill_trigger: "$",
56
- workflow_state_path: ".easy-coding/state.json",
56
+ workflow_state_path: ".easy-coding/sessions/",
57
57
  main_constraint_file: "AGENTS.md",
58
58
  python_cmd: pythonCmd,
59
59
  platform_config_dir: ".codex"
@@ -76,7 +76,7 @@ var PLATFORM_META = {
76
76
  sub_agent_dispatch: "Agent tool",
77
77
  platform_spawn_instruction: "Use the Agent tool with worktree isolation for parallel file edits.",
78
78
  skill_trigger: "/",
79
- workflow_state_path: ".easy-coding/state.json",
79
+ workflow_state_path: ".easy-coding/sessions/",
80
80
  main_constraint_file: "AGENTS.md",
81
81
  python_cmd: pythonCmd,
82
82
  platform_config_dir: ".qoder"
@@ -88,21 +88,24 @@ 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";
98
- var STATE_FILE = "state.json";
99
+ var INSTALL_MANIFEST_FILE = "install-manifest.json";
100
+ var SESSIONS_DIR = "sessions";
99
101
  var TASKS_DIR = "tasks";
100
102
  var PROJECT_INIT_TASK_ID = "project-init";
101
103
  var MEMORY_DIR = "memory";
102
104
  var SPEC_DIR = "spec";
103
105
  var MAIN_SPEC_DIR = "main";
104
106
  var DEV_SPEC_DIR = "dev";
105
- var STATE_GITIGNORE_ENTRY = ".easy-coding/state.json";
107
+ var TEMPLATES_DIR = "templates";
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 -->";
108
111
 
@@ -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,27 +500,38 @@ 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
- const next = current ? replaceMarkedRegion(current, generated) : generated;
295
- await writeTextFile(destination, next);
505
+ if (current) {
506
+ const markerContent = extractMarkedRegion(generated);
507
+ if (!markerContent) {
508
+ throw new Error(`Main constraint template ${templateName} generated content lacks markers.`);
509
+ }
510
+ await writeTextFile(destination, replaceMarkedRegion(current, markerContent));
511
+ } else {
512
+ await writeTextFile(destination, generated);
513
+ }
514
+ return destination;
296
515
  }
297
516
  async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
298
517
  await ensureDir(destination);
518
+ const written = [];
299
519
  for (const entry of (await readdir(source)).sort()) {
300
520
  if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
301
521
  continue;
302
522
  }
303
- const sourcePath = path3.join(source, entry);
304
- const destinationPath = path3.join(destination, stripTemplateExtension(entry));
523
+ const sourcePath = path4.join(source, entry);
524
+ const destinationPath = path4.join(destination, stripTemplateExtension(entry));
305
525
  const sourceStat = await stat2(sourcePath);
306
526
  if (sourceStat.isDirectory()) {
307
- await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs);
527
+ written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
308
528
  continue;
309
529
  }
310
530
  const content = resolvePlaceholders(await readTextFile(sourcePath), ctx, sourcePath);
311
531
  await writeTextFile(destinationPath, content);
532
+ written.push(destinationPath);
312
533
  }
534
+ return written;
313
535
  }
314
536
  function shouldSkipTemplateEntry(entry) {
315
537
  return entry === ".DS_Store" || entry === "__pycache__" || entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".map");
@@ -323,69 +545,194 @@ async function configureClaude(cwd) {
323
545
  const platform = "claude-code";
324
546
  const meta = PLATFORM_META[platform];
325
547
  const ctx = meta.templateContext;
326
- const dest = path4.join(cwd, ".claude");
327
- await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
328
- await writeSharedHooks(path4.join(dest, "hooks"), platform);
329
- await writeSkills(
330
- path4.join(cwd, meta.skillsDir),
331
- await resolveSkills(ctx),
332
- 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
+ )
333
566
  );
334
- await writeMainConstraint(cwd, platform);
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))
573
+ );
574
+ artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
575
+ return artifacts;
335
576
  }
336
577
 
337
578
  // src/configurators/codex.ts
338
- import path5 from "path";
579
+ import path6 from "path";
339
580
  async function configureCodex(cwd) {
340
581
  const platform = "codex";
341
582
  const meta = PLATFORM_META[platform];
342
583
  const ctx = meta.templateContext;
343
- await writeSkills(
344
- path5.join(cwd, meta.skillsDir),
345
- await resolveSkills(ctx),
346
- 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))
592
+ );
593
+ artifacts.push(
594
+ ...(await writeSharedHooks(path6.join(cwd, meta.hooksDir), platform, {
595
+ skipSubagentContext: true
596
+ })).map((filePath) => fileArtifact(filePath, "hook", platform))
347
597
  );
348
- await writeSharedHooks(path5.join(cwd, meta.hooksDir), platform, { skipSubagentContext: true });
349
- await copyPlatformTemplates("codex", path5.join(cwd, ".codex"), ["hooks"], ctx);
350
- await writeMainConstraint(cwd, platform);
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;
351
616
  }
352
617
 
353
618
  // src/configurators/qoder.ts
354
- import { existsSync as existsSync2, readdirSync } from "fs";
355
- import path6 from "path";
356
- function claudeSkillsExist(cwd) {
357
- const claudeSkillsDir = path6.join(cwd, ".claude", "skills");
358
- 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;
359
664
  try {
360
- return readdirSync(claudeSkillsDir).some((d) => d.startsWith("ec-"));
361
- } catch {
362
- return false;
665
+ entries = await readdir2(dir, { withFileTypes: true });
666
+ } catch (error) {
667
+ if (error.code === "ENOENT") {
668
+ return [];
669
+ }
670
+ throw error;
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
+ }
363
691
  }
692
+ return artifacts;
364
693
  }
365
694
  async function configureQoder(cwd) {
366
695
  const platform = "qoder";
367
- const baseDir = detectQoderCnVariant(cwd) ? ".qodercn" : ".qoder";
368
- const ctx = {
369
- ...PLATFORM_META[platform].templateContext,
370
- platform_config_dir: baseDir
371
- };
372
- const dest = path6.join(cwd, baseDir);
373
- await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
374
- await writeSharedHooks(path6.join(dest, "hooks"), platform);
375
- if (!claudeSkillsExist(cwd)) {
376
- await writeSkills(
377
- path6.join(dest, "skills"),
378
- await resolveSkills(ctx),
379
- 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
+ )
380
732
  );
381
733
  }
382
- await writeMainConstraint(cwd, platform);
383
- }
384
- function detectQoderCnVariant(cwd) {
385
- if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
386
- return true;
387
- }
388
- return existsSync2(path6.join(cwd, ".qodercn"));
734
+ artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
735
+ return artifacts;
389
736
  }
390
737
 
391
738
  // src/configurators/index.ts
@@ -395,23 +742,19 @@ var CONFIGURATORS = {
395
742
  qoder: configureQoder
396
743
  };
397
744
 
398
- // src/ui/banner.ts
399
- import chalk from "chalk";
400
- import figlet from "figlet";
401
-
402
745
  // src/constants/version.ts
403
746
  import { readFileSync } from "fs";
404
- import path7 from "path";
747
+ import path9 from "path";
405
748
  import { fileURLToPath as fileURLToPath2 } from "url";
406
749
  var packageMetadata = readPackageMetadata();
407
750
  var PACKAGE_NAME = packageMetadata.name;
408
751
  var VERSION = packageMetadata.version;
409
752
  function readPackageMetadata() {
410
- const here = path7.dirname(fileURLToPath2(import.meta.url));
753
+ const here = path9.dirname(fileURLToPath2(import.meta.url));
411
754
  const candidates = [
412
- path7.resolve(here, "../../package.json"),
413
- path7.resolve(here, "../package.json"),
414
- 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")
415
758
  ];
416
759
  for (const candidate of candidates) {
417
760
  try {
@@ -426,6 +769,8 @@ function readPackageMetadata() {
426
769
  }
427
770
 
428
771
  // src/ui/banner.ts
772
+ import chalk from "chalk";
773
+ import figlet from "figlet";
429
774
  var WIDE_TERMINAL_WIDTH = 88;
430
775
  var MEDIUM_TERMINAL_WIDTH = 72;
431
776
  function renderBanner() {
@@ -450,7 +795,7 @@ function colorizeBanner(art) {
450
795
  }
451
796
 
452
797
  // src/utils/config-yaml.ts
453
- import { readFile as readFile2 } from "fs/promises";
798
+ import { readFile as readFile3 } from "fs/promises";
454
799
  import YAML, { isScalar, isSeq, parseDocument } from "yaml";
455
800
  function createDefaultConfig(params) {
456
801
  return {
@@ -458,18 +803,13 @@ function createDefaultConfig(params) {
458
803
  harness_version: params.harnessVersion,
459
804
  agents: params.agents,
460
805
  project: {
461
- name: params.projectName,
462
- mode: "auto"
806
+ name: params.projectName
463
807
  },
464
808
  memory: {
465
809
  short_term_max: 10,
466
810
  short_term_keep: 5,
467
811
  schema_version: 2
468
812
  },
469
- test: {
470
- framework: "auto",
471
- command: ""
472
- },
473
813
  tasks: {
474
814
  auto_archive_days: 30
475
815
  },
@@ -486,11 +826,11 @@ async function writeConfigYaml(filePath, config) {
486
826
  await writeTextFile(filePath, stringifyConfig(config));
487
827
  }
488
828
  async function readConfigYaml(filePath) {
489
- const content = await readFile2(filePath, "utf8");
829
+ const content = await readFile3(filePath, "utf8");
490
830
  return YAML.parse(content);
491
831
  }
492
832
  async function updateConfigYaml(filePath, updater) {
493
- const content = await readFile2(filePath, "utf8");
833
+ const content = await readFile3(filePath, "utf8");
494
834
  const document = parseDocument(content);
495
835
  const config = document.toJSON();
496
836
  updater(config);
@@ -512,87 +852,10 @@ async function updateHarnessVersion(filePath, version) {
512
852
  });
513
853
  }
514
854
 
515
- // src/commands/platforms.ts
516
- import { cancel, confirm, multiselect } from "@clack/prompts";
517
- function parseAgentList(agentList) {
518
- const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
519
- if (values.length === 0) {
520
- throw new Error("No agent platform specified.");
521
- }
522
- const invalid = values.filter((value) => !isAgentPlatform(value));
523
- if (invalid.length > 0) {
524
- throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
525
- }
526
- return [...new Set(values)];
527
- }
528
- async function resolvePlatforms(opts, defaults = ["claude-code"]) {
529
- if (opts.agent) {
530
- return parseAgentList(opts.agent);
531
- }
532
- if (opts.yes) {
533
- return defaults;
534
- }
535
- let selectedDefaults = defaults;
536
- while (true) {
537
- const result = await multiselect({
538
- message: "Select agent platforms (Space to toggle, Enter to review)",
539
- options: AGENT_PLATFORMS.map((platform) => ({
540
- label: PLATFORM_META[platform].label,
541
- value: platform
542
- })),
543
- initialValues: selectedDefaults,
544
- required: true
545
- });
546
- if (typeof result === "symbol") {
547
- cancel("Platform selection cancelled.");
548
- process.exit(1);
549
- }
550
- const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
551
- const shouldInstall = await confirm({
552
- message: `Install Easy Coding for: ${labels}?`,
553
- initialValue: true
554
- });
555
- if (typeof shouldInstall === "symbol") {
556
- cancel("Platform selection cancelled.");
557
- process.exit(1);
558
- }
559
- if (shouldInstall) {
560
- return result;
561
- }
562
- selectedDefaults = result;
563
- }
564
- }
565
-
566
- // src/commands/add-agent.ts
567
- async function addAgent(opts) {
568
- renderBanner();
569
- const cwd = process.cwd();
570
- const configPath = path8.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
571
- if (!await pathExists(configPath)) {
572
- throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
573
- }
574
- const config = await readConfigYaml(configPath);
575
- const platforms = await resolvePlatforms(opts, ["claude-code"]);
576
- const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
577
- if (toInstall.length === 0) {
578
- outro(chalk2.yellow("All selected agent platforms are already installed."));
579
- return;
580
- }
581
- for (const platform of toInstall) {
582
- await CONFIGURATORS[platform](cwd);
583
- }
584
- await addAgentsToConfig(configPath, toInstall);
585
- outro(chalk2.green(`Added agent platforms: ${toInstall.join(", ")}`));
586
- }
587
-
588
- // src/commands/init.ts
589
- import { note, outro as outro2 } from "@clack/prompts";
590
- import chalk3 from "chalk";
591
-
592
855
  // src/utils/gitignore.ts
593
- import path9 from "path";
856
+ import path10 from "path";
594
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") {
595
- const gitignorePath = path9.join(cwd, ".gitignore");
858
+ const gitignorePath = path10.join(cwd, ".gitignore");
596
859
  const current = await readTextIfExists(gitignorePath) ?? "";
597
860
  const lines = current.split(/\r?\n/).map((line) => line.trim());
598
861
  if (lines.includes(entry)) {
@@ -603,93 +866,36 @@ async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550
603
866
  await writeTextFile(gitignorePath, next);
604
867
  return true;
605
868
  }
606
- async function ensureEasyCodingStateIgnored(cwd) {
607
- return ensureGitignoreEntry(cwd, STATE_GITIGNORE_ENTRY);
869
+ async function ensureEasyCodingSessionsIgnored(cwd) {
870
+ return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
608
871
  }
609
872
 
610
- // src/utils/install-state.ts
611
- import { readdir as readdir2 } from "fs/promises";
612
- import path10 from "path";
613
- var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
614
- async function detectEasyCodingInstallState(cwd) {
615
- const easyCodingDir = path10.join(cwd, EASY_CODING_DIR);
616
- if (!await pathExists(easyCodingDir)) {
617
- return { kind: "fresh", easyCodingDir };
618
- }
619
- const configPath = path10.join(easyCodingDir, CONFIG_FILE);
620
- if (await pathExists(configPath)) {
621
- return { kind: "installed", easyCodingDir, configPath };
622
- }
623
- const legacyAssets = await detectLegacyAssets(easyCodingDir);
624
- if (legacyAssets.length === 0) {
625
- return { kind: "unknown", easyCodingDir };
626
- }
627
- return {
628
- kind: "legacy",
629
- easyCodingDir,
630
- legacyAssets,
631
- missingHarnessFiles: [relativeConfigPath(), relativeProjectInitTaskPath()]
632
- };
633
- }
634
- async function detectLegacyAssets(easyCodingDir) {
635
- const assets = [];
636
- for (const file of LEGACY_ROOT_FILES) {
637
- if (await pathExists(path10.join(easyCodingDir, file))) {
638
- assets.push(relativeEasyCodingPath(file));
639
- }
640
- }
641
- if (await pathExists(path10.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
642
- assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
643
- }
644
- const shortMemoryFiles = await listMarkdownFiles(path10.join(easyCodingDir, "memory", "short"));
645
- assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
646
- for (const dir of ["spec", "prototype"]) {
647
- if (await hasAnyDirectoryEntry(path10.join(easyCodingDir, dir))) {
648
- assets.push(relativeEasyCodingPath(dir));
649
- }
650
- }
651
- return assets.sort();
652
- }
653
- async function listMarkdownFiles(dir) {
654
- if (!await isDirectory(dir)) {
655
- return [];
656
- }
657
- const entries = await readdir2(dir, { withFileTypes: true });
658
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
659
- }
660
- async function hasAnyDirectoryEntry(dir) {
661
- if (!await isDirectory(dir)) {
662
- return false;
663
- }
664
- return (await readdir2(dir)).length > 0;
665
- }
666
- function relativeEasyCodingPath(...segments) {
667
- return path10.posix.join(EASY_CODING_DIR, ...segments);
668
- }
669
- function relativeConfigPath() {
670
- return path10.posix.join(EASY_CODING_DIR, CONFIG_FILE);
671
- }
672
- function relativeProjectInitTaskPath() {
673
- return path10.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
674
- }
675
-
676
- // src/utils/runtime-scaffold.ts
677
- import path11 from "path";
678
- async function writeRuntimeScaffold(cwd, agents) {
679
- const easyCodingDir = path11.join(cwd, EASY_CODING_DIR);
680
- await ensureDir(easyCodingDir);
681
- const configPath = path11.join(easyCodingDir, CONFIG_FILE);
682
- if (!await pathExists(configPath)) {
683
- const projectName = path11.basename(cwd);
684
- await writeConfigYaml(
685
- configPath,
686
- createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
687
- );
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
+ );
688
885
  }
689
886
  await ensureDir(path11.join(easyCodingDir, "tasks"));
887
+ await ensureDir(path11.join(easyCodingDir, SESSIONS_DIR));
690
888
  await ensureDir(path11.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
691
889
  await ensureDir(path11.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
692
890
  await writeMemoryScaffold(easyCodingDir);
891
+ await writeTemplatesScaffold(easyCodingDir);
892
+ }
893
+ async function writeTemplatesScaffold(easyCodingDir) {
894
+ const templatesDir = path11.join(easyCodingDir, TEMPLATES_DIR);
895
+ await ensureDir(templatesDir);
896
+ const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
897
+ const dest = path11.join(templatesDir, "dev-spec-skeleton.md");
898
+ await writeTextFile(dest, await readTextFile(src));
693
899
  }
694
900
  async function writeMemoryScaffold(easyCodingDir) {
695
901
  const memoryDir = path11.join(easyCodingDir, MEMORY_DIR);
@@ -703,6 +909,11 @@ async function writeMemoryScaffold(easyCodingDir) {
703
909
  const templatePath = getTemplatePath("runtime", "memory", "long", file);
704
910
  await writeTextFile(destination, await readTextFile(templatePath));
705
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
+ }
706
917
  }
707
918
 
708
919
  // src/utils/task-json.ts
@@ -714,6 +925,8 @@ function createProjectInitTask(params) {
714
925
  status: "PENDING",
715
926
  created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
716
927
  created_by: "cli-init",
928
+ last_agent: "cli",
929
+ stage_history: [],
717
930
  context: {
718
931
  agents_installed: params.agents,
719
932
  cli_version: VERSION,
@@ -746,6 +959,14 @@ async function writeProjectInitTask(cwd, agents, options = {}) {
746
959
  })
747
960
  );
748
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
+ }
749
970
  async function listTasks(cwd) {
750
971
  const tasksDir = path12.join(cwd, EASY_CODING_DIR, TASKS_DIR);
751
972
  if (!await pathExists(tasksDir)) {
@@ -778,6 +999,672 @@ function isActiveTask(task) {
778
999
  return task.status !== "COMPLETE" && task.status !== "CLOSED";
779
1000
  }
780
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
+
781
1668
  // src/commands/init.ts
782
1669
  async function init(opts) {
783
1670
  renderBanner();
@@ -794,31 +1681,37 @@ async function init(opts) {
794
1681
  );
795
1682
  }
796
1683
  const platforms = await resolvePlatforms(opts, ["claude-code"]);
1684
+ const artifacts = [];
797
1685
  for (const platform of platforms) {
798
- await CONFIGURATORS[platform](cwd);
1686
+ artifacts.push(...await CONFIGURATORS[platform](cwd));
799
1687
  }
800
1688
  await writeRuntimeScaffold(cwd, platforms);
1689
+ await writeInstallManifest(cwd, {
1690
+ harnessVersion: VERSION,
1691
+ agents: platforms,
1692
+ artifacts
1693
+ });
801
1694
  await writeProjectInitTask(cwd, platforms, {
802
1695
  initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
803
1696
  legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
804
1697
  legacyMissingHarnessFiles: installState.kind === "legacy" ? installState.missingHarnessFiles : void 0
805
1698
  });
806
- await ensureEasyCodingStateIgnored(cwd);
1699
+ await ensureEasyCodingSessionsIgnored(cwd);
807
1700
  const triggers = platforms.map(
808
1701
  (platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
809
1702
  ).join("\n");
810
1703
  note(triggers, "Next step");
811
- 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."));
812
1705
  }
813
1706
 
814
1707
  // src/commands/status.ts
815
- import path15 from "path";
816
- import chalk4 from "chalk";
1708
+ import path18 from "path";
1709
+ import chalk5 from "chalk";
817
1710
 
818
1711
  // src/utils/compare-versions.ts
819
- import path13 from "path";
1712
+ import path16 from "path";
820
1713
  function normalize(version) {
821
- const [core] = version.split("-");
1714
+ const [core] = String(version ?? "").split("-");
822
1715
  return core.split(".").map((part) => {
823
1716
  const parsed = Number.parseInt(part, 10);
824
1717
  return Number.isNaN(parsed) ? 0 : parsed;
@@ -840,11 +1733,16 @@ function isVersionBehind(installed, current = VERSION) {
840
1733
  return compareVersions(installed, current) === -1;
841
1734
  }
842
1735
  async function checkForUpgrade(cwd) {
843
- const configPath = path13.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1736
+ const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
844
1737
  if (!await pathExists(configPath)) {
845
1738
  return;
846
1739
  }
847
- const config = await readConfigYaml(configPath);
1740
+ let config;
1741
+ try {
1742
+ config = await readConfigYaml(configPath);
1743
+ } catch {
1744
+ return;
1745
+ }
848
1746
  const installed = String(config.harness_version ?? "");
849
1747
  if (installed && isVersionBehind(installed)) {
850
1748
  process.stderr.write(
@@ -854,13 +1752,19 @@ async function checkForUpgrade(cwd) {
854
1752
  }
855
1753
  }
856
1754
 
857
- // src/utils/state-json.ts
858
- import path14 from "path";
859
- function getStateJsonPath(cwd) {
860
- return path14.join(cwd, EASY_CODING_DIR, STATE_FILE);
861
- }
862
- async function readStateJson(cwd) {
863
- const content = await readTextIfExists(getStateJsonPath(cwd));
1755
+ // src/utils/session.ts
1756
+ import { readdir as readdir6 } from "fs/promises";
1757
+ import path17 from "path";
1758
+ var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
1759
+ function getSessionDir(cwd) {
1760
+ return path17.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
1761
+ }
1762
+ function getSessionFilePath(cwd, ppid) {
1763
+ const pid = ppid ?? process.ppid;
1764
+ return path17.join(getSessionDir(cwd), `${pid}.json`);
1765
+ }
1766
+ async function readSessionFile(cwd, ppid) {
1767
+ const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
864
1768
  if (!content) {
865
1769
  return null;
866
1770
  }
@@ -871,7 +1775,7 @@ async function readStateJson(cwd) {
871
1775
  async function status() {
872
1776
  renderBanner();
873
1777
  const cwd = process.cwd();
874
- const configPath = path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1778
+ const configPath = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
875
1779
  if (!await pathExists(configPath)) {
876
1780
  throw new Error("No easy-coding harness found in this project.");
877
1781
  }
@@ -879,34 +1783,40 @@ async function status() {
879
1783
  const tasks = await listTasks(cwd);
880
1784
  const taskCounts = summarizeTaskStatuses(tasks);
881
1785
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
882
- const state = await readStateJson(cwd);
1786
+ const session = await readSessionFile(cwd);
883
1787
  const versionRelation = compareVersions(config.harness_version, VERSION);
884
- console.log(chalk4.bold("Harness"));
1788
+ console.log(chalk5.bold("Harness"));
885
1789
  console.log(` version: ${config.harness_version}`);
886
1790
  console.log(` cli: ${VERSION}`);
887
1791
  if (versionRelation === -1) {
888
- console.log(chalk4.yellow(" upgrade: available"));
1792
+ console.log(chalk5.yellow(" upgrade: available"));
889
1793
  } else if (versionRelation === 1) {
890
- console.log(chalk4.red(" upgrade: CLI is older than project harness"));
1794
+ console.log(chalk5.red(" upgrade: CLI is older than project harness"));
891
1795
  } else {
892
1796
  console.log(" upgrade: up to date");
893
1797
  }
894
1798
  console.log(` agents: ${config.agents.join(", ") || "(none)"}`);
895
- console.log(` project: ${config.project.name} (${config.project.mode})`);
1799
+ console.log(` project: ${config.project.name}`);
896
1800
  console.log("");
897
- console.log(chalk4.bold("State"));
898
- if (state) {
899
- console.log(` current_stage: ${state.current_stage}`);
900
- console.log(` current_task: ${state.current_task ?? "(none)"}`);
901
- console.log(` last_agent: ${state.last_agent}`);
1801
+ console.log(chalk5.bold("Session"));
1802
+ if (session?.current_task) {
1803
+ const taskPath = getTaskJsonPath(cwd, session.current_task);
1804
+ if (await pathExists(taskPath)) {
1805
+ const task = await readTaskJson(taskPath);
1806
+ console.log(` current_task: ${session.current_task}`);
1807
+ console.log(` current_stage: ${task.status}`);
1808
+ console.log(` last_agent: ${task.last_agent}`);
1809
+ } else {
1810
+ console.log(` current_task: ${session.current_task} (task.json missing)`);
1811
+ }
902
1812
  } else {
903
- console.log(" state.json: not created yet");
1813
+ console.log(" no active session");
904
1814
  }
905
1815
  console.log("");
906
- console.log(chalk4.bold("Tasks"));
1816
+ console.log(chalk5.bold("Tasks"));
907
1817
  console.log(` total: ${tasks.length}`);
908
- for (const [status2, count] of Object.entries(taskCounts)) {
909
- console.log(` ${status2}: ${count}`);
1818
+ for (const [taskStatus, count] of Object.entries(taskCounts)) {
1819
+ console.log(` ${taskStatus}: ${count}`);
910
1820
  }
911
1821
  if (activeTasks.length > 0) {
912
1822
  console.log(" active:");
@@ -918,52 +1828,69 @@ async function status() {
918
1828
  }
919
1829
 
920
1830
  // src/commands/upgrade.ts
921
- import path16 from "path";
922
- import { cancel as cancel2, confirm as confirm2, outro as outro3 } from "@clack/prompts";
923
- import chalk5 from "chalk";
1831
+ import path19 from "path";
1832
+ import { cancel as cancel3, confirm as confirm3, outro as outro4 } from "@clack/prompts";
1833
+ import chalk6 from "chalk";
924
1834
  async function upgrade(opts) {
925
1835
  renderBanner();
926
1836
  const cwd = process.cwd();
927
- const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
1837
+ const configPath = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
928
1838
  if (!await pathExists(configPath)) {
929
1839
  throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
930
1840
  }
931
1841
  const config = await readConfigYaml(configPath);
932
- const relation = compareVersions(config.harness_version, VERSION);
1842
+ const installedVersion = String(config.harness_version ?? "");
1843
+ const hasAgents = Array.isArray(config.agents) && config.agents.length > 0;
1844
+ if (!installedVersion || !hasAgents) {
1845
+ throw new Error(
1846
+ "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."
1847
+ );
1848
+ }
1849
+ const relation = compareVersions(installedVersion, VERSION);
933
1850
  if (relation === 0) {
934
- outro3(chalk5.green(`easy-coding harness is already up to date (${VERSION}).`));
1851
+ outro4(chalk6.green(`easy-coding harness is already up to date (${VERSION}).`));
935
1852
  return;
936
1853
  }
937
1854
  if (relation === 1) {
938
1855
  throw new Error(
939
- `Project harness version ${config.harness_version} is newer than CLI ${VERSION}. Update the CLI first.`
1856
+ `Project harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
940
1857
  );
941
1858
  }
942
1859
  const summary = [
943
- `Upgrade: ${config.harness_version} -> ${VERSION}`,
1860
+ `Upgrade: ${installedVersion || "unknown"} -> ${VERSION}`,
944
1861
  `Installed agents: ${config.agents.join(", ")}`,
945
- "Will overwrite managed skills, hooks, agents, and generated main-constraint regions.",
946
- "Will not touch tasks, memory, state, spec, or project knowledge files."
1862
+ "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
1863
+ "Will update project-init task to recommend ec-init re-run for version adaptation.",
1864
+ "Will not touch memory, state, spec, or project knowledge files."
947
1865
  ].join("\n");
948
1866
  if (opts.dryRun) {
949
1867
  console.log(summary);
950
1868
  return;
951
1869
  }
952
1870
  if (!opts.yes) {
953
- const shouldUpgrade = await confirm2({
1871
+ const shouldUpgrade = await confirm3({
954
1872
  message: "Apply this harness upgrade?",
955
1873
  initialValue: true
956
1874
  });
957
1875
  if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
958
- cancel2("Upgrade cancelled.");
1876
+ cancel3("Upgrade cancelled.");
959
1877
  return;
960
1878
  }
961
1879
  }
1880
+ const artifacts = [];
962
1881
  for (const platform of config.agents) {
963
- await CONFIGURATORS[platform](cwd);
1882
+ artifacts.push(...await CONFIGURATORS[platform](cwd));
964
1883
  }
1884
+ await writeRuntimeScaffold(cwd, config.agents);
1885
+ await writeInstallManifest(cwd, {
1886
+ harnessVersion: VERSION,
1887
+ agents: config.agents,
1888
+ artifacts
1889
+ });
1890
+ await ensureEasyCodingSessionsIgnored(cwd);
965
1891
  await updateHarnessVersion(configPath, VERSION);
966
- outro3(chalk5.green(`easy-coding harness upgraded to ${VERSION}.`));
1892
+ await setPendingInitSince(cwd, VERSION);
1893
+ outro4(chalk6.green(`easy-coding harness upgraded to ${VERSION}.`));
967
1894
  }
968
1895
 
969
1896
  // src/cli.ts
@@ -972,7 +1899,7 @@ function withErrorHandling(fn) {
972
1899
  try {
973
1900
  await fn(opts);
974
1901
  } catch (error) {
975
- console.error(chalk6.red("Error:"), error instanceof Error ? error.message : error);
1902
+ console.error(chalk7.red("Error:"), error instanceof Error ? error.message : error);
976
1903
  if (process.env.EC_DEBUG && error instanceof Error) {
977
1904
  console.error(error.stack);
978
1905
  }
@@ -987,5 +1914,6 @@ program.command("init").description("Initialize easy-coding harness in current p
987
1914
  program.command("add-agent").description("Add agent platform support to an existing project").option("--agent <list>", "Comma-separated platforms to add").action(withErrorHandling(addAgent));
988
1915
  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));
989
1916
  program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
1917
+ 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));
990
1918
  program.parse();
991
1919
  //# sourceMappingURL=cli.js.map