easy-coding-harness 0.6.1 → 0.7.1-beta0

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import chalk8 from "chalk";
4
+ import chalk9 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/add-agent.ts
@@ -103,9 +103,11 @@ async function isDirectory(filePath) {
103
103
  }
104
104
 
105
105
  // src/utils/config-yaml.ts
106
+ var CONFIG_SCHEMA_VERSION = 2;
107
+ var CONFIRM_MODES = ["approve", "guard", "auto"];
106
108
  function createDefaultConfig(params) {
107
- const config = {
108
- version: 1,
109
+ const config2 = {
110
+ version: CONFIG_SCHEMA_VERSION,
109
111
  harness_version: params.harnessVersion,
110
112
  agents: params.agents,
111
113
  project: {
@@ -121,20 +123,19 @@ function createDefaultConfig(params) {
121
123
  auto_archive_days: 30
122
124
  },
123
125
  behavior: {
124
- strict_confirm: true,
125
- auto_mode: false
126
+ confirm_mode: "guard"
126
127
  }
127
128
  };
128
129
  if (params.supermodule) {
129
- config.supermodule = params.supermodule;
130
+ config2.supermodule = params.supermodule;
130
131
  }
131
- return config;
132
+ return config2;
132
133
  }
133
- function stringifyConfig(config) {
134
- return YAML.stringify(config);
134
+ function stringifyConfig(config2) {
135
+ return YAML.stringify(config2);
135
136
  }
136
- async function writeConfigYaml(filePath, config) {
137
- await writeTextFile(filePath, stringifyConfig(config));
137
+ async function writeConfigYaml(filePath, config2) {
138
+ await writeTextFile(filePath, stringifyConfig(config2));
138
139
  }
139
140
  async function readConfigYaml(filePath) {
140
141
  const content = await readFile2(filePath, "utf8");
@@ -142,8 +143,8 @@ async function readConfigYaml(filePath) {
142
143
  }
143
144
  async function readProjectIdIfExists(filePath) {
144
145
  try {
145
- const config = await readConfigYaml(filePath);
146
- return typeof config.project?.id === "string" && config.project.id.trim() ? config.project.id : null;
146
+ const config2 = await readConfigYaml(filePath);
147
+ return typeof config2.project?.id === "string" && config2.project.id.trim() ? config2.project.id : null;
147
148
  } catch {
148
149
  return null;
149
150
  }
@@ -151,41 +152,74 @@ async function readProjectIdIfExists(filePath) {
151
152
  async function updateConfigYaml(filePath, updater) {
152
153
  const content = await readFile2(filePath, "utf8");
153
154
  const document = parseDocument(content);
154
- const config = document.toJSON();
155
- updater(config);
156
- for (const [key, value] of Object.entries(config)) {
155
+ const config2 = document.toJSON();
156
+ updater(config2);
157
+ for (const [key, value] of Object.entries(config2)) {
157
158
  document.set(key, value);
158
159
  }
159
160
  await writeTextFile(filePath, document.toString());
160
- return config;
161
+ return config2;
161
162
  }
162
163
  async function addAgentsToConfig(filePath, agents) {
163
- return updateConfigYaml(filePath, (config) => {
164
- const merged = /* @__PURE__ */ new Set([...config.agents ?? [], ...agents]);
165
- config.agents = [...merged];
164
+ return updateConfigYaml(filePath, (config2) => {
165
+ const merged = /* @__PURE__ */ new Set([...config2.agents ?? [], ...agents]);
166
+ config2.agents = [...merged];
166
167
  });
167
168
  }
168
169
  async function updateHarnessVersion(filePath, version) {
169
- return updateConfigYaml(filePath, (config) => {
170
- config.harness_version = version;
170
+ return updateConfigYaml(filePath, (config2) => {
171
+ config2.harness_version = version;
172
+ });
173
+ }
174
+ function isConfirmMode(value) {
175
+ return typeof value === "string" && CONFIRM_MODES.includes(value);
176
+ }
177
+ function resolveLegacyConfirmMode(config2) {
178
+ const behavior = config2.behavior ?? {};
179
+ if (isConfirmMode(behavior.confirm_mode)) {
180
+ return behavior.confirm_mode;
181
+ }
182
+ if (behavior.auto_mode === true) {
183
+ return "auto";
184
+ }
185
+ if (behavior.strict_confirm === true) {
186
+ return "approve";
187
+ }
188
+ return "guard";
189
+ }
190
+ async function setConfirmMode(filePath, mode) {
191
+ return updateConfigYaml(filePath, (config2) => {
192
+ const legacyBehavior = config2.behavior ?? {};
193
+ const behavior = Object.fromEntries(
194
+ Object.entries(legacyBehavior).filter(
195
+ ([key]) => key !== "strict_confirm" && key !== "auto_mode"
196
+ )
197
+ );
198
+ behavior.confirm_mode = mode;
199
+ config2.behavior = behavior;
200
+ config2.version = CONFIG_SCHEMA_VERSION;
171
201
  });
172
202
  }
203
+ async function migrateConfirmModeConfig(filePath) {
204
+ const config2 = await readConfigYaml(filePath);
205
+ return setConfirmMode(filePath, resolveLegacyConfirmMode(config2));
206
+ }
173
207
  async function ensureProjectId(filePath) {
174
208
  let projectId = "";
175
- await updateConfigYaml(filePath, (config) => {
176
- if (!config.project || typeof config.project !== "object") {
177
- config.project = { id: createProjectId(), name: "" };
209
+ await updateConfigYaml(filePath, (config2) => {
210
+ if (!config2.project || typeof config2.project !== "object") {
211
+ config2.project = { id: createProjectId(), name: "" };
178
212
  }
179
- if (typeof config.project.id !== "string" || !config.project.id.trim()) {
180
- config.project.id = createProjectId();
213
+ if (typeof config2.project.id !== "string" || !config2.project.id.trim()) {
214
+ config2.project.id = createProjectId();
181
215
  }
182
- projectId = config.project.id;
216
+ projectId = config2.project.id;
183
217
  });
184
218
  return projectId;
185
219
  }
186
220
  async function updateSupermoduleConfig(filePath, supermodule) {
187
- return updateConfigYaml(filePath, (config) => {
188
- config.supermodule = supermodule;
221
+ return updateConfigYaml(filePath, (config2) => {
222
+ config2.supermodule = supermodule;
189
223
  });
190
224
  }
191
225
  function createProjectId() {
@@ -1800,11 +1834,11 @@ async function listParentManagedSubmodulePaths(cwd) {
1800
1834
  return [];
1801
1835
  }
1802
1836
  try {
1803
- const config = await readConfigYaml(parentConfigPath);
1804
- if (config.supermodule?.role !== "super-parent" || !Array.isArray(config.supermodule.submodules)) {
1837
+ const config2 = await readConfigYaml(parentConfigPath);
1838
+ if (config2.supermodule?.role !== "super-parent" || !Array.isArray(config2.supermodule.submodules)) {
1805
1839
  return [];
1806
1840
  }
1807
- return config.supermodule.submodules.filter((submodulePath) => typeof submodulePath === "string").sort();
1841
+ return config2.supermodule.submodules.filter((submodulePath) => typeof submodulePath === "string").sort();
1808
1842
  } catch {
1809
1843
  return [];
1810
1844
  }
@@ -1889,9 +1923,9 @@ async function addAgent(opts) {
1889
1923
  if (!await pathExists(target.configPath)) {
1890
1924
  continue;
1891
1925
  }
1892
- const config = await readConfigYaml(target.configPath);
1893
- const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
1894
- const agents = [...config.agents, ...toInstall];
1926
+ const config2 = await readConfigYaml(target.configPath);
1927
+ const toInstall = platforms.filter((platform) => !config2.agents.includes(platform));
1928
+ const agents = [...config2.agents, ...toInstall];
1895
1929
  if (toInstall.length > 0) {
1896
1930
  const projectId = await writeRuntimeScaffold(target.dir, agents, {
1897
1931
  supermodule: target.supermodule
@@ -2000,18 +2034,18 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
2000
2034
  if (!parent || parent.label !== ".") {
2001
2035
  return;
2002
2036
  }
2003
- const config = await readConfigYaml(parent.configPath);
2004
- if (!Array.isArray(config.agents) || config.agents.length === 0) {
2037
+ const config2 = await readConfigYaml(parent.configPath);
2038
+ if (!Array.isArray(config2.agents) || config2.agents.length === 0) {
2005
2039
  return;
2006
2040
  }
2007
- await refreshSupermoduleParent(cwd, config.agents, parent.supermodule.submodules ?? []);
2041
+ await refreshSupermoduleParent(cwd, config2.agents, parent.supermodule.submodules ?? []);
2008
2042
  }
2009
2043
  async function resolveInstalledAgents(cwd) {
2010
2044
  const configPath2 = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2011
2045
  if (await pathExists(configPath2)) {
2012
2046
  try {
2013
- const config = await readConfigYaml(configPath2);
2014
- const listed = Array.isArray(config.agents) ? config.agents.filter(isAgentPlatform) : [];
2047
+ const config2 = await readConfigYaml(configPath2);
2048
+ const listed = Array.isArray(config2.agents) ? config2.agents.filter(isAgentPlatform) : [];
2015
2049
  if (listed.length > 0) {
2016
2050
  return listed;
2017
2051
  }
@@ -2352,8 +2386,8 @@ async function executeClearPlan(plan) {
2352
2386
  for (const target of plan.remove) {
2353
2387
  await rm(target, { recursive: true, force: true });
2354
2388
  }
2355
- for (const config of plan.pruneHookConfigs) {
2356
- await pruneHookConfig(config);
2389
+ for (const config2 of plan.pruneHookConfigs) {
2390
+ await pruneHookConfig(config2);
2357
2391
  }
2358
2392
  for (const constraint of plan.constraints) {
2359
2393
  await stripConstraintRegion(constraint);
@@ -2362,8 +2396,8 @@ async function executeClearPlan(plan) {
2362
2396
  await removeIfEmpty(dir);
2363
2397
  }
2364
2398
  }
2365
- async function pruneHookConfig(config) {
2366
- const { filePath, managedHookCommands, managedHookPaths } = config;
2399
+ async function pruneHookConfig(config2) {
2400
+ const { filePath, managedHookCommands, managedHookPaths } = config2;
2367
2401
  const content = await readTextIfExists(filePath);
2368
2402
  if (content === null) {
2369
2403
  return;
@@ -2538,21 +2572,163 @@ function renderTargetPlans(targetPlans) {
2538
2572
  ).join("\n\n");
2539
2573
  }
2540
2574
 
2541
- // src/commands/init.ts
2575
+ // src/commands/config.ts
2542
2576
  import path18 from "path";
2543
- import { note, outro as outro3 } from "@clack/prompts";
2577
+ import { cancel as cancel4, confirm as confirm3, outro as outro3, select } from "@clack/prompts";
2544
2578
  import chalk4 from "chalk";
2545
2579
 
2580
+ // src/utils/compare-versions.ts
2581
+ import path17 from "path";
2582
+ function parseVersion(version) {
2583
+ const withoutBuild = String(version ?? "").split("+", 1)[0];
2584
+ const separator = withoutBuild.indexOf("-");
2585
+ const coreText = separator === -1 ? withoutBuild : withoutBuild.slice(0, separator);
2586
+ const prereleaseText = separator === -1 ? "" : withoutBuild.slice(separator + 1);
2587
+ const core = coreText.split(".").map((part) => {
2588
+ const parsed = Number.parseInt(part, 10);
2589
+ return Number.isNaN(parsed) ? 0 : parsed;
2590
+ });
2591
+ return {
2592
+ core,
2593
+ prerelease: prereleaseText ? prereleaseText.split(".") : null
2594
+ };
2595
+ }
2596
+ function comparePrerelease(left, right) {
2597
+ if (left === null && right === null) return 0;
2598
+ if (left === null) return 1;
2599
+ if (right === null) return -1;
2600
+ const length = Math.max(left.length, right.length);
2601
+ for (let index = 0; index < length; index += 1) {
2602
+ const leftPart = left[index];
2603
+ const rightPart = right[index];
2604
+ if (leftPart === void 0) return -1;
2605
+ if (rightPart === void 0) return 1;
2606
+ if (leftPart === rightPart) continue;
2607
+ const leftNumeric = /^\d+$/.test(leftPart);
2608
+ const rightNumeric = /^\d+$/.test(rightPart);
2609
+ if (leftNumeric && rightNumeric) {
2610
+ const leftNumber = Number.parseInt(leftPart, 10);
2611
+ const rightNumber = Number.parseInt(rightPart, 10);
2612
+ return leftNumber < rightNumber ? -1 : 1;
2613
+ }
2614
+ if (leftNumeric) return -1;
2615
+ if (rightNumeric) return 1;
2616
+ return leftPart < rightPart ? -1 : 1;
2617
+ }
2618
+ return 0;
2619
+ }
2620
+ function compareVersions(a, b) {
2621
+ const left = parseVersion(a);
2622
+ const right = parseVersion(b);
2623
+ const length = Math.max(left.core.length, right.core.length);
2624
+ for (let index = 0; index < length; index += 1) {
2625
+ const leftPart = left.core[index] ?? 0;
2626
+ const rightPart = right.core[index] ?? 0;
2627
+ if (leftPart < rightPart) return -1;
2628
+ if (leftPart > rightPart) return 1;
2629
+ }
2630
+ return comparePrerelease(left.prerelease, right.prerelease);
2631
+ }
2632
+ function isVersionBehind(installed, current = VERSION) {
2633
+ return compareVersions(installed, current) === -1;
2634
+ }
2635
+ async function checkForUpgrade(cwd) {
2636
+ const configPath2 = path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2637
+ if (!await pathExists(configPath2)) {
2638
+ return;
2639
+ }
2640
+ let config2;
2641
+ try {
2642
+ config2 = await readConfigYaml(configPath2);
2643
+ } catch {
2644
+ return;
2645
+ }
2646
+ const installed = String(config2.harness_version ?? "");
2647
+ if (installed && isVersionBehind(installed)) {
2648
+ process.stderr.write(
2649
+ `easy-coding harness ${installed} is older than CLI ${VERSION}. Run easy-coding upgrade when ready.
2650
+ `
2651
+ );
2652
+ }
2653
+ }
2654
+
2655
+ // src/commands/config.ts
2656
+ async function config() {
2657
+ renderBanner();
2658
+ const configPath2 = path18.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
2659
+ if (!await pathExists(configPath2)) {
2660
+ throw new Error("No easy-coding harness found in this project.");
2661
+ }
2662
+ const projectConfig = await readConfigYaml(configPath2);
2663
+ if (projectConfig.harness_version !== VERSION) {
2664
+ const relation = compareVersions(projectConfig.harness_version, VERSION);
2665
+ if (relation === -1) {
2666
+ throw new Error(
2667
+ `Project harness ${projectConfig.harness_version} is older than CLI ${VERSION}. Run easy-coding upgrade first.`
2668
+ );
2669
+ }
2670
+ if (relation === 1) {
2671
+ throw new Error(
2672
+ `Project harness ${projectConfig.harness_version} is newer than CLI ${VERSION}. Update the CLI first.`
2673
+ );
2674
+ }
2675
+ throw new Error(
2676
+ `Project harness ${projectConfig.harness_version} does not exactly match CLI ${VERSION}. Upgrade the harness or update the CLI before changing config.`
2677
+ );
2678
+ }
2679
+ const current = resolveLegacyConfirmMode(projectConfig);
2680
+ const selected = await select({
2681
+ message: `Select project confirm mode (current: ${current})`,
2682
+ initialValue: current,
2683
+ options: [
2684
+ {
2685
+ value: "approve",
2686
+ label: "approve \u2014 confirm every stage transition",
2687
+ hint: "except INIT -> ANALYSIS and MEMORY -> COMPLETE"
2688
+ },
2689
+ {
2690
+ value: "guard",
2691
+ label: "guard \u2014 confirm critical gates (default)",
2692
+ hint: "ANALYSIS -> IMPLEMENT and VERIFICATION -> MEMORY"
2693
+ },
2694
+ {
2695
+ value: "auto",
2696
+ label: "auto \u2014 advance workflow stages automatically",
2697
+ hint: "task closure remains explicit"
2698
+ }
2699
+ ]
2700
+ });
2701
+ if (typeof selected === "symbol") {
2702
+ cancel4("Configuration cancelled.");
2703
+ return;
2704
+ }
2705
+ const shouldSave = await confirm3({
2706
+ message: `Set behavior.confirm_mode to ${selected}?`,
2707
+ initialValue: true
2708
+ });
2709
+ if (typeof shouldSave === "symbol" || !shouldSave) {
2710
+ cancel4("Configuration cancelled.");
2711
+ return;
2712
+ }
2713
+ await setConfirmMode(configPath2, selected);
2714
+ outro3(chalk4.green(`Project confirm mode updated to ${selected}.`));
2715
+ }
2716
+
2717
+ // src/commands/init.ts
2718
+ import path20 from "path";
2719
+ import { note, outro as outro4 } from "@clack/prompts";
2720
+ import chalk5 from "chalk";
2721
+
2546
2722
  // src/utils/install-state.ts
2547
2723
  import { readdir as readdir5 } from "fs/promises";
2548
- import path17 from "path";
2724
+ import path19 from "path";
2549
2725
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
2550
2726
  async function detectEasyCodingInstallState(cwd) {
2551
- const easyCodingDir = path17.join(cwd, EASY_CODING_DIR);
2727
+ const easyCodingDir = path19.join(cwd, EASY_CODING_DIR);
2552
2728
  if (!await pathExists(easyCodingDir)) {
2553
2729
  return { kind: "fresh", easyCodingDir };
2554
2730
  }
2555
- const configPath2 = path17.join(easyCodingDir, CONFIG_FILE);
2731
+ const configPath2 = path19.join(easyCodingDir, CONFIG_FILE);
2556
2732
  if (await pathExists(configPath2)) {
2557
2733
  return { kind: "installed", easyCodingDir, configPath: configPath2 };
2558
2734
  }
@@ -2570,17 +2746,17 @@ async function detectEasyCodingInstallState(cwd) {
2570
2746
  async function detectLegacyAssets(easyCodingDir) {
2571
2747
  const assets = [];
2572
2748
  for (const file of LEGACY_ROOT_FILES) {
2573
- if (await pathExists(path17.join(easyCodingDir, file))) {
2749
+ if (await pathExists(path19.join(easyCodingDir, file))) {
2574
2750
  assets.push(relativeEasyCodingPath(file));
2575
2751
  }
2576
2752
  }
2577
- if (await pathExists(path17.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
2753
+ if (await pathExists(path19.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
2578
2754
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
2579
2755
  }
2580
- const shortMemoryFiles = await listMarkdownFiles(path17.join(easyCodingDir, "memory", "short"));
2756
+ const shortMemoryFiles = await listMarkdownFiles(path19.join(easyCodingDir, "memory", "short"));
2581
2757
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
2582
2758
  for (const dir of ["spec", "prototype"]) {
2583
- if (await hasAnyDirectoryEntry(path17.join(easyCodingDir, dir))) {
2759
+ if (await hasAnyDirectoryEntry(path19.join(easyCodingDir, dir))) {
2584
2760
  assets.push(relativeEasyCodingPath(dir));
2585
2761
  }
2586
2762
  }
@@ -2600,13 +2776,13 @@ async function hasAnyDirectoryEntry(dir) {
2600
2776
  return (await readdir5(dir)).length > 0;
2601
2777
  }
2602
2778
  function relativeEasyCodingPath(...segments) {
2603
- return path17.posix.join(EASY_CODING_DIR, ...segments);
2779
+ return path19.posix.join(EASY_CODING_DIR, ...segments);
2604
2780
  }
2605
2781
  function relativeConfigPath() {
2606
- return path17.posix.join(EASY_CODING_DIR, CONFIG_FILE);
2782
+ return path19.posix.join(EASY_CODING_DIR, CONFIG_FILE);
2607
2783
  }
2608
2784
  function relativeProjectInitTaskPath() {
2609
- return path17.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
2785
+ return path19.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
2610
2786
  }
2611
2787
 
2612
2788
  // src/commands/init.ts
@@ -2626,7 +2802,7 @@ async function init(opts) {
2626
2802
  const platforms2 = await resolveInitPlatforms(cwd, opts, true);
2627
2803
  await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms2);
2628
2804
  }
2629
- outro3(chalk4.yellow("All selected easy-coding harness targets are already installed."));
2805
+ outro4(chalk5.yellow("All selected easy-coding harness targets are already installed."));
2630
2806
  return;
2631
2807
  }
2632
2808
  const platforms = await resolveInitPlatforms(cwd, opts, Boolean(parentTarget2?.installed));
@@ -2641,8 +2817,8 @@ async function init(opts) {
2641
2817
  (platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
2642
2818
  ).join("\n");
2643
2819
  note(triggers, "Next step");
2644
- outro3(
2645
- chalk4.green(
2820
+ outro4(
2821
+ chalk5.green(
2646
2822
  `easy-coding harness installed in ${installableTargets.map((target) => target.label).join(", ")}. Open your agent and run ec-init.`
2647
2823
  )
2648
2824
  );
@@ -2687,7 +2863,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
2687
2863
  if (!targetSubmodulePaths.has(entry.path)) {
2688
2864
  continue;
2689
2865
  }
2690
- const dir = path18.join(cwd, entry.path);
2866
+ const dir = path20.join(cwd, entry.path);
2691
2867
  targets.push(
2692
2868
  await targetFromState(dir, entry.path, "submodule-child", {
2693
2869
  parent: toPosixRelative2(dir, cwd)
@@ -2734,25 +2910,25 @@ function contextFromState(role, installState) {
2734
2910
  };
2735
2911
  }
2736
2912
  function toPosixRelative2(from, to) {
2737
- const relative = path18.relative(from, to);
2738
- return relative ? relative.split(path18.sep).join("/") : ".";
2913
+ const relative = path20.relative(from, to);
2914
+ return relative ? relative.split(path20.sep).join("/") : ".";
2739
2915
  }
2740
2916
  async function resolveInitPlatforms(cwd, opts, parentInstalled) {
2741
2917
  if (opts.agent || !parentInstalled) {
2742
2918
  return resolvePlatforms(opts, ["claude-code"]);
2743
2919
  }
2744
- const config = await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
2745
- if (Array.isArray(config.agents) && config.agents.length > 0) {
2746
- return config.agents;
2920
+ const config2 = await readConfigYaml(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
2921
+ if (Array.isArray(config2.agents) && config2.agents.length > 0) {
2922
+ return config2.agents;
2747
2923
  }
2748
2924
  return resolvePlatforms(opts, ["claude-code"]);
2749
2925
  }
2750
2926
  async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
2751
- if (!await pathExists(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
2927
+ if (!await pathExists(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
2752
2928
  return;
2753
2929
  }
2754
- const config = parentTarget2.installed ? await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
2755
- const platforms = Array.isArray(config.agents) && config.agents.length > 0 ? config.agents : installPlatforms;
2930
+ const config2 = parentTarget2.installed ? await readConfigYaml(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
2931
+ const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
2756
2932
  await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
2757
2933
  }
2758
2934
  async function refreshInstalledChildTopologies(targets) {
@@ -2760,7 +2936,7 @@ async function refreshInstalledChildTopologies(targets) {
2760
2936
  if (!target.installed || target.context.role !== "submodule-child") {
2761
2937
  continue;
2762
2938
  }
2763
- const configPath2 = path18.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
2939
+ const configPath2 = path20.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
2764
2940
  if (!await pathExists(configPath2)) {
2765
2941
  continue;
2766
2942
  }
@@ -2769,63 +2945,19 @@ async function refreshInstalledChildTopologies(targets) {
2769
2945
  }
2770
2946
 
2771
2947
  // src/commands/status.ts
2772
- import path21 from "path";
2773
- import chalk5 from "chalk";
2774
-
2775
- // src/utils/compare-versions.ts
2776
- import path19 from "path";
2777
- function normalize(version) {
2778
- const [core] = String(version ?? "").split("-");
2779
- return core.split(".").map((part) => {
2780
- const parsed = Number.parseInt(part, 10);
2781
- return Number.isNaN(parsed) ? 0 : parsed;
2782
- });
2783
- }
2784
- function compareVersions(a, b) {
2785
- const left = normalize(a);
2786
- const right = normalize(b);
2787
- const length = Math.max(left.length, right.length);
2788
- for (let index = 0; index < length; index += 1) {
2789
- const leftPart = left[index] ?? 0;
2790
- const rightPart = right[index] ?? 0;
2791
- if (leftPart < rightPart) return -1;
2792
- if (leftPart > rightPart) return 1;
2793
- }
2794
- return 0;
2795
- }
2796
- function isVersionBehind(installed, current = VERSION) {
2797
- return compareVersions(installed, current) === -1;
2798
- }
2799
- async function checkForUpgrade(cwd) {
2800
- const configPath2 = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2801
- if (!await pathExists(configPath2)) {
2802
- return;
2803
- }
2804
- let config;
2805
- try {
2806
- config = await readConfigYaml(configPath2);
2807
- } catch {
2808
- return;
2809
- }
2810
- const installed = String(config.harness_version ?? "");
2811
- if (installed && isVersionBehind(installed)) {
2812
- process.stderr.write(
2813
- `easy-coding harness ${installed} is older than CLI ${VERSION}. Run easy-coding upgrade when ready.
2814
- `
2815
- );
2816
- }
2817
- }
2948
+ import path22 from "path";
2949
+ import chalk6 from "chalk";
2818
2950
 
2819
2951
  // src/utils/session.ts
2820
2952
  import { readdir as readdir6 } from "fs/promises";
2821
- import path20 from "path";
2953
+ import path21 from "path";
2822
2954
  var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
2823
2955
  function getSessionDir(cwd) {
2824
- return path20.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
2956
+ return path21.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
2825
2957
  }
2826
2958
  function getSessionFilePath(cwd, ppid) {
2827
2959
  const pid = ppid ?? process.ppid;
2828
- return path20.join(getSessionDir(cwd), `${pid}.json`);
2960
+ return path21.join(getSessionDir(cwd), `${pid}.json`);
2829
2961
  }
2830
2962
  async function readSessionFile(cwd, ppid) {
2831
2963
  const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
@@ -2839,30 +2971,36 @@ async function readSessionFile(cwd, ppid) {
2839
2971
  async function status() {
2840
2972
  renderBanner();
2841
2973
  const cwd = process.cwd();
2842
- const configPath2 = path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2974
+ const configPath2 = path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
2843
2975
  if (!await pathExists(configPath2)) {
2844
2976
  throw new Error("No easy-coding harness found in this project.");
2845
2977
  }
2846
- const config = await readConfigYaml(configPath2);
2978
+ const config2 = await readConfigYaml(configPath2);
2847
2979
  const tasks = await listTasks(cwd);
2848
2980
  const taskCounts = summarizeTaskStatuses(tasks);
2849
2981
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
2850
2982
  const session = await readSessionFile(cwd);
2851
- const versionRelation = compareVersions(config.harness_version, VERSION);
2852
- console.log(chalk5.bold("Harness"));
2853
- console.log(` version: ${config.harness_version}`);
2983
+ const versionRelation = compareVersions(config2.harness_version, VERSION);
2984
+ console.log(chalk6.bold("Harness"));
2985
+ console.log(` version: ${config2.harness_version}`);
2854
2986
  console.log(` cli: ${VERSION}`);
2855
- if (versionRelation === -1) {
2856
- console.log(chalk5.yellow(" upgrade: available"));
2987
+ if (versionRelation === -1 || versionRelation === 0 && config2.harness_version !== VERSION) {
2988
+ console.log(chalk6.yellow(" upgrade: available"));
2857
2989
  } else if (versionRelation === 1) {
2858
- console.log(chalk5.red(" upgrade: CLI is older than project harness"));
2990
+ console.log(chalk6.red(" upgrade: CLI is older than project harness"));
2859
2991
  } else {
2860
2992
  console.log(" upgrade: up to date");
2861
2993
  }
2862
- console.log(` agents: ${config.agents.join(", ") || "(none)"}`);
2863
- console.log(` project: ${config.project.name}`);
2994
+ console.log(` agents: ${config2.agents.join(", ") || "(none)"}`);
2995
+ console.log(` project: ${config2.project.name}`);
2996
+ const projectConfirmMode = isConfirmMode(config2.behavior?.confirm_mode) ? config2.behavior.confirm_mode : "guard";
2997
+ console.log(` confirm_mode: ${projectConfirmMode}`);
2864
2998
  console.log("");
2865
- console.log(chalk5.bold("Session"));
2999
+ console.log(chalk6.bold("Session"));
3000
+ const sessionConfirmMode = session?.confirm_mode;
3001
+ console.log(` confirm_mode: ${sessionConfirmMode ?? "project default"}`);
3002
+ console.log(` effective_confirm_mode: ${sessionConfirmMode ?? projectConfirmMode}`);
3003
+ console.log(` harness: ${session?.harness_disabled ? "disabled for this session" : "enabled"}`);
2866
3004
  if (session?.current_task) {
2867
3005
  const taskPath = getTaskJsonPath(cwd, session.current_task);
2868
3006
  if (await pathExists(taskPath)) {
@@ -2877,7 +3015,7 @@ async function status() {
2877
3015
  console.log(" no active session");
2878
3016
  }
2879
3017
  console.log("");
2880
- console.log(chalk5.bold("Tasks"));
3018
+ console.log(chalk6.bold("Tasks"));
2881
3019
  console.log(` total: ${tasks.length}`);
2882
3020
  for (const [taskStatus, count] of Object.entries(taskCounts)) {
2883
3021
  console.log(` ${taskStatus}: ${count}`);
@@ -2893,8 +3031,8 @@ async function status() {
2893
3031
 
2894
3032
  // src/commands/update.ts
2895
3033
  import { execFileSync } from "child_process";
2896
- import { cancel as cancel4, confirm as confirm3, outro as outro4 } from "@clack/prompts";
2897
- import chalk6 from "chalk";
3034
+ import { cancel as cancel5, confirm as confirm4, outro as outro5 } from "@clack/prompts";
3035
+ import chalk7 from "chalk";
2898
3036
  async function update(opts) {
2899
3037
  renderBanner();
2900
3038
  const tag = opts.tag ?? "latest";
@@ -2907,24 +3045,24 @@ async function update(opts) {
2907
3045
  return;
2908
3046
  }
2909
3047
  if (!opts.yes) {
2910
- const confirmed = await confirm3({
3048
+ const confirmed = await confirm4({
2911
3049
  message: `Update the global CLI to ${spec}?`,
2912
3050
  initialValue: true
2913
3051
  });
2914
3052
  if (typeof confirmed === "symbol" || !confirmed) {
2915
- cancel4("Update cancelled.");
3053
+ cancel5("Update cancelled.");
2916
3054
  return;
2917
3055
  }
2918
3056
  }
2919
- console.log(chalk6.cyan(`\u2192 npm install -g ${spec}`));
3057
+ console.log(chalk7.cyan(`\u2192 npm install -g ${spec}`));
2920
3058
  execFileSync("npm", ["install", "-g", spec], { stdio: "inherit" });
2921
- outro4(chalk6.green("Global CLI updated."));
3059
+ outro5(chalk7.green("Global CLI updated."));
2922
3060
  }
2923
3061
 
2924
3062
  // src/commands/upgrade.ts
2925
- import path22 from "path";
2926
- import { cancel as cancel5, confirm as confirm4, outro as outro5 } from "@clack/prompts";
2927
- import chalk7 from "chalk";
3063
+ import path23 from "path";
3064
+ import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
3065
+ import chalk8 from "chalk";
2928
3066
  var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
2929
3067
  "claude-code": [
2930
3068
  { event: "SessionStart", scriptName: "session-start.py" },
@@ -2962,13 +3100,13 @@ async function upgrade(opts) {
2962
3100
  const parentTopologyRefresh = await resolveParentTopologyRefresh(targets, pending);
2963
3101
  const childTopologyRefreshes = await resolveChildTopologyRefreshes(targets, pending);
2964
3102
  if (pending.length === 0 && !parentTopologyRefresh && childTopologyRefreshes.length === 0) {
2965
- outro5(chalk7.green(`easy-coding harness is already up to date (${VERSION}).`));
3103
+ outro6(chalk8.green(`easy-coding harness is already up to date (${VERSION}).`));
2966
3104
  return;
2967
3105
  }
2968
3106
  const summary = [
2969
3107
  "Upgrade targets:",
2970
3108
  ...pending.length > 0 ? pending.map(
2971
- ({ target, config }) => `- ${target.label}: ${String(config.harness_version ?? "unknown")} -> ${VERSION}; agents: ${config.agents.join(", ")}`
3109
+ ({ target, config: config2 }) => `- ${target.label}: ${String(config2.harness_version ?? "unknown")} -> ${VERSION}; agents: ${config2.agents.join(", ")}`
2972
3110
  ) : ["- (none)"],
2973
3111
  ...parentTopologyRefresh ? [
2974
3112
  "Supermodule topology refresh:",
@@ -2980,6 +3118,7 @@ async function upgrade(opts) {
2980
3118
  ].filter(Boolean) : [],
2981
3119
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
2982
3120
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
3121
+ "Will migrate behavior.strict_confirm/auto_mode to behavior.confirm_mode and remove the old keys.",
2983
3122
  "Will migrate legacy workflow stage metadata; memory content, spec, and project knowledge files remain untouched."
2984
3123
  ].join("\n");
2985
3124
  if (opts.dryRun) {
@@ -2987,31 +3126,32 @@ async function upgrade(opts) {
2987
3126
  return;
2988
3127
  }
2989
3128
  if (!opts.yes) {
2990
- const shouldUpgrade = await confirm4({
3129
+ const shouldUpgrade = await confirm5({
2991
3130
  message: "Apply this harness upgrade?",
2992
3131
  initialValue: true
2993
3132
  });
2994
3133
  if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
2995
- cancel5("Upgrade cancelled.");
3134
+ cancel6("Upgrade cancelled.");
2996
3135
  return;
2997
3136
  }
2998
3137
  }
2999
- for (const { target, config } of pending) {
3000
- const projectId = await writeRuntimeScaffold(target.dir, config.agents, {
3138
+ for (const { target, config: config2 } of pending) {
3139
+ const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
3001
3140
  supermodule: target.supermodule
3002
3141
  });
3003
- const artifacts = await configurePlatformsForDir(target.dir, config.agents, {
3142
+ const artifacts = await configurePlatformsForDir(target.dir, config2.agents, {
3004
3143
  supermodule: target.boundary,
3005
3144
  projectId
3006
3145
  });
3007
3146
  await writeInstallManifest(target.dir, {
3008
3147
  harnessVersion: VERSION,
3009
- agents: config.agents,
3148
+ agents: config2.agents,
3010
3149
  artifacts
3011
3150
  });
3012
3151
  await ensureEasyCodingSessionsIgnored(target.dir);
3013
3152
  await ensureHookBytecodeIgnored(target.dir);
3014
3153
  await migrateLegacyWorkflowState(target.dir);
3154
+ await migrateConfirmModeConfig(target.configPath);
3015
3155
  await updateHarnessVersion(target.configPath, VERSION);
3016
3156
  await updateSupermoduleConfig(target.configPath, target.supermodule);
3017
3157
  await setPendingInitSince(target.dir, VERSION);
@@ -3027,8 +3167,8 @@ async function upgrade(opts) {
3027
3167
  for (const { target } of childTopologyRefreshes) {
3028
3168
  await updateSupermoduleConfig(target.configPath, target.supermodule);
3029
3169
  }
3030
- outro5(
3031
- chalk7.green(
3170
+ outro6(
3171
+ chalk8.green(
3032
3172
  pending.length > 0 ? `easy-coding harness upgraded to ${VERSION}.` : "easy-coding supermodule topology refreshed."
3033
3173
  )
3034
3174
  );
@@ -3039,9 +3179,9 @@ async function resolvePendingUpgradeTargets(targets) {
3039
3179
  if (!await pathExists(target.configPath)) {
3040
3180
  continue;
3041
3181
  }
3042
- const config = await readConfigYaml(target.configPath);
3043
- const installedVersion = String(config.harness_version ?? "");
3044
- const hasAgents = Array.isArray(config.agents) && config.agents.length > 0;
3182
+ const config2 = await readConfigYaml(target.configPath);
3183
+ const installedVersion = String(config2.harness_version ?? "");
3184
+ const hasAgents = Array.isArray(config2.agents) && config2.agents.length > 0;
3045
3185
  if (!installedVersion || !hasAgents) {
3046
3186
  throw new Error(
3047
3187
  `${target.label} config.yaml is missing required harness fields (harness_version / agents). This project predates the current config layout. Run \`easy-coding clear\` then \`easy-coding init\` to migrate \u2014 your tasks, spec, and memory are preserved.`
@@ -3053,21 +3193,21 @@ async function resolvePendingUpgradeTargets(targets) {
3053
3193
  `${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
3054
3194
  );
3055
3195
  }
3056
- if (relation === -1 || relation === 0 && (await needsHookConfigRefresh(target, config) || await hasLegacyWorkflowState(target.dir))) {
3057
- pending.push({ target, config });
3196
+ if (relation === -1 || relation === 0 && (installedVersion !== VERSION || await needsHookConfigRefresh(target, config2) || await hasLegacyWorkflowState(target.dir))) {
3197
+ pending.push({ target, config: config2 });
3058
3198
  }
3059
3199
  }
3060
3200
  return pending;
3061
3201
  }
3062
- async function needsHookConfigRefresh(target, config) {
3063
- const projectId = typeof config.project?.id === "string" ? config.project.id.trim() : "";
3202
+ async function needsHookConfigRefresh(target, config2) {
3203
+ const projectId = typeof config2.project?.id === "string" ? config2.project.id.trim() : "";
3064
3204
  if (!projectId) {
3065
3205
  return true;
3066
3206
  }
3067
3207
  const manifest = await readInstallManifest(target.dir);
3068
- for (const agent of config.agents) {
3208
+ for (const agent of config2.agents) {
3069
3209
  const meta = resolvePlatformMeta(target.dir, agent);
3070
- const configPath2 = path22.join(target.dir, meta.hookConfigFile);
3210
+ const configPath2 = path23.join(target.dir, meta.hookConfigFile);
3071
3211
  const content = await readTextIfExists(configPath2);
3072
3212
  if (content === null) {
3073
3213
  return true;
@@ -3184,7 +3324,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
3184
3324
  return true;
3185
3325
  }
3186
3326
  return pathAliases(
3187
- normalizePathForHookComparison(path22.resolve(cwd, relativeHookPath))
3327
+ normalizePathForHookComparison(path23.resolve(cwd, relativeHookPath))
3188
3328
  ).includes(normalizedHookPath);
3189
3329
  });
3190
3330
  }
@@ -3210,19 +3350,19 @@ async function resolveParentTopologyRefresh(targets, pending) {
3210
3350
  if (!await pathExists(parent.configPath)) {
3211
3351
  return null;
3212
3352
  }
3213
- const config = await readConfigYaml(parent.configPath);
3214
- if (!Array.isArray(config.agents) || config.agents.length === 0) {
3353
+ const config2 = await readConfigYaml(parent.configPath);
3354
+ if (!Array.isArray(config2.agents) || config2.agents.length === 0) {
3215
3355
  return null;
3216
3356
  }
3217
3357
  const submodulePaths = parent.supermodule.submodules ?? [];
3218
- const configuredSubmodules = Array.isArray(config.supermodule?.submodules) ? config.supermodule.submodules : [];
3219
- const needsRefresh = config.supermodule?.role !== "super-parent" || !sameStringList(configuredSubmodules, submodulePaths);
3358
+ const configuredSubmodules = Array.isArray(config2.supermodule?.submodules) ? config2.supermodule.submodules : [];
3359
+ const needsRefresh = config2.supermodule?.role !== "super-parent" || !sameStringList(configuredSubmodules, submodulePaths);
3220
3360
  if (!needsRefresh) {
3221
3361
  return null;
3222
3362
  }
3223
3363
  return {
3224
3364
  target: parent,
3225
- agents: config.agents,
3365
+ agents: config2.agents,
3226
3366
  submodulePaths
3227
3367
  };
3228
3368
  }
@@ -3233,8 +3373,8 @@ async function resolveChildTopologyRefreshes(targets, pending) {
3233
3373
  if (target.supermodule.role !== "submodule-child" || pendingLabels.has(target.label) || !await pathExists(target.configPath)) {
3234
3374
  continue;
3235
3375
  }
3236
- const config = await readConfigYaml(target.configPath);
3237
- if (config.supermodule?.role === "submodule-child" && config.supermodule.parent === target.supermodule.parent) {
3376
+ const config2 = await readConfigYaml(target.configPath);
3377
+ if (config2.supermodule?.role === "submodule-child" && config2.supermodule.parent === target.supermodule.parent) {
3238
3378
  continue;
3239
3379
  }
3240
3380
  refreshes.push({ target });
@@ -3256,7 +3396,7 @@ function withErrorHandling(fn) {
3256
3396
  try {
3257
3397
  await fn(opts);
3258
3398
  } catch (error) {
3259
- console.error(chalk8.red("Error:"), error instanceof Error ? error.message : error);
3399
+ console.error(chalk9.red("Error:"), error instanceof Error ? error.message : error);
3260
3400
  if (process.env.EC_DEBUG && error instanceof Error) {
3261
3401
  console.error(error.stack);
3262
3402
  }
@@ -3274,6 +3414,7 @@ program.command("init").description("Initialize easy-coding harness in current p
3274
3414
  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));
3275
3415
  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));
3276
3416
  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));
3417
+ program.command("config").description("Interactively configure project-level harness behavior").action(withErrorHandling(config));
3277
3418
  program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
3278
3419
  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));
3279
3420
  program.parse();