easy-coding-harness 1.1.0-beta.1 → 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
@@ -107,7 +109,71 @@ var CONFIG_SCHEMA_VERSION = 6;
107
109
  var DEFAULT_UT_COVERAGE_THRESHOLD = 90;
108
110
  var UNIT_TEST_MODES = ["none", "ut", "tdd"];
109
111
  var APPROVAL_MODES = ["approve", "guard", "confirm", "auto"];
112
+ var COOPERATE_MODES = ["default", "dispatch"];
110
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
+ }
111
177
  function createDefaultConfig(params) {
112
178
  const config2 = {
113
179
  version: CONFIG_SCHEMA_VERSION,
@@ -129,7 +195,8 @@ function createDefaultConfig(params) {
129
195
  approval_mode: "guard",
130
196
  workflow_mode: "adaptive",
131
197
  unit_test_mode: "none",
132
- ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD
198
+ ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD,
199
+ cooperate_mode: "default"
133
200
  }
134
201
  };
135
202
  if (params.supermodule) {
@@ -215,7 +282,7 @@ function resolveLegacyBehavior(config2) {
215
282
  const utCoverageThreshold = isUtCoverageThreshold(configuredThreshold) ? configuredThreshold : DEFAULT_UT_COVERAGE_THRESHOLD;
216
283
  return { approvalMode, workflowMode, unitTestMode, utCoverageThreshold };
217
284
  }
218
- async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold) {
285
+ async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold, cooperateMode) {
219
286
  if (unitTestMode !== void 0 && !isUnitTestMode(unitTestMode)) {
220
287
  throw new Error("Unit test mode must be none, ut, or tdd.");
221
288
  }
@@ -234,6 +301,7 @@ async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMo
234
301
  behavior.workflow_mode = workflowMode;
235
302
  behavior.unit_test_mode = unitTestMode ?? resolvedBehavior.unitTestMode;
236
303
  behavior.ut_coverage_threshold = utCoverageThreshold ?? resolvedBehavior.utCoverageThreshold;
304
+ behavior.cooperate_mode = cooperateMode ?? legacyBehavior.cooperate_mode ?? "default";
237
305
  config2.behavior = behavior;
238
306
  config2.version = CONFIG_SCHEMA_VERSION;
239
307
  });
@@ -266,7 +334,7 @@ function createProjectId() {
266
334
  }
267
335
 
268
336
  // src/utils/gitignore.ts
269
- import path3 from "path";
337
+ import path4 from "path";
270
338
 
271
339
  // src/constants/paths.ts
272
340
  var EASY_CODING_DIR = ".easy-coding";
@@ -290,7 +358,7 @@ var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness gene
290
358
 
291
359
  // src/utils/gitignore.ts
292
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") {
293
- const gitignorePath = path3.join(cwd, ".gitignore");
361
+ const gitignorePath = path4.join(cwd, ".gitignore");
294
362
  const current = await readTextIfExists(gitignorePath) ?? "";
295
363
  const lines = current.split(/\r?\n/).map((line) => line.trim());
296
364
  if (lines.includes(entry)) {
@@ -315,7 +383,7 @@ async function ensureHookBytecodeIgnored(cwd) {
315
383
  // src/utils/install-manifest.ts
316
384
  import { createHash } from "crypto";
317
385
  import { readFile as readFile3, rmdir, unlink } from "fs/promises";
318
- import path4 from "path";
386
+ import path5 from "path";
319
387
 
320
388
  // src/types/platform.ts
321
389
  var pythonCmd = process.platform === "win32" ? "python" : "python3";
@@ -487,12 +555,12 @@ async function writeInstallManifest(cwd, params) {
487
555
  constraint_regions: [...constraintRegions.values()].sort(byPath)
488
556
  };
489
557
  await writeTextFile(
490
- path4.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
558
+ path5.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
491
559
  JSON.stringify(manifest, null, 2)
492
560
  );
493
561
  }
494
562
  async function readInstallManifest(cwd) {
495
- const manifestPath2 = path4.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
563
+ const manifestPath2 = path5.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
496
564
  const content = await readTextIfExists(manifestPath2);
497
565
  if (content === null) {
498
566
  return null;
@@ -534,7 +602,7 @@ async function pruneRetiredManagedFiles(cwd, previous, artifacts) {
534
602
  await unlink(filePath);
535
603
  removed.push(file.path);
536
604
  try {
537
- await rmdir(path4.dirname(filePath));
605
+ await rmdir(path5.dirname(filePath));
538
606
  } catch (error) {
539
607
  if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) {
540
608
  throw error;
@@ -553,10 +621,10 @@ function manifestPath(cwd, projectPath) {
553
621
  return resolveProjectPath(cwd, projectPath);
554
622
  }
555
623
  function toProjectPath(cwd, filePath) {
556
- const root = path4.resolve(cwd);
557
- const resolved = path4.resolve(filePath);
624
+ const root = path5.resolve(cwd);
625
+ const resolved = path5.resolve(filePath);
558
626
  assertPathInsideProject(root, resolved, filePath);
559
- return path4.relative(root, resolved).split(path4.sep).join("/");
627
+ return path5.relative(root, resolved).split(path5.sep).join("/");
560
628
  }
561
629
  function normalizeCommand(command) {
562
630
  return command.replace(/\\/g, "/").trim().replace(/\s+/g, " ");
@@ -568,17 +636,17 @@ async function sha256File(filePath) {
568
636
  function resolveProjectPath(cwd, projectPath) {
569
637
  const normalized = projectPath.replace(/\\/g, "/");
570
638
  const parts = normalized.split("/");
571
- 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 === "..")) {
572
640
  throw new Error(`Unsafe install manifest path: ${projectPath}`);
573
641
  }
574
- const root = path4.resolve(cwd);
575
- const resolved = path4.resolve(root, normalized);
642
+ const root = path5.resolve(cwd);
643
+ const resolved = path5.resolve(root, normalized);
576
644
  assertPathInsideProject(root, resolved, projectPath);
577
645
  return resolved;
578
646
  }
579
647
  function assertPathInsideProject(root, resolvedPath, sourcePath) {
580
- const relative = path4.relative(root, resolvedPath);
581
- if (!relative || relative.startsWith("..") || path4.isAbsolute(relative)) {
648
+ const relative = path5.relative(root, resolvedPath);
649
+ if (!relative || relative.startsWith("..") || path5.isAbsolute(relative)) {
582
650
  throw new Error(`Unsafe install manifest path: ${sourcePath}`);
583
651
  }
584
652
  }
@@ -677,7 +745,7 @@ function isHookPythonPath(candidate) {
677
745
  }
678
746
  function normalizeHookPath(cwd, hookPath) {
679
747
  const normalized = hookPath.replace(/\\/g, "/").replace(/^\.\//, "");
680
- 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)) {
681
749
  return toProjectPath(cwd, hookPath);
682
750
  }
683
751
  return normalized;
@@ -687,11 +755,11 @@ function byPath(a, b) {
687
755
  }
688
756
 
689
757
  // src/utils/runtime-scaffold.ts
690
- import path7 from "path";
758
+ import path8 from "path";
691
759
 
692
760
  // src/utils/task-json.ts
693
761
  import { readdir } from "fs/promises";
694
- import path5 from "path";
762
+ import path6 from "path";
695
763
  var LEGACY_STAGE_MAP = {
696
764
  WAITING_CONFIRM: "ANALYSIS",
697
765
  MEMORY_SHORT: "MEMORY",
@@ -718,7 +786,7 @@ function createProjectInitTask(params) {
718
786
  };
719
787
  }
720
788
  function getTaskJsonPath(cwd, taskId) {
721
- return path5.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
789
+ return path6.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
722
790
  }
723
791
  async function writeTaskJson(filePath, task) {
724
792
  await writeTextFile(filePath, JSON.stringify(task, null, 2));
@@ -1031,16 +1099,16 @@ function migrationWorkflowMode(task, candidates, projectWorkflowMode) {
1031
1099
  );
1032
1100
  }
1033
1101
  async function taskFiles(cwd) {
1034
- const tasksDir = path5.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1102
+ const tasksDir = path6.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1035
1103
  if (!await pathExists(tasksDir)) return [];
1036
1104
  const entries = await readdir(tasksDir, { withFileTypes: true });
1037
- return entries.filter((entry) => entry.isDirectory()).map((entry) => path5.join(tasksDir, entry.name, "task.json"));
1105
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => path6.join(tasksDir, entry.name, "task.json"));
1038
1106
  }
1039
1107
  async function sessionFiles(cwd) {
1040
- const sessionsDir = path5.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
1108
+ const sessionsDir = path6.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
1041
1109
  if (!await pathExists(sessionsDir)) return [];
1042
1110
  const entries = await readdir(sessionsDir, { withFileTypes: true });
1043
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path5.join(sessionsDir, entry.name));
1111
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path6.join(sessionsDir, entry.name));
1044
1112
  }
1045
1113
  async function readJsonRecord(filePath) {
1046
1114
  try {
@@ -1078,7 +1146,7 @@ async function migrateLegacyWorkflowState(cwd) {
1078
1146
  let sessionsUpdated = 0;
1079
1147
  const updatedTaskPaths = /* @__PURE__ */ new Set();
1080
1148
  let projectWorkflowMode = "adaptive";
1081
- const configPath2 = path5.join(cwd, EASY_CODING_DIR, "config.yaml");
1149
+ const configPath2 = path6.join(cwd, EASY_CODING_DIR, "config.yaml");
1082
1150
  if (await pathExists(configPath2)) {
1083
1151
  try {
1084
1152
  const config2 = await readConfigYaml(configPath2);
@@ -1133,7 +1201,7 @@ async function migrateLegacyWorkflowState(cwd) {
1133
1201
  if (!task) continue;
1134
1202
  const migrationOwned = task.workflow_mode_legacy === true && task.workflow_mode_confirmed_by === "upgrade-migration";
1135
1203
  if (!migrationOwned) continue;
1136
- const taskId = path5.basename(path5.dirname(filePath));
1204
+ const taskId = path6.basename(path6.dirname(filePath));
1137
1205
  const mode = migrationWorkflowMode(
1138
1206
  task,
1139
1207
  candidatesByTask.get(taskId) ?? [],
@@ -1168,7 +1236,7 @@ async function setPendingInitSince(cwd, version) {
1168
1236
  await writeTaskJson(filePath, task);
1169
1237
  }
1170
1238
  async function listTasks(cwd) {
1171
- const tasksDir = path5.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1239
+ const tasksDir = path6.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1172
1240
  if (!await pathExists(tasksDir)) {
1173
1241
  return [];
1174
1242
  }
@@ -1201,15 +1269,15 @@ function isActiveTask(task) {
1201
1269
 
1202
1270
  // src/utils/template-paths.ts
1203
1271
  import { existsSync } from "fs";
1204
- import path6 from "path";
1272
+ import path7 from "path";
1205
1273
  import { fileURLToPath as fileURLToPath2 } from "url";
1206
1274
  function getTemplateRoot() {
1207
- const here = path6.dirname(fileURLToPath2(import.meta.url));
1275
+ const here = path7.dirname(fileURLToPath2(import.meta.url));
1208
1276
  const candidates = [
1209
- path6.resolve(here, "../templates"),
1210
- path6.resolve(here, "../../templates"),
1211
- path6.resolve(process.cwd(), "src/templates"),
1212
- path6.resolve(process.cwd(), "templates")
1277
+ path7.resolve(here, "../templates"),
1278
+ path7.resolve(here, "../../templates"),
1279
+ path7.resolve(process.cwd(), "src/templates"),
1280
+ path7.resolve(process.cwd(), "templates")
1213
1281
  ];
1214
1282
  const found = candidates.find((candidate) => existsSync(candidate));
1215
1283
  if (!found) {
@@ -1218,17 +1286,17 @@ function getTemplateRoot() {
1218
1286
  return found;
1219
1287
  }
1220
1288
  function getTemplatePath(...segments) {
1221
- return path6.join(getTemplateRoot(), ...segments);
1289
+ return path7.join(getTemplateRoot(), ...segments);
1222
1290
  }
1223
1291
 
1224
1292
  // src/utils/runtime-scaffold.ts
1225
1293
  async function writeRuntimeScaffold(cwd, agents, opts = {}) {
1226
- const easyCodingDir = path7.join(cwd, EASY_CODING_DIR);
1294
+ const easyCodingDir = path8.join(cwd, EASY_CODING_DIR);
1227
1295
  await ensureDir(easyCodingDir);
1228
- const configPath2 = path7.join(easyCodingDir, CONFIG_FILE);
1296
+ const configPath2 = path8.join(easyCodingDir, CONFIG_FILE);
1229
1297
  let projectId = opts.projectId ?? createProjectId();
1230
1298
  if (!await pathExists(configPath2)) {
1231
- const projectName = path7.basename(cwd);
1299
+ const projectName = path8.basename(cwd);
1232
1300
  await writeConfigYaml(
1233
1301
  configPath2,
1234
1302
  createDefaultConfig({
@@ -1242,10 +1310,10 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
1242
1310
  } else {
1243
1311
  projectId = await ensureProjectId(configPath2);
1244
1312
  }
1245
- await ensureDir(path7.join(easyCodingDir, "tasks"));
1246
- await ensureDir(path7.join(easyCodingDir, SESSIONS_DIR));
1247
- await ensureDir(path7.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
1248
- await ensureDir(path7.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
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));
1249
1317
  await writeMemoryScaffold(easyCodingDir);
1250
1318
  await writeTemplatesScaffold(easyCodingDir);
1251
1319
  await writeToolsScaffold(cwd);
@@ -1257,7 +1325,7 @@ async function runtimeToolUpdates(cwd) {
1257
1325
  );
1258
1326
  const updates = [];
1259
1327
  for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
1260
- const target = path7.join(cwd, EASY_CODING_DIR, TOOLS_DIR, file);
1328
+ const target = path8.join(cwd, EASY_CODING_DIR, TOOLS_DIR, file);
1261
1329
  const current = await readTextIfExists(target);
1262
1330
  if (frozen && current !== null) continue;
1263
1331
  const content = await readTextFile(getTemplatePath("runtime", "tools", file));
@@ -1266,32 +1334,32 @@ async function runtimeToolUpdates(cwd) {
1266
1334
  return updates;
1267
1335
  }
1268
1336
  async function writeToolsScaffold(cwd) {
1269
- const toolsDir = path7.join(cwd, EASY_CODING_DIR, TOOLS_DIR);
1337
+ const toolsDir = path8.join(cwd, EASY_CODING_DIR, TOOLS_DIR);
1270
1338
  await ensureDir(toolsDir);
1271
1339
  for (const update2 of await runtimeToolUpdates(cwd)) {
1272
1340
  await writeTextFile(update2.path, update2.content);
1273
1341
  }
1274
1342
  }
1275
1343
  async function writeTemplatesScaffold(easyCodingDir) {
1276
- const templatesDir = path7.join(easyCodingDir, TEMPLATES_DIR);
1344
+ const templatesDir = path8.join(easyCodingDir, TEMPLATES_DIR);
1277
1345
  await ensureDir(templatesDir);
1278
1346
  const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
1279
- const dest = path7.join(templatesDir, "dev-spec-skeleton.md");
1347
+ const dest = path8.join(templatesDir, "dev-spec-skeleton.md");
1280
1348
  await writeTextFile(dest, await readTextFile(src));
1281
1349
  }
1282
1350
  async function writeMemoryScaffold(easyCodingDir) {
1283
- const memoryDir = path7.join(easyCodingDir, MEMORY_DIR);
1284
- await ensureDir(path7.join(memoryDir, "short"));
1285
- await ensureDir(path7.join(memoryDir, "long"));
1351
+ const memoryDir = path8.join(easyCodingDir, MEMORY_DIR);
1352
+ await ensureDir(path8.join(memoryDir, "short"));
1353
+ await ensureDir(path8.join(memoryDir, "long"));
1286
1354
  for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
1287
- const destination = path7.join(memoryDir, "long", file);
1355
+ const destination = path8.join(memoryDir, "long", file);
1288
1356
  if (await pathExists(destination)) {
1289
1357
  continue;
1290
1358
  }
1291
1359
  const templatePath = getTemplatePath("runtime", "memory", "long", file);
1292
1360
  await writeTextFile(destination, await readTextFile(templatePath));
1293
1361
  }
1294
- const shortTemplateDest = path7.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
1362
+ const shortTemplateDest = path8.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
1295
1363
  if (!await pathExists(shortTemplateDest)) {
1296
1364
  const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
1297
1365
  await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
@@ -1299,14 +1367,14 @@ async function writeMemoryScaffold(easyCodingDir) {
1299
1367
  }
1300
1368
 
1301
1369
  // src/commands/install-harness.ts
1302
- import path13 from "path";
1370
+ import path14 from "path";
1303
1371
 
1304
1372
  // src/configurators/claude.ts
1305
- import path9 from "path";
1373
+ import path10 from "path";
1306
1374
 
1307
1375
  // src/configurators/shared.ts
1308
1376
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
1309
- import path8 from "path";
1377
+ import path9 from "path";
1310
1378
 
1311
1379
  // src/utils/marked-region.ts
1312
1380
  var MarkedRegionError = class extends Error {
@@ -1365,7 +1433,7 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
1365
1433
  return resolved;
1366
1434
  }
1367
1435
  async function withProjectInstallPaths(cwd, ctx, projectId) {
1368
- 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));
1369
1437
  return withInstallPaths(cwd, ctx, resolvedProjectId ?? void 0);
1370
1438
  }
1371
1439
  function withInstallPaths(cwd, ctx, projectId) {
@@ -1453,11 +1521,11 @@ async function resolveSkills(ctx) {
1453
1521
  const entries = await readdir2(skillsRoot);
1454
1522
  const skills = [];
1455
1523
  for (const entry of entries.sort()) {
1456
- const skillDir = path8.join(skillsRoot, entry);
1524
+ const skillDir = path9.join(skillsRoot, entry);
1457
1525
  if (!await isDirectory(skillDir)) {
1458
1526
  continue;
1459
1527
  }
1460
- const content = await readTextFile(path8.join(skillDir, "SKILL.md"));
1528
+ const content = await readTextFile(path9.join(skillDir, "SKILL.md"));
1461
1529
  skills.push({
1462
1530
  name: entry,
1463
1531
  content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
@@ -1473,7 +1541,7 @@ async function resolveBundledSkills(ctx) {
1473
1541
  const entries = await readdir2(bundledRoot);
1474
1542
  const bundled = [];
1475
1543
  for (const entry of entries.sort()) {
1476
- const sourceDir = path8.join(bundledRoot, entry);
1544
+ const sourceDir = path9.join(bundledRoot, entry);
1477
1545
  if (await isDirectory(sourceDir)) {
1478
1546
  bundled.push({ name: entry, sourceDir, context: ctx });
1479
1547
  }
@@ -1484,13 +1552,13 @@ async function writeSkills(dir, skills, bundled) {
1484
1552
  await ensureDir(dir);
1485
1553
  const written = [];
1486
1554
  for (const skill of skills) {
1487
- const destination = path8.join(dir, skill.name, "SKILL.md");
1555
+ const destination = path9.join(dir, skill.name, "SKILL.md");
1488
1556
  await writeTextFile(destination, skill.content);
1489
1557
  written.push(destination);
1490
1558
  }
1491
1559
  for (const skill of bundled) {
1492
1560
  written.push(
1493
- ...await copyTemplateDirectory(skill.sourceDir, path8.join(dir, skill.name), skill.context)
1561
+ ...await copyTemplateDirectory(skill.sourceDir, path9.join(dir, skill.name), skill.context)
1494
1562
  );
1495
1563
  }
1496
1564
  return written;
@@ -1505,7 +1573,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
1505
1573
  if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
1506
1574
  continue;
1507
1575
  }
1508
- const sourcePath = path8.join(hooksRoot, entry);
1576
+ const sourcePath = path9.join(hooksRoot, entry);
1509
1577
  if ((await stat2(sourcePath)).isDirectory()) {
1510
1578
  continue;
1511
1579
  }
@@ -1514,7 +1582,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
1514
1582
  ctx,
1515
1583
  `shared-hooks/${entry}`
1516
1584
  );
1517
- const destination = path8.join(dir, entry);
1585
+ const destination = path9.join(dir, entry);
1518
1586
  await writeTextFile(destination, content);
1519
1587
  await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
1520
1588
  written.push(destination);
@@ -1537,7 +1605,7 @@ async function writeMainConstraint(cwd, platform, opts = {}) {
1537
1605
  if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
1538
1606
  throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
1539
1607
  }
1540
- const destination = path8.join(cwd, meta.mainConstraint);
1608
+ const destination = path9.join(cwd, meta.mainConstraint);
1541
1609
  const current = await readTextIfExists(destination);
1542
1610
  if (current) {
1543
1611
  const markerContent = extractMarkedRegion(generated);
@@ -1579,8 +1647,8 @@ async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__
1579
1647
  if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
1580
1648
  continue;
1581
1649
  }
1582
- const sourcePath = path8.join(source, entry);
1583
- const destinationPath = path8.join(destination, stripTemplateExtension(entry));
1650
+ const sourcePath = path9.join(source, entry);
1651
+ const destinationPath = path9.join(destination, stripTemplateExtension(entry));
1584
1652
  const sourceStat = await stat2(sourcePath);
1585
1653
  if (sourceStat.isDirectory()) {
1586
1654
  written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
@@ -1604,28 +1672,28 @@ async function configureClaude(cwd, opts = {}) {
1604
1672
  const platform = "claude-code";
1605
1673
  const meta = PLATFORM_META[platform];
1606
1674
  const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
1607
- const dest = path9.join(cwd, ".claude");
1608
- const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
1675
+ const dest = path10.join(cwd, ".claude");
1676
+ const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
1609
1677
  const artifacts = [];
1610
1678
  const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
1611
1679
  artifacts.push(
1612
1680
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
1613
1681
  (filePath) => fileArtifact(
1614
1682
  filePath,
1615
- filePath.startsWith(path9.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1683
+ filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1616
1684
  platform
1617
1685
  )
1618
1686
  )
1619
1687
  );
1620
1688
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
1621
1689
  artifacts.push(
1622
- ...(await writeSharedHooks(path9.join(dest, "hooks"), platform)).map(
1690
+ ...(await writeSharedHooks(path10.join(dest, "hooks"), platform)).map(
1623
1691
  (filePath) => fileArtifact(filePath, "hook", platform)
1624
1692
  )
1625
1693
  );
1626
1694
  artifacts.push(
1627
1695
  ...(await writeSkills(
1628
- path9.join(cwd, meta.skillsDir),
1696
+ path10.join(cwd, meta.skillsDir),
1629
1697
  await resolveSkills(ctx),
1630
1698
  await resolveBundledSkills(ctx)
1631
1699
  )).map((filePath) => fileArtifact(filePath, "skill", platform))
@@ -1637,28 +1705,28 @@ async function configureClaude(cwd, opts = {}) {
1637
1705
  }
1638
1706
 
1639
1707
  // src/configurators/codex.ts
1640
- import path10 from "path";
1708
+ import path11 from "path";
1641
1709
  async function configureCodex(cwd, opts = {}) {
1642
1710
  const platform = "codex";
1643
1711
  const meta = PLATFORM_META[platform];
1644
1712
  const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
1645
- const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
1713
+ const hookConfigPath = path11.join(cwd, meta.hookConfigFile);
1646
1714
  const artifacts = [];
1647
1715
  artifacts.push(
1648
1716
  ...(await writeSkills(
1649
- path10.join(cwd, meta.skillsDir),
1717
+ path11.join(cwd, meta.skillsDir),
1650
1718
  await resolveSkills(ctx),
1651
1719
  await resolveBundledSkills(ctx)
1652
1720
  )).map((filePath) => fileArtifact(filePath, "skill", platform))
1653
1721
  );
1654
1722
  artifacts.push(
1655
- ...(await writeSharedHooks(path10.join(cwd, meta.hooksDir), platform, {
1723
+ ...(await writeSharedHooks(path11.join(cwd, meta.hooksDir), platform, {
1656
1724
  skipSubagentContext: true
1657
1725
  })).map((filePath) => fileArtifact(filePath, "hook", platform))
1658
1726
  );
1659
1727
  const platformFiles = await copyPlatformTemplates(
1660
1728
  "codex",
1661
- path10.join(cwd, ".codex"),
1729
+ path11.join(cwd, ".codex"),
1662
1730
  ["hooks"],
1663
1731
  ctx
1664
1732
  );
@@ -1666,7 +1734,7 @@ async function configureCodex(cwd, opts = {}) {
1666
1734
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
1667
1735
  (filePath) => fileArtifact(
1668
1736
  filePath,
1669
- filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1737
+ filePath.startsWith(path11.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1670
1738
  platform
1671
1739
  )
1672
1740
  )
@@ -1680,16 +1748,16 @@ async function configureCodex(cwd, opts = {}) {
1680
1748
 
1681
1749
  // src/configurators/qoder.ts
1682
1750
  import { readdir as readdir3 } from "fs/promises";
1683
- import path12 from "path";
1751
+ import path13 from "path";
1684
1752
 
1685
1753
  // src/utils/platform-paths.ts
1686
1754
  import { existsSync as existsSync2 } from "fs";
1687
- import path11 from "path";
1755
+ import path12 from "path";
1688
1756
  function detectQoderCnVariant(cwd) {
1689
1757
  if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
1690
1758
  return true;
1691
1759
  }
1692
- return existsSync2(path11.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
1760
+ return existsSync2(path12.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
1693
1761
  }
1694
1762
  function resolvePlatformMeta(cwd, platform) {
1695
1763
  const meta = PLATFORM_META[platform];
@@ -1720,7 +1788,7 @@ function resolveQoderMetaForBaseDir(baseDir) {
1720
1788
 
1721
1789
  // src/configurators/qoder.ts
1722
1790
  async function claudeHarnessSkillsExist(cwd) {
1723
- return pathExists(path12.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
1791
+ return pathExists(path13.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
1724
1792
  }
1725
1793
  async function listFilesRecursive(dir) {
1726
1794
  let entries;
@@ -1734,7 +1802,7 @@ async function listFilesRecursive(dir) {
1734
1802
  }
1735
1803
  const files = [];
1736
1804
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1737
- const entryPath = path12.join(dir, entry.name);
1805
+ const entryPath = path13.join(dir, entry.name);
1738
1806
  if (entry.isDirectory()) {
1739
1807
  files.push(...await listFilesRecursive(entryPath));
1740
1808
  continue;
@@ -1748,7 +1816,7 @@ async function listFilesRecursive(dir) {
1748
1816
  async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
1749
1817
  const artifacts = [];
1750
1818
  for (const skillName of skillNames) {
1751
- for (const filePath of await listFilesRecursive(path12.join(skillsDir, skillName))) {
1819
+ for (const filePath of await listFilesRecursive(path13.join(skillsDir, skillName))) {
1752
1820
  artifacts.push(fileArtifact(filePath, "skill", platform));
1753
1821
  }
1754
1822
  }
@@ -1758,22 +1826,22 @@ async function configureQoder(cwd, opts = {}) {
1758
1826
  const platform = "qoder";
1759
1827
  const meta = resolvePlatformMeta(cwd, platform);
1760
1828
  const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
1761
- const dest = path12.join(cwd, ctx.platform_config_dir);
1762
- 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);
1763
1831
  const artifacts = [];
1764
1832
  const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
1765
1833
  artifacts.push(
1766
1834
  ...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
1767
1835
  (filePath) => fileArtifact(
1768
1836
  filePath,
1769
- filePath.startsWith(path12.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1837
+ filePath.startsWith(path13.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
1770
1838
  platform
1771
1839
  )
1772
1840
  )
1773
1841
  );
1774
1842
  artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
1775
1843
  artifacts.push(
1776
- ...(await writeSharedHooks(path12.join(dest, "hooks"), platform)).map(
1844
+ ...(await writeSharedHooks(path13.join(dest, "hooks"), platform)).map(
1777
1845
  (filePath) => fileArtifact(filePath, "hook", platform)
1778
1846
  )
1779
1847
  );
@@ -1781,14 +1849,14 @@ async function configureQoder(cwd, opts = {}) {
1781
1849
  const bundledSkills = await resolveBundledSkills(ctx);
1782
1850
  if (!await claudeHarnessSkillsExist(cwd)) {
1783
1851
  artifacts.push(
1784
- ...(await writeSkills(path12.join(dest, "skills"), skills, bundledSkills)).map(
1852
+ ...(await writeSkills(path13.join(dest, "skills"), skills, bundledSkills)).map(
1785
1853
  (filePath) => fileArtifact(filePath, "skill", platform)
1786
1854
  )
1787
1855
  );
1788
1856
  } else {
1789
1857
  artifacts.push(
1790
1858
  ...await existingManagedSkillArtifacts(
1791
- path12.join(dest, "skills"),
1859
+ path13.join(dest, "skills"),
1792
1860
  [...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
1793
1861
  platform
1794
1862
  )
@@ -1859,7 +1927,7 @@ async function refreshSupermoduleParent(targetDir, platforms, submodulePaths) {
1859
1927
  role: "super-parent",
1860
1928
  submodules: submodulePaths
1861
1929
  };
1862
- await updateSupermoduleConfig(path13.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
1930
+ await updateSupermoduleConfig(path14.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
1863
1931
  for (const platform of platforms) {
1864
1932
  await writeMainConstraint(targetDir, platform, {
1865
1933
  supermodule: { submodulePaths }
@@ -1981,14 +2049,14 @@ async function resolveSubmodules(opts, available, defaultSelection = available)
1981
2049
  }
1982
2050
 
1983
2051
  // src/commands/supermodule-targets.ts
1984
- import path15 from "path";
2052
+ import path16 from "path";
1985
2053
  import { cancel as cancel2, multiselect as multiselect2 } from "@clack/prompts";
1986
2054
 
1987
2055
  // src/utils/gitmodules.ts
1988
2056
  import { lstat, realpath } from "fs/promises";
1989
- import path14 from "path";
2057
+ import path15 from "path";
1990
2058
  async function parseGitmodules(rootDir) {
1991
- const content = await readTextIfExists(path14.join(rootDir, ".gitmodules"));
2059
+ const content = await readTextIfExists(path15.join(rootDir, ".gitmodules"));
1992
2060
  if (content === null) {
1993
2061
  return [];
1994
2062
  }
@@ -2047,7 +2115,7 @@ async function listInstallableSubmodules(rootDir) {
2047
2115
  function normalizeSubmodulePath(value) {
2048
2116
  const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
2049
2117
  const parts = normalized.split("/").filter(Boolean);
2050
- 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 === "..")) {
2051
2119
  throw new Error(`Unsafe submodule path in .gitmodules: ${value}`);
2052
2120
  }
2053
2121
  return parts.join("/");
@@ -2085,7 +2153,7 @@ function isCommentStart(value, index) {
2085
2153
  return index === 0 || /\s/.test(value[index - 1]);
2086
2154
  }
2087
2155
  async function isSubmoduleWorktree(rootDir, submodulePath) {
2088
- const dir = path14.join(rootDir, submodulePath);
2156
+ const dir = path15.join(rootDir, submodulePath);
2089
2157
  try {
2090
2158
  if (!await pathHasNoSymlinkSegments(rootDir, submodulePath)) {
2091
2159
  return false;
@@ -2099,7 +2167,7 @@ async function isSubmoduleWorktree(rootDir, submodulePath) {
2099
2167
  if (!dirStat.isDirectory()) {
2100
2168
  return false;
2101
2169
  }
2102
- const gitMarkerStat = await lstat(path14.join(dir, ".git"));
2170
+ const gitMarkerStat = await lstat(path15.join(dir, ".git"));
2103
2171
  return !gitMarkerStat.isSymbolicLink() && (gitMarkerStat.isFile() || gitMarkerStat.isDirectory());
2104
2172
  } catch (error) {
2105
2173
  if (error.code === "ENOENT") {
@@ -2111,7 +2179,7 @@ async function isSubmoduleWorktree(rootDir, submodulePath) {
2111
2179
  async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
2112
2180
  let current = rootDir;
2113
2181
  for (const part of submodulePath.split("/")) {
2114
- current = path14.join(current, part);
2182
+ current = path15.join(current, part);
2115
2183
  const partStat = await lstat(current);
2116
2184
  if (partStat.isSymbolicLink()) {
2117
2185
  return false;
@@ -2120,8 +2188,8 @@ async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
2120
2188
  return true;
2121
2189
  }
2122
2190
  function isInsideDirectory(parent, child) {
2123
- const relative = path14.relative(parent, child);
2124
- return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
2191
+ const relative = path15.relative(parent, child);
2192
+ return Boolean(relative) && !relative.startsWith("..") && !path15.isAbsolute(relative);
2125
2193
  }
2126
2194
 
2127
2195
  // src/commands/supermodule-targets.ts
@@ -2173,7 +2241,7 @@ async function resolveClearTargets(cwd, opts) {
2173
2241
  return [standaloneTarget(cwd)];
2174
2242
  }
2175
2243
  const installedSubmodules = await listInstalledSubmodules(cwd);
2176
- 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;
2177
2245
  const children = installedSubmodules.map((entry) => childTarget(cwd, entry));
2178
2246
  if (opts.submodules === false) {
2179
2247
  return parent ? [parent] : [];
@@ -2213,7 +2281,7 @@ async function listInstalledSubmodules(cwd) {
2213
2281
  const entries = await listInstallableSubmodules(cwd);
2214
2282
  const installed = [];
2215
2283
  for (const entry of entries) {
2216
- if (await pathExists(configPath(path15.join(cwd, entry.path)))) {
2284
+ if (await pathExists(configPath(path16.join(cwd, entry.path)))) {
2217
2285
  installed.push(entry);
2218
2286
  }
2219
2287
  }
@@ -2256,7 +2324,7 @@ function parentTargetFromPaths(cwd, submodulePaths) {
2256
2324
  };
2257
2325
  }
2258
2326
  function childTarget(cwd, entry) {
2259
- const dir = path15.join(cwd, entry.path);
2327
+ const dir = path16.join(cwd, entry.path);
2260
2328
  return {
2261
2329
  dir,
2262
2330
  label: entry.path,
@@ -2292,11 +2360,11 @@ function parseSubmoduleSelection(submoduleList, available) {
2292
2360
  return selected.sort((a, b) => a.path.localeCompare(b.path));
2293
2361
  }
2294
2362
  function configPath(cwd) {
2295
- return path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2363
+ return path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2296
2364
  }
2297
2365
  function toPosixRelative(from, to) {
2298
- const relative = path15.relative(from, to);
2299
- return relative ? relative.split(path15.sep).join("/") : ".";
2366
+ const relative = path16.relative(from, to);
2367
+ return relative ? relative.split(path16.sep).join("/") : ".";
2300
2368
  }
2301
2369
 
2302
2370
  // src/commands/add-agent.ts
@@ -2372,7 +2440,7 @@ ${installedLabels.join("\n")}`));
2372
2440
 
2373
2441
  // src/commands/clear.ts
2374
2442
  import { readdir as readdir4, rm, writeFile } from "fs/promises";
2375
- import path16 from "path";
2443
+ import path17 from "path";
2376
2444
  import { cancel as cancel3, confirm as confirm2, outro as outro2 } from "@clack/prompts";
2377
2445
  import chalk3 from "chalk";
2378
2446
  var PLATFORM_TEMPLATE_DIR = {
@@ -2411,7 +2479,7 @@ async function clear(opts) {
2411
2479
  async function buildTargetClearPlans(targets) {
2412
2480
  const plans = [];
2413
2481
  for (const target of targets) {
2414
- const easyCodingDir = path16.join(target.dir, EASY_CODING_DIR);
2482
+ const easyCodingDir = path17.join(target.dir, EASY_CODING_DIR);
2415
2483
  if (!await pathExists(easyCodingDir)) {
2416
2484
  continue;
2417
2485
  }
@@ -2428,7 +2496,7 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
2428
2496
  if (targetPlans.some((targetPlan) => targetPlan.target.label === ".")) {
2429
2497
  return;
2430
2498
  }
2431
- if (!await pathExists(path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
2499
+ if (!await pathExists(path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
2432
2500
  return;
2433
2501
  }
2434
2502
  const [parent] = await resolveUpgradeTargets(cwd);
@@ -2442,7 +2510,7 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
2442
2510
  await refreshSupermoduleParent(cwd, config2.agents, parent.supermodule.submodules ?? []);
2443
2511
  }
2444
2512
  async function resolveInstalledAgents(cwd) {
2445
- const configPath2 = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2513
+ const configPath2 = path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2446
2514
  if (await pathExists(configPath2)) {
2447
2515
  try {
2448
2516
  const config2 = await readConfigYaml(configPath2);
@@ -2473,7 +2541,7 @@ async function hasPlatformInstall(cwd, platform) {
2473
2541
  async function hasInstallMarkers(cwd, meta) {
2474
2542
  const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
2475
2543
  for (const marker of markers) {
2476
- if (await pathExists(path16.join(cwd, marker))) {
2544
+ if (await pathExists(path17.join(cwd, marker))) {
2477
2545
  return true;
2478
2546
  }
2479
2547
  }
@@ -2529,7 +2597,7 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
2529
2597
  }
2530
2598
  if (await manifestFileMatches(filePath, file.sha256)) {
2531
2599
  plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
2532
- addEmptyDirChain(plan.emptyDirs, path16.dirname(filePath), cwd);
2600
+ addEmptyDirChain(plan.emptyDirs, path17.dirname(filePath), cwd);
2533
2601
  } else {
2534
2602
  plan.skippedModified.push(filePath);
2535
2603
  }
@@ -2579,28 +2647,28 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
2579
2647
  );
2580
2648
  for (const meta of metas) {
2581
2649
  for (const name of managedSkills) {
2582
- plan.remove.add(path16.join(cwd, meta.skillsDir, name));
2650
+ plan.remove.add(path17.join(cwd, meta.skillsDir, name));
2583
2651
  }
2584
2652
  for (const name of hookFileNames) {
2585
- plan.remove.add(path16.join(cwd, meta.hooksDir, name));
2653
+ plan.remove.add(path17.join(cwd, meta.hooksDir, name));
2586
2654
  }
2587
2655
  for (const name of agentFileNames) {
2588
- plan.remove.add(path16.join(cwd, meta.agentsDir, name));
2656
+ plan.remove.add(path17.join(cwd, meta.agentsDir, name));
2589
2657
  }
2590
2658
  addHookConfigPrune(
2591
2659
  plan.pruneHookConfigs,
2592
2660
  plan.pruneHookCommands,
2593
- path16.join(cwd, meta.hookConfigFile),
2661
+ path17.join(cwd, meta.hookConfigFile),
2594
2662
  managedHookPathsForTemplate(cwd, meta, hookFileNames),
2595
2663
  []
2596
2664
  );
2597
- plan.constraints.add(path16.join(cwd, meta.mainConstraint));
2665
+ plan.constraints.add(path17.join(cwd, meta.mainConstraint));
2598
2666
  if (platform === "codex") {
2599
- 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"));
2600
2668
  }
2601
- plan.emptyDirs.add(path16.join(cwd, meta.skillsDir));
2602
- plan.emptyDirs.add(path16.join(cwd, meta.hooksDir));
2603
- 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));
2604
2672
  }
2605
2673
  }
2606
2674
  async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
@@ -2647,7 +2715,7 @@ function managedHookPathsForRegistration(cwd, hookPath) {
2647
2715
  try {
2648
2716
  paths.push(...managedHookPathTokens(manifestPath(cwd, hookPath)));
2649
2717
  } catch {
2650
- if (path16.isAbsolute(hookPath)) {
2718
+ if (path17.isAbsolute(hookPath)) {
2651
2719
  paths.push(...managedHookPathTokens(hookPath));
2652
2720
  }
2653
2721
  }
@@ -2658,7 +2726,7 @@ function managedHookPathsForTemplate(cwd, meta, hookFileNames) {
2658
2726
  (name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`
2659
2727
  );
2660
2728
  const absolutePaths = hookFileNames.flatMap(
2661
- (name) => managedHookPathTokens(path16.join(cwd, meta.hooksDir, name))
2729
+ (name) => managedHookPathTokens(path17.join(cwd, meta.hooksDir, name))
2662
2730
  );
2663
2731
  return [...relativePaths, ...absolutePaths];
2664
2732
  }
@@ -2666,8 +2734,8 @@ function managedHookPathTokens(filePath) {
2666
2734
  const tokens = /* @__PURE__ */ new Set();
2667
2735
  for (const equivalentPath of equivalentAbsolutePaths(filePath)) {
2668
2736
  const normalized = equivalentPath.replace(/\\/g, "/");
2669
- const dir = path16.posix.dirname(normalized);
2670
- const basename = path16.posix.basename(normalized);
2737
+ const dir = path17.posix.dirname(normalized);
2738
+ const basename = path17.posix.basename(normalized);
2671
2739
  tokens.add(normalized);
2672
2740
  tokens.add(shellDoubleQuoteArg2(normalized));
2673
2741
  tokens.add(`${shellDoubleQuoteArg2(dir)}/${basename}`);
@@ -2675,7 +2743,7 @@ function managedHookPathTokens(filePath) {
2675
2743
  return [...tokens];
2676
2744
  }
2677
2745
  function equivalentAbsolutePaths(filePath) {
2678
- const normalized = path16.resolve(filePath).replace(/\\/g, "/");
2746
+ const normalized = path17.resolve(filePath).replace(/\\/g, "/");
2679
2747
  const equivalents = [normalized];
2680
2748
  if (normalized.startsWith("/private/var/")) {
2681
2749
  equivalents.push(normalized.replace(/^\/private\/var\//, "/var/"));
@@ -2749,11 +2817,11 @@ function addRuntimeClearEntries(plan, cwd) {
2749
2817
  plan.remove = [
2750
2818
  .../* @__PURE__ */ new Set([
2751
2819
  ...plan.remove,
2752
- path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
2753
- path16.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
2754
- path16.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
2755
- path16.join(cwd, EASY_CODING_DIR, TOOLS_DIR),
2756
- 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)
2757
2825
  ])
2758
2826
  ];
2759
2827
  }
@@ -2761,7 +2829,7 @@ function addEmptyDirChain(emptyDirs, startDir, cwd) {
2761
2829
  let current = startDir;
2762
2830
  while (current !== cwd && isInsideDirectory2(cwd, current)) {
2763
2831
  emptyDirs.add(current);
2764
- const parent = path16.dirname(current);
2832
+ const parent = path17.dirname(current);
2765
2833
  if (parent === current) {
2766
2834
  break;
2767
2835
  }
@@ -2769,8 +2837,8 @@ function addEmptyDirChain(emptyDirs, startDir, cwd) {
2769
2837
  }
2770
2838
  }
2771
2839
  function isInsideDirectory2(parent, child) {
2772
- const relative = path16.relative(parent, child);
2773
- return Boolean(relative) && !relative.startsWith("..") && !path16.isAbsolute(relative);
2840
+ const relative = path17.relative(parent, child);
2841
+ return Boolean(relative) && !relative.startsWith("..") && !path17.isAbsolute(relative);
2774
2842
  }
2775
2843
  async function listSharedHookNamesForPlatform(platform) {
2776
2844
  const names = await listFileNames(getTemplatePath("shared-hooks"));
@@ -2907,7 +2975,7 @@ function sortDirsDeepestFirst(dirs) {
2907
2975
  });
2908
2976
  }
2909
2977
  function pathDepth(dir) {
2910
- return path16.normalize(dir).split(path16.sep).filter(Boolean).length;
2978
+ return path17.normalize(dir).split(path17.sep).filter(Boolean).length;
2911
2979
  }
2912
2980
  async function listDirNames(dir) {
2913
2981
  try {
@@ -2926,7 +2994,7 @@ async function listFileNames(dir) {
2926
2994
  }
2927
2995
  }
2928
2996
  function renderPlan(cwd, agents, plan) {
2929
- const rel = (target) => path16.relative(cwd, target) || target;
2997
+ const rel = (target) => path17.relative(cwd, target) || target;
2930
2998
  const lines = [];
2931
2999
  lines.push(chalk3.bold("easy-coding clear"));
2932
3000
  lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
@@ -2975,12 +3043,12 @@ function renderTargetPlans(targetPlans) {
2975
3043
  }
2976
3044
 
2977
3045
  // src/commands/config.ts
2978
- import path19 from "path";
3046
+ import path20 from "path";
2979
3047
  import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
2980
3048
  import chalk4 from "chalk";
2981
3049
 
2982
3050
  // src/utils/compare-versions.ts
2983
- import path17 from "path";
3051
+ import path18 from "path";
2984
3052
  function parseVersion(version) {
2985
3053
  const withoutBuild = String(version ?? "").split("+", 1)[0];
2986
3054
  const separator = withoutBuild.indexOf("-");
@@ -3035,7 +3103,7 @@ function isVersionBehind(installed, current = VERSION) {
3035
3103
  return compareVersions(installed, current) === -1;
3036
3104
  }
3037
3105
  async function checkForUpgrade(cwd) {
3038
- const configPath2 = path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3106
+ const configPath2 = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3039
3107
  if (!await pathExists(configPath2)) {
3040
3108
  return;
3041
3109
  }
@@ -3056,7 +3124,7 @@ async function checkForUpgrade(cwd) {
3056
3124
 
3057
3125
  // src/utils/tdd-readiness.ts
3058
3126
  import { readFile as readFile4, realpath as realpath2 } from "fs/promises";
3059
- import path18 from "path";
3127
+ import path19 from "path";
3060
3128
  var TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1";
3061
3129
  var TDD_READINESS_SCOPE = "changed-production-lines";
3062
3130
  var TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA";
@@ -3064,7 +3132,7 @@ var TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD";
3064
3132
  var COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py";
3065
3133
  var JAVA_BUILD_FILE_NAMES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
3066
3134
  function readinessPath(root) {
3067
- 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);
3068
3136
  }
3069
3137
  function parseFileRecords(value, field, reasons) {
3070
3138
  if (!Array.isArray(value) || value.length === 0) {
@@ -3078,7 +3146,7 @@ function parseFileRecords(value, field, reasons) {
3078
3146
  continue;
3079
3147
  }
3080
3148
  const record = item;
3081
- 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)) {
3082
3150
  reasons.push(`${field} contains an invalid path`);
3083
3151
  continue;
3084
3152
  }
@@ -3092,15 +3160,15 @@ function usesRequiredGateVariables(command) {
3092
3160
  }
3093
3161
  function isSafeReportPattern(value) {
3094
3162
  const normalized = value.replaceAll("\\", "/");
3095
- return !path18.isAbsolute(value) && !normalized.split("/").includes("..");
3163
+ return !path19.isAbsolute(value) && !normalized.split("/").includes("..");
3096
3164
  }
3097
3165
  async function validateFiles(root, records, reasons) {
3098
3166
  const resolvedRoot = await realpath2(root);
3099
3167
  for (const record of records) {
3100
- const absolute = path18.resolve(root, record.path);
3168
+ const absolute = path19.resolve(root, record.path);
3101
3169
  try {
3102
3170
  const resolved = await realpath2(absolute);
3103
- if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path18.sep}`)) {
3171
+ if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path19.sep}`)) {
3104
3172
  reasons.push(`readiness file escapes project root: ${record.path}`);
3105
3173
  continue;
3106
3174
  }
@@ -3149,7 +3217,7 @@ async function inspectTddReadiness(root) {
3149
3217
  }
3150
3218
  const buildFiles = parseFileRecords(manifest.build_files, "build_files", reasons);
3151
3219
  const toolFiles = parseFileRecords(manifest.tool_files, "tool_files", reasons);
3152
- 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)))) {
3153
3221
  reasons.push("build_files must include a Maven or Gradle Java build file");
3154
3222
  }
3155
3223
  if (!toolFiles.some((record) => record.path.replaceAll("\\", "/") === COVERAGE_TOOL_PATH)) {
@@ -3165,9 +3233,17 @@ async function inspectTddReadiness(root) {
3165
3233
  }
3166
3234
 
3167
3235
  // src/commands/config.ts
3168
- async function config() {
3236
+ async function config(options = {}) {
3169
3237
  renderBanner();
3170
- 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);
3171
3247
  if (!await pathExists(configPath2)) {
3172
3248
  throw new Error("No easy-coding harness found in this project.");
3173
3249
  }
@@ -3188,6 +3264,10 @@ async function config() {
3188
3264
  `Project harness ${projectConfig.harness_version} does not exactly match CLI ${VERSION}. Upgrade the harness or update the CLI before changing config.`
3189
3265
  );
3190
3266
  }
3267
+ if (explicit) {
3268
+ await configureOverrides(configPath2, scope, options, true);
3269
+ return;
3270
+ }
3191
3271
  const current = resolveLegacyBehavior(projectConfig);
3192
3272
  const approvalMode = await select({
3193
3273
  message: `Select project approval mode (current: ${current.approvalMode})`,
@@ -3258,8 +3338,23 @@ async function config() {
3258
3338
  }
3259
3339
  utCoverageThreshold = Number(thresholdInput);
3260
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;
3355
+ }
3261
3356
  const shouldSave = await confirm3({
3262
- message: `Set approval=${approvalMode}, workflow=${workflowMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}?`,
3357
+ message: `Set approval=${approvalMode}, cooperate=${cooperateMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}?`,
3263
3358
  initialValue: true
3264
3359
  });
3265
3360
  if (typeof shouldSave === "symbol" || !shouldSave) {
@@ -3275,29 +3370,116 @@ async function config() {
3275
3370
  return;
3276
3371
  }
3277
3372
  }
3278
- await setBehaviorModes(configPath2, approvalMode, workflowMode, unitTestMode, utCoverageThreshold);
3373
+ await setBehaviorModes(
3374
+ configPath2,
3375
+ approvalMode,
3376
+ workflowMode,
3377
+ unitTestMode,
3378
+ utCoverageThreshold,
3379
+ cooperateMode
3380
+ );
3279
3381
  outro3(
3280
3382
  chalk4.green(
3281
3383
  `Project modes updated: approval=${approvalMode}, workflow=${workflowMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}.`
3282
3384
  )
3283
3385
  );
3284
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
+ }
3285
3467
 
3286
3468
  // src/commands/init.ts
3287
- import path21 from "path";
3469
+ import path22 from "path";
3288
3470
  import { note, outro as outro4 } from "@clack/prompts";
3289
3471
  import chalk5 from "chalk";
3290
3472
 
3291
3473
  // src/utils/install-state.ts
3292
3474
  import { readdir as readdir5 } from "fs/promises";
3293
- import path20 from "path";
3475
+ import path21 from "path";
3294
3476
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
3295
3477
  async function detectEasyCodingInstallState(cwd) {
3296
- const easyCodingDir = path20.join(cwd, EASY_CODING_DIR);
3478
+ const easyCodingDir = path21.join(cwd, EASY_CODING_DIR);
3297
3479
  if (!await pathExists(easyCodingDir)) {
3298
3480
  return { kind: "fresh", easyCodingDir };
3299
3481
  }
3300
- const configPath2 = path20.join(easyCodingDir, CONFIG_FILE);
3482
+ const configPath2 = path21.join(easyCodingDir, CONFIG_FILE);
3301
3483
  if (await pathExists(configPath2)) {
3302
3484
  return { kind: "installed", easyCodingDir, configPath: configPath2 };
3303
3485
  }
@@ -3315,17 +3497,17 @@ async function detectEasyCodingInstallState(cwd) {
3315
3497
  async function detectLegacyAssets(easyCodingDir) {
3316
3498
  const assets = [];
3317
3499
  for (const file of LEGACY_ROOT_FILES) {
3318
- if (await pathExists(path20.join(easyCodingDir, file))) {
3500
+ if (await pathExists(path21.join(easyCodingDir, file))) {
3319
3501
  assets.push(relativeEasyCodingPath(file));
3320
3502
  }
3321
3503
  }
3322
- if (await pathExists(path20.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3504
+ if (await pathExists(path21.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3323
3505
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
3324
3506
  }
3325
- const shortMemoryFiles = await listMarkdownFiles(path20.join(easyCodingDir, "memory", "short"));
3507
+ const shortMemoryFiles = await listMarkdownFiles(path21.join(easyCodingDir, "memory", "short"));
3326
3508
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
3327
3509
  for (const dir of ["spec", "prototype"]) {
3328
- if (await hasAnyDirectoryEntry(path20.join(easyCodingDir, dir))) {
3510
+ if (await hasAnyDirectoryEntry(path21.join(easyCodingDir, dir))) {
3329
3511
  assets.push(relativeEasyCodingPath(dir));
3330
3512
  }
3331
3513
  }
@@ -3345,13 +3527,13 @@ async function hasAnyDirectoryEntry(dir) {
3345
3527
  return (await readdir5(dir)).length > 0;
3346
3528
  }
3347
3529
  function relativeEasyCodingPath(...segments) {
3348
- return path20.posix.join(EASY_CODING_DIR, ...segments);
3530
+ return path21.posix.join(EASY_CODING_DIR, ...segments);
3349
3531
  }
3350
3532
  function relativeConfigPath() {
3351
- return path20.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3533
+ return path21.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3352
3534
  }
3353
3535
  function relativeProjectInitTaskPath() {
3354
- 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");
3355
3537
  }
3356
3538
 
3357
3539
  // src/commands/init.ts
@@ -3432,7 +3614,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
3432
3614
  if (!targetSubmodulePaths.has(entry.path)) {
3433
3615
  continue;
3434
3616
  }
3435
- const dir = path21.join(cwd, entry.path);
3617
+ const dir = path22.join(cwd, entry.path);
3436
3618
  targets.push(
3437
3619
  await targetFromState(dir, entry.path, "submodule-child", {
3438
3620
  parent: toPosixRelative2(dir, cwd)
@@ -3479,24 +3661,24 @@ function contextFromState(role, installState) {
3479
3661
  };
3480
3662
  }
3481
3663
  function toPosixRelative2(from, to) {
3482
- const relative = path21.relative(from, to);
3483
- return relative ? relative.split(path21.sep).join("/") : ".";
3664
+ const relative = path22.relative(from, to);
3665
+ return relative ? relative.split(path22.sep).join("/") : ".";
3484
3666
  }
3485
3667
  async function resolveInitPlatforms(cwd, opts, parentInstalled) {
3486
3668
  if (opts.agent || !parentInstalled) {
3487
3669
  return resolvePlatforms(opts, ["claude-code"]);
3488
3670
  }
3489
- 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));
3490
3672
  if (Array.isArray(config2.agents) && config2.agents.length > 0) {
3491
3673
  return config2.agents;
3492
3674
  }
3493
3675
  return resolvePlatforms(opts, ["claude-code"]);
3494
3676
  }
3495
3677
  async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
3496
- if (!await pathExists(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3678
+ if (!await pathExists(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3497
3679
  return;
3498
3680
  }
3499
- 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 };
3500
3682
  const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
3501
3683
  await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
3502
3684
  }
@@ -3505,7 +3687,7 @@ async function refreshInstalledChildTopologies(targets) {
3505
3687
  if (!target.installed || target.context.role !== "submodule-child") {
3506
3688
  continue;
3507
3689
  }
3508
- const configPath2 = path21.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3690
+ const configPath2 = path22.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3509
3691
  if (!await pathExists(configPath2)) {
3510
3692
  continue;
3511
3693
  }
@@ -3514,12 +3696,12 @@ async function refreshInstalledChildTopologies(targets) {
3514
3696
  }
3515
3697
 
3516
3698
  // src/commands/status.ts
3517
- import path23 from "path";
3699
+ import path24 from "path";
3518
3700
  import chalk6 from "chalk";
3519
3701
 
3520
3702
  // src/utils/session.ts
3521
3703
  import { readdir as readdir6, stat as stat3, unlink as unlink2 } from "fs/promises";
3522
- import path22 from "path";
3704
+ import path23 from "path";
3523
3705
  var DAY_MS = 24 * 60 * 60 * 1e3;
3524
3706
  var IDLE_SESSION_RETENTION_MS = 7 * DAY_MS;
3525
3707
  var ATTACHED_SESSION_RETENTION_MS = 30 * DAY_MS;
@@ -3533,7 +3715,7 @@ function parseSessionFile(content) {
3533
3715
  }
3534
3716
  }
3535
3717
  function getSessionDir(cwd) {
3536
- return path22.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3718
+ return path23.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3537
3719
  }
3538
3720
  async function listSessionFiles(cwd) {
3539
3721
  const dir = getSessionDir(cwd);
@@ -3545,7 +3727,7 @@ async function listSessionFiles(cwd) {
3545
3727
  if (!name.endsWith(".json")) {
3546
3728
  continue;
3547
3729
  }
3548
- const filePath = path22.join(dir, name);
3730
+ const filePath = path23.join(dir, name);
3549
3731
  const content = await readTextIfExists(filePath);
3550
3732
  if (!content) {
3551
3733
  continue;
@@ -3610,7 +3792,7 @@ async function listSessionCleanupCandidates(cwd) {
3610
3792
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
3611
3793
  continue;
3612
3794
  }
3613
- const filePath = path22.join(dir, entry.name);
3795
+ const filePath = path23.join(dir, entry.name);
3614
3796
  try {
3615
3797
  const [content, fileStat] = await Promise.all([readTextFile(filePath), stat3(filePath)]);
3616
3798
  const session = parseSessionFile(content);
@@ -3645,7 +3827,7 @@ async function unlinkIfUnchanged(candidate) {
3645
3827
  }
3646
3828
  }
3647
3829
  async function cleanOrphanAcceptanceSnapshots(cwd) {
3648
- const acceptanceDir = path22.join(getSessionDir(cwd), "acceptance");
3830
+ const acceptanceDir = path23.join(getSessionDir(cwd), "acceptance");
3649
3831
  if (!await pathExists(acceptanceDir)) {
3650
3832
  return 0;
3651
3833
  }
@@ -3654,7 +3836,7 @@ async function cleanOrphanAcceptanceSnapshots(cwd) {
3654
3836
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
3655
3837
  continue;
3656
3838
  }
3657
- const snapshotPath = path22.join(acceptanceDir, entry.name);
3839
+ const snapshotPath = path23.join(acceptanceDir, entry.name);
3658
3840
  if (!await isOrphanAcceptanceSnapshot(cwd, snapshotPath, entry.name.slice(0, -5))) {
3659
3841
  continue;
3660
3842
  }
@@ -3673,7 +3855,7 @@ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
3673
3855
  let taskContent;
3674
3856
  try {
3675
3857
  taskContent = await readTextFile(
3676
- path22.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json")
3858
+ path23.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json")
3677
3859
  );
3678
3860
  } catch (error) {
3679
3861
  if (isFileNotFound(error)) {
@@ -3695,7 +3877,7 @@ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
3695
3877
  return true;
3696
3878
  }
3697
3879
  const checkpoint = task.quality_checkpoint ?? task.verification_checkpoint;
3698
- 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);
3699
3881
  }
3700
3882
  function isFileNotFound(error) {
3701
3883
  return error.code === "ENOENT";
@@ -3705,7 +3887,7 @@ function isFileNotFound(error) {
3705
3887
  async function status() {
3706
3888
  renderBanner();
3707
3889
  const cwd = process.cwd();
3708
- const configPath2 = path23.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3890
+ const configPath2 = path24.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3709
3891
  if (!await pathExists(configPath2)) {
3710
3892
  throw new Error("No easy-coding harness found in this project.");
3711
3893
  }
@@ -3732,9 +3914,20 @@ async function status() {
3732
3914
  const projectWorkflowMode = isConfiguredWorkflowMode(config2.behavior?.workflow_mode) ? config2.behavior.workflow_mode : migratedBehavior.workflowMode;
3733
3915
  const projectUnitTestMode = migratedBehavior.unitTestMode;
3734
3916
  const projectUtCoverageThreshold = migratedBehavior.utCoverageThreshold;
3735
- const needsCoverage = projectUnitTestMode !== "none" || sessions.some(({ session }) => ["ut", "tdd"].includes(session.unit_test_mode ?? "none")) || activeTasks.some(({ task }) => ["ut", "tdd"].includes(task.unit_test_mode ?? "none"));
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"));
3736
3926
  const readiness = needsCoverage ? await inspectTddReadiness(cwd) : { status: "not_checked", reasons: [] };
3737
3927
  console.log(` approval_mode: ${projectApprovalMode}`);
3928
+ console.log(
3929
+ ` cooperate_mode: ${effective.values.cooperate_mode} (${effective.sources.cooperate_mode})`
3930
+ );
3738
3931
  console.log(` workflow_mode: ${projectWorkflowMode}`);
3739
3932
  console.log(` unit_test_mode: ${projectUnitTestMode}`);
3740
3933
  console.log(` ut_coverage_threshold: ${projectUtCoverageThreshold}`);
@@ -3748,11 +3941,16 @@ async function status() {
3748
3941
  console.log(` project_workflow_mode: ${projectWorkflowMode}`);
3749
3942
  console.log(` project_unit_test_mode: ${projectUnitTestMode}`);
3750
3943
  console.log(` project_ut_coverage_threshold: ${projectUtCoverageThreshold}`);
3751
- console.log(` effective_approval_mode: ${projectApprovalMode} (without a session override)`);
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
+ );
3752
3948
  console.log(` configured_workflow_mode: ${projectWorkflowMode} (without a session override)`);
3753
- console.log(` effective_unit_test_mode: ${projectUnitTestMode} (without a session override)`);
3754
3949
  console.log(
3755
- ` effective_ut_coverage_threshold: ${projectUtCoverageThreshold} (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})`
3756
3954
  );
3757
3955
  if (sessions.length === 0) {
3758
3956
  console.log(" no session files");
@@ -3766,19 +3964,26 @@ async function status() {
3766
3964
  const sessionWorkflowMode = session.workflow_mode ?? (legacySessionMode === "lite" ? "fast" : hasLegacySessionMode ? "adaptive" : void 0);
3767
3965
  const sessionUnitTestMode = session.unit_test_mode;
3768
3966
  const sessionUtCoverageThreshold = session.ut_coverage_threshold;
3967
+ const resolved = resolveBehaviorSettings(projectBehavior, localBehavior, {
3968
+ ...session,
3969
+ approval_mode: sessionApprovalMode
3970
+ });
3769
3971
  console.log(` - ${key}`);
3770
3972
  console.log(` agent: ${session.agent ?? "legacy/unknown"}`);
3771
3973
  console.log(` source: ${session.session_source ?? "legacy"}`);
3772
- console.log(` approval_mode: ${sessionApprovalMode ?? "project default"}`);
3974
+ console.log(` approval_mode: ${sessionApprovalMode ?? "inherit local/project"}`);
3773
3975
  console.log(` workflow_mode: ${sessionWorkflowMode ?? "project default"}`);
3774
- console.log(` unit_test_mode: ${sessionUnitTestMode ?? "project default"}`);
3775
- console.log(` ut_coverage_threshold: ${sessionUtCoverageThreshold ?? "project default"}`);
3776
- console.log(` effective_approval_mode: ${sessionApprovalMode ?? projectApprovalMode}`);
3777
- console.log(` configured_workflow_mode: ${sessionWorkflowMode ?? projectWorkflowMode}`);
3778
- console.log(` effective_unit_test_mode: ${sessionUnitTestMode ?? projectUnitTestMode}`);
3976
+ console.log(` unit_test_mode: ${sessionUnitTestMode ?? "inherit local/project"}`);
3779
3977
  console.log(
3780
- ` effective_ut_coverage_threshold: ${sessionUtCoverageThreshold ?? projectUtCoverageThreshold}`
3978
+ ` ut_coverage_threshold: ${sessionUtCoverageThreshold ?? "inherit local/project"}`
3781
3979
  );
3980
+ console.log(` effective_approval_mode: ${resolved.values.approval_mode}`);
3981
+ console.log(
3982
+ ` cooperate_mode: ${resolved.values.cooperate_mode} (${resolved.sources.cooperate_mode})`
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}`);
3782
3987
  console.log(
3783
3988
  ` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
3784
3989
  );
@@ -3851,7 +4056,7 @@ async function update(opts) {
3851
4056
  }
3852
4057
 
3853
4058
  // src/commands/upgrade.ts
3854
- import path24 from "path";
4059
+ import path25 from "path";
3855
4060
  import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
3856
4061
  import chalk8 from "chalk";
3857
4062
  var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
@@ -4025,7 +4230,7 @@ async function needsHookConfigRefresh(target, config2) {
4025
4230
  const manifest = await readInstallManifest(target.dir);
4026
4231
  for (const agent of config2.agents) {
4027
4232
  const meta = resolvePlatformMeta(target.dir, agent);
4028
- const configPath2 = path24.join(target.dir, meta.hookConfigFile);
4233
+ const configPath2 = path25.join(target.dir, meta.hookConfigFile);
4029
4234
  const content = await readTextIfExists(configPath2);
4030
4235
  if (content === null) {
4031
4236
  return true;
@@ -4165,7 +4370,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
4165
4370
  return true;
4166
4371
  }
4167
4372
  return pathAliases(
4168
- normalizePathForHookComparison(path24.resolve(cwd, relativeHookPath))
4373
+ normalizePathForHookComparison(path25.resolve(cwd, relativeHookPath))
4169
4374
  ).includes(normalizedHookPath);
4170
4375
  });
4171
4376
  }
@@ -4255,7 +4460,7 @@ program.command("init").description("Initialize easy-coding harness in current p
4255
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));
4256
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));
4257
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));
4258
- 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));
4259
4464
  program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
4260
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));
4261
4466
  program.parse();