easy-coding-harness 0.10.0-beta.1 → 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 (33) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +50 -20
  3. package/dist/cli.js +478 -47
  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 +18 -6
  11. package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +27 -12
  12. package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +1 -1
  13. package/templates/common/skills/ec-analysis/SKILL.md +141 -35
  14. package/templates/common/skills/ec-config/SKILL.md +24 -2
  15. package/templates/common/skills/ec-git/SKILL.md +7 -1
  16. package/templates/common/skills/ec-implementing/SKILL.md +61 -1
  17. package/templates/common/skills/ec-memory/SKILL.md +76 -6
  18. package/templates/common/skills/ec-reviewing/SKILL.md +21 -3
  19. package/templates/common/skills/ec-task-close/SKILL.md +4 -0
  20. package/templates/common/skills/ec-task-management/SKILL.md +7 -1
  21. package/templates/common/skills/ec-tdd-init/SKILL.md +101 -0
  22. package/templates/common/skills/ec-verification/SKILL.md +68 -26
  23. package/templates/common/skills/ec-workflow/SKILL.md +81 -22
  24. package/templates/main-constraint/AGENTS.md.tpl +49 -14
  25. package/templates/main-constraint/CLAUDE.md.tpl +46 -14
  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_tdd_readiness.py +306 -0
  30. package/templates/shared-hooks/easy_coding_state.py +3878 -259
  31. package/templates/shared-hooks/easy_dev_spec.py +444 -30
  32. package/templates/shared-hooks/easy_dev_spec_execution.py +1014 -0
  33. package/templates/shared-hooks/easy_dev_spec_protocol.py +1426 -18
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 -->";
@@ -308,6 +311,7 @@ var PLATFORM_META = {
308
311
  stateInjectEvent: ["SessionStart", "UserPromptSubmit"],
309
312
  hasSubagentContext: true,
310
313
  templateContext: {
314
+ workflow_agent_id: "claude-code",
311
315
  sub_agent_dispatch: "Agent tool",
312
316
  platform_spawn_instruction: 'Use the Agent tool with run_in_background when useful; use isolation: "worktree" for parallel file edits.',
313
317
  skill_trigger: "/",
@@ -331,6 +335,7 @@ var PLATFORM_META = {
331
335
  stateInjectEvent: ["SessionStart", "UserPromptSubmit"],
332
336
  hasSubagentContext: false,
333
337
  templateContext: {
338
+ workflow_agent_id: "codex",
334
339
  sub_agent_dispatch: "Codex sub-agent dispatch",
335
340
  platform_spawn_instruction: "Use Codex sub-agent delegation where available; pass the full task card in the prompt.",
336
341
  skill_trigger: "$",
@@ -355,6 +360,7 @@ var PLATFORM_META = {
355
360
  hasSubagentContext: true,
356
361
  cnVariant: ".qodercn",
357
362
  templateContext: {
363
+ workflow_agent_id: "qoder",
358
364
  sub_agent_dispatch: "Agent tool",
359
365
  platform_spawn_instruction: "Use the Agent tool with worktree isolation for parallel file edits.",
360
366
  skill_trigger: "/",
@@ -681,8 +687,10 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
681
687
  async function writeToolsScaffold(easyCodingDir) {
682
688
  const toolsDir = path6.join(easyCodingDir, TOOLS_DIR);
683
689
  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));
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
+ }
686
694
  }
687
695
  async function writeTemplatesScaffold(easyCodingDir) {
688
696
  const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
@@ -771,6 +779,87 @@ function isLegacyStage(value) {
771
779
  function migrateStage(value) {
772
780
  return isLegacyStage(value) ? LEGACY_STAGE_MAP[value] : value;
773
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
+ }
774
863
  function migrateStageHistory(task) {
775
864
  if (!Array.isArray(task.stage_history)) return false;
776
865
  let changed = false;
@@ -801,7 +890,8 @@ function migrateTaskWorkflowState(task) {
801
890
  const legacyRequestedAt = Array.isArray(task.stage_history) ? [...task.stage_history].reverse().find(
802
891
  (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && entry.stage === "WAITING_CONFIRM"
803
892
  ) : null;
804
- let changed = migrateStageHistory(task);
893
+ let changed = migrateTaskAgentIdentities(task);
894
+ changed = migrateStageHistory(task) || changed;
805
895
  if (legacyStatus) {
806
896
  task.status = LEGACY_STAGE_MAP[legacyStatus];
807
897
  changed = true;
@@ -853,7 +943,7 @@ function migrateTaskWorkflowState(task) {
853
943
  return changed;
854
944
  }
855
945
  function migrateSessionBehavior(session) {
856
- let changed = false;
946
+ let changed = migrateAgentFields(session, ["agent", "last_agent"]);
857
947
  const legacyMode = session.confirm_mode;
858
948
  const legacyLite = legacyMode === "lite";
859
949
  if (!["approve", "guard", "confirm", "auto"].includes(String(session.approval_mode ?? ""))) {
@@ -964,7 +1054,7 @@ async function hasLegacyWorkflowState(cwd) {
964
1054
  if (!await pathExists(filePath)) continue;
965
1055
  const task = await readJsonRecord(filePath);
966
1056
  if (!task) continue;
967
- 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(
968
1058
  (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && isLegacyStage(entry.stage)
969
1059
  )) {
970
1060
  return true;
@@ -973,7 +1063,12 @@ async function hasLegacyWorkflowState(cwd) {
973
1063
  for (const filePath of await sessionFiles(cwd)) {
974
1064
  const session = await readJsonRecord(filePath);
975
1065
  if (!session) continue;
976
- 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
+ }
977
1072
  }
978
1073
  return false;
979
1074
  }
@@ -2772,7 +2867,7 @@ function renderTargetPlans(targetPlans) {
2772
2867
  }
2773
2868
 
2774
2869
  // src/commands/config.ts
2775
- import path18 from "path";
2870
+ import path19 from "path";
2776
2871
  import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
2777
2872
  import chalk4 from "chalk";
2778
2873
 
@@ -2851,10 +2946,169 @@ async function checkForUpgrade(cwd) {
2851
2946
  }
2852
2947
  }
2853
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
+
2854
3108
  // src/commands/config.ts
2855
3109
  async function config() {
2856
3110
  renderBanner();
2857
- const configPath2 = path18.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
3111
+ const configPath2 = path19.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
2858
3112
  if (!await pathExists(configPath2)) {
2859
3113
  throw new Error("No easy-coding harness found in this project.");
2860
3114
  }
@@ -2898,7 +3152,7 @@ async function config() {
2898
3152
  {
2899
3153
  value: "auto",
2900
3154
  label: "auto \u2014 advance workflow stages automatically",
2901
- hint: "task closure remains explicit"
3155
+ hint: "only new post-verification code drift pauses for exact acceptance"
2902
3156
  }
2903
3157
  ]
2904
3158
  });
@@ -2948,6 +3202,15 @@ async function config() {
2948
3202
  cancel4("Configuration cancelled.");
2949
3203
  return;
2950
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
+ }
2951
3214
  let tddCoverageThreshold = current.tddCoverageThreshold;
2952
3215
  if (tddEnabled) {
2953
3216
  const thresholdInput = await text({
@@ -2972,6 +3235,15 @@ async function config() {
2972
3235
  cancel4("Configuration cancelled.");
2973
3236
  return;
2974
3237
  }
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
+ }
2975
3247
  await setBehaviorModes(configPath2, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold);
2976
3248
  outro3(
2977
3249
  chalk4.green(
@@ -2981,20 +3253,20 @@ async function config() {
2981
3253
  }
2982
3254
 
2983
3255
  // src/commands/init.ts
2984
- import path20 from "path";
3256
+ import path21 from "path";
2985
3257
  import { note, outro as outro4 } from "@clack/prompts";
2986
3258
  import chalk5 from "chalk";
2987
3259
 
2988
3260
  // src/utils/install-state.ts
2989
- import { readdir as readdir5 } from "fs/promises";
2990
- import path19 from "path";
3261
+ import { readdir as readdir6 } from "fs/promises";
3262
+ import path20 from "path";
2991
3263
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
2992
3264
  async function detectEasyCodingInstallState(cwd) {
2993
- const easyCodingDir = path19.join(cwd, EASY_CODING_DIR);
3265
+ const easyCodingDir = path20.join(cwd, EASY_CODING_DIR);
2994
3266
  if (!await pathExists(easyCodingDir)) {
2995
3267
  return { kind: "fresh", easyCodingDir };
2996
3268
  }
2997
- const configPath2 = path19.join(easyCodingDir, CONFIG_FILE);
3269
+ const configPath2 = path20.join(easyCodingDir, CONFIG_FILE);
2998
3270
  if (await pathExists(configPath2)) {
2999
3271
  return { kind: "installed", easyCodingDir, configPath: configPath2 };
3000
3272
  }
@@ -3012,17 +3284,17 @@ async function detectEasyCodingInstallState(cwd) {
3012
3284
  async function detectLegacyAssets(easyCodingDir) {
3013
3285
  const assets = [];
3014
3286
  for (const file of LEGACY_ROOT_FILES) {
3015
- if (await pathExists(path19.join(easyCodingDir, file))) {
3287
+ if (await pathExists(path20.join(easyCodingDir, file))) {
3016
3288
  assets.push(relativeEasyCodingPath(file));
3017
3289
  }
3018
3290
  }
3019
- if (await pathExists(path19.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3291
+ if (await pathExists(path20.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
3020
3292
  assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
3021
3293
  }
3022
- const shortMemoryFiles = await listMarkdownFiles(path19.join(easyCodingDir, "memory", "short"));
3294
+ const shortMemoryFiles = await listMarkdownFiles(path20.join(easyCodingDir, "memory", "short"));
3023
3295
  assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
3024
3296
  for (const dir of ["spec", "prototype"]) {
3025
- if (await hasAnyDirectoryEntry(path19.join(easyCodingDir, dir))) {
3297
+ if (await hasAnyDirectoryEntry(path20.join(easyCodingDir, dir))) {
3026
3298
  assets.push(relativeEasyCodingPath(dir));
3027
3299
  }
3028
3300
  }
@@ -3032,23 +3304,23 @@ async function listMarkdownFiles(dir) {
3032
3304
  if (!await isDirectory(dir)) {
3033
3305
  return [];
3034
3306
  }
3035
- const entries = await readdir5(dir, { withFileTypes: true });
3307
+ const entries = await readdir6(dir, { withFileTypes: true });
3036
3308
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
3037
3309
  }
3038
3310
  async function hasAnyDirectoryEntry(dir) {
3039
3311
  if (!await isDirectory(dir)) {
3040
3312
  return false;
3041
3313
  }
3042
- return (await readdir5(dir)).length > 0;
3314
+ return (await readdir6(dir)).length > 0;
3043
3315
  }
3044
3316
  function relativeEasyCodingPath(...segments) {
3045
- return path19.posix.join(EASY_CODING_DIR, ...segments);
3317
+ return path20.posix.join(EASY_CODING_DIR, ...segments);
3046
3318
  }
3047
3319
  function relativeConfigPath() {
3048
- return path19.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3320
+ return path20.posix.join(EASY_CODING_DIR, CONFIG_FILE);
3049
3321
  }
3050
3322
  function relativeProjectInitTaskPath() {
3051
- 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");
3052
3324
  }
3053
3325
 
3054
3326
  // src/commands/init.ts
@@ -3129,7 +3401,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
3129
3401
  if (!targetSubmodulePaths.has(entry.path)) {
3130
3402
  continue;
3131
3403
  }
3132
- const dir = path20.join(cwd, entry.path);
3404
+ const dir = path21.join(cwd, entry.path);
3133
3405
  targets.push(
3134
3406
  await targetFromState(dir, entry.path, "submodule-child", {
3135
3407
  parent: toPosixRelative2(dir, cwd)
@@ -3176,24 +3448,24 @@ function contextFromState(role, installState) {
3176
3448
  };
3177
3449
  }
3178
3450
  function toPosixRelative2(from, to) {
3179
- const relative = path20.relative(from, to);
3180
- return relative ? relative.split(path20.sep).join("/") : ".";
3451
+ const relative = path21.relative(from, to);
3452
+ return relative ? relative.split(path21.sep).join("/") : ".";
3181
3453
  }
3182
3454
  async function resolveInitPlatforms(cwd, opts, parentInstalled) {
3183
3455
  if (opts.agent || !parentInstalled) {
3184
3456
  return resolvePlatforms(opts, ["claude-code"]);
3185
3457
  }
3186
- 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));
3187
3459
  if (Array.isArray(config2.agents) && config2.agents.length > 0) {
3188
3460
  return config2.agents;
3189
3461
  }
3190
3462
  return resolvePlatforms(opts, ["claude-code"]);
3191
3463
  }
3192
3464
  async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
3193
- if (!await pathExists(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3465
+ if (!await pathExists(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
3194
3466
  return;
3195
3467
  }
3196
- 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 };
3197
3469
  const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
3198
3470
  await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
3199
3471
  }
@@ -3202,7 +3474,7 @@ async function refreshInstalledChildTopologies(targets) {
3202
3474
  if (!target.installed || target.context.role !== "submodule-child") {
3203
3475
  continue;
3204
3476
  }
3205
- const configPath2 = path20.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3477
+ const configPath2 = path21.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
3206
3478
  if (!await pathExists(configPath2)) {
3207
3479
  continue;
3208
3480
  }
@@ -3211,22 +3483,26 @@ async function refreshInstalledChildTopologies(targets) {
3211
3483
  }
3212
3484
 
3213
3485
  // src/commands/status.ts
3214
- import path22 from "path";
3486
+ import path23 from "path";
3215
3487
  import chalk6 from "chalk";
3216
3488
 
3217
3489
  // src/utils/session.ts
3218
- import { readdir as readdir6, unlink } from "fs/promises";
3219
- import path21 from "path";
3220
- 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;
3221
3496
  function parseSessionFile(content) {
3222
3497
  try {
3223
- return JSON.parse(content);
3498
+ const parsed = JSON.parse(content);
3499
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
3224
3500
  } catch {
3225
3501
  return null;
3226
3502
  }
3227
3503
  }
3228
3504
  function getSessionDir(cwd) {
3229
- return path21.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3505
+ return path22.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
3230
3506
  }
3231
3507
  async function listSessionFiles(cwd) {
3232
3508
  const dir = getSessionDir(cwd);
@@ -3234,11 +3510,11 @@ async function listSessionFiles(cwd) {
3234
3510
  return [];
3235
3511
  }
3236
3512
  const entries = [];
3237
- for (const name of await readdir6(dir)) {
3513
+ for (const name of await readdir7(dir)) {
3238
3514
  if (!name.endsWith(".json")) {
3239
3515
  continue;
3240
3516
  }
3241
- const filePath = path21.join(dir, name);
3517
+ const filePath = path22.join(dir, name);
3242
3518
  const content = await readTextIfExists(filePath);
3243
3519
  if (!content) {
3244
3520
  continue;
@@ -3255,12 +3531,144 @@ async function listSessionFiles(cwd) {
3255
3531
  }
3256
3532
  return entries.sort((left, right) => left.key.localeCompare(right.key));
3257
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
+ }
3258
3666
 
3259
3667
  // src/commands/status.ts
3260
3668
  async function status() {
3261
3669
  renderBanner();
3262
3670
  const cwd = process.cwd();
3263
- const configPath2 = path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3671
+ const configPath2 = path23.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
3264
3672
  if (!await pathExists(configPath2)) {
3265
3673
  throw new Error("No easy-coding harness found in this project.");
3266
3674
  }
@@ -3270,6 +3678,7 @@ async function status() {
3270
3678
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
3271
3679
  const sessions = await listSessionFiles(cwd);
3272
3680
  const versionRelation = compareVersions(config2.harness_version, VERSION);
3681
+ const tddReadiness = await inspectTddReadiness(cwd);
3273
3682
  console.log(chalk6.bold("Harness"));
3274
3683
  console.log(` version: ${config2.harness_version}`);
3275
3684
  console.log(` cli: ${VERSION}`);
@@ -3291,6 +3700,10 @@ async function status() {
3291
3700
  console.log(` workflow_mode: ${projectWorkflowMode}`);
3292
3701
  console.log(` tdd_enabled: ${projectTddEnabled}`);
3293
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
+ }
3294
3707
  console.log("");
3295
3708
  console.log(chalk6.bold("Sessions"));
3296
3709
  console.log(` project_approval_mode: ${projectApprovalMode}`);
@@ -3398,7 +3811,7 @@ async function update(opts) {
3398
3811
  }
3399
3812
 
3400
3813
  // src/commands/upgrade.ts
3401
- import path23 from "path";
3814
+ import path24 from "path";
3402
3815
  import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
3403
3816
  import chalk8 from "chalk";
3404
3817
  var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
@@ -3460,7 +3873,8 @@ async function upgrade(opts) {
3460
3873
  ].filter(Boolean) : [],
3461
3874
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
3462
3875
  "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.",
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.",
3464
3878
  "Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
3465
3879
  ].join("\n");
3466
3880
  if (opts.dryRun) {
@@ -3478,6 +3892,15 @@ async function upgrade(opts) {
3478
3892
  }
3479
3893
  }
3480
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;
3481
3904
  const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
3482
3905
  supermodule: target.supermodule
3483
3906
  });
@@ -3494,6 +3917,14 @@ async function upgrade(opts) {
3494
3917
  await ensureHookBytecodeIgnored(target.dir);
3495
3918
  await migrateLegacyWorkflowState(target.dir);
3496
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
+ }
3497
3928
  await updateHarnessVersion(target.configPath, VERSION);
3498
3929
  await updateSupermoduleConfig(target.configPath, target.supermodule);
3499
3930
  await setPendingInitSince(target.dir, VERSION);
@@ -3549,7 +3980,7 @@ async function needsHookConfigRefresh(target, config2) {
3549
3980
  const manifest = await readInstallManifest(target.dir);
3550
3981
  for (const agent of config2.agents) {
3551
3982
  const meta = resolvePlatformMeta(target.dir, agent);
3552
- const configPath2 = path23.join(target.dir, meta.hookConfigFile);
3983
+ const configPath2 = path24.join(target.dir, meta.hookConfigFile);
3553
3984
  const content = await readTextIfExists(configPath2);
3554
3985
  if (content === null) {
3555
3986
  return true;
@@ -3689,7 +4120,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
3689
4120
  return true;
3690
4121
  }
3691
4122
  return pathAliases(
3692
- normalizePathForHookComparison(path23.resolve(cwd, relativeHookPath))
4123
+ normalizePathForHookComparison(path24.resolve(cwd, relativeHookPath))
3693
4124
  ).includes(normalizedHookPath);
3694
4125
  });
3695
4126
  }