easy-coding-harness 0.10.0-beta.0 → 0.10.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
@@ -103,7 +103,8 @@ async function isDirectory(filePath) {
103
103
  }
104
104
 
105
105
  // src/utils/config-yaml.ts
106
- var CONFIG_SCHEMA_VERSION = 3;
106
+ var CONFIG_SCHEMA_VERSION = 5;
107
+ var DEFAULT_TDD_COVERAGE_THRESHOLD = 90;
107
108
  var APPROVAL_MODES = ["approve", "guard", "confirm", "auto"];
108
109
  var CONFIGURED_WORKFLOW_MODES = ["adaptive", "fast", "standard", "strict"];
109
110
  function createDefaultConfig(params) {
@@ -125,7 +126,9 @@ function createDefaultConfig(params) {
125
126
  },
126
127
  behavior: {
127
128
  approval_mode: "guard",
128
- workflow_mode: "adaptive"
129
+ workflow_mode: "adaptive",
130
+ tdd_enabled: false,
131
+ tdd_coverage_threshold: DEFAULT_TDD_COVERAGE_THRESHOLD
129
132
  }
130
133
  };
131
134
  if (params.supermodule) {
@@ -179,31 +182,44 @@ function isApprovalMode(value) {
179
182
  function isConfiguredWorkflowMode(value) {
180
183
  return typeof value === "string" && CONFIGURED_WORKFLOW_MODES.includes(value);
181
184
  }
185
+ function isTddCoverageThreshold(value) {
186
+ return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 100;
187
+ }
182
188
  function resolveLegacyBehavior(config2) {
183
189
  const behavior = config2.behavior ?? {};
184
190
  const legacyLite = behavior.confirm_mode === "lite";
185
191
  const approvalMode = isApprovalMode(behavior.approval_mode) ? behavior.approval_mode : isApprovalMode(behavior.confirm_mode) ? behavior.confirm_mode : behavior.auto_mode === true ? "auto" : behavior.strict_confirm === true ? "approve" : "guard";
186
192
  const workflowMode = isConfiguredWorkflowMode(behavior.workflow_mode) ? behavior.workflow_mode : legacyLite ? "fast" : "adaptive";
187
- return { approvalMode, workflowMode };
193
+ const supportsTddThreshold = Number(config2.version) >= 4;
194
+ const supportsReadyTdd = Number(config2.version) >= CONFIG_SCHEMA_VERSION;
195
+ const tddEnabled = supportsReadyTdd && behavior.tdd_enabled === true;
196
+ const tddCoverageThreshold = supportsTddThreshold && isTddCoverageThreshold(behavior.tdd_coverage_threshold) ? behavior.tdd_coverage_threshold : DEFAULT_TDD_COVERAGE_THRESHOLD;
197
+ return { approvalMode, workflowMode, tddEnabled, tddCoverageThreshold };
188
198
  }
189
- async function setBehaviorModes(filePath, approvalMode, workflowMode) {
199
+ async function setBehaviorModes(filePath, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold) {
200
+ if (tddCoverageThreshold !== void 0 && !isTddCoverageThreshold(tddCoverageThreshold)) {
201
+ throw new Error("TDD coverage threshold must be an integer from 1 to 100.");
202
+ }
190
203
  return updateConfigYaml(filePath, (config2) => {
191
204
  const legacyBehavior = config2.behavior ?? {};
205
+ const resolvedBehavior = resolveLegacyBehavior(config2);
192
206
  const behavior = Object.fromEntries(
193
207
  Object.entries(legacyBehavior).filter(
194
- ([key]) => key !== "strict_confirm" && key !== "auto_mode" && key !== "confirm_mode" && key !== "approval_mode" && key !== "workflow_mode"
208
+ ([key]) => key !== "strict_confirm" && key !== "auto_mode" && key !== "confirm_mode" && key !== "approval_mode" && key !== "workflow_mode" && key !== "tdd_enabled" && key !== "tdd_coverage_threshold"
195
209
  )
196
210
  );
197
211
  behavior.approval_mode = approvalMode;
198
212
  behavior.workflow_mode = workflowMode;
213
+ behavior.tdd_enabled = tddEnabled ?? resolvedBehavior.tddEnabled;
214
+ behavior.tdd_coverage_threshold = tddCoverageThreshold ?? resolvedBehavior.tddCoverageThreshold;
199
215
  config2.behavior = behavior;
200
216
  config2.version = CONFIG_SCHEMA_VERSION;
201
217
  });
202
218
  }
203
219
  async function migrateBehaviorConfig(filePath) {
204
220
  const config2 = await readConfigYaml(filePath);
205
- const { approvalMode, workflowMode } = resolveLegacyBehavior(config2);
206
- return setBehaviorModes(filePath, approvalMode, workflowMode);
221
+ const { approvalMode, workflowMode, tddEnabled, tddCoverageThreshold } = resolveLegacyBehavior(config2);
222
+ return setBehaviorModes(filePath, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold);
207
223
  }
208
224
  async function ensureProjectId(filePath) {
209
225
  let projectId = "";
@@ -242,6 +258,9 @@ var SPEC_DIR = "spec";
242
258
  var MAIN_SPEC_DIR = "main";
243
259
  var DEV_SPEC_DIR = "dev";
244
260
  var TEMPLATES_DIR = "templates";
261
+ var TOOLS_DIR = "tools";
262
+ var TDD_DIR = "tdd";
263
+ var TDD_READINESS_FILE = "readiness.json";
245
264
  var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
246
265
  var HOOK_BYTECODE_GITIGNORE_ENTRY = "__pycache__/";
247
266
  var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
@@ -659,8 +678,17 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
659
678
  await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
660
679
  await writeMemoryScaffold(easyCodingDir);
661
680
  await writeTemplatesScaffold(easyCodingDir);
681
+ await writeToolsScaffold(easyCodingDir);
662
682
  return projectId;
663
683
  }
684
+ async function writeToolsScaffold(easyCodingDir) {
685
+ const toolsDir = path6.join(easyCodingDir, TOOLS_DIR);
686
+ await ensureDir(toolsDir);
687
+ for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
688
+ const src = getTemplatePath("runtime", "tools", file);
689
+ await writeTextFile(path6.join(toolsDir, file), await readTextFile(src));
690
+ }
691
+ }
664
692
  async function writeTemplatesScaffold(easyCodingDir) {
665
693
  const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
666
694
  await ensureDir(templatesDir);
@@ -816,6 +844,17 @@ function migrateTaskWorkflowState(task) {
816
844
  task.workflow_mode_legacy = true;
817
845
  changed = true;
818
846
  }
847
+ if (isActive && taskType !== "project-init" && typeof task.tdd_enabled !== "boolean") {
848
+ task.tdd_enabled = false;
849
+ task.tdd_coverage_threshold = DEFAULT_TDD_COVERAGE_THRESHOLD;
850
+ task.tdd_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
851
+ task.tdd_confirmed_by = "upgrade-migration";
852
+ changed = true;
853
+ }
854
+ if (task.tdd_enabled === false && "tdd_baselines" in task) {
855
+ task.tdd_baselines = void 0;
856
+ changed = true;
857
+ }
819
858
  return changed;
820
859
  }
821
860
  function migrateSessionBehavior(session) {
@@ -2515,6 +2554,7 @@ function addRuntimeClearEntries(plan, cwd) {
2515
2554
  path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
2516
2555
  path16.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
2517
2556
  path16.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
2557
+ path16.join(cwd, EASY_CODING_DIR, TOOLS_DIR),
2518
2558
  path16.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
2519
2559
  ])
2520
2560
  ];
@@ -2737,8 +2777,8 @@ function renderTargetPlans(targetPlans) {
2737
2777
  }
2738
2778
 
2739
2779
  // src/commands/config.ts
2740
- import path18 from "path";
2741
- import { cancel as cancel4, confirm as confirm3, outro as outro3, select } from "@clack/prompts";
2780
+ import path19 from "path";
2781
+ import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
2742
2782
  import chalk4 from "chalk";
2743
2783
 
2744
2784
  // src/utils/compare-versions.ts
@@ -2816,10 +2856,169 @@ async function checkForUpgrade(cwd) {
2816
2856
  }
2817
2857
  }
2818
2858
 
2859
+ // src/utils/tdd-readiness.ts
2860
+ import { createHash as createHash2 } from "crypto";
2861
+ import { readFile as readFile4, readdir as readdir5, realpath as realpath2 } from "fs/promises";
2862
+ import path18 from "path";
2863
+ var TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1";
2864
+ var TDD_READINESS_SCOPE = "changed-production-lines";
2865
+ var TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA";
2866
+ var TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD";
2867
+ var COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py";
2868
+ var JAVA_BUILD_FILE_NAMES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
2869
+ var GITLAB_CI_ENTRY_FILES = /* @__PURE__ */ new Set([".gitlab-ci.yml", ".gitlab-ci.yaml"]);
2870
+ function readinessPath(root) {
2871
+ return path18.join(root, EASY_CODING_DIR, TDD_DIR, TDD_READINESS_FILE);
2872
+ }
2873
+ function parseFileRecords(value, field, reasons) {
2874
+ if (!Array.isArray(value) || value.length === 0) {
2875
+ reasons.push(`${field} must contain at least one file`);
2876
+ return [];
2877
+ }
2878
+ const records = [];
2879
+ for (const item of value) {
2880
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
2881
+ reasons.push(`${field} contains an invalid record`);
2882
+ continue;
2883
+ }
2884
+ const record = item;
2885
+ if (typeof record.path !== "string" || !record.path.trim() || path18.isAbsolute(record.path) || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
2886
+ reasons.push(`${field} contains an invalid path or SHA-256`);
2887
+ continue;
2888
+ }
2889
+ records.push({ path: record.path, sha256: record.sha256 });
2890
+ }
2891
+ return records;
2892
+ }
2893
+ function usesRequiredGateVariables(command) {
2894
+ const normalized = command.replaceAll(`\${${TDD_BASE_VARIABLE}}`, `$${TDD_BASE_VARIABLE}`).replaceAll(`\${${TDD_THRESHOLD_VARIABLE}}`, `$${TDD_THRESHOLD_VARIABLE}`);
2895
+ return new RegExp(`--base\\s+['"]?\\$${TDD_BASE_VARIABLE}(?:['"]|\\s|$)`).test(normalized) && new RegExp(`--threshold\\s+['"]?\\$${TDD_THRESHOLD_VARIABLE}(?:['"]|\\s|$)`).test(normalized);
2896
+ }
2897
+ function isSafeReportPattern(value) {
2898
+ const normalized = value.replaceAll("\\", "/");
2899
+ return !path18.isAbsolute(value) && !normalized.split("/").includes("..");
2900
+ }
2901
+ function activeCiContent(contents) {
2902
+ return contents.join("\n").split("\n").map((line) => line.replace(/^\s*#.*$/, "").replace(/\s+#.*$/, "")).join("\n");
2903
+ }
2904
+ async function validateFiles(root, records, reasons) {
2905
+ const contents = [];
2906
+ const resolvedRoot = await realpath2(root);
2907
+ for (const record of records) {
2908
+ const absolute = path18.resolve(root, record.path);
2909
+ try {
2910
+ const resolved = await realpath2(absolute);
2911
+ if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path18.sep}`)) {
2912
+ reasons.push(`readiness file escapes project root: ${record.path}`);
2913
+ continue;
2914
+ }
2915
+ const content = await readFile4(resolved);
2916
+ const digest = createHash2("sha256").update(content).digest("hex");
2917
+ if (digest !== record.sha256) reasons.push(`readiness file changed: ${record.path}`);
2918
+ contents.push(content.toString("utf8"));
2919
+ } catch {
2920
+ reasons.push(`readiness file is missing or unreadable: ${record.path}`);
2921
+ }
2922
+ }
2923
+ return contents;
2924
+ }
2925
+ async function inspectTddReadiness(root) {
2926
+ const manifestPath2 = readinessPath(root);
2927
+ if (!await pathExists(manifestPath2)) {
2928
+ return { status: "needs_init", reasons: ["TDD readiness receipt is missing"], manifestPath: manifestPath2 };
2929
+ }
2930
+ let manifest;
2931
+ try {
2932
+ manifest = JSON.parse(await readFile4(manifestPath2, "utf8"));
2933
+ } catch {
2934
+ return { status: "needs_init", reasons: ["TDD readiness receipt is invalid"], manifestPath: manifestPath2 };
2935
+ }
2936
+ const reasons = [];
2937
+ if (manifest.schema !== TDD_READINESS_SCHEMA) reasons.push("unsupported readiness schema");
2938
+ if (manifest.provider !== "gitlab") reasons.push("readiness provider must be gitlab");
2939
+ if (manifest.coverage_scope !== TDD_READINESS_SCOPE) {
2940
+ reasons.push("coverage scope must be changed-production-lines");
2941
+ }
2942
+ if (manifest.historical_coverage_required !== false) {
2943
+ reasons.push("historical coverage must remain disabled");
2944
+ }
2945
+ if (!Array.isArray(manifest.coverage_report_patterns) || manifest.coverage_report_patterns.length === 0 || manifest.coverage_report_patterns.some(
2946
+ (item) => typeof item !== "string" || !item.trim() || !isSafeReportPattern(item)
2947
+ )) {
2948
+ reasons.push("coverage_report_patterns must contain safe project-relative report patterns");
2949
+ }
2950
+ if (typeof manifest.changed_line_gate_command !== "string" || !manifest.changed_line_gate_command.includes(COVERAGE_TOOL_PATH)) {
2951
+ reasons.push("changed-line coverage gate command is missing");
2952
+ } else if (!usesRequiredGateVariables(manifest.changed_line_gate_command)) {
2953
+ reasons.push("changed-line coverage gate must use the task baseline and threshold variables");
2954
+ }
2955
+ const buildFiles = parseFileRecords(manifest.build_files, "build_files", reasons);
2956
+ const ciFiles = parseFileRecords(manifest.ci_files, "ci_files", reasons);
2957
+ const toolFiles = parseFileRecords(manifest.tool_files, "tool_files", reasons);
2958
+ if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(path18.basename(record.path)))) {
2959
+ reasons.push("build_files must include a Maven or Gradle Java build file");
2960
+ }
2961
+ if (!ciFiles.some((record) => GITLAB_CI_ENTRY_FILES.has(record.path.replaceAll("\\", "/")))) {
2962
+ reasons.push("ci_files must include the project-root GitLab CI entry file");
2963
+ }
2964
+ if (!toolFiles.some((record) => record.path.replaceAll("\\", "/") === COVERAGE_TOOL_PATH)) {
2965
+ reasons.push(`tool_files must include ${COVERAGE_TOOL_PATH}`);
2966
+ }
2967
+ const buildContents = await validateFiles(root, buildFiles, reasons);
2968
+ const ciContents = await validateFiles(root, ciFiles, reasons);
2969
+ await validateFiles(root, toolFiles, reasons);
2970
+ if (!buildContents.some((content) => /jacoco/i.test(content))) {
2971
+ reasons.push("build files do not configure JaCoCo");
2972
+ }
2973
+ const combinedCi = activeCiContent(ciContents);
2974
+ for (const marker of [
2975
+ "jacoco",
2976
+ "artifacts",
2977
+ COVERAGE_TOOL_PATH,
2978
+ TDD_BASE_VARIABLE,
2979
+ TDD_THRESHOLD_VARIABLE
2980
+ ]) {
2981
+ if (!combinedCi.toLowerCase().includes(marker.toLowerCase())) {
2982
+ reasons.push(`CI files do not contain required marker: ${marker}`);
2983
+ }
2984
+ }
2985
+ if (!usesRequiredGateVariables(combinedCi)) {
2986
+ reasons.push("CI changed-line gate must use the task baseline and threshold variables");
2987
+ }
2988
+ if (!/(?:^|\n)\s*stage\s*:\s*['"]?test['"]?\s*(?:#.*)?(?:\n|$)/i.test(combinedCi)) {
2989
+ reasons.push("CI files do not declare a TEST-stage job");
2990
+ }
2991
+ return {
2992
+ status: reasons.length === 0 ? "ready" : "needs_init",
2993
+ reasons: [...new Set(reasons)],
2994
+ manifestPath: manifestPath2
2995
+ };
2996
+ }
2997
+ async function disableUnreadySessionTddOverrides(root) {
2998
+ if ((await inspectTddReadiness(root)).status === "ready") return 0;
2999
+ const sessionsDir = path18.join(root, EASY_CODING_DIR, SESSIONS_DIR);
3000
+ if (!await pathExists(sessionsDir)) return 0;
3001
+ let updated = 0;
3002
+ for (const entry of await readdir5(sessionsDir, { withFileTypes: true })) {
3003
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3004
+ const filePath = path18.join(sessionsDir, entry.name);
3005
+ try {
3006
+ const session = JSON.parse(await readFile4(filePath, "utf8"));
3007
+ if (session.tdd_enabled !== true) continue;
3008
+ session.tdd_enabled = false;
3009
+ await writeTextFile(filePath, `${JSON.stringify(session, null, 2)}
3010
+ `);
3011
+ updated += 1;
3012
+ } catch {
3013
+ }
3014
+ }
3015
+ return updated;
3016
+ }
3017
+
2819
3018
  // src/commands/config.ts
2820
3019
  async function config() {
2821
3020
  renderBanner();
2822
- const configPath2 = path18.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
3021
+ const configPath2 = path19.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
2823
3022
  if (!await pathExists(configPath2)) {
2824
3023
  throw new Error("No easy-coding harness found in this project.");
2825
3024
  }
@@ -2901,33 +3100,83 @@ async function config() {
2901
3100
  cancel4("Configuration cancelled.");
2902
3101
  return;
2903
3102
  }
3103
+ const tddEnabled = await select({
3104
+ message: `Enable Java TDD for this project (current: ${current.tddEnabled ? "enabled" : "disabled"})`,
3105
+ initialValue: current.tddEnabled,
3106
+ options: [
3107
+ { value: false, label: "disabled \u2014 preserve current test depth (default)" },
3108
+ { value: true, label: "enabled \u2014 require TDD evidence and changed-line coverage" }
3109
+ ]
3110
+ });
3111
+ if (typeof tddEnabled === "symbol") {
3112
+ cancel4("Configuration cancelled.");
3113
+ return;
3114
+ }
3115
+ if (tddEnabled) {
3116
+ const readiness = await inspectTddReadiness(process.cwd());
3117
+ if (readiness.status !== "ready") {
3118
+ cancel4(
3119
+ `TDD was not enabled. Run ec-tdd-init first: ${readiness.reasons.join("; ")}. No project modes were changed.`
3120
+ );
3121
+ return;
3122
+ }
3123
+ }
3124
+ let tddCoverageThreshold = current.tddCoverageThreshold;
3125
+ if (tddEnabled) {
3126
+ const thresholdInput = await text({
3127
+ message: "Minimum changed-production-line coverage percentage",
3128
+ initialValue: String(current.tddCoverageThreshold),
3129
+ validate(value) {
3130
+ const parsed = Number(value);
3131
+ return isTddCoverageThreshold(parsed) ? void 0 : "Enter an integer from 1 to 100.";
3132
+ }
3133
+ });
3134
+ if (typeof thresholdInput === "symbol") {
3135
+ cancel4("Configuration cancelled.");
3136
+ return;
3137
+ }
3138
+ tddCoverageThreshold = Number(thresholdInput);
3139
+ }
2904
3140
  const shouldSave = await confirm3({
2905
- message: `Set behavior.approval_mode to ${approvalMode} and behavior.workflow_mode to ${workflowMode}?`,
3141
+ message: `Set approval=${approvalMode}, workflow=${workflowMode}, TDD=${tddEnabled ? `enabled (${tddCoverageThreshold}%)` : "disabled"}?`,
2906
3142
  initialValue: true
2907
3143
  });
2908
3144
  if (typeof shouldSave === "symbol" || !shouldSave) {
2909
3145
  cancel4("Configuration cancelled.");
2910
3146
  return;
2911
3147
  }
2912
- await setBehaviorModes(configPath2, approvalMode, workflowMode);
2913
- outro3(chalk4.green(`Project modes updated: approval=${approvalMode}, workflow=${workflowMode}.`));
3148
+ if (tddEnabled) {
3149
+ const readiness = await inspectTddReadiness(process.cwd());
3150
+ if (readiness.status !== "ready") {
3151
+ cancel4(
3152
+ `TDD was not enabled because readiness changed before save: ${readiness.reasons.join("; ")}. No project modes were changed.`
3153
+ );
3154
+ return;
3155
+ }
3156
+ }
3157
+ await setBehaviorModes(configPath2, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold);
3158
+ outro3(
3159
+ chalk4.green(
3160
+ `Project modes updated: approval=${approvalMode}, workflow=${workflowMode}, TDD=${tddEnabled ? `${tddCoverageThreshold}%` : "off"}.`
3161
+ )
3162
+ );
2914
3163
  }
2915
3164
 
2916
3165
  // src/commands/init.ts
2917
- import path20 from "path";
3166
+ import path21 from "path";
2918
3167
  import { note, outro as outro4 } from "@clack/prompts";
2919
3168
  import chalk5 from "chalk";
2920
3169
 
2921
3170
  // src/utils/install-state.ts
2922
- import { readdir as readdir5 } from "fs/promises";
2923
- import path19 from "path";
3171
+ import { readdir as readdir6 } from "fs/promises";
3172
+ import path20 from "path";
2924
3173
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
2925
3174
  async function detectEasyCodingInstallState(cwd) {
2926
- const easyCodingDir = path19.join(cwd, EASY_CODING_DIR);
3175
+ const easyCodingDir = path20.join(cwd, EASY_CODING_DIR);
2927
3176
  if (!await pathExists(easyCodingDir)) {
2928
3177
  return { kind: "fresh", easyCodingDir };
2929
3178
  }
2930
- const configPath2 = path19.join(easyCodingDir, CONFIG_FILE);
3179
+ const configPath2 = path20.join(easyCodingDir, CONFIG_FILE);
2931
3180
  if (await pathExists(configPath2)) {
2932
3181
  return { kind: "installed", easyCodingDir, configPath: configPath2 };
2933
3182
  }
@@ -2945,17 +3194,17 @@ async function detectEasyCodingInstallState(cwd) {
2945
3194
  async function detectLegacyAssets(easyCodingDir) {
2946
3195
  const assets = [];
2947
3196
  for (const file of LEGACY_ROOT_FILES) {
2948
- if (await pathExists(path19.join(easyCodingDir, file))) {
3197
+ if (await pathExists(path20.join(easyCodingDir, file))) {
2949
3198
  assets.push(relativeEasyCodingPath(file));
2950
3199
  }
2951
3200
  }
2952
- if (await pathExists(path19.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3201
+ if (await pathExists(path20.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
2953
3202
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
2954
3203
  }
2955
- const shortMemoryFiles = await listMarkdownFiles(path19.join(easyCodingDir, "memory", "short"));
3204
+ const shortMemoryFiles = await listMarkdownFiles(path20.join(easyCodingDir, "memory", "short"));
2956
3205
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
2957
3206
  for (const dir of ["spec", "prototype"]) {
2958
- if (await hasAnyDirectoryEntry(path19.join(easyCodingDir, dir))) {
3207
+ if (await hasAnyDirectoryEntry(path20.join(easyCodingDir, dir))) {
2959
3208
  assets.push(relativeEasyCodingPath(dir));
2960
3209
  }
2961
3210
  }
@@ -2965,23 +3214,23 @@ async function listMarkdownFiles(dir) {
2965
3214
  if (!await isDirectory(dir)) {
2966
3215
  return [];
2967
3216
  }
2968
- const entries = await readdir5(dir, { withFileTypes: true });
3217
+ const entries = await readdir6(dir, { withFileTypes: true });
2969
3218
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
2970
3219
  }
2971
3220
  async function hasAnyDirectoryEntry(dir) {
2972
3221
  if (!await isDirectory(dir)) {
2973
3222
  return false;
2974
3223
  }
2975
- return (await readdir5(dir)).length > 0;
3224
+ return (await readdir6(dir)).length > 0;
2976
3225
  }
2977
3226
  function relativeEasyCodingPath(...segments) {
2978
- return path19.posix.join(EASY_CODING_DIR, ...segments);
3227
+ return path20.posix.join(EASY_CODING_DIR, ...segments);
2979
3228
  }
2980
3229
  function relativeConfigPath() {
2981
- return path19.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3230
+ return path20.posix.join(EASY_CODING_DIR, CONFIG_FILE);
2982
3231
  }
2983
3232
  function relativeProjectInitTaskPath() {
2984
- return path19.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
3233
+ return path20.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
2985
3234
  }
2986
3235
 
2987
3236
  // src/commands/init.ts
@@ -3062,7 +3311,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
3062
3311
  if (!targetSubmodulePaths.has(entry.path)) {
3063
3312
  continue;
3064
3313
  }
3065
- const dir = path20.join(cwd, entry.path);
3314
+ const dir = path21.join(cwd, entry.path);
3066
3315
  targets.push(
3067
3316
  await targetFromState(dir, entry.path, "submodule-child", {
3068
3317
  parent: toPosixRelative2(dir, cwd)
@@ -3109,24 +3358,24 @@ function contextFromState(role, installState) {
3109
3358
  };
3110
3359
  }
3111
3360
  function toPosixRelative2(from, to) {
3112
- const relative = path20.relative(from, to);
3113
- return relative ? relative.split(path20.sep).join("/") : ".";
3361
+ const relative = path21.relative(from, to);
3362
+ return relative ? relative.split(path21.sep).join("/") : ".";
3114
3363
  }
3115
3364
  async function resolveInitPlatforms(cwd, opts, parentInstalled) {
3116
3365
  if (opts.agent || !parentInstalled) {
3117
3366
  return resolvePlatforms(opts, ["claude-code"]);
3118
3367
  }
3119
- const config2 = await readConfigYaml(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
3368
+ const config2 = await readConfigYaml(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
3120
3369
  if (Array.isArray(config2.agents) && config2.agents.length > 0) {
3121
3370
  return config2.agents;
3122
3371
  }
3123
3372
  return resolvePlatforms(opts, ["claude-code"]);
3124
3373
  }
3125
3374
  async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
3126
- if (!await pathExists(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3375
+ if (!await pathExists(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3127
3376
  return;
3128
3377
  }
3129
- const config2 = parentTarget2.installed ? await readConfigYaml(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
3378
+ const config2 = parentTarget2.installed ? await readConfigYaml(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
3130
3379
  const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
3131
3380
  await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
3132
3381
  }
@@ -3135,7 +3384,7 @@ async function refreshInstalledChildTopologies(targets) {
3135
3384
  if (!target.installed || target.context.role !== "submodule-child") {
3136
3385
  continue;
3137
3386
  }
3138
- const configPath2 = path20.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3387
+ const configPath2 = path21.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3139
3388
  if (!await pathExists(configPath2)) {
3140
3389
  continue;
3141
3390
  }
@@ -3144,12 +3393,12 @@ async function refreshInstalledChildTopologies(targets) {
3144
3393
  }
3145
3394
 
3146
3395
  // src/commands/status.ts
3147
- import path22 from "path";
3396
+ import path23 from "path";
3148
3397
  import chalk6 from "chalk";
3149
3398
 
3150
3399
  // src/utils/session.ts
3151
- import { readdir as readdir6, unlink } from "fs/promises";
3152
- import path21 from "path";
3400
+ import { readdir as readdir7, unlink } from "fs/promises";
3401
+ import path22 from "path";
3153
3402
  var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1e3;
3154
3403
  function parseSessionFile(content) {
3155
3404
  try {
@@ -3159,7 +3408,7 @@ function parseSessionFile(content) {
3159
3408
  }
3160
3409
  }
3161
3410
  function getSessionDir(cwd) {
3162
- return path21.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3411
+ return path22.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3163
3412
  }
3164
3413
  async function listSessionFiles(cwd) {
3165
3414
  const dir = getSessionDir(cwd);
@@ -3167,11 +3416,11 @@ async function listSessionFiles(cwd) {
3167
3416
  return [];
3168
3417
  }
3169
3418
  const entries = [];
3170
- for (const name of await readdir6(dir)) {
3419
+ for (const name of await readdir7(dir)) {
3171
3420
  if (!name.endsWith(".json")) {
3172
3421
  continue;
3173
3422
  }
3174
- const filePath = path21.join(dir, name);
3423
+ const filePath = path22.join(dir, name);
3175
3424
  const content = await readTextIfExists(filePath);
3176
3425
  if (!content) {
3177
3426
  continue;
@@ -3193,7 +3442,7 @@ async function listSessionFiles(cwd) {
3193
3442
  async function status() {
3194
3443
  renderBanner();
3195
3444
  const cwd = process.cwd();
3196
- const configPath2 = path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3445
+ const configPath2 = path23.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3197
3446
  if (!await pathExists(configPath2)) {
3198
3447
  throw new Error("No easy-coding harness found in this project.");
3199
3448
  }
@@ -3203,6 +3452,7 @@ async function status() {
3203
3452
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
3204
3453
  const sessions = await listSessionFiles(cwd);
3205
3454
  const versionRelation = compareVersions(config2.harness_version, VERSION);
3455
+ const tddReadiness = await inspectTddReadiness(cwd);
3206
3456
  console.log(chalk6.bold("Harness"));
3207
3457
  console.log(` version: ${config2.harness_version}`);
3208
3458
  console.log(` cli: ${VERSION}`);
@@ -3218,14 +3468,28 @@ async function status() {
3218
3468
  const migratedBehavior = resolveLegacyBehavior(config2);
3219
3469
  const projectApprovalMode = isApprovalMode(config2.behavior?.approval_mode) ? config2.behavior.approval_mode : migratedBehavior.approvalMode;
3220
3470
  const projectWorkflowMode = isConfiguredWorkflowMode(config2.behavior?.workflow_mode) ? config2.behavior.workflow_mode : migratedBehavior.workflowMode;
3471
+ const projectTddEnabled = migratedBehavior.tddEnabled;
3472
+ const projectTddCoverageThreshold = migratedBehavior.tddCoverageThreshold;
3221
3473
  console.log(` approval_mode: ${projectApprovalMode}`);
3222
3474
  console.log(` workflow_mode: ${projectWorkflowMode}`);
3475
+ console.log(` tdd_enabled: ${projectTddEnabled}`);
3476
+ console.log(` tdd_coverage_threshold: ${projectTddCoverageThreshold}`);
3477
+ console.log(` tdd_readiness: ${tddReadiness.status}`);
3478
+ if (tddReadiness.status !== "ready") {
3479
+ console.log(` tdd_readiness_reasons: ${tddReadiness.reasons.join("; ")}`);
3480
+ }
3223
3481
  console.log("");
3224
3482
  console.log(chalk6.bold("Sessions"));
3225
3483
  console.log(` project_approval_mode: ${projectApprovalMode}`);
3226
3484
  console.log(` project_workflow_mode: ${projectWorkflowMode}`);
3485
+ console.log(` project_tdd_enabled: ${projectTddEnabled}`);
3486
+ console.log(` project_tdd_coverage_threshold: ${projectTddCoverageThreshold}`);
3227
3487
  console.log(` effective_approval_mode: ${projectApprovalMode} (without a session override)`);
3228
3488
  console.log(` configured_workflow_mode: ${projectWorkflowMode} (without a session override)`);
3489
+ console.log(` effective_tdd_enabled: ${projectTddEnabled} (without a session override)`);
3490
+ console.log(
3491
+ ` effective_tdd_coverage_threshold: ${projectTddCoverageThreshold} (without a session override)`
3492
+ );
3229
3493
  if (sessions.length === 0) {
3230
3494
  console.log(" no session files");
3231
3495
  }
@@ -3236,13 +3500,21 @@ async function status() {
3236
3500
  );
3237
3501
  const sessionApprovalMode = session.approval_mode ?? (legacySessionMode === "lite" ? "guard" : legacySessionMode);
3238
3502
  const sessionWorkflowMode = session.workflow_mode ?? (legacySessionMode === "lite" ? "fast" : hasLegacySessionMode ? "adaptive" : void 0);
3503
+ const sessionTddEnabled = session.tdd_enabled;
3504
+ const sessionTddCoverageThreshold = session.tdd_coverage_threshold;
3239
3505
  console.log(` - ${key}`);
3240
3506
  console.log(` agent: ${session.agent ?? "legacy/unknown"}`);
3241
3507
  console.log(` source: ${session.session_source ?? "legacy"}`);
3242
3508
  console.log(` approval_mode: ${sessionApprovalMode ?? "project default"}`);
3243
3509
  console.log(` workflow_mode: ${sessionWorkflowMode ?? "project default"}`);
3510
+ console.log(` tdd_enabled: ${sessionTddEnabled ?? "project default"}`);
3511
+ console.log(` tdd_coverage_threshold: ${sessionTddCoverageThreshold ?? "project default"}`);
3244
3512
  console.log(` effective_approval_mode: ${sessionApprovalMode ?? projectApprovalMode}`);
3245
3513
  console.log(` configured_workflow_mode: ${sessionWorkflowMode ?? projectWorkflowMode}`);
3514
+ console.log(` effective_tdd_enabled: ${sessionTddEnabled ?? projectTddEnabled}`);
3515
+ console.log(
3516
+ ` effective_tdd_coverage_threshold: ${sessionTddCoverageThreshold ?? projectTddCoverageThreshold}`
3517
+ );
3246
3518
  console.log(
3247
3519
  ` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
3248
3520
  );
@@ -3258,6 +3530,10 @@ async function status() {
3258
3530
  console.log(
3259
3531
  ` task_workflow_mode: ${task.workflow_mode ?? task.workflow_mode_proposal?.selected_mode ?? "not resolved"}`
3260
3532
  );
3533
+ console.log(` task_tdd_enabled: ${task.tdd_enabled ?? "not frozen"}`);
3534
+ console.log(
3535
+ ` task_tdd_coverage_threshold: ${task.tdd_coverage_threshold ?? "not frozen"}`
3536
+ );
3261
3537
  console.log(` last_agent: ${task.last_agent}`);
3262
3538
  } else {
3263
3539
  console.log(` current_task: ${session.current_task} (task.json missing)`);
@@ -3309,7 +3585,7 @@ async function update(opts) {
3309
3585
  }
3310
3586
 
3311
3587
  // src/commands/upgrade.ts
3312
- import path23 from "path";
3588
+ import path24 from "path";
3313
3589
  import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
3314
3590
  import chalk8 from "chalk";
3315
3591
  var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
@@ -3371,8 +3647,8 @@ async function upgrade(opts) {
3371
3647
  ].filter(Boolean) : [],
3372
3648
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
3373
3649
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
3374
- "Will migrate legacy confirmation settings to behavior.approval_mode and behavior.workflow_mode.",
3375
- "Will migrate legacy workflow stage metadata; memory content, spec, and project knowledge files remain untouched."
3650
+ "Will migrate behavior config to schema 5 and disable unready project/session TDD settings.",
3651
+ "Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
3376
3652
  ].join("\n");
3377
3653
  if (opts.dryRun) {
3378
3654
  console.log(summary);
@@ -3389,6 +3665,7 @@ async function upgrade(opts) {
3389
3665
  }
3390
3666
  }
3391
3667
  for (const { target, config: config2 } of pending) {
3668
+ const beta1ProjectTddRequested = Number(config2.version) === 4 && config2.behavior?.tdd_enabled === true;
3392
3669
  const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
3393
3670
  supermodule: target.supermodule
3394
3671
  });
@@ -3405,6 +3682,14 @@ async function upgrade(opts) {
3405
3682
  await ensureHookBytecodeIgnored(target.dir);
3406
3683
  await migrateLegacyWorkflowState(target.dir);
3407
3684
  await migrateBehaviorConfig(target.configPath);
3685
+ const disabledSessionTddOverrides = await disableUnreadySessionTddOverrides(target.dir);
3686
+ if (beta1ProjectTddRequested || disabledSessionTddOverrides > 0) {
3687
+ console.log(
3688
+ chalk8.yellow(
3689
+ `${target.label}: TDD remains off until ec-tdd-init succeeds (project=${beta1ProjectTddRequested ? "disabled" : "unchanged"}, sessions_disabled=${disabledSessionTddOverrides}).`
3690
+ )
3691
+ );
3692
+ }
3408
3693
  await updateHarnessVersion(target.configPath, VERSION);
3409
3694
  await updateSupermoduleConfig(target.configPath, target.supermodule);
3410
3695
  await setPendingInitSince(target.dir, VERSION);
@@ -3460,7 +3745,7 @@ async function needsHookConfigRefresh(target, config2) {
3460
3745
  const manifest = await readInstallManifest(target.dir);
3461
3746
  for (const agent of config2.agents) {
3462
3747
  const meta = resolvePlatformMeta(target.dir, agent);
3463
- const configPath2 = path23.join(target.dir, meta.hookConfigFile);
3748
+ const configPath2 = path24.join(target.dir, meta.hookConfigFile);
3464
3749
  const content = await readTextIfExists(configPath2);
3465
3750
  if (content === null) {
3466
3751
  return true;
@@ -3600,7 +3885,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
3600
3885
  return true;
3601
3886
  }
3602
3887
  return pathAliases(
3603
- normalizePathForHookComparison(path23.resolve(cwd, relativeHookPath))
3888
+ normalizePathForHookComparison(path24.resolve(cwd, relativeHookPath))
3604
3889
  ).includes(normalizedHookPath);
3605
3890
  });
3606
3891
  }