easy-coding-harness 0.10.0-beta.1 → 0.10.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +8 -4
- package/dist/cli.js +236 -40
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +7 -4
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +3 -2
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +21 -9
- package/templates/common/skills/ec-config/SKILL.md +18 -2
- package/templates/common/skills/ec-memory/SKILL.md +4 -3
- package/templates/common/skills/ec-reviewing/SKILL.md +4 -2
- package/templates/common/skills/ec-tdd-init/SKILL.md +101 -0
- package/templates/common/skills/ec-verification/SKILL.md +19 -23
- package/templates/common/skills/ec-workflow/SKILL.md +11 -5
- package/templates/main-constraint/AGENTS.md.tpl +8 -4
- package/templates/main-constraint/CLAUDE.md.tpl +8 -4
- package/templates/runtime/tools/easy_coding_tdd_readiness.py +306 -0
- package/templates/shared-hooks/easy_coding_state.py +285 -38
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.10.0-beta.3
|
|
10
|
+
|
|
11
|
+
- TDD 业务任务的 `VERIFICATION` 门禁收口为本地证据:每个仓库(Canonical 场景下每个
|
|
12
|
+
source task)必须有通过的本地单测,以及达到冻结阈值的本地 JaCoCo changed-line
|
|
13
|
+
coverage;不再要求远程 GitLab pipeline URL、job identity 或成功状态。
|
|
14
|
+
- `ec-tdd-init` 继续在 TDD 关闭态生成并校验参数化 GitLab TEST-stage job、构建配置、
|
|
15
|
+
JaCoCo XML 与 readiness receipt,但远程 CI 只作为项目自动化能力,不再触发 Harness
|
|
16
|
+
中间提交、推送或等待远程结果。
|
|
17
|
+
- beta.1/beta.2 任务中已有的 `coverage_scope=gitlab` 记录保持原样并从新验收集合中忽略,
|
|
18
|
+
因此 pending/failed 远程记录不会阻塞升级后的本地门禁;本地 baseline、阈值、报告
|
|
19
|
+
指纹及 TDD 生命周期/review 约束保持不变。
|
|
20
|
+
|
|
21
|
+
## 0.10.0-beta.2
|
|
22
|
+
|
|
23
|
+
- 新增 `ec-tdd-init`,在 TDD 关闭态初始化或刷新 Java 单测执行、JaCoCo XML、GitLab
|
|
24
|
+
TEST job、报告 artifact 与 changed-line coverage gate;初始化只建设基础设施,不批量补
|
|
25
|
+
存量业务单测,也不要求仓库全量覆盖率。
|
|
26
|
+
- 新增 `.easy-coding/tools/easy_coding_tdd_readiness.py` 与覆盖构建、GitLab CI、coverage
|
|
27
|
+
工具的文件指纹 receipt;项目级 `easy-coding config`、session `set-tdd` 和 ANALYSIS →
|
|
28
|
+
IMPLEMENT 均在开启/冻结 TDD 前机械校验 readiness,缺失或漂移时只允许先初始化或
|
|
29
|
+
保持关闭。TDD 关闭的普通 hook 不扫描这些文件。
|
|
30
|
+
- `tdd-init` 作为专用代码任务始终冻结 `tdd_enabled=false`,即使遗留项目/session 或暂停
|
|
31
|
+
任务请求开启 TDD,也可正常创建和修改 CI,消除“开启 TDD 后又依赖尚未创建 CI”的
|
|
32
|
+
循环阻塞;初始化完成后仍需用户显式开启 TDD。
|
|
33
|
+
- 配置 schema 升至 5;升级时没有 readiness 的 beta.1 项目与 session TDD 请求迁移为
|
|
34
|
+
关闭并保留阈值,已冻结活动任务合同不被静默改写。后续业务任务仍只验收相对冻结
|
|
35
|
+
baseline 的新增/修改生产代码行,默认门槛 90%、测试设计目标接近 100%。
|
|
36
|
+
|
|
9
37
|
## 0.10.0-beta.1
|
|
10
38
|
|
|
11
39
|
- 新增默认关闭的 Java TDD 模式,支持项目级 CLI 配置与 session 覆盖;覆盖率阈值默认
|
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ any stage --[user abort via ec-task-close]--> CLOSED
|
|
|
86
86
|
ANALYSIS → IMPLEMENT 确认一次,随后各阶段在质量门禁通过后自动推进,`auto` 从开始即
|
|
87
87
|
自动推进。
|
|
88
88
|
- 工作流模式优先级为 session 覆盖 > 项目 `behavior.workflow_mode` > `adaptive`。Adaptive 在 ANALYSIS 结束时根据风险解析、展示并冻结为 `fast`、`standard` 或 `strict`,用户可在风险下限之上调整。
|
|
89
|
-
- Java TDD 默认关闭;优先级为 session 覆盖 > 项目配置 > `false/90
|
|
89
|
+
- Java TDD 默认关闭;优先级为 session 覆盖 > 项目配置 > `false/90%`。首次开启前必须运行 `ec-tdd-init`,只建设 JUnit/JaCoCo/GitLab 增量覆盖率基础设施,不补存量业务单测;readiness 通过后才允许显式开启。开启后在 ANALYSIS → IMPLEMENT 冻结开关、baseline 与阈值,只验收本任务新增/修改生产代码行,执行 RED/GREEN/REFACTOR(纯重构使用 characterization GREEN → GREEN),并要求本地单测通过、本地差异覆盖率达到冻结阈值。GitLab TEST-stage job 仍会生成,但远程 pipeline 结果不属于 Harness 验收证据,也不会触发中间提交推送。关闭时普通任务不扫描 CI/JaCoCo、不增加命令或提高原工作流验收深度。
|
|
90
90
|
- 所有新代码任务都完整进入 REVIEW;不同工作流模式只调整各状态内部的上下文加载、执行主体、审查独立性、验证范围和记忆深度,不绕过状态或证据门禁。
|
|
91
91
|
- 显式 `doc` / `analysis` / `report` 只读任务不生成 `test-strategy.md`;展示完整报告后按生效模式进入 COMPLETE,不执行 REVIEW、VERIFICATION 或 MEMORY,也不写任务记忆。
|
|
92
92
|
- `VERIFICATION` 是验证硬门控:Fast 运行最小充分检查,Standard 运行受影响范围检查,
|
|
@@ -132,7 +132,7 @@ Canonical Spec 是只读设计源;`.easy-coding/tasks/<task-id>/` 下的 `dev-
|
|
|
132
132
|
| `easy-coding add-agent` | 给已接入项目追加 Claude Code、Codex 或 Qoder 支持;supermodule 父仓可按已初始化子仓分层追加 |
|
|
133
133
|
| `easy-coding upgrade` | CLI 升级后同步项目内生成文件,生成区覆盖,用户资产保留;supermodule 父仓会同步升级已初始化子仓 |
|
|
134
134
|
| `easy-coding update` | 更新全局 CLI 到最新发布版 |
|
|
135
|
-
| `easy-coding config` | 交互修改当前项目的 Approval、Workflow、Java TDD
|
|
135
|
+
| `easy-coding config` | 交互修改当前项目的 Approval、Workflow、Java TDD 与覆盖率阈值;开启 TDD 前要求 readiness |
|
|
136
136
|
| `easy-coding status` | 查看已安装平台、harness 版本、当前任务状态 |
|
|
137
137
|
| `easy-coding clear` | 移除 harness 安装物,保留 tasks、spec、memory、project.yaml 等用户资产;supermodule 父仓支持交互选择、`--submodules` 和 `--no-submodules` |
|
|
138
138
|
|
|
@@ -151,6 +151,7 @@ Canonical Spec 是只读设计源;`.easy-coding/tasks/<task-id>/` 下的 `dev-
|
|
|
151
151
|
| `ec-memory` | 写短期记忆,并在超过阈值时沉淀长期记忆 |
|
|
152
152
|
| `ec-task-management` | 任务面板:查看、创建、选择、恢复、交接任务 |
|
|
153
153
|
| `ec-config` | 只读查看或显式修改项目/session 的 Approval、Workflow、TDD 与阈值 |
|
|
154
|
+
| `ec-tdd-init` | 在 TDD 关闭态初始化/刷新 Java changed-line coverage 基础设施,不补存量单测 |
|
|
154
155
|
| `ec-task-close` | 用户主动中断任务并关闭 |
|
|
155
156
|
| `ec-no-harness` | 当前会话仅旁路 Easy Coding Harness,使用原生 Agent 能力 |
|
|
156
157
|
| `ec-git` | 约束 git diff、commit、push、跨仓库提交等交付动作 |
|
|
@@ -187,8 +188,11 @@ easy-coding upgrade
|
|
|
187
188
|
`behavior.approval_mode` 与 `behavior.workflow_mode`;旧 `lite` 映射为 `guard + fast`。
|
|
188
189
|
项目级模式用 `easy-coding config` 修改(要求
|
|
189
190
|
项目 Harness 与 CLI 版本完全一致,否则先执行 `easy-coding upgrade` 或更新 CLI);当前
|
|
190
|
-
session 临时覆盖统一通过 `ec-config` 对话修改。升级到 0.10.0-beta.
|
|
191
|
-
|
|
191
|
+
session 临时覆盖统一通过 `ec-config` 对话修改。升级到 0.10.0-beta.2 时配置 schema 升至
|
|
192
|
+
5;未完成 `ec-tdd-init` readiness 的项目/session TDD 请求迁移为关闭并保留阈值,同时
|
|
193
|
+
部署共享 Java 差异覆盖率与 readiness 工具。0.10.0-beta.3 起,TDD 业务任务只依赖本地
|
|
194
|
+
单测与本地差异覆盖率,历史远程 CI 证据保留但不再参与验收。已经冻结的活动任务合同
|
|
195
|
+
不会被静默改写。
|
|
192
196
|
|
|
193
197
|
若当前会话不希望 Harness 接管,显式调用 `/ec-no-harness`(Codex 使用
|
|
194
198
|
`$ec-no-harness`)。它只旁路 Easy Coding,不关闭其他 hooks,也不忽略其他 skills;
|
package/dist/cli.js
CHANGED
|
@@ -103,7 +103,7 @@ async function isDirectory(filePath) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// src/utils/config-yaml.ts
|
|
106
|
-
var CONFIG_SCHEMA_VERSION =
|
|
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
|
|
194
|
-
const
|
|
195
|
-
const
|
|
193
|
+
const supportsTddThreshold = Number(config2.version) >= 4;
|
|
194
|
+
const supportsReadyTdd = Number(config2.version) >= CONFIG_SCHEMA_VERSION;
|
|
195
|
+
const tddEnabled = supportsReadyTdd && behavior.tdd_enabled === true;
|
|
196
|
+
const tddCoverageThreshold = supportsTddThreshold && isTddCoverageThreshold(behavior.tdd_coverage_threshold) ? behavior.tdd_coverage_threshold : DEFAULT_TDD_COVERAGE_THRESHOLD;
|
|
196
197
|
return { approvalMode, workflowMode, tddEnabled, tddCoverageThreshold };
|
|
197
198
|
}
|
|
198
199
|
async function setBehaviorModes(filePath, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold) {
|
|
@@ -258,6 +259,8 @@ var MAIN_SPEC_DIR = "main";
|
|
|
258
259
|
var DEV_SPEC_DIR = "dev";
|
|
259
260
|
var TEMPLATES_DIR = "templates";
|
|
260
261
|
var TOOLS_DIR = "tools";
|
|
262
|
+
var TDD_DIR = "tdd";
|
|
263
|
+
var TDD_READINESS_FILE = "readiness.json";
|
|
261
264
|
var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
|
|
262
265
|
var HOOK_BYTECODE_GITIGNORE_ENTRY = "__pycache__/";
|
|
263
266
|
var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
|
|
@@ -681,8 +684,10 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
|
681
684
|
async function writeToolsScaffold(easyCodingDir) {
|
|
682
685
|
const toolsDir = path6.join(easyCodingDir, TOOLS_DIR);
|
|
683
686
|
await ensureDir(toolsDir);
|
|
684
|
-
const
|
|
685
|
-
|
|
687
|
+
for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
|
|
688
|
+
const src = getTemplatePath("runtime", "tools", file);
|
|
689
|
+
await writeTextFile(path6.join(toolsDir, file), await readTextFile(src));
|
|
690
|
+
}
|
|
686
691
|
}
|
|
687
692
|
async function writeTemplatesScaffold(easyCodingDir) {
|
|
688
693
|
const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
|
|
@@ -2772,7 +2777,7 @@ function renderTargetPlans(targetPlans) {
|
|
|
2772
2777
|
}
|
|
2773
2778
|
|
|
2774
2779
|
// src/commands/config.ts
|
|
2775
|
-
import
|
|
2780
|
+
import path19 from "path";
|
|
2776
2781
|
import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
|
|
2777
2782
|
import chalk4 from "chalk";
|
|
2778
2783
|
|
|
@@ -2851,10 +2856,169 @@ async function checkForUpgrade(cwd) {
|
|
|
2851
2856
|
}
|
|
2852
2857
|
}
|
|
2853
2858
|
|
|
2859
|
+
// src/utils/tdd-readiness.ts
|
|
2860
|
+
import { createHash as createHash2 } from "crypto";
|
|
2861
|
+
import { readFile as readFile4, readdir as readdir5, realpath as realpath2 } from "fs/promises";
|
|
2862
|
+
import path18 from "path";
|
|
2863
|
+
var TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1";
|
|
2864
|
+
var TDD_READINESS_SCOPE = "changed-production-lines";
|
|
2865
|
+
var TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA";
|
|
2866
|
+
var TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD";
|
|
2867
|
+
var COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py";
|
|
2868
|
+
var JAVA_BUILD_FILE_NAMES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
|
|
2869
|
+
var GITLAB_CI_ENTRY_FILES = /* @__PURE__ */ new Set([".gitlab-ci.yml", ".gitlab-ci.yaml"]);
|
|
2870
|
+
function readinessPath(root) {
|
|
2871
|
+
return path18.join(root, EASY_CODING_DIR, TDD_DIR, TDD_READINESS_FILE);
|
|
2872
|
+
}
|
|
2873
|
+
function parseFileRecords(value, field, reasons) {
|
|
2874
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2875
|
+
reasons.push(`${field} must contain at least one file`);
|
|
2876
|
+
return [];
|
|
2877
|
+
}
|
|
2878
|
+
const records = [];
|
|
2879
|
+
for (const item of value) {
|
|
2880
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
2881
|
+
reasons.push(`${field} contains an invalid record`);
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const record = item;
|
|
2885
|
+
if (typeof record.path !== "string" || !record.path.trim() || path18.isAbsolute(record.path) || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
|
|
2886
|
+
reasons.push(`${field} contains an invalid path or SHA-256`);
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
records.push({ path: record.path, sha256: record.sha256 });
|
|
2890
|
+
}
|
|
2891
|
+
return records;
|
|
2892
|
+
}
|
|
2893
|
+
function usesRequiredGateVariables(command) {
|
|
2894
|
+
const normalized = command.replaceAll(`\${${TDD_BASE_VARIABLE}}`, `$${TDD_BASE_VARIABLE}`).replaceAll(`\${${TDD_THRESHOLD_VARIABLE}}`, `$${TDD_THRESHOLD_VARIABLE}`);
|
|
2895
|
+
return new RegExp(`--base\\s+['"]?\\$${TDD_BASE_VARIABLE}(?:['"]|\\s|$)`).test(normalized) && new RegExp(`--threshold\\s+['"]?\\$${TDD_THRESHOLD_VARIABLE}(?:['"]|\\s|$)`).test(normalized);
|
|
2896
|
+
}
|
|
2897
|
+
function isSafeReportPattern(value) {
|
|
2898
|
+
const normalized = value.replaceAll("\\", "/");
|
|
2899
|
+
return !path18.isAbsolute(value) && !normalized.split("/").includes("..");
|
|
2900
|
+
}
|
|
2901
|
+
function activeCiContent(contents) {
|
|
2902
|
+
return contents.join("\n").split("\n").map((line) => line.replace(/^\s*#.*$/, "").replace(/\s+#.*$/, "")).join("\n");
|
|
2903
|
+
}
|
|
2904
|
+
async function validateFiles(root, records, reasons) {
|
|
2905
|
+
const contents = [];
|
|
2906
|
+
const resolvedRoot = await realpath2(root);
|
|
2907
|
+
for (const record of records) {
|
|
2908
|
+
const absolute = path18.resolve(root, record.path);
|
|
2909
|
+
try {
|
|
2910
|
+
const resolved = await realpath2(absolute);
|
|
2911
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path18.sep}`)) {
|
|
2912
|
+
reasons.push(`readiness file escapes project root: ${record.path}`);
|
|
2913
|
+
continue;
|
|
2914
|
+
}
|
|
2915
|
+
const content = await readFile4(resolved);
|
|
2916
|
+
const digest = createHash2("sha256").update(content).digest("hex");
|
|
2917
|
+
if (digest !== record.sha256) reasons.push(`readiness file changed: ${record.path}`);
|
|
2918
|
+
contents.push(content.toString("utf8"));
|
|
2919
|
+
} catch {
|
|
2920
|
+
reasons.push(`readiness file is missing or unreadable: ${record.path}`);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return contents;
|
|
2924
|
+
}
|
|
2925
|
+
async function inspectTddReadiness(root) {
|
|
2926
|
+
const manifestPath2 = readinessPath(root);
|
|
2927
|
+
if (!await pathExists(manifestPath2)) {
|
|
2928
|
+
return { status: "needs_init", reasons: ["TDD readiness receipt is missing"], manifestPath: manifestPath2 };
|
|
2929
|
+
}
|
|
2930
|
+
let manifest;
|
|
2931
|
+
try {
|
|
2932
|
+
manifest = JSON.parse(await readFile4(manifestPath2, "utf8"));
|
|
2933
|
+
} catch {
|
|
2934
|
+
return { status: "needs_init", reasons: ["TDD readiness receipt is invalid"], manifestPath: manifestPath2 };
|
|
2935
|
+
}
|
|
2936
|
+
const reasons = [];
|
|
2937
|
+
if (manifest.schema !== TDD_READINESS_SCHEMA) reasons.push("unsupported readiness schema");
|
|
2938
|
+
if (manifest.provider !== "gitlab") reasons.push("readiness provider must be gitlab");
|
|
2939
|
+
if (manifest.coverage_scope !== TDD_READINESS_SCOPE) {
|
|
2940
|
+
reasons.push("coverage scope must be changed-production-lines");
|
|
2941
|
+
}
|
|
2942
|
+
if (manifest.historical_coverage_required !== false) {
|
|
2943
|
+
reasons.push("historical coverage must remain disabled");
|
|
2944
|
+
}
|
|
2945
|
+
if (!Array.isArray(manifest.coverage_report_patterns) || manifest.coverage_report_patterns.length === 0 || manifest.coverage_report_patterns.some(
|
|
2946
|
+
(item) => typeof item !== "string" || !item.trim() || !isSafeReportPattern(item)
|
|
2947
|
+
)) {
|
|
2948
|
+
reasons.push("coverage_report_patterns must contain safe project-relative report patterns");
|
|
2949
|
+
}
|
|
2950
|
+
if (typeof manifest.changed_line_gate_command !== "string" || !manifest.changed_line_gate_command.includes(COVERAGE_TOOL_PATH)) {
|
|
2951
|
+
reasons.push("changed-line coverage gate command is missing");
|
|
2952
|
+
} else if (!usesRequiredGateVariables(manifest.changed_line_gate_command)) {
|
|
2953
|
+
reasons.push("changed-line coverage gate must use the task baseline and threshold variables");
|
|
2954
|
+
}
|
|
2955
|
+
const buildFiles = parseFileRecords(manifest.build_files, "build_files", reasons);
|
|
2956
|
+
const ciFiles = parseFileRecords(manifest.ci_files, "ci_files", reasons);
|
|
2957
|
+
const toolFiles = parseFileRecords(manifest.tool_files, "tool_files", reasons);
|
|
2958
|
+
if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(path18.basename(record.path)))) {
|
|
2959
|
+
reasons.push("build_files must include a Maven or Gradle Java build file");
|
|
2960
|
+
}
|
|
2961
|
+
if (!ciFiles.some((record) => GITLAB_CI_ENTRY_FILES.has(record.path.replaceAll("\\", "/")))) {
|
|
2962
|
+
reasons.push("ci_files must include the project-root GitLab CI entry file");
|
|
2963
|
+
}
|
|
2964
|
+
if (!toolFiles.some((record) => record.path.replaceAll("\\", "/") === COVERAGE_TOOL_PATH)) {
|
|
2965
|
+
reasons.push(`tool_files must include ${COVERAGE_TOOL_PATH}`);
|
|
2966
|
+
}
|
|
2967
|
+
const buildContents = await validateFiles(root, buildFiles, reasons);
|
|
2968
|
+
const ciContents = await validateFiles(root, ciFiles, reasons);
|
|
2969
|
+
await validateFiles(root, toolFiles, reasons);
|
|
2970
|
+
if (!buildContents.some((content) => /jacoco/i.test(content))) {
|
|
2971
|
+
reasons.push("build files do not configure JaCoCo");
|
|
2972
|
+
}
|
|
2973
|
+
const combinedCi = activeCiContent(ciContents);
|
|
2974
|
+
for (const marker of [
|
|
2975
|
+
"jacoco",
|
|
2976
|
+
"artifacts",
|
|
2977
|
+
COVERAGE_TOOL_PATH,
|
|
2978
|
+
TDD_BASE_VARIABLE,
|
|
2979
|
+
TDD_THRESHOLD_VARIABLE
|
|
2980
|
+
]) {
|
|
2981
|
+
if (!combinedCi.toLowerCase().includes(marker.toLowerCase())) {
|
|
2982
|
+
reasons.push(`CI files do not contain required marker: ${marker}`);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
if (!usesRequiredGateVariables(combinedCi)) {
|
|
2986
|
+
reasons.push("CI changed-line gate must use the task baseline and threshold variables");
|
|
2987
|
+
}
|
|
2988
|
+
if (!/(?:^|\n)\s*stage\s*:\s*['"]?test['"]?\s*(?:#.*)?(?:\n|$)/i.test(combinedCi)) {
|
|
2989
|
+
reasons.push("CI files do not declare a TEST-stage job");
|
|
2990
|
+
}
|
|
2991
|
+
return {
|
|
2992
|
+
status: reasons.length === 0 ? "ready" : "needs_init",
|
|
2993
|
+
reasons: [...new Set(reasons)],
|
|
2994
|
+
manifestPath: manifestPath2
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
async function disableUnreadySessionTddOverrides(root) {
|
|
2998
|
+
if ((await inspectTddReadiness(root)).status === "ready") return 0;
|
|
2999
|
+
const sessionsDir = path18.join(root, EASY_CODING_DIR, SESSIONS_DIR);
|
|
3000
|
+
if (!await pathExists(sessionsDir)) return 0;
|
|
3001
|
+
let updated = 0;
|
|
3002
|
+
for (const entry of await readdir5(sessionsDir, { withFileTypes: true })) {
|
|
3003
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
3004
|
+
const filePath = path18.join(sessionsDir, entry.name);
|
|
3005
|
+
try {
|
|
3006
|
+
const session = JSON.parse(await readFile4(filePath, "utf8"));
|
|
3007
|
+
if (session.tdd_enabled !== true) continue;
|
|
3008
|
+
session.tdd_enabled = false;
|
|
3009
|
+
await writeTextFile(filePath, `${JSON.stringify(session, null, 2)}
|
|
3010
|
+
`);
|
|
3011
|
+
updated += 1;
|
|
3012
|
+
} catch {
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
return updated;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
2854
3018
|
// src/commands/config.ts
|
|
2855
3019
|
async function config() {
|
|
2856
3020
|
renderBanner();
|
|
2857
|
-
const configPath2 =
|
|
3021
|
+
const configPath2 = path19.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
|
|
2858
3022
|
if (!await pathExists(configPath2)) {
|
|
2859
3023
|
throw new Error("No easy-coding harness found in this project.");
|
|
2860
3024
|
}
|
|
@@ -2948,6 +3112,15 @@ async function config() {
|
|
|
2948
3112
|
cancel4("Configuration cancelled.");
|
|
2949
3113
|
return;
|
|
2950
3114
|
}
|
|
3115
|
+
if (tddEnabled) {
|
|
3116
|
+
const readiness = await inspectTddReadiness(process.cwd());
|
|
3117
|
+
if (readiness.status !== "ready") {
|
|
3118
|
+
cancel4(
|
|
3119
|
+
`TDD was not enabled. Run ec-tdd-init first: ${readiness.reasons.join("; ")}. No project modes were changed.`
|
|
3120
|
+
);
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
2951
3124
|
let tddCoverageThreshold = current.tddCoverageThreshold;
|
|
2952
3125
|
if (tddEnabled) {
|
|
2953
3126
|
const thresholdInput = await text({
|
|
@@ -2972,6 +3145,15 @@ async function config() {
|
|
|
2972
3145
|
cancel4("Configuration cancelled.");
|
|
2973
3146
|
return;
|
|
2974
3147
|
}
|
|
3148
|
+
if (tddEnabled) {
|
|
3149
|
+
const readiness = await inspectTddReadiness(process.cwd());
|
|
3150
|
+
if (readiness.status !== "ready") {
|
|
3151
|
+
cancel4(
|
|
3152
|
+
`TDD was not enabled because readiness changed before save: ${readiness.reasons.join("; ")}. No project modes were changed.`
|
|
3153
|
+
);
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
2975
3157
|
await setBehaviorModes(configPath2, approvalMode, workflowMode, tddEnabled, tddCoverageThreshold);
|
|
2976
3158
|
outro3(
|
|
2977
3159
|
chalk4.green(
|
|
@@ -2981,20 +3163,20 @@ async function config() {
|
|
|
2981
3163
|
}
|
|
2982
3164
|
|
|
2983
3165
|
// src/commands/init.ts
|
|
2984
|
-
import
|
|
3166
|
+
import path21 from "path";
|
|
2985
3167
|
import { note, outro as outro4 } from "@clack/prompts";
|
|
2986
3168
|
import chalk5 from "chalk";
|
|
2987
3169
|
|
|
2988
3170
|
// src/utils/install-state.ts
|
|
2989
|
-
import { readdir as
|
|
2990
|
-
import
|
|
3171
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
3172
|
+
import path20 from "path";
|
|
2991
3173
|
var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
|
|
2992
3174
|
async function detectEasyCodingInstallState(cwd) {
|
|
2993
|
-
const easyCodingDir =
|
|
3175
|
+
const easyCodingDir = path20.join(cwd, EASY_CODING_DIR);
|
|
2994
3176
|
if (!await pathExists(easyCodingDir)) {
|
|
2995
3177
|
return { kind: "fresh", easyCodingDir };
|
|
2996
3178
|
}
|
|
2997
|
-
const configPath2 =
|
|
3179
|
+
const configPath2 = path20.join(easyCodingDir, CONFIG_FILE);
|
|
2998
3180
|
if (await pathExists(configPath2)) {
|
|
2999
3181
|
return { kind: "installed", easyCodingDir, configPath: configPath2 };
|
|
3000
3182
|
}
|
|
@@ -3012,17 +3194,17 @@ async function detectEasyCodingInstallState(cwd) {
|
|
|
3012
3194
|
async function detectLegacyAssets(easyCodingDir) {
|
|
3013
3195
|
const assets = [];
|
|
3014
3196
|
for (const file of LEGACY_ROOT_FILES) {
|
|
3015
|
-
if (await pathExists(
|
|
3197
|
+
if (await pathExists(path20.join(easyCodingDir, file))) {
|
|
3016
3198
|
assets.push(relativeEasyCodingPath(file));
|
|
3017
3199
|
}
|
|
3018
3200
|
}
|
|
3019
|
-
if (await pathExists(
|
|
3201
|
+
if (await pathExists(path20.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
3020
3202
|
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
3021
3203
|
}
|
|
3022
|
-
const shortMemoryFiles = await listMarkdownFiles(
|
|
3204
|
+
const shortMemoryFiles = await listMarkdownFiles(path20.join(easyCodingDir, "memory", "short"));
|
|
3023
3205
|
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
3024
3206
|
for (const dir of ["spec", "prototype"]) {
|
|
3025
|
-
if (await hasAnyDirectoryEntry(
|
|
3207
|
+
if (await hasAnyDirectoryEntry(path20.join(easyCodingDir, dir))) {
|
|
3026
3208
|
assets.push(relativeEasyCodingPath(dir));
|
|
3027
3209
|
}
|
|
3028
3210
|
}
|
|
@@ -3032,23 +3214,23 @@ async function listMarkdownFiles(dir) {
|
|
|
3032
3214
|
if (!await isDirectory(dir)) {
|
|
3033
3215
|
return [];
|
|
3034
3216
|
}
|
|
3035
|
-
const entries = await
|
|
3217
|
+
const entries = await readdir6(dir, { withFileTypes: true });
|
|
3036
3218
|
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
3037
3219
|
}
|
|
3038
3220
|
async function hasAnyDirectoryEntry(dir) {
|
|
3039
3221
|
if (!await isDirectory(dir)) {
|
|
3040
3222
|
return false;
|
|
3041
3223
|
}
|
|
3042
|
-
return (await
|
|
3224
|
+
return (await readdir6(dir)).length > 0;
|
|
3043
3225
|
}
|
|
3044
3226
|
function relativeEasyCodingPath(...segments) {
|
|
3045
|
-
return
|
|
3227
|
+
return path20.posix.join(EASY_CODING_DIR, ...segments);
|
|
3046
3228
|
}
|
|
3047
3229
|
function relativeConfigPath() {
|
|
3048
|
-
return
|
|
3230
|
+
return path20.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
3049
3231
|
}
|
|
3050
3232
|
function relativeProjectInitTaskPath() {
|
|
3051
|
-
return
|
|
3233
|
+
return path20.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
3052
3234
|
}
|
|
3053
3235
|
|
|
3054
3236
|
// src/commands/init.ts
|
|
@@ -3129,7 +3311,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
|
|
|
3129
3311
|
if (!targetSubmodulePaths.has(entry.path)) {
|
|
3130
3312
|
continue;
|
|
3131
3313
|
}
|
|
3132
|
-
const dir =
|
|
3314
|
+
const dir = path21.join(cwd, entry.path);
|
|
3133
3315
|
targets.push(
|
|
3134
3316
|
await targetFromState(dir, entry.path, "submodule-child", {
|
|
3135
3317
|
parent: toPosixRelative2(dir, cwd)
|
|
@@ -3176,24 +3358,24 @@ function contextFromState(role, installState) {
|
|
|
3176
3358
|
};
|
|
3177
3359
|
}
|
|
3178
3360
|
function toPosixRelative2(from, to) {
|
|
3179
|
-
const relative =
|
|
3180
|
-
return relative ? relative.split(
|
|
3361
|
+
const relative = path21.relative(from, to);
|
|
3362
|
+
return relative ? relative.split(path21.sep).join("/") : ".";
|
|
3181
3363
|
}
|
|
3182
3364
|
async function resolveInitPlatforms(cwd, opts, parentInstalled) {
|
|
3183
3365
|
if (opts.agent || !parentInstalled) {
|
|
3184
3366
|
return resolvePlatforms(opts, ["claude-code"]);
|
|
3185
3367
|
}
|
|
3186
|
-
const config2 = await readConfigYaml(
|
|
3368
|
+
const config2 = await readConfigYaml(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
3187
3369
|
if (Array.isArray(config2.agents) && config2.agents.length > 0) {
|
|
3188
3370
|
return config2.agents;
|
|
3189
3371
|
}
|
|
3190
3372
|
return resolvePlatforms(opts, ["claude-code"]);
|
|
3191
3373
|
}
|
|
3192
3374
|
async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
|
|
3193
|
-
if (!await pathExists(
|
|
3375
|
+
if (!await pathExists(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
3194
3376
|
return;
|
|
3195
3377
|
}
|
|
3196
|
-
const config2 = parentTarget2.installed ? await readConfigYaml(
|
|
3378
|
+
const config2 = parentTarget2.installed ? await readConfigYaml(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
|
|
3197
3379
|
const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
|
|
3198
3380
|
await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
|
|
3199
3381
|
}
|
|
@@ -3202,7 +3384,7 @@ async function refreshInstalledChildTopologies(targets) {
|
|
|
3202
3384
|
if (!target.installed || target.context.role !== "submodule-child") {
|
|
3203
3385
|
continue;
|
|
3204
3386
|
}
|
|
3205
|
-
const configPath2 =
|
|
3387
|
+
const configPath2 = path21.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
|
|
3206
3388
|
if (!await pathExists(configPath2)) {
|
|
3207
3389
|
continue;
|
|
3208
3390
|
}
|
|
@@ -3211,12 +3393,12 @@ async function refreshInstalledChildTopologies(targets) {
|
|
|
3211
3393
|
}
|
|
3212
3394
|
|
|
3213
3395
|
// src/commands/status.ts
|
|
3214
|
-
import
|
|
3396
|
+
import path23 from "path";
|
|
3215
3397
|
import chalk6 from "chalk";
|
|
3216
3398
|
|
|
3217
3399
|
// src/utils/session.ts
|
|
3218
|
-
import { readdir as
|
|
3219
|
-
import
|
|
3400
|
+
import { readdir as readdir7, unlink } from "fs/promises";
|
|
3401
|
+
import path22 from "path";
|
|
3220
3402
|
var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3221
3403
|
function parseSessionFile(content) {
|
|
3222
3404
|
try {
|
|
@@ -3226,7 +3408,7 @@ function parseSessionFile(content) {
|
|
|
3226
3408
|
}
|
|
3227
3409
|
}
|
|
3228
3410
|
function getSessionDir(cwd) {
|
|
3229
|
-
return
|
|
3411
|
+
return path22.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
3230
3412
|
}
|
|
3231
3413
|
async function listSessionFiles(cwd) {
|
|
3232
3414
|
const dir = getSessionDir(cwd);
|
|
@@ -3234,11 +3416,11 @@ async function listSessionFiles(cwd) {
|
|
|
3234
3416
|
return [];
|
|
3235
3417
|
}
|
|
3236
3418
|
const entries = [];
|
|
3237
|
-
for (const name of await
|
|
3419
|
+
for (const name of await readdir7(dir)) {
|
|
3238
3420
|
if (!name.endsWith(".json")) {
|
|
3239
3421
|
continue;
|
|
3240
3422
|
}
|
|
3241
|
-
const filePath =
|
|
3423
|
+
const filePath = path22.join(dir, name);
|
|
3242
3424
|
const content = await readTextIfExists(filePath);
|
|
3243
3425
|
if (!content) {
|
|
3244
3426
|
continue;
|
|
@@ -3260,7 +3442,7 @@ async function listSessionFiles(cwd) {
|
|
|
3260
3442
|
async function status() {
|
|
3261
3443
|
renderBanner();
|
|
3262
3444
|
const cwd = process.cwd();
|
|
3263
|
-
const configPath2 =
|
|
3445
|
+
const configPath2 = path23.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
3264
3446
|
if (!await pathExists(configPath2)) {
|
|
3265
3447
|
throw new Error("No easy-coding harness found in this project.");
|
|
3266
3448
|
}
|
|
@@ -3270,6 +3452,7 @@ async function status() {
|
|
|
3270
3452
|
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
3271
3453
|
const sessions = await listSessionFiles(cwd);
|
|
3272
3454
|
const versionRelation = compareVersions(config2.harness_version, VERSION);
|
|
3455
|
+
const tddReadiness = await inspectTddReadiness(cwd);
|
|
3273
3456
|
console.log(chalk6.bold("Harness"));
|
|
3274
3457
|
console.log(` version: ${config2.harness_version}`);
|
|
3275
3458
|
console.log(` cli: ${VERSION}`);
|
|
@@ -3291,6 +3474,10 @@ async function status() {
|
|
|
3291
3474
|
console.log(` workflow_mode: ${projectWorkflowMode}`);
|
|
3292
3475
|
console.log(` tdd_enabled: ${projectTddEnabled}`);
|
|
3293
3476
|
console.log(` tdd_coverage_threshold: ${projectTddCoverageThreshold}`);
|
|
3477
|
+
console.log(` tdd_readiness: ${tddReadiness.status}`);
|
|
3478
|
+
if (tddReadiness.status !== "ready") {
|
|
3479
|
+
console.log(` tdd_readiness_reasons: ${tddReadiness.reasons.join("; ")}`);
|
|
3480
|
+
}
|
|
3294
3481
|
console.log("");
|
|
3295
3482
|
console.log(chalk6.bold("Sessions"));
|
|
3296
3483
|
console.log(` project_approval_mode: ${projectApprovalMode}`);
|
|
@@ -3398,7 +3585,7 @@ async function update(opts) {
|
|
|
3398
3585
|
}
|
|
3399
3586
|
|
|
3400
3587
|
// src/commands/upgrade.ts
|
|
3401
|
-
import
|
|
3588
|
+
import path24 from "path";
|
|
3402
3589
|
import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
|
|
3403
3590
|
import chalk8 from "chalk";
|
|
3404
3591
|
var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
|
|
@@ -3460,7 +3647,7 @@ async function upgrade(opts) {
|
|
|
3460
3647
|
].filter(Boolean) : [],
|
|
3461
3648
|
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
3462
3649
|
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
3463
|
-
"Will migrate behavior config to
|
|
3650
|
+
"Will migrate behavior config to schema 5 and disable unready project/session TDD settings.",
|
|
3464
3651
|
"Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
|
|
3465
3652
|
].join("\n");
|
|
3466
3653
|
if (opts.dryRun) {
|
|
@@ -3478,6 +3665,7 @@ async function upgrade(opts) {
|
|
|
3478
3665
|
}
|
|
3479
3666
|
}
|
|
3480
3667
|
for (const { target, config: config2 } of pending) {
|
|
3668
|
+
const beta1ProjectTddRequested = Number(config2.version) === 4 && config2.behavior?.tdd_enabled === true;
|
|
3481
3669
|
const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
|
|
3482
3670
|
supermodule: target.supermodule
|
|
3483
3671
|
});
|
|
@@ -3494,6 +3682,14 @@ async function upgrade(opts) {
|
|
|
3494
3682
|
await ensureHookBytecodeIgnored(target.dir);
|
|
3495
3683
|
await migrateLegacyWorkflowState(target.dir);
|
|
3496
3684
|
await migrateBehaviorConfig(target.configPath);
|
|
3685
|
+
const disabledSessionTddOverrides = await disableUnreadySessionTddOverrides(target.dir);
|
|
3686
|
+
if (beta1ProjectTddRequested || disabledSessionTddOverrides > 0) {
|
|
3687
|
+
console.log(
|
|
3688
|
+
chalk8.yellow(
|
|
3689
|
+
`${target.label}: TDD remains off until ec-tdd-init succeeds (project=${beta1ProjectTddRequested ? "disabled" : "unchanged"}, sessions_disabled=${disabledSessionTddOverrides}).`
|
|
3690
|
+
)
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3497
3693
|
await updateHarnessVersion(target.configPath, VERSION);
|
|
3498
3694
|
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
3499
3695
|
await setPendingInitSince(target.dir, VERSION);
|
|
@@ -3549,7 +3745,7 @@ async function needsHookConfigRefresh(target, config2) {
|
|
|
3549
3745
|
const manifest = await readInstallManifest(target.dir);
|
|
3550
3746
|
for (const agent of config2.agents) {
|
|
3551
3747
|
const meta = resolvePlatformMeta(target.dir, agent);
|
|
3552
|
-
const configPath2 =
|
|
3748
|
+
const configPath2 = path24.join(target.dir, meta.hookConfigFile);
|
|
3553
3749
|
const content = await readTextIfExists(configPath2);
|
|
3554
3750
|
if (content === null) {
|
|
3555
3751
|
return true;
|
|
@@ -3689,7 +3885,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
|
|
|
3689
3885
|
return true;
|
|
3690
3886
|
}
|
|
3691
3887
|
return pathAliases(
|
|
3692
|
-
normalizePathForHookComparison(
|
|
3888
|
+
normalizePathForHookComparison(path24.resolve(cwd, relativeHookPath))
|
|
3693
3889
|
).includes(normalizedHookPath);
|
|
3694
3890
|
});
|
|
3695
3891
|
}
|