easy-coding-harness 0.8.1-beta.0 → 0.8.1-beta.2

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,20 @@
6
6
  - `y`:常规功能升级
7
7
  - `z`:日常 bug 修复
8
8
 
9
+ ## 0.8.1-beta.2
10
+
11
+ - `ec-git` 将 `COMPLETE` 与 `CLOSED` 统一识别为终态;提交涉及这两类任务时无需再询问是否提交中间态,任务产物直接按正常提交范围处理。
12
+ - git 提交范围改为基于完整工作区变更而非当前 Agent 的写入来源;`easy-coding upgrade` 生成的受管 Harness 文件默认纳入提交候选,同时继续排除 sessions、默认排除 `spec/dev/`,并分离真正无关的预存改动。
13
+ - Claude Code、Codex、Qoder 安装回归测试与设计文档同步固化终态任务和 CLI 升级产物的提交契约。
14
+
15
+ ## 0.8.1-beta.1
16
+
17
+ - 短期记忆不再扫描目录生成 `001/002` 数字前缀,改由状态 API 生成 UUIDv7 通用 ID;文件名前缀与 schema v2 frontmatter `id` 完全一致,同时保留日期和可读摘要,避免多人或多 Agent 并发写入时重名。
18
+ - MEMORY 滑动窗口改按 frontmatter `date` 与 `id` 稳定排序;旧数字前缀和 `SM-YYYYMMDD-NNN` 记忆继续兼容读取,新检查点会拒绝 ID 与文件名前缀不一致的文件。
19
+ - 修复 Codex App 多个逻辑任务共享 Easy Coding session 的问题:session 文件不再以 PPID 为主键,统一使用 `<agent>-<session-id>.json`,避免 Claude Code、Codex、Qoder 的 ID 格式或取值冲突。
20
+ - hook session resolver 优先消费 payload `session_id`,对不安全 ID 使用稳定 hash;缺少逻辑 ID 时保留带 agent 前缀的 PPID 兼容回退,首次使用会接管旧 `<ppid>.json`,并按逻辑活跃时间清理无当前任务的过期 session。
21
+ - Codex `session-start.py` 改由 thread 级 `SessionStart` 触发;Claude Code 与 Qoder 也收敛为每个事件只有一个 session 写入 hook,避免初始化竞态。upgrade 按事件、命令和注册数量迁移旧 hook,并在 manifest 缺失时继续识别、清理 Qoder 旧 `session-start.py`;legacy `state.json` 通过原子迁移锁串行认领且仅在新 session 提交成功后删除。`ec-init` 优先沿用 hook 注入的逻辑 session,缺少 hook 上下文时先通过 snapshot 固定兼容 session,再执行 `project-init-complete`;CLI status 改为展示全部 agent session。
22
+
9
23
  ## 0.8.1-beta.0
10
24
 
11
25
  - 修复分析阶段结束后的确认门可能退化为单一“回复确认执行”提示的问题:存在 `pending_transition` 时必须实际调用平台原生选择能力,完整提供确认目标阶段、交接给其他智能体和 free-form Other。
package/dist/cli.js CHANGED
@@ -310,8 +310,8 @@ var PLATFORM_META = {
310
310
  agentFileExt: ".toml",
311
311
  mainConstraint: "AGENTS.md",
312
312
  skillTrigger: "$",
313
- hookEvents: ["UserPromptSubmit"],
314
- stateInjectEvent: ["UserPromptSubmit"],
313
+ hookEvents: ["SessionStart", "UserPromptSubmit"],
314
+ stateInjectEvent: ["SessionStart", "UserPromptSubmit"],
315
315
  hasSubagentContext: false,
316
316
  templateContext: {
317
317
  sub_agent_dispatch: "Codex sub-agent dispatch",
@@ -2954,22 +2954,45 @@ import path22 from "path";
2954
2954
  import chalk6 from "chalk";
2955
2955
 
2956
2956
  // src/utils/session.ts
2957
- import { readdir as readdir6 } from "fs/promises";
2957
+ import { readdir as readdir6, unlink } from "fs/promises";
2958
2958
  import path21 from "path";
2959
- var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
2959
+ var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1e3;
2960
+ function parseSessionFile(content) {
2961
+ try {
2962
+ return JSON.parse(content);
2963
+ } catch {
2964
+ return null;
2965
+ }
2966
+ }
2960
2967
  function getSessionDir(cwd) {
2961
2968
  return path21.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
2962
2969
  }
2963
- function getSessionFilePath(cwd, ppid) {
2964
- const pid = ppid ?? process.ppid;
2965
- return path21.join(getSessionDir(cwd), `${pid}.json`);
2966
- }
2967
- async function readSessionFile(cwd, ppid) {
2968
- const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
2969
- if (!content) {
2970
- return null;
2970
+ async function listSessionFiles(cwd) {
2971
+ const dir = getSessionDir(cwd);
2972
+ if (!await pathExists(dir)) {
2973
+ return [];
2971
2974
  }
2972
- return JSON.parse(content);
2975
+ const entries = [];
2976
+ for (const name of await readdir6(dir)) {
2977
+ if (!name.endsWith(".json")) {
2978
+ continue;
2979
+ }
2980
+ const filePath = path21.join(dir, name);
2981
+ const content = await readTextIfExists(filePath);
2982
+ if (!content) {
2983
+ continue;
2984
+ }
2985
+ const session = parseSessionFile(content);
2986
+ if (!session) {
2987
+ continue;
2988
+ }
2989
+ entries.push({
2990
+ key: name.slice(0, -".json".length),
2991
+ filePath,
2992
+ session
2993
+ });
2994
+ }
2995
+ return entries.sort((left, right) => left.key.localeCompare(right.key));
2973
2996
  }
2974
2997
 
2975
2998
  // src/commands/status.ts
@@ -2984,7 +3007,7 @@ async function status() {
2984
3007
  const tasks = await listTasks(cwd);
2985
3008
  const taskCounts = summarizeTaskStatuses(tasks);
2986
3009
  const activeTasks = tasks.filter((item) => isActiveTask(item.task));
2987
- const session = await readSessionFile(cwd);
3010
+ const sessions = await listSessionFiles(cwd);
2988
3011
  const versionRelation = compareVersions(config2.harness_version, VERSION);
2989
3012
  console.log(chalk6.bold("Harness"));
2990
3013
  console.log(` version: ${config2.harness_version}`);
@@ -3001,23 +3024,35 @@ async function status() {
3001
3024
  const projectConfirmMode = isConfirmMode(config2.behavior?.confirm_mode) ? config2.behavior.confirm_mode : "guard";
3002
3025
  console.log(` confirm_mode: ${projectConfirmMode}`);
3003
3026
  console.log("");
3004
- console.log(chalk6.bold("Session"));
3005
- const sessionConfirmMode = session?.confirm_mode;
3006
- console.log(` confirm_mode: ${sessionConfirmMode ?? "project default"}`);
3007
- console.log(` effective_confirm_mode: ${sessionConfirmMode ?? projectConfirmMode}`);
3008
- console.log(` harness: ${session?.harness_disabled ? "disabled for this session" : "enabled"}`);
3009
- if (session?.current_task) {
3027
+ console.log(chalk6.bold("Sessions"));
3028
+ console.log(` project_confirm_mode: ${projectConfirmMode}`);
3029
+ console.log(` effective_confirm_mode: ${projectConfirmMode} (without a session override)`);
3030
+ if (sessions.length === 0) {
3031
+ console.log(" no session files");
3032
+ }
3033
+ for (const { key, session } of sessions) {
3034
+ const sessionConfirmMode = session.confirm_mode;
3035
+ console.log(` - ${key}`);
3036
+ console.log(` agent: ${session.agent ?? "legacy/unknown"}`);
3037
+ console.log(` source: ${session.session_source ?? "legacy"}`);
3038
+ console.log(` confirm_mode: ${sessionConfirmMode ?? "project default"}`);
3039
+ console.log(` effective_confirm_mode: ${sessionConfirmMode ?? projectConfirmMode}`);
3040
+ console.log(
3041
+ ` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
3042
+ );
3043
+ if (!session.current_task) {
3044
+ console.log(" current_task: none");
3045
+ continue;
3046
+ }
3010
3047
  const taskPath = getTaskJsonPath(cwd, session.current_task);
3011
3048
  if (await pathExists(taskPath)) {
3012
3049
  const task = await readTaskJson(taskPath);
3013
- console.log(` current_task: ${session.current_task}`);
3014
- console.log(` current_stage: ${task.status}`);
3015
- console.log(` last_agent: ${task.last_agent}`);
3050
+ console.log(` current_task: ${session.current_task}`);
3051
+ console.log(` current_stage: ${task.status}`);
3052
+ console.log(` last_agent: ${task.last_agent}`);
3016
3053
  } else {
3017
- console.log(` current_task: ${session.current_task} (task.json missing)`);
3054
+ console.log(` current_task: ${session.current_task} (task.json missing)`);
3018
3055
  }
3019
- } else {
3020
- console.log(" no active session");
3021
3056
  }
3022
3057
  console.log("");
3023
3058
  console.log(chalk6.bold("Tasks"));
@@ -3071,16 +3106,14 @@ import chalk8 from "chalk";
3071
3106
  var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
3072
3107
  "claude-code": [
3073
3108
  { event: "SessionStart", scriptName: "session-start.py" },
3074
- { event: "UserPromptSubmit", scriptName: "session-start.py" },
3075
3109
  { event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" },
3076
3110
  { event: "PreToolUse", scriptName: "inject-subagent-context.py" }
3077
3111
  ],
3078
3112
  codex: [
3079
- { event: "UserPromptSubmit", scriptName: "session-start.py" },
3113
+ { event: "SessionStart", scriptName: "session-start.py" },
3080
3114
  { event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" }
3081
3115
  ],
3082
3116
  qoder: [
3083
- { event: "UserPromptSubmit", scriptName: "session-start.py" },
3084
3117
  { event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" },
3085
3118
  { event: "PreToolUse", scriptName: "inject-subagent-context.py" }
3086
3119
  ]
@@ -3092,7 +3125,13 @@ var MANAGED_HOOK_SCRIPT_NAMES = {
3092
3125
  )
3093
3126
  ],
3094
3127
  codex: [...new Set(EXPECTED_HOOK_REGISTRATION_SCRIPTS.codex.map(({ scriptName }) => scriptName))],
3095
- qoder: [...new Set(EXPECTED_HOOK_REGISTRATION_SCRIPTS.qoder.map(({ scriptName }) => scriptName))]
3128
+ // Keep removed registrations recognizable so same-version repair can clean them without a manifest.
3129
+ qoder: [
3130
+ .../* @__PURE__ */ new Set([
3131
+ ...EXPECTED_HOOK_REGISTRATION_SCRIPTS.qoder.map(({ scriptName }) => scriptName),
3132
+ "session-start.py"
3133
+ ])
3134
+ ]
3096
3135
  };
3097
3136
  async function upgrade(opts) {
3098
3137
  renderBanner();
@@ -3224,7 +3263,9 @@ async function needsHookConfigRefresh(target, config2) {
3224
3263
  return true;
3225
3264
  }
3226
3265
  const commandsByEvent = collectHookCommandsByEvent(parsed.hooks);
3227
- const commands = [...commandsByEvent.values()].flat();
3266
+ const actualRegistrations = [...commandsByEvent.entries()].flatMap(
3267
+ ([event, commands]) => commands.map((command) => ({ event, command }))
3268
+ );
3228
3269
  const expectedRegistrations = expectedHookRegistrations(target.dir, meta, agent, projectId);
3229
3270
  const expectedCommandSet = new Set(
3230
3271
  expectedRegistrations.map((registration) => registration.command)
@@ -3235,15 +3276,14 @@ async function needsHookConfigRefresh(target, config2) {
3235
3276
  const manifestManagedCommands = new Set(
3236
3277
  manifest?.hook_registrations.filter((registration) => registration.platform === agent).map((registration) => registration.command) ?? []
3237
3278
  );
3238
- if (commands.some(
3239
- (command) => isUnexpectedManagedHookCommand(
3240
- command,
3241
- expectedCommandSet,
3242
- target.dir,
3243
- meta,
3244
- agent,
3245
- manifestManagedCommands
3246
- )
3279
+ if (hasUnexpectedManagedHookRegistrations(
3280
+ actualRegistrations,
3281
+ expectedRegistrations,
3282
+ expectedCommandSet,
3283
+ target.dir,
3284
+ meta,
3285
+ agent,
3286
+ manifestManagedCommands
3247
3287
  )) {
3248
3288
  return true;
3249
3289
  }
@@ -3278,6 +3318,28 @@ function countRegistrations(registrations) {
3278
3318
  function hookRegistrationKey(registration) {
3279
3319
  return `${registration.event}\0${registration.command}`;
3280
3320
  }
3321
+ function hasUnexpectedManagedHookRegistrations(actualRegistrations, expectedRegistrations, expectedCommands, cwd, meta, platform, manifestManagedCommands) {
3322
+ const remainingExpected = countRegistrations(expectedRegistrations);
3323
+ for (const registration of actualRegistrations) {
3324
+ const key = hookRegistrationKey(registration);
3325
+ const remainingCount = remainingExpected.get(key) ?? 0;
3326
+ if (remainingCount > 0) {
3327
+ remainingExpected.set(key, remainingCount - 1);
3328
+ continue;
3329
+ }
3330
+ if (isManagedHookCommand(
3331
+ registration.command,
3332
+ expectedCommands,
3333
+ cwd,
3334
+ meta,
3335
+ platform,
3336
+ manifestManagedCommands
3337
+ )) {
3338
+ return true;
3339
+ }
3340
+ }
3341
+ return false;
3342
+ }
3281
3343
  function collectHookCommandsByEvent(hooks) {
3282
3344
  if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
3283
3345
  return /* @__PURE__ */ new Map();
@@ -3310,9 +3372,9 @@ function collectHookCommandsByEvent(hooks) {
3310
3372
  }
3311
3373
  return commandsByEvent;
3312
3374
  }
3313
- function isUnexpectedManagedHookCommand(command, expectedCommands, cwd, meta, platform, manifestManagedCommands) {
3375
+ function isManagedHookCommand(command, expectedCommands, cwd, meta, platform, manifestManagedCommands) {
3314
3376
  if (expectedCommands.has(command)) {
3315
- return false;
3377
+ return true;
3316
3378
  }
3317
3379
  if (manifestManagedCommands.has(command)) {
3318
3380
  return true;