easy-coding-harness 0.6.0 → 0.7.0
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 +26 -0
- package/README.md +24 -8
- package/dist/cli.js +309 -168
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +3 -0
- package/templates/codex/agents/ec-implementer.toml +3 -0
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +7 -3
- package/templates/common/skills/ec-analysis/SKILL.md +83 -48
- package/templates/common/skills/ec-brainstorming/SKILL.md +1 -1
- package/templates/common/skills/ec-git/SKILL.md +1 -1
- package/templates/common/skills/ec-implementing/SKILL.md +41 -13
- package/templates/common/skills/ec-memory/SKILL.md +11 -7
- package/templates/common/skills/ec-no-harness/SKILL.md +39 -0
- package/templates/common/skills/ec-reviewing/SKILL.md +6 -5
- package/templates/common/skills/ec-task-management/SKILL.md +24 -2
- package/templates/common/skills/ec-verification/SKILL.md +23 -19
- package/templates/common/skills/ec-workflow/SKILL.md +98 -50
- package/templates/main-constraint/AGENTS.md.tpl +21 -14
- package/templates/main-constraint/CLAUDE.md.tpl +21 -14
- package/templates/qoder/agents/ec-implementer.md +3 -0
- package/templates/runtime/templates/dev-spec-skeleton.md +41 -46
- package/templates/shared-hooks/easy_coding_state.py +690 -16
- package/templates/shared-hooks/inject-subagent-context.py +6 -2
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk9 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/add-agent.ts
|
|
@@ -103,9 +103,11 @@ async function isDirectory(filePath) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// src/utils/config-yaml.ts
|
|
106
|
+
var CONFIG_SCHEMA_VERSION = 2;
|
|
107
|
+
var CONFIRM_MODES = ["approve", "guard", "auto"];
|
|
106
108
|
function createDefaultConfig(params) {
|
|
107
|
-
const
|
|
108
|
-
version:
|
|
109
|
+
const config2 = {
|
|
110
|
+
version: CONFIG_SCHEMA_VERSION,
|
|
109
111
|
harness_version: params.harnessVersion,
|
|
110
112
|
agents: params.agents,
|
|
111
113
|
project: {
|
|
@@ -121,20 +123,19 @@ function createDefaultConfig(params) {
|
|
|
121
123
|
auto_archive_days: 30
|
|
122
124
|
},
|
|
123
125
|
behavior: {
|
|
124
|
-
|
|
125
|
-
auto_mode: false
|
|
126
|
+
confirm_mode: "guard"
|
|
126
127
|
}
|
|
127
128
|
};
|
|
128
129
|
if (params.supermodule) {
|
|
129
|
-
|
|
130
|
+
config2.supermodule = params.supermodule;
|
|
130
131
|
}
|
|
131
|
-
return
|
|
132
|
+
return config2;
|
|
132
133
|
}
|
|
133
|
-
function stringifyConfig(
|
|
134
|
-
return YAML.stringify(
|
|
134
|
+
function stringifyConfig(config2) {
|
|
135
|
+
return YAML.stringify(config2);
|
|
135
136
|
}
|
|
136
|
-
async function writeConfigYaml(filePath,
|
|
137
|
-
await writeTextFile(filePath, stringifyConfig(
|
|
137
|
+
async function writeConfigYaml(filePath, config2) {
|
|
138
|
+
await writeTextFile(filePath, stringifyConfig(config2));
|
|
138
139
|
}
|
|
139
140
|
async function readConfigYaml(filePath) {
|
|
140
141
|
const content = await readFile2(filePath, "utf8");
|
|
@@ -142,8 +143,8 @@ async function readConfigYaml(filePath) {
|
|
|
142
143
|
}
|
|
143
144
|
async function readProjectIdIfExists(filePath) {
|
|
144
145
|
try {
|
|
145
|
-
const
|
|
146
|
-
return typeof
|
|
146
|
+
const config2 = await readConfigYaml(filePath);
|
|
147
|
+
return typeof config2.project?.id === "string" && config2.project.id.trim() ? config2.project.id : null;
|
|
147
148
|
} catch {
|
|
148
149
|
return null;
|
|
149
150
|
}
|
|
@@ -151,41 +152,74 @@ async function readProjectIdIfExists(filePath) {
|
|
|
151
152
|
async function updateConfigYaml(filePath, updater) {
|
|
152
153
|
const content = await readFile2(filePath, "utf8");
|
|
153
154
|
const document = parseDocument(content);
|
|
154
|
-
const
|
|
155
|
-
updater(
|
|
156
|
-
for (const [key, value] of Object.entries(
|
|
155
|
+
const config2 = document.toJSON();
|
|
156
|
+
updater(config2);
|
|
157
|
+
for (const [key, value] of Object.entries(config2)) {
|
|
157
158
|
document.set(key, value);
|
|
158
159
|
}
|
|
159
160
|
await writeTextFile(filePath, document.toString());
|
|
160
|
-
return
|
|
161
|
+
return config2;
|
|
161
162
|
}
|
|
162
163
|
async function addAgentsToConfig(filePath, agents) {
|
|
163
|
-
return updateConfigYaml(filePath, (
|
|
164
|
-
const merged = /* @__PURE__ */ new Set([...
|
|
165
|
-
|
|
164
|
+
return updateConfigYaml(filePath, (config2) => {
|
|
165
|
+
const merged = /* @__PURE__ */ new Set([...config2.agents ?? [], ...agents]);
|
|
166
|
+
config2.agents = [...merged];
|
|
166
167
|
});
|
|
167
168
|
}
|
|
168
169
|
async function updateHarnessVersion(filePath, version) {
|
|
169
|
-
return updateConfigYaml(filePath, (
|
|
170
|
-
|
|
170
|
+
return updateConfigYaml(filePath, (config2) => {
|
|
171
|
+
config2.harness_version = version;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function isConfirmMode(value) {
|
|
175
|
+
return typeof value === "string" && CONFIRM_MODES.includes(value);
|
|
176
|
+
}
|
|
177
|
+
function resolveLegacyConfirmMode(config2) {
|
|
178
|
+
const behavior = config2.behavior ?? {};
|
|
179
|
+
if (isConfirmMode(behavior.confirm_mode)) {
|
|
180
|
+
return behavior.confirm_mode;
|
|
181
|
+
}
|
|
182
|
+
if (behavior.auto_mode === true) {
|
|
183
|
+
return "auto";
|
|
184
|
+
}
|
|
185
|
+
if (behavior.strict_confirm === true) {
|
|
186
|
+
return "approve";
|
|
187
|
+
}
|
|
188
|
+
return "guard";
|
|
189
|
+
}
|
|
190
|
+
async function setConfirmMode(filePath, mode) {
|
|
191
|
+
return updateConfigYaml(filePath, (config2) => {
|
|
192
|
+
const legacyBehavior = config2.behavior ?? {};
|
|
193
|
+
const behavior = Object.fromEntries(
|
|
194
|
+
Object.entries(legacyBehavior).filter(
|
|
195
|
+
([key]) => key !== "strict_confirm" && key !== "auto_mode"
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
behavior.confirm_mode = mode;
|
|
199
|
+
config2.behavior = behavior;
|
|
200
|
+
config2.version = CONFIG_SCHEMA_VERSION;
|
|
171
201
|
});
|
|
172
202
|
}
|
|
203
|
+
async function migrateConfirmModeConfig(filePath) {
|
|
204
|
+
const config2 = await readConfigYaml(filePath);
|
|
205
|
+
return setConfirmMode(filePath, resolveLegacyConfirmMode(config2));
|
|
206
|
+
}
|
|
173
207
|
async function ensureProjectId(filePath) {
|
|
174
208
|
let projectId = "";
|
|
175
|
-
await updateConfigYaml(filePath, (
|
|
176
|
-
if (!
|
|
177
|
-
|
|
209
|
+
await updateConfigYaml(filePath, (config2) => {
|
|
210
|
+
if (!config2.project || typeof config2.project !== "object") {
|
|
211
|
+
config2.project = { id: createProjectId(), name: "" };
|
|
178
212
|
}
|
|
179
|
-
if (typeof
|
|
180
|
-
|
|
213
|
+
if (typeof config2.project.id !== "string" || !config2.project.id.trim()) {
|
|
214
|
+
config2.project.id = createProjectId();
|
|
181
215
|
}
|
|
182
|
-
projectId =
|
|
216
|
+
projectId = config2.project.id;
|
|
183
217
|
});
|
|
184
218
|
return projectId;
|
|
185
219
|
}
|
|
186
220
|
async function updateSupermoduleConfig(filePath, supermodule) {
|
|
187
|
-
return updateConfigYaml(filePath, (
|
|
188
|
-
|
|
221
|
+
return updateConfigYaml(filePath, (config2) => {
|
|
222
|
+
config2.supermodule = supermodule;
|
|
189
223
|
});
|
|
190
224
|
}
|
|
191
225
|
function createProjectId() {
|
|
@@ -1800,11 +1834,11 @@ async function listParentManagedSubmodulePaths(cwd) {
|
|
|
1800
1834
|
return [];
|
|
1801
1835
|
}
|
|
1802
1836
|
try {
|
|
1803
|
-
const
|
|
1804
|
-
if (
|
|
1837
|
+
const config2 = await readConfigYaml(parentConfigPath);
|
|
1838
|
+
if (config2.supermodule?.role !== "super-parent" || !Array.isArray(config2.supermodule.submodules)) {
|
|
1805
1839
|
return [];
|
|
1806
1840
|
}
|
|
1807
|
-
return
|
|
1841
|
+
return config2.supermodule.submodules.filter((submodulePath) => typeof submodulePath === "string").sort();
|
|
1808
1842
|
} catch {
|
|
1809
1843
|
return [];
|
|
1810
1844
|
}
|
|
@@ -1889,9 +1923,9 @@ async function addAgent(opts) {
|
|
|
1889
1923
|
if (!await pathExists(target.configPath)) {
|
|
1890
1924
|
continue;
|
|
1891
1925
|
}
|
|
1892
|
-
const
|
|
1893
|
-
const toInstall = platforms.filter((platform) => !
|
|
1894
|
-
const agents = [...
|
|
1926
|
+
const config2 = await readConfigYaml(target.configPath);
|
|
1927
|
+
const toInstall = platforms.filter((platform) => !config2.agents.includes(platform));
|
|
1928
|
+
const agents = [...config2.agents, ...toInstall];
|
|
1895
1929
|
if (toInstall.length > 0) {
|
|
1896
1930
|
const projectId = await writeRuntimeScaffold(target.dir, agents, {
|
|
1897
1931
|
supermodule: target.supermodule
|
|
@@ -2000,18 +2034,18 @@ async function refreshParentAfterChildClear(cwd, targetPlans) {
|
|
|
2000
2034
|
if (!parent || parent.label !== ".") {
|
|
2001
2035
|
return;
|
|
2002
2036
|
}
|
|
2003
|
-
const
|
|
2004
|
-
if (!Array.isArray(
|
|
2037
|
+
const config2 = await readConfigYaml(parent.configPath);
|
|
2038
|
+
if (!Array.isArray(config2.agents) || config2.agents.length === 0) {
|
|
2005
2039
|
return;
|
|
2006
2040
|
}
|
|
2007
|
-
await refreshSupermoduleParent(cwd,
|
|
2041
|
+
await refreshSupermoduleParent(cwd, config2.agents, parent.supermodule.submodules ?? []);
|
|
2008
2042
|
}
|
|
2009
2043
|
async function resolveInstalledAgents(cwd) {
|
|
2010
2044
|
const configPath2 = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2011
2045
|
if (await pathExists(configPath2)) {
|
|
2012
2046
|
try {
|
|
2013
|
-
const
|
|
2014
|
-
const listed = Array.isArray(
|
|
2047
|
+
const config2 = await readConfigYaml(configPath2);
|
|
2048
|
+
const listed = Array.isArray(config2.agents) ? config2.agents.filter(isAgentPlatform) : [];
|
|
2015
2049
|
if (listed.length > 0) {
|
|
2016
2050
|
return listed;
|
|
2017
2051
|
}
|
|
@@ -2352,8 +2386,8 @@ async function executeClearPlan(plan) {
|
|
|
2352
2386
|
for (const target of plan.remove) {
|
|
2353
2387
|
await rm(target, { recursive: true, force: true });
|
|
2354
2388
|
}
|
|
2355
|
-
for (const
|
|
2356
|
-
await pruneHookConfig(
|
|
2389
|
+
for (const config2 of plan.pruneHookConfigs) {
|
|
2390
|
+
await pruneHookConfig(config2);
|
|
2357
2391
|
}
|
|
2358
2392
|
for (const constraint of plan.constraints) {
|
|
2359
2393
|
await stripConstraintRegion(constraint);
|
|
@@ -2362,8 +2396,8 @@ async function executeClearPlan(plan) {
|
|
|
2362
2396
|
await removeIfEmpty(dir);
|
|
2363
2397
|
}
|
|
2364
2398
|
}
|
|
2365
|
-
async function pruneHookConfig(
|
|
2366
|
-
const { filePath, managedHookCommands, managedHookPaths } =
|
|
2399
|
+
async function pruneHookConfig(config2) {
|
|
2400
|
+
const { filePath, managedHookCommands, managedHookPaths } = config2;
|
|
2367
2401
|
const content = await readTextIfExists(filePath);
|
|
2368
2402
|
if (content === null) {
|
|
2369
2403
|
return;
|
|
@@ -2538,21 +2572,163 @@ function renderTargetPlans(targetPlans) {
|
|
|
2538
2572
|
).join("\n\n");
|
|
2539
2573
|
}
|
|
2540
2574
|
|
|
2541
|
-
// src/commands/
|
|
2575
|
+
// src/commands/config.ts
|
|
2542
2576
|
import path18 from "path";
|
|
2543
|
-
import {
|
|
2577
|
+
import { cancel as cancel4, confirm as confirm3, outro as outro3, select } from "@clack/prompts";
|
|
2544
2578
|
import chalk4 from "chalk";
|
|
2545
2579
|
|
|
2580
|
+
// src/utils/compare-versions.ts
|
|
2581
|
+
import path17 from "path";
|
|
2582
|
+
function parseVersion(version) {
|
|
2583
|
+
const withoutBuild = String(version ?? "").split("+", 1)[0];
|
|
2584
|
+
const separator = withoutBuild.indexOf("-");
|
|
2585
|
+
const coreText = separator === -1 ? withoutBuild : withoutBuild.slice(0, separator);
|
|
2586
|
+
const prereleaseText = separator === -1 ? "" : withoutBuild.slice(separator + 1);
|
|
2587
|
+
const core = coreText.split(".").map((part) => {
|
|
2588
|
+
const parsed = Number.parseInt(part, 10);
|
|
2589
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
2590
|
+
});
|
|
2591
|
+
return {
|
|
2592
|
+
core,
|
|
2593
|
+
prerelease: prereleaseText ? prereleaseText.split(".") : null
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
function comparePrerelease(left, right) {
|
|
2597
|
+
if (left === null && right === null) return 0;
|
|
2598
|
+
if (left === null) return 1;
|
|
2599
|
+
if (right === null) return -1;
|
|
2600
|
+
const length = Math.max(left.length, right.length);
|
|
2601
|
+
for (let index = 0; index < length; index += 1) {
|
|
2602
|
+
const leftPart = left[index];
|
|
2603
|
+
const rightPart = right[index];
|
|
2604
|
+
if (leftPart === void 0) return -1;
|
|
2605
|
+
if (rightPart === void 0) return 1;
|
|
2606
|
+
if (leftPart === rightPart) continue;
|
|
2607
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
2608
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
2609
|
+
if (leftNumeric && rightNumeric) {
|
|
2610
|
+
const leftNumber = Number.parseInt(leftPart, 10);
|
|
2611
|
+
const rightNumber = Number.parseInt(rightPart, 10);
|
|
2612
|
+
return leftNumber < rightNumber ? -1 : 1;
|
|
2613
|
+
}
|
|
2614
|
+
if (leftNumeric) return -1;
|
|
2615
|
+
if (rightNumeric) return 1;
|
|
2616
|
+
return leftPart < rightPart ? -1 : 1;
|
|
2617
|
+
}
|
|
2618
|
+
return 0;
|
|
2619
|
+
}
|
|
2620
|
+
function compareVersions(a, b) {
|
|
2621
|
+
const left = parseVersion(a);
|
|
2622
|
+
const right = parseVersion(b);
|
|
2623
|
+
const length = Math.max(left.core.length, right.core.length);
|
|
2624
|
+
for (let index = 0; index < length; index += 1) {
|
|
2625
|
+
const leftPart = left.core[index] ?? 0;
|
|
2626
|
+
const rightPart = right.core[index] ?? 0;
|
|
2627
|
+
if (leftPart < rightPart) return -1;
|
|
2628
|
+
if (leftPart > rightPart) return 1;
|
|
2629
|
+
}
|
|
2630
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
2631
|
+
}
|
|
2632
|
+
function isVersionBehind(installed, current = VERSION) {
|
|
2633
|
+
return compareVersions(installed, current) === -1;
|
|
2634
|
+
}
|
|
2635
|
+
async function checkForUpgrade(cwd) {
|
|
2636
|
+
const configPath2 = path17.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2637
|
+
if (!await pathExists(configPath2)) {
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
let config2;
|
|
2641
|
+
try {
|
|
2642
|
+
config2 = await readConfigYaml(configPath2);
|
|
2643
|
+
} catch {
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
const installed = String(config2.harness_version ?? "");
|
|
2647
|
+
if (installed && isVersionBehind(installed)) {
|
|
2648
|
+
process.stderr.write(
|
|
2649
|
+
`easy-coding harness ${installed} is older than CLI ${VERSION}. Run easy-coding upgrade when ready.
|
|
2650
|
+
`
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/commands/config.ts
|
|
2656
|
+
async function config() {
|
|
2657
|
+
renderBanner();
|
|
2658
|
+
const configPath2 = path18.join(process.cwd(), EASY_CODING_DIR, CONFIG_FILE);
|
|
2659
|
+
if (!await pathExists(configPath2)) {
|
|
2660
|
+
throw new Error("No easy-coding harness found in this project.");
|
|
2661
|
+
}
|
|
2662
|
+
const projectConfig = await readConfigYaml(configPath2);
|
|
2663
|
+
if (projectConfig.harness_version !== VERSION) {
|
|
2664
|
+
const relation = compareVersions(projectConfig.harness_version, VERSION);
|
|
2665
|
+
if (relation === -1) {
|
|
2666
|
+
throw new Error(
|
|
2667
|
+
`Project harness ${projectConfig.harness_version} is older than CLI ${VERSION}. Run easy-coding upgrade first.`
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
if (relation === 1) {
|
|
2671
|
+
throw new Error(
|
|
2672
|
+
`Project harness ${projectConfig.harness_version} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
throw new Error(
|
|
2676
|
+
`Project harness ${projectConfig.harness_version} does not exactly match CLI ${VERSION}. Upgrade the harness or update the CLI before changing config.`
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
const current = resolveLegacyConfirmMode(projectConfig);
|
|
2680
|
+
const selected = await select({
|
|
2681
|
+
message: `Select project confirm mode (current: ${current})`,
|
|
2682
|
+
initialValue: current,
|
|
2683
|
+
options: [
|
|
2684
|
+
{
|
|
2685
|
+
value: "approve",
|
|
2686
|
+
label: "approve \u2014 confirm every stage transition",
|
|
2687
|
+
hint: "except INIT -> ANALYSIS and MEMORY -> COMPLETE"
|
|
2688
|
+
},
|
|
2689
|
+
{
|
|
2690
|
+
value: "guard",
|
|
2691
|
+
label: "guard \u2014 confirm critical gates (default)",
|
|
2692
|
+
hint: "ANALYSIS -> IMPLEMENT and VERIFICATION -> MEMORY"
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
value: "auto",
|
|
2696
|
+
label: "auto \u2014 advance workflow stages automatically",
|
|
2697
|
+
hint: "task closure remains explicit"
|
|
2698
|
+
}
|
|
2699
|
+
]
|
|
2700
|
+
});
|
|
2701
|
+
if (typeof selected === "symbol") {
|
|
2702
|
+
cancel4("Configuration cancelled.");
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
const shouldSave = await confirm3({
|
|
2706
|
+
message: `Set behavior.confirm_mode to ${selected}?`,
|
|
2707
|
+
initialValue: true
|
|
2708
|
+
});
|
|
2709
|
+
if (typeof shouldSave === "symbol" || !shouldSave) {
|
|
2710
|
+
cancel4("Configuration cancelled.");
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
await setConfirmMode(configPath2, selected);
|
|
2714
|
+
outro3(chalk4.green(`Project confirm mode updated to ${selected}.`));
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// src/commands/init.ts
|
|
2718
|
+
import path20 from "path";
|
|
2719
|
+
import { note, outro as outro4 } from "@clack/prompts";
|
|
2720
|
+
import chalk5 from "chalk";
|
|
2721
|
+
|
|
2546
2722
|
// src/utils/install-state.ts
|
|
2547
2723
|
import { readdir as readdir5 } from "fs/promises";
|
|
2548
|
-
import
|
|
2724
|
+
import path19 from "path";
|
|
2549
2725
|
var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
|
|
2550
2726
|
async function detectEasyCodingInstallState(cwd) {
|
|
2551
|
-
const easyCodingDir =
|
|
2727
|
+
const easyCodingDir = path19.join(cwd, EASY_CODING_DIR);
|
|
2552
2728
|
if (!await pathExists(easyCodingDir)) {
|
|
2553
2729
|
return { kind: "fresh", easyCodingDir };
|
|
2554
2730
|
}
|
|
2555
|
-
const configPath2 =
|
|
2731
|
+
const configPath2 = path19.join(easyCodingDir, CONFIG_FILE);
|
|
2556
2732
|
if (await pathExists(configPath2)) {
|
|
2557
2733
|
return { kind: "installed", easyCodingDir, configPath: configPath2 };
|
|
2558
2734
|
}
|
|
@@ -2570,17 +2746,17 @@ async function detectEasyCodingInstallState(cwd) {
|
|
|
2570
2746
|
async function detectLegacyAssets(easyCodingDir) {
|
|
2571
2747
|
const assets = [];
|
|
2572
2748
|
for (const file of LEGACY_ROOT_FILES) {
|
|
2573
|
-
if (await pathExists(
|
|
2749
|
+
if (await pathExists(path19.join(easyCodingDir, file))) {
|
|
2574
2750
|
assets.push(relativeEasyCodingPath(file));
|
|
2575
2751
|
}
|
|
2576
2752
|
}
|
|
2577
|
-
if (await pathExists(
|
|
2753
|
+
if (await pathExists(path19.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
2578
2754
|
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
2579
2755
|
}
|
|
2580
|
-
const shortMemoryFiles = await listMarkdownFiles(
|
|
2756
|
+
const shortMemoryFiles = await listMarkdownFiles(path19.join(easyCodingDir, "memory", "short"));
|
|
2581
2757
|
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
2582
2758
|
for (const dir of ["spec", "prototype"]) {
|
|
2583
|
-
if (await hasAnyDirectoryEntry(
|
|
2759
|
+
if (await hasAnyDirectoryEntry(path19.join(easyCodingDir, dir))) {
|
|
2584
2760
|
assets.push(relativeEasyCodingPath(dir));
|
|
2585
2761
|
}
|
|
2586
2762
|
}
|
|
@@ -2600,13 +2776,13 @@ async function hasAnyDirectoryEntry(dir) {
|
|
|
2600
2776
|
return (await readdir5(dir)).length > 0;
|
|
2601
2777
|
}
|
|
2602
2778
|
function relativeEasyCodingPath(...segments) {
|
|
2603
|
-
return
|
|
2779
|
+
return path19.posix.join(EASY_CODING_DIR, ...segments);
|
|
2604
2780
|
}
|
|
2605
2781
|
function relativeConfigPath() {
|
|
2606
|
-
return
|
|
2782
|
+
return path19.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
2607
2783
|
}
|
|
2608
2784
|
function relativeProjectInitTaskPath() {
|
|
2609
|
-
return
|
|
2785
|
+
return path19.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
2610
2786
|
}
|
|
2611
2787
|
|
|
2612
2788
|
// src/commands/init.ts
|
|
@@ -2626,7 +2802,7 @@ async function init(opts) {
|
|
|
2626
2802
|
const platforms2 = await resolveInitPlatforms(cwd, opts, true);
|
|
2627
2803
|
await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms2);
|
|
2628
2804
|
}
|
|
2629
|
-
|
|
2805
|
+
outro4(chalk5.yellow("All selected easy-coding harness targets are already installed."));
|
|
2630
2806
|
return;
|
|
2631
2807
|
}
|
|
2632
2808
|
const platforms = await resolveInitPlatforms(cwd, opts, Boolean(parentTarget2?.installed));
|
|
@@ -2641,8 +2817,8 @@ async function init(opts) {
|
|
|
2641
2817
|
(platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
|
|
2642
2818
|
).join("\n");
|
|
2643
2819
|
note(triggers, "Next step");
|
|
2644
|
-
|
|
2645
|
-
|
|
2820
|
+
outro4(
|
|
2821
|
+
chalk5.green(
|
|
2646
2822
|
`easy-coding harness installed in ${installableTargets.map((target) => target.label).join(", ")}. Open your agent and run ec-init.`
|
|
2647
2823
|
)
|
|
2648
2824
|
);
|
|
@@ -2687,7 +2863,7 @@ async function supermoduleTargets(cwd, opts, submodules) {
|
|
|
2687
2863
|
if (!targetSubmodulePaths.has(entry.path)) {
|
|
2688
2864
|
continue;
|
|
2689
2865
|
}
|
|
2690
|
-
const dir =
|
|
2866
|
+
const dir = path20.join(cwd, entry.path);
|
|
2691
2867
|
targets.push(
|
|
2692
2868
|
await targetFromState(dir, entry.path, "submodule-child", {
|
|
2693
2869
|
parent: toPosixRelative2(dir, cwd)
|
|
@@ -2734,25 +2910,25 @@ function contextFromState(role, installState) {
|
|
|
2734
2910
|
};
|
|
2735
2911
|
}
|
|
2736
2912
|
function toPosixRelative2(from, to) {
|
|
2737
|
-
const relative =
|
|
2738
|
-
return relative ? relative.split(
|
|
2913
|
+
const relative = path20.relative(from, to);
|
|
2914
|
+
return relative ? relative.split(path20.sep).join("/") : ".";
|
|
2739
2915
|
}
|
|
2740
2916
|
async function resolveInitPlatforms(cwd, opts, parentInstalled) {
|
|
2741
2917
|
if (opts.agent || !parentInstalled) {
|
|
2742
2918
|
return resolvePlatforms(opts, ["claude-code"]);
|
|
2743
2919
|
}
|
|
2744
|
-
const
|
|
2745
|
-
if (Array.isArray(
|
|
2746
|
-
return
|
|
2920
|
+
const config2 = await readConfigYaml(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
2921
|
+
if (Array.isArray(config2.agents) && config2.agents.length > 0) {
|
|
2922
|
+
return config2.agents;
|
|
2747
2923
|
}
|
|
2748
2924
|
return resolvePlatforms(opts, ["claude-code"]);
|
|
2749
2925
|
}
|
|
2750
2926
|
async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
|
|
2751
|
-
if (!await pathExists(
|
|
2927
|
+
if (!await pathExists(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
2752
2928
|
return;
|
|
2753
2929
|
}
|
|
2754
|
-
const
|
|
2755
|
-
const platforms = Array.isArray(
|
|
2930
|
+
const config2 = parentTarget2.installed ? await readConfigYaml(path20.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
|
|
2931
|
+
const platforms = Array.isArray(config2.agents) && config2.agents.length > 0 ? config2.agents : installPlatforms;
|
|
2756
2932
|
await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
|
|
2757
2933
|
}
|
|
2758
2934
|
async function refreshInstalledChildTopologies(targets) {
|
|
@@ -2760,7 +2936,7 @@ async function refreshInstalledChildTopologies(targets) {
|
|
|
2760
2936
|
if (!target.installed || target.context.role !== "submodule-child") {
|
|
2761
2937
|
continue;
|
|
2762
2938
|
}
|
|
2763
|
-
const configPath2 =
|
|
2939
|
+
const configPath2 = path20.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
|
|
2764
2940
|
if (!await pathExists(configPath2)) {
|
|
2765
2941
|
continue;
|
|
2766
2942
|
}
|
|
@@ -2769,63 +2945,19 @@ async function refreshInstalledChildTopologies(targets) {
|
|
|
2769
2945
|
}
|
|
2770
2946
|
|
|
2771
2947
|
// src/commands/status.ts
|
|
2772
|
-
import
|
|
2773
|
-
import
|
|
2774
|
-
|
|
2775
|
-
// src/utils/compare-versions.ts
|
|
2776
|
-
import path19 from "path";
|
|
2777
|
-
function normalize(version) {
|
|
2778
|
-
const [core] = String(version ?? "").split("-");
|
|
2779
|
-
return core.split(".").map((part) => {
|
|
2780
|
-
const parsed = Number.parseInt(part, 10);
|
|
2781
|
-
return Number.isNaN(parsed) ? 0 : parsed;
|
|
2782
|
-
});
|
|
2783
|
-
}
|
|
2784
|
-
function compareVersions(a, b) {
|
|
2785
|
-
const left = normalize(a);
|
|
2786
|
-
const right = normalize(b);
|
|
2787
|
-
const length = Math.max(left.length, right.length);
|
|
2788
|
-
for (let index = 0; index < length; index += 1) {
|
|
2789
|
-
const leftPart = left[index] ?? 0;
|
|
2790
|
-
const rightPart = right[index] ?? 0;
|
|
2791
|
-
if (leftPart < rightPart) return -1;
|
|
2792
|
-
if (leftPart > rightPart) return 1;
|
|
2793
|
-
}
|
|
2794
|
-
return 0;
|
|
2795
|
-
}
|
|
2796
|
-
function isVersionBehind(installed, current = VERSION) {
|
|
2797
|
-
return compareVersions(installed, current) === -1;
|
|
2798
|
-
}
|
|
2799
|
-
async function checkForUpgrade(cwd) {
|
|
2800
|
-
const configPath2 = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2801
|
-
if (!await pathExists(configPath2)) {
|
|
2802
|
-
return;
|
|
2803
|
-
}
|
|
2804
|
-
let config;
|
|
2805
|
-
try {
|
|
2806
|
-
config = await readConfigYaml(configPath2);
|
|
2807
|
-
} catch {
|
|
2808
|
-
return;
|
|
2809
|
-
}
|
|
2810
|
-
const installed = String(config.harness_version ?? "");
|
|
2811
|
-
if (installed && isVersionBehind(installed)) {
|
|
2812
|
-
process.stderr.write(
|
|
2813
|
-
`easy-coding harness ${installed} is older than CLI ${VERSION}. Run easy-coding upgrade when ready.
|
|
2814
|
-
`
|
|
2815
|
-
);
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2948
|
+
import path22 from "path";
|
|
2949
|
+
import chalk6 from "chalk";
|
|
2818
2950
|
|
|
2819
2951
|
// src/utils/session.ts
|
|
2820
2952
|
import { readdir as readdir6 } from "fs/promises";
|
|
2821
|
-
import
|
|
2953
|
+
import path21 from "path";
|
|
2822
2954
|
var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
2823
2955
|
function getSessionDir(cwd) {
|
|
2824
|
-
return
|
|
2956
|
+
return path21.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
2825
2957
|
}
|
|
2826
2958
|
function getSessionFilePath(cwd, ppid) {
|
|
2827
2959
|
const pid = ppid ?? process.ppid;
|
|
2828
|
-
return
|
|
2960
|
+
return path21.join(getSessionDir(cwd), `${pid}.json`);
|
|
2829
2961
|
}
|
|
2830
2962
|
async function readSessionFile(cwd, ppid) {
|
|
2831
2963
|
const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
|
|
@@ -2839,30 +2971,36 @@ async function readSessionFile(cwd, ppid) {
|
|
|
2839
2971
|
async function status() {
|
|
2840
2972
|
renderBanner();
|
|
2841
2973
|
const cwd = process.cwd();
|
|
2842
|
-
const configPath2 =
|
|
2974
|
+
const configPath2 = path22.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2843
2975
|
if (!await pathExists(configPath2)) {
|
|
2844
2976
|
throw new Error("No easy-coding harness found in this project.");
|
|
2845
2977
|
}
|
|
2846
|
-
const
|
|
2978
|
+
const config2 = await readConfigYaml(configPath2);
|
|
2847
2979
|
const tasks = await listTasks(cwd);
|
|
2848
2980
|
const taskCounts = summarizeTaskStatuses(tasks);
|
|
2849
2981
|
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
2850
2982
|
const session = await readSessionFile(cwd);
|
|
2851
|
-
const versionRelation = compareVersions(
|
|
2852
|
-
console.log(
|
|
2853
|
-
console.log(` version: ${
|
|
2983
|
+
const versionRelation = compareVersions(config2.harness_version, VERSION);
|
|
2984
|
+
console.log(chalk6.bold("Harness"));
|
|
2985
|
+
console.log(` version: ${config2.harness_version}`);
|
|
2854
2986
|
console.log(` cli: ${VERSION}`);
|
|
2855
|
-
if (versionRelation === -1) {
|
|
2856
|
-
console.log(
|
|
2987
|
+
if (versionRelation === -1 || versionRelation === 0 && config2.harness_version !== VERSION) {
|
|
2988
|
+
console.log(chalk6.yellow(" upgrade: available"));
|
|
2857
2989
|
} else if (versionRelation === 1) {
|
|
2858
|
-
console.log(
|
|
2990
|
+
console.log(chalk6.red(" upgrade: CLI is older than project harness"));
|
|
2859
2991
|
} else {
|
|
2860
2992
|
console.log(" upgrade: up to date");
|
|
2861
2993
|
}
|
|
2862
|
-
console.log(` agents: ${
|
|
2863
|
-
console.log(` project: ${
|
|
2994
|
+
console.log(` agents: ${config2.agents.join(", ") || "(none)"}`);
|
|
2995
|
+
console.log(` project: ${config2.project.name}`);
|
|
2996
|
+
const projectConfirmMode = isConfirmMode(config2.behavior?.confirm_mode) ? config2.behavior.confirm_mode : "guard";
|
|
2997
|
+
console.log(` confirm_mode: ${projectConfirmMode}`);
|
|
2864
2998
|
console.log("");
|
|
2865
|
-
console.log(
|
|
2999
|
+
console.log(chalk6.bold("Session"));
|
|
3000
|
+
const sessionConfirmMode = session?.confirm_mode;
|
|
3001
|
+
console.log(` confirm_mode: ${sessionConfirmMode ?? "project default"}`);
|
|
3002
|
+
console.log(` effective_confirm_mode: ${sessionConfirmMode ?? projectConfirmMode}`);
|
|
3003
|
+
console.log(` harness: ${session?.harness_disabled ? "disabled for this session" : "enabled"}`);
|
|
2866
3004
|
if (session?.current_task) {
|
|
2867
3005
|
const taskPath = getTaskJsonPath(cwd, session.current_task);
|
|
2868
3006
|
if (await pathExists(taskPath)) {
|
|
@@ -2877,7 +3015,7 @@ async function status() {
|
|
|
2877
3015
|
console.log(" no active session");
|
|
2878
3016
|
}
|
|
2879
3017
|
console.log("");
|
|
2880
|
-
console.log(
|
|
3018
|
+
console.log(chalk6.bold("Tasks"));
|
|
2881
3019
|
console.log(` total: ${tasks.length}`);
|
|
2882
3020
|
for (const [taskStatus, count] of Object.entries(taskCounts)) {
|
|
2883
3021
|
console.log(` ${taskStatus}: ${count}`);
|
|
@@ -2893,8 +3031,8 @@ async function status() {
|
|
|
2893
3031
|
|
|
2894
3032
|
// src/commands/update.ts
|
|
2895
3033
|
import { execFileSync } from "child_process";
|
|
2896
|
-
import { cancel as
|
|
2897
|
-
import
|
|
3034
|
+
import { cancel as cancel5, confirm as confirm4, outro as outro5 } from "@clack/prompts";
|
|
3035
|
+
import chalk7 from "chalk";
|
|
2898
3036
|
async function update(opts) {
|
|
2899
3037
|
renderBanner();
|
|
2900
3038
|
const tag = opts.tag ?? "latest";
|
|
@@ -2907,24 +3045,24 @@ async function update(opts) {
|
|
|
2907
3045
|
return;
|
|
2908
3046
|
}
|
|
2909
3047
|
if (!opts.yes) {
|
|
2910
|
-
const confirmed = await
|
|
3048
|
+
const confirmed = await confirm4({
|
|
2911
3049
|
message: `Update the global CLI to ${spec}?`,
|
|
2912
3050
|
initialValue: true
|
|
2913
3051
|
});
|
|
2914
3052
|
if (typeof confirmed === "symbol" || !confirmed) {
|
|
2915
|
-
|
|
3053
|
+
cancel5("Update cancelled.");
|
|
2916
3054
|
return;
|
|
2917
3055
|
}
|
|
2918
3056
|
}
|
|
2919
|
-
console.log(
|
|
3057
|
+
console.log(chalk7.cyan(`\u2192 npm install -g ${spec}`));
|
|
2920
3058
|
execFileSync("npm", ["install", "-g", spec], { stdio: "inherit" });
|
|
2921
|
-
|
|
3059
|
+
outro5(chalk7.green("Global CLI updated."));
|
|
2922
3060
|
}
|
|
2923
3061
|
|
|
2924
3062
|
// src/commands/upgrade.ts
|
|
2925
|
-
import
|
|
2926
|
-
import { cancel as
|
|
2927
|
-
import
|
|
3063
|
+
import path23 from "path";
|
|
3064
|
+
import { cancel as cancel6, confirm as confirm5, outro as outro6 } from "@clack/prompts";
|
|
3065
|
+
import chalk8 from "chalk";
|
|
2928
3066
|
var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
|
|
2929
3067
|
"claude-code": [
|
|
2930
3068
|
{ event: "SessionStart", scriptName: "session-start.py" },
|
|
@@ -2962,13 +3100,13 @@ async function upgrade(opts) {
|
|
|
2962
3100
|
const parentTopologyRefresh = await resolveParentTopologyRefresh(targets, pending);
|
|
2963
3101
|
const childTopologyRefreshes = await resolveChildTopologyRefreshes(targets, pending);
|
|
2964
3102
|
if (pending.length === 0 && !parentTopologyRefresh && childTopologyRefreshes.length === 0) {
|
|
2965
|
-
|
|
3103
|
+
outro6(chalk8.green(`easy-coding harness is already up to date (${VERSION}).`));
|
|
2966
3104
|
return;
|
|
2967
3105
|
}
|
|
2968
3106
|
const summary = [
|
|
2969
3107
|
"Upgrade targets:",
|
|
2970
3108
|
...pending.length > 0 ? pending.map(
|
|
2971
|
-
({ target, config }) => `- ${target.label}: ${String(
|
|
3109
|
+
({ target, config: config2 }) => `- ${target.label}: ${String(config2.harness_version ?? "unknown")} -> ${VERSION}; agents: ${config2.agents.join(", ")}`
|
|
2972
3110
|
) : ["- (none)"],
|
|
2973
3111
|
...parentTopologyRefresh ? [
|
|
2974
3112
|
"Supermodule topology refresh:",
|
|
@@ -2980,6 +3118,7 @@ async function upgrade(opts) {
|
|
|
2980
3118
|
].filter(Boolean) : [],
|
|
2981
3119
|
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
2982
3120
|
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
3121
|
+
"Will migrate behavior.strict_confirm/auto_mode to behavior.confirm_mode and remove the old keys.",
|
|
2983
3122
|
"Will migrate legacy workflow stage metadata; memory content, spec, and project knowledge files remain untouched."
|
|
2984
3123
|
].join("\n");
|
|
2985
3124
|
if (opts.dryRun) {
|
|
@@ -2987,31 +3126,32 @@ async function upgrade(opts) {
|
|
|
2987
3126
|
return;
|
|
2988
3127
|
}
|
|
2989
3128
|
if (!opts.yes) {
|
|
2990
|
-
const shouldUpgrade = await
|
|
3129
|
+
const shouldUpgrade = await confirm5({
|
|
2991
3130
|
message: "Apply this harness upgrade?",
|
|
2992
3131
|
initialValue: true
|
|
2993
3132
|
});
|
|
2994
3133
|
if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
|
|
2995
|
-
|
|
3134
|
+
cancel6("Upgrade cancelled.");
|
|
2996
3135
|
return;
|
|
2997
3136
|
}
|
|
2998
3137
|
}
|
|
2999
|
-
for (const { target, config } of pending) {
|
|
3000
|
-
const projectId = await writeRuntimeScaffold(target.dir,
|
|
3138
|
+
for (const { target, config: config2 } of pending) {
|
|
3139
|
+
const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
|
|
3001
3140
|
supermodule: target.supermodule
|
|
3002
3141
|
});
|
|
3003
|
-
const artifacts = await configurePlatformsForDir(target.dir,
|
|
3142
|
+
const artifacts = await configurePlatformsForDir(target.dir, config2.agents, {
|
|
3004
3143
|
supermodule: target.boundary,
|
|
3005
3144
|
projectId
|
|
3006
3145
|
});
|
|
3007
3146
|
await writeInstallManifest(target.dir, {
|
|
3008
3147
|
harnessVersion: VERSION,
|
|
3009
|
-
agents:
|
|
3148
|
+
agents: config2.agents,
|
|
3010
3149
|
artifacts
|
|
3011
3150
|
});
|
|
3012
3151
|
await ensureEasyCodingSessionsIgnored(target.dir);
|
|
3013
3152
|
await ensureHookBytecodeIgnored(target.dir);
|
|
3014
3153
|
await migrateLegacyWorkflowState(target.dir);
|
|
3154
|
+
await migrateConfirmModeConfig(target.configPath);
|
|
3015
3155
|
await updateHarnessVersion(target.configPath, VERSION);
|
|
3016
3156
|
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
3017
3157
|
await setPendingInitSince(target.dir, VERSION);
|
|
@@ -3027,8 +3167,8 @@ async function upgrade(opts) {
|
|
|
3027
3167
|
for (const { target } of childTopologyRefreshes) {
|
|
3028
3168
|
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
3029
3169
|
}
|
|
3030
|
-
|
|
3031
|
-
|
|
3170
|
+
outro6(
|
|
3171
|
+
chalk8.green(
|
|
3032
3172
|
pending.length > 0 ? `easy-coding harness upgraded to ${VERSION}.` : "easy-coding supermodule topology refreshed."
|
|
3033
3173
|
)
|
|
3034
3174
|
);
|
|
@@ -3039,9 +3179,9 @@ async function resolvePendingUpgradeTargets(targets) {
|
|
|
3039
3179
|
if (!await pathExists(target.configPath)) {
|
|
3040
3180
|
continue;
|
|
3041
3181
|
}
|
|
3042
|
-
const
|
|
3043
|
-
const installedVersion = String(
|
|
3044
|
-
const hasAgents = Array.isArray(
|
|
3182
|
+
const config2 = await readConfigYaml(target.configPath);
|
|
3183
|
+
const installedVersion = String(config2.harness_version ?? "");
|
|
3184
|
+
const hasAgents = Array.isArray(config2.agents) && config2.agents.length > 0;
|
|
3045
3185
|
if (!installedVersion || !hasAgents) {
|
|
3046
3186
|
throw new Error(
|
|
3047
3187
|
`${target.label} config.yaml is missing required harness fields (harness_version / agents). This project predates the current config layout. Run \`easy-coding clear\` then \`easy-coding init\` to migrate \u2014 your tasks, spec, and memory are preserved.`
|
|
@@ -3053,21 +3193,21 @@ async function resolvePendingUpgradeTargets(targets) {
|
|
|
3053
3193
|
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
3054
3194
|
);
|
|
3055
3195
|
}
|
|
3056
|
-
if (relation === -1 || relation === 0 && (await needsHookConfigRefresh(target,
|
|
3057
|
-
pending.push({ target, config });
|
|
3196
|
+
if (relation === -1 || relation === 0 && (installedVersion !== VERSION || await needsHookConfigRefresh(target, config2) || await hasLegacyWorkflowState(target.dir))) {
|
|
3197
|
+
pending.push({ target, config: config2 });
|
|
3058
3198
|
}
|
|
3059
3199
|
}
|
|
3060
3200
|
return pending;
|
|
3061
3201
|
}
|
|
3062
|
-
async function needsHookConfigRefresh(target,
|
|
3063
|
-
const projectId = typeof
|
|
3202
|
+
async function needsHookConfigRefresh(target, config2) {
|
|
3203
|
+
const projectId = typeof config2.project?.id === "string" ? config2.project.id.trim() : "";
|
|
3064
3204
|
if (!projectId) {
|
|
3065
3205
|
return true;
|
|
3066
3206
|
}
|
|
3067
3207
|
const manifest = await readInstallManifest(target.dir);
|
|
3068
|
-
for (const agent of
|
|
3208
|
+
for (const agent of config2.agents) {
|
|
3069
3209
|
const meta = resolvePlatformMeta(target.dir, agent);
|
|
3070
|
-
const configPath2 =
|
|
3210
|
+
const configPath2 = path23.join(target.dir, meta.hookConfigFile);
|
|
3071
3211
|
const content = await readTextIfExists(configPath2);
|
|
3072
3212
|
if (content === null) {
|
|
3073
3213
|
return true;
|
|
@@ -3184,7 +3324,7 @@ function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
|
|
|
3184
3324
|
return true;
|
|
3185
3325
|
}
|
|
3186
3326
|
return pathAliases(
|
|
3187
|
-
normalizePathForHookComparison(
|
|
3327
|
+
normalizePathForHookComparison(path23.resolve(cwd, relativeHookPath))
|
|
3188
3328
|
).includes(normalizedHookPath);
|
|
3189
3329
|
});
|
|
3190
3330
|
}
|
|
@@ -3210,19 +3350,19 @@ async function resolveParentTopologyRefresh(targets, pending) {
|
|
|
3210
3350
|
if (!await pathExists(parent.configPath)) {
|
|
3211
3351
|
return null;
|
|
3212
3352
|
}
|
|
3213
|
-
const
|
|
3214
|
-
if (!Array.isArray(
|
|
3353
|
+
const config2 = await readConfigYaml(parent.configPath);
|
|
3354
|
+
if (!Array.isArray(config2.agents) || config2.agents.length === 0) {
|
|
3215
3355
|
return null;
|
|
3216
3356
|
}
|
|
3217
3357
|
const submodulePaths = parent.supermodule.submodules ?? [];
|
|
3218
|
-
const configuredSubmodules = Array.isArray(
|
|
3219
|
-
const needsRefresh =
|
|
3358
|
+
const configuredSubmodules = Array.isArray(config2.supermodule?.submodules) ? config2.supermodule.submodules : [];
|
|
3359
|
+
const needsRefresh = config2.supermodule?.role !== "super-parent" || !sameStringList(configuredSubmodules, submodulePaths);
|
|
3220
3360
|
if (!needsRefresh) {
|
|
3221
3361
|
return null;
|
|
3222
3362
|
}
|
|
3223
3363
|
return {
|
|
3224
3364
|
target: parent,
|
|
3225
|
-
agents:
|
|
3365
|
+
agents: config2.agents,
|
|
3226
3366
|
submodulePaths
|
|
3227
3367
|
};
|
|
3228
3368
|
}
|
|
@@ -3233,8 +3373,8 @@ async function resolveChildTopologyRefreshes(targets, pending) {
|
|
|
3233
3373
|
if (target.supermodule.role !== "submodule-child" || pendingLabels.has(target.label) || !await pathExists(target.configPath)) {
|
|
3234
3374
|
continue;
|
|
3235
3375
|
}
|
|
3236
|
-
const
|
|
3237
|
-
if (
|
|
3376
|
+
const config2 = await readConfigYaml(target.configPath);
|
|
3377
|
+
if (config2.supermodule?.role === "submodule-child" && config2.supermodule.parent === target.supermodule.parent) {
|
|
3238
3378
|
continue;
|
|
3239
3379
|
}
|
|
3240
3380
|
refreshes.push({ target });
|
|
@@ -3256,7 +3396,7 @@ function withErrorHandling(fn) {
|
|
|
3256
3396
|
try {
|
|
3257
3397
|
await fn(opts);
|
|
3258
3398
|
} catch (error) {
|
|
3259
|
-
console.error(
|
|
3399
|
+
console.error(chalk9.red("Error:"), error instanceof Error ? error.message : error);
|
|
3260
3400
|
if (process.env.EC_DEBUG && error instanceof Error) {
|
|
3261
3401
|
console.error(error.stack);
|
|
3262
3402
|
}
|
|
@@ -3274,6 +3414,7 @@ program.command("init").description("Initialize easy-coding harness in current p
|
|
|
3274
3414
|
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));
|
|
3275
3415
|
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));
|
|
3276
3416
|
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));
|
|
3417
|
+
program.command("config").description("Interactively configure project-level harness behavior").action(withErrorHandling(config));
|
|
3277
3418
|
program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
|
|
3278
3419
|
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));
|
|
3279
3420
|
program.parse();
|