easy-coding-harness 0.5.2-beta.0 → 0.6.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 +18 -0
- package/README.md +8 -10
- package/dist/cli.js +156 -4
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +1 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +8 -5
- package/templates/common/skills/ec-analysis/SKILL.md +9 -8
- package/templates/common/skills/ec-brainstorming/SKILL.md +2 -1
- package/templates/common/skills/ec-implementing/SKILL.md +5 -4
- package/templates/common/skills/ec-memory/SKILL.md +42 -20
- package/templates/common/skills/ec-reviewing/SKILL.md +6 -3
- package/templates/common/skills/ec-task-close/SKILL.md +1 -1
- package/templates/common/skills/ec-verification/SKILL.md +16 -21
- package/templates/common/skills/ec-workflow/SKILL.md +70 -62
- package/templates/main-constraint/AGENTS.md.tpl +10 -6
- package/templates/main-constraint/CLAUDE.md.tpl +10 -6
- package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +2 -0
- package/templates/shared-hooks/easy_coding_state.py +560 -33
- package/templates/shared-hooks/inject-workflow-state.py +1 -176
- package/templates/shared-hooks/session-start.py +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
- 状态机移除无实际工作内容的 `WAITING_CONFIRM`;阶段完成后通过 `task.json.pending_transition` 记录待确认边,状态仍停留在当前阶段,直到用户明确确认。
|
|
12
|
+
- 所有合法阶段迁移默认统一提供“确认进入/返回目标阶段、交接给其他智能体、Other”三分支;优先使用智能体原生选项功能,纯文本编号仅作为无原生能力时的回退;handoff 会保留待确认边,下一智能体 claim 后可直接恢复该门禁。
|
|
13
|
+
- 合并 `MEMORY_SHORT` 与 `MEMORY_LONG` 为单一 `MEMORY`:先写并检查点化短期记忆,再由状态 API 计算长期记忆 `no-op/distill` 指令,原阈值门禁保持不变。
|
|
14
|
+
- MEMORY 短期记忆检查点校验 `memory_schema: 2`、`source_task` 当前任务归属和 SHA-256 内容指纹;长期记忆指令冻结 `candidate_files/kept_files`,只有明确候选允许被消费,保留项缺失或候选未清理均不能完成归档;旧 `MEMORY_LONG` 恢复通过显式兼容标记保留。
|
|
15
|
+
- MEMORY 配置强制满足 `0 <= short_term_keep <= short_term_max`;非法窗口直接阻断并提示修正 config.yaml,避免超过阈值后出现零候选、无法收敛的空蒸馏循环。
|
|
16
|
+
- 状态 API 新增 `request-transition`、`confirm-transition`、`cancel-transition` 以及 MEMORY 进度命令;hook 对用户输入完全只读,不再从原生选项、裸编号或自然语言自动写状态,所有确认均由智能体核对当前任务和目标状态后显式执行。
|
|
17
|
+
- `easy-coding upgrade` 幂等迁移 0.5.x 活跃任务和 session:`WAITING_CONFIRM → ANALYSIS`、`MEMORY_SHORT/MEMORY_LONG → MEMORY`,同步清洗阶段历史并保留记忆恢复进度。
|
|
18
|
+
- 工作流 skills、主约束、README、设计/介绍/使用文档及生成后 hook 测试全面对齐新状态模型。
|
|
19
|
+
|
|
20
|
+
## 0.5.3
|
|
21
|
+
|
|
22
|
+
- 根治 `task.json` 绝对路径泄漏:`.easy-coding/tasks/project-init/task.json` 不再写入本机仓库绝对路径。该 `project_path` 字段无任何消费方,直接移除,可提交产物彻底去本地化。
|
|
23
|
+
- `easy-coding upgrade` 会自动剥离存量项目中遗留的 `project_path` 字段,让老项目也随升级变干净。
|
|
24
|
+
- 根治 hook 编译产物:hook launcher 设置 `sys.dont_write_bytecode=True`,运行时不再在 `.claude/hooks/`、`.codex/hooks/`、`.qoder/hooks/` 旁生成 `__pycache__/*.pyc`;同时 `init` / `add-agent` / `upgrade` 会向项目 `.gitignore` 追加 `__pycache__/` 作为兜底防御。launcher 内容变化会触发 `upgrade` 刷新存量 hook 注册。
|
|
25
|
+
- 版本自 `0.5.2-beta.0` 升级为 `0.5.3`,合并 beta.0 的全部 portable hook 修复。跳过 `0.5.2` 正式版:升级检测的 `compareVersions` 会截断预发布后缀,若发 `0.5.2` 会把已安装 `0.5.2-beta.0` 的项目误判为已最新而不提示本次迁移;递增到 `0.5.3` 可让 beta 用户正确检测到升级。
|
|
26
|
+
|
|
9
27
|
## 0.5.2-beta.0
|
|
10
28
|
|
|
11
29
|
- 修复 Claude Code hook 命令使用直接相对路径导致在子目录 cwd 下找不到 `.claude/hooks/*.py` 的问题;Claude、Codex、Qoder 的 hook 配置现在使用可共享的 portable relative launcher,并绑定 `.easy-coding/config.yaml` 的 `project.id`,避免把本机仓库绝对路径写入可提交配置,同时防止 supermodule 父仓 hook 被子仓 cwd 错路由。
|
package/README.md
CHANGED
|
@@ -69,20 +69,18 @@ easy-coding init --submodules packages/a,packages/b
|
|
|
69
69
|
## 工作流
|
|
70
70
|
|
|
71
71
|
```text
|
|
72
|
-
INIT -> ANALYSIS ->
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
MEMORY_SHORT -> MEMORY_LONG -> COMPLETE
|
|
72
|
+
INIT -> ANALYSIS -> IMPLEMENT -> REVIEW -> VERIFICATION -> MEMORY -> COMPLETE
|
|
73
|
+
^ ^ | |
|
|
74
|
+
+-- replan ---+ +--- fix -----+
|
|
75
|
+
^ |
|
|
76
|
+
+------- repair ----------+
|
|
77
|
+
every edge --[user confirmation by default]--> target stage
|
|
79
78
|
any stage --[user abort via ec-task-close]--> CLOSED
|
|
80
79
|
```
|
|
81
80
|
|
|
82
|
-
- `
|
|
81
|
+
- 每个阶段完成后先记录 `pending_transition`,默认必须由用户确认才进入目标阶段;优先通过智能体原生选项功能选择确认、交接给其他智能体或使用 free-form Other,纯文本编号仅作回退。
|
|
83
82
|
- `VERIFICATION` 是验证硬门控,未实际运行的 lint、typecheck、test 不算通过。
|
|
84
|
-
- `
|
|
85
|
-
- `MEMORY_LONG` 只有短期记忆数量超过阈值时才沉淀长期记忆,否则 no-op。
|
|
83
|
+
- `MEMORY` 先写入本次任务短期记忆,再执行长期记忆阈值门禁;未超过阈值时长期沉淀为 no-op。
|
|
86
84
|
|
|
87
85
|
## Supermodule 模型
|
|
88
86
|
|
package/dist/cli.js
CHANGED
|
@@ -208,6 +208,7 @@ var MAIN_SPEC_DIR = "main";
|
|
|
208
208
|
var DEV_SPEC_DIR = "dev";
|
|
209
209
|
var TEMPLATES_DIR = "templates";
|
|
210
210
|
var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
|
|
211
|
+
var HOOK_BYTECODE_GITIGNORE_ENTRY = "__pycache__/";
|
|
211
212
|
var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
|
|
212
213
|
var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness generated \u2550\u2550\u2550 -->";
|
|
213
214
|
|
|
@@ -227,6 +228,13 @@ async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550
|
|
|
227
228
|
async function ensureEasyCodingSessionsIgnored(cwd) {
|
|
228
229
|
return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
|
|
229
230
|
}
|
|
231
|
+
async function ensureHookBytecodeIgnored(cwd) {
|
|
232
|
+
return ensureGitignoreEntry(
|
|
233
|
+
cwd,
|
|
234
|
+
HOOK_BYTECODE_GITIGNORE_ENTRY,
|
|
235
|
+
"# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Python bytecode from hook scripts; do not commit"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
230
238
|
|
|
231
239
|
// src/utils/install-manifest.ts
|
|
232
240
|
import { createHash } from "crypto";
|
|
@@ -647,6 +655,11 @@ async function writeMemoryScaffold(easyCodingDir) {
|
|
|
647
655
|
// src/utils/task-json.ts
|
|
648
656
|
import { readdir } from "fs/promises";
|
|
649
657
|
import path7 from "path";
|
|
658
|
+
var LEGACY_STAGE_MAP = {
|
|
659
|
+
WAITING_CONFIRM: "ANALYSIS",
|
|
660
|
+
MEMORY_SHORT: "MEMORY",
|
|
661
|
+
MEMORY_LONG: "MEMORY"
|
|
662
|
+
};
|
|
650
663
|
function createProjectInitTask(params) {
|
|
651
664
|
return {
|
|
652
665
|
type: "project-init",
|
|
@@ -658,7 +671,6 @@ function createProjectInitTask(params) {
|
|
|
658
671
|
context: {
|
|
659
672
|
agents_installed: params.agents,
|
|
660
673
|
cli_version: VERSION,
|
|
661
|
-
project_path: params.cwd,
|
|
662
674
|
init_source: params.initSource ?? "fresh",
|
|
663
675
|
...params.legacyAssets ? { legacy_assets: params.legacyAssets } : {},
|
|
664
676
|
...params.legacyMissingHarnessFiles ? { legacy_missing_harness_files: params.legacyMissingHarnessFiles } : {}
|
|
@@ -679,7 +691,6 @@ async function writeProjectInitTask(cwd, agents, options = {}) {
|
|
|
679
691
|
await writeTaskJson(
|
|
680
692
|
getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID),
|
|
681
693
|
createProjectInitTask({
|
|
682
|
-
cwd,
|
|
683
694
|
agents,
|
|
684
695
|
initSource: options.initSource,
|
|
685
696
|
legacyAssets: options.legacyAssets,
|
|
@@ -687,6 +698,141 @@ async function writeProjectInitTask(cwd, agents, options = {}) {
|
|
|
687
698
|
})
|
|
688
699
|
);
|
|
689
700
|
}
|
|
701
|
+
async function stripInitTaskProjectPath(cwd) {
|
|
702
|
+
const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
|
|
703
|
+
if (!await pathExists(filePath)) return false;
|
|
704
|
+
const task = await readTaskJson(filePath);
|
|
705
|
+
if (!task.context || !("project_path" in task.context)) return false;
|
|
706
|
+
task.context.project_path = void 0;
|
|
707
|
+
await writeTaskJson(filePath, task);
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
function isLegacyStage(value) {
|
|
711
|
+
return typeof value === "string" && value in LEGACY_STAGE_MAP;
|
|
712
|
+
}
|
|
713
|
+
function migrateStage(value) {
|
|
714
|
+
return isLegacyStage(value) ? LEGACY_STAGE_MAP[value] : value;
|
|
715
|
+
}
|
|
716
|
+
function migrateStageHistory(task) {
|
|
717
|
+
if (!Array.isArray(task.stage_history)) return false;
|
|
718
|
+
let changed = false;
|
|
719
|
+
const migrated = [];
|
|
720
|
+
for (const rawEntry of task.stage_history) {
|
|
721
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
722
|
+
migrated.push(rawEntry);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const entry = { ...rawEntry };
|
|
726
|
+
const mappedStage = migrateStage(entry.stage);
|
|
727
|
+
if (mappedStage !== entry.stage) {
|
|
728
|
+
entry.stage = mappedStage;
|
|
729
|
+
changed = true;
|
|
730
|
+
}
|
|
731
|
+
const previous = migrated.at(-1);
|
|
732
|
+
if (previous && typeof previous === "object" && !Array.isArray(previous) && previous.stage === entry.stage) {
|
|
733
|
+
changed = true;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
migrated.push(entry);
|
|
737
|
+
}
|
|
738
|
+
if (changed) task.stage_history = migrated;
|
|
739
|
+
return changed;
|
|
740
|
+
}
|
|
741
|
+
function migrateTaskWorkflowState(task) {
|
|
742
|
+
const legacyStatus = isLegacyStage(task.status) ? task.status : null;
|
|
743
|
+
const legacyRequestedAt = Array.isArray(task.stage_history) ? [...task.stage_history].reverse().find(
|
|
744
|
+
(entry) => entry && typeof entry === "object" && !Array.isArray(entry) && entry.stage === "WAITING_CONFIRM"
|
|
745
|
+
) : null;
|
|
746
|
+
let changed = migrateStageHistory(task);
|
|
747
|
+
if (legacyStatus) {
|
|
748
|
+
task.status = LEGACY_STAGE_MAP[legacyStatus];
|
|
749
|
+
changed = true;
|
|
750
|
+
}
|
|
751
|
+
if (legacyStatus === "WAITING_CONFIRM" && !task.pending_transition) {
|
|
752
|
+
const requestedAt = legacyRequestedAt && typeof legacyRequestedAt === "object" && !Array.isArray(legacyRequestedAt) ? String(
|
|
753
|
+
legacyRequestedAt.entered_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
754
|
+
) : (/* @__PURE__ */ new Date()).toISOString();
|
|
755
|
+
task.pending_transition = {
|
|
756
|
+
from: "ANALYSIS",
|
|
757
|
+
to: "IMPLEMENT",
|
|
758
|
+
requested_at: requestedAt,
|
|
759
|
+
requested_by: String(task.last_agent ?? "upgrade-migration"),
|
|
760
|
+
reason: "migrated-from-WAITING_CONFIRM"
|
|
761
|
+
};
|
|
762
|
+
changed = true;
|
|
763
|
+
}
|
|
764
|
+
if (legacyStatus === "MEMORY_LONG") {
|
|
765
|
+
const progress = task.memory_progress && typeof task.memory_progress === "object" ? { ...task.memory_progress } : {};
|
|
766
|
+
if (progress.short_memory_written !== true) {
|
|
767
|
+
progress.short_memory_written = true;
|
|
768
|
+
}
|
|
769
|
+
progress.legacy_short_memory_assumed = true;
|
|
770
|
+
progress.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
771
|
+
task.memory_progress = progress;
|
|
772
|
+
changed = true;
|
|
773
|
+
}
|
|
774
|
+
return changed;
|
|
775
|
+
}
|
|
776
|
+
async function taskFiles(cwd) {
|
|
777
|
+
const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
778
|
+
if (!await pathExists(tasksDir)) return [];
|
|
779
|
+
const entries = await readdir(tasksDir, { withFileTypes: true });
|
|
780
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => path7.join(tasksDir, entry.name, "task.json"));
|
|
781
|
+
}
|
|
782
|
+
async function sessionFiles(cwd) {
|
|
783
|
+
const sessionsDir = path7.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
784
|
+
if (!await pathExists(sessionsDir)) return [];
|
|
785
|
+
const entries = await readdir(sessionsDir, { withFileTypes: true });
|
|
786
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path7.join(sessionsDir, entry.name));
|
|
787
|
+
}
|
|
788
|
+
async function readJsonRecord(filePath) {
|
|
789
|
+
try {
|
|
790
|
+
const parsed = JSON.parse(await readTextFile(filePath));
|
|
791
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
792
|
+
} catch {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function hasLegacyWorkflowState(cwd) {
|
|
797
|
+
for (const filePath of await taskFiles(cwd)) {
|
|
798
|
+
if (!await pathExists(filePath)) continue;
|
|
799
|
+
const task = await readJsonRecord(filePath);
|
|
800
|
+
if (!task) continue;
|
|
801
|
+
if (isLegacyStage(task.status) || Array.isArray(task.stage_history) && task.stage_history.some(
|
|
802
|
+
(entry) => entry && typeof entry === "object" && !Array.isArray(entry) && isLegacyStage(entry.stage)
|
|
803
|
+
)) {
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
for (const filePath of await sessionFiles(cwd)) {
|
|
808
|
+
const session = await readJsonRecord(filePath);
|
|
809
|
+
if (!session) continue;
|
|
810
|
+
if (isLegacyStage(session.last_seen_stage)) return true;
|
|
811
|
+
}
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
async function migrateLegacyWorkflowState(cwd) {
|
|
815
|
+
let tasksUpdated = 0;
|
|
816
|
+
let sessionsUpdated = 0;
|
|
817
|
+
for (const filePath of await taskFiles(cwd)) {
|
|
818
|
+
if (!await pathExists(filePath)) continue;
|
|
819
|
+
const task = await readJsonRecord(filePath);
|
|
820
|
+
if (!task) continue;
|
|
821
|
+
if (!migrateTaskWorkflowState(task)) continue;
|
|
822
|
+
await writeTextFile(filePath, JSON.stringify(task, null, 2));
|
|
823
|
+
tasksUpdated += 1;
|
|
824
|
+
}
|
|
825
|
+
for (const filePath of await sessionFiles(cwd)) {
|
|
826
|
+
const session = await readJsonRecord(filePath);
|
|
827
|
+
if (!session) continue;
|
|
828
|
+
const mappedStage = migrateStage(session.last_seen_stage);
|
|
829
|
+
if (mappedStage === session.last_seen_stage) continue;
|
|
830
|
+
session.last_seen_stage = mappedStage;
|
|
831
|
+
await writeTextFile(filePath, JSON.stringify(session, null, 2));
|
|
832
|
+
sessionsUpdated += 1;
|
|
833
|
+
}
|
|
834
|
+
return { tasksUpdated, sessionsUpdated };
|
|
835
|
+
}
|
|
690
836
|
async function setPendingInitSince(cwd, version) {
|
|
691
837
|
const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
|
|
692
838
|
if (!await pathExists(filePath)) return;
|
|
@@ -830,6 +976,7 @@ function jsonStringContent(value) {
|
|
|
830
976
|
}
|
|
831
977
|
var HOOK_LAUNCHER_PYTHON = [
|
|
832
978
|
"import io,json,os,re,runpy,sys",
|
|
979
|
+
"sys.dont_write_bytecode=True",
|
|
833
980
|
"from pathlib import Path",
|
|
834
981
|
"payload=sys.stdin.read()",
|
|
835
982
|
"try:",
|
|
@@ -1264,6 +1411,7 @@ async function installHarnessToDir(targetDir, platforms, ctx) {
|
|
|
1264
1411
|
legacyMissingHarnessFiles: ctx.legacyMissingHarnessFiles
|
|
1265
1412
|
});
|
|
1266
1413
|
await ensureEasyCodingSessionsIgnored(targetDir);
|
|
1414
|
+
await ensureHookBytecodeIgnored(targetDir);
|
|
1267
1415
|
return artifacts;
|
|
1268
1416
|
}
|
|
1269
1417
|
function supermoduleConfigFromContext(ctx) {
|
|
@@ -1759,6 +1907,7 @@ async function addAgent(opts) {
|
|
|
1759
1907
|
mode: "merge"
|
|
1760
1908
|
});
|
|
1761
1909
|
await ensureEasyCodingSessionsIgnored(target.dir);
|
|
1910
|
+
await ensureHookBytecodeIgnored(target.dir);
|
|
1762
1911
|
await addAgentsToConfig(target.configPath, toInstall);
|
|
1763
1912
|
await setPendingInitSince(target.dir, VERSION);
|
|
1764
1913
|
installedLabels.push(`${target.label}: ${toInstall.join(", ")}`);
|
|
@@ -2831,7 +2980,7 @@ async function upgrade(opts) {
|
|
|
2831
2980
|
].filter(Boolean) : [],
|
|
2832
2981
|
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
2833
2982
|
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
2834
|
-
"Will
|
|
2983
|
+
"Will migrate legacy workflow stage metadata; memory content, spec, and project knowledge files remain untouched."
|
|
2835
2984
|
].join("\n");
|
|
2836
2985
|
if (opts.dryRun) {
|
|
2837
2986
|
console.log(summary);
|
|
@@ -2861,9 +3010,12 @@ async function upgrade(opts) {
|
|
|
2861
3010
|
artifacts
|
|
2862
3011
|
});
|
|
2863
3012
|
await ensureEasyCodingSessionsIgnored(target.dir);
|
|
3013
|
+
await ensureHookBytecodeIgnored(target.dir);
|
|
3014
|
+
await migrateLegacyWorkflowState(target.dir);
|
|
2864
3015
|
await updateHarnessVersion(target.configPath, VERSION);
|
|
2865
3016
|
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
2866
3017
|
await setPendingInitSince(target.dir, VERSION);
|
|
3018
|
+
await stripInitTaskProjectPath(target.dir);
|
|
2867
3019
|
}
|
|
2868
3020
|
if (parentTopologyRefresh) {
|
|
2869
3021
|
await refreshSupermoduleParent(
|
|
@@ -2901,7 +3053,7 @@ async function resolvePendingUpgradeTargets(targets) {
|
|
|
2901
3053
|
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
2902
3054
|
);
|
|
2903
3055
|
}
|
|
2904
|
-
if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config)) {
|
|
3056
|
+
if (relation === -1 || relation === 0 && (await needsHookConfigRefresh(target, config) || await hasLegacyWorkflowState(target.dir))) {
|
|
2905
3057
|
pending.push({ target, config });
|
|
2906
3058
|
}
|
|
2907
3059
|
}
|