easy-coding-harness 0.8.1-beta.0 → 0.8.1-beta.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 +8 -0
- package/dist/cli.js +104 -42
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/settings.json +0 -10
- package/templates/codex/hooks.json +4 -2
- package/templates/common/bundled-skills/ec-init/SKILL.md +23 -3
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +2 -1
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +19 -15
- package/templates/common/skills/ec-memory/SKILL.md +8 -3
- package/templates/qoder/settings.json +1 -10
- package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +7 -4
- package/templates/runtime/templates/dev-spec-skeleton.md +1 -1
- package/templates/shared-hooks/easy_coding_state.py +328 -11
- package/templates/shared-hooks/inject-subagent-context.py +19 -3
- package/templates/shared-hooks/inject-workflow-state.py +3 -6
- package/templates/shared-hooks/session-start.py +3 -108
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.8.1-beta.1
|
|
10
|
+
|
|
11
|
+
- 短期记忆不再扫描目录生成 `001/002` 数字前缀,改由状态 API 生成 UUIDv7 通用 ID;文件名前缀与 schema v2 frontmatter `id` 完全一致,同时保留日期和可读摘要,避免多人或多 Agent 并发写入时重名。
|
|
12
|
+
- MEMORY 滑动窗口改按 frontmatter `date` 与 `id` 稳定排序;旧数字前缀和 `SM-YYYYMMDD-NNN` 记忆继续兼容读取,新检查点会拒绝 ID 与文件名前缀不一致的文件。
|
|
13
|
+
- 修复 Codex App 多个逻辑任务共享 Easy Coding session 的问题:session 文件不再以 PPID 为主键,统一使用 `<agent>-<session-id>.json`,避免 Claude Code、Codex、Qoder 的 ID 格式或取值冲突。
|
|
14
|
+
- hook session resolver 优先消费 payload `session_id`,对不安全 ID 使用稳定 hash;缺少逻辑 ID 时保留带 agent 前缀的 PPID 兼容回退,首次使用会接管旧 `<ppid>.json`,并按逻辑活跃时间清理无当前任务的过期 session。
|
|
15
|
+
- 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。
|
|
16
|
+
|
|
9
17
|
## 0.8.1-beta.0
|
|
10
18
|
|
|
11
19
|
- 修复分析阶段结束后的确认门可能退化为单一“回复确认执行”提示的问题:存在 `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
|
|
2964
|
-
const
|
|
2965
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
3005
|
-
|
|
3006
|
-
console.log(`
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
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(`
|
|
3014
|
-
console.log(`
|
|
3015
|
-
console.log(`
|
|
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(`
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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 (
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
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
|
|
3375
|
+
function isManagedHookCommand(command, expectedCommands, cwd, meta, platform, manifestManagedCommands) {
|
|
3314
3376
|
if (expectedCommands.has(command)) {
|
|
3315
|
-
return
|
|
3377
|
+
return true;
|
|
3316
3378
|
}
|
|
3317
3379
|
if (manifestManagedCommands.has(command)) {
|
|
3318
3380
|
return true;
|