easy-coding-harness 0.10.0-beta.5 → 0.10.0-beta.7
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 +33 -0
- package/README.md +17 -10
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +10 -0
- package/templates/claude/agents/ec-reviewer.md +6 -1
- package/templates/codex/agents/ec-implementer.toml +10 -0
- package/templates/codex/agents/ec-reviewer.toml +6 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +8 -1
- package/templates/common/skills/ec-analysis/SKILL.md +74 -21
- package/templates/common/skills/ec-implementing/SKILL.md +27 -2
- package/templates/common/skills/ec-reviewing/SKILL.md +11 -1
- package/templates/common/skills/ec-workflow/SKILL.md +33 -17
- package/templates/main-constraint/AGENTS.md.tpl +5 -0
- package/templates/main-constraint/CLAUDE.md.tpl +5 -0
- package/templates/qoder/agents/ec-implementer.md +10 -0
- package/templates/qoder/agents/ec-reviewer.md +6 -1
- package/templates/shared-hooks/easy_coding_state.py +126 -48
- package/templates/shared-hooks/easy_dev_spec.py +385 -46
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
|
|
17
17
|
from easy_dev_spec import (
|
|
18
18
|
EasyDevSpecError,
|
|
19
|
+
inspect_manifest,
|
|
19
20
|
inspect_spec,
|
|
20
21
|
inspection_summary,
|
|
21
22
|
select_consumption_scopes,
|
|
@@ -86,10 +87,24 @@ WORKFLOW_MODES = {"fast", "standard", "strict"}
|
|
|
86
87
|
WORKFLOW_MODE_RANK = {"fast": 0, "standard": 1, "strict": 2}
|
|
87
88
|
STRICT_VERIFICATION_CHECK_TYPES = {"lint", "typecheck", "test", "build"}
|
|
88
89
|
REVIEW_FINDING_SEVERITIES = {"error", "warning", "info"}
|
|
89
|
-
|
|
90
|
-
r"(
|
|
91
|
-
r"
|
|
92
|
-
r"
|
|
90
|
+
HIGH_WORKFLOW_RISK_PATTERN = re.compile(
|
|
91
|
+
r"(\bhigh[-_ ]?risk\b|\bcritical\b|\bsevere\b|\birreversible\b|"
|
|
92
|
+
r"\bdata[-_ ]?loss\b|\bfinancial[-_ ]?loss\b|"
|
|
93
|
+
r"\bsecurity[-_ ]?(boundary|breach)\b|\bprivilege[-_ ]?escalation\b|"
|
|
94
|
+
r"\bproduction[-_ ]?outage\b|"
|
|
95
|
+
r"高风险|严重|不可逆|数据丢失|资损|安全边界|安全事件|权限提升|生产故障)",
|
|
96
|
+
re.IGNORECASE,
|
|
97
|
+
)
|
|
98
|
+
NEGATED_HIGH_WORKFLOW_RISK_PATTERN = re.compile(
|
|
99
|
+
r"(\b(?:non[-_ ]?|not[-_ ]+|no[-_ ]+)(?:high[-_ ]?risk|critical|severe|irreversible)\b|"
|
|
100
|
+
r"\b(?:no|without)[-_ ]+(?:risk[-_ ]+of[-_ ]+)?(?:data[-_ ]?loss|"
|
|
101
|
+
r"financial[-_ ]?loss|security[-_ ]?breach|production[-_ ]?outage)\b|"
|
|
102
|
+
r"低风险|非高风险|不严重|(?<!不)可逆|无(?:数据丢失|资损|安全事件|生产故障)|"
|
|
103
|
+
r"不会导致(?:数据丢失|资损|安全事件|生产故障))",
|
|
104
|
+
re.IGNORECASE,
|
|
105
|
+
)
|
|
106
|
+
WIDE_WORKFLOW_CONTRACT_PATTERN = re.compile(
|
|
107
|
+
r"(cross[-_ ]?repo|public[-_ ]?(api|contract)|跨仓|公共接口|公共契约)",
|
|
93
108
|
re.IGNORECASE,
|
|
94
109
|
)
|
|
95
110
|
DEFAULT_APPROVAL_MODE = "guard"
|
|
@@ -1588,7 +1603,7 @@ def is_valid_execution_plan(
|
|
|
1588
1603
|
has_empty_file_scope = True
|
|
1589
1604
|
if not is_string_list(unit.get("depends_on")):
|
|
1590
1605
|
return False
|
|
1591
|
-
for optional_list in ("rules_sections", "abstract_modules"):
|
|
1606
|
+
for optional_list in ("rules_sections", "abstract_modules", "local_baseline"):
|
|
1592
1607
|
if optional_list in unit and not is_string_list(unit.get(optional_list)):
|
|
1593
1608
|
return False
|
|
1594
1609
|
if require_unit_contracts:
|
|
@@ -1754,8 +1769,16 @@ def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
|
|
|
1754
1769
|
if stored.get(field) != expected.get(field):
|
|
1755
1770
|
raise StateError("Canonical Spec dependency metadata no longer matches source selection.")
|
|
1756
1771
|
refreshed = dict(stored)
|
|
1757
|
-
|
|
1758
|
-
|
|
1772
|
+
for field in (
|
|
1773
|
+
"status",
|
|
1774
|
+
"shared_status",
|
|
1775
|
+
"dependency_task_status",
|
|
1776
|
+
"basis",
|
|
1777
|
+
):
|
|
1778
|
+
if expected.get(field) is None:
|
|
1779
|
+
refreshed.pop(field, None)
|
|
1780
|
+
else:
|
|
1781
|
+
refreshed[field] = expected.get(field)
|
|
1759
1782
|
if expected.get("evidence"):
|
|
1760
1783
|
refreshed["evidence"] = expected.get("evidence")
|
|
1761
1784
|
refreshed_dependencies.append(refreshed)
|
|
@@ -2270,6 +2293,42 @@ def task_repository_roots(root: Path, task: dict | None, plan: dict) -> list[Pat
|
|
|
2270
2293
|
]
|
|
2271
2294
|
|
|
2272
2295
|
|
|
2296
|
+
def workflow_plan_repository_roots(root: Path, task: dict, plan: dict) -> list[Path]:
|
|
2297
|
+
"""Resolve only repositories that own files in the current execution plan."""
|
|
2298
|
+
repositories: set[Path] = set()
|
|
2299
|
+
repo_paths = task.get("repo_paths")
|
|
2300
|
+
canonical = isinstance(task.get("spec_source"), dict)
|
|
2301
|
+
|
|
2302
|
+
for unit in plan.get("units", []):
|
|
2303
|
+
if not isinstance(unit, dict):
|
|
2304
|
+
continue
|
|
2305
|
+
if canonical:
|
|
2306
|
+
repo_id = unit.get("repo_id")
|
|
2307
|
+
if not is_non_empty_string(repo_id) or not isinstance(repo_paths, dict):
|
|
2308
|
+
raise StateError("Canonical workflow Unit is missing its repository binding.")
|
|
2309
|
+
raw_repo_path = repo_paths.get(str(repo_id))
|
|
2310
|
+
if not is_non_empty_string(raw_repo_path):
|
|
2311
|
+
raise StateError(f"Canonical workflow repository path is missing: {repo_id}")
|
|
2312
|
+
candidate = Path(str(raw_repo_path))
|
|
2313
|
+
resolved = (candidate if candidate.is_absolute() else root / candidate).resolve()
|
|
2314
|
+
repository = git_repository_root(resolved)
|
|
2315
|
+
if repository is None or repository.resolve() != resolved:
|
|
2316
|
+
raise StateError(f"Canonical workflow repository binding is not a Git root: {repo_id}")
|
|
2317
|
+
repositories.add(repository.resolve())
|
|
2318
|
+
continue
|
|
2319
|
+
|
|
2320
|
+
for file_name in unit.get("files", []):
|
|
2321
|
+
if not is_non_empty_string(file_name):
|
|
2322
|
+
continue
|
|
2323
|
+
candidate = Path(str(file_name))
|
|
2324
|
+
resolved = (candidate if candidate.is_absolute() else root / candidate).resolve()
|
|
2325
|
+
repository = git_repository_root(resolved)
|
|
2326
|
+
if repository is not None:
|
|
2327
|
+
repositories.add(repository.resolve())
|
|
2328
|
+
|
|
2329
|
+
return sorted(repositories, key=lambda item: item.as_posix())
|
|
2330
|
+
|
|
2331
|
+
|
|
2273
2332
|
def tdd_repositories(root: Path, task: dict, plan: dict) -> dict[str, Path]:
|
|
2274
2333
|
if isinstance(task.get("spec_source"), dict):
|
|
2275
2334
|
repo_paths = task.get("repo_paths")
|
|
@@ -6074,54 +6133,65 @@ def calculate_workflow_floor(root: Path, task_id: str) -> tuple[str, list[str]]:
|
|
|
6074
6133
|
if not plan:
|
|
6075
6134
|
raise StateError("Cannot calculate workflow floor without a valid execution plan.")
|
|
6076
6135
|
units = [unit for unit in plan.get("units", []) if isinstance(unit, dict)]
|
|
6136
|
+
missing_local_baseline = [
|
|
6137
|
+
str(unit.get("id") or "<unknown>")
|
|
6138
|
+
for unit in units
|
|
6139
|
+
if not is_string_list(unit.get("local_baseline"), allow_empty=False)
|
|
6140
|
+
]
|
|
6141
|
+
if missing_local_baseline:
|
|
6142
|
+
raise StateError(
|
|
6143
|
+
"Workflow plan Units must record a non-empty local_baseline: "
|
|
6144
|
+
+ ", ".join(missing_local_baseline)
|
|
6145
|
+
)
|
|
6077
6146
|
files = {
|
|
6078
6147
|
str(file_name)
|
|
6079
6148
|
for unit in units
|
|
6080
6149
|
for file_name in unit.get("files", [])
|
|
6081
6150
|
if is_non_empty_string(file_name)
|
|
6082
6151
|
}
|
|
6083
|
-
repositories =
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6152
|
+
repositories = workflow_plan_repository_roots(root, task, plan)
|
|
6153
|
+
ignored_values = {"none", "no", "n/a", "无", "无风险"}
|
|
6154
|
+
risk_values = [
|
|
6155
|
+
str(item)
|
|
6156
|
+
for unit in units
|
|
6157
|
+
for item in unit.get("risks", [])
|
|
6158
|
+
if is_non_empty_string(item) and str(item).strip().lower() not in ignored_values
|
|
6159
|
+
]
|
|
6160
|
+
contract_values = [
|
|
6161
|
+
str(item)
|
|
6162
|
+
for unit in units
|
|
6163
|
+
for item in unit.get("contracts", [])
|
|
6164
|
+
if is_non_empty_string(item) and str(item).strip().lower() not in ignored_values
|
|
6165
|
+
]
|
|
6166
|
+
risk_text = NEGATED_HIGH_WORKFLOW_RISK_PATTERN.sub("", " ".join(risk_values))
|
|
6167
|
+
high_risk = bool(HIGH_WORKFLOW_RISK_PATTERN.search(risk_text))
|
|
6168
|
+
|
|
6169
|
+
complexity_reasons: list[str] = []
|
|
6170
|
+
if len(repositories) > 1:
|
|
6171
|
+
complexity_reasons.append("cross-repository-change")
|
|
6172
|
+
if len(units) >= 4 or len(files) >= 10:
|
|
6173
|
+
complexity_reasons.append("broad-change-scope")
|
|
6174
|
+
if WIDE_WORKFLOW_CONTRACT_PATTERN.search(" ".join(contract_values)):
|
|
6175
|
+
complexity_reasons.append("wide-contract-impact")
|
|
6176
|
+
if high_risk and complexity_reasons:
|
|
6177
|
+
return "strict", [
|
|
6178
|
+
"compound-high-risk-and-complexity",
|
|
6179
|
+
"explicit-high-risk-signal",
|
|
6180
|
+
*complexity_reasons,
|
|
6104
6181
|
]
|
|
6105
|
-
)
|
|
6106
|
-
strict_reasons: list[str] = []
|
|
6107
|
-
if repo_count > 1:
|
|
6108
|
-
strict_reasons.append("cross-repository-scope")
|
|
6109
|
-
if len(units) >= 4 or len(files) >= 8:
|
|
6110
|
-
strict_reasons.append("broad-change-scope")
|
|
6111
|
-
if STRICT_WORKFLOW_RISK_PATTERN.search(risk_text):
|
|
6112
|
-
strict_reasons.append("high-risk-contract-or-domain")
|
|
6113
|
-
if strict_reasons:
|
|
6114
|
-
return "strict", strict_reasons
|
|
6115
6182
|
|
|
6116
6183
|
standard_reasons: list[str] = []
|
|
6184
|
+
if high_risk:
|
|
6185
|
+
standard_reasons.append("bounded-high-risk-change")
|
|
6186
|
+
standard_reasons.extend(complexity_reasons)
|
|
6117
6187
|
if len(units) > 1:
|
|
6118
6188
|
standard_reasons.append("multiple-units")
|
|
6119
|
-
if len(files)
|
|
6189
|
+
if len(files) > 5:
|
|
6120
6190
|
standard_reasons.append("multi-file-impact")
|
|
6121
6191
|
if plan.get("strategy") == "parallel":
|
|
6122
6192
|
standard_reasons.append("parallel-execution")
|
|
6123
6193
|
if standard_reasons:
|
|
6124
|
-
return "standard", standard_reasons
|
|
6194
|
+
return "standard", list(dict.fromkeys(standard_reasons))
|
|
6125
6195
|
return "fast", ["single-bounded-unit"]
|
|
6126
6196
|
|
|
6127
6197
|
|
|
@@ -6735,6 +6805,8 @@ def main() -> int:
|
|
|
6735
6805
|
inspect_spec_parser = subcommands.add_parser("inspect-dev-spec", parents=[common])
|
|
6736
6806
|
inspect_spec_parser.add_argument("--spec", required=True)
|
|
6737
6807
|
inspect_spec_parser.add_argument("--repo-path", action="append", default=[])
|
|
6808
|
+
inspect_spec_parser.add_argument("--spec-task", action="append", default=[])
|
|
6809
|
+
inspect_spec_parser.add_argument("--manifest-only", action="store_true")
|
|
6738
6810
|
|
|
6739
6811
|
initialize_spec = subcommands.add_parser("initialize-spec-execution", parents=[common])
|
|
6740
6812
|
initialize_spec.add_argument("--spec", required=True)
|
|
@@ -6756,7 +6828,7 @@ def main() -> int:
|
|
|
6756
6828
|
create_from_spec.add_argument("--task-id", required=True)
|
|
6757
6829
|
create_from_spec.add_argument("--type", required=True)
|
|
6758
6830
|
create_from_spec.add_argument("--title", required=True)
|
|
6759
|
-
create_from_spec.add_argument("--repo-path",
|
|
6831
|
+
create_from_spec.add_argument("--repo-path", action="append", default=[])
|
|
6760
6832
|
create_from_spec.add_argument("--dependency-evidence", action="append", default=[])
|
|
6761
6833
|
create_from_spec.add_argument("--agent", required=True)
|
|
6762
6834
|
create_from_spec.add_argument("--no-set-current", action="store_true")
|
|
@@ -7017,15 +7089,21 @@ def main() -> int:
|
|
|
7017
7089
|
emit(snapshot_state(root, session_file))
|
|
7018
7090
|
elif command == "inspect-dev-spec":
|
|
7019
7091
|
spec_path = Path(args.spec).expanduser()
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7092
|
+
if args.manifest_only and args.spec_task:
|
|
7093
|
+
raise StateError("--manifest-only cannot be combined with --spec-task")
|
|
7094
|
+
resolved_spec = spec_path if spec_path.is_absolute() else root / spec_path
|
|
7095
|
+
repo_paths = parse_mapping_args(args.repo_path, "--repo-path")
|
|
7096
|
+
inspection = (
|
|
7097
|
+
inspect_manifest(resolved_spec, root, repo_paths)
|
|
7098
|
+
if args.manifest_only
|
|
7099
|
+
else inspect_spec(
|
|
7100
|
+
resolved_spec,
|
|
7101
|
+
root,
|
|
7102
|
+
repo_paths,
|
|
7103
|
+
args.spec_task or None,
|
|
7027
7104
|
)
|
|
7028
7105
|
)
|
|
7106
|
+
emit(inspection_summary(inspection))
|
|
7029
7107
|
elif command == "initialize-spec-execution":
|
|
7030
7108
|
emit(initialize_spec_execution_state(root, args.spec))
|
|
7031
7109
|
elif command == "select-dev-spec-scope":
|