easy-coding-harness 0.5.3 → 0.6.1
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 +27 -0
- package/README.md +13 -11
- package/dist/cli.js +134 -2
- 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-init/SKILL.md +1 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +12 -5
- package/templates/common/skills/ec-analysis/SKILL.md +87 -51
- package/templates/common/skills/ec-brainstorming/SKILL.md +2 -1
- package/templates/common/skills/ec-git/SKILL.md +1 -1
- package/templates/common/skills/ec-implementing/SKILL.md +42 -13
- package/templates/common/skills/ec-memory/SKILL.md +46 -20
- package/templates/common/skills/ec-reviewing/SKILL.md +10 -6
- package/templates/common/skills/ec-task-close/SKILL.md +1 -1
- package/templates/common/skills/ec-task-management/SKILL.md +1 -1
- package/templates/common/skills/ec-verification/SKILL.md +25 -26
- package/templates/common/skills/ec-workflow/SKILL.md +119 -69
- package/templates/main-constraint/AGENTS.md.tpl +20 -12
- package/templates/main-constraint/CLAUDE.md.tpl +20 -12
- package/templates/qoder/agents/ec-implementer.md +3 -0
- package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +2 -0
- package/templates/runtime/templates/dev-spec-skeleton.md +41 -46
- package/templates/shared-hooks/easy_coding_state.py +1040 -39
- 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,33 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.6.1
|
|
10
|
+
|
|
11
|
+
- 状态迁移按是否需要用户决策分层:`INIT → ANALYSIS` 在 INIT 工作完成后自动流转,`MEMORY → COMPLETE` 在记忆处理检查点完成后自动流转;这两条机械边不再创建 `pending_transition`,也不再展示确认或交接选项。
|
|
12
|
+
- 新增受限的 `auto-transition` 状态 API,只允许上述两条自动边;其他前进、修复和重规划边继续通过 `request-transition` / `confirm-transition` 显式确认,Hook 仍保持只读。
|
|
13
|
+
- IMPLEMENT 完成后允许用户选择进入 REVIEW,或明确跳过 REVIEW 直接进入 VERIFICATION;交接和 free-form Other 仍保留,跳过 REVIEW 不能绕过 VERIFICATION 硬门控。
|
|
14
|
+
- ANALYSIS 改为先原样落盘无阶段标签的 dev-spec 骨架,再在分析过程中即时询问并解决技术路线、接口、范围等决策问题,最后才填充完整方案;最终报告不再包含“待用户决策”章节。
|
|
15
|
+
- `ANALYSIS → IMPLEMENT` 在申请和确认迁移时都会校验完整 dev-spec 和最新有效 execution plan;代码任务还必须提供非空 test strategy,只读任务则禁止生成 `test-strategy.md`。必填章节正文、实施单元任务卡字段和并行分组均需完整,原始骨架、空章节、无界单元或缺失产物不能进入实施。
|
|
16
|
+
- dev-spec 骨架使用专用 `[[EC_TODO:...]]` 占位标记,既能可靠拦截未填字段,也不会把方案中合法的 `{title}`、`{type}` 等模板文本误判为残留占位符。
|
|
17
|
+
- execution plan 门禁校验依赖图无环及并行层级顺序;`doc` / `analysis` / `report` 显式无代码任务允许受限空文件范围并通过 `deliverable` 返回只读结果,代码任务仍禁止无界实施。
|
|
18
|
+
- 无代码 IMPLEMENT 单元必须返回非空 `deliverable` 且不得修改文件;主 Agent 在展示摘要或迁移选项前必须向用户原样输出完整 deliverable,避免结果只留在执行日志中。
|
|
19
|
+
- 无代码任务展示完整 deliverable 后直接通过受限 `IMPLEMENT → COMPLETE` 自动边结束;状态 API 会校验 single 空文件计划、匹配的 dispatch/result、零文件改动、非空 deliverable 和无遗留问题,不生成 `test-strategy.md`,不进入 REVIEW、VERIFICATION、MEMORY,也不写任务记忆。
|
|
20
|
+
- 介绍页展示版本与 `package.json` 保持一致,并由版本元数据测试防止后续发布再次漂移。
|
|
21
|
+
- 升级遗留的自动 `pending_transition` 在状态上下文中标记为 `auto-transition-ready`,不再错误注入用户确认提示。
|
|
22
|
+
- `single` / `sequential` / `parallel` 三种执行策略统一派发子代理,仅编排形态不同;技术方案骨架不再生成“主 Agent 直接执行”的冲突指令。
|
|
23
|
+
- 工作流 skills、主约束、README、设计/介绍/使用文档和状态 API 测试统一对齐新的自动边与可选 REVIEW 语义。
|
|
24
|
+
|
|
25
|
+
## 0.6.0
|
|
26
|
+
|
|
27
|
+
- 状态机移除无实际工作内容的 `WAITING_CONFIRM`;阶段完成后通过 `task.json.pending_transition` 记录待确认边,状态仍停留在当前阶段,直到用户明确确认。
|
|
28
|
+
- 所有合法阶段迁移默认统一提供“确认进入/返回目标阶段、交接给其他智能体、Other”三分支;优先使用智能体原生选项功能,纯文本编号仅作为无原生能力时的回退;handoff 会保留待确认边,下一智能体 claim 后可直接恢复该门禁。
|
|
29
|
+
- 合并 `MEMORY_SHORT` 与 `MEMORY_LONG` 为单一 `MEMORY`:先写并检查点化短期记忆,再由状态 API 计算长期记忆 `no-op/distill` 指令,原阈值门禁保持不变。
|
|
30
|
+
- MEMORY 短期记忆检查点校验 `memory_schema: 2`、`source_task` 当前任务归属和 SHA-256 内容指纹;长期记忆指令冻结 `candidate_files/kept_files`,只有明确候选允许被消费,保留项缺失或候选未清理均不能完成归档;旧 `MEMORY_LONG` 恢复通过显式兼容标记保留。
|
|
31
|
+
- MEMORY 配置强制满足 `0 <= short_term_keep <= short_term_max`;非法窗口直接阻断并提示修正 config.yaml,避免超过阈值后出现零候选、无法收敛的空蒸馏循环。
|
|
32
|
+
- 状态 API 新增 `request-transition`、`confirm-transition`、`cancel-transition` 以及 MEMORY 进度命令;hook 对用户输入完全只读,不再从原生选项、裸编号或自然语言自动写状态,所有确认均由智能体核对当前任务和目标状态后显式执行。
|
|
33
|
+
- `easy-coding upgrade` 幂等迁移 0.5.x 活跃任务和 session:`WAITING_CONFIRM → ANALYSIS`、`MEMORY_SHORT/MEMORY_LONG → MEMORY`,同步清洗阶段历史并保留记忆恢复进度。
|
|
34
|
+
- 工作流 skills、主约束、README、设计/介绍/使用文档及生成后 hook 测试全面对齐新状态模型。
|
|
35
|
+
|
|
9
36
|
## 0.5.3
|
|
10
37
|
|
|
11
38
|
- 根治 `task.json` 绝对路径泄漏:`.easy-coding/tasks/project-init/task.json` 不再写入本机仓库绝对路径。该 `project_path` 字段无任何消费方,直接移除,可提交产物彻底去本地化。
|
package/README.md
CHANGED
|
@@ -69,20 +69,22 @@ easy-coding init --submodules packages/a,packages/b
|
|
|
69
69
|
## 工作流
|
|
70
70
|
|
|
71
71
|
```text
|
|
72
|
-
INIT
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
INIT --[auto]--> ANALYSIS -> IMPLEMENT -> REVIEW -> VERIFICATION -> MEMORY --[auto]--> COMPLETE
|
|
73
|
+
\----------------> VERIFICATION
|
|
74
|
+
\--[read-only auto]------------------------------> COMPLETE
|
|
75
|
+
^ ^ | |
|
|
76
|
+
+-- replan ---+ +--- fix -----+
|
|
77
|
+
^ |
|
|
78
|
+
+------- repair ----------+
|
|
79
|
+
user-decision edge --[confirm / handoff / Other]--> target stage
|
|
79
80
|
any stage --[user abort via ec-task-close]--> CLOSED
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
- `
|
|
83
|
+
- `INIT → ANALYSIS` 与完成记忆处理后的 `MEMORY → COMPLETE` 自动流转,不生成 `pending_transition`,也不展示确认或交接。
|
|
84
|
+
- 其余用户关联边通过 `pending_transition` 等待明确确认;代码 IMPLEMENT 完成后可选择进入 REVIEW、跳过 REVIEW 直接进入 VERIFICATION,或交接给其他智能体。
|
|
85
|
+
- 显式 `doc` / `analysis` / `report` 只读任务不生成 `test-strategy.md`;IMPLEMENT 留下匹配的 dispatch/result 并展示完整报告后自动进入 COMPLETE,不执行 REVIEW、VERIFICATION 或 MEMORY,也不写任务记忆。
|
|
83
86
|
- `VERIFICATION` 是验证硬门控,未实际运行的 lint、typecheck、test 不算通过。
|
|
84
|
-
- `
|
|
85
|
-
- `MEMORY_LONG` 只有短期记忆数量超过阈值时才沉淀长期记忆,否则 no-op。
|
|
87
|
+
- `MEMORY` 先写入本次任务短期记忆,再执行长期记忆阈值门禁;未超过阈值时长期沉淀为 no-op。
|
|
86
88
|
|
|
87
89
|
## Supermodule 模型
|
|
88
90
|
|
|
@@ -117,7 +119,7 @@ any stage --[user abort via ec-task-close]--> CLOSED
|
|
|
117
119
|
| `ec-workflow` | 统一入口,负责任务创建、恢复、阶段流转和 stage skill 调度 |
|
|
118
120
|
| `ec-brainstorming` | 实现前的设计探索和方案发散 |
|
|
119
121
|
| `ec-analysis` | 生成 dev-spec、执行计划和测试策略 |
|
|
120
|
-
| `ec-implementing` |
|
|
122
|
+
| `ec-implementing` | 按确认后的计划执行代码实现或显式无代码只读交付 |
|
|
121
123
|
| `ec-reviewing` | 多维度代码审查,输出 accept / fix / replan / blocked 结论 |
|
|
122
124
|
| `ec-verification` | 执行 lint、typecheck、test 等验证硬门控,并处理验收修复循环 |
|
|
123
125
|
| `ec-memory` | 写短期记忆,并在超过阈值时沉淀长期记忆 |
|
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
|
}
|