easy-coding-harness 0.5.3 → 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 +11 -0
- package/README.md +8 -10
- package/dist/cli.js +134 -2
- 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,17 @@
|
|
|
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
|
+
|
|
9
20
|
## 0.5.3
|
|
10
21
|
|
|
11
22
|
- 根治 `task.json` 绝对路径泄漏:`.easy-coding/tasks/project-init/task.json` 不再写入本机仓库绝对路径。该 `project_path` 字段无任何消费方,直接移除,可提交产物彻底去本地化。
|
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
|
@@ -655,6 +655,11 @@ async function writeMemoryScaffold(easyCodingDir) {
|
|
|
655
655
|
// src/utils/task-json.ts
|
|
656
656
|
import { readdir } from "fs/promises";
|
|
657
657
|
import path7 from "path";
|
|
658
|
+
var LEGACY_STAGE_MAP = {
|
|
659
|
+
WAITING_CONFIRM: "ANALYSIS",
|
|
660
|
+
MEMORY_SHORT: "MEMORY",
|
|
661
|
+
MEMORY_LONG: "MEMORY"
|
|
662
|
+
};
|
|
658
663
|
function createProjectInitTask(params) {
|
|
659
664
|
return {
|
|
660
665
|
type: "project-init",
|
|
@@ -702,6 +707,132 @@ async function stripInitTaskProjectPath(cwd) {
|
|
|
702
707
|
await writeTaskJson(filePath, task);
|
|
703
708
|
return true;
|
|
704
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
|
+
}
|
|
705
836
|
async function setPendingInitSince(cwd, version) {
|
|
706
837
|
const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
|
|
707
838
|
if (!await pathExists(filePath)) return;
|
|
@@ -2849,7 +2980,7 @@ async function upgrade(opts) {
|
|
|
2849
2980
|
].filter(Boolean) : [],
|
|
2850
2981
|
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
2851
2982
|
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
2852
|
-
"Will
|
|
2983
|
+
"Will migrate legacy workflow stage metadata; memory content, spec, and project knowledge files remain untouched."
|
|
2853
2984
|
].join("\n");
|
|
2854
2985
|
if (opts.dryRun) {
|
|
2855
2986
|
console.log(summary);
|
|
@@ -2880,6 +3011,7 @@ async function upgrade(opts) {
|
|
|
2880
3011
|
});
|
|
2881
3012
|
await ensureEasyCodingSessionsIgnored(target.dir);
|
|
2882
3013
|
await ensureHookBytecodeIgnored(target.dir);
|
|
3014
|
+
await migrateLegacyWorkflowState(target.dir);
|
|
2883
3015
|
await updateHarnessVersion(target.configPath, VERSION);
|
|
2884
3016
|
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
2885
3017
|
await setPendingInitSince(target.dir, VERSION);
|
|
@@ -2921,7 +3053,7 @@ async function resolvePendingUpgradeTargets(targets) {
|
|
|
2921
3053
|
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
2922
3054
|
);
|
|
2923
3055
|
}
|
|
2924
|
-
if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config)) {
|
|
3056
|
+
if (relation === -1 || relation === 0 && (await needsHookConfigRefresh(target, config) || await hasLegacyWorkflowState(target.dir))) {
|
|
2925
3057
|
pending.push({ target, config });
|
|
2926
3058
|
}
|
|
2927
3059
|
}
|