easy-coding-harness 1.0.0-beta.2 → 1.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@
6
6
  - `y`:常规功能升级
7
7
  - `z`:日常 bug 修复
8
8
 
9
+ ## 1.0.1-beta.0
10
+
11
+ - TDD 日常 readiness 检查保留初始化入口和参数契约校验,取消构建文件、CI 和工具文件与
12
+ 初始化快照必须逐字一致的要求。Maven 版本、依赖和格式变更不再要求重新初始化;缺少
13
+ 凭据提示首次初始化,已有入口损坏提示具体修复,本地测试和差异覆盖率仍负责最终验收。
14
+ - 升级保留项目/session TDD 开关、覆盖率阈值与进行中任务的冻结配置;readiness 异常和
15
+ 工具更新不再自动关闭 TDD,升级清理保留带有显式 TDD 配置的 session。
16
+ - Canonical Spec 接手恢复绑定原稿的选中消费范围,核对设计版本并公开待同步状态;确认的
17
+ 需求变更先登记到任务,原稿修订与设计同步完成后才能继续实施和验收。
18
+ - 修复跨 Agent 恢复设计同步时把任务负责人改回原事件作者的问题;重放保持原事件归属与
19
+ 幂等键,当前负责人和会话保持为实际接手者。
20
+
21
+ ## 1.0.0
22
+
23
+ - 首个正式版本,固化统一的 `INIT → ANALYSIS → IMPLEMENT → QUALITY → MEMORY → COMPLETE`
24
+ 状态机;Fast、Standard、Strict 共享状态图,仅按任务复杂度和风险调整执行深度。
25
+ - 正式提供 Canonical Spec 精确消费、持久分析方案回执、双门 QUALITY、`ec-lite` 极简模式、
26
+ 会话清理与用户验收后差异接受等完整能力,并保持 Claude Code、Codex、Qoder 三端一致。
27
+ - 稳定编码约束:遵循最近邻代码风格和最小修改,避免过度设计、冗余校验、碎片方法与无意义
28
+ 常量;接口方法已有完整文档且实现无额外语义时,不重复编写实现方法 Javadoc。
29
+ - 正式版由 `1.0.0-beta.2` 晋升,不新增破坏性变更;默认安装命令将通过 npm `latest` 获取
30
+ `1.0.0`,预发布用户可直接升级。
31
+
9
32
  ## 1.0.0-beta.2
10
33
 
11
34
  - 核心 Java Javadoc 规则新增接口文档继承例外:接口方法已有完整、准确且可访问的
package/README.md CHANGED
@@ -213,16 +213,27 @@ easy-coding upgrade
213
213
  `behavior.approval_mode` 与 `behavior.workflow_mode`;旧 `lite` 映射为 `guard + fast`。
214
214
  项目级模式用 `easy-coding config` 修改(要求
215
215
  项目 Harness 与 CLI 版本完全一致,否则先执行 `easy-coding upgrade` 或更新 CLI);当前
216
- session 临时覆盖统一通过 `ec-config` 对话修改。升级到 0.10.0-beta.2 时配置 schema 升至
217
- 5;未完成 `ec-tdd-init` readiness 的项目/session TDD 请求迁移为关闭并保留阈值,同时
218
- 部署共享 Java 差异覆盖率与 readiness 工具。0.10.0-beta.3 起,TDD 业务任务只依赖本地
216
+ session 临时覆盖统一通过 `ec-config` 对话修改。当前配置 schema 5;升级保留项目和
217
+ session TDD 开关、阈值及继承关系,也保留任务已经冻结的基线。支持 TDD 的 schema 4
218
+ 迁移保留用户配置;readiness 缺失或损坏只报告问题,不自动关闭 TDD。TDD 业务任务只依赖本地
219
219
  单测与本地差异覆盖率,历史远程 CI 证据保留但不再参与验收。已经冻结的活动任务合同
220
220
  不会被静默改写。0.10.0-beta.4 起,仍停在 ANALYSIS 的旧任务必须补齐决策闭环后才能
221
221
  进入 IMPLEMENT;已经进入后续阶段的任务不受影响。
222
222
 
223
223
  session GC 只在创建新逻辑会话前和实际升级时触发:无任务绑定的会话保留 7 天、仍绑定
224
224
  任务的会话保留 30 天,并按最近活动时间将根目录 JSON 控制在 100 个以内。活动任务仍在
225
- 引用的 acceptance 验收快照会被保留;任务、记忆、Spec 和项目知识不参与清理。
225
+ 引用的 acceptance 验收快照会被保留;升级时带有显式 TDD 配置的 session 不参与清理。
226
+ 任务、记忆、Spec 和项目知识不参与清理。
227
+
228
+ TDD 凭据中的文件摘要是初始化历史。POM 版本、依赖、插件、CI 或托管工具变化无需重新
229
+ 初始化;本轮构建与工具变化会使旧验收证据失效。日常 readiness 只检查必要本地入口和
230
+ 参数契约;缺少凭据为 `needs_init`,已有凭据或入口损坏为 `needs_repair`。实际单测与
231
+ 增量覆盖率仍须通过。`ec-tdd-init` 的完整校验包含 GitLab CI。
232
+
233
+ 正式绑定的外部 Canonical Spec 在创建和接手时返回选中内容,恢复会话使用
234
+ `resume-spec-context`。需求确认后先用 `begin-spec-change` 登记,再更新绑定原稿、递增
235
+ revision、验证 READY 并执行 `sync-spec-design`;随后恢复上下文并更新派生计划。待同步
236
+ 变更会跨 Agent 保留并阻止实施和验收,交接摘要不能代替原稿。
226
237
 
227
238
  若当前会话不希望 Harness 接管,显式调用 `/ec-no-harness`(Codex 使用
228
239
  `$ec-no-harness`)。它只旁路 Easy Coding,不关闭其他 hooks,也不忽略其他 skills;
package/dist/cli.js CHANGED
@@ -191,8 +191,7 @@ function resolveLegacyBehavior(config2) {
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
193
  const supportsTddThreshold = Number(config2.version) >= 4;
194
- const supportsReadyTdd = Number(config2.version) >= CONFIG_SCHEMA_VERSION;
195
- const tddEnabled = supportsReadyTdd && behavior.tdd_enabled === true;
194
+ const tddEnabled = supportsTddThreshold && behavior.tdd_enabled === true;
196
195
  const tddCoverageThreshold = supportsTddThreshold && isTddCoverageThreshold(behavior.tdd_coverage_threshold) ? behavior.tdd_coverage_threshold : DEFAULT_TDD_COVERAGE_THRESHOLD;
197
196
  return { approvalMode, workflowMode, tddEnabled, tddCoverageThreshold };
198
197
  }
@@ -1009,15 +1008,11 @@ function migrateTaskWorkflowState(task) {
1009
1008
  }
1010
1009
  if (isActive && taskType !== "project-init" && typeof task.tdd_enabled !== "boolean") {
1011
1010
  task.tdd_enabled = false;
1012
- task.tdd_coverage_threshold = DEFAULT_TDD_COVERAGE_THRESHOLD;
1011
+ task.tdd_coverage_threshold ??= DEFAULT_TDD_COVERAGE_THRESHOLD;
1013
1012
  task.tdd_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
1014
1013
  task.tdd_confirmed_by = "upgrade-migration";
1015
1014
  changed = true;
1016
1015
  }
1017
- if (task.tdd_enabled === false && "tdd_baselines" in task) {
1018
- task.tdd_baselines = void 0;
1019
- changed = true;
1020
- }
1021
1016
  return changed;
1022
1017
  }
1023
1018
  function migrateSessionBehavior(session) {
@@ -3022,8 +3017,7 @@ async function checkForUpgrade(cwd) {
3022
3017
  }
3023
3018
 
3024
3019
  // src/utils/tdd-readiness.ts
3025
- import { createHash as createHash2 } from "crypto";
3026
- import { readFile as readFile4, readdir as readdir5, realpath as realpath2 } from "fs/promises";
3020
+ import { readFile as readFile4, realpath as realpath2 } from "fs/promises";
3027
3021
  import path18 from "path";
3028
3022
  var TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1";
3029
3023
  var TDD_READINESS_SCOPE = "changed-production-lines";
@@ -3031,7 +3025,6 @@ var TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA";
3031
3025
  var TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD";
3032
3026
  var COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py";
3033
3027
  var JAVA_BUILD_FILE_NAMES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
3034
- var GITLAB_CI_ENTRY_FILES = /* @__PURE__ */ new Set([".gitlab-ci.yml", ".gitlab-ci.yaml"]);
3035
3028
  function readinessPath(root) {
3036
3029
  return path18.join(root, EASY_CODING_DIR, TDD_DIR, TDD_READINESS_FILE);
3037
3030
  }
@@ -3047,11 +3040,11 @@ function parseFileRecords(value, field, reasons) {
3047
3040
  continue;
3048
3041
  }
3049
3042
  const record = item;
3050
- if (typeof record.path !== "string" || !record.path.trim() || path18.isAbsolute(record.path) || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
3051
- reasons.push(`${field} contains an invalid path or SHA-256`);
3043
+ if (typeof record.path !== "string" || !record.path.trim() || path18.isAbsolute(record.path)) {
3044
+ reasons.push(`${field} contains an invalid path`);
3052
3045
  continue;
3053
3046
  }
3054
- records.push({ path: record.path, sha256: record.sha256 });
3047
+ records.push({ path: record.path });
3055
3048
  }
3056
3049
  return records;
3057
3050
  }
@@ -3063,11 +3056,7 @@ function isSafeReportPattern(value) {
3063
3056
  const normalized = value.replaceAll("\\", "/");
3064
3057
  return !path18.isAbsolute(value) && !normalized.split("/").includes("..");
3065
3058
  }
3066
- function activeCiContent(contents) {
3067
- return contents.join("\n").split("\n").map((line) => line.replace(/^\s*#.*$/, "").replace(/\s+#.*$/, "")).join("\n");
3068
- }
3069
3059
  async function validateFiles(root, records, reasons) {
3070
- const contents = [];
3071
3060
  const resolvedRoot = await realpath2(root);
3072
3061
  for (const record of records) {
3073
3062
  const absolute = path18.resolve(root, record.path);
@@ -3077,15 +3066,11 @@ async function validateFiles(root, records, reasons) {
3077
3066
  reasons.push(`readiness file escapes project root: ${record.path}`);
3078
3067
  continue;
3079
3068
  }
3080
- const content = await readFile4(resolved);
3081
- const digest = createHash2("sha256").update(content).digest("hex");
3082
- if (digest !== record.sha256) reasons.push(`readiness file changed: ${record.path}`);
3083
- contents.push(content.toString("utf8"));
3069
+ await readFile4(resolved);
3084
3070
  } catch {
3085
3071
  reasons.push(`readiness file is missing or unreadable: ${record.path}`);
3086
3072
  }
3087
3073
  }
3088
- return contents;
3089
3074
  }
3090
3075
  async function inspectTddReadiness(root) {
3091
3076
  const manifestPath2 = readinessPath(root);
@@ -3096,7 +3081,14 @@ async function inspectTddReadiness(root) {
3096
3081
  try {
3097
3082
  manifest = JSON.parse(await readFile4(manifestPath2, "utf8"));
3098
3083
  } catch {
3099
- return { status: "needs_init", reasons: ["TDD readiness receipt is invalid"], manifestPath: manifestPath2 };
3084
+ return { status: "needs_repair", reasons: ["TDD readiness receipt is invalid"], manifestPath: manifestPath2 };
3085
+ }
3086
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
3087
+ return {
3088
+ status: "needs_repair",
3089
+ reasons: ["TDD readiness receipt must be a JSON object"],
3090
+ manifestPath: manifestPath2
3091
+ };
3100
3092
  }
3101
3093
  const reasons = [];
3102
3094
  if (manifest.schema !== TDD_READINESS_SCHEMA) reasons.push("unsupported readiness schema");
@@ -3118,67 +3110,21 @@ async function inspectTddReadiness(root) {
3118
3110
  reasons.push("changed-line coverage gate must use the task baseline and threshold variables");
3119
3111
  }
3120
3112
  const buildFiles = parseFileRecords(manifest.build_files, "build_files", reasons);
3121
- const ciFiles = parseFileRecords(manifest.ci_files, "ci_files", reasons);
3122
3113
  const toolFiles = parseFileRecords(manifest.tool_files, "tool_files", reasons);
3123
3114
  if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(path18.basename(record.path)))) {
3124
3115
  reasons.push("build_files must include a Maven or Gradle Java build file");
3125
3116
  }
3126
- if (!ciFiles.some((record) => GITLAB_CI_ENTRY_FILES.has(record.path.replaceAll("\\", "/")))) {
3127
- reasons.push("ci_files must include the project-root GitLab CI entry file");
3128
- }
3129
3117
  if (!toolFiles.some((record) => record.path.replaceAll("\\", "/") === COVERAGE_TOOL_PATH)) {
3130
3118
  reasons.push(`tool_files must include ${COVERAGE_TOOL_PATH}`);
3131
3119
  }
3132
- const buildContents = await validateFiles(root, buildFiles, reasons);
3133
- const ciContents = await validateFiles(root, ciFiles, reasons);
3120
+ await validateFiles(root, buildFiles, reasons);
3134
3121
  await validateFiles(root, toolFiles, reasons);
3135
- if (!buildContents.some((content) => /jacoco/i.test(content))) {
3136
- reasons.push("build files do not configure JaCoCo");
3137
- }
3138
- const combinedCi = activeCiContent(ciContents);
3139
- for (const marker of [
3140
- "jacoco",
3141
- "artifacts",
3142
- COVERAGE_TOOL_PATH,
3143
- TDD_BASE_VARIABLE,
3144
- TDD_THRESHOLD_VARIABLE
3145
- ]) {
3146
- if (!combinedCi.toLowerCase().includes(marker.toLowerCase())) {
3147
- reasons.push(`CI files do not contain required marker: ${marker}`);
3148
- }
3149
- }
3150
- if (!usesRequiredGateVariables(combinedCi)) {
3151
- reasons.push("CI changed-line gate must use the task baseline and threshold variables");
3152
- }
3153
- if (!/(?:^|\n)\s*stage\s*:\s*['"]?test['"]?\s*(?:#.*)?(?:\n|$)/i.test(combinedCi)) {
3154
- reasons.push("CI files do not declare a TEST-stage job");
3155
- }
3156
3122
  return {
3157
- status: reasons.length === 0 ? "ready" : "needs_init",
3123
+ status: reasons.length === 0 ? "ready" : "needs_repair",
3158
3124
  reasons: [...new Set(reasons)],
3159
3125
  manifestPath: manifestPath2
3160
3126
  };
3161
3127
  }
3162
- async function disableUnreadySessionTddOverrides(root) {
3163
- if ((await inspectTddReadiness(root)).status === "ready") return 0;
3164
- const sessionsDir = path18.join(root, EASY_CODING_DIR, SESSIONS_DIR);
3165
- if (!await pathExists(sessionsDir)) return 0;
3166
- let updated = 0;
3167
- for (const entry of await readdir5(sessionsDir, { withFileTypes: true })) {
3168
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3169
- const filePath = path18.join(sessionsDir, entry.name);
3170
- try {
3171
- const session = JSON.parse(await readFile4(filePath, "utf8"));
3172
- if (session.tdd_enabled !== true) continue;
3173
- session.tdd_enabled = false;
3174
- await writeTextFile(filePath, `${JSON.stringify(session, null, 2)}
3175
- `);
3176
- updated += 1;
3177
- } catch {
3178
- }
3179
- }
3180
- return updated;
3181
- }
3182
3128
 
3183
3129
  // src/commands/config.ts
3184
3130
  async function config() {
@@ -3281,7 +3227,7 @@ async function config() {
3281
3227
  const readiness = await inspectTddReadiness(process.cwd());
3282
3228
  if (readiness.status !== "ready") {
3283
3229
  cancel4(
3284
- `TDD was not enabled. Run ec-tdd-init first: ${readiness.reasons.join("; ")}. No project modes were changed.`
3230
+ `TDD was not enabled. ${readiness.status === "needs_init" ? "Run ec-tdd-init first" : "Repair TDD readiness"}: ${readiness.reasons.join("; ")}. No project modes were changed.`
3285
3231
  );
3286
3232
  return;
3287
3233
  }
@@ -3333,7 +3279,7 @@ import { note, outro as outro4 } from "@clack/prompts";
3333
3279
  import chalk5 from "chalk";
3334
3280
 
3335
3281
  // src/utils/install-state.ts
3336
- import { readdir as readdir6 } from "fs/promises";
3282
+ import { readdir as readdir5 } from "fs/promises";
3337
3283
  import path20 from "path";
3338
3284
  var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
3339
3285
  async function detectEasyCodingInstallState(cwd) {
@@ -3379,14 +3325,14 @@ async function listMarkdownFiles(dir) {
3379
3325
  if (!await isDirectory(dir)) {
3380
3326
  return [];
3381
3327
  }
3382
- const entries = await readdir6(dir, { withFileTypes: true });
3328
+ const entries = await readdir5(dir, { withFileTypes: true });
3383
3329
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
3384
3330
  }
3385
3331
  async function hasAnyDirectoryEntry(dir) {
3386
3332
  if (!await isDirectory(dir)) {
3387
3333
  return false;
3388
3334
  }
3389
- return (await readdir6(dir)).length > 0;
3335
+ return (await readdir5(dir)).length > 0;
3390
3336
  }
3391
3337
  function relativeEasyCodingPath(...segments) {
3392
3338
  return path20.posix.join(EASY_CODING_DIR, ...segments);
@@ -3562,7 +3508,7 @@ import path23 from "path";
3562
3508
  import chalk6 from "chalk";
3563
3509
 
3564
3510
  // src/utils/session.ts
3565
- import { readdir as readdir7, stat as stat3, unlink as unlink2 } from "fs/promises";
3511
+ import { readdir as readdir6, stat as stat3, unlink as unlink2 } from "fs/promises";
3566
3512
  import path22 from "path";
3567
3513
  var DAY_MS = 24 * 60 * 60 * 1e3;
3568
3514
  var IDLE_SESSION_RETENTION_MS = 7 * DAY_MS;
@@ -3585,7 +3531,7 @@ async function listSessionFiles(cwd) {
3585
3531
  return [];
3586
3532
  }
3587
3533
  const entries = [];
3588
- for (const name of await readdir7(dir)) {
3534
+ for (const name of await readdir6(dir)) {
3589
3535
  if (!name.endsWith(".json")) {
3590
3536
  continue;
3591
3537
  }
@@ -3612,7 +3558,11 @@ async function cleanSessionRuntime(cwd, options = {}) {
3612
3558
  const attachedRetentionMs = options.attachedRetentionMs ?? ATTACHED_SESSION_RETENTION_MS;
3613
3559
  const maxSessions = options.maxSessions ?? MAX_SESSION_FILES;
3614
3560
  const reserveSlots = options.reserveSlots ?? 0;
3615
- const candidates = await listSessionCleanupCandidates(cwd);
3561
+ const candidates = (await listSessionCleanupCandidates(cwd)).filter((candidate) => {
3562
+ if (!options.preserveTddSettings) return true;
3563
+ const session = parseSessionFile(candidate.content);
3564
+ return !session || !("tdd_enabled" in session || "tdd_coverage_threshold" in session);
3565
+ });
3616
3566
  const removed = /* @__PURE__ */ new Set();
3617
3567
  for (const candidate of candidates) {
3618
3568
  const retentionMs = candidate.attached ? attachedRetentionMs : idleRetentionMs;
@@ -3644,7 +3594,7 @@ async function listSessionCleanupCandidates(cwd) {
3644
3594
  return [];
3645
3595
  }
3646
3596
  const candidates = [];
3647
- for (const entry of await readdir7(dir, { withFileTypes: true })) {
3597
+ for (const entry of await readdir6(dir, { withFileTypes: true })) {
3648
3598
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
3649
3599
  continue;
3650
3600
  }
@@ -3688,7 +3638,7 @@ async function cleanOrphanAcceptanceSnapshots(cwd) {
3688
3638
  return 0;
3689
3639
  }
3690
3640
  let removed = 0;
3691
- for (const entry of await readdir7(acceptanceDir, { withFileTypes: true })) {
3641
+ for (const entry of await readdir6(acceptanceDir, { withFileTypes: true })) {
3692
3642
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
3693
3643
  continue;
3694
3644
  }
@@ -3953,8 +3903,8 @@ async function upgrade(opts) {
3953
3903
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
3954
3904
  "Will remove retired files that still match the previous install manifest and preserve locally modified copies.",
3955
3905
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
3956
- "Will migrate behavior config to schema 5 and disable unready project/session TDD settings.",
3957
- "Will prune expired session bindings and orphan acceptance snapshots in each upgraded target while preserving tasks, memory, spec, and project knowledge.",
3906
+ "Will migrate behavior config to schema 5 while preserving project/session TDD settings and frozen task baselines.",
3907
+ "Will prune expired session bindings without explicit TDD settings and orphan acceptance snapshots in each upgraded target while preserving tasks, memory, spec, and project knowledge.",
3958
3908
  "Will migrate active REVIEW/VERIFICATION tasks to QUALITY, retire active read-only task types as CLOSED, and preserve their artifacts and history.",
3959
3909
  "Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
3960
3910
  ].join("\n");
@@ -3974,7 +3924,7 @@ async function upgrade(opts) {
3974
3924
  }
3975
3925
  for (const { target, config: config2 } of pending) {
3976
3926
  const previousManifest = await readInstallManifest(target.dir);
3977
- const sessionCleanup = await cleanSessionRuntime(target.dir);
3927
+ const sessionCleanup = await cleanSessionRuntime(target.dir, { preserveTddSettings: true });
3978
3928
  if (sessionCleanup.sessionsRemoved > 0 || sessionCleanup.acceptanceSnapshotsRemoved > 0) {
3979
3929
  console.log(
3980
3930
  chalk8.yellow(
@@ -3982,7 +3932,6 @@ async function upgrade(opts) {
3982
3932
  )
3983
3933
  );
3984
3934
  }
3985
- const beta1ProjectTddRequested = Number(config2.version) === 4 && config2.behavior?.tdd_enabled === true;
3986
3935
  const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
3987
3936
  supermodule: target.supermodule
3988
3937
  });
@@ -4007,14 +3956,6 @@ async function upgrade(opts) {
4007
3956
  await ensureHookBytecodeIgnored(target.dir);
4008
3957
  await migrateLegacyWorkflowState(target.dir);
4009
3958
  await migrateBehaviorConfig(target.configPath);
4010
- const disabledSessionTddOverrides = await disableUnreadySessionTddOverrides(target.dir);
4011
- if (beta1ProjectTddRequested || disabledSessionTddOverrides > 0) {
4012
- console.log(
4013
- chalk8.yellow(
4014
- `${target.label}: TDD remains off until ec-tdd-init succeeds (project=${beta1ProjectTddRequested ? "disabled" : "unchanged"}, sessions_disabled=${disabledSessionTddOverrides}).`
4015
- )
4016
- );
4017
- }
4018
3959
  await updateHarnessVersion(target.configPath, VERSION);
4019
3960
  await updateSupermoduleConfig(target.configPath, target.supermodule);
4020
3961
  await setPendingInitSince(target.dir, VERSION);