easy-coding-harness 0.10.0-beta.0 → 0.10.0-beta.10

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +53 -19
  3. package/dist/cli.js +573 -53
  4. package/dist/cli.js.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/claude/agents/ec-implementer.md +11 -0
  7. package/templates/claude/agents/ec-reviewer.md +6 -1
  8. package/templates/codex/agents/ec-implementer.toml +11 -0
  9. package/templates/codex/agents/ec-reviewer.toml +6 -1
  10. package/templates/common/bundled-skills/ec-init/SKILL.md +20 -2
  11. package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +33 -11
  12. package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +1 -1
  13. package/templates/common/skills/ec-analysis/SKILL.md +162 -26
  14. package/templates/common/skills/ec-config/SKILL.md +76 -0
  15. package/templates/common/skills/ec-git/SKILL.md +7 -1
  16. package/templates/common/skills/ec-implementing/SKILL.md +72 -1
  17. package/templates/common/skills/ec-memory/SKILL.md +77 -3
  18. package/templates/common/skills/ec-reviewing/SKILL.md +25 -1
  19. package/templates/common/skills/ec-task-close/SKILL.md +4 -0
  20. package/templates/common/skills/ec-task-management/SKILL.md +13 -32
  21. package/templates/common/skills/ec-tdd-init/SKILL.md +101 -0
  22. package/templates/common/skills/ec-verification/SKILL.md +91 -5
  23. package/templates/common/skills/ec-workflow/SKILL.md +86 -21
  24. package/templates/main-constraint/AGENTS.md.tpl +54 -12
  25. package/templates/main-constraint/CLAUDE.md.tpl +51 -12
  26. package/templates/qoder/agents/ec-implementer.md +11 -0
  27. package/templates/qoder/agents/ec-reviewer.md +6 -1
  28. package/templates/runtime/templates/dev-spec-skeleton.md +8 -1
  29. package/templates/runtime/tools/easy_coding_java_coverage.py +317 -0
  30. package/templates/runtime/tools/easy_coding_tdd_readiness.py +306 -0
  31. package/templates/shared-hooks/easy_coding_state.py +4782 -574
  32. package/templates/shared-hooks/easy_dev_spec.py +444 -30
  33. package/templates/shared-hooks/easy_dev_spec_execution.py +1014 -0
  34. package/templates/shared-hooks/easy_dev_spec_protocol.py +1426 -18
  35. package/templates/shared-hooks/inject-subagent-context.py +5 -0
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 -->";
@@ -292,6 +311,7 @@ var PLATFORM_META = {
292
311
  stateInjectEvent: ["SessionStart", "UserPromptSubmit"],
293
312
  hasSubagentContext: true,
294
313
  templateContext: {
314
+ workflow_agent_id: "claude-code",
295
315
  sub_agent_dispatch: "Agent tool",
296
316
  platform_spawn_instruction: 'Use the Agent tool with run_in_background when useful; use isolation: "worktree" for parallel file edits.',
297
317
  skill_trigger: "/",
@@ -315,6 +335,7 @@ var PLATFORM_META = {
315
335
  stateInjectEvent: ["SessionStart", "UserPromptSubmit"],
316
336
  hasSubagentContext: false,
317
337
  templateContext: {
338
+ workflow_agent_id: "codex",
318
339
  sub_agent_dispatch: "Codex sub-agent dispatch",
319
340
  platform_spawn_instruction: "Use Codex sub-agent delegation where available; pass the full task card in the prompt.",
320
341
  skill_trigger: "$",
@@ -339,6 +360,7 @@ var PLATFORM_META = {
339
360
  hasSubagentContext: true,
340
361
  cnVariant: ".qodercn",
341
362
  templateContext: {
363
+ workflow_agent_id: "qoder",
342
364
  sub_agent_dispatch: "Agent tool",
343
365
  platform_spawn_instruction: "Use the Agent tool with worktree isolation for parallel file edits.",
344
366
  skill_trigger: "/",
@@ -659,8 +681,17 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
659
681
  await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
660
682
  await writeMemoryScaffold(easyCodingDir);
661
683
  await writeTemplatesScaffold(easyCodingDir);
684
+ await writeToolsScaffold(easyCodingDir);
662
685
  return projectId;
663
686
  }
687
+ async function writeToolsScaffold(easyCodingDir) {
688
+ const toolsDir = path6.join(easyCodingDir, TOOLS_DIR);
689
+ await ensureDir(toolsDir);
690
+ for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
691
+ const src = getTemplatePath("runtime", "tools", file);
692
+ await writeTextFile(path6.join(toolsDir, file), await readTextFile(src));
693
+ }
694
+ }
664
695
  async function writeTemplatesScaffold(easyCodingDir) {
665
696
  const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
666
697
  await ensureDir(templatesDir);
@@ -748,6 +779,87 @@ function isLegacyStage(value) {
748
779
  function migrateStage(value) {
749
780
  return isLegacyStage(value) ? LEGACY_STAGE_MAP[value] : value;
750
781
  }
782
+ var LEGACY_DISPLAY_AGENT_IDENTITIES = {
783
+ "claude with easy coding": "claude-code",
784
+ "claude-code with easy coding": "claude-code",
785
+ "claude code with easy coding": "claude-code",
786
+ "codex with easy coding": "codex",
787
+ "qoder with easy coding": "qoder"
788
+ };
789
+ function migratedAgentIdentity(value) {
790
+ if (typeof value !== "string") return void 0;
791
+ const normalized = value.trim().toLowerCase();
792
+ if (normalized === "claude-code" || normalized === "codex" || normalized === "qoder") {
793
+ return normalized;
794
+ }
795
+ if (/^\/?root(?:\/[a-z0-9._-]+)*$/.test(normalized)) return "codex";
796
+ return LEGACY_DISPLAY_AGENT_IDENTITIES[normalized];
797
+ }
798
+ function migrateAgentFields(record, fields) {
799
+ let changed = false;
800
+ for (const field of fields) {
801
+ const migrated = migratedAgentIdentity(record[field]);
802
+ if (migrated && migrated !== record[field]) {
803
+ record[field] = migrated;
804
+ changed = true;
805
+ }
806
+ }
807
+ return changed;
808
+ }
809
+ function taskAgentFieldGroups(task) {
810
+ const groups = [
811
+ {
812
+ record: task,
813
+ fields: ["created_by", "last_agent", "workflow_mode_confirmed_by", "tdd_confirmed_by"]
814
+ }
815
+ ];
816
+ for (const field of ["pending_transition", "workflow_mode_proposal", "verification_checkpoint"]) {
817
+ const value = task[field];
818
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
819
+ const identityField = field === "pending_transition" ? "requested_by" : field === "workflow_mode_proposal" ? "proposed_by" : "recorded_by";
820
+ groups.push({ record: value, fields: [identityField] });
821
+ }
822
+ if (Array.isArray(task.workflow_mode_escalations)) {
823
+ for (const escalation of task.workflow_mode_escalations) {
824
+ if (!escalation || typeof escalation !== "object" || Array.isArray(escalation)) continue;
825
+ groups.push({ record: escalation, fields: ["raised_by"] });
826
+ }
827
+ }
828
+ const memoryProgress = task.memory_progress;
829
+ if (memoryProgress && typeof memoryProgress === "object" && !Array.isArray(memoryProgress)) {
830
+ const assessment = memoryProgress.architecture_assessment;
831
+ if (assessment && typeof assessment === "object" && !Array.isArray(assessment)) {
832
+ groups.push({ record: assessment, fields: ["recorded_by"] });
833
+ }
834
+ }
835
+ if (Array.isArray(task.spec_dependency_evidence)) {
836
+ for (const evidence of task.spec_dependency_evidence) {
837
+ if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) continue;
838
+ groups.push({ record: evidence, fields: ["satisfied_by"] });
839
+ }
840
+ }
841
+ if (Array.isArray(task.stage_history)) {
842
+ for (const entry of task.stage_history) {
843
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
844
+ groups.push({ record: entry, fields: ["agent"] });
845
+ }
846
+ }
847
+ return groups;
848
+ }
849
+ function migrateTaskAgentIdentities(task) {
850
+ return taskAgentFieldGroups(task).reduce(
851
+ (changed, group) => migrateAgentFields(group.record, group.fields) || changed,
852
+ false
853
+ );
854
+ }
855
+ function hasLegacyTaskAgentIdentities(task) {
856
+ return taskAgentFieldGroups(task).some(
857
+ ({ record, fields }) => fields.some((field) => {
858
+ const migrated = migratedAgentIdentity(record[field]);
859
+ return migrated !== void 0 && migrated !== record[field];
860
+ })
861
+ );
862
+ }
751
863
  function migrateStageHistory(task) {
752
864
  if (!Array.isArray(task.stage_history)) return false;
753
865
  let changed = false;
@@ -778,7 +890,8 @@ function migrateTaskWorkflowState(task) {
778
890
  const legacyRequestedAt = Array.isArray(task.stage_history) ? [...task.stage_history].reverse().find(
779
891
  (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && entry.stage === "WAITING_CONFIRM"
780
892
  ) : null;
781
- let changed = migrateStageHistory(task);
893
+ let changed = migrateTaskAgentIdentities(task);
894
+ changed = migrateStageHistory(task) || changed;
782
895
  if (legacyStatus) {
783
896
  task.status = LEGACY_STAGE_MAP[legacyStatus];
784
897
  changed = true;
@@ -816,10 +929,21 @@ function migrateTaskWorkflowState(task) {
816
929
  task.workflow_mode_legacy = true;
817
930
  changed = true;
818
931
  }
932
+ if (isActive && taskType !== "project-init" && typeof task.tdd_enabled !== "boolean") {
933
+ task.tdd_enabled = false;
934
+ task.tdd_coverage_threshold = DEFAULT_TDD_COVERAGE_THRESHOLD;
935
+ task.tdd_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
936
+ task.tdd_confirmed_by = "upgrade-migration";
937
+ changed = true;
938
+ }
939
+ if (task.tdd_enabled === false && "tdd_baselines" in task) {
940
+ task.tdd_baselines = void 0;
941
+ changed = true;
942
+ }
819
943
  return changed;
820
944
  }
821
945
  function migrateSessionBehavior(session) {
822
- let changed = false;
946
+ let changed = migrateAgentFields(session, ["agent", "last_agent"]);
823
947
  const legacyMode = session.confirm_mode;
824
948
  const legacyLite = legacyMode === "lite";
825
949
  if (!["approve", "guard", "confirm", "auto"].includes(String(session.approval_mode ?? ""))) {
@@ -930,7 +1054,7 @@ async function hasLegacyWorkflowState(cwd) {
930
1054
  if (!await pathExists(filePath)) continue;
931
1055
  const task = await readJsonRecord(filePath);
932
1056
  if (!task) continue;
933
- if (isLegacyStage(task.status) || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && String(task.type ?? "") !== "project-init" && !["fast", "standard", "strict"].includes(String(task.workflow_mode ?? "")) || Array.isArray(task.stage_history) && task.stage_history.some(
1057
+ if (hasLegacyTaskAgentIdentities(task) || isLegacyStage(task.status) || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && String(task.type ?? "") !== "project-init" && !["fast", "standard", "strict"].includes(String(task.workflow_mode ?? "")) || Array.isArray(task.stage_history) && task.stage_history.some(
934
1058
  (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && isLegacyStage(entry.stage)
935
1059
  )) {
936
1060
  return true;
@@ -939,7 +1063,12 @@ async function hasLegacyWorkflowState(cwd) {
939
1063
  for (const filePath of await sessionFiles(cwd)) {
940
1064
  const session = await readJsonRecord(filePath);
941
1065
  if (!session) continue;
942
- if (isLegacyStage(session.last_seen_stage) || "confirm_mode" in session) return true;
1066
+ if (["agent", "last_agent"].some((field) => {
1067
+ const migrated = migratedAgentIdentity(session[field]);
1068
+ return migrated !== void 0 && migrated !== session[field];
1069
+ }) || isLegacyStage(session.last_seen_stage) || "confirm_mode" in session) {
1070
+ return true;
1071
+ }
943
1072
  }
944
1073
  return false;
945
1074
  }
@@ -2515,6 +2644,7 @@ function addRuntimeClearEntries(plan, cwd) {
2515
2644
  path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
2516
2645
  path16.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
2517
2646
  path16.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
2647
+ path16.join(cwd, EASY_CODING_DIR, TOOLS_DIR),
2518
2648
  path16.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
2519
2649
  ])
2520
2650
  ];
@@ -2737,8 +2867,8 @@ function renderTargetPlans(targetPlans) {
2737
2867
  }
2738
2868
 
2739
2869
  // src/commands/config.ts
2740
- import path18 from "path";
2741
- import { cancel as cancel4, confirm as confirm3, outro as outro3, select } from "@clack/prompts";
2870
+ import path19 from "path";
2871
+ import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
2742
2872
  import chalk4 from "chalk";
2743
2873
 
2744
2874
  // src/utils/compare-versions.ts
@@ -2816,10 +2946,169 @@ async function checkForUpgrade(cwd) {
2816
2946
  }
2817
2947
  }
2818
2948
 
2949
+ // src/utils/tdd-readiness.ts
2950
+ import { createHash as createHash2 } from "crypto";
2951
+ import { readFile as readFile4, readdir as readdir5, realpath as realpath2 } from "fs/promises";
2952
+ import path18 from "path";
2953
+ var TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1";
2954
+ var TDD_READINESS_SCOPE = "changed-production-lines";
2955
+ var TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA";
2956
+ var TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD";
2957
+ var COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py";
2958
+ var JAVA_BUILD_FILE_NAMES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
2959
+ var GITLAB_CI_ENTRY_FILES = /* @__PURE__ */ new Set([".gitlab-ci.yml", ".gitlab-ci.yaml"]);
2960
+ function readinessPath(root) {
2961
+ return path18.join(root, EASY_CODING_DIR, TDD_DIR, TDD_READINESS_FILE);
2962
+ }
2963
+ function parseFileRecords(value, field, reasons) {
2964
+ if (!Array.isArray(value) || value.length === 0) {
2965
+ reasons.push(`${field} must contain at least one file`);
2966
+ return [];
2967
+ }
2968
+ const records = [];
2969
+ for (const item of value) {
2970
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
2971
+ reasons.push(`${field} contains an invalid record`);
2972
+ continue;
2973
+ }
2974
+ const record = item;
2975
+ if (typeof record.path !== "string" || !record.path.trim() || path18.isAbsolute(record.path) || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
2976
+ reasons.push(`${field} contains an invalid path or SHA-256`);
2977
+ continue;
2978
+ }
2979
+ records.push({ path: record.path, sha256: record.sha256 });
2980
+ }
2981
+ return records;
2982
+ }
2983
+ function usesRequiredGateVariables(command) {
2984
+ const normalized = command.replaceAll(`\${${TDD_BASE_VARIABLE}}`, `$${TDD_BASE_VARIABLE}`).replaceAll(`\${${TDD_THRESHOLD_VARIABLE}}`, `$${TDD_THRESHOLD_VARIABLE}`);
2985
+ return new RegExp(`--base\\s+['"]?\\$${TDD_BASE_VARIABLE}(?:['"]|\\s|$)`).test(normalized) && new RegExp(`--threshold\\s+['"]?\\$${TDD_THRESHOLD_VARIABLE}(?:['"]|\\s|$)`).test(normalized);
2986
+ }
2987
+ function isSafeReportPattern(value) {
2988
+ const normalized = value.replaceAll("\\", "/");
2989
+ return !path18.isAbsolute(value) && !normalized.split("/").includes("..");
2990
+ }
2991
+ function activeCiContent(contents) {
2992
+ return contents.join("\n").split("\n").map((line) => line.replace(/^\s*#.*$/, "").replace(/\s+#.*$/, "")).join("\n");
2993
+ }
2994
+ async function validateFiles(root, records, reasons) {
2995
+ const contents = [];
2996
+ const resolvedRoot = await realpath2(root);
2997
+ for (const record of records) {
2998
+ const absolute = path18.resolve(root, record.path);
2999
+ try {
3000
+ const resolved = await realpath2(absolute);
3001
+ if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path18.sep}`)) {
3002
+ reasons.push(`readiness file escapes project root: ${record.path}`);
3003
+ continue;
3004
+ }
3005
+ const content = await readFile4(resolved);
3006
+ const digest = createHash2("sha256").update(content).digest("hex");
3007
+ if (digest !== record.sha256) reasons.push(`readiness file changed: ${record.path}`);
3008
+ contents.push(content.toString("utf8"));
3009
+ } catch {
3010
+ reasons.push(`readiness file is missing or unreadable: ${record.path}`);
3011
+ }
3012
+ }
3013
+ return contents;
3014
+ }
3015
+ async function inspectTddReadiness(root) {
3016
+ const manifestPath2 = readinessPath(root);
3017
+ if (!await pathExists(manifestPath2)) {
3018
+ return { status: "needs_init", reasons: ["TDD readiness receipt is missing"], manifestPath: manifestPath2 };
3019
+ }
3020
+ let manifest;
3021
+ try {
3022
+ manifest = JSON.parse(await readFile4(manifestPath2, "utf8"));
3023
+ } catch {
3024
+ return { status: "needs_init", reasons: ["TDD readiness receipt is invalid"], manifestPath: manifestPath2 };
3025
+ }
3026
+ const reasons = [];
3027
+ if (manifest.schema !== TDD_READINESS_SCHEMA) reasons.push("unsupported readiness schema");
3028
+ if (manifest.provider !== "gitlab") reasons.push("readiness provider must be gitlab");
3029
+ if (manifest.coverage_scope !== TDD_READINESS_SCOPE) {
3030
+ reasons.push("coverage scope must be changed-production-lines");
3031
+ }
3032
+ if (manifest.historical_coverage_required !== false) {
3033
+ reasons.push("historical coverage must remain disabled");
3034
+ }
3035
+ if (!Array.isArray(manifest.coverage_report_patterns) || manifest.coverage_report_patterns.length === 0 || manifest.coverage_report_patterns.some(
3036
+ (item) => typeof item !== "string" || !item.trim() || !isSafeReportPattern(item)
3037
+ )) {
3038
+ reasons.push("coverage_report_patterns must contain safe project-relative report patterns");
3039
+ }
3040
+ if (typeof manifest.changed_line_gate_command !== "string" || !manifest.changed_line_gate_command.includes(COVERAGE_TOOL_PATH)) {
3041
+ reasons.push("changed-line coverage gate command is missing");
3042
+ } else if (!usesRequiredGateVariables(manifest.changed_line_gate_command)) {
3043
+ reasons.push("changed-line coverage gate must use the task baseline and threshold variables");
3044
+ }
3045
+ const buildFiles = parseFileRecords(manifest.build_files, "build_files", reasons);
3046
+ const ciFiles = parseFileRecords(manifest.ci_files, "ci_files", reasons);
3047
+ const toolFiles = parseFileRecords(manifest.tool_files, "tool_files", reasons);
3048
+ if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(path18.basename(record.path)))) {
3049
+ reasons.push("build_files must include a Maven or Gradle Java build file");
3050
+ }
3051
+ if (!ciFiles.some((record) => GITLAB_CI_ENTRY_FILES.has(record.path.replaceAll("\\", "/")))) {
3052
+ reasons.push("ci_files must include the project-root GitLab CI entry file");
3053
+ }
3054
+ if (!toolFiles.some((record) => record.path.replaceAll("\\", "/") === COVERAGE_TOOL_PATH)) {
3055
+ reasons.push(`tool_files must include ${COVERAGE_TOOL_PATH}`);
3056
+ }
3057
+ const buildContents = await validateFiles(root, buildFiles, reasons);
3058
+ const ciContents = await validateFiles(root, ciFiles, reasons);
3059
+ await validateFiles(root, toolFiles, reasons);
3060
+ if (!buildContents.some((content) => /jacoco/i.test(content))) {
3061
+ reasons.push("build files do not configure JaCoCo");
3062
+ }
3063
+ const combinedCi = activeCiContent(ciContents);
3064
+ for (const marker of [
3065
+ "jacoco",
3066
+ "artifacts",
3067
+ COVERAGE_TOOL_PATH,
3068
+ TDD_BASE_VARIABLE,
3069
+ TDD_THRESHOLD_VARIABLE
3070
+ ]) {
3071
+ if (!combinedCi.toLowerCase().includes(marker.toLowerCase())) {
3072
+ reasons.push(`CI files do not contain required marker: ${marker}`);
3073
+ }
3074
+ }
3075
+ if (!usesRequiredGateVariables(combinedCi)) {
3076
+ reasons.push("CI changed-line gate must use the task baseline and threshold variables");
3077
+ }
3078
+ if (!/(?:^|\n)\s*stage\s*:\s*['"]?test['"]?\s*(?:#.*)?(?:\n|$)/i.test(combinedCi)) {
3079
+ reasons.push("CI files do not declare a TEST-stage job");
3080
+ }
3081
+ return {
3082
+ status: reasons.length === 0 ? "ready" : "needs_init",
3083
+ reasons: [...new Set(reasons)],
3084
+ manifestPath: manifestPath2
3085
+ };
3086
+ }
3087
+ async function disableUnreadySessionTddOverrides(root) {
3088
+ if ((await inspectTddReadiness(root)).status === "ready") return 0;
3089
+ const sessionsDir = path18.join(root, EASY_CODING_DIR, SESSIONS_DIR);
3090
+ if (!await pathExists(sessionsDir)) return 0;
3091
+ let updated = 0;
3092
+ for (const entry of await readdir5(sessionsDir, { withFileTypes: true })) {
3093
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3094
+ const filePath = path18.join(sessionsDir, entry.name);
3095
+ try {
3096
+ const session = JSON.parse(await readFile4(filePath, "utf8"));
3097
+ if (session.tdd_enabled !== true) continue;
3098
+ session.tdd_enabled = false;
3099
+ await writeTextFile(filePath, `${JSON.stringify(session, null, 2)}
3100
+ `);
3101
+ updated += 1;
3102
+ } catch {
3103
+ }
3104
+ }
3105
+ return updated;
3106
+ }
3107
+
2819
3108
  // src/commands/config.ts
2820
3109
  async function config() {
2821
3110
  renderBanner();
2822
- const configPath2 = path18.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
3111
+ const configPath2 = path19.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
2823
3112
  if (!await pathExists(configPath2)) {
2824
3113
  throw new Error("No easy-coding harness found in this project.");
2825
3114
  }
@@ -2863,7 +3152,7 @@ async function config() {
2863
3152
  {
2864
3153
  value: "auto",
2865
3154
  label: "auto \u2014 advance workflow stages automatically",
2866
- hint: "task closure remains explicit"
3155
+ hint: "only new post-verification code drift pauses for exact acceptance"
2867
3156
  }
2868
3157
  ]
2869
3158
  });
@@ -2901,33 +3190,83 @@ async function config() {
2901
3190
  cancel4("Configuration cancelled.");
2902
3191
  return;
2903
3192
  }
3193
+ const tddEnabled = await select({
3194
+ message: `Enable Java TDD for this project (current: ${current.tddEnabled ? "enabled" : "disabled"})`,
3195
+ initialValue: current.tddEnabled,
3196
+ options: [
3197
+ { value: false, label: "disabled \u2014 preserve current test depth (default)" },
3198
+ { value: true, label: "enabled \u2014 require TDD evidence and changed-line coverage" }
3199
+ ]
3200
+ });
3201
+ if (typeof tddEnabled === "symbol") {
3202
+ cancel4("Configuration cancelled.");
3203
+ return;
3204
+ }
3205
+ if (tddEnabled) {
3206
+ const readiness = await inspectTddReadiness(process.cwd());
3207
+ if (readiness.status !== "ready") {
3208
+ cancel4(
3209
+ `TDD was not enabled. Run ec-tdd-init first: ${readiness.reasons.join("; ")}. No project modes were changed.`
3210
+ );
3211
+ return;
3212
+ }
3213
+ }
3214
+ let tddCoverageThreshold = current.tddCoverageThreshold;
3215
+ if (tddEnabled) {
3216
+ const thresholdInput = await text({
3217
+ message: "Minimum changed-production-line coverage percentage",
3218
+ initialValue: String(current.tddCoverageThreshold),
3219
+ validate(value) {
3220
+ const parsed = Number(value);
3221
+ return isTddCoverageThreshold(parsed) ? void 0 : "Enter an integer from 1 to 100.";
3222
+ }
3223
+ });
3224
+ if (typeof thresholdInput === "symbol") {
3225
+ cancel4("Configuration cancelled.");
3226
+ return;
3227
+ }
3228
+ tddCoverageThreshold = Number(thresholdInput);
3229
+ }
2904
3230
  const shouldSave = await confirm3({
2905
- message: `Set behavior.approval_mode to ${approvalMode} and behavior.workflow_mode to ${workflowMode}?`,
3231
+ message: `Set approval=${approvalMode}, workflow=${workflowMode}, TDD=${tddEnabled ? `enabled (${tddCoverageThreshold}%)` : "disabled"}?`,
2906
3232
  initialValue: true
2907
3233
  });
2908
3234
  if (typeof shouldSave === "symbol" || !shouldSave) {
2909
3235
  cancel4("Configuration cancelled.");
2910
3236
  return;
2911
3237
  }
2912
- await setBehaviorModes(configPath2, approvalMode, workflowMode);
2913
- outro3(chalk4.green(`Project modes updated: approval=${approvalMode}, workflow=${workflowMode}.`));
3238
+ if (tddEnabled) {
3239
+ const readiness = await inspectTddReadiness(process.cwd());
3240
+ if (readiness.status !== "ready") {
3241
+ cancel4(
3242
+ `TDD was not enabled because readiness changed before save: ${readiness.reasons.join("; ")}. No project modes were changed.`
3243
+ );
3244
+ return;
3245
+ }
3246
+ }
3247
+ await setBehaviorModes(configPath2, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold);
3248
+ outro3(
3249
+ chalk4.green(
3250
+ `Project modes updated: approval=${approvalMode}, workflow=${workflowMode}, TDD=${tddEnabled ? `${tddCoverageThreshold}%` : "off"}.`
3251
+ )
3252
+ );
2914
3253
  }
2915
3254
 
2916
3255
  // src/commands/init.ts
2917
- import path20 from "path";
3256
+ import path21 from "path";
2918
3257
  import { note, outro as outro4 } from "@clack/prompts";
2919
3258
  import chalk5 from "chalk";
2920
3259
 
2921
3260
  // src/utils/install-state.ts
2922
- import { readdir as readdir5 } from "fs/promises";
2923
- import path19 from "path";
3261
+ import { readdir as readdir6 } from "fs/promises";
3262
+ import path20 from "path";
2924
3263
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
2925
3264
  async function detectEasyCodingInstallState(cwd) {
2926
- const easyCodingDir = path19.join(cwd, EASY_CODING_DIR);
3265
+ const easyCodingDir = path20.join(cwd, EASY_CODING_DIR);
2927
3266
  if (!await pathExists(easyCodingDir)) {
2928
3267
  return { kind: "fresh", easyCodingDir };
2929
3268
  }
2930
- const configPath2 = path19.join(easyCodingDir, CONFIG_FILE);
3269
+ const configPath2 = path20.join(easyCodingDir, CONFIG_FILE);
2931
3270
  if (await pathExists(configPath2)) {
2932
3271
  return { kind: "installed", easyCodingDir, configPath: configPath2 };
2933
3272
  }
@@ -2945,17 +3284,17 @@ async function detectEasyCodingInstallState(cwd) {
2945
3284
  async function detectLegacyAssets(easyCodingDir) {
2946
3285
  const assets = [];
2947
3286
  for (const file of LEGACY_ROOT_FILES) {
2948
- if (await pathExists(path19.join(easyCodingDir, file))) {
3287
+ if (await pathExists(path20.join(easyCodingDir, file))) {
2949
3288
  assets.push(relativeEasyCodingPath(file));
2950
3289
  }
2951
3290
  }
2952
- if (await pathExists(path19.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3291
+ if (await pathExists(path20.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
2953
3292
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
2954
3293
  }
2955
- const shortMemoryFiles = await listMarkdownFiles(path19.join(easyCodingDir, "memory", "short"));
3294
+ const shortMemoryFiles = await listMarkdownFiles(path20.join(easyCodingDir, "memory", "short"));
2956
3295
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
2957
3296
  for (const dir of ["spec", "prototype"]) {
2958
- if (await hasAnyDirectoryEntry(path19.join(easyCodingDir, dir))) {
3297
+ if (await hasAnyDirectoryEntry(path20.join(easyCodingDir, dir))) {
2959
3298
  assets.push(relativeEasyCodingPath(dir));
2960
3299
  }
2961
3300
  }
@@ -2965,23 +3304,23 @@ async function listMarkdownFiles(dir) {
2965
3304
  if (!await isDirectory(dir)) {
2966
3305
  return [];
2967
3306
  }
2968
- const entries = await readdir5(dir, { withFileTypes: true });
3307
+ const entries = await readdir6(dir, { withFileTypes: true });
2969
3308
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
2970
3309
  }
2971
3310
  async function hasAnyDirectoryEntry(dir) {
2972
3311
  if (!await isDirectory(dir)) {
2973
3312
  return false;
2974
3313
  }
2975
- return (await readdir5(dir)).length > 0;
3314
+ return (await readdir6(dir)).length > 0;
2976
3315
  }
2977
3316
  function relativeEasyCodingPath(...segments) {
2978
- return path19.posix.join(EASY_CODING_DIR, ...segments);
3317
+ return path20.posix.join(EASY_CODING_DIR, ...segments);
2979
3318
  }
2980
3319
  function relativeConfigPath() {
2981
- return path19.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3320
+ return path20.posix.join(EASY_CODING_DIR, CONFIG_FILE);
2982
3321
  }
2983
3322
  function relativeProjectInitTaskPath() {
2984
- return path19.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
3323
+ return path20.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
2985
3324
  }
2986
3325
 
2987
3326
  // src/commands/init.ts
@@ -3062,7 +3401,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
3062
3401
  if (!targetSubmodulePaths.has(entry.path)) {
3063
3402
  continue;
3064
3403
  }
3065
- const dir = path20.join(cwd, entry.path);
3404
+ const dir = path21.join(cwd, entry.path);
3066
3405
  targets.push(
3067
3406
  await targetFromState(dir, entry.path, "submodule-child", {
3068
3407
  parent: toPosixRelative2(dir, cwd)
@@ -3109,24 +3448,24 @@ function contextFromState(role, installState) {
3109
3448
  };
3110
3449
  }
3111
3450
  function toPosixRelative2(from, to) {
3112
- const relative = path20.relative(from, to);
3113
- return relative ? relative.split(path20.sep).join("/") : ".";
3451
+ const relative = path21.relative(from, to);
3452
+ return relative ? relative.split(path21.sep).join("/") : ".";
3114
3453
  }
3115
3454
  async function resolveInitPlatforms(cwd, opts, parentInstalled) {
3116
3455
  if (opts.agent || !parentInstalled) {
3117
3456
  return resolvePlatforms(opts, ["claude-code"]);
3118
3457
  }
3119
- const config2 = await readConfigYaml(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
3458
+ const config2 = await readConfigYaml(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
3120
3459
  if (Array.isArray(config2.agents) && config2.agents.length > 0) {
3121
3460
  return config2.agents;
3122
3461
  }
3123
3462
  return resolvePlatforms(opts, ["claude-code"]);
3124
3463
  }
3125
3464
  async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
3126
- if (!await pathExists(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3465
+ if (!await pathExists(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3127
3466
  return;
3128
3467
  }
3129
- const config2 = parentTarget2.installed ? await readConfigYaml(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
3468
+ const config2 = parentTarget2.installed ? await readConfigYaml(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
3130
3469
  const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
3131
3470
  await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
3132
3471
  }
@@ -3135,7 +3474,7 @@ async function refreshInstalledChildTopologies(targets) {
3135
3474
  if (!target.installed || target.context.role !== "submodule-child") {
3136
3475
  continue;
3137
3476
  }
3138
- const configPath2 = path20.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3477
+ const configPath2 = path21.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3139
3478
  if (!await pathExists(configPath2)) {
3140
3479
  continue;
3141
3480
  }
@@ -3144,22 +3483,26 @@ async function refreshInstalledChildTopologies(targets) {
3144
3483
  }
3145
3484
 
3146
3485
  // src/commands/status.ts
3147
- import path22 from "path";
3486
+ import path23 from "path";
3148
3487
  import chalk6 from "chalk";
3149
3488
 
3150
3489
  // src/utils/session.ts
3151
- import { readdir as readdir6, unlink } from "fs/promises";
3152
- import path21 from "path";
3153
- var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1e3;
3490
+ import { readdir as readdir7, stat as stat3, unlink } from "fs/promises";
3491
+ import path22 from "path";
3492
+ var DAY_MS = 24 * 60 * 60 * 1e3;
3493
+ var IDLE_SESSION_RETENTION_MS = 7 * DAY_MS;
3494
+ var ATTACHED_SESSION_RETENTION_MS = 30 * DAY_MS;
3495
+ var MAX_SESSION_FILES = 100;
3154
3496
  function parseSessionFile(content) {
3155
3497
  try {
3156
- return JSON.parse(content);
3498
+ const parsed = JSON.parse(content);
3499
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
3157
3500
  } catch {
3158
3501
  return null;
3159
3502
  }
3160
3503
  }
3161
3504
  function getSessionDir(cwd) {
3162
- return path21.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3505
+ return path22.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3163
3506
  }
3164
3507
  async function listSessionFiles(cwd) {
3165
3508
  const dir = getSessionDir(cwd);
@@ -3167,11 +3510,11 @@ async function listSessionFiles(cwd) {
3167
3510
  return [];
3168
3511
  }
3169
3512
  const entries = [];
3170
- for (const name of await readdir6(dir)) {
3513
+ for (const name of await readdir7(dir)) {
3171
3514
  if (!name.endsWith(".json")) {
3172
3515
  continue;
3173
3516
  }
3174
- const filePath = path21.join(dir, name);
3517
+ const filePath = path22.join(dir, name);
3175
3518
  const content = await readTextIfExists(filePath);
3176
3519
  if (!content) {
3177
3520
  continue;
@@ -3188,12 +3531,144 @@ async function listSessionFiles(cwd) {
3188
3531
  }
3189
3532
  return entries.sort((left, right) => left.key.localeCompare(right.key));
3190
3533
  }
3534
+ async function cleanSessionRuntime(cwd, options = {}) {
3535
+ const now = Date.now();
3536
+ const idleRetentionMs = options.idleRetentionMs ?? IDLE_SESSION_RETENTION_MS;
3537
+ const attachedRetentionMs = options.attachedRetentionMs ?? ATTACHED_SESSION_RETENTION_MS;
3538
+ const maxSessions = options.maxSessions ?? MAX_SESSION_FILES;
3539
+ const reserveSlots = options.reserveSlots ?? 0;
3540
+ const candidates = await listSessionCleanupCandidates(cwd);
3541
+ const removed = /* @__PURE__ */ new Set();
3542
+ for (const candidate of candidates) {
3543
+ const retentionMs = candidate.attached ? attachedRetentionMs : idleRetentionMs;
3544
+ if (now - candidate.activityTime <= retentionMs) {
3545
+ continue;
3546
+ }
3547
+ if (await unlinkIfUnchanged(candidate)) {
3548
+ removed.add(candidate.filePath);
3549
+ }
3550
+ }
3551
+ const allowedExistingSessions = Math.max(0, maxSessions - reserveSlots);
3552
+ const remaining = candidates.filter((candidate) => !removed.has(candidate.filePath)).sort((left, right) => left.activityTime - right.activityTime);
3553
+ for (const candidate of remaining.slice(
3554
+ 0,
3555
+ Math.max(0, remaining.length - allowedExistingSessions)
3556
+ )) {
3557
+ if (await unlinkIfUnchanged(candidate)) {
3558
+ removed.add(candidate.filePath);
3559
+ }
3560
+ }
3561
+ return {
3562
+ sessionsRemoved: removed.size,
3563
+ acceptanceSnapshotsRemoved: await cleanOrphanAcceptanceSnapshots(cwd)
3564
+ };
3565
+ }
3566
+ async function listSessionCleanupCandidates(cwd) {
3567
+ const dir = getSessionDir(cwd);
3568
+ if (!await pathExists(dir)) {
3569
+ return [];
3570
+ }
3571
+ const candidates = [];
3572
+ for (const entry of await readdir7(dir, { withFileTypes: true })) {
3573
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
3574
+ continue;
3575
+ }
3576
+ const filePath = path22.join(dir, entry.name);
3577
+ try {
3578
+ const [content, fileStat] = await Promise.all([readTextFile(filePath), stat3(filePath)]);
3579
+ const session = parseSessionFile(content);
3580
+ const activityValue = session?.last_active_at ?? session?.created_at;
3581
+ const parsedActivity = typeof activityValue === "string" ? new Date(activityValue).getTime() : Number.NaN;
3582
+ candidates.push({
3583
+ filePath,
3584
+ content,
3585
+ activityTime: Number.isNaN(parsedActivity) ? fileStat.mtimeMs : parsedActivity,
3586
+ attached: Boolean(session?.current_task)
3587
+ });
3588
+ } catch (error) {
3589
+ if (!isFileNotFound(error)) {
3590
+ throw error;
3591
+ }
3592
+ }
3593
+ }
3594
+ return candidates;
3595
+ }
3596
+ async function unlinkIfUnchanged(candidate) {
3597
+ try {
3598
+ if (await readTextFile(candidate.filePath) !== candidate.content) {
3599
+ return false;
3600
+ }
3601
+ await unlink(candidate.filePath);
3602
+ return true;
3603
+ } catch (error) {
3604
+ if (isFileNotFound(error)) {
3605
+ return false;
3606
+ }
3607
+ throw error;
3608
+ }
3609
+ }
3610
+ async function cleanOrphanAcceptanceSnapshots(cwd) {
3611
+ const acceptanceDir = path22.join(getSessionDir(cwd), "acceptance");
3612
+ if (!await pathExists(acceptanceDir)) {
3613
+ return 0;
3614
+ }
3615
+ let removed = 0;
3616
+ for (const entry of await readdir7(acceptanceDir, { withFileTypes: true })) {
3617
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
3618
+ continue;
3619
+ }
3620
+ const snapshotPath = path22.join(acceptanceDir, entry.name);
3621
+ if (!await isOrphanAcceptanceSnapshot(cwd, snapshotPath, entry.name.slice(0, -5))) {
3622
+ continue;
3623
+ }
3624
+ try {
3625
+ await unlink(snapshotPath);
3626
+ removed++;
3627
+ } catch (error) {
3628
+ if (!isFileNotFound(error)) {
3629
+ throw error;
3630
+ }
3631
+ }
3632
+ }
3633
+ return removed;
3634
+ }
3635
+ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
3636
+ let taskContent;
3637
+ try {
3638
+ taskContent = await readTextFile(
3639
+ path22.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json")
3640
+ );
3641
+ } catch (error) {
3642
+ if (isFileNotFound(error)) {
3643
+ return true;
3644
+ }
3645
+ throw error;
3646
+ }
3647
+ let parsedTask;
3648
+ try {
3649
+ parsedTask = JSON.parse(taskContent);
3650
+ } catch {
3651
+ return false;
3652
+ }
3653
+ if (typeof parsedTask !== "object" || parsedTask === null || Array.isArray(parsedTask)) {
3654
+ return false;
3655
+ }
3656
+ const task = parsedTask;
3657
+ if (task.status === "COMPLETE" || task.status === "CLOSED") {
3658
+ return true;
3659
+ }
3660
+ const checkpoint = task.verification_checkpoint;
3661
+ return typeof checkpoint?.snapshot_file !== "string" || path22.resolve(cwd, checkpoint.snapshot_file) !== path22.resolve(snapshotPath);
3662
+ }
3663
+ function isFileNotFound(error) {
3664
+ return error.code === "ENOENT";
3665
+ }
3191
3666
 
3192
3667
  // src/commands/status.ts
3193
3668
  async function status() {
3194
3669
  renderBanner();
3195
3670
  const cwd = process.cwd();
3196
- const configPath2 = path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3671
+ const configPath2 = path23.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3197
3672
  if (!await pathExists(configPath2)) {
3198
3673
  throw new Error("No easy-coding harness found in this project.");
3199
3674
  }
@@ -3203,6 +3678,7 @@ async function status() {
3203
3678
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
3204
3679
  const sessions = await listSessionFiles(cwd);
3205
3680
  const versionRelation = compareVersions(config2.harness_version, VERSION);
3681
+ const tddReadiness = await inspectTddReadiness(cwd);
3206
3682
  console.log(chalk6.bold("Harness"));
3207
3683
  console.log(` version: ${config2.harness_version}`);
3208
3684
  console.log(` cli: ${VERSION}`);
@@ -3218,14 +3694,28 @@ async function status() {
3218
3694
  const migratedBehavior = resolveLegacyBehavior(config2);
3219
3695
  const projectApprovalMode = isApprovalMode(config2.behavior?.approval_mode) ? config2.behavior.approval_mode : migratedBehavior.approvalMode;
3220
3696
  const projectWorkflowMode = isConfiguredWorkflowMode(config2.behavior?.workflow_mode) ? config2.behavior.workflow_mode : migratedBehavior.workflowMode;
3697
+ const projectTddEnabled = migratedBehavior.tddEnabled;
3698
+ const projectTddCoverageThreshold = migratedBehavior.tddCoverageThreshold;
3221
3699
  console.log(` approval_mode: ${projectApprovalMode}`);
3222
3700
  console.log(` workflow_mode: ${projectWorkflowMode}`);
3701
+ console.log(` tdd_enabled: ${projectTddEnabled}`);
3702
+ console.log(` tdd_coverage_threshold: ${projectTddCoverageThreshold}`);
3703
+ console.log(` tdd_readiness: ${tddReadiness.status}`);
3704
+ if (tddReadiness.status !== "ready") {
3705
+ console.log(` tdd_readiness_reasons: ${tddReadiness.reasons.join("; ")}`);
3706
+ }
3223
3707
  console.log("");
3224
3708
  console.log(chalk6.bold("Sessions"));
3225
3709
  console.log(` project_approval_mode: ${projectApprovalMode}`);
3226
3710
  console.log(` project_workflow_mode: ${projectWorkflowMode}`);
3711
+ console.log(` project_tdd_enabled: ${projectTddEnabled}`);
3712
+ console.log(` project_tdd_coverage_threshold: ${projectTddCoverageThreshold}`);
3227
3713
  console.log(` effective_approval_mode: ${projectApprovalMode} (without a session override)`);
3228
3714
  console.log(` configured_workflow_mode: ${projectWorkflowMode} (without a session override)`);
3715
+ console.log(` effective_tdd_enabled: ${projectTddEnabled} (without a session override)`);
3716
+ console.log(
3717
+ ` effective_tdd_coverage_threshold: ${projectTddCoverageThreshold} (without a session override)`
3718
+ );
3229
3719
  if (sessions.length === 0) {
3230
3720
  console.log(" no session files");
3231
3721
  }
@@ -3236,13 +3726,21 @@ async function status() {
3236
3726
  );
3237
3727
  const sessionApprovalMode = session.approval_mode ?? (legacySessionMode === "lite" ? "guard" : legacySessionMode);
3238
3728
  const sessionWorkflowMode = session.workflow_mode ?? (legacySessionMode === "lite" ? "fast" : hasLegacySessionMode ? "adaptive" : void 0);
3729
+ const sessionTddEnabled = session.tdd_enabled;
3730
+ const sessionTddCoverageThreshold = session.tdd_coverage_threshold;
3239
3731
  console.log(` - ${key}`);
3240
3732
  console.log(` agent: ${session.agent ?? "legacy/unknown"}`);
3241
3733
  console.log(` source: ${session.session_source ?? "legacy"}`);
3242
3734
  console.log(` approval_mode: ${sessionApprovalMode ?? "project default"}`);
3243
3735
  console.log(` workflow_mode: ${sessionWorkflowMode ?? "project default"}`);
3736
+ console.log(` tdd_enabled: ${sessionTddEnabled ?? "project default"}`);
3737
+ console.log(` tdd_coverage_threshold: ${sessionTddCoverageThreshold ?? "project default"}`);
3244
3738
  console.log(` effective_approval_mode: ${sessionApprovalMode ?? projectApprovalMode}`);
3245
3739
  console.log(` configured_workflow_mode: ${sessionWorkflowMode ?? projectWorkflowMode}`);
3740
+ console.log(` effective_tdd_enabled: ${sessionTddEnabled ?? projectTddEnabled}`);
3741
+ console.log(
3742
+ ` effective_tdd_coverage_threshold: ${sessionTddCoverageThreshold ?? projectTddCoverageThreshold}`
3743
+ );
3246
3744
  console.log(
3247
3745
  ` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
3248
3746
  );
@@ -3258,6 +3756,10 @@ async function status() {
3258
3756
  console.log(
3259
3757
  ` task_workflow_mode: ${task.workflow_mode ?? task.workflow_mode_proposal?.selected_mode ?? "not resolved"}`
3260
3758
  );
3759
+ console.log(` task_tdd_enabled: ${task.tdd_enabled ?? "not frozen"}`);
3760
+ console.log(
3761
+ ` task_tdd_coverage_threshold: ${task.tdd_coverage_threshold ?? "not frozen"}`
3762
+ );
3261
3763
  console.log(` last_agent: ${task.last_agent}`);
3262
3764
  } else {
3263
3765
  console.log(` current_task: ${session.current_task} (task.json missing)`);
@@ -3309,7 +3811,7 @@ async function update(opts) {
3309
3811
  }
3310
3812
 
3311
3813
  // src/commands/upgrade.ts
3312
- import path23 from "path";
3814
+ import path24 from "path";
3313
3815
  import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
3314
3816
  import chalk8 from "chalk";
3315
3817
  var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
@@ -3371,8 +3873,9 @@ async function upgrade(opts) {
3371
3873
  ].filter(Boolean) : [],
3372
3874
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
3373
3875
  "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."
3876
+ "Will migrate behavior config to schema 5 and disable unready project/session TDD settings.",
3877
+ "Will prune expired session bindings and orphan acceptance snapshots in each upgraded target while preserving tasks, memory, spec, and project knowledge.",
3878
+ "Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
3376
3879
  ].join("\n");
3377
3880
  if (opts.dryRun) {
3378
3881
  console.log(summary);
@@ -3389,6 +3892,15 @@ async function upgrade(opts) {
3389
3892
  }
3390
3893
  }
3391
3894
  for (const { target, config: config2 } of pending) {
3895
+ const sessionCleanup = await cleanSessionRuntime(target.dir);
3896
+ if (sessionCleanup.sessionsRemoved > 0 || sessionCleanup.acceptanceSnapshotsRemoved > 0) {
3897
+ console.log(
3898
+ chalk8.yellow(
3899
+ `${target.label}: session GC removed ${sessionCleanup.sessionsRemoved} session file(s) and ${sessionCleanup.acceptanceSnapshotsRemoved} orphan acceptance snapshot(s).`
3900
+ )
3901
+ );
3902
+ }
3903
+ const beta1ProjectTddRequested = Number(config2.version) === 4 && config2.behavior?.tdd_enabled === true;
3392
3904
  const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
3393
3905
  supermodule: target.supermodule
3394
3906
  });
@@ -3405,6 +3917,14 @@ async function upgrade(opts) {
3405
3917
  await ensureHookBytecodeIgnored(target.dir);
3406
3918
  await migrateLegacyWorkflowState(target.dir);
3407
3919
  await migrateBehaviorConfig(target.configPath);
3920
+ const disabledSessionTddOverrides = await disableUnreadySessionTddOverrides(target.dir);
3921
+ if (beta1ProjectTddRequested || disabledSessionTddOverrides > 0) {
3922
+ console.log(
3923
+ chalk8.yellow(
3924
+ `${target.label}: TDD remains off until ec-tdd-init succeeds (project=${beta1ProjectTddRequested ? "disabled" : "unchanged"}, sessions_disabled=${disabledSessionTddOverrides}).`
3925
+ )
3926
+ );
3927
+ }
3408
3928
  await updateHarnessVersion(target.configPath, VERSION);
3409
3929
  await updateSupermoduleConfig(target.configPath, target.supermodule);
3410
3930
  await setPendingInitSince(target.dir, VERSION);
@@ -3460,7 +3980,7 @@ async function needsHookConfigRefresh(target, config2) {
3460
3980
  const manifest = await readInstallManifest(target.dir);
3461
3981
  for (const agent of config2.agents) {
3462
3982
  const meta = resolvePlatformMeta(target.dir, agent);
3463
- const configPath2 = path23.join(target.dir, meta.hookConfigFile);
3983
+ const configPath2 = path24.join(target.dir, meta.hookConfigFile);
3464
3984
  const content = await readTextIfExists(configPath2);
3465
3985
  if (content === null) {
3466
3986
  return true;
@@ -3600,7 +4120,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
3600
4120
  return true;
3601
4121
  }
3602
4122
  return pathAliases(
3603
- normalizePathForHookComparison(path23.resolve(cwd, relativeHookPath))
4123
+ normalizePathForHookComparison(path24.resolve(cwd, relativeHookPath))
3604
4124
  ).includes(normalizedHookPath);
3605
4125
  });
3606
4126
  }