easy-coding-harness 1.1.0-beta.0 → 1.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -63,6 +63,8 @@ function colorizeBanner(art) {
63
63
  // src/utils/config-yaml.ts
64
64
  import { randomUUID } from "crypto";
65
65
  import { readFile as readFile2 } from "fs/promises";
66
+ import os from "os";
67
+ import path3 from "path";
66
68
  import YAML, { isScalar, isSeq, parseDocument } from "yaml";
67
69
 
68
70
  // src/utils/file-writer.ts
@@ -103,10 +105,75 @@ async function isDirectory(filePath) {
103
105
  }
104
106
 
105
107
  // src/utils/config-yaml.ts
106
- var CONFIG_SCHEMA_VERSION = 5;
107
- var DEFAULT_TDD_COVERAGE_THRESHOLD = 90;
108
+ var CONFIG_SCHEMA_VERSION = 6;
109
+ var DEFAULT_UT_COVERAGE_THRESHOLD = 90;
110
+ var UNIT_TEST_MODES = ["none", "ut", "tdd"];
108
111
  var APPROVAL_MODES = ["approve", "guard", "confirm", "auto"];
112
+ var COOPERATE_MODES = ["default", "dispatch"];
109
113
  var CONFIGURED_WORKFLOW_MODES = ["adaptive", "fast", "standard", "strict"];
114
+ var BEHAVIOR_DEFAULTS = {
115
+ approval_mode: "guard",
116
+ cooperate_mode: "default",
117
+ unit_test_mode: "none",
118
+ ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD
119
+ };
120
+ var BEHAVIOR_KEYS = Object.keys(BEHAVIOR_DEFAULTS);
121
+ function localConfigPath() {
122
+ return path3.join(os.homedir(), ".easy-coding", "config.yaml");
123
+ }
124
+ function validateBehaviorValue(key, value) {
125
+ const valid = key === "approval_mode" ? isApprovalMode(value) : key === "cooperate_mode" ? COOPERATE_MODES.includes(value) : key === "unit_test_mode" ? isUnitTestMode(value) : isUtCoverageThreshold(value);
126
+ if (!valid) throw new Error(`Invalid behavior.${key}: ${String(value)}`);
127
+ }
128
+ async function readLocalBehavior() {
129
+ let content;
130
+ try {
131
+ content = await readFile2(localConfigPath(), "utf8");
132
+ } catch (error) {
133
+ if (error.code === "ENOENT") return {};
134
+ throw error;
135
+ }
136
+ const settings = YAML.parse(content)?.behavior ?? {};
137
+ for (const key of BEHAVIOR_KEYS) {
138
+ if (settings[key] !== void 0) validateBehaviorValue(key, settings[key]);
139
+ }
140
+ return Object.fromEntries(
141
+ BEHAVIOR_KEYS.filter((key) => settings[key] !== void 0).map((key) => [key, settings[key]])
142
+ );
143
+ }
144
+ function resolveBehaviorSettings(project, local = {}, session = {}) {
145
+ const values = { ...BEHAVIOR_DEFAULTS };
146
+ const sources = {};
147
+ for (const key of BEHAVIOR_KEYS) {
148
+ const layer = session[key] !== void 0 ? "session" : local[key] !== void 0 ? "local" : project[key] !== void 0 ? "project" : "default";
149
+ const value = layer === "session" ? session[key] : layer === "local" ? local[key] : layer === "project" ? project[key] : BEHAVIOR_DEFAULTS[key];
150
+ validateBehaviorValue(key, value);
151
+ Object.assign(values, { [key]: value });
152
+ sources[key] = layer;
153
+ }
154
+ return { values, sources };
155
+ }
156
+ async function writeBehaviorOverrides(filePath, changes) {
157
+ let content = "";
158
+ try {
159
+ content = await readFile2(filePath, "utf8");
160
+ } catch (error) {
161
+ if (error.code !== "ENOENT") throw error;
162
+ }
163
+ const document = parseDocument(content);
164
+ for (const key of BEHAVIOR_KEYS) {
165
+ const value = changes[key];
166
+ if (value === void 0) continue;
167
+ if (value === null) {
168
+ if (document.hasIn(["behavior", key])) document.deleteIn(["behavior", key]);
169
+ } else {
170
+ validateBehaviorValue(key, value);
171
+ document.setIn(["behavior", key], value);
172
+ }
173
+ }
174
+ if (!content && !document.has("behavior")) return;
175
+ await writeTextFile(filePath, document.toString());
176
+ }
110
177
  function createDefaultConfig(params) {
111
178
  const config2 = {
112
179
  version: CONFIG_SCHEMA_VERSION,
@@ -127,8 +194,9 @@ function createDefaultConfig(params) {
127
194
  behavior: {
128
195
  approval_mode: "guard",
129
196
  workflow_mode: "adaptive",
130
- tdd_enabled: false,
131
- tdd_coverage_threshold: DEFAULT_TDD_COVERAGE_THRESHOLD
197
+ unit_test_mode: "none",
198
+ ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD,
199
+ cooperate_mode: "default"
132
200
  }
133
201
  };
134
202
  if (params.supermodule) {
@@ -182,43 +250,66 @@ function isApprovalMode(value) {
182
250
  function isConfiguredWorkflowMode(value) {
183
251
  return typeof value === "string" && CONFIGURED_WORKFLOW_MODES.includes(value);
184
252
  }
185
- function isTddCoverageThreshold(value) {
253
+ function isUtCoverageThreshold(value) {
186
254
  return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 100;
187
255
  }
256
+ function isUnitTestMode(value) {
257
+ return typeof value === "string" && UNIT_TEST_MODES.includes(value);
258
+ }
259
+ function migrateUnitTestSettings(record) {
260
+ let changed = false;
261
+ if ("tdd_enabled" in record) {
262
+ if (!("unit_test_mode" in record) && typeof record.tdd_enabled === "boolean") {
263
+ record.unit_test_mode = record.tdd_enabled ? "tdd" : "none";
264
+ }
265
+ Reflect.deleteProperty(record, "tdd_enabled");
266
+ changed = true;
267
+ }
268
+ if ("tdd_coverage_threshold" in record) {
269
+ record.ut_coverage_threshold ??= record.tdd_coverage_threshold;
270
+ Reflect.deleteProperty(record, "tdd_coverage_threshold");
271
+ changed = true;
272
+ }
273
+ return changed;
274
+ }
188
275
  function resolveLegacyBehavior(config2) {
189
276
  const behavior = config2.behavior ?? {};
190
277
  const legacyLite = behavior.confirm_mode === "lite";
191
278
  const approvalMode = isApprovalMode(behavior.approval_mode) ? behavior.approval_mode : isApprovalMode(behavior.confirm_mode) ? behavior.confirm_mode : behavior.auto_mode === true ? "auto" : behavior.strict_confirm === true ? "approve" : "guard";
192
279
  const workflowMode = isConfiguredWorkflowMode(behavior.workflow_mode) ? behavior.workflow_mode : legacyLite ? "fast" : "adaptive";
193
- const supportsTddThreshold = Number(config2.version) >= 4;
194
- const tddEnabled = supportsTddThreshold && behavior.tdd_enabled === true;
195
- const tddCoverageThreshold = supportsTddThreshold && isTddCoverageThreshold(behavior.tdd_coverage_threshold) ? behavior.tdd_coverage_threshold : DEFAULT_TDD_COVERAGE_THRESHOLD;
196
- return { approvalMode, workflowMode, tddEnabled, tddCoverageThreshold };
280
+ const unitTestMode = isUnitTestMode(behavior.unit_test_mode) ? behavior.unit_test_mode : Number(config2.version) >= 4 && behavior.tdd_enabled === true ? "tdd" : "none";
281
+ const configuredThreshold = behavior.ut_coverage_threshold ?? (Number(config2.version) >= 4 ? behavior.tdd_coverage_threshold : void 0);
282
+ const utCoverageThreshold = isUtCoverageThreshold(configuredThreshold) ? configuredThreshold : DEFAULT_UT_COVERAGE_THRESHOLD;
283
+ return { approvalMode, workflowMode, unitTestMode, utCoverageThreshold };
197
284
  }
198
- async function setBehaviorModes(filePath, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold) {
199
- if (tddCoverageThreshold !== void 0 && !isTddCoverageThreshold(tddCoverageThreshold)) {
200
- throw new Error("TDD coverage threshold must be an integer from 1 to 100.");
285
+ async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold, cooperateMode) {
286
+ if (unitTestMode !== void 0 && !isUnitTestMode(unitTestMode)) {
287
+ throw new Error("Unit test mode must be none, ut, or tdd.");
288
+ }
289
+ if (utCoverageThreshold !== void 0 && !isUtCoverageThreshold(utCoverageThreshold)) {
290
+ throw new Error("Unit test coverage threshold must be an integer from 1 to 100.");
201
291
  }
202
292
  return updateConfigYaml(filePath, (config2) => {
203
293
  const legacyBehavior = config2.behavior ?? {};
204
294
  const resolvedBehavior = resolveLegacyBehavior(config2);
205
295
  const behavior = Object.fromEntries(
206
296
  Object.entries(legacyBehavior).filter(
207
- ([key]) => key !== "strict_confirm" && key !== "auto_mode" && key !== "confirm_mode" && key !== "approval_mode" && key !== "workflow_mode" && key !== "tdd_enabled" && key !== "tdd_coverage_threshold"
297
+ ([key]) => key !== "strict_confirm" && key !== "auto_mode" && key !== "confirm_mode" && key !== "approval_mode" && key !== "workflow_mode" && key !== "tdd_enabled" && key !== "tdd_coverage_threshold" && key !== "unit_test_mode" && key !== "ut_coverage_threshold"
208
298
  )
209
299
  );
210
300
  behavior.approval_mode = approvalMode;
211
301
  behavior.workflow_mode = workflowMode;
212
- behavior.tdd_enabled = tddEnabled ?? resolvedBehavior.tddEnabled;
213
- behavior.tdd_coverage_threshold = tddCoverageThreshold ?? resolvedBehavior.tddCoverageThreshold;
302
+ behavior.unit_test_mode = unitTestMode ?? resolvedBehavior.unitTestMode;
303
+ behavior.ut_coverage_threshold = utCoverageThreshold ?? resolvedBehavior.utCoverageThreshold;
304
+ behavior.cooperate_mode = cooperateMode ?? legacyBehavior.cooperate_mode ?? "default";
214
305
  config2.behavior = behavior;
215
306
  config2.version = CONFIG_SCHEMA_VERSION;
216
307
  });
217
308
  }
218
309
  async function migrateBehaviorConfig(filePath) {
219
310
  const config2 = await readConfigYaml(filePath);
220
- const { approvalMode, workflowMode, tddEnabled, tddCoverageThreshold } = resolveLegacyBehavior(config2);
221
- return setBehaviorModes(filePath, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold);
311
+ const { approvalMode, workflowMode, unitTestMode, utCoverageThreshold } = resolveLegacyBehavior(config2);
312
+ return setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold);
222
313
  }
223
314
  async function ensureProjectId(filePath) {
224
315
  let projectId = "";
@@ -243,7 +334,7 @@ function createProjectId() {
243
334
  }
244
335
 
245
336
  // src/utils/gitignore.ts
246
- import path3 from "path";
337
+ import path4 from "path";
247
338
 
248
339
  // src/constants/paths.ts
249
340
  var EASY_CODING_DIR = ".easy-coding";
@@ -267,7 +358,7 @@ var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness gene
267
358
 
268
359
  // src/utils/gitignore.ts
269
360
  async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Personal runtime state; do not commit") {
270
- const gitignorePath = path3.join(cwd, ".gitignore");
361
+ const gitignorePath = path4.join(cwd, ".gitignore");
271
362
  const current = await readTextIfExists(gitignorePath) ?? "";
272
363
  const lines = current.split(/\r?\n/).map((line) => line.trim());
273
364
  if (lines.includes(entry)) {
@@ -292,7 +383,7 @@ async function ensureHookBytecodeIgnored(cwd) {
292
383
  // src/utils/install-manifest.ts
293
384
  import { createHash } from "crypto";
294
385
  import { readFile as readFile3, rmdir, unlink } from "fs/promises";
295
- import path4 from "path";
386
+ import path5 from "path";
296
387
 
297
388
  // src/types/platform.ts
298
389
  var pythonCmd = process.platform === "win32" ? "python" : "python3";
@@ -464,12 +555,12 @@ async function writeInstallManifest(cwd, params) {
464
555
  constraint_regions: [...constraintRegions.values()].sort(byPath)
465
556
  };
466
557
  await writeTextFile(
467
- path4.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
558
+ path5.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
468
559
  JSON.stringify(manifest, null, 2)
469
560
  );
470
561
  }
471
562
  async function readInstallManifest(cwd) {
472
- const manifestPath2 = path4.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
563
+ const manifestPath2 = path5.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
473
564
  const content = await readTextIfExists(manifestPath2);
474
565
  if (content === null) {
475
566
  return null;
@@ -511,7 +602,7 @@ async function pruneRetiredManagedFiles(cwd, previous, artifacts) {
511
602
  await unlink(filePath);
512
603
  removed.push(file.path);
513
604
  try {
514
- await rmdir(path4.dirname(filePath));
605
+ await rmdir(path5.dirname(filePath));
515
606
  } catch (error) {
516
607
  if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) {
517
608
  throw error;
@@ -530,10 +621,10 @@ function manifestPath(cwd, projectPath) {
530
621
  return resolveProjectPath(cwd, projectPath);
531
622
  }
532
623
  function toProjectPath(cwd, filePath) {
533
- const root = path4.resolve(cwd);
534
- const resolved = path4.resolve(filePath);
624
+ const root = path5.resolve(cwd);
625
+ const resolved = path5.resolve(filePath);
535
626
  assertPathInsideProject(root, resolved, filePath);
536
- return path4.relative(root, resolved).split(path4.sep).join("/");
627
+ return path5.relative(root, resolved).split(path5.sep).join("/");
537
628
  }
538
629
  function normalizeCommand(command) {
539
630
  return command.replace(/\\/g, "/").trim().replace(/\s+/g, " ");
@@ -545,17 +636,17 @@ async function sha256File(filePath) {
545
636
  function resolveProjectPath(cwd, projectPath) {
546
637
  const normalized = projectPath.replace(/\\/g, "/");
547
638
  const parts = normalized.split("/");
548
- if (normalized.trim() === "" || path4.isAbsolute(projectPath) || path4.posix.isAbsolute(normalized) || path4.win32.isAbsolute(projectPath) || /^[A-Za-z]:/.test(projectPath) || parts.some((part) => part === "..")) {
639
+ if (normalized.trim() === "" || path5.isAbsolute(projectPath) || path5.posix.isAbsolute(normalized) || path5.win32.isAbsolute(projectPath) || /^[A-Za-z]:/.test(projectPath) || parts.some((part) => part === "..")) {
549
640
  throw new Error(`Unsafe install manifest path: ${projectPath}`);
550
641
  }
551
- const root = path4.resolve(cwd);
552
- const resolved = path4.resolve(root, normalized);
642
+ const root = path5.resolve(cwd);
643
+ const resolved = path5.resolve(root, normalized);
553
644
  assertPathInsideProject(root, resolved, projectPath);
554
645
  return resolved;
555
646
  }
556
647
  function assertPathInsideProject(root, resolvedPath, sourcePath) {
557
- const relative = path4.relative(root, resolvedPath);
558
- if (!relative || relative.startsWith("..") || path4.isAbsolute(relative)) {
648
+ const relative = path5.relative(root, resolvedPath);
649
+ if (!relative || relative.startsWith("..") || path5.isAbsolute(relative)) {
559
650
  throw new Error(`Unsafe install manifest path: ${sourcePath}`);
560
651
  }
561
652
  }
@@ -654,7 +745,7 @@ function isHookPythonPath(candidate) {
654
745
  }
655
746
  function normalizeHookPath(cwd, hookPath) {
656
747
  const normalized = hookPath.replace(/\\/g, "/").replace(/^\.\//, "");
657
- if (path4.isAbsolute(hookPath) || path4.posix.isAbsolute(normalized) || path4.win32.isAbsolute(hookPath)) {
748
+ if (path5.isAbsolute(hookPath) || path5.posix.isAbsolute(normalized) || path5.win32.isAbsolute(hookPath)) {
658
749
  return toProjectPath(cwd, hookPath);
659
750
  }
660
751
  return normalized;
@@ -664,97 +755,11 @@ function byPath(a, b) {
664
755
  }
665
756
 
666
757
  // src/utils/runtime-scaffold.ts
667
- import path6 from "path";
668
-
669
- // src/utils/template-paths.ts
670
- import { existsSync } from "fs";
671
- import path5 from "path";
672
- import { fileURLToPath as fileURLToPath2 } from "url";
673
- function getTemplateRoot() {
674
- const here = path5.dirname(fileURLToPath2(import.meta.url));
675
- const candidates = [
676
- path5.resolve(here, "../templates"),
677
- path5.resolve(here, "../../templates"),
678
- path5.resolve(process.cwd(), "src/templates"),
679
- path5.resolve(process.cwd(), "templates")
680
- ];
681
- const found = candidates.find((candidate) => existsSync(candidate));
682
- if (!found) {
683
- throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
684
- }
685
- return found;
686
- }
687
- function getTemplatePath(...segments) {
688
- return path5.join(getTemplateRoot(), ...segments);
689
- }
690
-
691
- // src/utils/runtime-scaffold.ts
692
- async function writeRuntimeScaffold(cwd, agents, opts = {}) {
693
- const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
694
- await ensureDir(easyCodingDir);
695
- const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
696
- let projectId = opts.projectId ?? createProjectId();
697
- if (!await pathExists(configPath2)) {
698
- const projectName = path6.basename(cwd);
699
- await writeConfigYaml(
700
- configPath2,
701
- createDefaultConfig({
702
- projectName,
703
- projectId,
704
- harnessVersion: VERSION,
705
- agents,
706
- supermodule: opts.supermodule
707
- })
708
- );
709
- } else {
710
- projectId = await ensureProjectId(configPath2);
711
- }
712
- await ensureDir(path6.join(easyCodingDir, "tasks"));
713
- await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
714
- await ensureDir(path6.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
715
- await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
716
- await writeMemoryScaffold(easyCodingDir);
717
- await writeTemplatesScaffold(easyCodingDir);
718
- await writeToolsScaffold(easyCodingDir);
719
- return projectId;
720
- }
721
- async function writeToolsScaffold(easyCodingDir) {
722
- const toolsDir = path6.join(easyCodingDir, TOOLS_DIR);
723
- await ensureDir(toolsDir);
724
- for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
725
- const src = getTemplatePath("runtime", "tools", file);
726
- await writeTextFile(path6.join(toolsDir, file), await readTextFile(src));
727
- }
728
- }
729
- async function writeTemplatesScaffold(easyCodingDir) {
730
- const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
731
- await ensureDir(templatesDir);
732
- const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
733
- const dest = path6.join(templatesDir, "dev-spec-skeleton.md");
734
- await writeTextFile(dest, await readTextFile(src));
735
- }
736
- async function writeMemoryScaffold(easyCodingDir) {
737
- const memoryDir = path6.join(easyCodingDir, MEMORY_DIR);
738
- await ensureDir(path6.join(memoryDir, "short"));
739
- await ensureDir(path6.join(memoryDir, "long"));
740
- for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
741
- const destination = path6.join(memoryDir, "long", file);
742
- if (await pathExists(destination)) {
743
- continue;
744
- }
745
- const templatePath = getTemplatePath("runtime", "memory", "long", file);
746
- await writeTextFile(destination, await readTextFile(templatePath));
747
- }
748
- const shortTemplateDest = path6.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
749
- if (!await pathExists(shortTemplateDest)) {
750
- const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
751
- await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
752
- }
753
- }
758
+ import path8 from "path";
754
759
 
755
760
  // src/utils/task-json.ts
756
761
  import { readdir } from "fs/promises";
757
- import path7 from "path";
762
+ import path6 from "path";
758
763
  var LEGACY_STAGE_MAP = {
759
764
  WAITING_CONFIRM: "ANALYSIS",
760
765
  MEMORY_SHORT: "MEMORY",
@@ -781,7 +786,7 @@ function createProjectInitTask(params) {
781
786
  };
782
787
  }
783
788
  function getTaskJsonPath(cwd, taskId) {
784
- return path7.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
789
+ return path6.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
785
790
  }
786
791
  async function writeTaskJson(filePath, task) {
787
792
  await writeTextFile(filePath, JSON.stringify(task, null, 2));
@@ -1006,9 +1011,10 @@ function migrateTaskWorkflowState(task) {
1006
1011
  task.workflow_mode_legacy = true;
1007
1012
  changed = true;
1008
1013
  }
1009
- if (isActive && taskType !== "project-init" && typeof task.tdd_enabled !== "boolean") {
1010
- task.tdd_enabled = false;
1011
- task.tdd_coverage_threshold ??= DEFAULT_TDD_COVERAGE_THRESHOLD;
1014
+ changed = migrateUnitTestSettings(task) || changed;
1015
+ if (isActive && taskType !== "project-init" && task.unit_test_mode === void 0) {
1016
+ task.unit_test_mode = "none";
1017
+ task.ut_coverage_threshold ??= DEFAULT_UT_COVERAGE_THRESHOLD;
1012
1018
  task.tdd_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
1013
1019
  task.tdd_confirmed_by = "upgrade-migration";
1014
1020
  changed = true;
@@ -1017,6 +1023,7 @@ function migrateTaskWorkflowState(task) {
1017
1023
  }
1018
1024
  function migrateSessionBehavior(session) {
1019
1025
  let changed = migrateAgentFields(session, ["agent", "last_agent"]);
1026
+ changed = migrateUnitTestSettings(session) || changed;
1020
1027
  const legacyMode = session.confirm_mode;
1021
1028
  const legacyLite = legacyMode === "lite";
1022
1029
  if (!["approve", "guard", "confirm", "auto"].includes(String(session.approval_mode ?? ""))) {
@@ -1092,16 +1099,16 @@ function migrationWorkflowMode(task, candidates, projectWorkflowMode) {
1092
1099
  );
1093
1100
  }
1094
1101
  async function taskFiles(cwd) {
1095
- const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1102
+ const tasksDir = path6.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1096
1103
  if (!await pathExists(tasksDir)) return [];
1097
1104
  const entries = await readdir(tasksDir, { withFileTypes: true });
1098
- return entries.filter((entry) => entry.isDirectory()).map((entry) => path7.join(tasksDir, entry.name, "task.json"));
1105
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => path6.join(tasksDir, entry.name, "task.json"));
1099
1106
  }
1100
1107
  async function sessionFiles(cwd) {
1101
- const sessionsDir = path7.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
1108
+ const sessionsDir = path6.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
1102
1109
  if (!await pathExists(sessionsDir)) return [];
1103
1110
  const entries = await readdir(sessionsDir, { withFileTypes: true });
1104
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path7.join(sessionsDir, entry.name));
1111
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path6.join(sessionsDir, entry.name));
1105
1112
  }
1106
1113
  async function readJsonRecord(filePath) {
1107
1114
  try {
@@ -1116,7 +1123,7 @@ async function hasLegacyWorkflowState(cwd) {
1116
1123
  if (!await pathExists(filePath)) continue;
1117
1124
  const task = await readJsonRecord(filePath);
1118
1125
  if (!task) continue;
1119
- if (hasLegacyTaskAgentIdentities(task) || isLegacyStage(task.status) || "verification_checkpoint" in task || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && ["analysis", "doc", "report"].includes(String(task.type ?? "").toLowerCase()) || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && String(task.type ?? "") !== "project-init" && !["fast", "standard", "strict"].includes(String(task.workflow_mode ?? "")) || Array.isArray(task.stage_history) && task.stage_history.some(
1126
+ if (hasLegacyTaskAgentIdentities(task) || isLegacyStage(task.status) || "verification_checkpoint" in task || "tdd_enabled" in task || "tdd_coverage_threshold" in task || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && ["analysis", "doc", "report"].includes(String(task.type ?? "").toLowerCase()) || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && String(task.type ?? "") !== "project-init" && !["fast", "standard", "strict"].includes(String(task.workflow_mode ?? "")) || Array.isArray(task.stage_history) && task.stage_history.some(
1120
1127
  (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && isLegacyStage(entry.stage)
1121
1128
  )) {
1122
1129
  return true;
@@ -1128,7 +1135,7 @@ async function hasLegacyWorkflowState(cwd) {
1128
1135
  if (["agent", "last_agent"].some((field) => {
1129
1136
  const migrated = migratedAgentIdentity(session[field]);
1130
1137
  return migrated !== void 0 && migrated !== session[field];
1131
- }) || isLegacyStage(session.last_seen_stage) || "confirm_mode" in session) {
1138
+ }) || isLegacyStage(session.last_seen_stage) || "confirm_mode" in session || "tdd_enabled" in session || "tdd_coverage_threshold" in session) {
1132
1139
  return true;
1133
1140
  }
1134
1141
  }
@@ -1139,7 +1146,7 @@ async function migrateLegacyWorkflowState(cwd) {
1139
1146
  let sessionsUpdated = 0;
1140
1147
  const updatedTaskPaths = /* @__PURE__ */ new Set();
1141
1148
  let projectWorkflowMode = "adaptive";
1142
- const configPath2 = path7.join(cwd, EASY_CODING_DIR, "config.yaml");
1149
+ const configPath2 = path6.join(cwd, EASY_CODING_DIR, "config.yaml");
1143
1150
  if (await pathExists(configPath2)) {
1144
1151
  try {
1145
1152
  const config2 = await readConfigYaml(configPath2);
@@ -1194,7 +1201,7 @@ async function migrateLegacyWorkflowState(cwd) {
1194
1201
  if (!task) continue;
1195
1202
  const migrationOwned = task.workflow_mode_legacy === true && task.workflow_mode_confirmed_by === "upgrade-migration";
1196
1203
  if (!migrationOwned) continue;
1197
- const taskId = path7.basename(path7.dirname(filePath));
1204
+ const taskId = path6.basename(path6.dirname(filePath));
1198
1205
  const mode = migrationWorkflowMode(
1199
1206
  task,
1200
1207
  candidatesByTask.get(taskId) ?? [],
@@ -1229,7 +1236,7 @@ async function setPendingInitSince(cwd, version) {
1229
1236
  await writeTaskJson(filePath, task);
1230
1237
  }
1231
1238
  async function listTasks(cwd) {
1232
- const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1239
+ const tasksDir = path6.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1233
1240
  if (!await pathExists(tasksDir)) {
1234
1241
  return [];
1235
1242
  }
@@ -1260,15 +1267,114 @@ function isActiveTask(task) {
1260
1267
  return task.status !== "COMPLETE" && task.status !== "CLOSED";
1261
1268
  }
1262
1269
 
1270
+ // src/utils/template-paths.ts
1271
+ import { existsSync } from "fs";
1272
+ import path7 from "path";
1273
+ import { fileURLToPath as fileURLToPath2 } from "url";
1274
+ function getTemplateRoot() {
1275
+ const here = path7.dirname(fileURLToPath2(import.meta.url));
1276
+ const candidates = [
1277
+ path7.resolve(here, "../templates"),
1278
+ path7.resolve(here, "../../templates"),
1279
+ path7.resolve(process.cwd(), "src/templates"),
1280
+ path7.resolve(process.cwd(), "templates")
1281
+ ];
1282
+ const found = candidates.find((candidate) => existsSync(candidate));
1283
+ if (!found) {
1284
+ throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
1285
+ }
1286
+ return found;
1287
+ }
1288
+ function getTemplatePath(...segments) {
1289
+ return path7.join(getTemplateRoot(), ...segments);
1290
+ }
1291
+
1292
+ // src/utils/runtime-scaffold.ts
1293
+ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
1294
+ const easyCodingDir = path8.join(cwd, EASY_CODING_DIR);
1295
+ await ensureDir(easyCodingDir);
1296
+ const configPath2 = path8.join(easyCodingDir, CONFIG_FILE);
1297
+ let projectId = opts.projectId ?? createProjectId();
1298
+ if (!await pathExists(configPath2)) {
1299
+ const projectName = path8.basename(cwd);
1300
+ await writeConfigYaml(
1301
+ configPath2,
1302
+ createDefaultConfig({
1303
+ projectName,
1304
+ projectId,
1305
+ harnessVersion: VERSION,
1306
+ agents,
1307
+ supermodule: opts.supermodule
1308
+ })
1309
+ );
1310
+ } else {
1311
+ projectId = await ensureProjectId(configPath2);
1312
+ }
1313
+ await ensureDir(path8.join(easyCodingDir, "tasks"));
1314
+ await ensureDir(path8.join(easyCodingDir, SESSIONS_DIR));
1315
+ await ensureDir(path8.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
1316
+ await ensureDir(path8.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
1317
+ await writeMemoryScaffold(easyCodingDir);
1318
+ await writeTemplatesScaffold(easyCodingDir);
1319
+ await writeToolsScaffold(cwd);
1320
+ return projectId;
1321
+ }
1322
+ async function runtimeToolUpdates(cwd) {
1323
+ const frozen = (await listTasks(cwd)).some(
1324
+ ({ task }) => isActiveTask(task) && (task.unit_test_mode === "ut" || task.unit_test_mode === "tdd" || task.tdd_enabled === true)
1325
+ );
1326
+ const updates = [];
1327
+ for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
1328
+ const target = path8.join(cwd, EASY_CODING_DIR, TOOLS_DIR, file);
1329
+ const current = await readTextIfExists(target);
1330
+ if (frozen && current !== null) continue;
1331
+ const content = await readTextFile(getTemplatePath("runtime", "tools", file));
1332
+ if (content !== current) updates.push({ path: target, content });
1333
+ }
1334
+ return updates;
1335
+ }
1336
+ async function writeToolsScaffold(cwd) {
1337
+ const toolsDir = path8.join(cwd, EASY_CODING_DIR, TOOLS_DIR);
1338
+ await ensureDir(toolsDir);
1339
+ for (const update2 of await runtimeToolUpdates(cwd)) {
1340
+ await writeTextFile(update2.path, update2.content);
1341
+ }
1342
+ }
1343
+ async function writeTemplatesScaffold(easyCodingDir) {
1344
+ const templatesDir = path8.join(easyCodingDir, TEMPLATES_DIR);
1345
+ await ensureDir(templatesDir);
1346
+ const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
1347
+ const dest = path8.join(templatesDir, "dev-spec-skeleton.md");
1348
+ await writeTextFile(dest, await readTextFile(src));
1349
+ }
1350
+ async function writeMemoryScaffold(easyCodingDir) {
1351
+ const memoryDir = path8.join(easyCodingDir, MEMORY_DIR);
1352
+ await ensureDir(path8.join(memoryDir, "short"));
1353
+ await ensureDir(path8.join(memoryDir, "long"));
1354
+ for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
1355
+ const destination = path8.join(memoryDir, "long", file);
1356
+ if (await pathExists(destination)) {
1357
+ continue;
1358
+ }
1359
+ const templatePath = getTemplatePath("runtime", "memory", "long", file);
1360
+ await writeTextFile(destination, await readTextFile(templatePath));
1361
+ }
1362
+ const shortTemplateDest = path8.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
1363
+ if (!await pathExists(shortTemplateDest)) {
1364
+ const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
1365
+ await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
1366
+ }
1367
+ }
1368
+
1263
1369
  // src/commands/install-harness.ts
1264
- import path13 from "path";
1370
+ import path14 from "path";
1265
1371
 
1266
1372
  // src/configurators/claude.ts
1267
- import path9 from "path";
1373
+ import path10 from "path";
1268
1374
 
1269
1375
  // src/configurators/shared.ts
1270
1376
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
1271
- import path8 from "path";
1377
+ import path9 from "path";
1272
1378
 
1273
1379
  // src/utils/marked-region.ts
1274
1380
  var MarkedRegionError = class extends Error {
@@ -1327,7 +1433,7 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
1327
1433
  return resolved;
1328
1434
  }
1329
1435
  async function withProjectInstallPaths(cwd, ctx, projectId) {
1330
- const resolvedProjectId = projectId ?? await readProjectIdIfExists(path8.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
1436
+ const resolvedProjectId = projectId ?? await readProjectIdIfExists(path9.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
1331
1437
  return withInstallPaths(cwd, ctx, resolvedProjectId ?? void 0);
1332
1438
  }
1333
1439
  function withInstallPaths(cwd, ctx, projectId) {
@@ -1415,11 +1521,11 @@ async function resolveSkills(ctx) {
1415
1521
  const entries = await readdir2(skillsRoot);
1416
1522
  const skills = [];
1417
1523
  for (const entry of entries.sort()) {
1418
- const skillDir = path8.join(skillsRoot, entry);
1524
+ const skillDir = path9.join(skillsRoot, entry);
1419
1525
  if (!await isDirectory(skillDir)) {
1420
1526
  continue;
1421
1527
  }
1422
- const content = await readTextFile(path8.join(skillDir, "SKILL.md"));
1528
+ const content = await readTextFile(path9.join(skillDir, "SKILL.md"));
1423
1529
  skills.push({
1424
1530
  name: entry,
1425
1531
  content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
@@ -1435,7 +1541,7 @@ async function resolveBundledSkills(ctx) {
1435
1541
  const entries = await readdir2(bundledRoot);
1436
1542
  const bundled = [];
1437
1543
  for (const entry of entries.sort()) {
1438
- const sourceDir = path8.join(bundledRoot, entry);
1544
+ const sourceDir = path9.join(bundledRoot, entry);
1439
1545
  if (await isDirectory(sourceDir)) {
1440
1546
  bundled.push({ name: entry, sourceDir, context: ctx });
1441
1547
  }
@@ -1446,13 +1552,13 @@ async function writeSkills(dir, skills, bundled) {
1446
1552
  await ensureDir(dir);
1447
1553
  const written = [];
1448
1554
  for (const skill of skills) {
1449
- const destination = path8.join(dir, skill.name, "SKILL.md");
1555
+ const destination = path9.join(dir, skill.name, "SKILL.md");
1450
1556
  await writeTextFile(destination, skill.content);
1451
1557
  written.push(destination);
1452
1558
  }
1453
1559
  for (const skill of bundled) {
1454
1560
  written.push(
1455
- ...await copyTemplateDirectory(skill.sourceDir, path8.join(dir, skill.name), skill.context)
1561
+ ...await copyTemplateDirectory(skill.sourceDir, path9.join(dir, skill.name), skill.context)
1456
1562
  );
1457
1563
  }
1458
1564
  return written;
@@ -1467,7 +1573,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
1467
1573
  if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
1468
1574
  continue;
1469
1575
  }
1470
- const sourcePath = path8.join(hooksRoot, entry);
1576
+ const sourcePath = path9.join(hooksRoot, entry);
1471
1577
  if ((await stat2(sourcePath)).isDirectory()) {
1472
1578
  continue;
1473
1579
  }
@@ -1476,7 +1582,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
1476
1582
  ctx,
1477
1583
  `shared-hooks/${entry}`
1478
1584
  );
1479
- const destination = path8.join(dir, entry);
1585
+ const destination = path9.join(dir, entry);
1480
1586
  await writeTextFile(destination, content);
1481
1587
  await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
1482
1588
  written.push(destination);
@@ -1499,7 +1605,7 @@ async function writeMainConstraint(cwd, platform, opts = {}) {
1499
1605
  if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
1500
1606
  throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
1501
1607
  }
1502
- const destination = path8.join(cwd, meta.mainConstraint);
1608
+ const destination = path9.join(cwd, meta.mainConstraint);
1503
1609
  const current = await readTextIfExists(destination);
1504
1610
  if (current) {
1505
1611
  const markerContent = extractMarkedRegion(generated);
@@ -1541,8 +1647,8 @@ async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__
1541
1647
  if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
1542
1648
  continue;
1543
1649
  }
1544
- const sourcePath = path8.join(source, entry);
1545
- const destinationPath = path8.join(destination, stripTemplateExtension(entry));
1650
+ const sourcePath = path9.join(source, entry);
1651
+ const destinationPath = path9.join(destination, stripTemplateExtension(entry));
1546
1652
  const sourceStat = await stat2(sourcePath);
1547
1653
  if (sourceStat.isDirectory()) {
1548
1654
  written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
@@ -1566,28 +1672,28 @@ async function configureClaude(cwd, opts = {}) {
1566
1672
  const platform = "claude-code";
1567
1673
  const meta = PLATFORM_META[platform];
1568
1674
  const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
1569
- const dest = path9.join(cwd, ".claude");
1570
- const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
1675
+ const dest = path10.join(cwd, ".claude");
1676
+ const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
1571
1677
  const artifacts = [];
1572
1678
  const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
1573
1679
  artifacts.push(
1574
1680
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
1575
1681
  (filePath) => fileArtifact(
1576
1682
  filePath,
1577
- filePath.startsWith(path9.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1683
+ filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1578
1684
  platform
1579
1685
  )
1580
1686
  )
1581
1687
  );
1582
1688
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
1583
1689
  artifacts.push(
1584
- ...(await writeSharedHooks(path9.join(dest, "hooks"), platform)).map(
1690
+ ...(await writeSharedHooks(path10.join(dest, "hooks"), platform)).map(
1585
1691
  (filePath) => fileArtifact(filePath, "hook", platform)
1586
1692
  )
1587
1693
  );
1588
1694
  artifacts.push(
1589
1695
  ...(await writeSkills(
1590
- path9.join(cwd, meta.skillsDir),
1696
+ path10.join(cwd, meta.skillsDir),
1591
1697
  await resolveSkills(ctx),
1592
1698
  await resolveBundledSkills(ctx)
1593
1699
  )).map((filePath) => fileArtifact(filePath, "skill", platform))
@@ -1599,28 +1705,28 @@ async function configureClaude(cwd, opts = {}) {
1599
1705
  }
1600
1706
 
1601
1707
  // src/configurators/codex.ts
1602
- import path10 from "path";
1708
+ import path11 from "path";
1603
1709
  async function configureCodex(cwd, opts = {}) {
1604
1710
  const platform = "codex";
1605
1711
  const meta = PLATFORM_META[platform];
1606
1712
  const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
1607
- const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
1713
+ const hookConfigPath = path11.join(cwd, meta.hookConfigFile);
1608
1714
  const artifacts = [];
1609
1715
  artifacts.push(
1610
1716
  ...(await writeSkills(
1611
- path10.join(cwd, meta.skillsDir),
1717
+ path11.join(cwd, meta.skillsDir),
1612
1718
  await resolveSkills(ctx),
1613
1719
  await resolveBundledSkills(ctx)
1614
1720
  )).map((filePath) => fileArtifact(filePath, "skill", platform))
1615
1721
  );
1616
1722
  artifacts.push(
1617
- ...(await writeSharedHooks(path10.join(cwd, meta.hooksDir), platform, {
1723
+ ...(await writeSharedHooks(path11.join(cwd, meta.hooksDir), platform, {
1618
1724
  skipSubagentContext: true
1619
1725
  })).map((filePath) => fileArtifact(filePath, "hook", platform))
1620
1726
  );
1621
1727
  const platformFiles = await copyPlatformTemplates(
1622
1728
  "codex",
1623
- path10.join(cwd, ".codex"),
1729
+ path11.join(cwd, ".codex"),
1624
1730
  ["hooks"],
1625
1731
  ctx
1626
1732
  );
@@ -1628,7 +1734,7 @@ async function configureCodex(cwd, opts = {}) {
1628
1734
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
1629
1735
  (filePath) => fileArtifact(
1630
1736
  filePath,
1631
- filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1737
+ filePath.startsWith(path11.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1632
1738
  platform
1633
1739
  )
1634
1740
  )
@@ -1642,16 +1748,16 @@ async function configureCodex(cwd, opts = {}) {
1642
1748
 
1643
1749
  // src/configurators/qoder.ts
1644
1750
  import { readdir as readdir3 } from "fs/promises";
1645
- import path12 from "path";
1751
+ import path13 from "path";
1646
1752
 
1647
1753
  // src/utils/platform-paths.ts
1648
1754
  import { existsSync as existsSync2 } from "fs";
1649
- import path11 from "path";
1755
+ import path12 from "path";
1650
1756
  function detectQoderCnVariant(cwd) {
1651
1757
  if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
1652
1758
  return true;
1653
1759
  }
1654
- return existsSync2(path11.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
1760
+ return existsSync2(path12.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
1655
1761
  }
1656
1762
  function resolvePlatformMeta(cwd, platform) {
1657
1763
  const meta = PLATFORM_META[platform];
@@ -1682,7 +1788,7 @@ function resolveQoderMetaForBaseDir(baseDir) {
1682
1788
 
1683
1789
  // src/configurators/qoder.ts
1684
1790
  async function claudeHarnessSkillsExist(cwd) {
1685
- return pathExists(path12.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
1791
+ return pathExists(path13.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
1686
1792
  }
1687
1793
  async function listFilesRecursive(dir) {
1688
1794
  let entries;
@@ -1696,7 +1802,7 @@ async function listFilesRecursive(dir) {
1696
1802
  }
1697
1803
  const files = [];
1698
1804
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1699
- const entryPath = path12.join(dir, entry.name);
1805
+ const entryPath = path13.join(dir, entry.name);
1700
1806
  if (entry.isDirectory()) {
1701
1807
  files.push(...await listFilesRecursive(entryPath));
1702
1808
  continue;
@@ -1710,7 +1816,7 @@ async function listFilesRecursive(dir) {
1710
1816
  async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
1711
1817
  const artifacts = [];
1712
1818
  for (const skillName of skillNames) {
1713
- for (const filePath of await listFilesRecursive(path12.join(skillsDir, skillName))) {
1819
+ for (const filePath of await listFilesRecursive(path13.join(skillsDir, skillName))) {
1714
1820
  artifacts.push(fileArtifact(filePath, "skill", platform));
1715
1821
  }
1716
1822
  }
@@ -1720,22 +1826,22 @@ async function configureQoder(cwd, opts = {}) {
1720
1826
  const platform = "qoder";
1721
1827
  const meta = resolvePlatformMeta(cwd, platform);
1722
1828
  const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
1723
- const dest = path12.join(cwd, ctx.platform_config_dir);
1724
- const hookConfigPath = path12.join(cwd, meta.hookConfigFile);
1829
+ const dest = path13.join(cwd, ctx.platform_config_dir);
1830
+ const hookConfigPath = path13.join(cwd, meta.hookConfigFile);
1725
1831
  const artifacts = [];
1726
1832
  const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
1727
1833
  artifacts.push(
1728
1834
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
1729
1835
  (filePath) => fileArtifact(
1730
1836
  filePath,
1731
- filePath.startsWith(path12.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1837
+ filePath.startsWith(path13.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1732
1838
  platform
1733
1839
  )
1734
1840
  )
1735
1841
  );
1736
1842
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
1737
1843
  artifacts.push(
1738
- ...(await writeSharedHooks(path12.join(dest, "hooks"), platform)).map(
1844
+ ...(await writeSharedHooks(path13.join(dest, "hooks"), platform)).map(
1739
1845
  (filePath) => fileArtifact(filePath, "hook", platform)
1740
1846
  )
1741
1847
  );
@@ -1743,14 +1849,14 @@ async function configureQoder(cwd, opts = {}) {
1743
1849
  const bundledSkills = await resolveBundledSkills(ctx);
1744
1850
  if (!await claudeHarnessSkillsExist(cwd)) {
1745
1851
  artifacts.push(
1746
- ...(await writeSkills(path12.join(dest, "skills"), skills, bundledSkills)).map(
1852
+ ...(await writeSkills(path13.join(dest, "skills"), skills, bundledSkills)).map(
1747
1853
  (filePath) => fileArtifact(filePath, "skill", platform)
1748
1854
  )
1749
1855
  );
1750
1856
  } else {
1751
1857
  artifacts.push(
1752
1858
  ...await existingManagedSkillArtifacts(
1753
- path12.join(dest, "skills"),
1859
+ path13.join(dest, "skills"),
1754
1860
  [...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
1755
1861
  platform
1756
1862
  )
@@ -1821,7 +1927,7 @@ async function refreshSupermoduleParent(targetDir, platforms, submodulePaths) {
1821
1927
  role: "super-parent",
1822
1928
  submodules: submodulePaths
1823
1929
  };
1824
- await updateSupermoduleConfig(path13.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
1930
+ await updateSupermoduleConfig(path14.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
1825
1931
  for (const platform of platforms) {
1826
1932
  await writeMainConstraint(targetDir, platform, {
1827
1933
  supermodule: { submodulePaths }
@@ -1943,14 +2049,14 @@ async function resolveSubmodules(opts, available, defaultSelection = available)
1943
2049
  }
1944
2050
 
1945
2051
  // src/commands/supermodule-targets.ts
1946
- import path15 from "path";
2052
+ import path16 from "path";
1947
2053
  import { cancel as cancel2, multiselect as multiselect2 } from "@clack/prompts";
1948
2054
 
1949
2055
  // src/utils/gitmodules.ts
1950
2056
  import { lstat, realpath } from "fs/promises";
1951
- import path14 from "path";
2057
+ import path15 from "path";
1952
2058
  async function parseGitmodules(rootDir) {
1953
- const content = await readTextIfExists(path14.join(rootDir, ".gitmodules"));
2059
+ const content = await readTextIfExists(path15.join(rootDir, ".gitmodules"));
1954
2060
  if (content === null) {
1955
2061
  return [];
1956
2062
  }
@@ -2009,7 +2115,7 @@ async function listInstallableSubmodules(rootDir) {
2009
2115
  function normalizeSubmodulePath(value) {
2010
2116
  const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
2011
2117
  const parts = normalized.split("/").filter(Boolean);
2012
- if (normalized === "" || path14.posix.isAbsolute(normalized) || path14.win32.isAbsolute(value) || parts.some((part) => part === "..")) {
2118
+ if (normalized === "" || path15.posix.isAbsolute(normalized) || path15.win32.isAbsolute(value) || parts.some((part) => part === "..")) {
2013
2119
  throw new Error(`Unsafe submodule path in .gitmodules: ${value}`);
2014
2120
  }
2015
2121
  return parts.join("/");
@@ -2047,7 +2153,7 @@ function isCommentStart(value, index) {
2047
2153
  return index === 0 || /\s/.test(value[index - 1]);
2048
2154
  }
2049
2155
  async function isSubmoduleWorktree(rootDir, submodulePath) {
2050
- const dir = path14.join(rootDir, submodulePath);
2156
+ const dir = path15.join(rootDir, submodulePath);
2051
2157
  try {
2052
2158
  if (!await pathHasNoSymlinkSegments(rootDir, submodulePath)) {
2053
2159
  return false;
@@ -2061,7 +2167,7 @@ async function isSubmoduleWorktree(rootDir, submodulePath) {
2061
2167
  if (!dirStat.isDirectory()) {
2062
2168
  return false;
2063
2169
  }
2064
- const gitMarkerStat = await lstat(path14.join(dir, ".git"));
2170
+ const gitMarkerStat = await lstat(path15.join(dir, ".git"));
2065
2171
  return !gitMarkerStat.isSymbolicLink() && (gitMarkerStat.isFile() || gitMarkerStat.isDirectory());
2066
2172
  } catch (error) {
2067
2173
  if (error.code === "ENOENT") {
@@ -2073,7 +2179,7 @@ async function isSubmoduleWorktree(rootDir, submodulePath) {
2073
2179
  async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
2074
2180
  let current = rootDir;
2075
2181
  for (const part of submodulePath.split("/")) {
2076
- current = path14.join(current, part);
2182
+ current = path15.join(current, part);
2077
2183
  const partStat = await lstat(current);
2078
2184
  if (partStat.isSymbolicLink()) {
2079
2185
  return false;
@@ -2082,8 +2188,8 @@ async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
2082
2188
  return true;
2083
2189
  }
2084
2190
  function isInsideDirectory(parent, child) {
2085
- const relative = path14.relative(parent, child);
2086
- return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
2191
+ const relative = path15.relative(parent, child);
2192
+ return Boolean(relative) && !relative.startsWith("..") && !path15.isAbsolute(relative);
2087
2193
  }
2088
2194
 
2089
2195
  // src/commands/supermodule-targets.ts
@@ -2135,7 +2241,7 @@ async function resolveClearTargets(cwd, opts) {
2135
2241
  return [standaloneTarget(cwd)];
2136
2242
  }
2137
2243
  const installedSubmodules = await listInstalledSubmodules(cwd);
2138
- const parent = await pathExists(path15.join(cwd, EASY_CODING_DIR)) ? parentTarget(cwd, installedSubmodules) : null;
2244
+ const parent = await pathExists(path16.join(cwd, EASY_CODING_DIR)) ? parentTarget(cwd, installedSubmodules) : null;
2139
2245
  const children = installedSubmodules.map((entry) => childTarget(cwd, entry));
2140
2246
  if (opts.submodules === false) {
2141
2247
  return parent ? [parent] : [];
@@ -2175,7 +2281,7 @@ async function listInstalledSubmodules(cwd) {
2175
2281
  const entries = await listInstallableSubmodules(cwd);
2176
2282
  const installed = [];
2177
2283
  for (const entry of entries) {
2178
- if (await pathExists(configPath(path15.join(cwd, entry.path)))) {
2284
+ if (await pathExists(configPath(path16.join(cwd, entry.path)))) {
2179
2285
  installed.push(entry);
2180
2286
  }
2181
2287
  }
@@ -2218,7 +2324,7 @@ function parentTargetFromPaths(cwd, submodulePaths) {
2218
2324
  };
2219
2325
  }
2220
2326
  function childTarget(cwd, entry) {
2221
- const dir = path15.join(cwd, entry.path);
2327
+ const dir = path16.join(cwd, entry.path);
2222
2328
  return {
2223
2329
  dir,
2224
2330
  label: entry.path,
@@ -2254,11 +2360,11 @@ function parseSubmoduleSelection(submoduleList, available) {
2254
2360
  return selected.sort((a, b) => a.path.localeCompare(b.path));
2255
2361
  }
2256
2362
  function configPath(cwd) {
2257
- return path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2363
+ return path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2258
2364
  }
2259
2365
  function toPosixRelative(from, to) {
2260
- const relative = path15.relative(from, to);
2261
- return relative ? relative.split(path15.sep).join("/") : ".";
2366
+ const relative = path16.relative(from, to);
2367
+ return relative ? relative.split(path16.sep).join("/") : ".";
2262
2368
  }
2263
2369
 
2264
2370
  // src/commands/add-agent.ts
@@ -2334,7 +2440,7 @@ ${installedLabels.join("\n")}`));
2334
2440
 
2335
2441
  // src/commands/clear.ts
2336
2442
  import { readdir as readdir4, rm, writeFile } from "fs/promises";
2337
- import path16 from "path";
2443
+ import path17 from "path";
2338
2444
  import { cancel as cancel3, confirm as confirm2, outro as outro2 } from "@clack/prompts";
2339
2445
  import chalk3 from "chalk";
2340
2446
  var PLATFORM_TEMPLATE_DIR = {
@@ -2373,7 +2479,7 @@ async function clear(opts) {
2373
2479
  async function buildTargetClearPlans(targets) {
2374
2480
  const plans = [];
2375
2481
  for (const target of targets) {
2376
- const easyCodingDir = path16.join(target.dir, EASY_CODING_DIR);
2482
+ const easyCodingDir = path17.join(target.dir, EASY_CODING_DIR);
2377
2483
  if (!await pathExists(easyCodingDir)) {
2378
2484
  continue;
2379
2485
  }
@@ -2390,7 +2496,7 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
2390
2496
  if (targetPlans.some((targetPlan) => targetPlan.target.label === ".")) {
2391
2497
  return;
2392
2498
  }
2393
- if (!await pathExists(path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
2499
+ if (!await pathExists(path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
2394
2500
  return;
2395
2501
  }
2396
2502
  const [parent] = await resolveUpgradeTargets(cwd);
@@ -2404,7 +2510,7 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
2404
2510
  await refreshSupermoduleParent(cwd, config2.agents, parent.supermodule.submodules ?? []);
2405
2511
  }
2406
2512
  async function resolveInstalledAgents(cwd) {
2407
- const configPath2 = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2513
+ const configPath2 = path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2408
2514
  if (await pathExists(configPath2)) {
2409
2515
  try {
2410
2516
  const config2 = await readConfigYaml(configPath2);
@@ -2435,7 +2541,7 @@ async function hasPlatformInstall(cwd, platform) {
2435
2541
  async function hasInstallMarkers(cwd, meta) {
2436
2542
  const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
2437
2543
  for (const marker of markers) {
2438
- if (await pathExists(path16.join(cwd, marker))) {
2544
+ if (await pathExists(path17.join(cwd, marker))) {
2439
2545
  return true;
2440
2546
  }
2441
2547
  }
@@ -2491,7 +2597,7 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
2491
2597
  }
2492
2598
  if (await manifestFileMatches(filePath, file.sha256)) {
2493
2599
  plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
2494
- addEmptyDirChain(plan.emptyDirs, path16.dirname(filePath), cwd);
2600
+ addEmptyDirChain(plan.emptyDirs, path17.dirname(filePath), cwd);
2495
2601
  } else {
2496
2602
  plan.skippedModified.push(filePath);
2497
2603
  }
@@ -2541,28 +2647,28 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
2541
2647
  );
2542
2648
  for (const meta of metas) {
2543
2649
  for (const name of managedSkills) {
2544
- plan.remove.add(path16.join(cwd, meta.skillsDir, name));
2650
+ plan.remove.add(path17.join(cwd, meta.skillsDir, name));
2545
2651
  }
2546
2652
  for (const name of hookFileNames) {
2547
- plan.remove.add(path16.join(cwd, meta.hooksDir, name));
2653
+ plan.remove.add(path17.join(cwd, meta.hooksDir, name));
2548
2654
  }
2549
2655
  for (const name of agentFileNames) {
2550
- plan.remove.add(path16.join(cwd, meta.agentsDir, name));
2656
+ plan.remove.add(path17.join(cwd, meta.agentsDir, name));
2551
2657
  }
2552
2658
  addHookConfigPrune(
2553
2659
  plan.pruneHookConfigs,
2554
2660
  plan.pruneHookCommands,
2555
- path16.join(cwd, meta.hookConfigFile),
2661
+ path17.join(cwd, meta.hookConfigFile),
2556
2662
  managedHookPathsForTemplate(cwd, meta, hookFileNames),
2557
2663
  []
2558
2664
  );
2559
- plan.constraints.add(path16.join(cwd, meta.mainConstraint));
2665
+ plan.constraints.add(path17.join(cwd, meta.mainConstraint));
2560
2666
  if (platform === "codex") {
2561
- plan.remove.add(path16.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
2667
+ plan.remove.add(path17.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
2562
2668
  }
2563
- plan.emptyDirs.add(path16.join(cwd, meta.skillsDir));
2564
- plan.emptyDirs.add(path16.join(cwd, meta.hooksDir));
2565
- plan.emptyDirs.add(path16.join(cwd, meta.agentsDir));
2669
+ plan.emptyDirs.add(path17.join(cwd, meta.skillsDir));
2670
+ plan.emptyDirs.add(path17.join(cwd, meta.hooksDir));
2671
+ plan.emptyDirs.add(path17.join(cwd, meta.agentsDir));
2566
2672
  }
2567
2673
  }
2568
2674
  async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
@@ -2609,7 +2715,7 @@ function managedHookPathsForRegistration(cwd, hookPath) {
2609
2715
  try {
2610
2716
  paths.push(...managedHookPathTokens(manifestPath(cwd, hookPath)));
2611
2717
  } catch {
2612
- if (path16.isAbsolute(hookPath)) {
2718
+ if (path17.isAbsolute(hookPath)) {
2613
2719
  paths.push(...managedHookPathTokens(hookPath));
2614
2720
  }
2615
2721
  }
@@ -2620,7 +2726,7 @@ function managedHookPathsForTemplate(cwd, meta, hookFileNames) {
2620
2726
  (name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`
2621
2727
  );
2622
2728
  const absolutePaths = hookFileNames.flatMap(
2623
- (name) => managedHookPathTokens(path16.join(cwd, meta.hooksDir, name))
2729
+ (name) => managedHookPathTokens(path17.join(cwd, meta.hooksDir, name))
2624
2730
  );
2625
2731
  return [...relativePaths, ...absolutePaths];
2626
2732
  }
@@ -2628,8 +2734,8 @@ function managedHookPathTokens(filePath) {
2628
2734
  const tokens = /* @__PURE__ */ new Set();
2629
2735
  for (const equivalentPath of equivalentAbsolutePaths(filePath)) {
2630
2736
  const normalized = equivalentPath.replace(/\\/g, "/");
2631
- const dir = path16.posix.dirname(normalized);
2632
- const basename = path16.posix.basename(normalized);
2737
+ const dir = path17.posix.dirname(normalized);
2738
+ const basename = path17.posix.basename(normalized);
2633
2739
  tokens.add(normalized);
2634
2740
  tokens.add(shellDoubleQuoteArg2(normalized));
2635
2741
  tokens.add(`${shellDoubleQuoteArg2(dir)}/${basename}`);
@@ -2637,7 +2743,7 @@ function managedHookPathTokens(filePath) {
2637
2743
  return [...tokens];
2638
2744
  }
2639
2745
  function equivalentAbsolutePaths(filePath) {
2640
- const normalized = path16.resolve(filePath).replace(/\\/g, "/");
2746
+ const normalized = path17.resolve(filePath).replace(/\\/g, "/");
2641
2747
  const equivalents = [normalized];
2642
2748
  if (normalized.startsWith("/private/var/")) {
2643
2749
  equivalents.push(normalized.replace(/^\/private\/var\//, "/var/"));
@@ -2711,11 +2817,11 @@ function addRuntimeClearEntries(plan, cwd) {
2711
2817
  plan.remove = [
2712
2818
  .../* @__PURE__ */ new Set([
2713
2819
  ...plan.remove,
2714
- path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
2715
- path16.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
2716
- path16.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
2717
- path16.join(cwd, EASY_CODING_DIR, TOOLS_DIR),
2718
- path16.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
2820
+ path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
2821
+ path17.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
2822
+ path17.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
2823
+ path17.join(cwd, EASY_CODING_DIR, TOOLS_DIR),
2824
+ path17.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
2719
2825
  ])
2720
2826
  ];
2721
2827
  }
@@ -2723,7 +2829,7 @@ function addEmptyDirChain(emptyDirs, startDir, cwd) {
2723
2829
  let current = startDir;
2724
2830
  while (current !== cwd && isInsideDirectory2(cwd, current)) {
2725
2831
  emptyDirs.add(current);
2726
- const parent = path16.dirname(current);
2832
+ const parent = path17.dirname(current);
2727
2833
  if (parent === current) {
2728
2834
  break;
2729
2835
  }
@@ -2731,8 +2837,8 @@ function addEmptyDirChain(emptyDirs, startDir, cwd) {
2731
2837
  }
2732
2838
  }
2733
2839
  function isInsideDirectory2(parent, child) {
2734
- const relative = path16.relative(parent, child);
2735
- return Boolean(relative) && !relative.startsWith("..") && !path16.isAbsolute(relative);
2840
+ const relative = path17.relative(parent, child);
2841
+ return Boolean(relative) && !relative.startsWith("..") && !path17.isAbsolute(relative);
2736
2842
  }
2737
2843
  async function listSharedHookNamesForPlatform(platform) {
2738
2844
  const names = await listFileNames(getTemplatePath("shared-hooks"));
@@ -2869,7 +2975,7 @@ function sortDirsDeepestFirst(dirs) {
2869
2975
  });
2870
2976
  }
2871
2977
  function pathDepth(dir) {
2872
- return path16.normalize(dir).split(path16.sep).filter(Boolean).length;
2978
+ return path17.normalize(dir).split(path17.sep).filter(Boolean).length;
2873
2979
  }
2874
2980
  async function listDirNames(dir) {
2875
2981
  try {
@@ -2888,7 +2994,7 @@ async function listFileNames(dir) {
2888
2994
  }
2889
2995
  }
2890
2996
  function renderPlan(cwd, agents, plan) {
2891
- const rel = (target) => path16.relative(cwd, target) || target;
2997
+ const rel = (target) => path17.relative(cwd, target) || target;
2892
2998
  const lines = [];
2893
2999
  lines.push(chalk3.bold("easy-coding clear"));
2894
3000
  lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
@@ -2937,12 +3043,12 @@ function renderTargetPlans(targetPlans) {
2937
3043
  }
2938
3044
 
2939
3045
  // src/commands/config.ts
2940
- import path19 from "path";
3046
+ import path20 from "path";
2941
3047
  import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
2942
3048
  import chalk4 from "chalk";
2943
3049
 
2944
3050
  // src/utils/compare-versions.ts
2945
- import path17 from "path";
3051
+ import path18 from "path";
2946
3052
  function parseVersion(version) {
2947
3053
  const withoutBuild = String(version ?? "").split("+", 1)[0];
2948
3054
  const separator = withoutBuild.indexOf("-");
@@ -2997,7 +3103,7 @@ function isVersionBehind(installed, current = VERSION) {
2997
3103
  return compareVersions(installed, current) === -1;
2998
3104
  }
2999
3105
  async function checkForUpgrade(cwd) {
3000
- const configPath2 = path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3106
+ const configPath2 = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3001
3107
  if (!await pathExists(configPath2)) {
3002
3108
  return;
3003
3109
  }
@@ -3018,7 +3124,7 @@ async function checkForUpgrade(cwd) {
3018
3124
 
3019
3125
  // src/utils/tdd-readiness.ts
3020
3126
  import { readFile as readFile4, realpath as realpath2 } from "fs/promises";
3021
- import path18 from "path";
3127
+ import path19 from "path";
3022
3128
  var TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1";
3023
3129
  var TDD_READINESS_SCOPE = "changed-production-lines";
3024
3130
  var TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA";
@@ -3026,7 +3132,7 @@ var TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD";
3026
3132
  var COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py";
3027
3133
  var JAVA_BUILD_FILE_NAMES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
3028
3134
  function readinessPath(root) {
3029
- return path18.join(root, EASY_CODING_DIR, TDD_DIR, TDD_READINESS_FILE);
3135
+ return path19.join(root, EASY_CODING_DIR, TDD_DIR, TDD_READINESS_FILE);
3030
3136
  }
3031
3137
  function parseFileRecords(value, field, reasons) {
3032
3138
  if (!Array.isArray(value) || value.length === 0) {
@@ -3040,7 +3146,7 @@ function parseFileRecords(value, field, reasons) {
3040
3146
  continue;
3041
3147
  }
3042
3148
  const record = item;
3043
- if (typeof record.path !== "string" || !record.path.trim() || path18.isAbsolute(record.path)) {
3149
+ if (typeof record.path !== "string" || !record.path.trim() || path19.isAbsolute(record.path)) {
3044
3150
  reasons.push(`${field} contains an invalid path`);
3045
3151
  continue;
3046
3152
  }
@@ -3054,15 +3160,15 @@ function usesRequiredGateVariables(command) {
3054
3160
  }
3055
3161
  function isSafeReportPattern(value) {
3056
3162
  const normalized = value.replaceAll("\\", "/");
3057
- return !path18.isAbsolute(value) && !normalized.split("/").includes("..");
3163
+ return !path19.isAbsolute(value) && !normalized.split("/").includes("..");
3058
3164
  }
3059
3165
  async function validateFiles(root, records, reasons) {
3060
3166
  const resolvedRoot = await realpath2(root);
3061
3167
  for (const record of records) {
3062
- const absolute = path18.resolve(root, record.path);
3168
+ const absolute = path19.resolve(root, record.path);
3063
3169
  try {
3064
3170
  const resolved = await realpath2(absolute);
3065
- if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path18.sep}`)) {
3171
+ if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path19.sep}`)) {
3066
3172
  reasons.push(`readiness file escapes project root: ${record.path}`);
3067
3173
  continue;
3068
3174
  }
@@ -3111,7 +3217,7 @@ async function inspectTddReadiness(root) {
3111
3217
  }
3112
3218
  const buildFiles = parseFileRecords(manifest.build_files, "build_files", reasons);
3113
3219
  const toolFiles = parseFileRecords(manifest.tool_files, "tool_files", reasons);
3114
- if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(path18.basename(record.path)))) {
3220
+ if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(path19.basename(record.path)))) {
3115
3221
  reasons.push("build_files must include a Maven or Gradle Java build file");
3116
3222
  }
3117
3223
  if (!toolFiles.some((record) => record.path.replaceAll("\\", "/") === COVERAGE_TOOL_PATH)) {
@@ -3127,9 +3233,17 @@ async function inspectTddReadiness(root) {
3127
3233
  }
3128
3234
 
3129
3235
  // src/commands/config.ts
3130
- async function config() {
3236
+ async function config(options = {}) {
3131
3237
  renderBanner();
3132
- const configPath2 = path19.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
3238
+ const scope = options.scope ?? "project";
3239
+ if (scope !== "project" && scope !== "local")
3240
+ throw new Error("Config scope must be project or local.");
3241
+ const explicit = options.approvalMode !== void 0 || options.cooperateMode !== void 0 || options.unitTestMode !== void 0 || options.utCoverageThreshold !== void 0 || options.reset !== void 0;
3242
+ if (scope === "local") {
3243
+ await configureOverrides(localConfigPath(), scope, options, explicit);
3244
+ return;
3245
+ }
3246
+ const configPath2 = path20.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
3133
3247
  if (!await pathExists(configPath2)) {
3134
3248
  throw new Error("No easy-coding harness found in this project.");
3135
3249
  }
@@ -3150,6 +3264,10 @@ async function config() {
3150
3264
  `Project harness ${projectConfig.harness_version} does not exactly match CLI ${VERSION}. Upgrade the harness or update the CLI before changing config.`
3151
3265
  );
3152
3266
  }
3267
+ if (explicit) {
3268
+ await configureOverrides(configPath2, scope, options, true);
3269
+ return;
3270
+ }
3153
3271
  const current = resolveLegacyBehavior(projectConfig);
3154
3272
  const approvalMode = await select({
3155
3273
  message: `Select project approval mode (current: ${current.approvalMode})`,
@@ -3182,83 +3300,186 @@ async function config() {
3182
3300
  return;
3183
3301
  }
3184
3302
  const workflowMode = "adaptive";
3185
- const tddEnabled = await select({
3186
- message: `Enable Java TDD for this project (current: ${current.tddEnabled ? "enabled" : "disabled"})`,
3187
- initialValue: current.tddEnabled,
3303
+ const unitTestMode = await select({
3304
+ message: `Select Java unit test strategy (current: ${current.unitTestMode})`,
3305
+ initialValue: current.unitTestMode,
3188
3306
  options: [
3189
- { value: false, label: "disabled \u2014 preserve current test depth (default)" },
3190
- { value: true, label: "enabled \u2014 require TDD evidence and changed-line coverage" }
3307
+ { value: "none", label: "none \u2014 preserve task-required verification (default)" },
3308
+ { value: "ut", label: "UT \u2014 passing unit tests and changed-line coverage" },
3309
+ { value: "tdd", label: "TDD \u2014 test-first development and changed-line coverage" }
3191
3310
  ]
3192
3311
  });
3193
- if (typeof tddEnabled === "symbol") {
3312
+ if (typeof unitTestMode === "symbol") {
3194
3313
  cancel4("Configuration cancelled.");
3195
3314
  return;
3196
3315
  }
3197
- if (tddEnabled) {
3316
+ if (unitTestMode !== "none") {
3198
3317
  const readiness = await inspectTddReadiness(process.cwd());
3199
3318
  if (readiness.status !== "ready") {
3200
3319
  cancel4(
3201
- `TDD was not enabled. ${readiness.status === "needs_init" ? "Run ec-tdd-init first" : "Repair TDD readiness"}: ${readiness.reasons.join("; ")}. No project modes were changed.`
3320
+ `Unit test strategy was not enabled. ${readiness.status === "needs_init" ? "Run ec-tdd-init first" : "Repair coverage readiness"}: ${readiness.reasons.join("; ")}. No project modes were changed.`
3202
3321
  );
3203
3322
  return;
3204
3323
  }
3205
3324
  }
3206
- let tddCoverageThreshold = current.tddCoverageThreshold;
3207
- if (tddEnabled) {
3325
+ let utCoverageThreshold = current.utCoverageThreshold;
3326
+ if (unitTestMode !== "none") {
3208
3327
  const thresholdInput = await text({
3209
3328
  message: "Minimum changed-production-line coverage percentage",
3210
- initialValue: String(current.tddCoverageThreshold),
3329
+ initialValue: String(current.utCoverageThreshold),
3211
3330
  validate(value) {
3212
3331
  const parsed = Number(value);
3213
- return isTddCoverageThreshold(parsed) ? void 0 : "Enter an integer from 1 to 100.";
3332
+ return isUtCoverageThreshold(parsed) ? void 0 : "Enter an integer from 1 to 100.";
3214
3333
  }
3215
3334
  });
3216
3335
  if (typeof thresholdInput === "symbol") {
3217
3336
  cancel4("Configuration cancelled.");
3218
3337
  return;
3219
3338
  }
3220
- tddCoverageThreshold = Number(thresholdInput);
3339
+ utCoverageThreshold = Number(thresholdInput);
3340
+ }
3341
+ const cooperateMode = await select({
3342
+ message: `Select project cooperation (current: ${projectConfig.behavior.cooperate_mode ?? "default"})`,
3343
+ initialValue: projectConfig.behavior.cooperate_mode ?? "default",
3344
+ options: [
3345
+ { value: "default", label: "default \u2014 hand off at stage boundaries" },
3346
+ {
3347
+ value: "dispatch",
3348
+ label: "dispatch \u2014 manually hand off implementation and QUALITY repairs"
3349
+ }
3350
+ ]
3351
+ });
3352
+ if (typeof cooperateMode === "symbol") {
3353
+ cancel4("Configuration cancelled.");
3354
+ return;
3221
3355
  }
3222
3356
  const shouldSave = await confirm3({
3223
- message: `Set approval=${approvalMode}, workflow=${workflowMode}, TDD=${tddEnabled ? `enabled (${tddCoverageThreshold}%)` : "disabled"}?`,
3357
+ message: `Set approval=${approvalMode}, cooperate=${cooperateMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}?`,
3224
3358
  initialValue: true
3225
3359
  });
3226
3360
  if (typeof shouldSave === "symbol" || !shouldSave) {
3227
3361
  cancel4("Configuration cancelled.");
3228
3362
  return;
3229
3363
  }
3230
- if (tddEnabled) {
3364
+ if (unitTestMode !== "none") {
3231
3365
  const readiness = await inspectTddReadiness(process.cwd());
3232
3366
  if (readiness.status !== "ready") {
3233
3367
  cancel4(
3234
- `TDD was not enabled because readiness changed before save: ${readiness.reasons.join("; ")}. No project modes were changed.`
3368
+ `Unit test strategy was not enabled because readiness changed before save: ${readiness.reasons.join("; ")}. No project modes were changed.`
3235
3369
  );
3236
3370
  return;
3237
3371
  }
3238
3372
  }
3239
- await setBehaviorModes(configPath2, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold);
3373
+ await setBehaviorModes(
3374
+ configPath2,
3375
+ approvalMode,
3376
+ workflowMode,
3377
+ unitTestMode,
3378
+ utCoverageThreshold,
3379
+ cooperateMode
3380
+ );
3240
3381
  outro3(
3241
3382
  chalk4.green(
3242
- `Project modes updated: approval=${approvalMode}, workflow=${workflowMode}, TDD=${tddEnabled ? `${tddCoverageThreshold}%` : "off"}.`
3383
+ `Project modes updated: approval=${approvalMode}, workflow=${workflowMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}.`
3243
3384
  )
3244
3385
  );
3245
3386
  }
3387
+ async function configureOverrides(filePath, scope, options, explicit) {
3388
+ const changes = {};
3389
+ const flags = {
3390
+ approval_mode: options.approvalMode,
3391
+ cooperate_mode: options.cooperateMode,
3392
+ unit_test_mode: options.unitTestMode,
3393
+ ut_coverage_threshold: options.utCoverageThreshold
3394
+ };
3395
+ for (const key of BEHAVIOR_KEYS) {
3396
+ if (flags[key] !== void 0)
3397
+ changes[key] = key === "ut_coverage_threshold" ? Number(flags[key]) : flags[key];
3398
+ }
3399
+ if (options.reset) {
3400
+ if (!BEHAVIOR_KEYS.includes(options.reset))
3401
+ throw new Error("Unknown behavior field to reset.");
3402
+ changes[options.reset] = null;
3403
+ }
3404
+ if (!explicit) {
3405
+ const local = await readLocalBehavior();
3406
+ const key = await select({
3407
+ message: "Select local override to edit",
3408
+ options: BEHAVIOR_KEYS.map((value) => ({
3409
+ value,
3410
+ label: `${value}: ${local[value] ?? "inherit project"}`
3411
+ }))
3412
+ });
3413
+ if (typeof key === "symbol") {
3414
+ cancel4("Configuration cancelled.");
3415
+ return;
3416
+ }
3417
+ if (key === "ut_coverage_threshold") {
3418
+ const value = await text({
3419
+ message: "Coverage threshold 1..100, or inherit",
3420
+ initialValue: String(local[key] ?? "inherit"),
3421
+ validate: (value2) => value2 === "inherit" || isUtCoverageThreshold(Number(value2)) ? void 0 : "Enter 1..100 or inherit."
3422
+ });
3423
+ if (typeof value === "symbol") {
3424
+ cancel4("Configuration cancelled.");
3425
+ return;
3426
+ }
3427
+ changes[key] = value === "inherit" ? null : Number(value);
3428
+ } else {
3429
+ const values = key === "approval_mode" ? APPROVAL_MODES : key === "cooperate_mode" ? COOPERATE_MODES : UNIT_TEST_MODES;
3430
+ const value = await select({
3431
+ message: `Set local ${key}`,
3432
+ initialValue: local[key] ?? "inherit",
3433
+ options: [
3434
+ { value: "inherit", label: "inherit \u2014 use project setting" },
3435
+ ...values.map((value2) => ({ value: value2, label: value2 }))
3436
+ ]
3437
+ });
3438
+ if (typeof value === "symbol") {
3439
+ cancel4("Configuration cancelled.");
3440
+ return;
3441
+ }
3442
+ changes[key] = value === "inherit" ? null : value;
3443
+ }
3444
+ }
3445
+ for (const key of BEHAVIOR_KEYS) {
3446
+ if (changes[key] !== void 0 && changes[key] !== null)
3447
+ validateBehaviorValue(key, changes[key]);
3448
+ }
3449
+ if (!options.yes) {
3450
+ const accepted = await confirm3({
3451
+ message: `Save ${scope} overrides ${JSON.stringify(changes)}?`,
3452
+ initialValue: true
3453
+ });
3454
+ if (accepted !== true) {
3455
+ cancel4("Configuration cancelled.");
3456
+ return;
3457
+ }
3458
+ }
3459
+ if (scope === "project" && changes.unit_test_mode && changes.unit_test_mode !== "none") {
3460
+ const readiness = await inspectTddReadiness(process.cwd());
3461
+ if (readiness.status !== "ready")
3462
+ throw new Error(`Unit test readiness: ${readiness.status}. ${readiness.reasons.join("; ")}`);
3463
+ }
3464
+ await writeBehaviorOverrides(filePath, changes);
3465
+ outro3(chalk4.green(`${scope} behavior overrides saved: ${filePath}`));
3466
+ }
3246
3467
 
3247
3468
  // src/commands/init.ts
3248
- import path21 from "path";
3469
+ import path22 from "path";
3249
3470
  import { note, outro as outro4 } from "@clack/prompts";
3250
3471
  import chalk5 from "chalk";
3251
3472
 
3252
3473
  // src/utils/install-state.ts
3253
3474
  import { readdir as readdir5 } from "fs/promises";
3254
- import path20 from "path";
3475
+ import path21 from "path";
3255
3476
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
3256
3477
  async function detectEasyCodingInstallState(cwd) {
3257
- const easyCodingDir = path20.join(cwd, EASY_CODING_DIR);
3478
+ const easyCodingDir = path21.join(cwd, EASY_CODING_DIR);
3258
3479
  if (!await pathExists(easyCodingDir)) {
3259
3480
  return { kind: "fresh", easyCodingDir };
3260
3481
  }
3261
- const configPath2 = path20.join(easyCodingDir, CONFIG_FILE);
3482
+ const configPath2 = path21.join(easyCodingDir, CONFIG_FILE);
3262
3483
  if (await pathExists(configPath2)) {
3263
3484
  return { kind: "installed", easyCodingDir, configPath: configPath2 };
3264
3485
  }
@@ -3276,17 +3497,17 @@ async function detectEasyCodingInstallState(cwd) {
3276
3497
  async function detectLegacyAssets(easyCodingDir) {
3277
3498
  const assets = [];
3278
3499
  for (const file of LEGACY_ROOT_FILES) {
3279
- if (await pathExists(path20.join(easyCodingDir, file))) {
3500
+ if (await pathExists(path21.join(easyCodingDir, file))) {
3280
3501
  assets.push(relativeEasyCodingPath(file));
3281
3502
  }
3282
3503
  }
3283
- if (await pathExists(path20.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3504
+ if (await pathExists(path21.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3284
3505
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
3285
3506
  }
3286
- const shortMemoryFiles = await listMarkdownFiles(path20.join(easyCodingDir, "memory", "short"));
3507
+ const shortMemoryFiles = await listMarkdownFiles(path21.join(easyCodingDir, "memory", "short"));
3287
3508
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
3288
3509
  for (const dir of ["spec", "prototype"]) {
3289
- if (await hasAnyDirectoryEntry(path20.join(easyCodingDir, dir))) {
3510
+ if (await hasAnyDirectoryEntry(path21.join(easyCodingDir, dir))) {
3290
3511
  assets.push(relativeEasyCodingPath(dir));
3291
3512
  }
3292
3513
  }
@@ -3306,13 +3527,13 @@ async function hasAnyDirectoryEntry(dir) {
3306
3527
  return (await readdir5(dir)).length > 0;
3307
3528
  }
3308
3529
  function relativeEasyCodingPath(...segments) {
3309
- return path20.posix.join(EASY_CODING_DIR, ...segments);
3530
+ return path21.posix.join(EASY_CODING_DIR, ...segments);
3310
3531
  }
3311
3532
  function relativeConfigPath() {
3312
- return path20.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3533
+ return path21.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3313
3534
  }
3314
3535
  function relativeProjectInitTaskPath() {
3315
- return path20.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
3536
+ return path21.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
3316
3537
  }
3317
3538
 
3318
3539
  // src/commands/init.ts
@@ -3393,7 +3614,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
3393
3614
  if (!targetSubmodulePaths.has(entry.path)) {
3394
3615
  continue;
3395
3616
  }
3396
- const dir = path21.join(cwd, entry.path);
3617
+ const dir = path22.join(cwd, entry.path);
3397
3618
  targets.push(
3398
3619
  await targetFromState(dir, entry.path, "submodule-child", {
3399
3620
  parent: toPosixRelative2(dir, cwd)
@@ -3440,24 +3661,24 @@ function contextFromState(role, installState) {
3440
3661
  };
3441
3662
  }
3442
3663
  function toPosixRelative2(from, to) {
3443
- const relative = path21.relative(from, to);
3444
- return relative ? relative.split(path21.sep).join("/") : ".";
3664
+ const relative = path22.relative(from, to);
3665
+ return relative ? relative.split(path22.sep).join("/") : ".";
3445
3666
  }
3446
3667
  async function resolveInitPlatforms(cwd, opts, parentInstalled) {
3447
3668
  if (opts.agent || !parentInstalled) {
3448
3669
  return resolvePlatforms(opts, ["claude-code"]);
3449
3670
  }
3450
- const config2 = await readConfigYaml(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
3671
+ const config2 = await readConfigYaml(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
3451
3672
  if (Array.isArray(config2.agents) && config2.agents.length > 0) {
3452
3673
  return config2.agents;
3453
3674
  }
3454
3675
  return resolvePlatforms(opts, ["claude-code"]);
3455
3676
  }
3456
3677
  async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
3457
- if (!await pathExists(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3678
+ if (!await pathExists(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3458
3679
  return;
3459
3680
  }
3460
- const config2 = parentTarget2.installed ? await readConfigYaml(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
3681
+ const config2 = parentTarget2.installed ? await readConfigYaml(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
3461
3682
  const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
3462
3683
  await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
3463
3684
  }
@@ -3466,7 +3687,7 @@ async function refreshInstalledChildTopologies(targets) {
3466
3687
  if (!target.installed || target.context.role !== "submodule-child") {
3467
3688
  continue;
3468
3689
  }
3469
- const configPath2 = path21.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3690
+ const configPath2 = path22.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3470
3691
  if (!await pathExists(configPath2)) {
3471
3692
  continue;
3472
3693
  }
@@ -3475,12 +3696,12 @@ async function refreshInstalledChildTopologies(targets) {
3475
3696
  }
3476
3697
 
3477
3698
  // src/commands/status.ts
3478
- import path23 from "path";
3699
+ import path24 from "path";
3479
3700
  import chalk6 from "chalk";
3480
3701
 
3481
3702
  // src/utils/session.ts
3482
3703
  import { readdir as readdir6, stat as stat3, unlink as unlink2 } from "fs/promises";
3483
- import path22 from "path";
3704
+ import path23 from "path";
3484
3705
  var DAY_MS = 24 * 60 * 60 * 1e3;
3485
3706
  var IDLE_SESSION_RETENTION_MS = 7 * DAY_MS;
3486
3707
  var ATTACHED_SESSION_RETENTION_MS = 30 * DAY_MS;
@@ -3494,7 +3715,7 @@ function parseSessionFile(content) {
3494
3715
  }
3495
3716
  }
3496
3717
  function getSessionDir(cwd) {
3497
- return path22.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3718
+ return path23.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3498
3719
  }
3499
3720
  async function listSessionFiles(cwd) {
3500
3721
  const dir = getSessionDir(cwd);
@@ -3506,7 +3727,7 @@ async function listSessionFiles(cwd) {
3506
3727
  if (!name.endsWith(".json")) {
3507
3728
  continue;
3508
3729
  }
3509
- const filePath = path22.join(dir, name);
3730
+ const filePath = path23.join(dir, name);
3510
3731
  const content = await readTextIfExists(filePath);
3511
3732
  if (!content) {
3512
3733
  continue;
@@ -3530,9 +3751,11 @@ async function cleanSessionRuntime(cwd, options = {}) {
3530
3751
  const maxSessions = options.maxSessions ?? MAX_SESSION_FILES;
3531
3752
  const reserveSlots = options.reserveSlots ?? 0;
3532
3753
  const candidates = (await listSessionCleanupCandidates(cwd)).filter((candidate) => {
3533
- if (!options.preserveTddSettings) return true;
3754
+ if (!options.preserveUnitTestSettings) return true;
3534
3755
  const session = parseSessionFile(candidate.content);
3535
- return !session || !("tdd_enabled" in session || "tdd_coverage_threshold" in session);
3756
+ return !session || !["unit_test_mode", "ut_coverage_threshold", "tdd_enabled", "tdd_coverage_threshold"].some(
3757
+ (key) => key in session
3758
+ );
3536
3759
  });
3537
3760
  const removed = /* @__PURE__ */ new Set();
3538
3761
  for (const candidate of candidates) {
@@ -3569,7 +3792,7 @@ async function listSessionCleanupCandidates(cwd) {
3569
3792
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
3570
3793
  continue;
3571
3794
  }
3572
- const filePath = path22.join(dir, entry.name);
3795
+ const filePath = path23.join(dir, entry.name);
3573
3796
  try {
3574
3797
  const [content, fileStat] = await Promise.all([readTextFile(filePath), stat3(filePath)]);
3575
3798
  const session = parseSessionFile(content);
@@ -3604,7 +3827,7 @@ async function unlinkIfUnchanged(candidate) {
3604
3827
  }
3605
3828
  }
3606
3829
  async function cleanOrphanAcceptanceSnapshots(cwd) {
3607
- const acceptanceDir = path22.join(getSessionDir(cwd), "acceptance");
3830
+ const acceptanceDir = path23.join(getSessionDir(cwd), "acceptance");
3608
3831
  if (!await pathExists(acceptanceDir)) {
3609
3832
  return 0;
3610
3833
  }
@@ -3613,7 +3836,7 @@ async function cleanOrphanAcceptanceSnapshots(cwd) {
3613
3836
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
3614
3837
  continue;
3615
3838
  }
3616
- const snapshotPath = path22.join(acceptanceDir, entry.name);
3839
+ const snapshotPath = path23.join(acceptanceDir, entry.name);
3617
3840
  if (!await isOrphanAcceptanceSnapshot(cwd, snapshotPath, entry.name.slice(0, -5))) {
3618
3841
  continue;
3619
3842
  }
@@ -3632,7 +3855,7 @@ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
3632
3855
  let taskContent;
3633
3856
  try {
3634
3857
  taskContent = await readTextFile(
3635
- path22.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json")
3858
+ path23.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json")
3636
3859
  );
3637
3860
  } catch (error) {
3638
3861
  if (isFileNotFound(error)) {
@@ -3654,7 +3877,7 @@ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
3654
3877
  return true;
3655
3878
  }
3656
3879
  const checkpoint = task.quality_checkpoint ?? task.verification_checkpoint;
3657
- return typeof checkpoint?.snapshot_file !== "string" || path22.resolve(cwd, checkpoint.snapshot_file) !== path22.resolve(snapshotPath);
3880
+ return typeof checkpoint?.snapshot_file !== "string" || path23.resolve(cwd, checkpoint.snapshot_file) !== path23.resolve(snapshotPath);
3658
3881
  }
3659
3882
  function isFileNotFound(error) {
3660
3883
  return error.code === "ENOENT";
@@ -3664,7 +3887,7 @@ function isFileNotFound(error) {
3664
3887
  async function status() {
3665
3888
  renderBanner();
3666
3889
  const cwd = process.cwd();
3667
- const configPath2 = path23.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3890
+ const configPath2 = path24.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3668
3891
  if (!await pathExists(configPath2)) {
3669
3892
  throw new Error("No easy-coding harness found in this project.");
3670
3893
  }
@@ -3674,7 +3897,6 @@ async function status() {
3674
3897
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
3675
3898
  const sessions = await listSessionFiles(cwd);
3676
3899
  const versionRelation = compareVersions(config2.harness_version, VERSION);
3677
- const tddReadiness = await inspectTddReadiness(cwd);
3678
3900
  console.log(chalk6.bold("Harness"));
3679
3901
  console.log(` version: ${config2.harness_version}`);
3680
3902
  console.log(` cli: ${VERSION}`);
@@ -3690,27 +3912,45 @@ async function status() {
3690
3912
  const migratedBehavior = resolveLegacyBehavior(config2);
3691
3913
  const projectApprovalMode = isApprovalMode(config2.behavior?.approval_mode) ? config2.behavior.approval_mode : migratedBehavior.approvalMode;
3692
3914
  const projectWorkflowMode = isConfiguredWorkflowMode(config2.behavior?.workflow_mode) ? config2.behavior.workflow_mode : migratedBehavior.workflowMode;
3693
- const projectTddEnabled = migratedBehavior.tddEnabled;
3694
- const projectTddCoverageThreshold = migratedBehavior.tddCoverageThreshold;
3915
+ const projectUnitTestMode = migratedBehavior.unitTestMode;
3916
+ const projectUtCoverageThreshold = migratedBehavior.utCoverageThreshold;
3917
+ const localBehavior = await readLocalBehavior();
3918
+ const projectBehavior = {
3919
+ ...config2.behavior,
3920
+ approval_mode: projectApprovalMode,
3921
+ unit_test_mode: projectUnitTestMode,
3922
+ ut_coverage_threshold: projectUtCoverageThreshold
3923
+ };
3924
+ const effective = resolveBehaviorSettings(projectBehavior, localBehavior);
3925
+ const needsCoverage = effective.values.unit_test_mode !== "none" || sessions.some(({ session }) => ["ut", "tdd"].includes(session.unit_test_mode ?? "none")) || activeTasks.some(({ task }) => ["ut", "tdd"].includes(task.unit_test_mode ?? "none"));
3926
+ const readiness = needsCoverage ? await inspectTddReadiness(cwd) : { status: "not_checked", reasons: [] };
3695
3927
  console.log(` approval_mode: ${projectApprovalMode}`);
3928
+ console.log(
3929
+ ` cooperate_mode: ${effective.values.cooperate_mode} (${effective.sources.cooperate_mode})`
3930
+ );
3696
3931
  console.log(` workflow_mode: ${projectWorkflowMode}`);
3697
- console.log(` tdd_enabled: ${projectTddEnabled}`);
3698
- console.log(` tdd_coverage_threshold: ${projectTddCoverageThreshold}`);
3699
- console.log(` tdd_readiness: ${tddReadiness.status}`);
3700
- if (tddReadiness.status !== "ready") {
3701
- console.log(` tdd_readiness_reasons: ${tddReadiness.reasons.join("; ")}`);
3932
+ console.log(` unit_test_mode: ${projectUnitTestMode}`);
3933
+ console.log(` ut_coverage_threshold: ${projectUtCoverageThreshold}`);
3934
+ console.log(` unit_test_readiness: ${readiness.status}`);
3935
+ if (readiness.reasons.length > 0) {
3936
+ console.log(` unit_test_readiness_reasons: ${readiness.reasons.join("; ")}`);
3702
3937
  }
3703
3938
  console.log("");
3704
3939
  console.log(chalk6.bold("Sessions"));
3705
3940
  console.log(` project_approval_mode: ${projectApprovalMode}`);
3706
3941
  console.log(` project_workflow_mode: ${projectWorkflowMode}`);
3707
- console.log(` project_tdd_enabled: ${projectTddEnabled}`);
3708
- console.log(` project_tdd_coverage_threshold: ${projectTddCoverageThreshold}`);
3709
- console.log(` effective_approval_mode: ${projectApprovalMode} (without a session override)`);
3942
+ console.log(` project_unit_test_mode: ${projectUnitTestMode}`);
3943
+ console.log(` project_ut_coverage_threshold: ${projectUtCoverageThreshold}`);
3944
+ for (const [key, value] of Object.entries(localBehavior)) console.log(` local_${key}: ${value}`);
3945
+ console.log(
3946
+ ` effective_approval_mode: ${effective.values.approval_mode} (${effective.sources.approval_mode})`
3947
+ );
3710
3948
  console.log(` configured_workflow_mode: ${projectWorkflowMode} (without a session override)`);
3711
- console.log(` effective_tdd_enabled: ${projectTddEnabled} (without a session override)`);
3712
3949
  console.log(
3713
- ` effective_tdd_coverage_threshold: ${projectTddCoverageThreshold} (without a session override)`
3950
+ ` effective_unit_test_mode: ${effective.values.unit_test_mode} (${effective.sources.unit_test_mode})`
3951
+ );
3952
+ console.log(
3953
+ ` effective_ut_coverage_threshold: ${effective.values.ut_coverage_threshold} (${effective.sources.ut_coverage_threshold})`
3714
3954
  );
3715
3955
  if (sessions.length === 0) {
3716
3956
  console.log(" no session files");
@@ -3722,21 +3962,28 @@ async function status() {
3722
3962
  );
3723
3963
  const sessionApprovalMode = session.approval_mode ?? (legacySessionMode === "lite" ? "guard" : legacySessionMode);
3724
3964
  const sessionWorkflowMode = session.workflow_mode ?? (legacySessionMode === "lite" ? "fast" : hasLegacySessionMode ? "adaptive" : void 0);
3725
- const sessionTddEnabled = session.tdd_enabled;
3726
- const sessionTddCoverageThreshold = session.tdd_coverage_threshold;
3965
+ const sessionUnitTestMode = session.unit_test_mode;
3966
+ const sessionUtCoverageThreshold = session.ut_coverage_threshold;
3967
+ const resolved = resolveBehaviorSettings(projectBehavior, localBehavior, {
3968
+ ...session,
3969
+ approval_mode: sessionApprovalMode
3970
+ });
3727
3971
  console.log(` - ${key}`);
3728
3972
  console.log(` agent: ${session.agent ?? "legacy/unknown"}`);
3729
3973
  console.log(` source: ${session.session_source ?? "legacy"}`);
3730
- console.log(` approval_mode: ${sessionApprovalMode ?? "project default"}`);
3974
+ console.log(` approval_mode: ${sessionApprovalMode ?? "inherit local/project"}`);
3731
3975
  console.log(` workflow_mode: ${sessionWorkflowMode ?? "project default"}`);
3732
- console.log(` tdd_enabled: ${sessionTddEnabled ?? "project default"}`);
3733
- console.log(` tdd_coverage_threshold: ${sessionTddCoverageThreshold ?? "project default"}`);
3734
- console.log(` effective_approval_mode: ${sessionApprovalMode ?? projectApprovalMode}`);
3735
- console.log(` configured_workflow_mode: ${sessionWorkflowMode ?? projectWorkflowMode}`);
3736
- console.log(` effective_tdd_enabled: ${sessionTddEnabled ?? projectTddEnabled}`);
3976
+ console.log(` unit_test_mode: ${sessionUnitTestMode ?? "inherit local/project"}`);
3977
+ console.log(
3978
+ ` ut_coverage_threshold: ${sessionUtCoverageThreshold ?? "inherit local/project"}`
3979
+ );
3980
+ console.log(` effective_approval_mode: ${resolved.values.approval_mode}`);
3737
3981
  console.log(
3738
- ` effective_tdd_coverage_threshold: ${sessionTddCoverageThreshold ?? projectTddCoverageThreshold}`
3982
+ ` cooperate_mode: ${resolved.values.cooperate_mode} (${resolved.sources.cooperate_mode})`
3739
3983
  );
3984
+ console.log(` configured_workflow_mode: ${sessionWorkflowMode ?? projectWorkflowMode}`);
3985
+ console.log(` effective_unit_test_mode: ${resolved.values.unit_test_mode}`);
3986
+ console.log(` effective_ut_coverage_threshold: ${resolved.values.ut_coverage_threshold}`);
3740
3987
  console.log(
3741
3988
  ` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
3742
3989
  );
@@ -3756,10 +4003,8 @@ async function status() {
3756
4003
  console.log(
3757
4004
  ` task_workflow_mode: ${task.workflow_mode ?? task.workflow_mode_proposal?.selected_mode ?? "not resolved"}`
3758
4005
  );
3759
- console.log(` task_tdd_enabled: ${task.tdd_enabled ?? "not frozen"}`);
3760
- console.log(
3761
- ` task_tdd_coverage_threshold: ${task.tdd_coverage_threshold ?? "not frozen"}`
3762
- );
4006
+ console.log(` task_unit_test_mode: ${task.unit_test_mode ?? "not frozen"}`);
4007
+ console.log(` task_ut_coverage_threshold: ${task.ut_coverage_threshold ?? "not frozen"}`);
3763
4008
  console.log(` last_agent: ${task.last_agent}`);
3764
4009
  } else {
3765
4010
  console.log(` current_task: ${session.current_task} (task.json missing)`);
@@ -3811,7 +4056,7 @@ async function update(opts) {
3811
4056
  }
3812
4057
 
3813
4058
  // src/commands/upgrade.ts
3814
- import path24 from "path";
4059
+ import path25 from "path";
3815
4060
  import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
3816
4061
  import chalk8 from "chalk";
3817
4062
  var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
@@ -3874,10 +4119,11 @@ async function upgrade(opts) {
3874
4119
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
3875
4120
  "Will remove retired files that still match the previous install manifest and preserve locally modified copies.",
3876
4121
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
3877
- "Will migrate behavior config to schema 5 while preserving project/session TDD settings and frozen task baselines.",
3878
- "Will prune expired session bindings without explicit TDD settings and orphan acceptance snapshots in each upgraded target while preserving tasks, memory, spec, and project knowledge.",
4122
+ "Will migrate behavior config to schema 6 while preserving project/session unit test settings and frozen task baselines.",
4123
+ "Will preserve runtime tools used by active UT/TDD tasks; run upgrade after those tasks finish to refresh the tools.",
4124
+ "Will prune expired session bindings without explicit unit test settings and orphan acceptance snapshots in each upgraded target while preserving tasks, memory, spec, and project knowledge.",
3879
4125
  "Will migrate active REVIEW/VERIFICATION tasks to QUALITY, retire active read-only task types as CLOSED, and preserve their artifacts and history.",
3880
- "Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
4126
+ "Will migrate legacy workflow/unit test task metadata; memory content, spec, and project knowledge files remain untouched."
3881
4127
  ].join("\n");
3882
4128
  if (opts.dryRun) {
3883
4129
  console.log(summary);
@@ -3895,7 +4141,9 @@ async function upgrade(opts) {
3895
4141
  }
3896
4142
  for (const { target, config: config2 } of pending) {
3897
4143
  const previousManifest = await readInstallManifest(target.dir);
3898
- const sessionCleanup = await cleanSessionRuntime(target.dir, { preserveTddSettings: true });
4144
+ const sessionCleanup = await cleanSessionRuntime(target.dir, {
4145
+ preserveUnitTestSettings: true
4146
+ });
3899
4147
  if (sessionCleanup.sessionsRemoved > 0 || sessionCleanup.acceptanceSnapshotsRemoved > 0) {
3900
4148
  console.log(
3901
4149
  chalk8.yellow(
@@ -3968,7 +4216,7 @@ async function resolvePendingUpgradeTargets(targets) {
3968
4216
  `${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
3969
4217
  );
3970
4218
  }
3971
- if (relation === -1 || relation === 0 && (installedVersion !== VERSION || await needsHookConfigRefresh(target, config2) || await hasLegacyWorkflowState(target.dir))) {
4219
+ if (relation === -1 || relation === 0 && (installedVersion !== VERSION || await needsHookConfigRefresh(target, config2) || await hasLegacyWorkflowState(target.dir) || (await runtimeToolUpdates(target.dir)).length > 0)) {
3972
4220
  pending.push({ target, config: config2 });
3973
4221
  }
3974
4222
  }
@@ -3982,7 +4230,7 @@ async function needsHookConfigRefresh(target, config2) {
3982
4230
  const manifest = await readInstallManifest(target.dir);
3983
4231
  for (const agent of config2.agents) {
3984
4232
  const meta = resolvePlatformMeta(target.dir, agent);
3985
- const configPath2 = path24.join(target.dir, meta.hookConfigFile);
4233
+ const configPath2 = path25.join(target.dir, meta.hookConfigFile);
3986
4234
  const content = await readTextIfExists(configPath2);
3987
4235
  if (content === null) {
3988
4236
  return true;
@@ -4122,7 +4370,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
4122
4370
  return true;
4123
4371
  }
4124
4372
  return pathAliases(
4125
- normalizePathForHookComparison(path24.resolve(cwd, relativeHookPath))
4373
+ normalizePathForHookComparison(path25.resolve(cwd, relativeHookPath))
4126
4374
  ).includes(normalizedHookPath);
4127
4375
  });
4128
4376
  }
@@ -4212,7 +4460,7 @@ program.command("init").description("Initialize easy-coding harness in current p
4212
4460
  program.command("add-agent").description("Add agent platform support to an existing project").option("--agent <list>", "Comma-separated platforms to add").option("--submodules <list>", "Comma-separated initialized submodule paths or names to update").option("--no-submodules", "Add the agent only to the current directory").action(withErrorHandling(addAgent));
4213
4461
  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));
4214
4462
  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));
4215
- program.command("config").description("Interactively configure project-level harness behavior").action(withErrorHandling(config));
4463
+ program.command("config").description("Configure project or local harness behavior").option("--scope <scope>", "project or local", "project").option("--approval-mode <mode>", "approve, guard, confirm, or auto").option("--cooperate-mode <mode>", "default or dispatch").option("--unit-test-mode <mode>", "none, ut, or tdd").option("--ut-coverage-threshold <percent>", "coverage threshold 1..100").option("--reset <field>", "Remove an override to restore inheritance").option("-y, --yes", "Save explicitly selected fields without another prompt").action(withErrorHandling(config));
4216
4464
  program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
4217
4465
  program.command("clear").description("Remove installed harness files (skills, hooks, config); keep tasks, spec, memory").option("--submodules <list>", "Comma-separated initialized submodule paths or names to clear").option("--no-submodules", "Clear only the current directory").option("--dry-run", "Preview what would be removed without deleting").option("-y, --yes", "Skip confirmation").action(withErrorHandling(clear));
4218
4466
  program.parse();