easy-coding-harness 1.0.1 → 1.1.0-beta.1
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 +19 -0
- package/README.md +17 -15
- package/dist/cli.js +209 -195
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +4 -0
- package/templates/claude/agents/ec-reviewer.md +6 -0
- package/templates/codex/agents/ec-implementer.toml +4 -0
- package/templates/codex/agents/ec-reviewer.toml +6 -0
- package/templates/common/bundled-skills/ec-init/SKILL.md +2 -2
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +6 -6
- package/templates/common/skills/ec-analysis/SKILL.md +56 -104
- package/templates/common/skills/ec-config/SKILL.md +37 -47
- package/templates/common/skills/ec-implementing/SKILL.md +26 -10
- package/templates/common/skills/ec-lite/SKILL.md +1 -1
- package/templates/common/skills/ec-memory/SKILL.md +3 -4
- package/templates/common/skills/ec-quality/SKILL.md +46 -25
- package/templates/common/skills/ec-task-management/SKILL.md +2 -2
- package/templates/common/skills/ec-tdd-init/SKILL.md +7 -7
- package/templates/common/skills/ec-workflow/SKILL.md +24 -22
- package/templates/main-constraint/AGENTS.md.tpl +23 -16
- package/templates/main-constraint/CLAUDE.md.tpl +23 -16
- package/templates/qoder/agents/ec-implementer.md +4 -0
- package/templates/qoder/agents/ec-reviewer.md +6 -0
- package/templates/runtime/tools/easy_coding_java_coverage.py +5 -5
- package/templates/shared-hooks/easy_coding_inputs.py +305 -0
- package/templates/shared-hooks/easy_coding_state.py +495 -461
- package/templates/shared-hooks/inject-subagent-context.py +3 -3
package/dist/cli.js
CHANGED
|
@@ -103,8 +103,9 @@ async function isDirectory(filePath) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// src/utils/config-yaml.ts
|
|
106
|
-
var CONFIG_SCHEMA_VERSION =
|
|
107
|
-
var
|
|
106
|
+
var CONFIG_SCHEMA_VERSION = 6;
|
|
107
|
+
var DEFAULT_UT_COVERAGE_THRESHOLD = 90;
|
|
108
|
+
var UNIT_TEST_MODES = ["none", "ut", "tdd"];
|
|
108
109
|
var APPROVAL_MODES = ["approve", "guard", "confirm", "auto"];
|
|
109
110
|
var CONFIGURED_WORKFLOW_MODES = ["adaptive", "fast", "standard", "strict"];
|
|
110
111
|
function createDefaultConfig(params) {
|
|
@@ -127,8 +128,8 @@ function createDefaultConfig(params) {
|
|
|
127
128
|
behavior: {
|
|
128
129
|
approval_mode: "guard",
|
|
129
130
|
workflow_mode: "adaptive",
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
unit_test_mode: "none",
|
|
132
|
+
ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD
|
|
132
133
|
}
|
|
133
134
|
};
|
|
134
135
|
if (params.supermodule) {
|
|
@@ -182,43 +183,65 @@ function isApprovalMode(value) {
|
|
|
182
183
|
function isConfiguredWorkflowMode(value) {
|
|
183
184
|
return typeof value === "string" && CONFIGURED_WORKFLOW_MODES.includes(value);
|
|
184
185
|
}
|
|
185
|
-
function
|
|
186
|
+
function isUtCoverageThreshold(value) {
|
|
186
187
|
return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 100;
|
|
187
188
|
}
|
|
189
|
+
function isUnitTestMode(value) {
|
|
190
|
+
return typeof value === "string" && UNIT_TEST_MODES.includes(value);
|
|
191
|
+
}
|
|
192
|
+
function migrateUnitTestSettings(record) {
|
|
193
|
+
let changed = false;
|
|
194
|
+
if ("tdd_enabled" in record) {
|
|
195
|
+
if (!("unit_test_mode" in record) && typeof record.tdd_enabled === "boolean") {
|
|
196
|
+
record.unit_test_mode = record.tdd_enabled ? "tdd" : "none";
|
|
197
|
+
}
|
|
198
|
+
Reflect.deleteProperty(record, "tdd_enabled");
|
|
199
|
+
changed = true;
|
|
200
|
+
}
|
|
201
|
+
if ("tdd_coverage_threshold" in record) {
|
|
202
|
+
record.ut_coverage_threshold ??= record.tdd_coverage_threshold;
|
|
203
|
+
Reflect.deleteProperty(record, "tdd_coverage_threshold");
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
return changed;
|
|
207
|
+
}
|
|
188
208
|
function resolveLegacyBehavior(config2) {
|
|
189
209
|
const behavior = config2.behavior ?? {};
|
|
190
210
|
const legacyLite = behavior.confirm_mode === "lite";
|
|
191
211
|
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
212
|
const workflowMode = isConfiguredWorkflowMode(behavior.workflow_mode) ? behavior.workflow_mode : legacyLite ? "fast" : "adaptive";
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
return { approvalMode, workflowMode,
|
|
213
|
+
const unitTestMode = isUnitTestMode(behavior.unit_test_mode) ? behavior.unit_test_mode : Number(config2.version) >= 4 && behavior.tdd_enabled === true ? "tdd" : "none";
|
|
214
|
+
const configuredThreshold = behavior.ut_coverage_threshold ?? (Number(config2.version) >= 4 ? behavior.tdd_coverage_threshold : void 0);
|
|
215
|
+
const utCoverageThreshold = isUtCoverageThreshold(configuredThreshold) ? configuredThreshold : DEFAULT_UT_COVERAGE_THRESHOLD;
|
|
216
|
+
return { approvalMode, workflowMode, unitTestMode, utCoverageThreshold };
|
|
197
217
|
}
|
|
198
|
-
async function setBehaviorModes(filePath, approvalMode, workflowMode,
|
|
199
|
-
if (
|
|
200
|
-
throw new Error("
|
|
218
|
+
async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold) {
|
|
219
|
+
if (unitTestMode !== void 0 && !isUnitTestMode(unitTestMode)) {
|
|
220
|
+
throw new Error("Unit test mode must be none, ut, or tdd.");
|
|
221
|
+
}
|
|
222
|
+
if (utCoverageThreshold !== void 0 && !isUtCoverageThreshold(utCoverageThreshold)) {
|
|
223
|
+
throw new Error("Unit test coverage threshold must be an integer from 1 to 100.");
|
|
201
224
|
}
|
|
202
225
|
return updateConfigYaml(filePath, (config2) => {
|
|
203
226
|
const legacyBehavior = config2.behavior ?? {};
|
|
204
227
|
const resolvedBehavior = resolveLegacyBehavior(config2);
|
|
205
228
|
const behavior = Object.fromEntries(
|
|
206
229
|
Object.entries(legacyBehavior).filter(
|
|
207
|
-
([key]) => key !== "strict_confirm" && key !== "auto_mode" && key !== "confirm_mode" && key !== "approval_mode" && key !== "workflow_mode" && key !== "tdd_enabled" && key !== "tdd_coverage_threshold"
|
|
230
|
+
([key]) => key !== "strict_confirm" && key !== "auto_mode" && key !== "confirm_mode" && key !== "approval_mode" && key !== "workflow_mode" && key !== "tdd_enabled" && key !== "tdd_coverage_threshold" && key !== "unit_test_mode" && key !== "ut_coverage_threshold"
|
|
208
231
|
)
|
|
209
232
|
);
|
|
210
233
|
behavior.approval_mode = approvalMode;
|
|
211
234
|
behavior.workflow_mode = workflowMode;
|
|
212
|
-
behavior.
|
|
213
|
-
behavior.
|
|
235
|
+
behavior.unit_test_mode = unitTestMode ?? resolvedBehavior.unitTestMode;
|
|
236
|
+
behavior.ut_coverage_threshold = utCoverageThreshold ?? resolvedBehavior.utCoverageThreshold;
|
|
214
237
|
config2.behavior = behavior;
|
|
215
238
|
config2.version = CONFIG_SCHEMA_VERSION;
|
|
216
239
|
});
|
|
217
240
|
}
|
|
218
241
|
async function migrateBehaviorConfig(filePath) {
|
|
219
242
|
const config2 = await readConfigYaml(filePath);
|
|
220
|
-
const { approvalMode, workflowMode,
|
|
221
|
-
return setBehaviorModes(filePath, approvalMode, workflowMode,
|
|
243
|
+
const { approvalMode, workflowMode, unitTestMode, utCoverageThreshold } = resolveLegacyBehavior(config2);
|
|
244
|
+
return setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold);
|
|
222
245
|
}
|
|
223
246
|
async function ensureProjectId(filePath) {
|
|
224
247
|
let projectId = "";
|
|
@@ -664,97 +687,11 @@ function byPath(a, b) {
|
|
|
664
687
|
}
|
|
665
688
|
|
|
666
689
|
// src/utils/runtime-scaffold.ts
|
|
667
|
-
import
|
|
668
|
-
|
|
669
|
-
// src/utils/template-paths.ts
|
|
670
|
-
import { existsSync } from "fs";
|
|
671
|
-
import path5 from "path";
|
|
672
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
673
|
-
function getTemplateRoot() {
|
|
674
|
-
const here = path5.dirname(fileURLToPath2(import.meta.url));
|
|
675
|
-
const candidates = [
|
|
676
|
-
path5.resolve(here, "../templates"),
|
|
677
|
-
path5.resolve(here, "../../templates"),
|
|
678
|
-
path5.resolve(process.cwd(), "src/templates"),
|
|
679
|
-
path5.resolve(process.cwd(), "templates")
|
|
680
|
-
];
|
|
681
|
-
const found = candidates.find((candidate) => existsSync(candidate));
|
|
682
|
-
if (!found) {
|
|
683
|
-
throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
|
|
684
|
-
}
|
|
685
|
-
return found;
|
|
686
|
-
}
|
|
687
|
-
function getTemplatePath(...segments) {
|
|
688
|
-
return path5.join(getTemplateRoot(), ...segments);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// src/utils/runtime-scaffold.ts
|
|
692
|
-
async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
693
|
-
const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
|
|
694
|
-
await ensureDir(easyCodingDir);
|
|
695
|
-
const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
|
|
696
|
-
let projectId = opts.projectId ?? createProjectId();
|
|
697
|
-
if (!await pathExists(configPath2)) {
|
|
698
|
-
const projectName = path6.basename(cwd);
|
|
699
|
-
await writeConfigYaml(
|
|
700
|
-
configPath2,
|
|
701
|
-
createDefaultConfig({
|
|
702
|
-
projectName,
|
|
703
|
-
projectId,
|
|
704
|
-
harnessVersion: VERSION,
|
|
705
|
-
agents,
|
|
706
|
-
supermodule: opts.supermodule
|
|
707
|
-
})
|
|
708
|
-
);
|
|
709
|
-
} else {
|
|
710
|
-
projectId = await ensureProjectId(configPath2);
|
|
711
|
-
}
|
|
712
|
-
await ensureDir(path6.join(easyCodingDir, "tasks"));
|
|
713
|
-
await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
|
|
714
|
-
await ensureDir(path6.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
715
|
-
await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
716
|
-
await writeMemoryScaffold(easyCodingDir);
|
|
717
|
-
await writeTemplatesScaffold(easyCodingDir);
|
|
718
|
-
await writeToolsScaffold(easyCodingDir);
|
|
719
|
-
return projectId;
|
|
720
|
-
}
|
|
721
|
-
async function writeToolsScaffold(easyCodingDir) {
|
|
722
|
-
const toolsDir = path6.join(easyCodingDir, TOOLS_DIR);
|
|
723
|
-
await ensureDir(toolsDir);
|
|
724
|
-
for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
|
|
725
|
-
const src = getTemplatePath("runtime", "tools", file);
|
|
726
|
-
await writeTextFile(path6.join(toolsDir, file), await readTextFile(src));
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
async function writeTemplatesScaffold(easyCodingDir) {
|
|
730
|
-
const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
|
|
731
|
-
await ensureDir(templatesDir);
|
|
732
|
-
const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
|
|
733
|
-
const dest = path6.join(templatesDir, "dev-spec-skeleton.md");
|
|
734
|
-
await writeTextFile(dest, await readTextFile(src));
|
|
735
|
-
}
|
|
736
|
-
async function writeMemoryScaffold(easyCodingDir) {
|
|
737
|
-
const memoryDir = path6.join(easyCodingDir, MEMORY_DIR);
|
|
738
|
-
await ensureDir(path6.join(memoryDir, "short"));
|
|
739
|
-
await ensureDir(path6.join(memoryDir, "long"));
|
|
740
|
-
for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
|
|
741
|
-
const destination = path6.join(memoryDir, "long", file);
|
|
742
|
-
if (await pathExists(destination)) {
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
746
|
-
await writeTextFile(destination, await readTextFile(templatePath));
|
|
747
|
-
}
|
|
748
|
-
const shortTemplateDest = path6.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
|
|
749
|
-
if (!await pathExists(shortTemplateDest)) {
|
|
750
|
-
const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
|
|
751
|
-
await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
|
|
752
|
-
}
|
|
753
|
-
}
|
|
690
|
+
import path7 from "path";
|
|
754
691
|
|
|
755
692
|
// src/utils/task-json.ts
|
|
756
693
|
import { readdir } from "fs/promises";
|
|
757
|
-
import
|
|
694
|
+
import path5 from "path";
|
|
758
695
|
var LEGACY_STAGE_MAP = {
|
|
759
696
|
WAITING_CONFIRM: "ANALYSIS",
|
|
760
697
|
MEMORY_SHORT: "MEMORY",
|
|
@@ -781,7 +718,7 @@ function createProjectInitTask(params) {
|
|
|
781
718
|
};
|
|
782
719
|
}
|
|
783
720
|
function getTaskJsonPath(cwd, taskId) {
|
|
784
|
-
return
|
|
721
|
+
return path5.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
|
|
785
722
|
}
|
|
786
723
|
async function writeTaskJson(filePath, task) {
|
|
787
724
|
await writeTextFile(filePath, JSON.stringify(task, null, 2));
|
|
@@ -1006,9 +943,10 @@ function migrateTaskWorkflowState(task) {
|
|
|
1006
943
|
task.workflow_mode_legacy = true;
|
|
1007
944
|
changed = true;
|
|
1008
945
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
task.
|
|
946
|
+
changed = migrateUnitTestSettings(task) || changed;
|
|
947
|
+
if (isActive && taskType !== "project-init" && task.unit_test_mode === void 0) {
|
|
948
|
+
task.unit_test_mode = "none";
|
|
949
|
+
task.ut_coverage_threshold ??= DEFAULT_UT_COVERAGE_THRESHOLD;
|
|
1012
950
|
task.tdd_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1013
951
|
task.tdd_confirmed_by = "upgrade-migration";
|
|
1014
952
|
changed = true;
|
|
@@ -1017,6 +955,7 @@ function migrateTaskWorkflowState(task) {
|
|
|
1017
955
|
}
|
|
1018
956
|
function migrateSessionBehavior(session) {
|
|
1019
957
|
let changed = migrateAgentFields(session, ["agent", "last_agent"]);
|
|
958
|
+
changed = migrateUnitTestSettings(session) || changed;
|
|
1020
959
|
const legacyMode = session.confirm_mode;
|
|
1021
960
|
const legacyLite = legacyMode === "lite";
|
|
1022
961
|
if (!["approve", "guard", "confirm", "auto"].includes(String(session.approval_mode ?? ""))) {
|
|
@@ -1092,16 +1031,16 @@ function migrationWorkflowMode(task, candidates, projectWorkflowMode) {
|
|
|
1092
1031
|
);
|
|
1093
1032
|
}
|
|
1094
1033
|
async function taskFiles(cwd) {
|
|
1095
|
-
const tasksDir =
|
|
1034
|
+
const tasksDir = path5.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
1096
1035
|
if (!await pathExists(tasksDir)) return [];
|
|
1097
1036
|
const entries = await readdir(tasksDir, { withFileTypes: true });
|
|
1098
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) =>
|
|
1037
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => path5.join(tasksDir, entry.name, "task.json"));
|
|
1099
1038
|
}
|
|
1100
1039
|
async function sessionFiles(cwd) {
|
|
1101
|
-
const sessionsDir =
|
|
1040
|
+
const sessionsDir = path5.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
1102
1041
|
if (!await pathExists(sessionsDir)) return [];
|
|
1103
1042
|
const entries = await readdir(sessionsDir, { withFileTypes: true });
|
|
1104
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) =>
|
|
1043
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path5.join(sessionsDir, entry.name));
|
|
1105
1044
|
}
|
|
1106
1045
|
async function readJsonRecord(filePath) {
|
|
1107
1046
|
try {
|
|
@@ -1116,7 +1055,7 @@ async function hasLegacyWorkflowState(cwd) {
|
|
|
1116
1055
|
if (!await pathExists(filePath)) continue;
|
|
1117
1056
|
const task = await readJsonRecord(filePath);
|
|
1118
1057
|
if (!task) continue;
|
|
1119
|
-
if (hasLegacyTaskAgentIdentities(task) || isLegacyStage(task.status) || "verification_checkpoint" in task || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && ["analysis", "doc", "report"].includes(String(task.type ?? "").toLowerCase()) || !["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(
|
|
1058
|
+
if (hasLegacyTaskAgentIdentities(task) || isLegacyStage(task.status) || "verification_checkpoint" in task || "tdd_enabled" in task || "tdd_coverage_threshold" in task || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && ["analysis", "doc", "report"].includes(String(task.type ?? "").toLowerCase()) || !["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(
|
|
1120
1059
|
(entry) => entry && typeof entry === "object" && !Array.isArray(entry) && isLegacyStage(entry.stage)
|
|
1121
1060
|
)) {
|
|
1122
1061
|
return true;
|
|
@@ -1128,7 +1067,7 @@ async function hasLegacyWorkflowState(cwd) {
|
|
|
1128
1067
|
if (["agent", "last_agent"].some((field) => {
|
|
1129
1068
|
const migrated = migratedAgentIdentity(session[field]);
|
|
1130
1069
|
return migrated !== void 0 && migrated !== session[field];
|
|
1131
|
-
}) || isLegacyStage(session.last_seen_stage) || "confirm_mode" in session) {
|
|
1070
|
+
}) || isLegacyStage(session.last_seen_stage) || "confirm_mode" in session || "tdd_enabled" in session || "tdd_coverage_threshold" in session) {
|
|
1132
1071
|
return true;
|
|
1133
1072
|
}
|
|
1134
1073
|
}
|
|
@@ -1139,7 +1078,7 @@ async function migrateLegacyWorkflowState(cwd) {
|
|
|
1139
1078
|
let sessionsUpdated = 0;
|
|
1140
1079
|
const updatedTaskPaths = /* @__PURE__ */ new Set();
|
|
1141
1080
|
let projectWorkflowMode = "adaptive";
|
|
1142
|
-
const configPath2 =
|
|
1081
|
+
const configPath2 = path5.join(cwd, EASY_CODING_DIR, "config.yaml");
|
|
1143
1082
|
if (await pathExists(configPath2)) {
|
|
1144
1083
|
try {
|
|
1145
1084
|
const config2 = await readConfigYaml(configPath2);
|
|
@@ -1194,7 +1133,7 @@ async function migrateLegacyWorkflowState(cwd) {
|
|
|
1194
1133
|
if (!task) continue;
|
|
1195
1134
|
const migrationOwned = task.workflow_mode_legacy === true && task.workflow_mode_confirmed_by === "upgrade-migration";
|
|
1196
1135
|
if (!migrationOwned) continue;
|
|
1197
|
-
const taskId =
|
|
1136
|
+
const taskId = path5.basename(path5.dirname(filePath));
|
|
1198
1137
|
const mode = migrationWorkflowMode(
|
|
1199
1138
|
task,
|
|
1200
1139
|
candidatesByTask.get(taskId) ?? [],
|
|
@@ -1229,7 +1168,7 @@ async function setPendingInitSince(cwd, version) {
|
|
|
1229
1168
|
await writeTaskJson(filePath, task);
|
|
1230
1169
|
}
|
|
1231
1170
|
async function listTasks(cwd) {
|
|
1232
|
-
const tasksDir =
|
|
1171
|
+
const tasksDir = path5.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
1233
1172
|
if (!await pathExists(tasksDir)) {
|
|
1234
1173
|
return [];
|
|
1235
1174
|
}
|
|
@@ -1260,6 +1199,105 @@ function isActiveTask(task) {
|
|
|
1260
1199
|
return task.status !== "COMPLETE" && task.status !== "CLOSED";
|
|
1261
1200
|
}
|
|
1262
1201
|
|
|
1202
|
+
// src/utils/template-paths.ts
|
|
1203
|
+
import { existsSync } from "fs";
|
|
1204
|
+
import path6 from "path";
|
|
1205
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1206
|
+
function getTemplateRoot() {
|
|
1207
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1208
|
+
const candidates = [
|
|
1209
|
+
path6.resolve(here, "../templates"),
|
|
1210
|
+
path6.resolve(here, "../../templates"),
|
|
1211
|
+
path6.resolve(process.cwd(), "src/templates"),
|
|
1212
|
+
path6.resolve(process.cwd(), "templates")
|
|
1213
|
+
];
|
|
1214
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
1215
|
+
if (!found) {
|
|
1216
|
+
throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
|
|
1217
|
+
}
|
|
1218
|
+
return found;
|
|
1219
|
+
}
|
|
1220
|
+
function getTemplatePath(...segments) {
|
|
1221
|
+
return path6.join(getTemplateRoot(), ...segments);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/utils/runtime-scaffold.ts
|
|
1225
|
+
async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
1226
|
+
const easyCodingDir = path7.join(cwd, EASY_CODING_DIR);
|
|
1227
|
+
await ensureDir(easyCodingDir);
|
|
1228
|
+
const configPath2 = path7.join(easyCodingDir, CONFIG_FILE);
|
|
1229
|
+
let projectId = opts.projectId ?? createProjectId();
|
|
1230
|
+
if (!await pathExists(configPath2)) {
|
|
1231
|
+
const projectName = path7.basename(cwd);
|
|
1232
|
+
await writeConfigYaml(
|
|
1233
|
+
configPath2,
|
|
1234
|
+
createDefaultConfig({
|
|
1235
|
+
projectName,
|
|
1236
|
+
projectId,
|
|
1237
|
+
harnessVersion: VERSION,
|
|
1238
|
+
agents,
|
|
1239
|
+
supermodule: opts.supermodule
|
|
1240
|
+
})
|
|
1241
|
+
);
|
|
1242
|
+
} else {
|
|
1243
|
+
projectId = await ensureProjectId(configPath2);
|
|
1244
|
+
}
|
|
1245
|
+
await ensureDir(path7.join(easyCodingDir, "tasks"));
|
|
1246
|
+
await ensureDir(path7.join(easyCodingDir, SESSIONS_DIR));
|
|
1247
|
+
await ensureDir(path7.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
1248
|
+
await ensureDir(path7.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
1249
|
+
await writeMemoryScaffold(easyCodingDir);
|
|
1250
|
+
await writeTemplatesScaffold(easyCodingDir);
|
|
1251
|
+
await writeToolsScaffold(cwd);
|
|
1252
|
+
return projectId;
|
|
1253
|
+
}
|
|
1254
|
+
async function runtimeToolUpdates(cwd) {
|
|
1255
|
+
const frozen = (await listTasks(cwd)).some(
|
|
1256
|
+
({ task }) => isActiveTask(task) && (task.unit_test_mode === "ut" || task.unit_test_mode === "tdd" || task.tdd_enabled === true)
|
|
1257
|
+
);
|
|
1258
|
+
const updates = [];
|
|
1259
|
+
for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
|
|
1260
|
+
const target = path7.join(cwd, EASY_CODING_DIR, TOOLS_DIR, file);
|
|
1261
|
+
const current = await readTextIfExists(target);
|
|
1262
|
+
if (frozen && current !== null) continue;
|
|
1263
|
+
const content = await readTextFile(getTemplatePath("runtime", "tools", file));
|
|
1264
|
+
if (content !== current) updates.push({ path: target, content });
|
|
1265
|
+
}
|
|
1266
|
+
return updates;
|
|
1267
|
+
}
|
|
1268
|
+
async function writeToolsScaffold(cwd) {
|
|
1269
|
+
const toolsDir = path7.join(cwd, EASY_CODING_DIR, TOOLS_DIR);
|
|
1270
|
+
await ensureDir(toolsDir);
|
|
1271
|
+
for (const update2 of await runtimeToolUpdates(cwd)) {
|
|
1272
|
+
await writeTextFile(update2.path, update2.content);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
async function writeTemplatesScaffold(easyCodingDir) {
|
|
1276
|
+
const templatesDir = path7.join(easyCodingDir, TEMPLATES_DIR);
|
|
1277
|
+
await ensureDir(templatesDir);
|
|
1278
|
+
const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
|
|
1279
|
+
const dest = path7.join(templatesDir, "dev-spec-skeleton.md");
|
|
1280
|
+
await writeTextFile(dest, await readTextFile(src));
|
|
1281
|
+
}
|
|
1282
|
+
async function writeMemoryScaffold(easyCodingDir) {
|
|
1283
|
+
const memoryDir = path7.join(easyCodingDir, MEMORY_DIR);
|
|
1284
|
+
await ensureDir(path7.join(memoryDir, "short"));
|
|
1285
|
+
await ensureDir(path7.join(memoryDir, "long"));
|
|
1286
|
+
for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
|
|
1287
|
+
const destination = path7.join(memoryDir, "long", file);
|
|
1288
|
+
if (await pathExists(destination)) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
1292
|
+
await writeTextFile(destination, await readTextFile(templatePath));
|
|
1293
|
+
}
|
|
1294
|
+
const shortTemplateDest = path7.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
|
|
1295
|
+
if (!await pathExists(shortTemplateDest)) {
|
|
1296
|
+
const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
|
|
1297
|
+
await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1263
1301
|
// src/commands/install-harness.ts
|
|
1264
1302
|
import path13 from "path";
|
|
1265
1303
|
|
|
@@ -3181,94 +3219,66 @@ async function config() {
|
|
|
3181
3219
|
cancel4("Configuration cancelled.");
|
|
3182
3220
|
return;
|
|
3183
3221
|
}
|
|
3184
|
-
const workflowMode =
|
|
3185
|
-
|
|
3186
|
-
|
|
3222
|
+
const workflowMode = "adaptive";
|
|
3223
|
+
const unitTestMode = await select({
|
|
3224
|
+
message: `Select Java unit test strategy (current: ${current.unitTestMode})`,
|
|
3225
|
+
initialValue: current.unitTestMode,
|
|
3187
3226
|
options: [
|
|
3188
|
-
{
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
hint: "freezes to fast, standard, or strict after ANALYSIS"
|
|
3192
|
-
},
|
|
3193
|
-
{
|
|
3194
|
-
value: "fast",
|
|
3195
|
-
label: "fast \u2014 compact execution for low-risk tasks",
|
|
3196
|
-
hint: "all workflow stages still run"
|
|
3197
|
-
},
|
|
3198
|
-
{
|
|
3199
|
-
value: "standard",
|
|
3200
|
-
label: "standard \u2014 balanced execution",
|
|
3201
|
-
hint: "independent review and impacted verification"
|
|
3202
|
-
},
|
|
3203
|
-
{
|
|
3204
|
-
value: "strict",
|
|
3205
|
-
label: "strict \u2014 maximum assurance",
|
|
3206
|
-
hint: "multi-dimensional review and full verification"
|
|
3207
|
-
}
|
|
3227
|
+
{ value: "none", label: "none \u2014 preserve task-required verification (default)" },
|
|
3228
|
+
{ value: "ut", label: "UT \u2014 passing unit tests and changed-line coverage" },
|
|
3229
|
+
{ value: "tdd", label: "TDD \u2014 test-first development and changed-line coverage" }
|
|
3208
3230
|
]
|
|
3209
3231
|
});
|
|
3210
|
-
if (typeof
|
|
3232
|
+
if (typeof unitTestMode === "symbol") {
|
|
3211
3233
|
cancel4("Configuration cancelled.");
|
|
3212
3234
|
return;
|
|
3213
3235
|
}
|
|
3214
|
-
|
|
3215
|
-
message: `Enable Java TDD for this project (current: ${current.tddEnabled ? "enabled" : "disabled"})`,
|
|
3216
|
-
initialValue: current.tddEnabled,
|
|
3217
|
-
options: [
|
|
3218
|
-
{ value: false, label: "disabled \u2014 preserve current test depth (default)" },
|
|
3219
|
-
{ value: true, label: "enabled \u2014 require TDD evidence and changed-line coverage" }
|
|
3220
|
-
]
|
|
3221
|
-
});
|
|
3222
|
-
if (typeof tddEnabled === "symbol") {
|
|
3223
|
-
cancel4("Configuration cancelled.");
|
|
3224
|
-
return;
|
|
3225
|
-
}
|
|
3226
|
-
if (tddEnabled) {
|
|
3236
|
+
if (unitTestMode !== "none") {
|
|
3227
3237
|
const readiness = await inspectTddReadiness(process.cwd());
|
|
3228
3238
|
if (readiness.status !== "ready") {
|
|
3229
3239
|
cancel4(
|
|
3230
|
-
`
|
|
3240
|
+
`Unit test strategy was not enabled. ${readiness.status === "needs_init" ? "Run ec-tdd-init first" : "Repair coverage readiness"}: ${readiness.reasons.join("; ")}. No project modes were changed.`
|
|
3231
3241
|
);
|
|
3232
3242
|
return;
|
|
3233
3243
|
}
|
|
3234
3244
|
}
|
|
3235
|
-
let
|
|
3236
|
-
if (
|
|
3245
|
+
let utCoverageThreshold = current.utCoverageThreshold;
|
|
3246
|
+
if (unitTestMode !== "none") {
|
|
3237
3247
|
const thresholdInput = await text({
|
|
3238
3248
|
message: "Minimum changed-production-line coverage percentage",
|
|
3239
|
-
initialValue: String(current.
|
|
3249
|
+
initialValue: String(current.utCoverageThreshold),
|
|
3240
3250
|
validate(value) {
|
|
3241
3251
|
const parsed = Number(value);
|
|
3242
|
-
return
|
|
3252
|
+
return isUtCoverageThreshold(parsed) ? void 0 : "Enter an integer from 1 to 100.";
|
|
3243
3253
|
}
|
|
3244
3254
|
});
|
|
3245
3255
|
if (typeof thresholdInput === "symbol") {
|
|
3246
3256
|
cancel4("Configuration cancelled.");
|
|
3247
3257
|
return;
|
|
3248
3258
|
}
|
|
3249
|
-
|
|
3259
|
+
utCoverageThreshold = Number(thresholdInput);
|
|
3250
3260
|
}
|
|
3251
3261
|
const shouldSave = await confirm3({
|
|
3252
|
-
message: `Set approval=${approvalMode}, workflow=${workflowMode},
|
|
3262
|
+
message: `Set approval=${approvalMode}, workflow=${workflowMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}?`,
|
|
3253
3263
|
initialValue: true
|
|
3254
3264
|
});
|
|
3255
3265
|
if (typeof shouldSave === "symbol" || !shouldSave) {
|
|
3256
3266
|
cancel4("Configuration cancelled.");
|
|
3257
3267
|
return;
|
|
3258
3268
|
}
|
|
3259
|
-
if (
|
|
3269
|
+
if (unitTestMode !== "none") {
|
|
3260
3270
|
const readiness = await inspectTddReadiness(process.cwd());
|
|
3261
3271
|
if (readiness.status !== "ready") {
|
|
3262
3272
|
cancel4(
|
|
3263
|
-
`
|
|
3273
|
+
`Unit test strategy was not enabled because readiness changed before save: ${readiness.reasons.join("; ")}. No project modes were changed.`
|
|
3264
3274
|
);
|
|
3265
3275
|
return;
|
|
3266
3276
|
}
|
|
3267
3277
|
}
|
|
3268
|
-
await setBehaviorModes(configPath2, approvalMode, workflowMode,
|
|
3278
|
+
await setBehaviorModes(configPath2, approvalMode, workflowMode, unitTestMode, utCoverageThreshold);
|
|
3269
3279
|
outro3(
|
|
3270
3280
|
chalk4.green(
|
|
3271
|
-
`Project modes updated: approval=${approvalMode}, workflow=${workflowMode},
|
|
3281
|
+
`Project modes updated: approval=${approvalMode}, workflow=${workflowMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}.`
|
|
3272
3282
|
)
|
|
3273
3283
|
);
|
|
3274
3284
|
}
|
|
@@ -3559,9 +3569,11 @@ async function cleanSessionRuntime(cwd, options = {}) {
|
|
|
3559
3569
|
const maxSessions = options.maxSessions ?? MAX_SESSION_FILES;
|
|
3560
3570
|
const reserveSlots = options.reserveSlots ?? 0;
|
|
3561
3571
|
const candidates = (await listSessionCleanupCandidates(cwd)).filter((candidate) => {
|
|
3562
|
-
if (!options.
|
|
3572
|
+
if (!options.preserveUnitTestSettings) return true;
|
|
3563
3573
|
const session = parseSessionFile(candidate.content);
|
|
3564
|
-
return !session || !
|
|
3574
|
+
return !session || !["unit_test_mode", "ut_coverage_threshold", "tdd_enabled", "tdd_coverage_threshold"].some(
|
|
3575
|
+
(key) => key in session
|
|
3576
|
+
);
|
|
3565
3577
|
});
|
|
3566
3578
|
const removed = /* @__PURE__ */ new Set();
|
|
3567
3579
|
for (const candidate of candidates) {
|
|
@@ -3703,7 +3715,6 @@ async function status() {
|
|
|
3703
3715
|
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
3704
3716
|
const sessions = await listSessionFiles(cwd);
|
|
3705
3717
|
const versionRelation = compareVersions(config2.harness_version, VERSION);
|
|
3706
|
-
const tddReadiness = await inspectTddReadiness(cwd);
|
|
3707
3718
|
console.log(chalk6.bold("Harness"));
|
|
3708
3719
|
console.log(` version: ${config2.harness_version}`);
|
|
3709
3720
|
console.log(` cli: ${VERSION}`);
|
|
@@ -3719,27 +3730,29 @@ async function status() {
|
|
|
3719
3730
|
const migratedBehavior = resolveLegacyBehavior(config2);
|
|
3720
3731
|
const projectApprovalMode = isApprovalMode(config2.behavior?.approval_mode) ? config2.behavior.approval_mode : migratedBehavior.approvalMode;
|
|
3721
3732
|
const projectWorkflowMode = isConfiguredWorkflowMode(config2.behavior?.workflow_mode) ? config2.behavior.workflow_mode : migratedBehavior.workflowMode;
|
|
3722
|
-
const
|
|
3723
|
-
const
|
|
3733
|
+
const projectUnitTestMode = migratedBehavior.unitTestMode;
|
|
3734
|
+
const projectUtCoverageThreshold = migratedBehavior.utCoverageThreshold;
|
|
3735
|
+
const needsCoverage = projectUnitTestMode !== "none" || sessions.some(({ session }) => ["ut", "tdd"].includes(session.unit_test_mode ?? "none")) || activeTasks.some(({ task }) => ["ut", "tdd"].includes(task.unit_test_mode ?? "none"));
|
|
3736
|
+
const readiness = needsCoverage ? await inspectTddReadiness(cwd) : { status: "not_checked", reasons: [] };
|
|
3724
3737
|
console.log(` approval_mode: ${projectApprovalMode}`);
|
|
3725
3738
|
console.log(` workflow_mode: ${projectWorkflowMode}`);
|
|
3726
|
-
console.log(`
|
|
3727
|
-
console.log(`
|
|
3728
|
-
console.log(`
|
|
3729
|
-
if (
|
|
3730
|
-
console.log(`
|
|
3739
|
+
console.log(` unit_test_mode: ${projectUnitTestMode}`);
|
|
3740
|
+
console.log(` ut_coverage_threshold: ${projectUtCoverageThreshold}`);
|
|
3741
|
+
console.log(` unit_test_readiness: ${readiness.status}`);
|
|
3742
|
+
if (readiness.reasons.length > 0) {
|
|
3743
|
+
console.log(` unit_test_readiness_reasons: ${readiness.reasons.join("; ")}`);
|
|
3731
3744
|
}
|
|
3732
3745
|
console.log("");
|
|
3733
3746
|
console.log(chalk6.bold("Sessions"));
|
|
3734
3747
|
console.log(` project_approval_mode: ${projectApprovalMode}`);
|
|
3735
3748
|
console.log(` project_workflow_mode: ${projectWorkflowMode}`);
|
|
3736
|
-
console.log(`
|
|
3737
|
-
console.log(`
|
|
3749
|
+
console.log(` project_unit_test_mode: ${projectUnitTestMode}`);
|
|
3750
|
+
console.log(` project_ut_coverage_threshold: ${projectUtCoverageThreshold}`);
|
|
3738
3751
|
console.log(` effective_approval_mode: ${projectApprovalMode} (without a session override)`);
|
|
3739
3752
|
console.log(` configured_workflow_mode: ${projectWorkflowMode} (without a session override)`);
|
|
3740
|
-
console.log(`
|
|
3753
|
+
console.log(` effective_unit_test_mode: ${projectUnitTestMode} (without a session override)`);
|
|
3741
3754
|
console.log(
|
|
3742
|
-
`
|
|
3755
|
+
` effective_ut_coverage_threshold: ${projectUtCoverageThreshold} (without a session override)`
|
|
3743
3756
|
);
|
|
3744
3757
|
if (sessions.length === 0) {
|
|
3745
3758
|
console.log(" no session files");
|
|
@@ -3751,20 +3764,20 @@ async function status() {
|
|
|
3751
3764
|
);
|
|
3752
3765
|
const sessionApprovalMode = session.approval_mode ?? (legacySessionMode === "lite" ? "guard" : legacySessionMode);
|
|
3753
3766
|
const sessionWorkflowMode = session.workflow_mode ?? (legacySessionMode === "lite" ? "fast" : hasLegacySessionMode ? "adaptive" : void 0);
|
|
3754
|
-
const
|
|
3755
|
-
const
|
|
3767
|
+
const sessionUnitTestMode = session.unit_test_mode;
|
|
3768
|
+
const sessionUtCoverageThreshold = session.ut_coverage_threshold;
|
|
3756
3769
|
console.log(` - ${key}`);
|
|
3757
3770
|
console.log(` agent: ${session.agent ?? "legacy/unknown"}`);
|
|
3758
3771
|
console.log(` source: ${session.session_source ?? "legacy"}`);
|
|
3759
3772
|
console.log(` approval_mode: ${sessionApprovalMode ?? "project default"}`);
|
|
3760
3773
|
console.log(` workflow_mode: ${sessionWorkflowMode ?? "project default"}`);
|
|
3761
|
-
console.log(`
|
|
3762
|
-
console.log(`
|
|
3774
|
+
console.log(` unit_test_mode: ${sessionUnitTestMode ?? "project default"}`);
|
|
3775
|
+
console.log(` ut_coverage_threshold: ${sessionUtCoverageThreshold ?? "project default"}`);
|
|
3763
3776
|
console.log(` effective_approval_mode: ${sessionApprovalMode ?? projectApprovalMode}`);
|
|
3764
3777
|
console.log(` configured_workflow_mode: ${sessionWorkflowMode ?? projectWorkflowMode}`);
|
|
3765
|
-
console.log(`
|
|
3778
|
+
console.log(` effective_unit_test_mode: ${sessionUnitTestMode ?? projectUnitTestMode}`);
|
|
3766
3779
|
console.log(
|
|
3767
|
-
`
|
|
3780
|
+
` effective_ut_coverage_threshold: ${sessionUtCoverageThreshold ?? projectUtCoverageThreshold}`
|
|
3768
3781
|
);
|
|
3769
3782
|
console.log(
|
|
3770
3783
|
` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
|
|
@@ -3785,10 +3798,8 @@ async function status() {
|
|
|
3785
3798
|
console.log(
|
|
3786
3799
|
` task_workflow_mode: ${task.workflow_mode ?? task.workflow_mode_proposal?.selected_mode ?? "not resolved"}`
|
|
3787
3800
|
);
|
|
3788
|
-
console.log(`
|
|
3789
|
-
console.log(
|
|
3790
|
-
` task_tdd_coverage_threshold: ${task.tdd_coverage_threshold ?? "not frozen"}`
|
|
3791
|
-
);
|
|
3801
|
+
console.log(` task_unit_test_mode: ${task.unit_test_mode ?? "not frozen"}`);
|
|
3802
|
+
console.log(` task_ut_coverage_threshold: ${task.ut_coverage_threshold ?? "not frozen"}`);
|
|
3792
3803
|
console.log(` last_agent: ${task.last_agent}`);
|
|
3793
3804
|
} else {
|
|
3794
3805
|
console.log(` current_task: ${session.current_task} (task.json missing)`);
|
|
@@ -3903,10 +3914,11 @@ async function upgrade(opts) {
|
|
|
3903
3914
|
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
3904
3915
|
"Will remove retired files that still match the previous install manifest and preserve locally modified copies.",
|
|
3905
3916
|
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
3906
|
-
"Will migrate behavior config to schema
|
|
3907
|
-
"Will
|
|
3917
|
+
"Will migrate behavior config to schema 6 while preserving project/session unit test settings and frozen task baselines.",
|
|
3918
|
+
"Will preserve runtime tools used by active UT/TDD tasks; run upgrade after those tasks finish to refresh the tools.",
|
|
3919
|
+
"Will prune expired session bindings without explicit unit test settings and orphan acceptance snapshots in each upgraded target while preserving tasks, memory, spec, and project knowledge.",
|
|
3908
3920
|
"Will migrate active REVIEW/VERIFICATION tasks to QUALITY, retire active read-only task types as CLOSED, and preserve their artifacts and history.",
|
|
3909
|
-
"Will migrate legacy workflow/
|
|
3921
|
+
"Will migrate legacy workflow/unit test task metadata; memory content, spec, and project knowledge files remain untouched."
|
|
3910
3922
|
].join("\n");
|
|
3911
3923
|
if (opts.dryRun) {
|
|
3912
3924
|
console.log(summary);
|
|
@@ -3924,7 +3936,9 @@ async function upgrade(opts) {
|
|
|
3924
3936
|
}
|
|
3925
3937
|
for (const { target, config: config2 } of pending) {
|
|
3926
3938
|
const previousManifest = await readInstallManifest(target.dir);
|
|
3927
|
-
const sessionCleanup = await cleanSessionRuntime(target.dir, {
|
|
3939
|
+
const sessionCleanup = await cleanSessionRuntime(target.dir, {
|
|
3940
|
+
preserveUnitTestSettings: true
|
|
3941
|
+
});
|
|
3928
3942
|
if (sessionCleanup.sessionsRemoved > 0 || sessionCleanup.acceptanceSnapshotsRemoved > 0) {
|
|
3929
3943
|
console.log(
|
|
3930
3944
|
chalk8.yellow(
|
|
@@ -3997,7 +4011,7 @@ async function resolvePendingUpgradeTargets(targets) {
|
|
|
3997
4011
|
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
3998
4012
|
);
|
|
3999
4013
|
}
|
|
4000
|
-
if (relation === -1 || relation === 0 && (installedVersion !== VERSION || await needsHookConfigRefresh(target, config2) || await hasLegacyWorkflowState(target.dir))) {
|
|
4014
|
+
if (relation === -1 || relation === 0 && (installedVersion !== VERSION || await needsHookConfigRefresh(target, config2) || await hasLegacyWorkflowState(target.dir) || (await runtimeToolUpdates(target.dir)).length > 0)) {
|
|
4001
4015
|
pending.push({ target, config: config2 });
|
|
4002
4016
|
}
|
|
4003
4017
|
}
|