easy-coding-harness 1.1.0-beta.0 → 1.1.0-beta.2
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 +21 -0
- package/README.md +23 -13
- package/dist/cli.js +559 -311
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- 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 +34 -43
- package/templates/common/skills/ec-config/SKILL.md +65 -46
- package/templates/common/skills/ec-implementing/SKILL.md +14 -5
- 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 +51 -14
- package/templates/common/skills/ec-task-management/SKILL.md +11 -4
- package/templates/common/skills/ec-tdd-init/SKILL.md +7 -7
- package/templates/common/skills/ec-workflow/SKILL.md +37 -17
- package/templates/main-constraint/AGENTS.md.tpl +37 -19
- package/templates/main-constraint/CLAUDE.md.tpl +37 -19
- package/templates/runtime/tools/easy_coding_java_coverage.py +5 -5
- package/templates/shared-hooks/easy_coding_inputs.py +19 -1
- package/templates/shared-hooks/easy_coding_state.py +536 -220
- package/templates/shared-hooks/inject-subagent-context.py +3 -3
package/dist/cli.js
CHANGED
|
@@ -63,6 +63,8 @@ function colorizeBanner(art) {
|
|
|
63
63
|
// src/utils/config-yaml.ts
|
|
64
64
|
import { randomUUID } from "crypto";
|
|
65
65
|
import { readFile as readFile2 } from "fs/promises";
|
|
66
|
+
import os from "os";
|
|
67
|
+
import path3 from "path";
|
|
66
68
|
import YAML, { isScalar, isSeq, parseDocument } from "yaml";
|
|
67
69
|
|
|
68
70
|
// src/utils/file-writer.ts
|
|
@@ -103,10 +105,75 @@ async function isDirectory(filePath) {
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
// src/utils/config-yaml.ts
|
|
106
|
-
var CONFIG_SCHEMA_VERSION =
|
|
107
|
-
var
|
|
108
|
+
var CONFIG_SCHEMA_VERSION = 6;
|
|
109
|
+
var DEFAULT_UT_COVERAGE_THRESHOLD = 90;
|
|
110
|
+
var UNIT_TEST_MODES = ["none", "ut", "tdd"];
|
|
108
111
|
var APPROVAL_MODES = ["approve", "guard", "confirm", "auto"];
|
|
112
|
+
var COOPERATE_MODES = ["default", "dispatch"];
|
|
109
113
|
var CONFIGURED_WORKFLOW_MODES = ["adaptive", "fast", "standard", "strict"];
|
|
114
|
+
var BEHAVIOR_DEFAULTS = {
|
|
115
|
+
approval_mode: "guard",
|
|
116
|
+
cooperate_mode: "default",
|
|
117
|
+
unit_test_mode: "none",
|
|
118
|
+
ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD
|
|
119
|
+
};
|
|
120
|
+
var BEHAVIOR_KEYS = Object.keys(BEHAVIOR_DEFAULTS);
|
|
121
|
+
function localConfigPath() {
|
|
122
|
+
return path3.join(os.homedir(), ".easy-coding", "config.yaml");
|
|
123
|
+
}
|
|
124
|
+
function validateBehaviorValue(key, value) {
|
|
125
|
+
const valid = key === "approval_mode" ? isApprovalMode(value) : key === "cooperate_mode" ? COOPERATE_MODES.includes(value) : key === "unit_test_mode" ? isUnitTestMode(value) : isUtCoverageThreshold(value);
|
|
126
|
+
if (!valid) throw new Error(`Invalid behavior.${key}: ${String(value)}`);
|
|
127
|
+
}
|
|
128
|
+
async function readLocalBehavior() {
|
|
129
|
+
let content;
|
|
130
|
+
try {
|
|
131
|
+
content = await readFile2(localConfigPath(), "utf8");
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error.code === "ENOENT") return {};
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const settings = YAML.parse(content)?.behavior ?? {};
|
|
137
|
+
for (const key of BEHAVIOR_KEYS) {
|
|
138
|
+
if (settings[key] !== void 0) validateBehaviorValue(key, settings[key]);
|
|
139
|
+
}
|
|
140
|
+
return Object.fromEntries(
|
|
141
|
+
BEHAVIOR_KEYS.filter((key) => settings[key] !== void 0).map((key) => [key, settings[key]])
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function resolveBehaviorSettings(project, local = {}, session = {}) {
|
|
145
|
+
const values = { ...BEHAVIOR_DEFAULTS };
|
|
146
|
+
const sources = {};
|
|
147
|
+
for (const key of BEHAVIOR_KEYS) {
|
|
148
|
+
const layer = session[key] !== void 0 ? "session" : local[key] !== void 0 ? "local" : project[key] !== void 0 ? "project" : "default";
|
|
149
|
+
const value = layer === "session" ? session[key] : layer === "local" ? local[key] : layer === "project" ? project[key] : BEHAVIOR_DEFAULTS[key];
|
|
150
|
+
validateBehaviorValue(key, value);
|
|
151
|
+
Object.assign(values, { [key]: value });
|
|
152
|
+
sources[key] = layer;
|
|
153
|
+
}
|
|
154
|
+
return { values, sources };
|
|
155
|
+
}
|
|
156
|
+
async function writeBehaviorOverrides(filePath, changes) {
|
|
157
|
+
let content = "";
|
|
158
|
+
try {
|
|
159
|
+
content = await readFile2(filePath, "utf8");
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error.code !== "ENOENT") throw error;
|
|
162
|
+
}
|
|
163
|
+
const document = parseDocument(content);
|
|
164
|
+
for (const key of BEHAVIOR_KEYS) {
|
|
165
|
+
const value = changes[key];
|
|
166
|
+
if (value === void 0) continue;
|
|
167
|
+
if (value === null) {
|
|
168
|
+
if (document.hasIn(["behavior", key])) document.deleteIn(["behavior", key]);
|
|
169
|
+
} else {
|
|
170
|
+
validateBehaviorValue(key, value);
|
|
171
|
+
document.setIn(["behavior", key], value);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!content && !document.has("behavior")) return;
|
|
175
|
+
await writeTextFile(filePath, document.toString());
|
|
176
|
+
}
|
|
110
177
|
function createDefaultConfig(params) {
|
|
111
178
|
const config2 = {
|
|
112
179
|
version: CONFIG_SCHEMA_VERSION,
|
|
@@ -127,8 +194,9 @@ function createDefaultConfig(params) {
|
|
|
127
194
|
behavior: {
|
|
128
195
|
approval_mode: "guard",
|
|
129
196
|
workflow_mode: "adaptive",
|
|
130
|
-
|
|
131
|
-
|
|
197
|
+
unit_test_mode: "none",
|
|
198
|
+
ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD,
|
|
199
|
+
cooperate_mode: "default"
|
|
132
200
|
}
|
|
133
201
|
};
|
|
134
202
|
if (params.supermodule) {
|
|
@@ -182,43 +250,66 @@ function isApprovalMode(value) {
|
|
|
182
250
|
function isConfiguredWorkflowMode(value) {
|
|
183
251
|
return typeof value === "string" && CONFIGURED_WORKFLOW_MODES.includes(value);
|
|
184
252
|
}
|
|
185
|
-
function
|
|
253
|
+
function isUtCoverageThreshold(value) {
|
|
186
254
|
return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 100;
|
|
187
255
|
}
|
|
256
|
+
function isUnitTestMode(value) {
|
|
257
|
+
return typeof value === "string" && UNIT_TEST_MODES.includes(value);
|
|
258
|
+
}
|
|
259
|
+
function migrateUnitTestSettings(record) {
|
|
260
|
+
let changed = false;
|
|
261
|
+
if ("tdd_enabled" in record) {
|
|
262
|
+
if (!("unit_test_mode" in record) && typeof record.tdd_enabled === "boolean") {
|
|
263
|
+
record.unit_test_mode = record.tdd_enabled ? "tdd" : "none";
|
|
264
|
+
}
|
|
265
|
+
Reflect.deleteProperty(record, "tdd_enabled");
|
|
266
|
+
changed = true;
|
|
267
|
+
}
|
|
268
|
+
if ("tdd_coverage_threshold" in record) {
|
|
269
|
+
record.ut_coverage_threshold ??= record.tdd_coverage_threshold;
|
|
270
|
+
Reflect.deleteProperty(record, "tdd_coverage_threshold");
|
|
271
|
+
changed = true;
|
|
272
|
+
}
|
|
273
|
+
return changed;
|
|
274
|
+
}
|
|
188
275
|
function resolveLegacyBehavior(config2) {
|
|
189
276
|
const behavior = config2.behavior ?? {};
|
|
190
277
|
const legacyLite = behavior.confirm_mode === "lite";
|
|
191
278
|
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
279
|
const workflowMode = isConfiguredWorkflowMode(behavior.workflow_mode) ? behavior.workflow_mode : legacyLite ? "fast" : "adaptive";
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
return { approvalMode, workflowMode,
|
|
280
|
+
const unitTestMode = isUnitTestMode(behavior.unit_test_mode) ? behavior.unit_test_mode : Number(config2.version) >= 4 && behavior.tdd_enabled === true ? "tdd" : "none";
|
|
281
|
+
const configuredThreshold = behavior.ut_coverage_threshold ?? (Number(config2.version) >= 4 ? behavior.tdd_coverage_threshold : void 0);
|
|
282
|
+
const utCoverageThreshold = isUtCoverageThreshold(configuredThreshold) ? configuredThreshold : DEFAULT_UT_COVERAGE_THRESHOLD;
|
|
283
|
+
return { approvalMode, workflowMode, unitTestMode, utCoverageThreshold };
|
|
197
284
|
}
|
|
198
|
-
async function setBehaviorModes(filePath, approvalMode, workflowMode,
|
|
199
|
-
if (
|
|
200
|
-
throw new Error("
|
|
285
|
+
async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold, cooperateMode) {
|
|
286
|
+
if (unitTestMode !== void 0 && !isUnitTestMode(unitTestMode)) {
|
|
287
|
+
throw new Error("Unit test mode must be none, ut, or tdd.");
|
|
288
|
+
}
|
|
289
|
+
if (utCoverageThreshold !== void 0 && !isUtCoverageThreshold(utCoverageThreshold)) {
|
|
290
|
+
throw new Error("Unit test coverage threshold must be an integer from 1 to 100.");
|
|
201
291
|
}
|
|
202
292
|
return updateConfigYaml(filePath, (config2) => {
|
|
203
293
|
const legacyBehavior = config2.behavior ?? {};
|
|
204
294
|
const resolvedBehavior = resolveLegacyBehavior(config2);
|
|
205
295
|
const behavior = Object.fromEntries(
|
|
206
296
|
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"
|
|
297
|
+
([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
298
|
)
|
|
209
299
|
);
|
|
210
300
|
behavior.approval_mode = approvalMode;
|
|
211
301
|
behavior.workflow_mode = workflowMode;
|
|
212
|
-
behavior.
|
|
213
|
-
behavior.
|
|
302
|
+
behavior.unit_test_mode = unitTestMode ?? resolvedBehavior.unitTestMode;
|
|
303
|
+
behavior.ut_coverage_threshold = utCoverageThreshold ?? resolvedBehavior.utCoverageThreshold;
|
|
304
|
+
behavior.cooperate_mode = cooperateMode ?? legacyBehavior.cooperate_mode ?? "default";
|
|
214
305
|
config2.behavior = behavior;
|
|
215
306
|
config2.version = CONFIG_SCHEMA_VERSION;
|
|
216
307
|
});
|
|
217
308
|
}
|
|
218
309
|
async function migrateBehaviorConfig(filePath) {
|
|
219
310
|
const config2 = await readConfigYaml(filePath);
|
|
220
|
-
const { approvalMode, workflowMode,
|
|
221
|
-
return setBehaviorModes(filePath, approvalMode, workflowMode,
|
|
311
|
+
const { approvalMode, workflowMode, unitTestMode, utCoverageThreshold } = resolveLegacyBehavior(config2);
|
|
312
|
+
return setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold);
|
|
222
313
|
}
|
|
223
314
|
async function ensureProjectId(filePath) {
|
|
224
315
|
let projectId = "";
|
|
@@ -243,7 +334,7 @@ function createProjectId() {
|
|
|
243
334
|
}
|
|
244
335
|
|
|
245
336
|
// src/utils/gitignore.ts
|
|
246
|
-
import
|
|
337
|
+
import path4 from "path";
|
|
247
338
|
|
|
248
339
|
// src/constants/paths.ts
|
|
249
340
|
var EASY_CODING_DIR = ".easy-coding";
|
|
@@ -267,7 +358,7 @@ var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness gene
|
|
|
267
358
|
|
|
268
359
|
// src/utils/gitignore.ts
|
|
269
360
|
async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Personal runtime state; do not commit") {
|
|
270
|
-
const gitignorePath =
|
|
361
|
+
const gitignorePath = path4.join(cwd, ".gitignore");
|
|
271
362
|
const current = await readTextIfExists(gitignorePath) ?? "";
|
|
272
363
|
const lines = current.split(/\r?\n/).map((line) => line.trim());
|
|
273
364
|
if (lines.includes(entry)) {
|
|
@@ -292,7 +383,7 @@ async function ensureHookBytecodeIgnored(cwd) {
|
|
|
292
383
|
// src/utils/install-manifest.ts
|
|
293
384
|
import { createHash } from "crypto";
|
|
294
385
|
import { readFile as readFile3, rmdir, unlink } from "fs/promises";
|
|
295
|
-
import
|
|
386
|
+
import path5 from "path";
|
|
296
387
|
|
|
297
388
|
// src/types/platform.ts
|
|
298
389
|
var pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
@@ -464,12 +555,12 @@ async function writeInstallManifest(cwd, params) {
|
|
|
464
555
|
constraint_regions: [...constraintRegions.values()].sort(byPath)
|
|
465
556
|
};
|
|
466
557
|
await writeTextFile(
|
|
467
|
-
|
|
558
|
+
path5.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
|
|
468
559
|
JSON.stringify(manifest, null, 2)
|
|
469
560
|
);
|
|
470
561
|
}
|
|
471
562
|
async function readInstallManifest(cwd) {
|
|
472
|
-
const manifestPath2 =
|
|
563
|
+
const manifestPath2 = path5.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
|
|
473
564
|
const content = await readTextIfExists(manifestPath2);
|
|
474
565
|
if (content === null) {
|
|
475
566
|
return null;
|
|
@@ -511,7 +602,7 @@ async function pruneRetiredManagedFiles(cwd, previous, artifacts) {
|
|
|
511
602
|
await unlink(filePath);
|
|
512
603
|
removed.push(file.path);
|
|
513
604
|
try {
|
|
514
|
-
await rmdir(
|
|
605
|
+
await rmdir(path5.dirname(filePath));
|
|
515
606
|
} catch (error) {
|
|
516
607
|
if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) {
|
|
517
608
|
throw error;
|
|
@@ -530,10 +621,10 @@ function manifestPath(cwd, projectPath) {
|
|
|
530
621
|
return resolveProjectPath(cwd, projectPath);
|
|
531
622
|
}
|
|
532
623
|
function toProjectPath(cwd, filePath) {
|
|
533
|
-
const root =
|
|
534
|
-
const resolved =
|
|
624
|
+
const root = path5.resolve(cwd);
|
|
625
|
+
const resolved = path5.resolve(filePath);
|
|
535
626
|
assertPathInsideProject(root, resolved, filePath);
|
|
536
|
-
return
|
|
627
|
+
return path5.relative(root, resolved).split(path5.sep).join("/");
|
|
537
628
|
}
|
|
538
629
|
function normalizeCommand(command) {
|
|
539
630
|
return command.replace(/\\/g, "/").trim().replace(/\s+/g, " ");
|
|
@@ -545,17 +636,17 @@ async function sha256File(filePath) {
|
|
|
545
636
|
function resolveProjectPath(cwd, projectPath) {
|
|
546
637
|
const normalized = projectPath.replace(/\\/g, "/");
|
|
547
638
|
const parts = normalized.split("/");
|
|
548
|
-
if (normalized.trim() === "" ||
|
|
639
|
+
if (normalized.trim() === "" || path5.isAbsolute(projectPath) || path5.posix.isAbsolute(normalized) || path5.win32.isAbsolute(projectPath) || /^[A-Za-z]:/.test(projectPath) || parts.some((part) => part === "..")) {
|
|
549
640
|
throw new Error(`Unsafe install manifest path: ${projectPath}`);
|
|
550
641
|
}
|
|
551
|
-
const root =
|
|
552
|
-
const resolved =
|
|
642
|
+
const root = path5.resolve(cwd);
|
|
643
|
+
const resolved = path5.resolve(root, normalized);
|
|
553
644
|
assertPathInsideProject(root, resolved, projectPath);
|
|
554
645
|
return resolved;
|
|
555
646
|
}
|
|
556
647
|
function assertPathInsideProject(root, resolvedPath, sourcePath) {
|
|
557
|
-
const relative =
|
|
558
|
-
if (!relative || relative.startsWith("..") ||
|
|
648
|
+
const relative = path5.relative(root, resolvedPath);
|
|
649
|
+
if (!relative || relative.startsWith("..") || path5.isAbsolute(relative)) {
|
|
559
650
|
throw new Error(`Unsafe install manifest path: ${sourcePath}`);
|
|
560
651
|
}
|
|
561
652
|
}
|
|
@@ -654,7 +745,7 @@ function isHookPythonPath(candidate) {
|
|
|
654
745
|
}
|
|
655
746
|
function normalizeHookPath(cwd, hookPath) {
|
|
656
747
|
const normalized = hookPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
657
|
-
if (
|
|
748
|
+
if (path5.isAbsolute(hookPath) || path5.posix.isAbsolute(normalized) || path5.win32.isAbsolute(hookPath)) {
|
|
658
749
|
return toProjectPath(cwd, hookPath);
|
|
659
750
|
}
|
|
660
751
|
return normalized;
|
|
@@ -664,97 +755,11 @@ function byPath(a, b) {
|
|
|
664
755
|
}
|
|
665
756
|
|
|
666
757
|
// 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
|
-
}
|
|
758
|
+
import path8 from "path";
|
|
754
759
|
|
|
755
760
|
// src/utils/task-json.ts
|
|
756
761
|
import { readdir } from "fs/promises";
|
|
757
|
-
import
|
|
762
|
+
import path6 from "path";
|
|
758
763
|
var LEGACY_STAGE_MAP = {
|
|
759
764
|
WAITING_CONFIRM: "ANALYSIS",
|
|
760
765
|
MEMORY_SHORT: "MEMORY",
|
|
@@ -781,7 +786,7 @@ function createProjectInitTask(params) {
|
|
|
781
786
|
};
|
|
782
787
|
}
|
|
783
788
|
function getTaskJsonPath(cwd, taskId) {
|
|
784
|
-
return
|
|
789
|
+
return path6.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
|
|
785
790
|
}
|
|
786
791
|
async function writeTaskJson(filePath, task) {
|
|
787
792
|
await writeTextFile(filePath, JSON.stringify(task, null, 2));
|
|
@@ -1006,9 +1011,10 @@ function migrateTaskWorkflowState(task) {
|
|
|
1006
1011
|
task.workflow_mode_legacy = true;
|
|
1007
1012
|
changed = true;
|
|
1008
1013
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
task.
|
|
1014
|
+
changed = migrateUnitTestSettings(task) || changed;
|
|
1015
|
+
if (isActive && taskType !== "project-init" && task.unit_test_mode === void 0) {
|
|
1016
|
+
task.unit_test_mode = "none";
|
|
1017
|
+
task.ut_coverage_threshold ??= DEFAULT_UT_COVERAGE_THRESHOLD;
|
|
1012
1018
|
task.tdd_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1013
1019
|
task.tdd_confirmed_by = "upgrade-migration";
|
|
1014
1020
|
changed = true;
|
|
@@ -1017,6 +1023,7 @@ function migrateTaskWorkflowState(task) {
|
|
|
1017
1023
|
}
|
|
1018
1024
|
function migrateSessionBehavior(session) {
|
|
1019
1025
|
let changed = migrateAgentFields(session, ["agent", "last_agent"]);
|
|
1026
|
+
changed = migrateUnitTestSettings(session) || changed;
|
|
1020
1027
|
const legacyMode = session.confirm_mode;
|
|
1021
1028
|
const legacyLite = legacyMode === "lite";
|
|
1022
1029
|
if (!["approve", "guard", "confirm", "auto"].includes(String(session.approval_mode ?? ""))) {
|
|
@@ -1092,16 +1099,16 @@ function migrationWorkflowMode(task, candidates, projectWorkflowMode) {
|
|
|
1092
1099
|
);
|
|
1093
1100
|
}
|
|
1094
1101
|
async function taskFiles(cwd) {
|
|
1095
|
-
const tasksDir =
|
|
1102
|
+
const tasksDir = path6.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
1096
1103
|
if (!await pathExists(tasksDir)) return [];
|
|
1097
1104
|
const entries = await readdir(tasksDir, { withFileTypes: true });
|
|
1098
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) =>
|
|
1105
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => path6.join(tasksDir, entry.name, "task.json"));
|
|
1099
1106
|
}
|
|
1100
1107
|
async function sessionFiles(cwd) {
|
|
1101
|
-
const sessionsDir =
|
|
1108
|
+
const sessionsDir = path6.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
1102
1109
|
if (!await pathExists(sessionsDir)) return [];
|
|
1103
1110
|
const entries = await readdir(sessionsDir, { withFileTypes: true });
|
|
1104
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) =>
|
|
1111
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path6.join(sessionsDir, entry.name));
|
|
1105
1112
|
}
|
|
1106
1113
|
async function readJsonRecord(filePath) {
|
|
1107
1114
|
try {
|
|
@@ -1116,7 +1123,7 @@ async function hasLegacyWorkflowState(cwd) {
|
|
|
1116
1123
|
if (!await pathExists(filePath)) continue;
|
|
1117
1124
|
const task = await readJsonRecord(filePath);
|
|
1118
1125
|
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(
|
|
1126
|
+
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
1127
|
(entry) => entry && typeof entry === "object" && !Array.isArray(entry) && isLegacyStage(entry.stage)
|
|
1121
1128
|
)) {
|
|
1122
1129
|
return true;
|
|
@@ -1128,7 +1135,7 @@ async function hasLegacyWorkflowState(cwd) {
|
|
|
1128
1135
|
if (["agent", "last_agent"].some((field) => {
|
|
1129
1136
|
const migrated = migratedAgentIdentity(session[field]);
|
|
1130
1137
|
return migrated !== void 0 && migrated !== session[field];
|
|
1131
|
-
}) || isLegacyStage(session.last_seen_stage) || "confirm_mode" in session) {
|
|
1138
|
+
}) || isLegacyStage(session.last_seen_stage) || "confirm_mode" in session || "tdd_enabled" in session || "tdd_coverage_threshold" in session) {
|
|
1132
1139
|
return true;
|
|
1133
1140
|
}
|
|
1134
1141
|
}
|
|
@@ -1139,7 +1146,7 @@ async function migrateLegacyWorkflowState(cwd) {
|
|
|
1139
1146
|
let sessionsUpdated = 0;
|
|
1140
1147
|
const updatedTaskPaths = /* @__PURE__ */ new Set();
|
|
1141
1148
|
let projectWorkflowMode = "adaptive";
|
|
1142
|
-
const configPath2 =
|
|
1149
|
+
const configPath2 = path6.join(cwd, EASY_CODING_DIR, "config.yaml");
|
|
1143
1150
|
if (await pathExists(configPath2)) {
|
|
1144
1151
|
try {
|
|
1145
1152
|
const config2 = await readConfigYaml(configPath2);
|
|
@@ -1194,7 +1201,7 @@ async function migrateLegacyWorkflowState(cwd) {
|
|
|
1194
1201
|
if (!task) continue;
|
|
1195
1202
|
const migrationOwned = task.workflow_mode_legacy === true && task.workflow_mode_confirmed_by === "upgrade-migration";
|
|
1196
1203
|
if (!migrationOwned) continue;
|
|
1197
|
-
const taskId =
|
|
1204
|
+
const taskId = path6.basename(path6.dirname(filePath));
|
|
1198
1205
|
const mode = migrationWorkflowMode(
|
|
1199
1206
|
task,
|
|
1200
1207
|
candidatesByTask.get(taskId) ?? [],
|
|
@@ -1229,7 +1236,7 @@ async function setPendingInitSince(cwd, version) {
|
|
|
1229
1236
|
await writeTaskJson(filePath, task);
|
|
1230
1237
|
}
|
|
1231
1238
|
async function listTasks(cwd) {
|
|
1232
|
-
const tasksDir =
|
|
1239
|
+
const tasksDir = path6.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
1233
1240
|
if (!await pathExists(tasksDir)) {
|
|
1234
1241
|
return [];
|
|
1235
1242
|
}
|
|
@@ -1260,15 +1267,114 @@ function isActiveTask(task) {
|
|
|
1260
1267
|
return task.status !== "COMPLETE" && task.status !== "CLOSED";
|
|
1261
1268
|
}
|
|
1262
1269
|
|
|
1270
|
+
// src/utils/template-paths.ts
|
|
1271
|
+
import { existsSync } from "fs";
|
|
1272
|
+
import path7 from "path";
|
|
1273
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1274
|
+
function getTemplateRoot() {
|
|
1275
|
+
const here = path7.dirname(fileURLToPath2(import.meta.url));
|
|
1276
|
+
const candidates = [
|
|
1277
|
+
path7.resolve(here, "../templates"),
|
|
1278
|
+
path7.resolve(here, "../../templates"),
|
|
1279
|
+
path7.resolve(process.cwd(), "src/templates"),
|
|
1280
|
+
path7.resolve(process.cwd(), "templates")
|
|
1281
|
+
];
|
|
1282
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
1283
|
+
if (!found) {
|
|
1284
|
+
throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
|
|
1285
|
+
}
|
|
1286
|
+
return found;
|
|
1287
|
+
}
|
|
1288
|
+
function getTemplatePath(...segments) {
|
|
1289
|
+
return path7.join(getTemplateRoot(), ...segments);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/utils/runtime-scaffold.ts
|
|
1293
|
+
async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
1294
|
+
const easyCodingDir = path8.join(cwd, EASY_CODING_DIR);
|
|
1295
|
+
await ensureDir(easyCodingDir);
|
|
1296
|
+
const configPath2 = path8.join(easyCodingDir, CONFIG_FILE);
|
|
1297
|
+
let projectId = opts.projectId ?? createProjectId();
|
|
1298
|
+
if (!await pathExists(configPath2)) {
|
|
1299
|
+
const projectName = path8.basename(cwd);
|
|
1300
|
+
await writeConfigYaml(
|
|
1301
|
+
configPath2,
|
|
1302
|
+
createDefaultConfig({
|
|
1303
|
+
projectName,
|
|
1304
|
+
projectId,
|
|
1305
|
+
harnessVersion: VERSION,
|
|
1306
|
+
agents,
|
|
1307
|
+
supermodule: opts.supermodule
|
|
1308
|
+
})
|
|
1309
|
+
);
|
|
1310
|
+
} else {
|
|
1311
|
+
projectId = await ensureProjectId(configPath2);
|
|
1312
|
+
}
|
|
1313
|
+
await ensureDir(path8.join(easyCodingDir, "tasks"));
|
|
1314
|
+
await ensureDir(path8.join(easyCodingDir, SESSIONS_DIR));
|
|
1315
|
+
await ensureDir(path8.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
1316
|
+
await ensureDir(path8.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
1317
|
+
await writeMemoryScaffold(easyCodingDir);
|
|
1318
|
+
await writeTemplatesScaffold(easyCodingDir);
|
|
1319
|
+
await writeToolsScaffold(cwd);
|
|
1320
|
+
return projectId;
|
|
1321
|
+
}
|
|
1322
|
+
async function runtimeToolUpdates(cwd) {
|
|
1323
|
+
const frozen = (await listTasks(cwd)).some(
|
|
1324
|
+
({ task }) => isActiveTask(task) && (task.unit_test_mode === "ut" || task.unit_test_mode === "tdd" || task.tdd_enabled === true)
|
|
1325
|
+
);
|
|
1326
|
+
const updates = [];
|
|
1327
|
+
for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
|
|
1328
|
+
const target = path8.join(cwd, EASY_CODING_DIR, TOOLS_DIR, file);
|
|
1329
|
+
const current = await readTextIfExists(target);
|
|
1330
|
+
if (frozen && current !== null) continue;
|
|
1331
|
+
const content = await readTextFile(getTemplatePath("runtime", "tools", file));
|
|
1332
|
+
if (content !== current) updates.push({ path: target, content });
|
|
1333
|
+
}
|
|
1334
|
+
return updates;
|
|
1335
|
+
}
|
|
1336
|
+
async function writeToolsScaffold(cwd) {
|
|
1337
|
+
const toolsDir = path8.join(cwd, EASY_CODING_DIR, TOOLS_DIR);
|
|
1338
|
+
await ensureDir(toolsDir);
|
|
1339
|
+
for (const update2 of await runtimeToolUpdates(cwd)) {
|
|
1340
|
+
await writeTextFile(update2.path, update2.content);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
async function writeTemplatesScaffold(easyCodingDir) {
|
|
1344
|
+
const templatesDir = path8.join(easyCodingDir, TEMPLATES_DIR);
|
|
1345
|
+
await ensureDir(templatesDir);
|
|
1346
|
+
const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
|
|
1347
|
+
const dest = path8.join(templatesDir, "dev-spec-skeleton.md");
|
|
1348
|
+
await writeTextFile(dest, await readTextFile(src));
|
|
1349
|
+
}
|
|
1350
|
+
async function writeMemoryScaffold(easyCodingDir) {
|
|
1351
|
+
const memoryDir = path8.join(easyCodingDir, MEMORY_DIR);
|
|
1352
|
+
await ensureDir(path8.join(memoryDir, "short"));
|
|
1353
|
+
await ensureDir(path8.join(memoryDir, "long"));
|
|
1354
|
+
for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
|
|
1355
|
+
const destination = path8.join(memoryDir, "long", file);
|
|
1356
|
+
if (await pathExists(destination)) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
1360
|
+
await writeTextFile(destination, await readTextFile(templatePath));
|
|
1361
|
+
}
|
|
1362
|
+
const shortTemplateDest = path8.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
|
|
1363
|
+
if (!await pathExists(shortTemplateDest)) {
|
|
1364
|
+
const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
|
|
1365
|
+
await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1263
1369
|
// src/commands/install-harness.ts
|
|
1264
|
-
import
|
|
1370
|
+
import path14 from "path";
|
|
1265
1371
|
|
|
1266
1372
|
// src/configurators/claude.ts
|
|
1267
|
-
import
|
|
1373
|
+
import path10 from "path";
|
|
1268
1374
|
|
|
1269
1375
|
// src/configurators/shared.ts
|
|
1270
1376
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
1271
|
-
import
|
|
1377
|
+
import path9 from "path";
|
|
1272
1378
|
|
|
1273
1379
|
// src/utils/marked-region.ts
|
|
1274
1380
|
var MarkedRegionError = class extends Error {
|
|
@@ -1327,7 +1433,7 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
|
|
|
1327
1433
|
return resolved;
|
|
1328
1434
|
}
|
|
1329
1435
|
async function withProjectInstallPaths(cwd, ctx, projectId) {
|
|
1330
|
-
const resolvedProjectId = projectId ?? await readProjectIdIfExists(
|
|
1436
|
+
const resolvedProjectId = projectId ?? await readProjectIdIfExists(path9.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
1331
1437
|
return withInstallPaths(cwd, ctx, resolvedProjectId ?? void 0);
|
|
1332
1438
|
}
|
|
1333
1439
|
function withInstallPaths(cwd, ctx, projectId) {
|
|
@@ -1415,11 +1521,11 @@ async function resolveSkills(ctx) {
|
|
|
1415
1521
|
const entries = await readdir2(skillsRoot);
|
|
1416
1522
|
const skills = [];
|
|
1417
1523
|
for (const entry of entries.sort()) {
|
|
1418
|
-
const skillDir =
|
|
1524
|
+
const skillDir = path9.join(skillsRoot, entry);
|
|
1419
1525
|
if (!await isDirectory(skillDir)) {
|
|
1420
1526
|
continue;
|
|
1421
1527
|
}
|
|
1422
|
-
const content = await readTextFile(
|
|
1528
|
+
const content = await readTextFile(path9.join(skillDir, "SKILL.md"));
|
|
1423
1529
|
skills.push({
|
|
1424
1530
|
name: entry,
|
|
1425
1531
|
content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
|
|
@@ -1435,7 +1541,7 @@ async function resolveBundledSkills(ctx) {
|
|
|
1435
1541
|
const entries = await readdir2(bundledRoot);
|
|
1436
1542
|
const bundled = [];
|
|
1437
1543
|
for (const entry of entries.sort()) {
|
|
1438
|
-
const sourceDir =
|
|
1544
|
+
const sourceDir = path9.join(bundledRoot, entry);
|
|
1439
1545
|
if (await isDirectory(sourceDir)) {
|
|
1440
1546
|
bundled.push({ name: entry, sourceDir, context: ctx });
|
|
1441
1547
|
}
|
|
@@ -1446,13 +1552,13 @@ async function writeSkills(dir, skills, bundled) {
|
|
|
1446
1552
|
await ensureDir(dir);
|
|
1447
1553
|
const written = [];
|
|
1448
1554
|
for (const skill of skills) {
|
|
1449
|
-
const destination =
|
|
1555
|
+
const destination = path9.join(dir, skill.name, "SKILL.md");
|
|
1450
1556
|
await writeTextFile(destination, skill.content);
|
|
1451
1557
|
written.push(destination);
|
|
1452
1558
|
}
|
|
1453
1559
|
for (const skill of bundled) {
|
|
1454
1560
|
written.push(
|
|
1455
|
-
...await copyTemplateDirectory(skill.sourceDir,
|
|
1561
|
+
...await copyTemplateDirectory(skill.sourceDir, path9.join(dir, skill.name), skill.context)
|
|
1456
1562
|
);
|
|
1457
1563
|
}
|
|
1458
1564
|
return written;
|
|
@@ -1467,7 +1573,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
|
|
|
1467
1573
|
if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
|
|
1468
1574
|
continue;
|
|
1469
1575
|
}
|
|
1470
|
-
const sourcePath =
|
|
1576
|
+
const sourcePath = path9.join(hooksRoot, entry);
|
|
1471
1577
|
if ((await stat2(sourcePath)).isDirectory()) {
|
|
1472
1578
|
continue;
|
|
1473
1579
|
}
|
|
@@ -1476,7 +1582,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
|
|
|
1476
1582
|
ctx,
|
|
1477
1583
|
`shared-hooks/${entry}`
|
|
1478
1584
|
);
|
|
1479
|
-
const destination =
|
|
1585
|
+
const destination = path9.join(dir, entry);
|
|
1480
1586
|
await writeTextFile(destination, content);
|
|
1481
1587
|
await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
|
|
1482
1588
|
written.push(destination);
|
|
@@ -1499,7 +1605,7 @@ async function writeMainConstraint(cwd, platform, opts = {}) {
|
|
|
1499
1605
|
if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
|
|
1500
1606
|
throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
|
|
1501
1607
|
}
|
|
1502
|
-
const destination =
|
|
1608
|
+
const destination = path9.join(cwd, meta.mainConstraint);
|
|
1503
1609
|
const current = await readTextIfExists(destination);
|
|
1504
1610
|
if (current) {
|
|
1505
1611
|
const markerContent = extractMarkedRegion(generated);
|
|
@@ -1541,8 +1647,8 @@ async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__
|
|
|
1541
1647
|
if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
|
|
1542
1648
|
continue;
|
|
1543
1649
|
}
|
|
1544
|
-
const sourcePath =
|
|
1545
|
-
const destinationPath =
|
|
1650
|
+
const sourcePath = path9.join(source, entry);
|
|
1651
|
+
const destinationPath = path9.join(destination, stripTemplateExtension(entry));
|
|
1546
1652
|
const sourceStat = await stat2(sourcePath);
|
|
1547
1653
|
if (sourceStat.isDirectory()) {
|
|
1548
1654
|
written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
|
|
@@ -1566,28 +1672,28 @@ async function configureClaude(cwd, opts = {}) {
|
|
|
1566
1672
|
const platform = "claude-code";
|
|
1567
1673
|
const meta = PLATFORM_META[platform];
|
|
1568
1674
|
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
1569
|
-
const dest =
|
|
1570
|
-
const hookConfigPath =
|
|
1675
|
+
const dest = path10.join(cwd, ".claude");
|
|
1676
|
+
const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
|
|
1571
1677
|
const artifacts = [];
|
|
1572
1678
|
const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
|
|
1573
1679
|
artifacts.push(
|
|
1574
1680
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
1575
1681
|
(filePath) => fileArtifact(
|
|
1576
1682
|
filePath,
|
|
1577
|
-
filePath.startsWith(
|
|
1683
|
+
filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
1578
1684
|
platform
|
|
1579
1685
|
)
|
|
1580
1686
|
)
|
|
1581
1687
|
);
|
|
1582
1688
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
1583
1689
|
artifacts.push(
|
|
1584
|
-
...(await writeSharedHooks(
|
|
1690
|
+
...(await writeSharedHooks(path10.join(dest, "hooks"), platform)).map(
|
|
1585
1691
|
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
1586
1692
|
)
|
|
1587
1693
|
);
|
|
1588
1694
|
artifacts.push(
|
|
1589
1695
|
...(await writeSkills(
|
|
1590
|
-
|
|
1696
|
+
path10.join(cwd, meta.skillsDir),
|
|
1591
1697
|
await resolveSkills(ctx),
|
|
1592
1698
|
await resolveBundledSkills(ctx)
|
|
1593
1699
|
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
@@ -1599,28 +1705,28 @@ async function configureClaude(cwd, opts = {}) {
|
|
|
1599
1705
|
}
|
|
1600
1706
|
|
|
1601
1707
|
// src/configurators/codex.ts
|
|
1602
|
-
import
|
|
1708
|
+
import path11 from "path";
|
|
1603
1709
|
async function configureCodex(cwd, opts = {}) {
|
|
1604
1710
|
const platform = "codex";
|
|
1605
1711
|
const meta = PLATFORM_META[platform];
|
|
1606
1712
|
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
1607
|
-
const hookConfigPath =
|
|
1713
|
+
const hookConfigPath = path11.join(cwd, meta.hookConfigFile);
|
|
1608
1714
|
const artifacts = [];
|
|
1609
1715
|
artifacts.push(
|
|
1610
1716
|
...(await writeSkills(
|
|
1611
|
-
|
|
1717
|
+
path11.join(cwd, meta.skillsDir),
|
|
1612
1718
|
await resolveSkills(ctx),
|
|
1613
1719
|
await resolveBundledSkills(ctx)
|
|
1614
1720
|
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
1615
1721
|
);
|
|
1616
1722
|
artifacts.push(
|
|
1617
|
-
...(await writeSharedHooks(
|
|
1723
|
+
...(await writeSharedHooks(path11.join(cwd, meta.hooksDir), platform, {
|
|
1618
1724
|
skipSubagentContext: true
|
|
1619
1725
|
})).map((filePath) => fileArtifact(filePath, "hook", platform))
|
|
1620
1726
|
);
|
|
1621
1727
|
const platformFiles = await copyPlatformTemplates(
|
|
1622
1728
|
"codex",
|
|
1623
|
-
|
|
1729
|
+
path11.join(cwd, ".codex"),
|
|
1624
1730
|
["hooks"],
|
|
1625
1731
|
ctx
|
|
1626
1732
|
);
|
|
@@ -1628,7 +1734,7 @@ async function configureCodex(cwd, opts = {}) {
|
|
|
1628
1734
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
1629
1735
|
(filePath) => fileArtifact(
|
|
1630
1736
|
filePath,
|
|
1631
|
-
filePath.startsWith(
|
|
1737
|
+
filePath.startsWith(path11.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
1632
1738
|
platform
|
|
1633
1739
|
)
|
|
1634
1740
|
)
|
|
@@ -1642,16 +1748,16 @@ async function configureCodex(cwd, opts = {}) {
|
|
|
1642
1748
|
|
|
1643
1749
|
// src/configurators/qoder.ts
|
|
1644
1750
|
import { readdir as readdir3 } from "fs/promises";
|
|
1645
|
-
import
|
|
1751
|
+
import path13 from "path";
|
|
1646
1752
|
|
|
1647
1753
|
// src/utils/platform-paths.ts
|
|
1648
1754
|
import { existsSync as existsSync2 } from "fs";
|
|
1649
|
-
import
|
|
1755
|
+
import path12 from "path";
|
|
1650
1756
|
function detectQoderCnVariant(cwd) {
|
|
1651
1757
|
if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
|
|
1652
1758
|
return true;
|
|
1653
1759
|
}
|
|
1654
|
-
return existsSync2(
|
|
1760
|
+
return existsSync2(path12.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
|
|
1655
1761
|
}
|
|
1656
1762
|
function resolvePlatformMeta(cwd, platform) {
|
|
1657
1763
|
const meta = PLATFORM_META[platform];
|
|
@@ -1682,7 +1788,7 @@ function resolveQoderMetaForBaseDir(baseDir) {
|
|
|
1682
1788
|
|
|
1683
1789
|
// src/configurators/qoder.ts
|
|
1684
1790
|
async function claudeHarnessSkillsExist(cwd) {
|
|
1685
|
-
return pathExists(
|
|
1791
|
+
return pathExists(path13.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
|
|
1686
1792
|
}
|
|
1687
1793
|
async function listFilesRecursive(dir) {
|
|
1688
1794
|
let entries;
|
|
@@ -1696,7 +1802,7 @@ async function listFilesRecursive(dir) {
|
|
|
1696
1802
|
}
|
|
1697
1803
|
const files = [];
|
|
1698
1804
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1699
|
-
const entryPath =
|
|
1805
|
+
const entryPath = path13.join(dir, entry.name);
|
|
1700
1806
|
if (entry.isDirectory()) {
|
|
1701
1807
|
files.push(...await listFilesRecursive(entryPath));
|
|
1702
1808
|
continue;
|
|
@@ -1710,7 +1816,7 @@ async function listFilesRecursive(dir) {
|
|
|
1710
1816
|
async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
|
|
1711
1817
|
const artifacts = [];
|
|
1712
1818
|
for (const skillName of skillNames) {
|
|
1713
|
-
for (const filePath of await listFilesRecursive(
|
|
1819
|
+
for (const filePath of await listFilesRecursive(path13.join(skillsDir, skillName))) {
|
|
1714
1820
|
artifacts.push(fileArtifact(filePath, "skill", platform));
|
|
1715
1821
|
}
|
|
1716
1822
|
}
|
|
@@ -1720,22 +1826,22 @@ async function configureQoder(cwd, opts = {}) {
|
|
|
1720
1826
|
const platform = "qoder";
|
|
1721
1827
|
const meta = resolvePlatformMeta(cwd, platform);
|
|
1722
1828
|
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
1723
|
-
const dest =
|
|
1724
|
-
const hookConfigPath =
|
|
1829
|
+
const dest = path13.join(cwd, ctx.platform_config_dir);
|
|
1830
|
+
const hookConfigPath = path13.join(cwd, meta.hookConfigFile);
|
|
1725
1831
|
const artifacts = [];
|
|
1726
1832
|
const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
|
|
1727
1833
|
artifacts.push(
|
|
1728
1834
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
1729
1835
|
(filePath) => fileArtifact(
|
|
1730
1836
|
filePath,
|
|
1731
|
-
filePath.startsWith(
|
|
1837
|
+
filePath.startsWith(path13.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
1732
1838
|
platform
|
|
1733
1839
|
)
|
|
1734
1840
|
)
|
|
1735
1841
|
);
|
|
1736
1842
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
1737
1843
|
artifacts.push(
|
|
1738
|
-
...(await writeSharedHooks(
|
|
1844
|
+
...(await writeSharedHooks(path13.join(dest, "hooks"), platform)).map(
|
|
1739
1845
|
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
1740
1846
|
)
|
|
1741
1847
|
);
|
|
@@ -1743,14 +1849,14 @@ async function configureQoder(cwd, opts = {}) {
|
|
|
1743
1849
|
const bundledSkills = await resolveBundledSkills(ctx);
|
|
1744
1850
|
if (!await claudeHarnessSkillsExist(cwd)) {
|
|
1745
1851
|
artifacts.push(
|
|
1746
|
-
...(await writeSkills(
|
|
1852
|
+
...(await writeSkills(path13.join(dest, "skills"), skills, bundledSkills)).map(
|
|
1747
1853
|
(filePath) => fileArtifact(filePath, "skill", platform)
|
|
1748
1854
|
)
|
|
1749
1855
|
);
|
|
1750
1856
|
} else {
|
|
1751
1857
|
artifacts.push(
|
|
1752
1858
|
...await existingManagedSkillArtifacts(
|
|
1753
|
-
|
|
1859
|
+
path13.join(dest, "skills"),
|
|
1754
1860
|
[...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
|
|
1755
1861
|
platform
|
|
1756
1862
|
)
|
|
@@ -1821,7 +1927,7 @@ async function refreshSupermoduleParent(targetDir, platforms, submodulePaths) {
|
|
|
1821
1927
|
role: "super-parent",
|
|
1822
1928
|
submodules: submodulePaths
|
|
1823
1929
|
};
|
|
1824
|
-
await updateSupermoduleConfig(
|
|
1930
|
+
await updateSupermoduleConfig(path14.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
|
|
1825
1931
|
for (const platform of platforms) {
|
|
1826
1932
|
await writeMainConstraint(targetDir, platform, {
|
|
1827
1933
|
supermodule: { submodulePaths }
|
|
@@ -1943,14 +2049,14 @@ async function resolveSubmodules(opts, available, defaultSelection = available)
|
|
|
1943
2049
|
}
|
|
1944
2050
|
|
|
1945
2051
|
// src/commands/supermodule-targets.ts
|
|
1946
|
-
import
|
|
2052
|
+
import path16 from "path";
|
|
1947
2053
|
import { cancel as cancel2, multiselect as multiselect2 } from "@clack/prompts";
|
|
1948
2054
|
|
|
1949
2055
|
// src/utils/gitmodules.ts
|
|
1950
2056
|
import { lstat, realpath } from "fs/promises";
|
|
1951
|
-
import
|
|
2057
|
+
import path15 from "path";
|
|
1952
2058
|
async function parseGitmodules(rootDir) {
|
|
1953
|
-
const content = await readTextIfExists(
|
|
2059
|
+
const content = await readTextIfExists(path15.join(rootDir, ".gitmodules"));
|
|
1954
2060
|
if (content === null) {
|
|
1955
2061
|
return [];
|
|
1956
2062
|
}
|
|
@@ -2009,7 +2115,7 @@ async function listInstallableSubmodules(rootDir) {
|
|
|
2009
2115
|
function normalizeSubmodulePath(value) {
|
|
2010
2116
|
const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
|
|
2011
2117
|
const parts = normalized.split("/").filter(Boolean);
|
|
2012
|
-
if (normalized === "" ||
|
|
2118
|
+
if (normalized === "" || path15.posix.isAbsolute(normalized) || path15.win32.isAbsolute(value) || parts.some((part) => part === "..")) {
|
|
2013
2119
|
throw new Error(`Unsafe submodule path in .gitmodules: ${value}`);
|
|
2014
2120
|
}
|
|
2015
2121
|
return parts.join("/");
|
|
@@ -2047,7 +2153,7 @@ function isCommentStart(value, index) {
|
|
|
2047
2153
|
return index === 0 || /\s/.test(value[index - 1]);
|
|
2048
2154
|
}
|
|
2049
2155
|
async function isSubmoduleWorktree(rootDir, submodulePath) {
|
|
2050
|
-
const dir =
|
|
2156
|
+
const dir = path15.join(rootDir, submodulePath);
|
|
2051
2157
|
try {
|
|
2052
2158
|
if (!await pathHasNoSymlinkSegments(rootDir, submodulePath)) {
|
|
2053
2159
|
return false;
|
|
@@ -2061,7 +2167,7 @@ async function isSubmoduleWorktree(rootDir, submodulePath) {
|
|
|
2061
2167
|
if (!dirStat.isDirectory()) {
|
|
2062
2168
|
return false;
|
|
2063
2169
|
}
|
|
2064
|
-
const gitMarkerStat = await lstat(
|
|
2170
|
+
const gitMarkerStat = await lstat(path15.join(dir, ".git"));
|
|
2065
2171
|
return !gitMarkerStat.isSymbolicLink() && (gitMarkerStat.isFile() || gitMarkerStat.isDirectory());
|
|
2066
2172
|
} catch (error) {
|
|
2067
2173
|
if (error.code === "ENOENT") {
|
|
@@ -2073,7 +2179,7 @@ async function isSubmoduleWorktree(rootDir, submodulePath) {
|
|
|
2073
2179
|
async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
|
|
2074
2180
|
let current = rootDir;
|
|
2075
2181
|
for (const part of submodulePath.split("/")) {
|
|
2076
|
-
current =
|
|
2182
|
+
current = path15.join(current, part);
|
|
2077
2183
|
const partStat = await lstat(current);
|
|
2078
2184
|
if (partStat.isSymbolicLink()) {
|
|
2079
2185
|
return false;
|
|
@@ -2082,8 +2188,8 @@ async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
|
|
|
2082
2188
|
return true;
|
|
2083
2189
|
}
|
|
2084
2190
|
function isInsideDirectory(parent, child) {
|
|
2085
|
-
const relative =
|
|
2086
|
-
return Boolean(relative) && !relative.startsWith("..") && !
|
|
2191
|
+
const relative = path15.relative(parent, child);
|
|
2192
|
+
return Boolean(relative) && !relative.startsWith("..") && !path15.isAbsolute(relative);
|
|
2087
2193
|
}
|
|
2088
2194
|
|
|
2089
2195
|
// src/commands/supermodule-targets.ts
|
|
@@ -2135,7 +2241,7 @@ async function resolveClearTargets(cwd, opts) {
|
|
|
2135
2241
|
return [standaloneTarget(cwd)];
|
|
2136
2242
|
}
|
|
2137
2243
|
const installedSubmodules = await listInstalledSubmodules(cwd);
|
|
2138
|
-
const parent = await pathExists(
|
|
2244
|
+
const parent = await pathExists(path16.join(cwd, EASY_CODING_DIR)) ? parentTarget(cwd, installedSubmodules) : null;
|
|
2139
2245
|
const children = installedSubmodules.map((entry) => childTarget(cwd, entry));
|
|
2140
2246
|
if (opts.submodules === false) {
|
|
2141
2247
|
return parent ? [parent] : [];
|
|
@@ -2175,7 +2281,7 @@ async function listInstalledSubmodules(cwd) {
|
|
|
2175
2281
|
const entries = await listInstallableSubmodules(cwd);
|
|
2176
2282
|
const installed = [];
|
|
2177
2283
|
for (const entry of entries) {
|
|
2178
|
-
if (await pathExists(configPath(
|
|
2284
|
+
if (await pathExists(configPath(path16.join(cwd, entry.path)))) {
|
|
2179
2285
|
installed.push(entry);
|
|
2180
2286
|
}
|
|
2181
2287
|
}
|
|
@@ -2218,7 +2324,7 @@ function parentTargetFromPaths(cwd, submodulePaths) {
|
|
|
2218
2324
|
};
|
|
2219
2325
|
}
|
|
2220
2326
|
function childTarget(cwd, entry) {
|
|
2221
|
-
const dir =
|
|
2327
|
+
const dir = path16.join(cwd, entry.path);
|
|
2222
2328
|
return {
|
|
2223
2329
|
dir,
|
|
2224
2330
|
label: entry.path,
|
|
@@ -2254,11 +2360,11 @@ function parseSubmoduleSelection(submoduleList, available) {
|
|
|
2254
2360
|
return selected.sort((a, b) => a.path.localeCompare(b.path));
|
|
2255
2361
|
}
|
|
2256
2362
|
function configPath(cwd) {
|
|
2257
|
-
return
|
|
2363
|
+
return path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2258
2364
|
}
|
|
2259
2365
|
function toPosixRelative(from, to) {
|
|
2260
|
-
const relative =
|
|
2261
|
-
return relative ? relative.split(
|
|
2366
|
+
const relative = path16.relative(from, to);
|
|
2367
|
+
return relative ? relative.split(path16.sep).join("/") : ".";
|
|
2262
2368
|
}
|
|
2263
2369
|
|
|
2264
2370
|
// src/commands/add-agent.ts
|
|
@@ -2334,7 +2440,7 @@ ${installedLabels.join("\n")}`));
|
|
|
2334
2440
|
|
|
2335
2441
|
// src/commands/clear.ts
|
|
2336
2442
|
import { readdir as readdir4, rm, writeFile } from "fs/promises";
|
|
2337
|
-
import
|
|
2443
|
+
import path17 from "path";
|
|
2338
2444
|
import { cancel as cancel3, confirm as confirm2, outro as outro2 } from "@clack/prompts";
|
|
2339
2445
|
import chalk3 from "chalk";
|
|
2340
2446
|
var PLATFORM_TEMPLATE_DIR = {
|
|
@@ -2373,7 +2479,7 @@ async function clear(opts) {
|
|
|
2373
2479
|
async function buildTargetClearPlans(targets) {
|
|
2374
2480
|
const plans = [];
|
|
2375
2481
|
for (const target of targets) {
|
|
2376
|
-
const easyCodingDir =
|
|
2482
|
+
const easyCodingDir = path17.join(target.dir, EASY_CODING_DIR);
|
|
2377
2483
|
if (!await pathExists(easyCodingDir)) {
|
|
2378
2484
|
continue;
|
|
2379
2485
|
}
|
|
@@ -2390,7 +2496,7 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
|
|
|
2390
2496
|
if (targetPlans.some((targetPlan) => targetPlan.target.label === ".")) {
|
|
2391
2497
|
return;
|
|
2392
2498
|
}
|
|
2393
|
-
if (!await pathExists(
|
|
2499
|
+
if (!await pathExists(path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
2394
2500
|
return;
|
|
2395
2501
|
}
|
|
2396
2502
|
const [parent] = await resolveUpgradeTargets(cwd);
|
|
@@ -2404,7 +2510,7 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
|
|
|
2404
2510
|
await refreshSupermoduleParent(cwd, config2.agents, parent.supermodule.submodules ?? []);
|
|
2405
2511
|
}
|
|
2406
2512
|
async function resolveInstalledAgents(cwd) {
|
|
2407
|
-
const configPath2 =
|
|
2513
|
+
const configPath2 = path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2408
2514
|
if (await pathExists(configPath2)) {
|
|
2409
2515
|
try {
|
|
2410
2516
|
const config2 = await readConfigYaml(configPath2);
|
|
@@ -2435,7 +2541,7 @@ async function hasPlatformInstall(cwd, platform) {
|
|
|
2435
2541
|
async function hasInstallMarkers(cwd, meta) {
|
|
2436
2542
|
const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
|
|
2437
2543
|
for (const marker of markers) {
|
|
2438
|
-
if (await pathExists(
|
|
2544
|
+
if (await pathExists(path17.join(cwd, marker))) {
|
|
2439
2545
|
return true;
|
|
2440
2546
|
}
|
|
2441
2547
|
}
|
|
@@ -2491,7 +2597,7 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
|
|
|
2491
2597
|
}
|
|
2492
2598
|
if (await manifestFileMatches(filePath, file.sha256)) {
|
|
2493
2599
|
plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
|
|
2494
|
-
addEmptyDirChain(plan.emptyDirs,
|
|
2600
|
+
addEmptyDirChain(plan.emptyDirs, path17.dirname(filePath), cwd);
|
|
2495
2601
|
} else {
|
|
2496
2602
|
plan.skippedModified.push(filePath);
|
|
2497
2603
|
}
|
|
@@ -2541,28 +2647,28 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
|
|
|
2541
2647
|
);
|
|
2542
2648
|
for (const meta of metas) {
|
|
2543
2649
|
for (const name of managedSkills) {
|
|
2544
|
-
plan.remove.add(
|
|
2650
|
+
plan.remove.add(path17.join(cwd, meta.skillsDir, name));
|
|
2545
2651
|
}
|
|
2546
2652
|
for (const name of hookFileNames) {
|
|
2547
|
-
plan.remove.add(
|
|
2653
|
+
plan.remove.add(path17.join(cwd, meta.hooksDir, name));
|
|
2548
2654
|
}
|
|
2549
2655
|
for (const name of agentFileNames) {
|
|
2550
|
-
plan.remove.add(
|
|
2656
|
+
plan.remove.add(path17.join(cwd, meta.agentsDir, name));
|
|
2551
2657
|
}
|
|
2552
2658
|
addHookConfigPrune(
|
|
2553
2659
|
plan.pruneHookConfigs,
|
|
2554
2660
|
plan.pruneHookCommands,
|
|
2555
|
-
|
|
2661
|
+
path17.join(cwd, meta.hookConfigFile),
|
|
2556
2662
|
managedHookPathsForTemplate(cwd, meta, hookFileNames),
|
|
2557
2663
|
[]
|
|
2558
2664
|
);
|
|
2559
|
-
plan.constraints.add(
|
|
2665
|
+
plan.constraints.add(path17.join(cwd, meta.mainConstraint));
|
|
2560
2666
|
if (platform === "codex") {
|
|
2561
|
-
plan.remove.add(
|
|
2667
|
+
plan.remove.add(path17.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
|
|
2562
2668
|
}
|
|
2563
|
-
plan.emptyDirs.add(
|
|
2564
|
-
plan.emptyDirs.add(
|
|
2565
|
-
plan.emptyDirs.add(
|
|
2669
|
+
plan.emptyDirs.add(path17.join(cwd, meta.skillsDir));
|
|
2670
|
+
plan.emptyDirs.add(path17.join(cwd, meta.hooksDir));
|
|
2671
|
+
plan.emptyDirs.add(path17.join(cwd, meta.agentsDir));
|
|
2566
2672
|
}
|
|
2567
2673
|
}
|
|
2568
2674
|
async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
|
|
@@ -2609,7 +2715,7 @@ function managedHookPathsForRegistration(cwd, hookPath) {
|
|
|
2609
2715
|
try {
|
|
2610
2716
|
paths.push(...managedHookPathTokens(manifestPath(cwd, hookPath)));
|
|
2611
2717
|
} catch {
|
|
2612
|
-
if (
|
|
2718
|
+
if (path17.isAbsolute(hookPath)) {
|
|
2613
2719
|
paths.push(...managedHookPathTokens(hookPath));
|
|
2614
2720
|
}
|
|
2615
2721
|
}
|
|
@@ -2620,7 +2726,7 @@ function managedHookPathsForTemplate(cwd, meta, hookFileNames) {
|
|
|
2620
2726
|
(name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`
|
|
2621
2727
|
);
|
|
2622
2728
|
const absolutePaths = hookFileNames.flatMap(
|
|
2623
|
-
(name) => managedHookPathTokens(
|
|
2729
|
+
(name) => managedHookPathTokens(path17.join(cwd, meta.hooksDir, name))
|
|
2624
2730
|
);
|
|
2625
2731
|
return [...relativePaths, ...absolutePaths];
|
|
2626
2732
|
}
|
|
@@ -2628,8 +2734,8 @@ function managedHookPathTokens(filePath) {
|
|
|
2628
2734
|
const tokens = /* @__PURE__ */ new Set();
|
|
2629
2735
|
for (const equivalentPath of equivalentAbsolutePaths(filePath)) {
|
|
2630
2736
|
const normalized = equivalentPath.replace(/\\/g, "/");
|
|
2631
|
-
const dir =
|
|
2632
|
-
const basename =
|
|
2737
|
+
const dir = path17.posix.dirname(normalized);
|
|
2738
|
+
const basename = path17.posix.basename(normalized);
|
|
2633
2739
|
tokens.add(normalized);
|
|
2634
2740
|
tokens.add(shellDoubleQuoteArg2(normalized));
|
|
2635
2741
|
tokens.add(`${shellDoubleQuoteArg2(dir)}/${basename}`);
|
|
@@ -2637,7 +2743,7 @@ function managedHookPathTokens(filePath) {
|
|
|
2637
2743
|
return [...tokens];
|
|
2638
2744
|
}
|
|
2639
2745
|
function equivalentAbsolutePaths(filePath) {
|
|
2640
|
-
const normalized =
|
|
2746
|
+
const normalized = path17.resolve(filePath).replace(/\\/g, "/");
|
|
2641
2747
|
const equivalents = [normalized];
|
|
2642
2748
|
if (normalized.startsWith("/private/var/")) {
|
|
2643
2749
|
equivalents.push(normalized.replace(/^\/private\/var\//, "/var/"));
|
|
@@ -2711,11 +2817,11 @@ function addRuntimeClearEntries(plan, cwd) {
|
|
|
2711
2817
|
plan.remove = [
|
|
2712
2818
|
.../* @__PURE__ */ new Set([
|
|
2713
2819
|
...plan.remove,
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2820
|
+
path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
|
|
2821
|
+
path17.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
|
|
2822
|
+
path17.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
|
|
2823
|
+
path17.join(cwd, EASY_CODING_DIR, TOOLS_DIR),
|
|
2824
|
+
path17.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
|
|
2719
2825
|
])
|
|
2720
2826
|
];
|
|
2721
2827
|
}
|
|
@@ -2723,7 +2829,7 @@ function addEmptyDirChain(emptyDirs, startDir, cwd) {
|
|
|
2723
2829
|
let current = startDir;
|
|
2724
2830
|
while (current !== cwd && isInsideDirectory2(cwd, current)) {
|
|
2725
2831
|
emptyDirs.add(current);
|
|
2726
|
-
const parent =
|
|
2832
|
+
const parent = path17.dirname(current);
|
|
2727
2833
|
if (parent === current) {
|
|
2728
2834
|
break;
|
|
2729
2835
|
}
|
|
@@ -2731,8 +2837,8 @@ function addEmptyDirChain(emptyDirs, startDir, cwd) {
|
|
|
2731
2837
|
}
|
|
2732
2838
|
}
|
|
2733
2839
|
function isInsideDirectory2(parent, child) {
|
|
2734
|
-
const relative =
|
|
2735
|
-
return Boolean(relative) && !relative.startsWith("..") && !
|
|
2840
|
+
const relative = path17.relative(parent, child);
|
|
2841
|
+
return Boolean(relative) && !relative.startsWith("..") && !path17.isAbsolute(relative);
|
|
2736
2842
|
}
|
|
2737
2843
|
async function listSharedHookNamesForPlatform(platform) {
|
|
2738
2844
|
const names = await listFileNames(getTemplatePath("shared-hooks"));
|
|
@@ -2869,7 +2975,7 @@ function sortDirsDeepestFirst(dirs) {
|
|
|
2869
2975
|
});
|
|
2870
2976
|
}
|
|
2871
2977
|
function pathDepth(dir) {
|
|
2872
|
-
return
|
|
2978
|
+
return path17.normalize(dir).split(path17.sep).filter(Boolean).length;
|
|
2873
2979
|
}
|
|
2874
2980
|
async function listDirNames(dir) {
|
|
2875
2981
|
try {
|
|
@@ -2888,7 +2994,7 @@ async function listFileNames(dir) {
|
|
|
2888
2994
|
}
|
|
2889
2995
|
}
|
|
2890
2996
|
function renderPlan(cwd, agents, plan) {
|
|
2891
|
-
const rel = (target) =>
|
|
2997
|
+
const rel = (target) => path17.relative(cwd, target) || target;
|
|
2892
2998
|
const lines = [];
|
|
2893
2999
|
lines.push(chalk3.bold("easy-coding clear"));
|
|
2894
3000
|
lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
|
|
@@ -2937,12 +3043,12 @@ function renderTargetPlans(targetPlans) {
|
|
|
2937
3043
|
}
|
|
2938
3044
|
|
|
2939
3045
|
// src/commands/config.ts
|
|
2940
|
-
import
|
|
3046
|
+
import path20 from "path";
|
|
2941
3047
|
import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
|
|
2942
3048
|
import chalk4 from "chalk";
|
|
2943
3049
|
|
|
2944
3050
|
// src/utils/compare-versions.ts
|
|
2945
|
-
import
|
|
3051
|
+
import path18 from "path";
|
|
2946
3052
|
function parseVersion(version) {
|
|
2947
3053
|
const withoutBuild = String(version ?? "").split("+", 1)[0];
|
|
2948
3054
|
const separator = withoutBuild.indexOf("-");
|
|
@@ -2997,7 +3103,7 @@ function isVersionBehind(installed, current = VERSION) {
|
|
|
2997
3103
|
return compareVersions(installed, current) === -1;
|
|
2998
3104
|
}
|
|
2999
3105
|
async function checkForUpgrade(cwd) {
|
|
3000
|
-
const configPath2 =
|
|
3106
|
+
const configPath2 = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
3001
3107
|
if (!await pathExists(configPath2)) {
|
|
3002
3108
|
return;
|
|
3003
3109
|
}
|
|
@@ -3018,7 +3124,7 @@ async function checkForUpgrade(cwd) {
|
|
|
3018
3124
|
|
|
3019
3125
|
// src/utils/tdd-readiness.ts
|
|
3020
3126
|
import { readFile as readFile4, realpath as realpath2 } from "fs/promises";
|
|
3021
|
-
import
|
|
3127
|
+
import path19 from "path";
|
|
3022
3128
|
var TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1";
|
|
3023
3129
|
var TDD_READINESS_SCOPE = "changed-production-lines";
|
|
3024
3130
|
var TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA";
|
|
@@ -3026,7 +3132,7 @@ var TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD";
|
|
|
3026
3132
|
var COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py";
|
|
3027
3133
|
var JAVA_BUILD_FILE_NAMES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
|
|
3028
3134
|
function readinessPath(root) {
|
|
3029
|
-
return
|
|
3135
|
+
return path19.join(root, EASY_CODING_DIR, TDD_DIR, TDD_READINESS_FILE);
|
|
3030
3136
|
}
|
|
3031
3137
|
function parseFileRecords(value, field, reasons) {
|
|
3032
3138
|
if (!Array.isArray(value) || value.length === 0) {
|
|
@@ -3040,7 +3146,7 @@ function parseFileRecords(value, field, reasons) {
|
|
|
3040
3146
|
continue;
|
|
3041
3147
|
}
|
|
3042
3148
|
const record = item;
|
|
3043
|
-
if (typeof record.path !== "string" || !record.path.trim() ||
|
|
3149
|
+
if (typeof record.path !== "string" || !record.path.trim() || path19.isAbsolute(record.path)) {
|
|
3044
3150
|
reasons.push(`${field} contains an invalid path`);
|
|
3045
3151
|
continue;
|
|
3046
3152
|
}
|
|
@@ -3054,15 +3160,15 @@ function usesRequiredGateVariables(command) {
|
|
|
3054
3160
|
}
|
|
3055
3161
|
function isSafeReportPattern(value) {
|
|
3056
3162
|
const normalized = value.replaceAll("\\", "/");
|
|
3057
|
-
return !
|
|
3163
|
+
return !path19.isAbsolute(value) && !normalized.split("/").includes("..");
|
|
3058
3164
|
}
|
|
3059
3165
|
async function validateFiles(root, records, reasons) {
|
|
3060
3166
|
const resolvedRoot = await realpath2(root);
|
|
3061
3167
|
for (const record of records) {
|
|
3062
|
-
const absolute =
|
|
3168
|
+
const absolute = path19.resolve(root, record.path);
|
|
3063
3169
|
try {
|
|
3064
3170
|
const resolved = await realpath2(absolute);
|
|
3065
|
-
if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${
|
|
3171
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path19.sep}`)) {
|
|
3066
3172
|
reasons.push(`readiness file escapes project root: ${record.path}`);
|
|
3067
3173
|
continue;
|
|
3068
3174
|
}
|
|
@@ -3111,7 +3217,7 @@ async function inspectTddReadiness(root) {
|
|
|
3111
3217
|
}
|
|
3112
3218
|
const buildFiles = parseFileRecords(manifest.build_files, "build_files", reasons);
|
|
3113
3219
|
const toolFiles = parseFileRecords(manifest.tool_files, "tool_files", reasons);
|
|
3114
|
-
if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(
|
|
3220
|
+
if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(path19.basename(record.path)))) {
|
|
3115
3221
|
reasons.push("build_files must include a Maven or Gradle Java build file");
|
|
3116
3222
|
}
|
|
3117
3223
|
if (!toolFiles.some((record) => record.path.replaceAll("\\", "/") === COVERAGE_TOOL_PATH)) {
|
|
@@ -3127,9 +3233,17 @@ async function inspectTddReadiness(root) {
|
|
|
3127
3233
|
}
|
|
3128
3234
|
|
|
3129
3235
|
// src/commands/config.ts
|
|
3130
|
-
async function config() {
|
|
3236
|
+
async function config(options = {}) {
|
|
3131
3237
|
renderBanner();
|
|
3132
|
-
const
|
|
3238
|
+
const scope = options.scope ?? "project";
|
|
3239
|
+
if (scope !== "project" && scope !== "local")
|
|
3240
|
+
throw new Error("Config scope must be project or local.");
|
|
3241
|
+
const explicit = options.approvalMode !== void 0 || options.cooperateMode !== void 0 || options.unitTestMode !== void 0 || options.utCoverageThreshold !== void 0 || options.reset !== void 0;
|
|
3242
|
+
if (scope === "local") {
|
|
3243
|
+
await configureOverrides(localConfigPath(), scope, options, explicit);
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
const configPath2 = path20.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
|
|
3133
3247
|
if (!await pathExists(configPath2)) {
|
|
3134
3248
|
throw new Error("No easy-coding harness found in this project.");
|
|
3135
3249
|
}
|
|
@@ -3150,6 +3264,10 @@ async function config() {
|
|
|
3150
3264
|
`Project harness ${projectConfig.harness_version} does not exactly match CLI ${VERSION}. Upgrade the harness or update the CLI before changing config.`
|
|
3151
3265
|
);
|
|
3152
3266
|
}
|
|
3267
|
+
if (explicit) {
|
|
3268
|
+
await configureOverrides(configPath2, scope, options, true);
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3153
3271
|
const current = resolveLegacyBehavior(projectConfig);
|
|
3154
3272
|
const approvalMode = await select({
|
|
3155
3273
|
message: `Select project approval mode (current: ${current.approvalMode})`,
|
|
@@ -3182,83 +3300,186 @@ async function config() {
|
|
|
3182
3300
|
return;
|
|
3183
3301
|
}
|
|
3184
3302
|
const workflowMode = "adaptive";
|
|
3185
|
-
const
|
|
3186
|
-
message: `
|
|
3187
|
-
initialValue: current.
|
|
3303
|
+
const unitTestMode = await select({
|
|
3304
|
+
message: `Select Java unit test strategy (current: ${current.unitTestMode})`,
|
|
3305
|
+
initialValue: current.unitTestMode,
|
|
3188
3306
|
options: [
|
|
3189
|
-
{ value:
|
|
3190
|
-
{ value:
|
|
3307
|
+
{ value: "none", label: "none \u2014 preserve task-required verification (default)" },
|
|
3308
|
+
{ value: "ut", label: "UT \u2014 passing unit tests and changed-line coverage" },
|
|
3309
|
+
{ value: "tdd", label: "TDD \u2014 test-first development and changed-line coverage" }
|
|
3191
3310
|
]
|
|
3192
3311
|
});
|
|
3193
|
-
if (typeof
|
|
3312
|
+
if (typeof unitTestMode === "symbol") {
|
|
3194
3313
|
cancel4("Configuration cancelled.");
|
|
3195
3314
|
return;
|
|
3196
3315
|
}
|
|
3197
|
-
if (
|
|
3316
|
+
if (unitTestMode !== "none") {
|
|
3198
3317
|
const readiness = await inspectTddReadiness(process.cwd());
|
|
3199
3318
|
if (readiness.status !== "ready") {
|
|
3200
3319
|
cancel4(
|
|
3201
|
-
`
|
|
3320
|
+
`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.`
|
|
3202
3321
|
);
|
|
3203
3322
|
return;
|
|
3204
3323
|
}
|
|
3205
3324
|
}
|
|
3206
|
-
let
|
|
3207
|
-
if (
|
|
3325
|
+
let utCoverageThreshold = current.utCoverageThreshold;
|
|
3326
|
+
if (unitTestMode !== "none") {
|
|
3208
3327
|
const thresholdInput = await text({
|
|
3209
3328
|
message: "Minimum changed-production-line coverage percentage",
|
|
3210
|
-
initialValue: String(current.
|
|
3329
|
+
initialValue: String(current.utCoverageThreshold),
|
|
3211
3330
|
validate(value) {
|
|
3212
3331
|
const parsed = Number(value);
|
|
3213
|
-
return
|
|
3332
|
+
return isUtCoverageThreshold(parsed) ? void 0 : "Enter an integer from 1 to 100.";
|
|
3214
3333
|
}
|
|
3215
3334
|
});
|
|
3216
3335
|
if (typeof thresholdInput === "symbol") {
|
|
3217
3336
|
cancel4("Configuration cancelled.");
|
|
3218
3337
|
return;
|
|
3219
3338
|
}
|
|
3220
|
-
|
|
3339
|
+
utCoverageThreshold = Number(thresholdInput);
|
|
3340
|
+
}
|
|
3341
|
+
const cooperateMode = await select({
|
|
3342
|
+
message: `Select project cooperation (current: ${projectConfig.behavior.cooperate_mode ?? "default"})`,
|
|
3343
|
+
initialValue: projectConfig.behavior.cooperate_mode ?? "default",
|
|
3344
|
+
options: [
|
|
3345
|
+
{ value: "default", label: "default \u2014 hand off at stage boundaries" },
|
|
3346
|
+
{
|
|
3347
|
+
value: "dispatch",
|
|
3348
|
+
label: "dispatch \u2014 manually hand off implementation and QUALITY repairs"
|
|
3349
|
+
}
|
|
3350
|
+
]
|
|
3351
|
+
});
|
|
3352
|
+
if (typeof cooperateMode === "symbol") {
|
|
3353
|
+
cancel4("Configuration cancelled.");
|
|
3354
|
+
return;
|
|
3221
3355
|
}
|
|
3222
3356
|
const shouldSave = await confirm3({
|
|
3223
|
-
message: `Set approval=${approvalMode},
|
|
3357
|
+
message: `Set approval=${approvalMode}, cooperate=${cooperateMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}?`,
|
|
3224
3358
|
initialValue: true
|
|
3225
3359
|
});
|
|
3226
3360
|
if (typeof shouldSave === "symbol" || !shouldSave) {
|
|
3227
3361
|
cancel4("Configuration cancelled.");
|
|
3228
3362
|
return;
|
|
3229
3363
|
}
|
|
3230
|
-
if (
|
|
3364
|
+
if (unitTestMode !== "none") {
|
|
3231
3365
|
const readiness = await inspectTddReadiness(process.cwd());
|
|
3232
3366
|
if (readiness.status !== "ready") {
|
|
3233
3367
|
cancel4(
|
|
3234
|
-
`
|
|
3368
|
+
`Unit test strategy was not enabled because readiness changed before save: ${readiness.reasons.join("; ")}. No project modes were changed.`
|
|
3235
3369
|
);
|
|
3236
3370
|
return;
|
|
3237
3371
|
}
|
|
3238
3372
|
}
|
|
3239
|
-
await setBehaviorModes(
|
|
3373
|
+
await setBehaviorModes(
|
|
3374
|
+
configPath2,
|
|
3375
|
+
approvalMode,
|
|
3376
|
+
workflowMode,
|
|
3377
|
+
unitTestMode,
|
|
3378
|
+
utCoverageThreshold,
|
|
3379
|
+
cooperateMode
|
|
3380
|
+
);
|
|
3240
3381
|
outro3(
|
|
3241
3382
|
chalk4.green(
|
|
3242
|
-
`Project modes updated: approval=${approvalMode}, workflow=${workflowMode},
|
|
3383
|
+
`Project modes updated: approval=${approvalMode}, workflow=${workflowMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}.`
|
|
3243
3384
|
)
|
|
3244
3385
|
);
|
|
3245
3386
|
}
|
|
3387
|
+
async function configureOverrides(filePath, scope, options, explicit) {
|
|
3388
|
+
const changes = {};
|
|
3389
|
+
const flags = {
|
|
3390
|
+
approval_mode: options.approvalMode,
|
|
3391
|
+
cooperate_mode: options.cooperateMode,
|
|
3392
|
+
unit_test_mode: options.unitTestMode,
|
|
3393
|
+
ut_coverage_threshold: options.utCoverageThreshold
|
|
3394
|
+
};
|
|
3395
|
+
for (const key of BEHAVIOR_KEYS) {
|
|
3396
|
+
if (flags[key] !== void 0)
|
|
3397
|
+
changes[key] = key === "ut_coverage_threshold" ? Number(flags[key]) : flags[key];
|
|
3398
|
+
}
|
|
3399
|
+
if (options.reset) {
|
|
3400
|
+
if (!BEHAVIOR_KEYS.includes(options.reset))
|
|
3401
|
+
throw new Error("Unknown behavior field to reset.");
|
|
3402
|
+
changes[options.reset] = null;
|
|
3403
|
+
}
|
|
3404
|
+
if (!explicit) {
|
|
3405
|
+
const local = await readLocalBehavior();
|
|
3406
|
+
const key = await select({
|
|
3407
|
+
message: "Select local override to edit",
|
|
3408
|
+
options: BEHAVIOR_KEYS.map((value) => ({
|
|
3409
|
+
value,
|
|
3410
|
+
label: `${value}: ${local[value] ?? "inherit project"}`
|
|
3411
|
+
}))
|
|
3412
|
+
});
|
|
3413
|
+
if (typeof key === "symbol") {
|
|
3414
|
+
cancel4("Configuration cancelled.");
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
if (key === "ut_coverage_threshold") {
|
|
3418
|
+
const value = await text({
|
|
3419
|
+
message: "Coverage threshold 1..100, or inherit",
|
|
3420
|
+
initialValue: String(local[key] ?? "inherit"),
|
|
3421
|
+
validate: (value2) => value2 === "inherit" || isUtCoverageThreshold(Number(value2)) ? void 0 : "Enter 1..100 or inherit."
|
|
3422
|
+
});
|
|
3423
|
+
if (typeof value === "symbol") {
|
|
3424
|
+
cancel4("Configuration cancelled.");
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
changes[key] = value === "inherit" ? null : Number(value);
|
|
3428
|
+
} else {
|
|
3429
|
+
const values = key === "approval_mode" ? APPROVAL_MODES : key === "cooperate_mode" ? COOPERATE_MODES : UNIT_TEST_MODES;
|
|
3430
|
+
const value = await select({
|
|
3431
|
+
message: `Set local ${key}`,
|
|
3432
|
+
initialValue: local[key] ?? "inherit",
|
|
3433
|
+
options: [
|
|
3434
|
+
{ value: "inherit", label: "inherit \u2014 use project setting" },
|
|
3435
|
+
...values.map((value2) => ({ value: value2, label: value2 }))
|
|
3436
|
+
]
|
|
3437
|
+
});
|
|
3438
|
+
if (typeof value === "symbol") {
|
|
3439
|
+
cancel4("Configuration cancelled.");
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
changes[key] = value === "inherit" ? null : value;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
for (const key of BEHAVIOR_KEYS) {
|
|
3446
|
+
if (changes[key] !== void 0 && changes[key] !== null)
|
|
3447
|
+
validateBehaviorValue(key, changes[key]);
|
|
3448
|
+
}
|
|
3449
|
+
if (!options.yes) {
|
|
3450
|
+
const accepted = await confirm3({
|
|
3451
|
+
message: `Save ${scope} overrides ${JSON.stringify(changes)}?`,
|
|
3452
|
+
initialValue: true
|
|
3453
|
+
});
|
|
3454
|
+
if (accepted !== true) {
|
|
3455
|
+
cancel4("Configuration cancelled.");
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
if (scope === "project" && changes.unit_test_mode && changes.unit_test_mode !== "none") {
|
|
3460
|
+
const readiness = await inspectTddReadiness(process.cwd());
|
|
3461
|
+
if (readiness.status !== "ready")
|
|
3462
|
+
throw new Error(`Unit test readiness: ${readiness.status}. ${readiness.reasons.join("; ")}`);
|
|
3463
|
+
}
|
|
3464
|
+
await writeBehaviorOverrides(filePath, changes);
|
|
3465
|
+
outro3(chalk4.green(`${scope} behavior overrides saved: ${filePath}`));
|
|
3466
|
+
}
|
|
3246
3467
|
|
|
3247
3468
|
// src/commands/init.ts
|
|
3248
|
-
import
|
|
3469
|
+
import path22 from "path";
|
|
3249
3470
|
import { note, outro as outro4 } from "@clack/prompts";
|
|
3250
3471
|
import chalk5 from "chalk";
|
|
3251
3472
|
|
|
3252
3473
|
// src/utils/install-state.ts
|
|
3253
3474
|
import { readdir as readdir5 } from "fs/promises";
|
|
3254
|
-
import
|
|
3475
|
+
import path21 from "path";
|
|
3255
3476
|
var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
|
|
3256
3477
|
async function detectEasyCodingInstallState(cwd) {
|
|
3257
|
-
const easyCodingDir =
|
|
3478
|
+
const easyCodingDir = path21.join(cwd, EASY_CODING_DIR);
|
|
3258
3479
|
if (!await pathExists(easyCodingDir)) {
|
|
3259
3480
|
return { kind: "fresh", easyCodingDir };
|
|
3260
3481
|
}
|
|
3261
|
-
const configPath2 =
|
|
3482
|
+
const configPath2 = path21.join(easyCodingDir, CONFIG_FILE);
|
|
3262
3483
|
if (await pathExists(configPath2)) {
|
|
3263
3484
|
return { kind: "installed", easyCodingDir, configPath: configPath2 };
|
|
3264
3485
|
}
|
|
@@ -3276,17 +3497,17 @@ async function detectEasyCodingInstallState(cwd) {
|
|
|
3276
3497
|
async function detectLegacyAssets(easyCodingDir) {
|
|
3277
3498
|
const assets = [];
|
|
3278
3499
|
for (const file of LEGACY_ROOT_FILES) {
|
|
3279
|
-
if (await pathExists(
|
|
3500
|
+
if (await pathExists(path21.join(easyCodingDir, file))) {
|
|
3280
3501
|
assets.push(relativeEasyCodingPath(file));
|
|
3281
3502
|
}
|
|
3282
3503
|
}
|
|
3283
|
-
if (await pathExists(
|
|
3504
|
+
if (await pathExists(path21.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
3284
3505
|
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
3285
3506
|
}
|
|
3286
|
-
const shortMemoryFiles = await listMarkdownFiles(
|
|
3507
|
+
const shortMemoryFiles = await listMarkdownFiles(path21.join(easyCodingDir, "memory", "short"));
|
|
3287
3508
|
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
3288
3509
|
for (const dir of ["spec", "prototype"]) {
|
|
3289
|
-
if (await hasAnyDirectoryEntry(
|
|
3510
|
+
if (await hasAnyDirectoryEntry(path21.join(easyCodingDir, dir))) {
|
|
3290
3511
|
assets.push(relativeEasyCodingPath(dir));
|
|
3291
3512
|
}
|
|
3292
3513
|
}
|
|
@@ -3306,13 +3527,13 @@ async function hasAnyDirectoryEntry(dir) {
|
|
|
3306
3527
|
return (await readdir5(dir)).length > 0;
|
|
3307
3528
|
}
|
|
3308
3529
|
function relativeEasyCodingPath(...segments) {
|
|
3309
|
-
return
|
|
3530
|
+
return path21.posix.join(EASY_CODING_DIR, ...segments);
|
|
3310
3531
|
}
|
|
3311
3532
|
function relativeConfigPath() {
|
|
3312
|
-
return
|
|
3533
|
+
return path21.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
3313
3534
|
}
|
|
3314
3535
|
function relativeProjectInitTaskPath() {
|
|
3315
|
-
return
|
|
3536
|
+
return path21.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
3316
3537
|
}
|
|
3317
3538
|
|
|
3318
3539
|
// src/commands/init.ts
|
|
@@ -3393,7 +3614,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
|
|
|
3393
3614
|
if (!targetSubmodulePaths.has(entry.path)) {
|
|
3394
3615
|
continue;
|
|
3395
3616
|
}
|
|
3396
|
-
const dir =
|
|
3617
|
+
const dir = path22.join(cwd, entry.path);
|
|
3397
3618
|
targets.push(
|
|
3398
3619
|
await targetFromState(dir, entry.path, "submodule-child", {
|
|
3399
3620
|
parent: toPosixRelative2(dir, cwd)
|
|
@@ -3440,24 +3661,24 @@ function contextFromState(role, installState) {
|
|
|
3440
3661
|
};
|
|
3441
3662
|
}
|
|
3442
3663
|
function toPosixRelative2(from, to) {
|
|
3443
|
-
const relative =
|
|
3444
|
-
return relative ? relative.split(
|
|
3664
|
+
const relative = path22.relative(from, to);
|
|
3665
|
+
return relative ? relative.split(path22.sep).join("/") : ".";
|
|
3445
3666
|
}
|
|
3446
3667
|
async function resolveInitPlatforms(cwd, opts, parentInstalled) {
|
|
3447
3668
|
if (opts.agent || !parentInstalled) {
|
|
3448
3669
|
return resolvePlatforms(opts, ["claude-code"]);
|
|
3449
3670
|
}
|
|
3450
|
-
const config2 = await readConfigYaml(
|
|
3671
|
+
const config2 = await readConfigYaml(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
3451
3672
|
if (Array.isArray(config2.agents) && config2.agents.length > 0) {
|
|
3452
3673
|
return config2.agents;
|
|
3453
3674
|
}
|
|
3454
3675
|
return resolvePlatforms(opts, ["claude-code"]);
|
|
3455
3676
|
}
|
|
3456
3677
|
async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
|
|
3457
|
-
if (!await pathExists(
|
|
3678
|
+
if (!await pathExists(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
3458
3679
|
return;
|
|
3459
3680
|
}
|
|
3460
|
-
const config2 = parentTarget2.installed ? await readConfigYaml(
|
|
3681
|
+
const config2 = parentTarget2.installed ? await readConfigYaml(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
|
|
3461
3682
|
const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
|
|
3462
3683
|
await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
|
|
3463
3684
|
}
|
|
@@ -3466,7 +3687,7 @@ async function refreshInstalledChildTopologies(targets) {
|
|
|
3466
3687
|
if (!target.installed || target.context.role !== "submodule-child") {
|
|
3467
3688
|
continue;
|
|
3468
3689
|
}
|
|
3469
|
-
const configPath2 =
|
|
3690
|
+
const configPath2 = path22.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
|
|
3470
3691
|
if (!await pathExists(configPath2)) {
|
|
3471
3692
|
continue;
|
|
3472
3693
|
}
|
|
@@ -3475,12 +3696,12 @@ async function refreshInstalledChildTopologies(targets) {
|
|
|
3475
3696
|
}
|
|
3476
3697
|
|
|
3477
3698
|
// src/commands/status.ts
|
|
3478
|
-
import
|
|
3699
|
+
import path24 from "path";
|
|
3479
3700
|
import chalk6 from "chalk";
|
|
3480
3701
|
|
|
3481
3702
|
// src/utils/session.ts
|
|
3482
3703
|
import { readdir as readdir6, stat as stat3, unlink as unlink2 } from "fs/promises";
|
|
3483
|
-
import
|
|
3704
|
+
import path23 from "path";
|
|
3484
3705
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3485
3706
|
var IDLE_SESSION_RETENTION_MS = 7 * DAY_MS;
|
|
3486
3707
|
var ATTACHED_SESSION_RETENTION_MS = 30 * DAY_MS;
|
|
@@ -3494,7 +3715,7 @@ function parseSessionFile(content) {
|
|
|
3494
3715
|
}
|
|
3495
3716
|
}
|
|
3496
3717
|
function getSessionDir(cwd) {
|
|
3497
|
-
return
|
|
3718
|
+
return path23.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
3498
3719
|
}
|
|
3499
3720
|
async function listSessionFiles(cwd) {
|
|
3500
3721
|
const dir = getSessionDir(cwd);
|
|
@@ -3506,7 +3727,7 @@ async function listSessionFiles(cwd) {
|
|
|
3506
3727
|
if (!name.endsWith(".json")) {
|
|
3507
3728
|
continue;
|
|
3508
3729
|
}
|
|
3509
|
-
const filePath =
|
|
3730
|
+
const filePath = path23.join(dir, name);
|
|
3510
3731
|
const content = await readTextIfExists(filePath);
|
|
3511
3732
|
if (!content) {
|
|
3512
3733
|
continue;
|
|
@@ -3530,9 +3751,11 @@ async function cleanSessionRuntime(cwd, options = {}) {
|
|
|
3530
3751
|
const maxSessions = options.maxSessions ?? MAX_SESSION_FILES;
|
|
3531
3752
|
const reserveSlots = options.reserveSlots ?? 0;
|
|
3532
3753
|
const candidates = (await listSessionCleanupCandidates(cwd)).filter((candidate) => {
|
|
3533
|
-
if (!options.
|
|
3754
|
+
if (!options.preserveUnitTestSettings) return true;
|
|
3534
3755
|
const session = parseSessionFile(candidate.content);
|
|
3535
|
-
return !session || !
|
|
3756
|
+
return !session || !["unit_test_mode", "ut_coverage_threshold", "tdd_enabled", "tdd_coverage_threshold"].some(
|
|
3757
|
+
(key) => key in session
|
|
3758
|
+
);
|
|
3536
3759
|
});
|
|
3537
3760
|
const removed = /* @__PURE__ */ new Set();
|
|
3538
3761
|
for (const candidate of candidates) {
|
|
@@ -3569,7 +3792,7 @@ async function listSessionCleanupCandidates(cwd) {
|
|
|
3569
3792
|
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
3570
3793
|
continue;
|
|
3571
3794
|
}
|
|
3572
|
-
const filePath =
|
|
3795
|
+
const filePath = path23.join(dir, entry.name);
|
|
3573
3796
|
try {
|
|
3574
3797
|
const [content, fileStat] = await Promise.all([readTextFile(filePath), stat3(filePath)]);
|
|
3575
3798
|
const session = parseSessionFile(content);
|
|
@@ -3604,7 +3827,7 @@ async function unlinkIfUnchanged(candidate) {
|
|
|
3604
3827
|
}
|
|
3605
3828
|
}
|
|
3606
3829
|
async function cleanOrphanAcceptanceSnapshots(cwd) {
|
|
3607
|
-
const acceptanceDir =
|
|
3830
|
+
const acceptanceDir = path23.join(getSessionDir(cwd), "acceptance");
|
|
3608
3831
|
if (!await pathExists(acceptanceDir)) {
|
|
3609
3832
|
return 0;
|
|
3610
3833
|
}
|
|
@@ -3613,7 +3836,7 @@ async function cleanOrphanAcceptanceSnapshots(cwd) {
|
|
|
3613
3836
|
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
3614
3837
|
continue;
|
|
3615
3838
|
}
|
|
3616
|
-
const snapshotPath =
|
|
3839
|
+
const snapshotPath = path23.join(acceptanceDir, entry.name);
|
|
3617
3840
|
if (!await isOrphanAcceptanceSnapshot(cwd, snapshotPath, entry.name.slice(0, -5))) {
|
|
3618
3841
|
continue;
|
|
3619
3842
|
}
|
|
@@ -3632,7 +3855,7 @@ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
|
|
|
3632
3855
|
let taskContent;
|
|
3633
3856
|
try {
|
|
3634
3857
|
taskContent = await readTextFile(
|
|
3635
|
-
|
|
3858
|
+
path23.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json")
|
|
3636
3859
|
);
|
|
3637
3860
|
} catch (error) {
|
|
3638
3861
|
if (isFileNotFound(error)) {
|
|
@@ -3654,7 +3877,7 @@ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
|
|
|
3654
3877
|
return true;
|
|
3655
3878
|
}
|
|
3656
3879
|
const checkpoint = task.quality_checkpoint ?? task.verification_checkpoint;
|
|
3657
|
-
return typeof checkpoint?.snapshot_file !== "string" ||
|
|
3880
|
+
return typeof checkpoint?.snapshot_file !== "string" || path23.resolve(cwd, checkpoint.snapshot_file) !== path23.resolve(snapshotPath);
|
|
3658
3881
|
}
|
|
3659
3882
|
function isFileNotFound(error) {
|
|
3660
3883
|
return error.code === "ENOENT";
|
|
@@ -3664,7 +3887,7 @@ function isFileNotFound(error) {
|
|
|
3664
3887
|
async function status() {
|
|
3665
3888
|
renderBanner();
|
|
3666
3889
|
const cwd = process.cwd();
|
|
3667
|
-
const configPath2 =
|
|
3890
|
+
const configPath2 = path24.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
3668
3891
|
if (!await pathExists(configPath2)) {
|
|
3669
3892
|
throw new Error("No easy-coding harness found in this project.");
|
|
3670
3893
|
}
|
|
@@ -3674,7 +3897,6 @@ async function status() {
|
|
|
3674
3897
|
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
3675
3898
|
const sessions = await listSessionFiles(cwd);
|
|
3676
3899
|
const versionRelation = compareVersions(config2.harness_version, VERSION);
|
|
3677
|
-
const tddReadiness = await inspectTddReadiness(cwd);
|
|
3678
3900
|
console.log(chalk6.bold("Harness"));
|
|
3679
3901
|
console.log(` version: ${config2.harness_version}`);
|
|
3680
3902
|
console.log(` cli: ${VERSION}`);
|
|
@@ -3690,27 +3912,45 @@ async function status() {
|
|
|
3690
3912
|
const migratedBehavior = resolveLegacyBehavior(config2);
|
|
3691
3913
|
const projectApprovalMode = isApprovalMode(config2.behavior?.approval_mode) ? config2.behavior.approval_mode : migratedBehavior.approvalMode;
|
|
3692
3914
|
const projectWorkflowMode = isConfiguredWorkflowMode(config2.behavior?.workflow_mode) ? config2.behavior.workflow_mode : migratedBehavior.workflowMode;
|
|
3693
|
-
const
|
|
3694
|
-
const
|
|
3915
|
+
const projectUnitTestMode = migratedBehavior.unitTestMode;
|
|
3916
|
+
const projectUtCoverageThreshold = migratedBehavior.utCoverageThreshold;
|
|
3917
|
+
const localBehavior = await readLocalBehavior();
|
|
3918
|
+
const projectBehavior = {
|
|
3919
|
+
...config2.behavior,
|
|
3920
|
+
approval_mode: projectApprovalMode,
|
|
3921
|
+
unit_test_mode: projectUnitTestMode,
|
|
3922
|
+
ut_coverage_threshold: projectUtCoverageThreshold
|
|
3923
|
+
};
|
|
3924
|
+
const effective = resolveBehaviorSettings(projectBehavior, localBehavior);
|
|
3925
|
+
const needsCoverage = effective.values.unit_test_mode !== "none" || sessions.some(({ session }) => ["ut", "tdd"].includes(session.unit_test_mode ?? "none")) || activeTasks.some(({ task }) => ["ut", "tdd"].includes(task.unit_test_mode ?? "none"));
|
|
3926
|
+
const readiness = needsCoverage ? await inspectTddReadiness(cwd) : { status: "not_checked", reasons: [] };
|
|
3695
3927
|
console.log(` approval_mode: ${projectApprovalMode}`);
|
|
3928
|
+
console.log(
|
|
3929
|
+
` cooperate_mode: ${effective.values.cooperate_mode} (${effective.sources.cooperate_mode})`
|
|
3930
|
+
);
|
|
3696
3931
|
console.log(` workflow_mode: ${projectWorkflowMode}`);
|
|
3697
|
-
console.log(`
|
|
3698
|
-
console.log(`
|
|
3699
|
-
console.log(`
|
|
3700
|
-
if (
|
|
3701
|
-
console.log(`
|
|
3932
|
+
console.log(` unit_test_mode: ${projectUnitTestMode}`);
|
|
3933
|
+
console.log(` ut_coverage_threshold: ${projectUtCoverageThreshold}`);
|
|
3934
|
+
console.log(` unit_test_readiness: ${readiness.status}`);
|
|
3935
|
+
if (readiness.reasons.length > 0) {
|
|
3936
|
+
console.log(` unit_test_readiness_reasons: ${readiness.reasons.join("; ")}`);
|
|
3702
3937
|
}
|
|
3703
3938
|
console.log("");
|
|
3704
3939
|
console.log(chalk6.bold("Sessions"));
|
|
3705
3940
|
console.log(` project_approval_mode: ${projectApprovalMode}`);
|
|
3706
3941
|
console.log(` project_workflow_mode: ${projectWorkflowMode}`);
|
|
3707
|
-
console.log(`
|
|
3708
|
-
console.log(`
|
|
3709
|
-
console.log(`
|
|
3942
|
+
console.log(` project_unit_test_mode: ${projectUnitTestMode}`);
|
|
3943
|
+
console.log(` project_ut_coverage_threshold: ${projectUtCoverageThreshold}`);
|
|
3944
|
+
for (const [key, value] of Object.entries(localBehavior)) console.log(` local_${key}: ${value}`);
|
|
3945
|
+
console.log(
|
|
3946
|
+
` effective_approval_mode: ${effective.values.approval_mode} (${effective.sources.approval_mode})`
|
|
3947
|
+
);
|
|
3710
3948
|
console.log(` configured_workflow_mode: ${projectWorkflowMode} (without a session override)`);
|
|
3711
|
-
console.log(` effective_tdd_enabled: ${projectTddEnabled} (without a session override)`);
|
|
3712
3949
|
console.log(
|
|
3713
|
-
`
|
|
3950
|
+
` effective_unit_test_mode: ${effective.values.unit_test_mode} (${effective.sources.unit_test_mode})`
|
|
3951
|
+
);
|
|
3952
|
+
console.log(
|
|
3953
|
+
` effective_ut_coverage_threshold: ${effective.values.ut_coverage_threshold} (${effective.sources.ut_coverage_threshold})`
|
|
3714
3954
|
);
|
|
3715
3955
|
if (sessions.length === 0) {
|
|
3716
3956
|
console.log(" no session files");
|
|
@@ -3722,21 +3962,28 @@ async function status() {
|
|
|
3722
3962
|
);
|
|
3723
3963
|
const sessionApprovalMode = session.approval_mode ?? (legacySessionMode === "lite" ? "guard" : legacySessionMode);
|
|
3724
3964
|
const sessionWorkflowMode = session.workflow_mode ?? (legacySessionMode === "lite" ? "fast" : hasLegacySessionMode ? "adaptive" : void 0);
|
|
3725
|
-
const
|
|
3726
|
-
const
|
|
3965
|
+
const sessionUnitTestMode = session.unit_test_mode;
|
|
3966
|
+
const sessionUtCoverageThreshold = session.ut_coverage_threshold;
|
|
3967
|
+
const resolved = resolveBehaviorSettings(projectBehavior, localBehavior, {
|
|
3968
|
+
...session,
|
|
3969
|
+
approval_mode: sessionApprovalMode
|
|
3970
|
+
});
|
|
3727
3971
|
console.log(` - ${key}`);
|
|
3728
3972
|
console.log(` agent: ${session.agent ?? "legacy/unknown"}`);
|
|
3729
3973
|
console.log(` source: ${session.session_source ?? "legacy"}`);
|
|
3730
|
-
console.log(` approval_mode: ${sessionApprovalMode ?? "project
|
|
3974
|
+
console.log(` approval_mode: ${sessionApprovalMode ?? "inherit local/project"}`);
|
|
3731
3975
|
console.log(` workflow_mode: ${sessionWorkflowMode ?? "project default"}`);
|
|
3732
|
-
console.log(`
|
|
3733
|
-
console.log(
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
console.log(`
|
|
3976
|
+
console.log(` unit_test_mode: ${sessionUnitTestMode ?? "inherit local/project"}`);
|
|
3977
|
+
console.log(
|
|
3978
|
+
` ut_coverage_threshold: ${sessionUtCoverageThreshold ?? "inherit local/project"}`
|
|
3979
|
+
);
|
|
3980
|
+
console.log(` effective_approval_mode: ${resolved.values.approval_mode}`);
|
|
3737
3981
|
console.log(
|
|
3738
|
-
`
|
|
3982
|
+
` cooperate_mode: ${resolved.values.cooperate_mode} (${resolved.sources.cooperate_mode})`
|
|
3739
3983
|
);
|
|
3984
|
+
console.log(` configured_workflow_mode: ${sessionWorkflowMode ?? projectWorkflowMode}`);
|
|
3985
|
+
console.log(` effective_unit_test_mode: ${resolved.values.unit_test_mode}`);
|
|
3986
|
+
console.log(` effective_ut_coverage_threshold: ${resolved.values.ut_coverage_threshold}`);
|
|
3740
3987
|
console.log(
|
|
3741
3988
|
` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
|
|
3742
3989
|
);
|
|
@@ -3756,10 +4003,8 @@ async function status() {
|
|
|
3756
4003
|
console.log(
|
|
3757
4004
|
` task_workflow_mode: ${task.workflow_mode ?? task.workflow_mode_proposal?.selected_mode ?? "not resolved"}`
|
|
3758
4005
|
);
|
|
3759
|
-
console.log(`
|
|
3760
|
-
console.log(
|
|
3761
|
-
` task_tdd_coverage_threshold: ${task.tdd_coverage_threshold ?? "not frozen"}`
|
|
3762
|
-
);
|
|
4006
|
+
console.log(` task_unit_test_mode: ${task.unit_test_mode ?? "not frozen"}`);
|
|
4007
|
+
console.log(` task_ut_coverage_threshold: ${task.ut_coverage_threshold ?? "not frozen"}`);
|
|
3763
4008
|
console.log(` last_agent: ${task.last_agent}`);
|
|
3764
4009
|
} else {
|
|
3765
4010
|
console.log(` current_task: ${session.current_task} (task.json missing)`);
|
|
@@ -3811,7 +4056,7 @@ async function update(opts) {
|
|
|
3811
4056
|
}
|
|
3812
4057
|
|
|
3813
4058
|
// src/commands/upgrade.ts
|
|
3814
|
-
import
|
|
4059
|
+
import path25 from "path";
|
|
3815
4060
|
import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
|
|
3816
4061
|
import chalk8 from "chalk";
|
|
3817
4062
|
var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
|
|
@@ -3874,10 +4119,11 @@ async function upgrade(opts) {
|
|
|
3874
4119
|
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
3875
4120
|
"Will remove retired files that still match the previous install manifest and preserve locally modified copies.",
|
|
3876
4121
|
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
3877
|
-
"Will migrate behavior config to schema
|
|
3878
|
-
"Will
|
|
4122
|
+
"Will migrate behavior config to schema 6 while preserving project/session unit test settings and frozen task baselines.",
|
|
4123
|
+
"Will preserve runtime tools used by active UT/TDD tasks; run upgrade after those tasks finish to refresh the tools.",
|
|
4124
|
+
"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.",
|
|
3879
4125
|
"Will migrate active REVIEW/VERIFICATION tasks to QUALITY, retire active read-only task types as CLOSED, and preserve their artifacts and history.",
|
|
3880
|
-
"Will migrate legacy workflow/
|
|
4126
|
+
"Will migrate legacy workflow/unit test task metadata; memory content, spec, and project knowledge files remain untouched."
|
|
3881
4127
|
].join("\n");
|
|
3882
4128
|
if (opts.dryRun) {
|
|
3883
4129
|
console.log(summary);
|
|
@@ -3895,7 +4141,9 @@ async function upgrade(opts) {
|
|
|
3895
4141
|
}
|
|
3896
4142
|
for (const { target, config: config2 } of pending) {
|
|
3897
4143
|
const previousManifest = await readInstallManifest(target.dir);
|
|
3898
|
-
const sessionCleanup = await cleanSessionRuntime(target.dir, {
|
|
4144
|
+
const sessionCleanup = await cleanSessionRuntime(target.dir, {
|
|
4145
|
+
preserveUnitTestSettings: true
|
|
4146
|
+
});
|
|
3899
4147
|
if (sessionCleanup.sessionsRemoved > 0 || sessionCleanup.acceptanceSnapshotsRemoved > 0) {
|
|
3900
4148
|
console.log(
|
|
3901
4149
|
chalk8.yellow(
|
|
@@ -3968,7 +4216,7 @@ async function resolvePendingUpgradeTargets(targets) {
|
|
|
3968
4216
|
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
3969
4217
|
);
|
|
3970
4218
|
}
|
|
3971
|
-
if (relation === -1 || relation === 0 && (installedVersion !== VERSION || await needsHookConfigRefresh(target, config2) || await hasLegacyWorkflowState(target.dir))) {
|
|
4219
|
+
if (relation === -1 || relation === 0 && (installedVersion !== VERSION || await needsHookConfigRefresh(target, config2) || await hasLegacyWorkflowState(target.dir) || (await runtimeToolUpdates(target.dir)).length > 0)) {
|
|
3972
4220
|
pending.push({ target, config: config2 });
|
|
3973
4221
|
}
|
|
3974
4222
|
}
|
|
@@ -3982,7 +4230,7 @@ async function needsHookConfigRefresh(target, config2) {
|
|
|
3982
4230
|
const manifest = await readInstallManifest(target.dir);
|
|
3983
4231
|
for (const agent of config2.agents) {
|
|
3984
4232
|
const meta = resolvePlatformMeta(target.dir, agent);
|
|
3985
|
-
const configPath2 =
|
|
4233
|
+
const configPath2 = path25.join(target.dir, meta.hookConfigFile);
|
|
3986
4234
|
const content = await readTextIfExists(configPath2);
|
|
3987
4235
|
if (content === null) {
|
|
3988
4236
|
return true;
|
|
@@ -4122,7 +4370,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
|
|
|
4122
4370
|
return true;
|
|
4123
4371
|
}
|
|
4124
4372
|
return pathAliases(
|
|
4125
|
-
normalizePathForHookComparison(
|
|
4373
|
+
normalizePathForHookComparison(path25.resolve(cwd, relativeHookPath))
|
|
4126
4374
|
).includes(normalizedHookPath);
|
|
4127
4375
|
});
|
|
4128
4376
|
}
|
|
@@ -4212,7 +4460,7 @@ program.command("init").description("Initialize easy-coding harness in current p
|
|
|
4212
4460
|
program.command("add-agent").description("Add agent platform support to an existing project").option("--agent <list>", "Comma-separated platforms to add").option("--submodules <list>", "Comma-separated initialized submodule paths or names to update").option("--no-submodules", "Add the agent only to the current directory").action(withErrorHandling(addAgent));
|
|
4213
4461
|
program.command("upgrade").description("Upgrade harness files to current CLI version").option("--dry-run", "Preview changes without applying").option("-y, --yes", "Skip confirmation").action(withErrorHandling(upgrade));
|
|
4214
4462
|
program.command("update").description("Refresh the global CLI to the latest published version").option("--tag <tag>", "npm dist-tag or version to install", "latest").option("--dry-run", "Preview the install command without running it").option("-y, --yes", "Skip confirmation").action(withErrorHandling(update));
|
|
4215
|
-
program.command("config").description("
|
|
4463
|
+
program.command("config").description("Configure project or local harness behavior").option("--scope <scope>", "project or local", "project").option("--approval-mode <mode>", "approve, guard, confirm, or auto").option("--cooperate-mode <mode>", "default or dispatch").option("--unit-test-mode <mode>", "none, ut, or tdd").option("--ut-coverage-threshold <percent>", "coverage threshold 1..100").option("--reset <field>", "Remove an override to restore inheritance").option("-y, --yes", "Save explicitly selected fields without another prompt").action(withErrorHandling(config));
|
|
4216
4464
|
program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
|
|
4217
4465
|
program.command("clear").description("Remove installed harness files (skills, hooks, config); keep tasks, spec, memory").option("--submodules <list>", "Comma-separated initialized submodule paths or names to clear").option("--no-submodules", "Clear only the current directory").option("--dry-run", "Preview what would be removed without deleting").option("-y, --yes", "Skip confirmation").action(withErrorHandling(clear));
|
|
4218
4466
|
program.parse();
|