easy-coding-harness 1.1.0-beta.3 → 1.1.0-beta.4
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 +9 -0
- package/package.json +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +5 -1
- package/templates/common/skills/ec-implementing/SKILL.md +3 -0
- package/templates/common/skills/ec-quality/SKILL.md +11 -8
- package/templates/common/skills/ec-workflow/SKILL.md +3 -0
- package/templates/shared-hooks/easy_coding_inputs.py +55 -38
- package/templates/shared-hooks/easy_coding_operation.py +39 -0
- package/templates/shared-hooks/easy_coding_state.py +177 -1737
- package/templates/shared-hooks/easy_coding_status.py +412 -5
- package/templates/shared-hooks/easy_coding_store.py +1193 -0
- package/templates/shared-hooks/easy_dev_spec.py +5 -4
- package/templates/shared-hooks/inject-subagent-context.py +6 -1
- package/templates/shared-hooks/inject-workflow-state.py +6 -2
- package/templates/shared-hooks/session-start.py +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 1.1.0-beta.4
|
|
10
|
+
|
|
11
|
+
- Hook 使用独立的会话存储与状态展示模块,避免加载完整工作流和 Canonical Spec 校验器;
|
|
12
|
+
同次调用复用配置、任务与执行日志读取,保留会话迁移及状态提示。
|
|
13
|
+
- 定向检查区分生产依赖、选中测试与公共辅助输入,保留构建所需依赖,减少无关测试变化导致的重跑。
|
|
14
|
+
- 验收快照复用未变 Git 对象,只读取变化内容;自动验收共享本次漂移结果,保留全仓变化检测。
|
|
15
|
+
- Canonical 写回复用同次前置检查与写后确认,保留设计新鲜度、CAS 和幂等语义;批量准备、
|
|
16
|
+
登记检查作为默认调用方式,减少重复状态查询。
|
|
17
|
+
|
|
9
18
|
## 1.1.0-beta.3
|
|
10
19
|
|
|
11
20
|
- MEMORY 以可复用开发知识为正文,提炼业务规则、设计原因、修改入口、适用边界与真实踩坑经验;
|
package/package.json
CHANGED
|
@@ -208,7 +208,11 @@ Acceptance: <observable outcome and minimum check>
|
|
|
208
208
|
Record Unit `input_files` for the known additional direct inputs (an empty list means the
|
|
209
209
|
Unit files are self-contained). Include shared helpers, fixtures, schemas and configuration
|
|
210
210
|
actually consumed by its checks. Without a declared closure, checks cover the owning module.
|
|
211
|
-
|
|
211
|
+
For targeted Vitest/Jest/Python/Node checks, name the selected test file in the command and declare
|
|
212
|
+
its helper/fixture inputs so unrelated test cases can retain their evidence. Module production
|
|
213
|
+
inputs and build configuration remain included. Maven/Gradle/tsc compilation still includes
|
|
214
|
+
neighboring sources/tests even with a test selector. Do not infer a whole-program call graph.
|
|
215
|
+
Keep Unit contracts and test points in the existing execution plan. Do not duplicate them across
|
|
212
216
|
full template chapters or create a separate test strategy for this compact form. Canonical work
|
|
213
217
|
consumes the selected source closure; it does not redesign unrelated selected tasks.
|
|
214
218
|
|
|
@@ -37,6 +37,9 @@ tracking metadata is handled once; do not reopen design or rewrite the complete
|
|
|
37
37
|
|
|
38
38
|
When a lifecycle check is necessary, call `prepare-check` before running it and `record-check`
|
|
39
39
|
afterward as documented in ec-quality. QUALITY reuses these input-bound results.
|
|
40
|
+
Batch independent checks at the same code state with the array forms of `--record` and `--result`.
|
|
41
|
+
Use each state operation's returned context and next action; query again only when required
|
|
42
|
+
information is absent or an intervening edit makes the returned context stale.
|
|
40
43
|
|
|
41
44
|
## Non-negotiable gates
|
|
42
45
|
|
|
@@ -22,24 +22,27 @@ Call `evidence-fingerprints` once to obtain the runtime-owned attempt and candid
|
|
|
22
22
|
owns signatures and prior-evidence references. Never calculate historical fingerprints, import
|
|
23
23
|
runtime internals to reconstruct old candidates, or ask a reviewer to audit workflow bookkeeping.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Prepare independent checks for the same unchanged candidate in one batch before executing them:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py prepare-check \
|
|
29
|
-
--record '<review/verify JSON with unit_id, dimension or check/check_type/command
|
|
29
|
+
--record '[<review/verify JSON with unit_id, dimension or check/check_type/command>, ...]' \
|
|
30
30
|
--agent <agent-id> --session-file <P>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
For each returned item with `reusable:true`, use its evidence index and skip that check. Run the
|
|
34
|
+
remaining checks once, then register their real results in one batch:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py record-check \
|
|
38
|
-
--
|
|
38
|
+
--result '[{"prepared_id":"<returned-id>","result":<JSON with passed, exit_code for verification, findings/reviewer for review>}, ...]' \
|
|
39
39
|
--agent <agent-id> --session-file <P>
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Preparation binds code/test inputs, module dependencies, build files and the actual command.
|
|
43
|
+
Batching preserves each check's result and evidence identity. A single check may still use
|
|
44
|
+
`--prepared-id` with one result object. Start a new preparation batch after a code change;
|
|
45
|
+
never reuse a pre-edit input snapshot for post-edit checks.
|
|
43
46
|
Use the analyzed Unit `input_files` closure for additional helpers/fixtures/configuration, and record intentional
|
|
44
47
|
environment overrides in the check descriptor. Production-only reviews may declare
|
|
45
48
|
`review_scope:"production"`; test review still covers changed test behavior. A result is accepted
|
|
@@ -185,9 +188,9 @@ This returns other-Agent repairs to the coordinator for delta review/verificatio
|
|
|
185
188
|
may pass while the repair is pending. Scope/contract changes must be resolved rather than silently
|
|
186
189
|
included in the accepted bundle. Repeated starts/completions reuse the existing repair ID.
|
|
187
190
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
Use the same batch prepare/record flow for repair checks. Consume returned state and next-action
|
|
192
|
+
fields directly; do not follow a successful state operation with an unchanged `snapshot` or
|
|
193
|
+
Spec inspection merely to retrieve fields already returned.
|
|
191
194
|
|
|
192
195
|
After repair, choose the minimum honest evidence refresh:
|
|
193
196
|
|
|
@@ -57,6 +57,9 @@ or `qoder`. Never use a display or source-author attribution such as `Codex with
|
|
|
57
57
|
|
|
58
58
|
1. Read the injected state breadcrumbs or call:
|
|
59
59
|
|
|
60
|
+
Prefer the injected or most recent operation's state. Call `snapshot` only when that context
|
|
61
|
+
is missing or stale; do not repeat it after a successful operation that already returns state.
|
|
62
|
+
|
|
60
63
|
```bash
|
|
61
64
|
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py snapshot --agent <agent-id> --session-file <P>
|
|
62
65
|
```
|
|
@@ -10,47 +10,16 @@ import stat
|
|
|
10
10
|
import subprocess
|
|
11
11
|
import sys
|
|
12
12
|
import xml.etree.ElementTree as ET
|
|
13
|
-
from
|
|
14
|
-
from contextvars import ContextVar
|
|
13
|
+
from easy_coding_operation import evidence_operation, memo, cached_memo, invalidate_memo
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
_operation = ContextVar("evidence_operation", default=None)
|
|
19
17
|
BUILD_FILES = ("pom.xml", "package.json", "package-lock.json", "pnpm-lock.yaml",
|
|
20
18
|
"yarn.lock", "tsconfig.json", "build.gradle", "build.gradle.kts",
|
|
21
19
|
"settings.gradle", "gradle.properties", ".gitattributes", ".npmrc",
|
|
22
20
|
"vitest.config.ts", "jest.config.js", "biome.json", "pytest.ini", "pyproject.toml")
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
@contextmanager
|
|
26
|
-
def evidence_operation():
|
|
27
|
-
token = _operation.set({})
|
|
28
|
-
try:
|
|
29
|
-
yield
|
|
30
|
-
finally:
|
|
31
|
-
_operation.reset(token)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def memo(key, compute):
|
|
35
|
-
cache = _operation.get()
|
|
36
|
-
if cache is None:
|
|
37
|
-
return compute()
|
|
38
|
-
if key not in cache:
|
|
39
|
-
cache[key] = compute()
|
|
40
|
-
return cache[key]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def invalidate_memo(key):
|
|
44
|
-
cache = _operation.get()
|
|
45
|
-
if cache is not None:
|
|
46
|
-
cache.pop(key, None)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def cached_memo(key):
|
|
50
|
-
cache = _operation.get()
|
|
51
|
-
return cache.get(key) if cache is not None else None
|
|
52
|
-
|
|
53
|
-
|
|
54
23
|
def command_identity(command):
|
|
55
24
|
# Shell 展开和重定向受引号影响;只归一化普通 argv 命令,避免错误复用。
|
|
56
25
|
if re.search(r"[\$`\\\n;|&<>*?\[\]{}~#]", command):
|
|
@@ -153,7 +122,7 @@ def is_test(path):
|
|
|
153
122
|
|
|
154
123
|
def is_test_case(path):
|
|
155
124
|
name = Path(path).name
|
|
156
|
-
return bool(re.search(r"(?:Test|Tests|IT)\.java$|\.(?:test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$", name))
|
|
125
|
+
return bool(re.search(r"(?:Test|Tests|IT)\.java$|\.(?:test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$", name))
|
|
157
126
|
|
|
158
127
|
|
|
159
128
|
def command_tokens(command):
|
|
@@ -165,6 +134,36 @@ def command_tokens(command):
|
|
|
165
134
|
return tokens
|
|
166
135
|
|
|
167
136
|
|
|
137
|
+
def targeted_test_files(command):
|
|
138
|
+
"""只识别完整、无 shell 展开的文件选择调用;未知参数保持模块范围。"""
|
|
139
|
+
if not isinstance(command_identity(command), tuple):
|
|
140
|
+
return set()
|
|
141
|
+
tokens = command_tokens(command)
|
|
142
|
+
if not tokens:
|
|
143
|
+
return set()
|
|
144
|
+
if Path(tokens[0]).name == "npx":
|
|
145
|
+
tokens = tokens[1:]
|
|
146
|
+
elif tokens[:2] in (["pnpm", "exec"], ["npm", "exec"]):
|
|
147
|
+
tokens = tokens[2:]
|
|
148
|
+
if tokens[:1] == ["--"]:
|
|
149
|
+
tokens = tokens[1:]
|
|
150
|
+
if not tokens:
|
|
151
|
+
return set()
|
|
152
|
+
runner, arguments = Path(tokens[0]).name, tokens[1:]
|
|
153
|
+
if runner in {"python", "python3"} and arguments[:2] == ["-m", "pytest"]:
|
|
154
|
+
runner, arguments = "pytest", arguments[2:]
|
|
155
|
+
if runner == "vitest" and arguments[:1] == ["run"]:
|
|
156
|
+
arguments = arguments[1:]
|
|
157
|
+
elif runner == "node" and arguments[:1] == ["--test"]:
|
|
158
|
+
arguments = arguments[1:]
|
|
159
|
+
elif runner not in {"jest", "pytest", "python", "python3"}:
|
|
160
|
+
return set()
|
|
161
|
+
files = [value for value in arguments if value not in {"-q", "-v", "--runInBand", "--coverage"}]
|
|
162
|
+
if not files or any(value.startswith("-") or not is_test_case(value) for value in files):
|
|
163
|
+
return set()
|
|
164
|
+
return {str(Path(value)) for value in files}
|
|
165
|
+
|
|
166
|
+
|
|
168
167
|
def toolchain_identity(command):
|
|
169
168
|
tokens = command_tokens(command)
|
|
170
169
|
executable = tokens[0] if tokens else ""
|
|
@@ -220,14 +219,25 @@ def input_spec(root, task, plan, check):
|
|
|
220
219
|
tokens = command_tokens(command)
|
|
221
220
|
executable = Path(tokens[0]).name if tokens else ""
|
|
222
221
|
builds_module = check.get("type") == "verify" and executable in {
|
|
223
|
-
"mvn", "mvnw", "gradle", "gradlew", "npm", "npx", "pnpm", "yarn", "tsc"
|
|
222
|
+
"mvn", "mvnw", "gradle", "gradlew", "npm", "npx", "pnpm", "yarn", "tsc",
|
|
223
|
+
"vitest", "jest", "pytest", "python", "python3", "node",
|
|
224
224
|
}
|
|
225
|
+
selected_tests = targeted_test_files(command)
|
|
225
226
|
repositories = {}
|
|
227
|
+
production_repositories = {}
|
|
226
228
|
for unit in selected:
|
|
227
229
|
repo_id = unit.get("repo_id") or "current"
|
|
228
230
|
base = Path(task.get("repo_paths", {}).get(repo_id, root))
|
|
229
231
|
base = (base if base.is_absolute() else root / base).resolve()
|
|
230
232
|
paths = repositories.setdefault(str(base), set())
|
|
233
|
+
# 只有明确文件选择器和已声明依赖闭包的脚本测试才收窄测试输入。
|
|
234
|
+
# Maven/Gradle/tsc 的编译阶段仍消费模块内源文件,不能按测试名称裁剪。
|
|
235
|
+
test_files = {str(Path(name)) for name in [*unit.get("files", []), *unit.get("input_files", [])]
|
|
236
|
+
if is_test_case(name)}
|
|
237
|
+
targeted_script = (
|
|
238
|
+
check.get("type") == "verify" and "input_files" in unit
|
|
239
|
+
and bool(selected_tests) and selected_tests <= test_files
|
|
240
|
+
)
|
|
231
241
|
for name in [*unit.get("files", []), *unit.get("input_files", [])]:
|
|
232
242
|
absolute = Path(name) if Path(name).is_absolute() else base / name
|
|
233
243
|
relative = absolute.relative_to(base).as_posix()
|
|
@@ -261,16 +271,18 @@ def input_spec(root, task, plan, check):
|
|
|
261
271
|
pending.append(candidate)
|
|
262
272
|
for module in involved:
|
|
263
273
|
for folder in ("src/main", "src" if not (module / "pom.xml").exists() else "src/test"):
|
|
264
|
-
if (builds_module or "input_files" not in unit) and (module / folder).is_dir():
|
|
265
|
-
|
|
274
|
+
if (builds_module or targeted_script or "input_files" not in unit) and (module / folder).is_dir():
|
|
275
|
+
destination = (production_repositories.setdefault(str(base), set())
|
|
276
|
+
if targeted_script else paths)
|
|
277
|
+
destination.add((module / folder).relative_to(base).as_posix())
|
|
266
278
|
for parent in [module, *module.parents]:
|
|
267
279
|
if not parent.is_relative_to(base):
|
|
268
280
|
break
|
|
269
281
|
for filename in BUILD_FILES:
|
|
270
282
|
if (parent / filename).is_file():
|
|
271
283
|
paths.add((parent / filename).relative_to(base).as_posix())
|
|
272
|
-
for config in (".mvn", "gradle", "test" if builds_module else "test/fixtures",
|
|
273
|
-
"tests" if builds_module else "tests/fixtures"):
|
|
284
|
+
for config in (".mvn", "gradle", "test" if builds_module and not targeted_script else "test/fixtures",
|
|
285
|
+
"tests" if builds_module and not targeted_script else "tests/fixtures"):
|
|
274
286
|
if (base / config).is_dir():
|
|
275
287
|
paths.add(config)
|
|
276
288
|
if task.get("unit_test_mode") in {"ut", "tdd"}:
|
|
@@ -284,6 +296,8 @@ def input_spec(root, task, plan, check):
|
|
|
284
296
|
return {
|
|
285
297
|
"schema": 1,
|
|
286
298
|
"repositories": {r: sorted(paths) for r, paths in sorted(repositories.items())},
|
|
299
|
+
**({"production_repositories": {r: sorted(paths) for r, paths in sorted(production_repositories.items())}}
|
|
300
|
+
if production_repositories else {}),
|
|
287
301
|
"production_only": check.get("review_scope") == "production",
|
|
288
302
|
"contract": [{"id": u["id"], "contracts": u.get("contracts", []),
|
|
289
303
|
"acceptance_criteria": u.get("acceptance_criteria", [])} for u in owners]
|
|
@@ -299,6 +313,9 @@ def capture(spec):
|
|
|
299
313
|
for root, scopes in spec["repositories"].items():
|
|
300
314
|
repository = memo(("repository", root), lambda: RepositoryInputs(Path(root)))
|
|
301
315
|
inputs[root] = repository.capture(scopes, spec["production_only"])
|
|
316
|
+
production_scopes = spec.get("production_repositories", {}).get(root, [])
|
|
317
|
+
if production_scopes:
|
|
318
|
+
inputs[root].update(repository.capture(production_scopes, production_only=True))
|
|
302
319
|
return {"spec": spec, "files": inputs, "signature": digest([spec, inputs])}
|
|
303
320
|
|
|
304
321
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Operation-local caches shared by hooks and state commands."""
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_operation = ContextVar("evidence_operation", default=None)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@contextmanager
|
|
11
|
+
def evidence_operation():
|
|
12
|
+
if _operation.get() is not None:
|
|
13
|
+
yield
|
|
14
|
+
return
|
|
15
|
+
token = _operation.set({})
|
|
16
|
+
try:
|
|
17
|
+
yield
|
|
18
|
+
finally:
|
|
19
|
+
_operation.reset(token)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def memo(key, compute):
|
|
23
|
+
cache = _operation.get()
|
|
24
|
+
if cache is None:
|
|
25
|
+
return compute()
|
|
26
|
+
if key not in cache:
|
|
27
|
+
cache[key] = compute()
|
|
28
|
+
return cache[key]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def invalidate_memo(key):
|
|
32
|
+
cache = _operation.get()
|
|
33
|
+
if cache is not None:
|
|
34
|
+
cache.pop(key, None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cached_memo(key):
|
|
38
|
+
cache = _operation.get()
|
|
39
|
+
return cache.get(key) if cache is not None else None
|