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.
- package/CHANGELOG.md +151 -0
- package/README.md +50 -20
- package/dist/cli.js +478 -47
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +11 -0
- package/templates/claude/agents/ec-reviewer.md +6 -1
- package/templates/codex/agents/ec-implementer.toml +11 -0
- package/templates/codex/agents/ec-reviewer.toml +6 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +18 -6
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +27 -12
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +141 -35
- package/templates/common/skills/ec-config/SKILL.md +24 -2
- package/templates/common/skills/ec-git/SKILL.md +7 -1
- package/templates/common/skills/ec-implementing/SKILL.md +61 -1
- package/templates/common/skills/ec-memory/SKILL.md +76 -6
- package/templates/common/skills/ec-reviewing/SKILL.md +21 -3
- package/templates/common/skills/ec-task-close/SKILL.md +4 -0
- package/templates/common/skills/ec-task-management/SKILL.md +7 -1
- package/templates/common/skills/ec-tdd-init/SKILL.md +101 -0
- package/templates/common/skills/ec-verification/SKILL.md +68 -26
- package/templates/common/skills/ec-workflow/SKILL.md +81 -22
- package/templates/main-constraint/AGENTS.md.tpl +49 -14
- package/templates/main-constraint/CLAUDE.md.tpl +46 -14
- package/templates/qoder/agents/ec-implementer.md +11 -0
- package/templates/qoder/agents/ec-reviewer.md +6 -1
- package/templates/runtime/templates/dev-spec-skeleton.md +8 -1
- package/templates/runtime/tools/easy_coding_tdd_readiness.py +306 -0
- package/templates/shared-hooks/easy_coding_state.py +3878 -259
- package/templates/shared-hooks/easy_dev_spec.py +444 -30
- package/templates/shared-hooks/easy_dev_spec_execution.py +1014 -0
- 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 =
|
|
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 -->";
|
|
@@ -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
|
|
685
|
-
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
|
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 =
|
|
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: "
|
|
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
|
|
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
|
|
2990
|
-
import
|
|
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 =
|
|
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 =
|
|
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(
|
|
3287
|
+
if (await pathExists(path20.join(easyCodingDir, file))) {
|
|
3016
3288
|
assets.push(relativeEasyCodingPath(file));
|
|
3017
3289
|
}
|
|
3018
3290
|
}
|
|
3019
|
-
if (await pathExists(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
3314
|
+
return (await readdir6(dir)).length > 0;
|
|
3043
3315
|
}
|
|
3044
3316
|
function relativeEasyCodingPath(...segments) {
|
|
3045
|
-
return
|
|
3317
|
+
return path20.posix.join(EASY_CODING_DIR, ...segments);
|
|
3046
3318
|
}
|
|
3047
3319
|
function relativeConfigPath() {
|
|
3048
|
-
return
|
|
3320
|
+
return path20.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
3049
3321
|
}
|
|
3050
3322
|
function relativeProjectInitTaskPath() {
|
|
3051
|
-
return
|
|
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 =
|
|
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 =
|
|
3180
|
-
return relative ? relative.split(
|
|
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(
|
|
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(
|
|
3465
|
+
if (!await pathExists(path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
3194
3466
|
return;
|
|
3195
3467
|
}
|
|
3196
|
-
const config2 = parentTarget2.installed ? await readConfigYaml(
|
|
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 =
|
|
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
|
|
3486
|
+
import path23 from "path";
|
|
3215
3487
|
import chalk6 from "chalk";
|
|
3216
3488
|
|
|
3217
3489
|
// src/utils/session.ts
|
|
3218
|
-
import { readdir as
|
|
3219
|
-
import
|
|
3220
|
-
var
|
|
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
|
-
|
|
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
|
|
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
|
|
3513
|
+
for (const name of await readdir7(dir)) {
|
|
3238
3514
|
if (!name.endsWith(".json")) {
|
|
3239
3515
|
continue;
|
|
3240
3516
|
}
|
|
3241
|
-
const filePath =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
4123
|
+
normalizePathForHookComparison(path24.resolve(cwd, relativeHookPath))
|
|
3693
4124
|
).includes(normalizedHookPath);
|
|
3694
4125
|
});
|
|
3695
4126
|
}
|