easy-coding-harness 1.1.0-beta.1 → 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 +10 -0
- package/README.md +13 -4
- package/dist/cli.js +395 -190
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +4 -2
- package/templates/common/skills/ec-config/SKILL.md +33 -5
- package/templates/common/skills/ec-implementing/SKILL.md +10 -2
- package/templates/common/skills/ec-quality/SKILL.md +42 -10
- package/templates/common/skills/ec-task-management/SKILL.md +10 -3
- package/templates/common/skills/ec-workflow/SKILL.md +30 -10
- package/templates/main-constraint/AGENTS.md.tpl +24 -7
- package/templates/main-constraint/CLAUDE.md.tpl +24 -7
- package/templates/shared-hooks/easy_coding_inputs.py +18 -0
- package/templates/shared-hooks/easy_coding_state.py +367 -86
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
|
|
@@ -107,7 +109,71 @@ var CONFIG_SCHEMA_VERSION = 6;
|
|
|
107
109
|
var DEFAULT_UT_COVERAGE_THRESHOLD = 90;
|
|
108
110
|
var UNIT_TEST_MODES = ["none", "ut", "tdd"];
|
|
109
111
|
var APPROVAL_MODES = ["approve", "guard", "confirm", "auto"];
|
|
112
|
+
var COOPERATE_MODES = ["default", "dispatch"];
|
|
110
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
|
+
}
|
|
111
177
|
function createDefaultConfig(params) {
|
|
112
178
|
const config2 = {
|
|
113
179
|
version: CONFIG_SCHEMA_VERSION,
|
|
@@ -129,7 +195,8 @@ function createDefaultConfig(params) {
|
|
|
129
195
|
approval_mode: "guard",
|
|
130
196
|
workflow_mode: "adaptive",
|
|
131
197
|
unit_test_mode: "none",
|
|
132
|
-
ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD
|
|
198
|
+
ut_coverage_threshold: DEFAULT_UT_COVERAGE_THRESHOLD,
|
|
199
|
+
cooperate_mode: "default"
|
|
133
200
|
}
|
|
134
201
|
};
|
|
135
202
|
if (params.supermodule) {
|
|
@@ -215,7 +282,7 @@ function resolveLegacyBehavior(config2) {
|
|
|
215
282
|
const utCoverageThreshold = isUtCoverageThreshold(configuredThreshold) ? configuredThreshold : DEFAULT_UT_COVERAGE_THRESHOLD;
|
|
216
283
|
return { approvalMode, workflowMode, unitTestMode, utCoverageThreshold };
|
|
217
284
|
}
|
|
218
|
-
async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold) {
|
|
285
|
+
async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMode, utCoverageThreshold, cooperateMode) {
|
|
219
286
|
if (unitTestMode !== void 0 && !isUnitTestMode(unitTestMode)) {
|
|
220
287
|
throw new Error("Unit test mode must be none, ut, or tdd.");
|
|
221
288
|
}
|
|
@@ -234,6 +301,7 @@ async function setBehaviorModes(filePath, approvalMode, workflowMode, unitTestMo
|
|
|
234
301
|
behavior.workflow_mode = workflowMode;
|
|
235
302
|
behavior.unit_test_mode = unitTestMode ?? resolvedBehavior.unitTestMode;
|
|
236
303
|
behavior.ut_coverage_threshold = utCoverageThreshold ?? resolvedBehavior.utCoverageThreshold;
|
|
304
|
+
behavior.cooperate_mode = cooperateMode ?? legacyBehavior.cooperate_mode ?? "default";
|
|
237
305
|
config2.behavior = behavior;
|
|
238
306
|
config2.version = CONFIG_SCHEMA_VERSION;
|
|
239
307
|
});
|
|
@@ -266,7 +334,7 @@ function createProjectId() {
|
|
|
266
334
|
}
|
|
267
335
|
|
|
268
336
|
// src/utils/gitignore.ts
|
|
269
|
-
import
|
|
337
|
+
import path4 from "path";
|
|
270
338
|
|
|
271
339
|
// src/constants/paths.ts
|
|
272
340
|
var EASY_CODING_DIR = ".easy-coding";
|
|
@@ -290,7 +358,7 @@ var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness gene
|
|
|
290
358
|
|
|
291
359
|
// src/utils/gitignore.ts
|
|
292
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") {
|
|
293
|
-
const gitignorePath =
|
|
361
|
+
const gitignorePath = path4.join(cwd, ".gitignore");
|
|
294
362
|
const current = await readTextIfExists(gitignorePath) ?? "";
|
|
295
363
|
const lines = current.split(/\r?\n/).map((line) => line.trim());
|
|
296
364
|
if (lines.includes(entry)) {
|
|
@@ -315,7 +383,7 @@ async function ensureHookBytecodeIgnored(cwd) {
|
|
|
315
383
|
// src/utils/install-manifest.ts
|
|
316
384
|
import { createHash } from "crypto";
|
|
317
385
|
import { readFile as readFile3, rmdir, unlink } from "fs/promises";
|
|
318
|
-
import
|
|
386
|
+
import path5 from "path";
|
|
319
387
|
|
|
320
388
|
// src/types/platform.ts
|
|
321
389
|
var pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
@@ -487,12 +555,12 @@ async function writeInstallManifest(cwd, params) {
|
|
|
487
555
|
constraint_regions: [...constraintRegions.values()].sort(byPath)
|
|
488
556
|
};
|
|
489
557
|
await writeTextFile(
|
|
490
|
-
|
|
558
|
+
path5.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
|
|
491
559
|
JSON.stringify(manifest, null, 2)
|
|
492
560
|
);
|
|
493
561
|
}
|
|
494
562
|
async function readInstallManifest(cwd) {
|
|
495
|
-
const manifestPath2 =
|
|
563
|
+
const manifestPath2 = path5.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
|
|
496
564
|
const content = await readTextIfExists(manifestPath2);
|
|
497
565
|
if (content === null) {
|
|
498
566
|
return null;
|
|
@@ -534,7 +602,7 @@ async function pruneRetiredManagedFiles(cwd, previous, artifacts) {
|
|
|
534
602
|
await unlink(filePath);
|
|
535
603
|
removed.push(file.path);
|
|
536
604
|
try {
|
|
537
|
-
await rmdir(
|
|
605
|
+
await rmdir(path5.dirname(filePath));
|
|
538
606
|
} catch (error) {
|
|
539
607
|
if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) {
|
|
540
608
|
throw error;
|
|
@@ -553,10 +621,10 @@ function manifestPath(cwd, projectPath) {
|
|
|
553
621
|
return resolveProjectPath(cwd, projectPath);
|
|
554
622
|
}
|
|
555
623
|
function toProjectPath(cwd, filePath) {
|
|
556
|
-
const root =
|
|
557
|
-
const resolved =
|
|
624
|
+
const root = path5.resolve(cwd);
|
|
625
|
+
const resolved = path5.resolve(filePath);
|
|
558
626
|
assertPathInsideProject(root, resolved, filePath);
|
|
559
|
-
return
|
|
627
|
+
return path5.relative(root, resolved).split(path5.sep).join("/");
|
|
560
628
|
}
|
|
561
629
|
function normalizeCommand(command) {
|
|
562
630
|
return command.replace(/\\/g, "/").trim().replace(/\s+/g, " ");
|
|
@@ -568,17 +636,17 @@ async function sha256File(filePath) {
|
|
|
568
636
|
function resolveProjectPath(cwd, projectPath) {
|
|
569
637
|
const normalized = projectPath.replace(/\\/g, "/");
|
|
570
638
|
const parts = normalized.split("/");
|
|
571
|
-
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 === "..")) {
|
|
572
640
|
throw new Error(`Unsafe install manifest path: ${projectPath}`);
|
|
573
641
|
}
|
|
574
|
-
const root =
|
|
575
|
-
const resolved =
|
|
642
|
+
const root = path5.resolve(cwd);
|
|
643
|
+
const resolved = path5.resolve(root, normalized);
|
|
576
644
|
assertPathInsideProject(root, resolved, projectPath);
|
|
577
645
|
return resolved;
|
|
578
646
|
}
|
|
579
647
|
function assertPathInsideProject(root, resolvedPath, sourcePath) {
|
|
580
|
-
const relative =
|
|
581
|
-
if (!relative || relative.startsWith("..") ||
|
|
648
|
+
const relative = path5.relative(root, resolvedPath);
|
|
649
|
+
if (!relative || relative.startsWith("..") || path5.isAbsolute(relative)) {
|
|
582
650
|
throw new Error(`Unsafe install manifest path: ${sourcePath}`);
|
|
583
651
|
}
|
|
584
652
|
}
|
|
@@ -677,7 +745,7 @@ function isHookPythonPath(candidate) {
|
|
|
677
745
|
}
|
|
678
746
|
function normalizeHookPath(cwd, hookPath) {
|
|
679
747
|
const normalized = hookPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
680
|
-
if (
|
|
748
|
+
if (path5.isAbsolute(hookPath) || path5.posix.isAbsolute(normalized) || path5.win32.isAbsolute(hookPath)) {
|
|
681
749
|
return toProjectPath(cwd, hookPath);
|
|
682
750
|
}
|
|
683
751
|
return normalized;
|
|
@@ -687,11 +755,11 @@ function byPath(a, b) {
|
|
|
687
755
|
}
|
|
688
756
|
|
|
689
757
|
// src/utils/runtime-scaffold.ts
|
|
690
|
-
import
|
|
758
|
+
import path8 from "path";
|
|
691
759
|
|
|
692
760
|
// src/utils/task-json.ts
|
|
693
761
|
import { readdir } from "fs/promises";
|
|
694
|
-
import
|
|
762
|
+
import path6 from "path";
|
|
695
763
|
var LEGACY_STAGE_MAP = {
|
|
696
764
|
WAITING_CONFIRM: "ANALYSIS",
|
|
697
765
|
MEMORY_SHORT: "MEMORY",
|
|
@@ -718,7 +786,7 @@ function createProjectInitTask(params) {
|
|
|
718
786
|
};
|
|
719
787
|
}
|
|
720
788
|
function getTaskJsonPath(cwd, taskId) {
|
|
721
|
-
return
|
|
789
|
+
return path6.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
|
|
722
790
|
}
|
|
723
791
|
async function writeTaskJson(filePath, task) {
|
|
724
792
|
await writeTextFile(filePath, JSON.stringify(task, null, 2));
|
|
@@ -1031,16 +1099,16 @@ function migrationWorkflowMode(task, candidates, projectWorkflowMode) {
|
|
|
1031
1099
|
);
|
|
1032
1100
|
}
|
|
1033
1101
|
async function taskFiles(cwd) {
|
|
1034
|
-
const tasksDir =
|
|
1102
|
+
const tasksDir = path6.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
1035
1103
|
if (!await pathExists(tasksDir)) return [];
|
|
1036
1104
|
const entries = await readdir(tasksDir, { withFileTypes: true });
|
|
1037
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) =>
|
|
1105
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => path6.join(tasksDir, entry.name, "task.json"));
|
|
1038
1106
|
}
|
|
1039
1107
|
async function sessionFiles(cwd) {
|
|
1040
|
-
const sessionsDir =
|
|
1108
|
+
const sessionsDir = path6.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
1041
1109
|
if (!await pathExists(sessionsDir)) return [];
|
|
1042
1110
|
const entries = await readdir(sessionsDir, { withFileTypes: true });
|
|
1043
|
-
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));
|
|
1044
1112
|
}
|
|
1045
1113
|
async function readJsonRecord(filePath) {
|
|
1046
1114
|
try {
|
|
@@ -1078,7 +1146,7 @@ async function migrateLegacyWorkflowState(cwd) {
|
|
|
1078
1146
|
let sessionsUpdated = 0;
|
|
1079
1147
|
const updatedTaskPaths = /* @__PURE__ */ new Set();
|
|
1080
1148
|
let projectWorkflowMode = "adaptive";
|
|
1081
|
-
const configPath2 =
|
|
1149
|
+
const configPath2 = path6.join(cwd, EASY_CODING_DIR, "config.yaml");
|
|
1082
1150
|
if (await pathExists(configPath2)) {
|
|
1083
1151
|
try {
|
|
1084
1152
|
const config2 = await readConfigYaml(configPath2);
|
|
@@ -1133,7 +1201,7 @@ async function migrateLegacyWorkflowState(cwd) {
|
|
|
1133
1201
|
if (!task) continue;
|
|
1134
1202
|
const migrationOwned = task.workflow_mode_legacy === true && task.workflow_mode_confirmed_by === "upgrade-migration";
|
|
1135
1203
|
if (!migrationOwned) continue;
|
|
1136
|
-
const taskId =
|
|
1204
|
+
const taskId = path6.basename(path6.dirname(filePath));
|
|
1137
1205
|
const mode = migrationWorkflowMode(
|
|
1138
1206
|
task,
|
|
1139
1207
|
candidatesByTask.get(taskId) ?? [],
|
|
@@ -1168,7 +1236,7 @@ async function setPendingInitSince(cwd, version) {
|
|
|
1168
1236
|
await writeTaskJson(filePath, task);
|
|
1169
1237
|
}
|
|
1170
1238
|
async function listTasks(cwd) {
|
|
1171
|
-
const tasksDir =
|
|
1239
|
+
const tasksDir = path6.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
1172
1240
|
if (!await pathExists(tasksDir)) {
|
|
1173
1241
|
return [];
|
|
1174
1242
|
}
|
|
@@ -1201,15 +1269,15 @@ function isActiveTask(task) {
|
|
|
1201
1269
|
|
|
1202
1270
|
// src/utils/template-paths.ts
|
|
1203
1271
|
import { existsSync } from "fs";
|
|
1204
|
-
import
|
|
1272
|
+
import path7 from "path";
|
|
1205
1273
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1206
1274
|
function getTemplateRoot() {
|
|
1207
|
-
const here =
|
|
1275
|
+
const here = path7.dirname(fileURLToPath2(import.meta.url));
|
|
1208
1276
|
const candidates = [
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1277
|
+
path7.resolve(here, "../templates"),
|
|
1278
|
+
path7.resolve(here, "../../templates"),
|
|
1279
|
+
path7.resolve(process.cwd(), "src/templates"),
|
|
1280
|
+
path7.resolve(process.cwd(), "templates")
|
|
1213
1281
|
];
|
|
1214
1282
|
const found = candidates.find((candidate) => existsSync(candidate));
|
|
1215
1283
|
if (!found) {
|
|
@@ -1218,17 +1286,17 @@ function getTemplateRoot() {
|
|
|
1218
1286
|
return found;
|
|
1219
1287
|
}
|
|
1220
1288
|
function getTemplatePath(...segments) {
|
|
1221
|
-
return
|
|
1289
|
+
return path7.join(getTemplateRoot(), ...segments);
|
|
1222
1290
|
}
|
|
1223
1291
|
|
|
1224
1292
|
// src/utils/runtime-scaffold.ts
|
|
1225
1293
|
async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
1226
|
-
const easyCodingDir =
|
|
1294
|
+
const easyCodingDir = path8.join(cwd, EASY_CODING_DIR);
|
|
1227
1295
|
await ensureDir(easyCodingDir);
|
|
1228
|
-
const configPath2 =
|
|
1296
|
+
const configPath2 = path8.join(easyCodingDir, CONFIG_FILE);
|
|
1229
1297
|
let projectId = opts.projectId ?? createProjectId();
|
|
1230
1298
|
if (!await pathExists(configPath2)) {
|
|
1231
|
-
const projectName =
|
|
1299
|
+
const projectName = path8.basename(cwd);
|
|
1232
1300
|
await writeConfigYaml(
|
|
1233
1301
|
configPath2,
|
|
1234
1302
|
createDefaultConfig({
|
|
@@ -1242,10 +1310,10 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
|
1242
1310
|
} else {
|
|
1243
1311
|
projectId = await ensureProjectId(configPath2);
|
|
1244
1312
|
}
|
|
1245
|
-
await ensureDir(
|
|
1246
|
-
await ensureDir(
|
|
1247
|
-
await ensureDir(
|
|
1248
|
-
await ensureDir(
|
|
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));
|
|
1249
1317
|
await writeMemoryScaffold(easyCodingDir);
|
|
1250
1318
|
await writeTemplatesScaffold(easyCodingDir);
|
|
1251
1319
|
await writeToolsScaffold(cwd);
|
|
@@ -1257,7 +1325,7 @@ async function runtimeToolUpdates(cwd) {
|
|
|
1257
1325
|
);
|
|
1258
1326
|
const updates = [];
|
|
1259
1327
|
for (const file of ["easy_coding_java_coverage.py", "easy_coding_tdd_readiness.py"]) {
|
|
1260
|
-
const target =
|
|
1328
|
+
const target = path8.join(cwd, EASY_CODING_DIR, TOOLS_DIR, file);
|
|
1261
1329
|
const current = await readTextIfExists(target);
|
|
1262
1330
|
if (frozen && current !== null) continue;
|
|
1263
1331
|
const content = await readTextFile(getTemplatePath("runtime", "tools", file));
|
|
@@ -1266,32 +1334,32 @@ async function runtimeToolUpdates(cwd) {
|
|
|
1266
1334
|
return updates;
|
|
1267
1335
|
}
|
|
1268
1336
|
async function writeToolsScaffold(cwd) {
|
|
1269
|
-
const toolsDir =
|
|
1337
|
+
const toolsDir = path8.join(cwd, EASY_CODING_DIR, TOOLS_DIR);
|
|
1270
1338
|
await ensureDir(toolsDir);
|
|
1271
1339
|
for (const update2 of await runtimeToolUpdates(cwd)) {
|
|
1272
1340
|
await writeTextFile(update2.path, update2.content);
|
|
1273
1341
|
}
|
|
1274
1342
|
}
|
|
1275
1343
|
async function writeTemplatesScaffold(easyCodingDir) {
|
|
1276
|
-
const templatesDir =
|
|
1344
|
+
const templatesDir = path8.join(easyCodingDir, TEMPLATES_DIR);
|
|
1277
1345
|
await ensureDir(templatesDir);
|
|
1278
1346
|
const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
|
|
1279
|
-
const dest =
|
|
1347
|
+
const dest = path8.join(templatesDir, "dev-spec-skeleton.md");
|
|
1280
1348
|
await writeTextFile(dest, await readTextFile(src));
|
|
1281
1349
|
}
|
|
1282
1350
|
async function writeMemoryScaffold(easyCodingDir) {
|
|
1283
|
-
const memoryDir =
|
|
1284
|
-
await ensureDir(
|
|
1285
|
-
await ensureDir(
|
|
1351
|
+
const memoryDir = path8.join(easyCodingDir, MEMORY_DIR);
|
|
1352
|
+
await ensureDir(path8.join(memoryDir, "short"));
|
|
1353
|
+
await ensureDir(path8.join(memoryDir, "long"));
|
|
1286
1354
|
for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
|
|
1287
|
-
const destination =
|
|
1355
|
+
const destination = path8.join(memoryDir, "long", file);
|
|
1288
1356
|
if (await pathExists(destination)) {
|
|
1289
1357
|
continue;
|
|
1290
1358
|
}
|
|
1291
1359
|
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
1292
1360
|
await writeTextFile(destination, await readTextFile(templatePath));
|
|
1293
1361
|
}
|
|
1294
|
-
const shortTemplateDest =
|
|
1362
|
+
const shortTemplateDest = path8.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
|
|
1295
1363
|
if (!await pathExists(shortTemplateDest)) {
|
|
1296
1364
|
const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
|
|
1297
1365
|
await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
|
|
@@ -1299,14 +1367,14 @@ async function writeMemoryScaffold(easyCodingDir) {
|
|
|
1299
1367
|
}
|
|
1300
1368
|
|
|
1301
1369
|
// src/commands/install-harness.ts
|
|
1302
|
-
import
|
|
1370
|
+
import path14 from "path";
|
|
1303
1371
|
|
|
1304
1372
|
// src/configurators/claude.ts
|
|
1305
|
-
import
|
|
1373
|
+
import path10 from "path";
|
|
1306
1374
|
|
|
1307
1375
|
// src/configurators/shared.ts
|
|
1308
1376
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
1309
|
-
import
|
|
1377
|
+
import path9 from "path";
|
|
1310
1378
|
|
|
1311
1379
|
// src/utils/marked-region.ts
|
|
1312
1380
|
var MarkedRegionError = class extends Error {
|
|
@@ -1365,7 +1433,7 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
|
|
|
1365
1433
|
return resolved;
|
|
1366
1434
|
}
|
|
1367
1435
|
async function withProjectInstallPaths(cwd, ctx, projectId) {
|
|
1368
|
-
const resolvedProjectId = projectId ?? await readProjectIdIfExists(
|
|
1436
|
+
const resolvedProjectId = projectId ?? await readProjectIdIfExists(path9.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
1369
1437
|
return withInstallPaths(cwd, ctx, resolvedProjectId ?? void 0);
|
|
1370
1438
|
}
|
|
1371
1439
|
function withInstallPaths(cwd, ctx, projectId) {
|
|
@@ -1453,11 +1521,11 @@ async function resolveSkills(ctx) {
|
|
|
1453
1521
|
const entries = await readdir2(skillsRoot);
|
|
1454
1522
|
const skills = [];
|
|
1455
1523
|
for (const entry of entries.sort()) {
|
|
1456
|
-
const skillDir =
|
|
1524
|
+
const skillDir = path9.join(skillsRoot, entry);
|
|
1457
1525
|
if (!await isDirectory(skillDir)) {
|
|
1458
1526
|
continue;
|
|
1459
1527
|
}
|
|
1460
|
-
const content = await readTextFile(
|
|
1528
|
+
const content = await readTextFile(path9.join(skillDir, "SKILL.md"));
|
|
1461
1529
|
skills.push({
|
|
1462
1530
|
name: entry,
|
|
1463
1531
|
content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
|
|
@@ -1473,7 +1541,7 @@ async function resolveBundledSkills(ctx) {
|
|
|
1473
1541
|
const entries = await readdir2(bundledRoot);
|
|
1474
1542
|
const bundled = [];
|
|
1475
1543
|
for (const entry of entries.sort()) {
|
|
1476
|
-
const sourceDir =
|
|
1544
|
+
const sourceDir = path9.join(bundledRoot, entry);
|
|
1477
1545
|
if (await isDirectory(sourceDir)) {
|
|
1478
1546
|
bundled.push({ name: entry, sourceDir, context: ctx });
|
|
1479
1547
|
}
|
|
@@ -1484,13 +1552,13 @@ async function writeSkills(dir, skills, bundled) {
|
|
|
1484
1552
|
await ensureDir(dir);
|
|
1485
1553
|
const written = [];
|
|
1486
1554
|
for (const skill of skills) {
|
|
1487
|
-
const destination =
|
|
1555
|
+
const destination = path9.join(dir, skill.name, "SKILL.md");
|
|
1488
1556
|
await writeTextFile(destination, skill.content);
|
|
1489
1557
|
written.push(destination);
|
|
1490
1558
|
}
|
|
1491
1559
|
for (const skill of bundled) {
|
|
1492
1560
|
written.push(
|
|
1493
|
-
...await copyTemplateDirectory(skill.sourceDir,
|
|
1561
|
+
...await copyTemplateDirectory(skill.sourceDir, path9.join(dir, skill.name), skill.context)
|
|
1494
1562
|
);
|
|
1495
1563
|
}
|
|
1496
1564
|
return written;
|
|
@@ -1505,7 +1573,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
|
|
|
1505
1573
|
if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
|
|
1506
1574
|
continue;
|
|
1507
1575
|
}
|
|
1508
|
-
const sourcePath =
|
|
1576
|
+
const sourcePath = path9.join(hooksRoot, entry);
|
|
1509
1577
|
if ((await stat2(sourcePath)).isDirectory()) {
|
|
1510
1578
|
continue;
|
|
1511
1579
|
}
|
|
@@ -1514,7 +1582,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
|
|
|
1514
1582
|
ctx,
|
|
1515
1583
|
`shared-hooks/${entry}`
|
|
1516
1584
|
);
|
|
1517
|
-
const destination =
|
|
1585
|
+
const destination = path9.join(dir, entry);
|
|
1518
1586
|
await writeTextFile(destination, content);
|
|
1519
1587
|
await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
|
|
1520
1588
|
written.push(destination);
|
|
@@ -1537,7 +1605,7 @@ async function writeMainConstraint(cwd, platform, opts = {}) {
|
|
|
1537
1605
|
if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
|
|
1538
1606
|
throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
|
|
1539
1607
|
}
|
|
1540
|
-
const destination =
|
|
1608
|
+
const destination = path9.join(cwd, meta.mainConstraint);
|
|
1541
1609
|
const current = await readTextIfExists(destination);
|
|
1542
1610
|
if (current) {
|
|
1543
1611
|
const markerContent = extractMarkedRegion(generated);
|
|
@@ -1579,8 +1647,8 @@ async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__
|
|
|
1579
1647
|
if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
|
|
1580
1648
|
continue;
|
|
1581
1649
|
}
|
|
1582
|
-
const sourcePath =
|
|
1583
|
-
const destinationPath =
|
|
1650
|
+
const sourcePath = path9.join(source, entry);
|
|
1651
|
+
const destinationPath = path9.join(destination, stripTemplateExtension(entry));
|
|
1584
1652
|
const sourceStat = await stat2(sourcePath);
|
|
1585
1653
|
if (sourceStat.isDirectory()) {
|
|
1586
1654
|
written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
|
|
@@ -1604,28 +1672,28 @@ async function configureClaude(cwd, opts = {}) {
|
|
|
1604
1672
|
const platform = "claude-code";
|
|
1605
1673
|
const meta = PLATFORM_META[platform];
|
|
1606
1674
|
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
1607
|
-
const dest =
|
|
1608
|
-
const hookConfigPath =
|
|
1675
|
+
const dest = path10.join(cwd, ".claude");
|
|
1676
|
+
const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
|
|
1609
1677
|
const artifacts = [];
|
|
1610
1678
|
const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
|
|
1611
1679
|
artifacts.push(
|
|
1612
1680
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
1613
1681
|
(filePath) => fileArtifact(
|
|
1614
1682
|
filePath,
|
|
1615
|
-
filePath.startsWith(
|
|
1683
|
+
filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
1616
1684
|
platform
|
|
1617
1685
|
)
|
|
1618
1686
|
)
|
|
1619
1687
|
);
|
|
1620
1688
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
1621
1689
|
artifacts.push(
|
|
1622
|
-
...(await writeSharedHooks(
|
|
1690
|
+
...(await writeSharedHooks(path10.join(dest, "hooks"), platform)).map(
|
|
1623
1691
|
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
1624
1692
|
)
|
|
1625
1693
|
);
|
|
1626
1694
|
artifacts.push(
|
|
1627
1695
|
...(await writeSkills(
|
|
1628
|
-
|
|
1696
|
+
path10.join(cwd, meta.skillsDir),
|
|
1629
1697
|
await resolveSkills(ctx),
|
|
1630
1698
|
await resolveBundledSkills(ctx)
|
|
1631
1699
|
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
@@ -1637,28 +1705,28 @@ async function configureClaude(cwd, opts = {}) {
|
|
|
1637
1705
|
}
|
|
1638
1706
|
|
|
1639
1707
|
// src/configurators/codex.ts
|
|
1640
|
-
import
|
|
1708
|
+
import path11 from "path";
|
|
1641
1709
|
async function configureCodex(cwd, opts = {}) {
|
|
1642
1710
|
const platform = "codex";
|
|
1643
1711
|
const meta = PLATFORM_META[platform];
|
|
1644
1712
|
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
1645
|
-
const hookConfigPath =
|
|
1713
|
+
const hookConfigPath = path11.join(cwd, meta.hookConfigFile);
|
|
1646
1714
|
const artifacts = [];
|
|
1647
1715
|
artifacts.push(
|
|
1648
1716
|
...(await writeSkills(
|
|
1649
|
-
|
|
1717
|
+
path11.join(cwd, meta.skillsDir),
|
|
1650
1718
|
await resolveSkills(ctx),
|
|
1651
1719
|
await resolveBundledSkills(ctx)
|
|
1652
1720
|
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
1653
1721
|
);
|
|
1654
1722
|
artifacts.push(
|
|
1655
|
-
...(await writeSharedHooks(
|
|
1723
|
+
...(await writeSharedHooks(path11.join(cwd, meta.hooksDir), platform, {
|
|
1656
1724
|
skipSubagentContext: true
|
|
1657
1725
|
})).map((filePath) => fileArtifact(filePath, "hook", platform))
|
|
1658
1726
|
);
|
|
1659
1727
|
const platformFiles = await copyPlatformTemplates(
|
|
1660
1728
|
"codex",
|
|
1661
|
-
|
|
1729
|
+
path11.join(cwd, ".codex"),
|
|
1662
1730
|
["hooks"],
|
|
1663
1731
|
ctx
|
|
1664
1732
|
);
|
|
@@ -1666,7 +1734,7 @@ async function configureCodex(cwd, opts = {}) {
|
|
|
1666
1734
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
1667
1735
|
(filePath) => fileArtifact(
|
|
1668
1736
|
filePath,
|
|
1669
|
-
filePath.startsWith(
|
|
1737
|
+
filePath.startsWith(path11.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
1670
1738
|
platform
|
|
1671
1739
|
)
|
|
1672
1740
|
)
|
|
@@ -1680,16 +1748,16 @@ async function configureCodex(cwd, opts = {}) {
|
|
|
1680
1748
|
|
|
1681
1749
|
// src/configurators/qoder.ts
|
|
1682
1750
|
import { readdir as readdir3 } from "fs/promises";
|
|
1683
|
-
import
|
|
1751
|
+
import path13 from "path";
|
|
1684
1752
|
|
|
1685
1753
|
// src/utils/platform-paths.ts
|
|
1686
1754
|
import { existsSync as existsSync2 } from "fs";
|
|
1687
|
-
import
|
|
1755
|
+
import path12 from "path";
|
|
1688
1756
|
function detectQoderCnVariant(cwd) {
|
|
1689
1757
|
if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
|
|
1690
1758
|
return true;
|
|
1691
1759
|
}
|
|
1692
|
-
return existsSync2(
|
|
1760
|
+
return existsSync2(path12.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
|
|
1693
1761
|
}
|
|
1694
1762
|
function resolvePlatformMeta(cwd, platform) {
|
|
1695
1763
|
const meta = PLATFORM_META[platform];
|
|
@@ -1720,7 +1788,7 @@ function resolveQoderMetaForBaseDir(baseDir) {
|
|
|
1720
1788
|
|
|
1721
1789
|
// src/configurators/qoder.ts
|
|
1722
1790
|
async function claudeHarnessSkillsExist(cwd) {
|
|
1723
|
-
return pathExists(
|
|
1791
|
+
return pathExists(path13.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
|
|
1724
1792
|
}
|
|
1725
1793
|
async function listFilesRecursive(dir) {
|
|
1726
1794
|
let entries;
|
|
@@ -1734,7 +1802,7 @@ async function listFilesRecursive(dir) {
|
|
|
1734
1802
|
}
|
|
1735
1803
|
const files = [];
|
|
1736
1804
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1737
|
-
const entryPath =
|
|
1805
|
+
const entryPath = path13.join(dir, entry.name);
|
|
1738
1806
|
if (entry.isDirectory()) {
|
|
1739
1807
|
files.push(...await listFilesRecursive(entryPath));
|
|
1740
1808
|
continue;
|
|
@@ -1748,7 +1816,7 @@ async function listFilesRecursive(dir) {
|
|
|
1748
1816
|
async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
|
|
1749
1817
|
const artifacts = [];
|
|
1750
1818
|
for (const skillName of skillNames) {
|
|
1751
|
-
for (const filePath of await listFilesRecursive(
|
|
1819
|
+
for (const filePath of await listFilesRecursive(path13.join(skillsDir, skillName))) {
|
|
1752
1820
|
artifacts.push(fileArtifact(filePath, "skill", platform));
|
|
1753
1821
|
}
|
|
1754
1822
|
}
|
|
@@ -1758,22 +1826,22 @@ async function configureQoder(cwd, opts = {}) {
|
|
|
1758
1826
|
const platform = "qoder";
|
|
1759
1827
|
const meta = resolvePlatformMeta(cwd, platform);
|
|
1760
1828
|
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
1761
|
-
const dest =
|
|
1762
|
-
const hookConfigPath =
|
|
1829
|
+
const dest = path13.join(cwd, ctx.platform_config_dir);
|
|
1830
|
+
const hookConfigPath = path13.join(cwd, meta.hookConfigFile);
|
|
1763
1831
|
const artifacts = [];
|
|
1764
1832
|
const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
|
|
1765
1833
|
artifacts.push(
|
|
1766
1834
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
1767
1835
|
(filePath) => fileArtifact(
|
|
1768
1836
|
filePath,
|
|
1769
|
-
filePath.startsWith(
|
|
1837
|
+
filePath.startsWith(path13.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
1770
1838
|
platform
|
|
1771
1839
|
)
|
|
1772
1840
|
)
|
|
1773
1841
|
);
|
|
1774
1842
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
1775
1843
|
artifacts.push(
|
|
1776
|
-
...(await writeSharedHooks(
|
|
1844
|
+
...(await writeSharedHooks(path13.join(dest, "hooks"), platform)).map(
|
|
1777
1845
|
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
1778
1846
|
)
|
|
1779
1847
|
);
|
|
@@ -1781,14 +1849,14 @@ async function configureQoder(cwd, opts = {}) {
|
|
|
1781
1849
|
const bundledSkills = await resolveBundledSkills(ctx);
|
|
1782
1850
|
if (!await claudeHarnessSkillsExist(cwd)) {
|
|
1783
1851
|
artifacts.push(
|
|
1784
|
-
...(await writeSkills(
|
|
1852
|
+
...(await writeSkills(path13.join(dest, "skills"), skills, bundledSkills)).map(
|
|
1785
1853
|
(filePath) => fileArtifact(filePath, "skill", platform)
|
|
1786
1854
|
)
|
|
1787
1855
|
);
|
|
1788
1856
|
} else {
|
|
1789
1857
|
artifacts.push(
|
|
1790
1858
|
...await existingManagedSkillArtifacts(
|
|
1791
|
-
|
|
1859
|
+
path13.join(dest, "skills"),
|
|
1792
1860
|
[...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
|
|
1793
1861
|
platform
|
|
1794
1862
|
)
|
|
@@ -1859,7 +1927,7 @@ async function refreshSupermoduleParent(targetDir, platforms, submodulePaths) {
|
|
|
1859
1927
|
role: "super-parent",
|
|
1860
1928
|
submodules: submodulePaths
|
|
1861
1929
|
};
|
|
1862
|
-
await updateSupermoduleConfig(
|
|
1930
|
+
await updateSupermoduleConfig(path14.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
|
|
1863
1931
|
for (const platform of platforms) {
|
|
1864
1932
|
await writeMainConstraint(targetDir, platform, {
|
|
1865
1933
|
supermodule: { submodulePaths }
|
|
@@ -1981,14 +2049,14 @@ async function resolveSubmodules(opts, available, defaultSelection = available)
|
|
|
1981
2049
|
}
|
|
1982
2050
|
|
|
1983
2051
|
// src/commands/supermodule-targets.ts
|
|
1984
|
-
import
|
|
2052
|
+
import path16 from "path";
|
|
1985
2053
|
import { cancel as cancel2, multiselect as multiselect2 } from "@clack/prompts";
|
|
1986
2054
|
|
|
1987
2055
|
// src/utils/gitmodules.ts
|
|
1988
2056
|
import { lstat, realpath } from "fs/promises";
|
|
1989
|
-
import
|
|
2057
|
+
import path15 from "path";
|
|
1990
2058
|
async function parseGitmodules(rootDir) {
|
|
1991
|
-
const content = await readTextIfExists(
|
|
2059
|
+
const content = await readTextIfExists(path15.join(rootDir, ".gitmodules"));
|
|
1992
2060
|
if (content === null) {
|
|
1993
2061
|
return [];
|
|
1994
2062
|
}
|
|
@@ -2047,7 +2115,7 @@ async function listInstallableSubmodules(rootDir) {
|
|
|
2047
2115
|
function normalizeSubmodulePath(value) {
|
|
2048
2116
|
const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
|
|
2049
2117
|
const parts = normalized.split("/").filter(Boolean);
|
|
2050
|
-
if (normalized === "" ||
|
|
2118
|
+
if (normalized === "" || path15.posix.isAbsolute(normalized) || path15.win32.isAbsolute(value) || parts.some((part) => part === "..")) {
|
|
2051
2119
|
throw new Error(`Unsafe submodule path in .gitmodules: ${value}`);
|
|
2052
2120
|
}
|
|
2053
2121
|
return parts.join("/");
|
|
@@ -2085,7 +2153,7 @@ function isCommentStart(value, index) {
|
|
|
2085
2153
|
return index === 0 || /\s/.test(value[index - 1]);
|
|
2086
2154
|
}
|
|
2087
2155
|
async function isSubmoduleWorktree(rootDir, submodulePath) {
|
|
2088
|
-
const dir =
|
|
2156
|
+
const dir = path15.join(rootDir, submodulePath);
|
|
2089
2157
|
try {
|
|
2090
2158
|
if (!await pathHasNoSymlinkSegments(rootDir, submodulePath)) {
|
|
2091
2159
|
return false;
|
|
@@ -2099,7 +2167,7 @@ async function isSubmoduleWorktree(rootDir, submodulePath) {
|
|
|
2099
2167
|
if (!dirStat.isDirectory()) {
|
|
2100
2168
|
return false;
|
|
2101
2169
|
}
|
|
2102
|
-
const gitMarkerStat = await lstat(
|
|
2170
|
+
const gitMarkerStat = await lstat(path15.join(dir, ".git"));
|
|
2103
2171
|
return !gitMarkerStat.isSymbolicLink() && (gitMarkerStat.isFile() || gitMarkerStat.isDirectory());
|
|
2104
2172
|
} catch (error) {
|
|
2105
2173
|
if (error.code === "ENOENT") {
|
|
@@ -2111,7 +2179,7 @@ async function isSubmoduleWorktree(rootDir, submodulePath) {
|
|
|
2111
2179
|
async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
|
|
2112
2180
|
let current = rootDir;
|
|
2113
2181
|
for (const part of submodulePath.split("/")) {
|
|
2114
|
-
current =
|
|
2182
|
+
current = path15.join(current, part);
|
|
2115
2183
|
const partStat = await lstat(current);
|
|
2116
2184
|
if (partStat.isSymbolicLink()) {
|
|
2117
2185
|
return false;
|
|
@@ -2120,8 +2188,8 @@ async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
|
|
|
2120
2188
|
return true;
|
|
2121
2189
|
}
|
|
2122
2190
|
function isInsideDirectory(parent, child) {
|
|
2123
|
-
const relative =
|
|
2124
|
-
return Boolean(relative) && !relative.startsWith("..") && !
|
|
2191
|
+
const relative = path15.relative(parent, child);
|
|
2192
|
+
return Boolean(relative) && !relative.startsWith("..") && !path15.isAbsolute(relative);
|
|
2125
2193
|
}
|
|
2126
2194
|
|
|
2127
2195
|
// src/commands/supermodule-targets.ts
|
|
@@ -2173,7 +2241,7 @@ async function resolveClearTargets(cwd, opts) {
|
|
|
2173
2241
|
return [standaloneTarget(cwd)];
|
|
2174
2242
|
}
|
|
2175
2243
|
const installedSubmodules = await listInstalledSubmodules(cwd);
|
|
2176
|
-
const parent = await pathExists(
|
|
2244
|
+
const parent = await pathExists(path16.join(cwd, EASY_CODING_DIR)) ? parentTarget(cwd, installedSubmodules) : null;
|
|
2177
2245
|
const children = installedSubmodules.map((entry) => childTarget(cwd, entry));
|
|
2178
2246
|
if (opts.submodules === false) {
|
|
2179
2247
|
return parent ? [parent] : [];
|
|
@@ -2213,7 +2281,7 @@ async function listInstalledSubmodules(cwd) {
|
|
|
2213
2281
|
const entries = await listInstallableSubmodules(cwd);
|
|
2214
2282
|
const installed = [];
|
|
2215
2283
|
for (const entry of entries) {
|
|
2216
|
-
if (await pathExists(configPath(
|
|
2284
|
+
if (await pathExists(configPath(path16.join(cwd, entry.path)))) {
|
|
2217
2285
|
installed.push(entry);
|
|
2218
2286
|
}
|
|
2219
2287
|
}
|
|
@@ -2256,7 +2324,7 @@ function parentTargetFromPaths(cwd, submodulePaths) {
|
|
|
2256
2324
|
};
|
|
2257
2325
|
}
|
|
2258
2326
|
function childTarget(cwd, entry) {
|
|
2259
|
-
const dir =
|
|
2327
|
+
const dir = path16.join(cwd, entry.path);
|
|
2260
2328
|
return {
|
|
2261
2329
|
dir,
|
|
2262
2330
|
label: entry.path,
|
|
@@ -2292,11 +2360,11 @@ function parseSubmoduleSelection(submoduleList, available) {
|
|
|
2292
2360
|
return selected.sort((a, b) => a.path.localeCompare(b.path));
|
|
2293
2361
|
}
|
|
2294
2362
|
function configPath(cwd) {
|
|
2295
|
-
return
|
|
2363
|
+
return path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2296
2364
|
}
|
|
2297
2365
|
function toPosixRelative(from, to) {
|
|
2298
|
-
const relative =
|
|
2299
|
-
return relative ? relative.split(
|
|
2366
|
+
const relative = path16.relative(from, to);
|
|
2367
|
+
return relative ? relative.split(path16.sep).join("/") : ".";
|
|
2300
2368
|
}
|
|
2301
2369
|
|
|
2302
2370
|
// src/commands/add-agent.ts
|
|
@@ -2372,7 +2440,7 @@ ${installedLabels.join("\n")}`));
|
|
|
2372
2440
|
|
|
2373
2441
|
// src/commands/clear.ts
|
|
2374
2442
|
import { readdir as readdir4, rm, writeFile } from "fs/promises";
|
|
2375
|
-
import
|
|
2443
|
+
import path17 from "path";
|
|
2376
2444
|
import { cancel as cancel3, confirm as confirm2, outro as outro2 } from "@clack/prompts";
|
|
2377
2445
|
import chalk3 from "chalk";
|
|
2378
2446
|
var PLATFORM_TEMPLATE_DIR = {
|
|
@@ -2411,7 +2479,7 @@ async function clear(opts) {
|
|
|
2411
2479
|
async function buildTargetClearPlans(targets) {
|
|
2412
2480
|
const plans = [];
|
|
2413
2481
|
for (const target of targets) {
|
|
2414
|
-
const easyCodingDir =
|
|
2482
|
+
const easyCodingDir = path17.join(target.dir, EASY_CODING_DIR);
|
|
2415
2483
|
if (!await pathExists(easyCodingDir)) {
|
|
2416
2484
|
continue;
|
|
2417
2485
|
}
|
|
@@ -2428,7 +2496,7 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
|
|
|
2428
2496
|
if (targetPlans.some((targetPlan) => targetPlan.target.label === ".")) {
|
|
2429
2497
|
return;
|
|
2430
2498
|
}
|
|
2431
|
-
if (!await pathExists(
|
|
2499
|
+
if (!await pathExists(path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
2432
2500
|
return;
|
|
2433
2501
|
}
|
|
2434
2502
|
const [parent] = await resolveUpgradeTargets(cwd);
|
|
@@ -2442,7 +2510,7 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
|
|
|
2442
2510
|
await refreshSupermoduleParent(cwd, config2.agents, parent.supermodule.submodules ?? []);
|
|
2443
2511
|
}
|
|
2444
2512
|
async function resolveInstalledAgents(cwd) {
|
|
2445
|
-
const configPath2 =
|
|
2513
|
+
const configPath2 = path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2446
2514
|
if (await pathExists(configPath2)) {
|
|
2447
2515
|
try {
|
|
2448
2516
|
const config2 = await readConfigYaml(configPath2);
|
|
@@ -2473,7 +2541,7 @@ async function hasPlatformInstall(cwd, platform) {
|
|
|
2473
2541
|
async function hasInstallMarkers(cwd, meta) {
|
|
2474
2542
|
const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
|
|
2475
2543
|
for (const marker of markers) {
|
|
2476
|
-
if (await pathExists(
|
|
2544
|
+
if (await pathExists(path17.join(cwd, marker))) {
|
|
2477
2545
|
return true;
|
|
2478
2546
|
}
|
|
2479
2547
|
}
|
|
@@ -2529,7 +2597,7 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
|
|
|
2529
2597
|
}
|
|
2530
2598
|
if (await manifestFileMatches(filePath, file.sha256)) {
|
|
2531
2599
|
plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
|
|
2532
|
-
addEmptyDirChain(plan.emptyDirs,
|
|
2600
|
+
addEmptyDirChain(plan.emptyDirs, path17.dirname(filePath), cwd);
|
|
2533
2601
|
} else {
|
|
2534
2602
|
plan.skippedModified.push(filePath);
|
|
2535
2603
|
}
|
|
@@ -2579,28 +2647,28 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
|
|
|
2579
2647
|
);
|
|
2580
2648
|
for (const meta of metas) {
|
|
2581
2649
|
for (const name of managedSkills) {
|
|
2582
|
-
plan.remove.add(
|
|
2650
|
+
plan.remove.add(path17.join(cwd, meta.skillsDir, name));
|
|
2583
2651
|
}
|
|
2584
2652
|
for (const name of hookFileNames) {
|
|
2585
|
-
plan.remove.add(
|
|
2653
|
+
plan.remove.add(path17.join(cwd, meta.hooksDir, name));
|
|
2586
2654
|
}
|
|
2587
2655
|
for (const name of agentFileNames) {
|
|
2588
|
-
plan.remove.add(
|
|
2656
|
+
plan.remove.add(path17.join(cwd, meta.agentsDir, name));
|
|
2589
2657
|
}
|
|
2590
2658
|
addHookConfigPrune(
|
|
2591
2659
|
plan.pruneHookConfigs,
|
|
2592
2660
|
plan.pruneHookCommands,
|
|
2593
|
-
|
|
2661
|
+
path17.join(cwd, meta.hookConfigFile),
|
|
2594
2662
|
managedHookPathsForTemplate(cwd, meta, hookFileNames),
|
|
2595
2663
|
[]
|
|
2596
2664
|
);
|
|
2597
|
-
plan.constraints.add(
|
|
2665
|
+
plan.constraints.add(path17.join(cwd, meta.mainConstraint));
|
|
2598
2666
|
if (platform === "codex") {
|
|
2599
|
-
plan.remove.add(
|
|
2667
|
+
plan.remove.add(path17.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
|
|
2600
2668
|
}
|
|
2601
|
-
plan.emptyDirs.add(
|
|
2602
|
-
plan.emptyDirs.add(
|
|
2603
|
-
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));
|
|
2604
2672
|
}
|
|
2605
2673
|
}
|
|
2606
2674
|
async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
|
|
@@ -2647,7 +2715,7 @@ function managedHookPathsForRegistration(cwd, hookPath) {
|
|
|
2647
2715
|
try {
|
|
2648
2716
|
paths.push(...managedHookPathTokens(manifestPath(cwd, hookPath)));
|
|
2649
2717
|
} catch {
|
|
2650
|
-
if (
|
|
2718
|
+
if (path17.isAbsolute(hookPath)) {
|
|
2651
2719
|
paths.push(...managedHookPathTokens(hookPath));
|
|
2652
2720
|
}
|
|
2653
2721
|
}
|
|
@@ -2658,7 +2726,7 @@ function managedHookPathsForTemplate(cwd, meta, hookFileNames) {
|
|
|
2658
2726
|
(name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`
|
|
2659
2727
|
);
|
|
2660
2728
|
const absolutePaths = hookFileNames.flatMap(
|
|
2661
|
-
(name) => managedHookPathTokens(
|
|
2729
|
+
(name) => managedHookPathTokens(path17.join(cwd, meta.hooksDir, name))
|
|
2662
2730
|
);
|
|
2663
2731
|
return [...relativePaths, ...absolutePaths];
|
|
2664
2732
|
}
|
|
@@ -2666,8 +2734,8 @@ function managedHookPathTokens(filePath) {
|
|
|
2666
2734
|
const tokens = /* @__PURE__ */ new Set();
|
|
2667
2735
|
for (const equivalentPath of equivalentAbsolutePaths(filePath)) {
|
|
2668
2736
|
const normalized = equivalentPath.replace(/\\/g, "/");
|
|
2669
|
-
const dir =
|
|
2670
|
-
const basename =
|
|
2737
|
+
const dir = path17.posix.dirname(normalized);
|
|
2738
|
+
const basename = path17.posix.basename(normalized);
|
|
2671
2739
|
tokens.add(normalized);
|
|
2672
2740
|
tokens.add(shellDoubleQuoteArg2(normalized));
|
|
2673
2741
|
tokens.add(`${shellDoubleQuoteArg2(dir)}/${basename}`);
|
|
@@ -2675,7 +2743,7 @@ function managedHookPathTokens(filePath) {
|
|
|
2675
2743
|
return [...tokens];
|
|
2676
2744
|
}
|
|
2677
2745
|
function equivalentAbsolutePaths(filePath) {
|
|
2678
|
-
const normalized =
|
|
2746
|
+
const normalized = path17.resolve(filePath).replace(/\\/g, "/");
|
|
2679
2747
|
const equivalents = [normalized];
|
|
2680
2748
|
if (normalized.startsWith("/private/var/")) {
|
|
2681
2749
|
equivalents.push(normalized.replace(/^\/private\/var\//, "/var/"));
|
|
@@ -2749,11 +2817,11 @@ function addRuntimeClearEntries(plan, cwd) {
|
|
|
2749
2817
|
plan.remove = [
|
|
2750
2818
|
.../* @__PURE__ */ new Set([
|
|
2751
2819
|
...plan.remove,
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
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)
|
|
2757
2825
|
])
|
|
2758
2826
|
];
|
|
2759
2827
|
}
|
|
@@ -2761,7 +2829,7 @@ function addEmptyDirChain(emptyDirs, startDir, cwd) {
|
|
|
2761
2829
|
let current = startDir;
|
|
2762
2830
|
while (current !== cwd && isInsideDirectory2(cwd, current)) {
|
|
2763
2831
|
emptyDirs.add(current);
|
|
2764
|
-
const parent =
|
|
2832
|
+
const parent = path17.dirname(current);
|
|
2765
2833
|
if (parent === current) {
|
|
2766
2834
|
break;
|
|
2767
2835
|
}
|
|
@@ -2769,8 +2837,8 @@ function addEmptyDirChain(emptyDirs, startDir, cwd) {
|
|
|
2769
2837
|
}
|
|
2770
2838
|
}
|
|
2771
2839
|
function isInsideDirectory2(parent, child) {
|
|
2772
|
-
const relative =
|
|
2773
|
-
return Boolean(relative) && !relative.startsWith("..") && !
|
|
2840
|
+
const relative = path17.relative(parent, child);
|
|
2841
|
+
return Boolean(relative) && !relative.startsWith("..") && !path17.isAbsolute(relative);
|
|
2774
2842
|
}
|
|
2775
2843
|
async function listSharedHookNamesForPlatform(platform) {
|
|
2776
2844
|
const names = await listFileNames(getTemplatePath("shared-hooks"));
|
|
@@ -2907,7 +2975,7 @@ function sortDirsDeepestFirst(dirs) {
|
|
|
2907
2975
|
});
|
|
2908
2976
|
}
|
|
2909
2977
|
function pathDepth(dir) {
|
|
2910
|
-
return
|
|
2978
|
+
return path17.normalize(dir).split(path17.sep).filter(Boolean).length;
|
|
2911
2979
|
}
|
|
2912
2980
|
async function listDirNames(dir) {
|
|
2913
2981
|
try {
|
|
@@ -2926,7 +2994,7 @@ async function listFileNames(dir) {
|
|
|
2926
2994
|
}
|
|
2927
2995
|
}
|
|
2928
2996
|
function renderPlan(cwd, agents, plan) {
|
|
2929
|
-
const rel = (target) =>
|
|
2997
|
+
const rel = (target) => path17.relative(cwd, target) || target;
|
|
2930
2998
|
const lines = [];
|
|
2931
2999
|
lines.push(chalk3.bold("easy-coding clear"));
|
|
2932
3000
|
lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
|
|
@@ -2975,12 +3043,12 @@ function renderTargetPlans(targetPlans) {
|
|
|
2975
3043
|
}
|
|
2976
3044
|
|
|
2977
3045
|
// src/commands/config.ts
|
|
2978
|
-
import
|
|
3046
|
+
import path20 from "path";
|
|
2979
3047
|
import { cancel as cancel4, confirm as confirm3, outro as outro3, select, text } from "@clack/prompts";
|
|
2980
3048
|
import chalk4 from "chalk";
|
|
2981
3049
|
|
|
2982
3050
|
// src/utils/compare-versions.ts
|
|
2983
|
-
import
|
|
3051
|
+
import path18 from "path";
|
|
2984
3052
|
function parseVersion(version) {
|
|
2985
3053
|
const withoutBuild = String(version ?? "").split("+", 1)[0];
|
|
2986
3054
|
const separator = withoutBuild.indexOf("-");
|
|
@@ -3035,7 +3103,7 @@ function isVersionBehind(installed, current = VERSION) {
|
|
|
3035
3103
|
return compareVersions(installed, current) === -1;
|
|
3036
3104
|
}
|
|
3037
3105
|
async function checkForUpgrade(cwd) {
|
|
3038
|
-
const configPath2 =
|
|
3106
|
+
const configPath2 = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
3039
3107
|
if (!await pathExists(configPath2)) {
|
|
3040
3108
|
return;
|
|
3041
3109
|
}
|
|
@@ -3056,7 +3124,7 @@ async function checkForUpgrade(cwd) {
|
|
|
3056
3124
|
|
|
3057
3125
|
// src/utils/tdd-readiness.ts
|
|
3058
3126
|
import { readFile as readFile4, realpath as realpath2 } from "fs/promises";
|
|
3059
|
-
import
|
|
3127
|
+
import path19 from "path";
|
|
3060
3128
|
var TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1";
|
|
3061
3129
|
var TDD_READINESS_SCOPE = "changed-production-lines";
|
|
3062
3130
|
var TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA";
|
|
@@ -3064,7 +3132,7 @@ var TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD";
|
|
|
3064
3132
|
var COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py";
|
|
3065
3133
|
var JAVA_BUILD_FILE_NAMES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
|
|
3066
3134
|
function readinessPath(root) {
|
|
3067
|
-
return
|
|
3135
|
+
return path19.join(root, EASY_CODING_DIR, TDD_DIR, TDD_READINESS_FILE);
|
|
3068
3136
|
}
|
|
3069
3137
|
function parseFileRecords(value, field, reasons) {
|
|
3070
3138
|
if (!Array.isArray(value) || value.length === 0) {
|
|
@@ -3078,7 +3146,7 @@ function parseFileRecords(value, field, reasons) {
|
|
|
3078
3146
|
continue;
|
|
3079
3147
|
}
|
|
3080
3148
|
const record = item;
|
|
3081
|
-
if (typeof record.path !== "string" || !record.path.trim() ||
|
|
3149
|
+
if (typeof record.path !== "string" || !record.path.trim() || path19.isAbsolute(record.path)) {
|
|
3082
3150
|
reasons.push(`${field} contains an invalid path`);
|
|
3083
3151
|
continue;
|
|
3084
3152
|
}
|
|
@@ -3092,15 +3160,15 @@ function usesRequiredGateVariables(command) {
|
|
|
3092
3160
|
}
|
|
3093
3161
|
function isSafeReportPattern(value) {
|
|
3094
3162
|
const normalized = value.replaceAll("\\", "/");
|
|
3095
|
-
return !
|
|
3163
|
+
return !path19.isAbsolute(value) && !normalized.split("/").includes("..");
|
|
3096
3164
|
}
|
|
3097
3165
|
async function validateFiles(root, records, reasons) {
|
|
3098
3166
|
const resolvedRoot = await realpath2(root);
|
|
3099
3167
|
for (const record of records) {
|
|
3100
|
-
const absolute =
|
|
3168
|
+
const absolute = path19.resolve(root, record.path);
|
|
3101
3169
|
try {
|
|
3102
3170
|
const resolved = await realpath2(absolute);
|
|
3103
|
-
if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${
|
|
3171
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path19.sep}`)) {
|
|
3104
3172
|
reasons.push(`readiness file escapes project root: ${record.path}`);
|
|
3105
3173
|
continue;
|
|
3106
3174
|
}
|
|
@@ -3149,7 +3217,7 @@ async function inspectTddReadiness(root) {
|
|
|
3149
3217
|
}
|
|
3150
3218
|
const buildFiles = parseFileRecords(manifest.build_files, "build_files", reasons);
|
|
3151
3219
|
const toolFiles = parseFileRecords(manifest.tool_files, "tool_files", reasons);
|
|
3152
|
-
if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(
|
|
3220
|
+
if (!buildFiles.some((record) => JAVA_BUILD_FILE_NAMES.has(path19.basename(record.path)))) {
|
|
3153
3221
|
reasons.push("build_files must include a Maven or Gradle Java build file");
|
|
3154
3222
|
}
|
|
3155
3223
|
if (!toolFiles.some((record) => record.path.replaceAll("\\", "/") === COVERAGE_TOOL_PATH)) {
|
|
@@ -3165,9 +3233,17 @@ async function inspectTddReadiness(root) {
|
|
|
3165
3233
|
}
|
|
3166
3234
|
|
|
3167
3235
|
// src/commands/config.ts
|
|
3168
|
-
async function config() {
|
|
3236
|
+
async function config(options = {}) {
|
|
3169
3237
|
renderBanner();
|
|
3170
|
-
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);
|
|
3171
3247
|
if (!await pathExists(configPath2)) {
|
|
3172
3248
|
throw new Error("No easy-coding harness found in this project.");
|
|
3173
3249
|
}
|
|
@@ -3188,6 +3264,10 @@ async function config() {
|
|
|
3188
3264
|
`Project harness ${projectConfig.harness_version} does not exactly match CLI ${VERSION}. Upgrade the harness or update the CLI before changing config.`
|
|
3189
3265
|
);
|
|
3190
3266
|
}
|
|
3267
|
+
if (explicit) {
|
|
3268
|
+
await configureOverrides(configPath2, scope, options, true);
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3191
3271
|
const current = resolveLegacyBehavior(projectConfig);
|
|
3192
3272
|
const approvalMode = await select({
|
|
3193
3273
|
message: `Select project approval mode (current: ${current.approvalMode})`,
|
|
@@ -3258,8 +3338,23 @@ async function config() {
|
|
|
3258
3338
|
}
|
|
3259
3339
|
utCoverageThreshold = Number(thresholdInput);
|
|
3260
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;
|
|
3355
|
+
}
|
|
3261
3356
|
const shouldSave = await confirm3({
|
|
3262
|
-
message: `Set approval=${approvalMode},
|
|
3357
|
+
message: `Set approval=${approvalMode}, cooperate=${cooperateMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}?`,
|
|
3263
3358
|
initialValue: true
|
|
3264
3359
|
});
|
|
3265
3360
|
if (typeof shouldSave === "symbol" || !shouldSave) {
|
|
@@ -3275,29 +3370,116 @@ async function config() {
|
|
|
3275
3370
|
return;
|
|
3276
3371
|
}
|
|
3277
3372
|
}
|
|
3278
|
-
await setBehaviorModes(
|
|
3373
|
+
await setBehaviorModes(
|
|
3374
|
+
configPath2,
|
|
3375
|
+
approvalMode,
|
|
3376
|
+
workflowMode,
|
|
3377
|
+
unitTestMode,
|
|
3378
|
+
utCoverageThreshold,
|
|
3379
|
+
cooperateMode
|
|
3380
|
+
);
|
|
3279
3381
|
outro3(
|
|
3280
3382
|
chalk4.green(
|
|
3281
3383
|
`Project modes updated: approval=${approvalMode}, workflow=${workflowMode}, unit-test=${unitTestMode}${unitTestMode === "none" ? "" : ` (${utCoverageThreshold}%)`}.`
|
|
3282
3384
|
)
|
|
3283
3385
|
);
|
|
3284
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
|
+
}
|
|
3285
3467
|
|
|
3286
3468
|
// src/commands/init.ts
|
|
3287
|
-
import
|
|
3469
|
+
import path22 from "path";
|
|
3288
3470
|
import { note, outro as outro4 } from "@clack/prompts";
|
|
3289
3471
|
import chalk5 from "chalk";
|
|
3290
3472
|
|
|
3291
3473
|
// src/utils/install-state.ts
|
|
3292
3474
|
import { readdir as readdir5 } from "fs/promises";
|
|
3293
|
-
import
|
|
3475
|
+
import path21 from "path";
|
|
3294
3476
|
var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
|
|
3295
3477
|
async function detectEasyCodingInstallState(cwd) {
|
|
3296
|
-
const easyCodingDir =
|
|
3478
|
+
const easyCodingDir = path21.join(cwd, EASY_CODING_DIR);
|
|
3297
3479
|
if (!await pathExists(easyCodingDir)) {
|
|
3298
3480
|
return { kind: "fresh", easyCodingDir };
|
|
3299
3481
|
}
|
|
3300
|
-
const configPath2 =
|
|
3482
|
+
const configPath2 = path21.join(easyCodingDir, CONFIG_FILE);
|
|
3301
3483
|
if (await pathExists(configPath2)) {
|
|
3302
3484
|
return { kind: "installed", easyCodingDir, configPath: configPath2 };
|
|
3303
3485
|
}
|
|
@@ -3315,17 +3497,17 @@ async function detectEasyCodingInstallState(cwd) {
|
|
|
3315
3497
|
async function detectLegacyAssets(easyCodingDir) {
|
|
3316
3498
|
const assets = [];
|
|
3317
3499
|
for (const file of LEGACY_ROOT_FILES) {
|
|
3318
|
-
if (await pathExists(
|
|
3500
|
+
if (await pathExists(path21.join(easyCodingDir, file))) {
|
|
3319
3501
|
assets.push(relativeEasyCodingPath(file));
|
|
3320
3502
|
}
|
|
3321
3503
|
}
|
|
3322
|
-
if (await pathExists(
|
|
3504
|
+
if (await pathExists(path21.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
3323
3505
|
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
3324
3506
|
}
|
|
3325
|
-
const shortMemoryFiles = await listMarkdownFiles(
|
|
3507
|
+
const shortMemoryFiles = await listMarkdownFiles(path21.join(easyCodingDir, "memory", "short"));
|
|
3326
3508
|
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
3327
3509
|
for (const dir of ["spec", "prototype"]) {
|
|
3328
|
-
if (await hasAnyDirectoryEntry(
|
|
3510
|
+
if (await hasAnyDirectoryEntry(path21.join(easyCodingDir, dir))) {
|
|
3329
3511
|
assets.push(relativeEasyCodingPath(dir));
|
|
3330
3512
|
}
|
|
3331
3513
|
}
|
|
@@ -3345,13 +3527,13 @@ async function hasAnyDirectoryEntry(dir) {
|
|
|
3345
3527
|
return (await readdir5(dir)).length > 0;
|
|
3346
3528
|
}
|
|
3347
3529
|
function relativeEasyCodingPath(...segments) {
|
|
3348
|
-
return
|
|
3530
|
+
return path21.posix.join(EASY_CODING_DIR, ...segments);
|
|
3349
3531
|
}
|
|
3350
3532
|
function relativeConfigPath() {
|
|
3351
|
-
return
|
|
3533
|
+
return path21.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
3352
3534
|
}
|
|
3353
3535
|
function relativeProjectInitTaskPath() {
|
|
3354
|
-
return
|
|
3536
|
+
return path21.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
3355
3537
|
}
|
|
3356
3538
|
|
|
3357
3539
|
// src/commands/init.ts
|
|
@@ -3432,7 +3614,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
|
|
|
3432
3614
|
if (!targetSubmodulePaths.has(entry.path)) {
|
|
3433
3615
|
continue;
|
|
3434
3616
|
}
|
|
3435
|
-
const dir =
|
|
3617
|
+
const dir = path22.join(cwd, entry.path);
|
|
3436
3618
|
targets.push(
|
|
3437
3619
|
await targetFromState(dir, entry.path, "submodule-child", {
|
|
3438
3620
|
parent: toPosixRelative2(dir, cwd)
|
|
@@ -3479,24 +3661,24 @@ function contextFromState(role, installState) {
|
|
|
3479
3661
|
};
|
|
3480
3662
|
}
|
|
3481
3663
|
function toPosixRelative2(from, to) {
|
|
3482
|
-
const relative =
|
|
3483
|
-
return relative ? relative.split(
|
|
3664
|
+
const relative = path22.relative(from, to);
|
|
3665
|
+
return relative ? relative.split(path22.sep).join("/") : ".";
|
|
3484
3666
|
}
|
|
3485
3667
|
async function resolveInitPlatforms(cwd, opts, parentInstalled) {
|
|
3486
3668
|
if (opts.agent || !parentInstalled) {
|
|
3487
3669
|
return resolvePlatforms(opts, ["claude-code"]);
|
|
3488
3670
|
}
|
|
3489
|
-
const config2 = await readConfigYaml(
|
|
3671
|
+
const config2 = await readConfigYaml(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
3490
3672
|
if (Array.isArray(config2.agents) && config2.agents.length > 0) {
|
|
3491
3673
|
return config2.agents;
|
|
3492
3674
|
}
|
|
3493
3675
|
return resolvePlatforms(opts, ["claude-code"]);
|
|
3494
3676
|
}
|
|
3495
3677
|
async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
|
|
3496
|
-
if (!await pathExists(
|
|
3678
|
+
if (!await pathExists(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
3497
3679
|
return;
|
|
3498
3680
|
}
|
|
3499
|
-
const config2 = parentTarget2.installed ? await readConfigYaml(
|
|
3681
|
+
const config2 = parentTarget2.installed ? await readConfigYaml(path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
|
|
3500
3682
|
const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
|
|
3501
3683
|
await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
|
|
3502
3684
|
}
|
|
@@ -3505,7 +3687,7 @@ async function refreshInstalledChildTopologies(targets) {
|
|
|
3505
3687
|
if (!target.installed || target.context.role !== "submodule-child") {
|
|
3506
3688
|
continue;
|
|
3507
3689
|
}
|
|
3508
|
-
const configPath2 =
|
|
3690
|
+
const configPath2 = path22.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
|
|
3509
3691
|
if (!await pathExists(configPath2)) {
|
|
3510
3692
|
continue;
|
|
3511
3693
|
}
|
|
@@ -3514,12 +3696,12 @@ async function refreshInstalledChildTopologies(targets) {
|
|
|
3514
3696
|
}
|
|
3515
3697
|
|
|
3516
3698
|
// src/commands/status.ts
|
|
3517
|
-
import
|
|
3699
|
+
import path24 from "path";
|
|
3518
3700
|
import chalk6 from "chalk";
|
|
3519
3701
|
|
|
3520
3702
|
// src/utils/session.ts
|
|
3521
3703
|
import { readdir as readdir6, stat as stat3, unlink as unlink2 } from "fs/promises";
|
|
3522
|
-
import
|
|
3704
|
+
import path23 from "path";
|
|
3523
3705
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3524
3706
|
var IDLE_SESSION_RETENTION_MS = 7 * DAY_MS;
|
|
3525
3707
|
var ATTACHED_SESSION_RETENTION_MS = 30 * DAY_MS;
|
|
@@ -3533,7 +3715,7 @@ function parseSessionFile(content) {
|
|
|
3533
3715
|
}
|
|
3534
3716
|
}
|
|
3535
3717
|
function getSessionDir(cwd) {
|
|
3536
|
-
return
|
|
3718
|
+
return path23.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
3537
3719
|
}
|
|
3538
3720
|
async function listSessionFiles(cwd) {
|
|
3539
3721
|
const dir = getSessionDir(cwd);
|
|
@@ -3545,7 +3727,7 @@ async function listSessionFiles(cwd) {
|
|
|
3545
3727
|
if (!name.endsWith(".json")) {
|
|
3546
3728
|
continue;
|
|
3547
3729
|
}
|
|
3548
|
-
const filePath =
|
|
3730
|
+
const filePath = path23.join(dir, name);
|
|
3549
3731
|
const content = await readTextIfExists(filePath);
|
|
3550
3732
|
if (!content) {
|
|
3551
3733
|
continue;
|
|
@@ -3610,7 +3792,7 @@ async function listSessionCleanupCandidates(cwd) {
|
|
|
3610
3792
|
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
3611
3793
|
continue;
|
|
3612
3794
|
}
|
|
3613
|
-
const filePath =
|
|
3795
|
+
const filePath = path23.join(dir, entry.name);
|
|
3614
3796
|
try {
|
|
3615
3797
|
const [content, fileStat] = await Promise.all([readTextFile(filePath), stat3(filePath)]);
|
|
3616
3798
|
const session = parseSessionFile(content);
|
|
@@ -3645,7 +3827,7 @@ async function unlinkIfUnchanged(candidate) {
|
|
|
3645
3827
|
}
|
|
3646
3828
|
}
|
|
3647
3829
|
async function cleanOrphanAcceptanceSnapshots(cwd) {
|
|
3648
|
-
const acceptanceDir =
|
|
3830
|
+
const acceptanceDir = path23.join(getSessionDir(cwd), "acceptance");
|
|
3649
3831
|
if (!await pathExists(acceptanceDir)) {
|
|
3650
3832
|
return 0;
|
|
3651
3833
|
}
|
|
@@ -3654,7 +3836,7 @@ async function cleanOrphanAcceptanceSnapshots(cwd) {
|
|
|
3654
3836
|
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
3655
3837
|
continue;
|
|
3656
3838
|
}
|
|
3657
|
-
const snapshotPath =
|
|
3839
|
+
const snapshotPath = path23.join(acceptanceDir, entry.name);
|
|
3658
3840
|
if (!await isOrphanAcceptanceSnapshot(cwd, snapshotPath, entry.name.slice(0, -5))) {
|
|
3659
3841
|
continue;
|
|
3660
3842
|
}
|
|
@@ -3673,7 +3855,7 @@ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
|
|
|
3673
3855
|
let taskContent;
|
|
3674
3856
|
try {
|
|
3675
3857
|
taskContent = await readTextFile(
|
|
3676
|
-
|
|
3858
|
+
path23.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json")
|
|
3677
3859
|
);
|
|
3678
3860
|
} catch (error) {
|
|
3679
3861
|
if (isFileNotFound(error)) {
|
|
@@ -3695,7 +3877,7 @@ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
|
|
|
3695
3877
|
return true;
|
|
3696
3878
|
}
|
|
3697
3879
|
const checkpoint = task.quality_checkpoint ?? task.verification_checkpoint;
|
|
3698
|
-
return typeof checkpoint?.snapshot_file !== "string" ||
|
|
3880
|
+
return typeof checkpoint?.snapshot_file !== "string" || path23.resolve(cwd, checkpoint.snapshot_file) !== path23.resolve(snapshotPath);
|
|
3699
3881
|
}
|
|
3700
3882
|
function isFileNotFound(error) {
|
|
3701
3883
|
return error.code === "ENOENT";
|
|
@@ -3705,7 +3887,7 @@ function isFileNotFound(error) {
|
|
|
3705
3887
|
async function status() {
|
|
3706
3888
|
renderBanner();
|
|
3707
3889
|
const cwd = process.cwd();
|
|
3708
|
-
const configPath2 =
|
|
3890
|
+
const configPath2 = path24.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
3709
3891
|
if (!await pathExists(configPath2)) {
|
|
3710
3892
|
throw new Error("No easy-coding harness found in this project.");
|
|
3711
3893
|
}
|
|
@@ -3732,9 +3914,20 @@ async function status() {
|
|
|
3732
3914
|
const projectWorkflowMode = isConfiguredWorkflowMode(config2.behavior?.workflow_mode) ? config2.behavior.workflow_mode : migratedBehavior.workflowMode;
|
|
3733
3915
|
const projectUnitTestMode = migratedBehavior.unitTestMode;
|
|
3734
3916
|
const projectUtCoverageThreshold = migratedBehavior.utCoverageThreshold;
|
|
3735
|
-
const
|
|
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"));
|
|
3736
3926
|
const readiness = needsCoverage ? await inspectTddReadiness(cwd) : { status: "not_checked", reasons: [] };
|
|
3737
3927
|
console.log(` approval_mode: ${projectApprovalMode}`);
|
|
3928
|
+
console.log(
|
|
3929
|
+
` cooperate_mode: ${effective.values.cooperate_mode} (${effective.sources.cooperate_mode})`
|
|
3930
|
+
);
|
|
3738
3931
|
console.log(` workflow_mode: ${projectWorkflowMode}`);
|
|
3739
3932
|
console.log(` unit_test_mode: ${projectUnitTestMode}`);
|
|
3740
3933
|
console.log(` ut_coverage_threshold: ${projectUtCoverageThreshold}`);
|
|
@@ -3748,11 +3941,16 @@ async function status() {
|
|
|
3748
3941
|
console.log(` project_workflow_mode: ${projectWorkflowMode}`);
|
|
3749
3942
|
console.log(` project_unit_test_mode: ${projectUnitTestMode}`);
|
|
3750
3943
|
console.log(` project_ut_coverage_threshold: ${projectUtCoverageThreshold}`);
|
|
3751
|
-
console.log(`
|
|
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
|
+
);
|
|
3752
3948
|
console.log(` configured_workflow_mode: ${projectWorkflowMode} (without a session override)`);
|
|
3753
|
-
console.log(` effective_unit_test_mode: ${projectUnitTestMode} (without a session override)`);
|
|
3754
3949
|
console.log(
|
|
3755
|
-
`
|
|
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})`
|
|
3756
3954
|
);
|
|
3757
3955
|
if (sessions.length === 0) {
|
|
3758
3956
|
console.log(" no session files");
|
|
@@ -3766,19 +3964,26 @@ async function status() {
|
|
|
3766
3964
|
const sessionWorkflowMode = session.workflow_mode ?? (legacySessionMode === "lite" ? "fast" : hasLegacySessionMode ? "adaptive" : void 0);
|
|
3767
3965
|
const sessionUnitTestMode = session.unit_test_mode;
|
|
3768
3966
|
const sessionUtCoverageThreshold = session.ut_coverage_threshold;
|
|
3967
|
+
const resolved = resolveBehaviorSettings(projectBehavior, localBehavior, {
|
|
3968
|
+
...session,
|
|
3969
|
+
approval_mode: sessionApprovalMode
|
|
3970
|
+
});
|
|
3769
3971
|
console.log(` - ${key}`);
|
|
3770
3972
|
console.log(` agent: ${session.agent ?? "legacy/unknown"}`);
|
|
3771
3973
|
console.log(` source: ${session.session_source ?? "legacy"}`);
|
|
3772
|
-
console.log(` approval_mode: ${sessionApprovalMode ?? "project
|
|
3974
|
+
console.log(` approval_mode: ${sessionApprovalMode ?? "inherit local/project"}`);
|
|
3773
3975
|
console.log(` workflow_mode: ${sessionWorkflowMode ?? "project default"}`);
|
|
3774
|
-
console.log(` unit_test_mode: ${sessionUnitTestMode ?? "project
|
|
3775
|
-
console.log(` ut_coverage_threshold: ${sessionUtCoverageThreshold ?? "project default"}`);
|
|
3776
|
-
console.log(` effective_approval_mode: ${sessionApprovalMode ?? projectApprovalMode}`);
|
|
3777
|
-
console.log(` configured_workflow_mode: ${sessionWorkflowMode ?? projectWorkflowMode}`);
|
|
3778
|
-
console.log(` effective_unit_test_mode: ${sessionUnitTestMode ?? projectUnitTestMode}`);
|
|
3976
|
+
console.log(` unit_test_mode: ${sessionUnitTestMode ?? "inherit local/project"}`);
|
|
3779
3977
|
console.log(
|
|
3780
|
-
`
|
|
3978
|
+
` ut_coverage_threshold: ${sessionUtCoverageThreshold ?? "inherit local/project"}`
|
|
3781
3979
|
);
|
|
3980
|
+
console.log(` effective_approval_mode: ${resolved.values.approval_mode}`);
|
|
3981
|
+
console.log(
|
|
3982
|
+
` cooperate_mode: ${resolved.values.cooperate_mode} (${resolved.sources.cooperate_mode})`
|
|
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}`);
|
|
3782
3987
|
console.log(
|
|
3783
3988
|
` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
|
|
3784
3989
|
);
|
|
@@ -3851,7 +4056,7 @@ async function update(opts) {
|
|
|
3851
4056
|
}
|
|
3852
4057
|
|
|
3853
4058
|
// src/commands/upgrade.ts
|
|
3854
|
-
import
|
|
4059
|
+
import path25 from "path";
|
|
3855
4060
|
import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
|
|
3856
4061
|
import chalk8 from "chalk";
|
|
3857
4062
|
var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
|
|
@@ -4025,7 +4230,7 @@ async function needsHookConfigRefresh(target, config2) {
|
|
|
4025
4230
|
const manifest = await readInstallManifest(target.dir);
|
|
4026
4231
|
for (const agent of config2.agents) {
|
|
4027
4232
|
const meta = resolvePlatformMeta(target.dir, agent);
|
|
4028
|
-
const configPath2 =
|
|
4233
|
+
const configPath2 = path25.join(target.dir, meta.hookConfigFile);
|
|
4029
4234
|
const content = await readTextIfExists(configPath2);
|
|
4030
4235
|
if (content === null) {
|
|
4031
4236
|
return true;
|
|
@@ -4165,7 +4370,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
|
|
|
4165
4370
|
return true;
|
|
4166
4371
|
}
|
|
4167
4372
|
return pathAliases(
|
|
4168
|
-
normalizePathForHookComparison(
|
|
4373
|
+
normalizePathForHookComparison(path25.resolve(cwd, relativeHookPath))
|
|
4169
4374
|
).includes(normalizedHookPath);
|
|
4170
4375
|
});
|
|
4171
4376
|
}
|
|
@@ -4255,7 +4460,7 @@ program.command("init").description("Initialize easy-coding harness in current p
|
|
|
4255
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));
|
|
4256
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));
|
|
4257
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));
|
|
4258
|
-
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));
|
|
4259
4464
|
program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
|
|
4260
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));
|
|
4261
4466
|
program.parse();
|