easy-coding-harness 0.5.1 → 0.5.2-beta.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,11 +6,12 @@
6
6
  - `y`:常规功能升级
7
7
  - `z`:日常 bug 修复
8
8
 
9
- ## 0.5.1
9
+ ## 0.5.2-beta.0
10
10
 
11
- - 修复 Claude Code hook 命令使用相对路径导致在子目录 cwd 下找不到 `.claude/hooks/*.py` 的问题;Claude、Codex、Qoder 的 hook 配置现在安装时渲染为带引号的绝对脚本路径。
12
- - `easy-coding upgrade` 可直接刷新存量 0.5.0 项目的 hook 配置并解决该问题;升级后 `ec-init` 合规扫描会校验 hook 配置是否已使用当前版本要求的绝对路径。
13
- - `easy-coding clear` 和 `install-manifest.json` 兼容新绝对 hook 命令,清理时仍按项目相对 hook 路径识别托管注册项。
11
+ - 修复 Claude Code hook 命令使用直接相对路径导致在子目录 cwd 下找不到 `.claude/hooks/*.py` 的问题;Claude、Codex、Qoder 的 hook 配置现在使用可共享的 portable relative launcher,并绑定 `.easy-coding/config.yaml` 的 `project.id`,避免把本机仓库绝对路径写入可提交配置,同时防止 supermodule 父仓 hook 被子仓 cwd 错路由。
12
+ - `easy-coding upgrade` 可直接刷新存量 0.5.0 项目和已安装 0.5.1 beta 绝对路径配置;同版本也会修复缺失、重复、事件错位或 stale 的托管 hook 注册。
13
+ - `easy-coding clear` 和 `install-manifest.json` 兼容 portable launcher、旧直接相对命令和旧绝对 hook 命令,清理时仍按项目相对 hook 路径识别托管注册项。
14
+ - npm `0.5.1` 已发布不可覆盖,本版本作为 beta 修复包发布,用于替代原 `0.5.1` beta。
14
15
 
15
16
  ## 0.5.0
16
17
 
package/dist/cli.js CHANGED
@@ -61,6 +61,7 @@ function colorizeBanner(art) {
61
61
  }
62
62
 
63
63
  // src/utils/config-yaml.ts
64
+ import { randomUUID } from "crypto";
64
65
  import { readFile as readFile2 } from "fs/promises";
65
66
  import YAML, { isScalar, isSeq, parseDocument } from "yaml";
66
67
 
@@ -108,6 +109,7 @@ function createDefaultConfig(params) {
108
109
  harness_version: params.harnessVersion,
109
110
  agents: params.agents,
110
111
  project: {
112
+ id: params.projectId ?? createProjectId(),
111
113
  name: params.projectName
112
114
  },
113
115
  memory: {
@@ -138,6 +140,14 @@ async function readConfigYaml(filePath) {
138
140
  const content = await readFile2(filePath, "utf8");
139
141
  return YAML.parse(content);
140
142
  }
143
+ async function readProjectIdIfExists(filePath) {
144
+ try {
145
+ const config = await readConfigYaml(filePath);
146
+ return typeof config.project?.id === "string" && config.project.id.trim() ? config.project.id : null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
141
151
  async function updateConfigYaml(filePath, updater) {
142
152
  const content = await readFile2(filePath, "utf8");
143
153
  const document = parseDocument(content);
@@ -160,11 +170,27 @@ async function updateHarnessVersion(filePath, version) {
160
170
  config.harness_version = version;
161
171
  });
162
172
  }
173
+ async function ensureProjectId(filePath) {
174
+ let projectId = "";
175
+ await updateConfigYaml(filePath, (config) => {
176
+ if (!config.project || typeof config.project !== "object") {
177
+ config.project = { id: createProjectId(), name: "" };
178
+ }
179
+ if (typeof config.project.id !== "string" || !config.project.id.trim()) {
180
+ config.project.id = createProjectId();
181
+ }
182
+ projectId = config.project.id;
183
+ });
184
+ return projectId;
185
+ }
163
186
  async function updateSupermoduleConfig(filePath, supermodule) {
164
187
  return updateConfigYaml(filePath, (config) => {
165
188
  config.supermodule = supermodule;
166
189
  });
167
190
  }
191
+ function createProjectId() {
192
+ return `ec-${randomUUID()}`;
193
+ }
168
194
 
169
195
  // src/utils/gitignore.ts
170
196
  import path3 from "path";
@@ -568,17 +594,21 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
568
594
  const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
569
595
  await ensureDir(easyCodingDir);
570
596
  const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
597
+ let projectId = opts.projectId ?? createProjectId();
571
598
  if (!await pathExists(configPath2)) {
572
599
  const projectName = path6.basename(cwd);
573
600
  await writeConfigYaml(
574
601
  configPath2,
575
602
  createDefaultConfig({
576
603
  projectName,
604
+ projectId,
577
605
  harnessVersion: VERSION,
578
606
  agents,
579
607
  supermodule: opts.supermodule
580
608
  })
581
609
  );
610
+ } else {
611
+ projectId = await ensureProjectId(configPath2);
582
612
  }
583
613
  await ensureDir(path6.join(easyCodingDir, "tasks"));
584
614
  await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
@@ -586,6 +616,7 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
586
616
  await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
587
617
  await writeMemoryScaffold(easyCodingDir);
588
618
  await writeTemplatesScaffold(easyCodingDir);
619
+ return projectId;
589
620
  }
590
621
  async function writeTemplatesScaffold(easyCodingDir) {
591
622
  const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
@@ -762,17 +793,28 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
762
793
  }
763
794
  return resolved;
764
795
  }
765
- function withInstallPaths(cwd, ctx) {
766
- const hooksDir = toPosixPath(path8.resolve(cwd, ctx.platform_config_dir, "hooks"));
796
+ async function withProjectInstallPaths(cwd, ctx, projectId) {
797
+ const resolvedProjectId = projectId ?? await readProjectIdIfExists(path8.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
798
+ return withInstallPaths(cwd, ctx, resolvedProjectId ?? void 0);
799
+ }
800
+ function withInstallPaths(cwd, ctx, projectId) {
767
801
  return {
768
802
  ...ctx,
769
- platform_hooks_dir_abs: hooksDir,
770
- platform_hooks_dir_shell: jsonStringContent(shellDoubleQuoteArg(hooksDir))
803
+ platform_hook_session_start_command: jsonStringContent(
804
+ renderHookCommand(cwd, ctx, "session-start.py", process.platform, projectId)
805
+ ),
806
+ platform_hook_inject_workflow_state_command: jsonStringContent(
807
+ renderHookCommand(cwd, ctx, "inject-workflow-state.py", process.platform, projectId)
808
+ ),
809
+ platform_hook_inject_subagent_context_command: jsonStringContent(
810
+ renderHookCommand(cwd, ctx, "inject-subagent-context.py", process.platform, projectId)
811
+ )
771
812
  };
772
813
  }
773
- function renderHookCommand(cwd, ctx, scriptName, platform = process.platform) {
774
- const hooksDir = toPosixPath(path8.resolve(cwd, ctx.platform_config_dir, "hooks"));
775
- return `${ctx.python_cmd} ${shellDoubleQuoteArg(hooksDir, platform)}/${scriptName}`;
814
+ function renderHookCommand(_cwd, ctx, scriptName, platform = process.platform, projectId) {
815
+ const launcher = `import base64;exec(base64.b64decode('${Buffer.from(HOOK_LAUNCHER_PYTHON).toString("base64")}').decode())`;
816
+ const rootIdArg = projectId ? ` ${shellDoubleQuoteArg(projectId, platform)}` : "";
817
+ return `${ctx.python_cmd} -c ${shellDoubleQuoteArg(launcher, platform)}${rootIdArg} ${ctx.platform_config_dir}/hooks/${scriptName}`;
776
818
  }
777
819
  function shellDoubleQuoteArg(value, platform = process.platform) {
778
820
  if (platform === "win32") {
@@ -783,12 +825,57 @@ function shellDoubleQuoteArg(value, platform = process.platform) {
783
825
  }
784
826
  return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
785
827
  }
786
- function toPosixPath(filePath) {
787
- return filePath.split(path8.sep).join("/");
788
- }
789
828
  function jsonStringContent(value) {
790
829
  return JSON.stringify(value).slice(1, -1);
791
830
  }
831
+ var HOOK_LAUNCHER_PYTHON = [
832
+ "import io,json,os,re,runpy,sys",
833
+ "from pathlib import Path",
834
+ "payload=sys.stdin.read()",
835
+ "try:",
836
+ " data=json.loads(payload or '{}')",
837
+ "except Exception:",
838
+ " data={}",
839
+ "expected_id=sys.argv[1] if len(sys.argv) > 2 else None",
840
+ "script_arg=sys.argv[2] if len(sys.argv) > 2 else (sys.argv[1] if len(sys.argv) > 1 else None)",
841
+ "def project_id(root):",
842
+ " try:",
843
+ " lines=(root / '.easy-coding' / 'config.yaml').read_text(encoding='utf-8').splitlines()",
844
+ " except Exception:",
845
+ " return None",
846
+ " in_project=False",
847
+ " for line in lines:",
848
+ " if not in_project:",
849
+ " if re.match(r'^project\\s*:\\s*(?:#.*)?$', line):",
850
+ " in_project=True",
851
+ " continue",
852
+ " if not line.strip() or line.lstrip().startswith('#'):",
853
+ " continue",
854
+ " if not line.startswith((' ', '\\t')):",
855
+ " return None",
856
+ ` match=re.match(r"\\s+id\\s*:\\s*['\\"]?([^'\\"#\\s]+)", line)`,
857
+ " if match:",
858
+ " return match.group(1)",
859
+ " return None",
860
+ "def roots_from(raw):",
861
+ " if not raw:",
862
+ " return []",
863
+ " start=Path(raw).resolve()",
864
+ " return [candidate for candidate in (start, *start.parents) if (candidate / '.easy-coding').is_dir()]",
865
+ "sources=[os.environ.get('CLAUDE_PROJECT_DIR'), os.environ.get('QODER_PROJECT_DIR'), data.get('cwd'), os.getcwd()]",
866
+ "root=None",
867
+ "if expected_id:",
868
+ " root=next((candidate for raw in sources for candidate in roots_from(raw) if project_id(candidate) == expected_id), None)",
869
+ "else:",
870
+ " root=next((candidate for raw in sources for candidate in roots_from(raw)), None)",
871
+ "target=(root / script_arg) if root and script_arg else None",
872
+ "if target and target.is_file():",
873
+ " os.chdir(root)",
874
+ " data['cwd']=str(root)",
875
+ " sys.stdin=io.StringIO(json.dumps(data))",
876
+ " sys.path.insert(0, str(target.parent))",
877
+ " runpy.run_path(str(target), run_name='__main__')"
878
+ ].join("\n");
792
879
  async function resolveSkills(ctx) {
793
880
  const skillsRoot = getTemplatePath("common", "skills");
794
881
  const entries = await readdir2(skillsRoot);
@@ -944,7 +1031,7 @@ function stripTemplateExtension(fileName) {
944
1031
  async function configureClaude(cwd, opts = {}) {
945
1032
  const platform = "claude-code";
946
1033
  const meta = PLATFORM_META[platform];
947
- const ctx = withInstallPaths(cwd, meta.templateContext);
1034
+ const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
948
1035
  const dest = path9.join(cwd, ".claude");
949
1036
  const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
950
1037
  const artifacts = [];
@@ -982,7 +1069,7 @@ import path10 from "path";
982
1069
  async function configureCodex(cwd, opts = {}) {
983
1070
  const platform = "codex";
984
1071
  const meta = PLATFORM_META[platform];
985
- const ctx = withInstallPaths(cwd, meta.templateContext);
1072
+ const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
986
1073
  const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
987
1074
  const artifacts = [];
988
1075
  artifacts.push(
@@ -1098,7 +1185,7 @@ async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
1098
1185
  async function configureQoder(cwd, opts = {}) {
1099
1186
  const platform = "qoder";
1100
1187
  const meta = resolvePlatformMeta(cwd, platform);
1101
- const ctx = withInstallPaths(cwd, meta.templateContext);
1188
+ const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
1102
1189
  const dest = path12.join(cwd, ctx.platform_config_dir);
1103
1190
  const hookConfigPath = path12.join(cwd, meta.hookConfigFile);
1104
1191
  const artifacts = [];
@@ -1149,17 +1236,22 @@ var CONFIGURATORS = {
1149
1236
  };
1150
1237
 
1151
1238
  // src/commands/install-harness.ts
1152
- async function configurePlatformsForDir(targetDir, platforms, boundary) {
1239
+ async function configurePlatformsForDir(targetDir, platforms, opts = {}) {
1153
1240
  const artifacts = [];
1154
1241
  for (const platform of platforms) {
1155
- artifacts.push(...await CONFIGURATORS[platform](targetDir, { supermodule: boundary }));
1242
+ artifacts.push(...await CONFIGURATORS[platform](targetDir, opts));
1156
1243
  }
1157
1244
  return artifacts;
1158
1245
  }
1159
1246
  async function installHarnessToDir(targetDir, platforms, ctx) {
1160
- const artifacts = await configurePlatformsForDir(targetDir, platforms, boundaryFromContext(ctx));
1247
+ const projectId = createProjectId();
1248
+ const artifacts = await configurePlatformsForDir(targetDir, platforms, {
1249
+ supermodule: boundaryFromContext(ctx),
1250
+ projectId
1251
+ });
1161
1252
  await writeRuntimeScaffold(targetDir, platforms, {
1162
- supermodule: supermoduleConfigFromContext(ctx)
1253
+ supermodule: supermoduleConfigFromContext(ctx),
1254
+ projectId
1163
1255
  });
1164
1256
  await writeInstallManifest(targetDir, {
1165
1257
  harnessVersion: VERSION,
@@ -1653,14 +1745,13 @@ async function addAgent(opts) {
1653
1745
  const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
1654
1746
  const agents = [...config.agents, ...toInstall];
1655
1747
  if (toInstall.length > 0) {
1656
- const artifacts = await configurePlatformsForDir(
1657
- target.dir,
1658
- toInstall,
1659
- target.boundary
1660
- );
1661
- await writeRuntimeScaffold(target.dir, agents, {
1748
+ const projectId = await writeRuntimeScaffold(target.dir, agents, {
1662
1749
  supermodule: target.supermodule
1663
1750
  });
1751
+ const artifacts = await configurePlatformsForDir(target.dir, toInstall, {
1752
+ supermodule: target.boundary,
1753
+ projectId
1754
+ });
1664
1755
  await writeInstallManifest(target.dir, {
1665
1756
  harnessVersion: VERSION,
1666
1757
  agents: toInstall,
@@ -2757,14 +2848,13 @@ async function upgrade(opts) {
2757
2848
  }
2758
2849
  }
2759
2850
  for (const { target, config } of pending) {
2760
- const artifacts = await configurePlatformsForDir(
2761
- target.dir,
2762
- config.agents,
2763
- target.boundary
2764
- );
2765
- await writeRuntimeScaffold(target.dir, config.agents, {
2851
+ const projectId = await writeRuntimeScaffold(target.dir, config.agents, {
2766
2852
  supermodule: target.supermodule
2767
2853
  });
2854
+ const artifacts = await configurePlatformsForDir(target.dir, config.agents, {
2855
+ supermodule: target.boundary,
2856
+ projectId
2857
+ });
2768
2858
  await writeInstallManifest(target.dir, {
2769
2859
  harnessVersion: VERSION,
2770
2860
  agents: config.agents,
@@ -2811,15 +2901,19 @@ async function resolvePendingUpgradeTargets(targets) {
2811
2901
  `${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
2812
2902
  );
2813
2903
  }
2814
- if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config.agents)) {
2904
+ if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config)) {
2815
2905
  pending.push({ target, config });
2816
2906
  }
2817
2907
  }
2818
2908
  return pending;
2819
2909
  }
2820
- async function needsHookConfigRefresh(target, agents) {
2910
+ async function needsHookConfigRefresh(target, config) {
2911
+ const projectId = typeof config.project?.id === "string" ? config.project.id.trim() : "";
2912
+ if (!projectId) {
2913
+ return true;
2914
+ }
2821
2915
  const manifest = await readInstallManifest(target.dir);
2822
- for (const agent of agents) {
2916
+ for (const agent of config.agents) {
2823
2917
  const meta = resolvePlatformMeta(target.dir, agent);
2824
2918
  const configPath2 = path22.join(target.dir, meta.hookConfigFile);
2825
2919
  const content = await readTextIfExists(configPath2);
@@ -2834,7 +2928,7 @@ async function needsHookConfigRefresh(target, agents) {
2834
2928
  }
2835
2929
  const commandsByEvent = collectHookCommandsByEvent(parsed.hooks);
2836
2930
  const commands = [...commandsByEvent.values()].flat();
2837
- const expectedRegistrations = expectedHookRegistrations(target.dir, meta, agent);
2931
+ const expectedRegistrations = expectedHookRegistrations(target.dir, meta, agent, projectId);
2838
2932
  const expectedCommandSet = new Set(
2839
2933
  expectedRegistrations.map((registration) => registration.command)
2840
2934
  );
@@ -2859,10 +2953,10 @@ async function needsHookConfigRefresh(target, agents) {
2859
2953
  }
2860
2954
  return false;
2861
2955
  }
2862
- function expectedHookRegistrations(cwd, meta, platform) {
2956
+ function expectedHookRegistrations(cwd, meta, platform, projectId) {
2863
2957
  return EXPECTED_HOOK_REGISTRATION_SCRIPTS[platform].map(({ event, scriptName }) => ({
2864
2958
  event,
2865
- command: renderHookCommand(cwd, meta.templateContext, scriptName)
2959
+ command: renderHookCommand(cwd, meta.templateContext, scriptName, process.platform, projectId)
2866
2960
  }));
2867
2961
  }
2868
2962
  function hasExpectedHookRegistrations(commandsByEvent, expectedRegistrations) {