easy-coding-harness 0.10.0-beta.1 → 0.10.0-beta.3

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/CHANGELOG.md CHANGED
@@ -6,6 +6,34 @@
6
6
  - `y`:常规功能升级
7
7
  - `z`:日常 bug 修复
8
8
 
9
+ ## 0.10.0-beta.3
10
+
11
+ - TDD 业务任务的 `VERIFICATION` 门禁收口为本地证据:每个仓库(Canonical 场景下每个
12
+ source task)必须有通过的本地单测,以及达到冻结阈值的本地 JaCoCo changed-line
13
+ coverage;不再要求远程 GitLab pipeline URL、job identity 或成功状态。
14
+ - `ec-tdd-init` 继续在 TDD 关闭态生成并校验参数化 GitLab TEST-stage job、构建配置、
15
+ JaCoCo XML 与 readiness receipt,但远程 CI 只作为项目自动化能力,不再触发 Harness
16
+ 中间提交、推送或等待远程结果。
17
+ - beta.1/beta.2 任务中已有的 `coverage_scope=gitlab` 记录保持原样并从新验收集合中忽略,
18
+ 因此 pending/failed 远程记录不会阻塞升级后的本地门禁;本地 baseline、阈值、报告
19
+ 指纹及 TDD 生命周期/review 约束保持不变。
20
+
21
+ ## 0.10.0-beta.2
22
+
23
+ - 新增 `ec-tdd-init`,在 TDD 关闭态初始化或刷新 Java 单测执行、JaCoCo XML、GitLab
24
+ TEST job、报告 artifact 与 changed-line coverage gate;初始化只建设基础设施,不批量补
25
+ 存量业务单测,也不要求仓库全量覆盖率。
26
+ - 新增 `.easy-coding/tools/easy_coding_tdd_readiness.py` 与覆盖构建、GitLab CI、coverage
27
+ 工具的文件指纹 receipt;项目级 `easy-coding config`、session `set-tdd` 和 ANALYSIS →
28
+ IMPLEMENT 均在开启/冻结 TDD 前机械校验 readiness,缺失或漂移时只允许先初始化或
29
+ 保持关闭。TDD 关闭的普通 hook 不扫描这些文件。
30
+ - `tdd-init` 作为专用代码任务始终冻结 `tdd_enabled=false`,即使遗留项目/session 或暂停
31
+ 任务请求开启 TDD,也可正常创建和修改 CI,消除“开启 TDD 后又依赖尚未创建 CI”的
32
+ 循环阻塞;初始化完成后仍需用户显式开启 TDD。
33
+ - 配置 schema 升至 5;升级时没有 readiness 的 beta.1 项目与 session TDD 请求迁移为
34
+ 关闭并保留阈值,已冻结活动任务合同不被静默改写。后续业务任务仍只验收相对冻结
35
+ baseline 的新增/修改生产代码行,默认门槛 90%、测试设计目标接近 100%。
36
+
9
37
  ## 0.10.0-beta.1
10
38
 
11
39
  - 新增默认关闭的 Java TDD 模式,支持项目级 CLI 配置与 session 覆盖;覆盖率阈值默认
package/README.md CHANGED
@@ -86,7 +86,7 @@ any stage --[user abort via ec-task-close]--> CLOSED
86
86
  ANALYSIS → IMPLEMENT 确认一次,随后各阶段在质量门禁通过后自动推进,`auto` 从开始即
87
87
  自动推进。
88
88
  - 工作流模式优先级为 session 覆盖 > 项目 `behavior.workflow_mode` > `adaptive`。Adaptive 在 ANALYSIS 结束时根据风险解析、展示并冻结为 `fast`、`standard` 或 `strict`,用户可在风险下限之上调整。
89
- - Java TDD 默认关闭;优先级为 session 覆盖 > 项目配置 > `false/90%`。开启后在 ANALYSIS → IMPLEMENT 冻结开关与阈值,执行 RED/GREEN/REFACTOR(纯重构使用 characterization GREEN → GREEN),并以同一工具执行本地与 GitLab TEST-stage 差异覆盖率门禁。关闭时不扫描 CI/JaCoCo、不增加命令或提高原工作流验收深度。
89
+ - Java TDD 默认关闭;优先级为 session 覆盖 > 项目配置 > `false/90%`。首次开启前必须运行 `ec-tdd-init`,只建设 JUnit/JaCoCo/GitLab 增量覆盖率基础设施,不补存量业务单测;readiness 通过后才允许显式开启。开启后在 ANALYSIS → IMPLEMENT 冻结开关、baseline 与阈值,只验收本任务新增/修改生产代码行,执行 RED/GREEN/REFACTOR(纯重构使用 characterization GREEN → GREEN),并要求本地单测通过、本地差异覆盖率达到冻结阈值。GitLab TEST-stage job 仍会生成,但远程 pipeline 结果不属于 Harness 验收证据,也不会触发中间提交推送。关闭时普通任务不扫描 CI/JaCoCo、不增加命令或提高原工作流验收深度。
90
90
  - 所有新代码任务都完整进入 REVIEW;不同工作流模式只调整各状态内部的上下文加载、执行主体、审查独立性、验证范围和记忆深度,不绕过状态或证据门禁。
91
91
  - 显式 `doc` / `analysis` / `report` 只读任务不生成 `test-strategy.md`;展示完整报告后按生效模式进入 COMPLETE,不执行 REVIEW、VERIFICATION 或 MEMORY,也不写任务记忆。
92
92
  - `VERIFICATION` 是验证硬门控:Fast 运行最小充分检查,Standard 运行受影响范围检查,
@@ -132,7 +132,7 @@ Canonical Spec 是只读设计源;`.easy-coding/tasks/<task-id>/` 下的 `dev-
132
132
  | `easy-coding add-agent` | 给已接入项目追加 Claude Code、Codex 或 Qoder 支持;supermodule 父仓可按已初始化子仓分层追加 |
133
133
  | `easy-coding upgrade` | CLI 升级后同步项目内生成文件,生成区覆盖,用户资产保留;supermodule 父仓会同步升级已初始化子仓 |
134
134
  | `easy-coding update` | 更新全局 CLI 到最新发布版 |
135
- | `easy-coding config` | 交互修改当前项目的 Approval、Workflow、Java TDD 与覆盖率阈值 |
135
+ | `easy-coding config` | 交互修改当前项目的 Approval、Workflow、Java TDD 与覆盖率阈值;开启 TDD 前要求 readiness |
136
136
  | `easy-coding status` | 查看已安装平台、harness 版本、当前任务状态 |
137
137
  | `easy-coding clear` | 移除 harness 安装物,保留 tasks、spec、memory、project.yaml 等用户资产;supermodule 父仓支持交互选择、`--submodules` 和 `--no-submodules` |
138
138
 
@@ -151,6 +151,7 @@ Canonical Spec 是只读设计源;`.easy-coding/tasks/<task-id>/` 下的 `dev-
151
151
  | `ec-memory` | 写短期记忆,并在超过阈值时沉淀长期记忆 |
152
152
  | `ec-task-management` | 任务面板:查看、创建、选择、恢复、交接任务 |
153
153
  | `ec-config` | 只读查看或显式修改项目/session 的 Approval、Workflow、TDD 与阈值 |
154
+ | `ec-tdd-init` | 在 TDD 关闭态初始化/刷新 Java changed-line coverage 基础设施,不补存量单测 |
154
155
  | `ec-task-close` | 用户主动中断任务并关闭 |
155
156
  | `ec-no-harness` | 当前会话仅旁路 Easy Coding Harness,使用原生 Agent 能力 |
156
157
  | `ec-git` | 约束 git diff、commit、push、跨仓库提交等交付动作 |
@@ -187,8 +188,11 @@ easy-coding upgrade
187
188
  `behavior.approval_mode` 与 `behavior.workflow_mode`;旧 `lite` 映射为 `guard + fast`。
188
189
  项目级模式用 `easy-coding config` 修改(要求
189
190
  项目 Harness 与 CLI 版本完全一致,否则先执行 `easy-coding upgrade` 或更新 CLI);当前
190
- session 临时覆盖统一通过 `ec-config` 对话修改。升级到 0.10.0-beta.1 时配置 schema 升至
191
- 4,旧项目、会话和在途任务均迁移为 TDD 关闭;同时部署共享 Java 差异覆盖率工具。
191
+ session 临时覆盖统一通过 `ec-config` 对话修改。升级到 0.10.0-beta.2 时配置 schema 升至
192
+ 5;未完成 `ec-tdd-init` readiness 的项目/session TDD 请求迁移为关闭并保留阈值,同时
193
+ 部署共享 Java 差异覆盖率与 readiness 工具。0.10.0-beta.3 起,TDD 业务任务只依赖本地
194
+ 单测与本地差异覆盖率,历史远程 CI 证据保留但不再参与验收。已经冻结的活动任务合同
195
+ 不会被静默改写。
192
196
 
193
197
  若当前会话不希望 Harness 接管,显式调用 `/ec-no-harness`(Codex 使用
194
198
  `$ec-no-harness`)。它只旁路 Easy Coding,不关闭其他 hooks,也不忽略其他 skills;
package/dist/cli.js CHANGED
@@ -103,7 +103,7 @@ async function isDirectory(filePath) {
103
103
  }
104
104
 
105
105
  // src/utils/config-yaml.ts
106
- var CONFIG_SCHEMA_VERSION = 4;
106
+ var CONFIG_SCHEMA_VERSION = 5;
107
107
  var DEFAULT_TDD_COVERAGE_THRESHOLD = 90;
108
108
  var APPROVAL_MODES = ["approve", "guard", "confirm", "auto"];
109
109
  var CONFIGURED_WORKFLOW_MODES = ["adaptive", "fast", "standard", "strict"];
@@ -190,9 +190,10 @@ function resolveLegacyBehavior(config2) {
190
190
  const legacyLite = behavior.confirm_mode === "lite";
191
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";
192
192
  const workflowMode = isConfiguredWorkflowMode(behavior.workflow_mode) ? behavior.workflow_mode : legacyLite ? "fast" : "adaptive";
193
- const supportsTdd = Number(config2.version) >= CONFIG_SCHEMA_VERSION;
194
- const tddEnabled = supportsTdd && behavior.tdd_enabled === true;
195
- const tddCoverageThreshold = supportsTdd && isTddCoverageThreshold(behavior.tdd_coverage_threshold) ? behavior.tdd_coverage_threshold : DEFAULT_TDD_COVERAGE_THRESHOLD;
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;
196
197
  return { approvalMode, workflowMode, tddEnabled, tddCoverageThreshold };
197
198
  }
198
199
  async function setBehaviorModes(filePath, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold) {
@@ -258,6 +259,8 @@ var MAIN_SPEC_DIR = "main";
258
259
  var DEV_SPEC_DIR = "dev";
259
260
  var TEMPLATES_DIR = "templates";
260
261
  var TOOLS_DIR = "tools";
262
+ var TDD_DIR = "tdd";
263
+ var TDD_READINESS_FILE = "readiness.json";
261
264
  var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
262
265
  var HOOK_BYTECODE_GITIGNORE_ENTRY = "__pycache__/";
263
266
  var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
@@ -681,8 +684,10 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
681
684
  async function writeToolsScaffold(easyCodingDir) {
682
685
  const toolsDir = path6.join(easyCodingDir, TOOLS_DIR);
683
686
  await ensureDir(toolsDir);
684
- const src = getTemplatePath("runtime", "tools", "easy_coding_java_coverage.py");
685
- await writeTextFile(path6.join(toolsDir, "easy_coding_java_coverage.py"), await readTextFile(src));
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
+ }
686
691
  }
687
692
  async function writeTemplatesScaffold(easyCodingDir) {
688
693
  const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
@@ -2772,7 +2777,7 @@ function renderTargetPlans(targetPlans) {
2772
2777
  }
2773
2778
 
2774
2779
  // src/commands/config.ts
2775
- import path18 from "path";
2780
+ import path19 from "path";
2776
2781
  import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
2777
2782
  import chalk4 from "chalk";
2778
2783
 
@@ -2851,10 +2856,169 @@ async function checkForUpgrade(cwd) {
2851
2856
  }
2852
2857
  }
2853
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
+
2854
3018
  // src/commands/config.ts
2855
3019
  async function config() {
2856
3020
  renderBanner();
2857
- const configPath2 = path18.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
3021
+ const configPath2 = path19.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
2858
3022
  if (!await pathExists(configPath2)) {
2859
3023
  throw new Error("No easy-coding harness found in this project.");
2860
3024
  }
@@ -2948,6 +3112,15 @@ async function config() {
2948
3112
  cancel4("Configuration cancelled.");
2949
3113
  return;
2950
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
+ }
2951
3124
  let tddCoverageThreshold = current.tddCoverageThreshold;
2952
3125
  if (tddEnabled) {
2953
3126
  const thresholdInput = await text({
@@ -2972,6 +3145,15 @@ async function config() {
2972
3145
  cancel4("Configuration cancelled.");
2973
3146
  return;
2974
3147
  }
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
+ }
2975
3157
  await setBehaviorModes(configPath2, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold);
2976
3158
  outro3(
2977
3159
  chalk4.green(
@@ -2981,20 +3163,20 @@ async function config() {
2981
3163
  }
2982
3164
 
2983
3165
  // src/commands/init.ts
2984
- import path20 from "path";
3166
+ import path21 from "path";
2985
3167
  import { note, outro as outro4 } from "@clack/prompts";
2986
3168
  import chalk5 from "chalk";
2987
3169
 
2988
3170
  // src/utils/install-state.ts
2989
- import { readdir as readdir5 } from "fs/promises";
2990
- import path19 from "path";
3171
+ import { readdir as readdir6 } from "fs/promises";
3172
+ import path20 from "path";
2991
3173
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
2992
3174
  async function detectEasyCodingInstallState(cwd) {
2993
- const easyCodingDir = path19.join(cwd, EASY_CODING_DIR);
3175
+ const easyCodingDir = path20.join(cwd, EASY_CODING_DIR);
2994
3176
  if (!await pathExists(easyCodingDir)) {
2995
3177
  return { kind: "fresh", easyCodingDir };
2996
3178
  }
2997
- const configPath2 = path19.join(easyCodingDir, CONFIG_FILE);
3179
+ const configPath2 = path20.join(easyCodingDir, CONFIG_FILE);
2998
3180
  if (await pathExists(configPath2)) {
2999
3181
  return { kind: "installed", easyCodingDir, configPath: configPath2 };
3000
3182
  }
@@ -3012,17 +3194,17 @@ async function detectEasyCodingInstallState(cwd) {
3012
3194
  async function detectLegacyAssets(easyCodingDir) {
3013
3195
  const assets = [];
3014
3196
  for (const file of LEGACY_ROOT_FILES) {
3015
- if (await pathExists(path19.join(easyCodingDir, file))) {
3197
+ if (await pathExists(path20.join(easyCodingDir, file))) {
3016
3198
  assets.push(relativeEasyCodingPath(file));
3017
3199
  }
3018
3200
  }
3019
- if (await pathExists(path19.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3201
+ if (await pathExists(path20.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3020
3202
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
3021
3203
  }
3022
- const shortMemoryFiles = await listMarkdownFiles(path19.join(easyCodingDir, "memory", "short"));
3204
+ const shortMemoryFiles = await listMarkdownFiles(path20.join(easyCodingDir, "memory", "short"));
3023
3205
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
3024
3206
  for (const dir of ["spec", "prototype"]) {
3025
- if (await hasAnyDirectoryEntry(path19.join(easyCodingDir, dir))) {
3207
+ if (await hasAnyDirectoryEntry(path20.join(easyCodingDir, dir))) {
3026
3208
  assets.push(relativeEasyCodingPath(dir));
3027
3209
  }
3028
3210
  }
@@ -3032,23 +3214,23 @@ async function listMarkdownFiles(dir) {
3032
3214
  if (!await isDirectory(dir)) {
3033
3215
  return [];
3034
3216
  }
3035
- const entries = await readdir5(dir, { withFileTypes: true });
3217
+ const entries = await readdir6(dir, { withFileTypes: true });
3036
3218
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
3037
3219
  }
3038
3220
  async function hasAnyDirectoryEntry(dir) {
3039
3221
  if (!await isDirectory(dir)) {
3040
3222
  return false;
3041
3223
  }
3042
- return (await readdir5(dir)).length > 0;
3224
+ return (await readdir6(dir)).length > 0;
3043
3225
  }
3044
3226
  function relativeEasyCodingPath(...segments) {
3045
- return path19.posix.join(EASY_CODING_DIR, ...segments);
3227
+ return path20.posix.join(EASY_CODING_DIR, ...segments);
3046
3228
  }
3047
3229
  function relativeConfigPath() {
3048
- return path19.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3230
+ return path20.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3049
3231
  }
3050
3232
  function relativeProjectInitTaskPath() {
3051
- 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");
3052
3234
  }
3053
3235
 
3054
3236
  // src/commands/init.ts
@@ -3129,7 +3311,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
3129
3311
  if (!targetSubmodulePaths.has(entry.path)) {
3130
3312
  continue;
3131
3313
  }
3132
- const dir = path20.join(cwd, entry.path);
3314
+ const dir = path21.join(cwd, entry.path);
3133
3315
  targets.push(
3134
3316
  await targetFromState(dir, entry.path, "submodule-child", {
3135
3317
  parent: toPosixRelative2(dir, cwd)
@@ -3176,24 +3358,24 @@ function contextFromState(role, installState) {
3176
3358
  };
3177
3359
  }
3178
3360
  function toPosixRelative2(from, to) {
3179
- const relative = path20.relative(from, to);
3180
- return relative ? relative.split(path20.sep).join("/") : ".";
3361
+ const relative = path21.relative(from, to);
3362
+ return relative ? relative.split(path21.sep).join("/") : ".";
3181
3363
  }
3182
3364
  async function resolveInitPlatforms(cwd, opts, parentInstalled) {
3183
3365
  if (opts.agent || !parentInstalled) {
3184
3366
  return resolvePlatforms(opts, ["claude-code"]);
3185
3367
  }
3186
- 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));
3187
3369
  if (Array.isArray(config2.agents) && config2.agents.length > 0) {
3188
3370
  return config2.agents;
3189
3371
  }
3190
3372
  return resolvePlatforms(opts, ["claude-code"]);
3191
3373
  }
3192
3374
  async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
3193
- if (!await pathExists(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3375
+ if (!await pathExists(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3194
3376
  return;
3195
3377
  }
3196
- 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 };
3197
3379
  const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
3198
3380
  await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
3199
3381
  }
@@ -3202,7 +3384,7 @@ async function refreshInstalledChildTopologies(targets) {
3202
3384
  if (!target.installed || target.context.role !== "submodule-child") {
3203
3385
  continue;
3204
3386
  }
3205
- const configPath2 = path20.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3387
+ const configPath2 = path21.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3206
3388
  if (!await pathExists(configPath2)) {
3207
3389
  continue;
3208
3390
  }
@@ -3211,12 +3393,12 @@ async function refreshInstalledChildTopologies(targets) {
3211
3393
  }
3212
3394
 
3213
3395
  // src/commands/status.ts
3214
- import path22 from "path";
3396
+ import path23 from "path";
3215
3397
  import chalk6 from "chalk";
3216
3398
 
3217
3399
  // src/utils/session.ts
3218
- import { readdir as readdir6, unlink } from "fs/promises";
3219
- import path21 from "path";
3400
+ import { readdir as readdir7, unlink } from "fs/promises";
3401
+ import path22 from "path";
3220
3402
  var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1e3;
3221
3403
  function parseSessionFile(content) {
3222
3404
  try {
@@ -3226,7 +3408,7 @@ function parseSessionFile(content) {
3226
3408
  }
3227
3409
  }
3228
3410
  function getSessionDir(cwd) {
3229
- return path21.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3411
+ return path22.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3230
3412
  }
3231
3413
  async function listSessionFiles(cwd) {
3232
3414
  const dir = getSessionDir(cwd);
@@ -3234,11 +3416,11 @@ async function listSessionFiles(cwd) {
3234
3416
  return [];
3235
3417
  }
3236
3418
  const entries = [];
3237
- for (const name of await readdir6(dir)) {
3419
+ for (const name of await readdir7(dir)) {
3238
3420
  if (!name.endsWith(".json")) {
3239
3421
  continue;
3240
3422
  }
3241
- const filePath = path21.join(dir, name);
3423
+ const filePath = path22.join(dir, name);
3242
3424
  const content = await readTextIfExists(filePath);
3243
3425
  if (!content) {
3244
3426
  continue;
@@ -3260,7 +3442,7 @@ async function listSessionFiles(cwd) {
3260
3442
  async function status() {
3261
3443
  renderBanner();
3262
3444
  const cwd = process.cwd();
3263
- const configPath2 = path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3445
+ const configPath2 = path23.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3264
3446
  if (!await pathExists(configPath2)) {
3265
3447
  throw new Error("No easy-coding harness found in this project.");
3266
3448
  }
@@ -3270,6 +3452,7 @@ async function status() {
3270
3452
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
3271
3453
  const sessions = await listSessionFiles(cwd);
3272
3454
  const versionRelation = compareVersions(config2.harness_version, VERSION);
3455
+ const tddReadiness = await inspectTddReadiness(cwd);
3273
3456
  console.log(chalk6.bold("Harness"));
3274
3457
  console.log(` version: ${config2.harness_version}`);
3275
3458
  console.log(` cli: ${VERSION}`);
@@ -3291,6 +3474,10 @@ async function status() {
3291
3474
  console.log(` workflow_mode: ${projectWorkflowMode}`);
3292
3475
  console.log(` tdd_enabled: ${projectTddEnabled}`);
3293
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
+ }
3294
3481
  console.log("");
3295
3482
  console.log(chalk6.bold("Sessions"));
3296
3483
  console.log(` project_approval_mode: ${projectApprovalMode}`);
@@ -3398,7 +3585,7 @@ async function update(opts) {
3398
3585
  }
3399
3586
 
3400
3587
  // src/commands/upgrade.ts
3401
- import path23 from "path";
3588
+ import path24 from "path";
3402
3589
  import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
3403
3590
  import chalk8 from "chalk";
3404
3591
  var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
@@ -3460,7 +3647,7 @@ async function upgrade(opts) {
3460
3647
  ].filter(Boolean) : [],
3461
3648
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
3462
3649
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
3463
- "Will migrate behavior config to Approval, Workflow, and default-off Java TDD schema 4 fields.",
3650
+ "Will migrate behavior config to schema 5 and disable unready project/session TDD settings.",
3464
3651
  "Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
3465
3652
  ].join("\n");
3466
3653
  if (opts.dryRun) {
@@ -3478,6 +3665,7 @@ async function upgrade(opts) {
3478
3665
  }
3479
3666
  }
3480
3667
  for (const { target, config: config2 } of pending) {
3668
+ const beta1ProjectTddRequested = Number(config2.version) === 4 && config2.behavior?.tdd_enabled === true;
3481
3669
  const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
3482
3670
  supermodule: target.supermodule
3483
3671
  });
@@ -3494,6 +3682,14 @@ async function upgrade(opts) {
3494
3682
  await ensureHookBytecodeIgnored(target.dir);
3495
3683
  await migrateLegacyWorkflowState(target.dir);
3496
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
+ }
3497
3693
  await updateHarnessVersion(target.configPath, VERSION);
3498
3694
  await updateSupermoduleConfig(target.configPath, target.supermodule);
3499
3695
  await setPendingInitSince(target.dir, VERSION);
@@ -3549,7 +3745,7 @@ async function needsHookConfigRefresh(target, config2) {
3549
3745
  const manifest = await readInstallManifest(target.dir);
3550
3746
  for (const agent of config2.agents) {
3551
3747
  const meta = resolvePlatformMeta(target.dir, agent);
3552
- const configPath2 = path23.join(target.dir, meta.hookConfigFile);
3748
+ const configPath2 = path24.join(target.dir, meta.hookConfigFile);
3553
3749
  const content = await readTextIfExists(configPath2);
3554
3750
  if (content === null) {
3555
3751
  return true;
@@ -3689,7 +3885,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
3689
3885
  return true;
3690
3886
  }
3691
3887
  return pathAliases(
3692
- normalizePathForHookComparison(path23.resolve(cwd, relativeHookPath))
3888
+ normalizePathForHookComparison(path24.resolve(cwd, relativeHookPath))
3693
3889
  ).includes(normalizedHookPath);
3694
3890
  });
3695
3891
  }