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 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 -> WAITING_CONFIRM -> IMPLEMENT -> REVIEW -> VERIFICATION
73
- ^ ^ |
74
- | +---- repair loop -----+
75
- +--- revision ---+ |
76
- [user acceptance]
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
- - `WAITING_CONFIRM` 是计划确认硬门控,未确认不进入实现。
81
+ - 每个阶段完成后先记录 `pending_transition`,默认必须由用户确认才进入目标阶段;优先通过智能体原生选项功能选择确认、交接给其他智能体或使用 free-form Other,纯文本编号仅作回退。
83
82
  - `VERIFICATION` 是验证硬门控,未实际运行的 lint、typecheck、test 不算通过。
84
- - `MEMORY_SHORT` 写入本次任务短期记忆。
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 not touch memory, state, spec, or project knowledge files."
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
  }