easy-coding-harness 0.6.1 → 0.7.1-beta0
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 +16 -0
- package/README.md +19 -7
- package/dist/cli.js +309 -168
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +3 -3
- package/templates/common/skills/ec-implementing/SKILL.md +10 -10
- package/templates/common/skills/ec-memory/SKILL.md +3 -3
- package/templates/common/skills/ec-no-harness/SKILL.md +39 -0
- package/templates/common/skills/ec-reviewing/SKILL.md +2 -2
- package/templates/common/skills/ec-task-management/SKILL.md +45 -5
- package/templates/common/skills/ec-verification/SKILL.md +16 -16
- package/templates/common/skills/ec-workflow/SKILL.md +62 -56
- package/templates/main-constraint/AGENTS.md.tpl +16 -13
- package/templates/main-constraint/CLAUDE.md.tpl +16 -13
- package/templates/shared-hooks/easy_coding_state.py +220 -20
- package/templates/shared-hooks/inject-subagent-context.py +6 -2
|
@@ -12,11 +12,11 @@ import sys
|
|
|
12
12
|
TERMINAL_STATUSES = {"COMPLETE", "CLOSED"}
|
|
13
13
|
HELP_SUFFIX = (
|
|
14
14
|
"Use `ec-workflow` to start or resume a task, "
|
|
15
|
-
"`ec-brainstorming` to brainstorm, or `ec-task-management` to
|
|
15
|
+
"`ec-brainstorming` to brainstorm, or `ec-task-management` to manage tasks or session settings"
|
|
16
16
|
)
|
|
17
17
|
READY_LINE = (
|
|
18
18
|
"> **Easy Coding** · Ready · Use `ec-workflow` to start or resume a task, "
|
|
19
|
-
"`ec-brainstorming` to brainstorm, or `ec-task-management` to
|
|
19
|
+
"`ec-brainstorming` to brainstorm, or `ec-task-management` to manage tasks or session settings"
|
|
20
20
|
)
|
|
21
21
|
WAITING_INIT_LINE = "> **Easy Coding** · Waiting init · Use `ec-init` to initialize"
|
|
22
22
|
|
|
@@ -47,12 +47,18 @@ VALID_TRANSITIONS: dict[str, set[str]] = {
|
|
|
47
47
|
"CLOSED": set(),
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
ALWAYS_AUTO_TRANSITIONS = {
|
|
51
51
|
("INIT", "ANALYSIS"),
|
|
52
52
|
("MEMORY", "COMPLETE"),
|
|
53
53
|
}
|
|
54
54
|
READ_ONLY_COMPLETION_TRANSITION = ("IMPLEMENT", "COMPLETE")
|
|
55
55
|
NO_CODE_TASK_TYPES = {"analysis", "doc", "report"}
|
|
56
|
+
CONFIRM_MODES = {"approve", "guard", "auto"}
|
|
57
|
+
DEFAULT_CONFIRM_MODE = "guard"
|
|
58
|
+
GUARD_CONFIRM_TRANSITIONS = {
|
|
59
|
+
("ANALYSIS", "IMPLEMENT"),
|
|
60
|
+
("VERIFICATION", "MEMORY"),
|
|
61
|
+
}
|
|
56
62
|
|
|
57
63
|
LEGACY_STAGE_MAP = {
|
|
58
64
|
"WAITING_CONFIRM": "ANALYSIS",
|
|
@@ -165,6 +171,51 @@ def read_memory_config(root: Path) -> dict[str, int]:
|
|
|
165
171
|
return config
|
|
166
172
|
|
|
167
173
|
|
|
174
|
+
def read_project_confirm_mode(root: Path) -> str:
|
|
175
|
+
path = root / ".easy-coding" / "config.yaml"
|
|
176
|
+
try:
|
|
177
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
178
|
+
except OSError:
|
|
179
|
+
return DEFAULT_CONFIRM_MODE
|
|
180
|
+
|
|
181
|
+
in_behavior = False
|
|
182
|
+
behavior_indent = 0
|
|
183
|
+
for raw_line in lines:
|
|
184
|
+
without_comment = raw_line.split("#", 1)[0].rstrip()
|
|
185
|
+
stripped = without_comment.strip()
|
|
186
|
+
if not stripped:
|
|
187
|
+
continue
|
|
188
|
+
indent = len(without_comment) - len(without_comment.lstrip(" "))
|
|
189
|
+
if stripped == "behavior:":
|
|
190
|
+
in_behavior = True
|
|
191
|
+
behavior_indent = indent
|
|
192
|
+
continue
|
|
193
|
+
if in_behavior and indent <= behavior_indent:
|
|
194
|
+
in_behavior = False
|
|
195
|
+
if not in_behavior or ":" not in stripped:
|
|
196
|
+
continue
|
|
197
|
+
key, value = stripped.split(":", 1)
|
|
198
|
+
if key != "confirm_mode":
|
|
199
|
+
continue
|
|
200
|
+
mode = value.strip().strip("'\"")
|
|
201
|
+
if mode not in CONFIRM_MODES:
|
|
202
|
+
raise StateError(
|
|
203
|
+
"Invalid behavior.confirm_mode in .easy-coding/config.yaml: "
|
|
204
|
+
"expected approve, guard, or auto."
|
|
205
|
+
)
|
|
206
|
+
return mode
|
|
207
|
+
return DEFAULT_CONFIRM_MODE
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def resolve_confirm_mode(root: Path, session: dict) -> tuple[str, str | None, str]:
|
|
211
|
+
project_mode = read_project_confirm_mode(root)
|
|
212
|
+
session_mode = session.get("confirm_mode")
|
|
213
|
+
if session_mode is not None and session_mode not in CONFIRM_MODES:
|
|
214
|
+
raise StateError("Invalid session confirm_mode: expected approve, guard, or auto.")
|
|
215
|
+
effective_mode = str(session_mode or project_mode)
|
|
216
|
+
return project_mode, str(session_mode) if session_mode else None, effective_mode
|
|
217
|
+
|
|
218
|
+
|
|
168
219
|
def short_memory_entries(root: Path) -> list[dict[str, object]]:
|
|
169
220
|
short_dir = root / ".easy-coding" / "memory" / "short"
|
|
170
221
|
if not short_dir.is_dir():
|
|
@@ -890,13 +941,32 @@ def get_pending_init_version(root: Path) -> str | None:
|
|
|
890
941
|
return None
|
|
891
942
|
|
|
892
943
|
|
|
893
|
-
def
|
|
894
|
-
|
|
944
|
+
def transition_requires_confirmation(
|
|
945
|
+
previous: str,
|
|
946
|
+
current: str,
|
|
947
|
+
task_type: str,
|
|
948
|
+
confirm_mode: str,
|
|
949
|
+
) -> bool:
|
|
950
|
+
if (previous, current) in ALWAYS_AUTO_TRANSITIONS:
|
|
951
|
+
return False
|
|
952
|
+
if current == "CLOSED":
|
|
895
953
|
return True
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
954
|
+
if confirm_mode == "auto":
|
|
955
|
+
return False
|
|
956
|
+
if confirm_mode == "guard":
|
|
957
|
+
return (previous, current) in GUARD_CONFIRM_TRANSITIONS
|
|
958
|
+
if confirm_mode == "approve":
|
|
959
|
+
return True
|
|
960
|
+
raise StateError(f"Unknown confirm mode: {confirm_mode}")
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def is_automatic_transition(
|
|
964
|
+
previous: str,
|
|
965
|
+
current: str,
|
|
966
|
+
task_type: str,
|
|
967
|
+
confirm_mode: str,
|
|
968
|
+
) -> bool:
|
|
969
|
+
return not transition_requires_confirmation(previous, current, task_type, confirm_mode)
|
|
900
970
|
|
|
901
971
|
|
|
902
972
|
def validate_transition(previous: str, current: str, task_type: str = "") -> str | None:
|
|
@@ -943,6 +1013,10 @@ def snapshot_state(
|
|
|
943
1013
|
missing = False
|
|
944
1014
|
status = "idle"
|
|
945
1015
|
|
|
1016
|
+
project_confirm_mode, session_confirm_mode, effective_confirm_mode = resolve_confirm_mode(
|
|
1017
|
+
root, resolved_session
|
|
1018
|
+
)
|
|
1019
|
+
|
|
946
1020
|
return {
|
|
947
1021
|
"session_file": display_path(root, session_path),
|
|
948
1022
|
"current_task": str(task_id) if task_id else None,
|
|
@@ -955,6 +1029,10 @@ def snapshot_state(
|
|
|
955
1029
|
"last_agent": task.get("last_agent") if task else None,
|
|
956
1030
|
"project_init_required": is_project_init_required(root),
|
|
957
1031
|
"pending_init_version": get_pending_init_version(root),
|
|
1032
|
+
"project_confirm_mode": project_confirm_mode,
|
|
1033
|
+
"session_confirm_mode": session_confirm_mode,
|
|
1034
|
+
"effective_confirm_mode": effective_confirm_mode,
|
|
1035
|
+
"harness_disabled": resolved_session.get("harness_disabled") is True,
|
|
958
1036
|
}
|
|
959
1037
|
|
|
960
1038
|
|
|
@@ -1000,7 +1078,11 @@ def build_machine_breadcrumbs(
|
|
|
1000
1078
|
task = state["task"]
|
|
1001
1079
|
stage = str(state["status"]) if task else "idle"
|
|
1002
1080
|
resolved_session_file = str(state["session_file"])
|
|
1003
|
-
lines = [
|
|
1081
|
+
lines = [
|
|
1082
|
+
f"[workflow-state:{stage}]",
|
|
1083
|
+
f"[easy-coding:session-file:{resolved_session_file}]",
|
|
1084
|
+
f"[easy-coding:confirm-mode:{state['effective_confirm_mode']}]",
|
|
1085
|
+
]
|
|
1004
1086
|
|
|
1005
1087
|
if task_id:
|
|
1006
1088
|
lines.append(f"[current-task:{task_id}]")
|
|
@@ -1016,7 +1098,12 @@ def build_machine_breadcrumbs(
|
|
|
1016
1098
|
if target:
|
|
1017
1099
|
lines.append(f"[easy-coding:pending-transition:{source}->{target}]")
|
|
1018
1100
|
task_type = str(task.get("type") or "") if task else ""
|
|
1019
|
-
if is_automatic_transition(
|
|
1101
|
+
if is_automatic_transition(
|
|
1102
|
+
source,
|
|
1103
|
+
target,
|
|
1104
|
+
task_type,
|
|
1105
|
+
str(state["effective_confirm_mode"]),
|
|
1106
|
+
):
|
|
1020
1107
|
lines.append(f"[easy-coding:auto-transition-ready:{source}->{target}]")
|
|
1021
1108
|
else:
|
|
1022
1109
|
lines.append("[easy-coding:transition-confirmation-required]")
|
|
@@ -1063,6 +1150,14 @@ def build_status_context(
|
|
|
1063
1150
|
agent: str | None = None,
|
|
1064
1151
|
session_file: str | Path | None = None,
|
|
1065
1152
|
) -> str:
|
|
1153
|
+
if session.get("harness_disabled") is True:
|
|
1154
|
+
session_path = resolve_session_path(root, session_file)
|
|
1155
|
+
return "\n".join(
|
|
1156
|
+
[
|
|
1157
|
+
"[easy-coding:no-harness]",
|
|
1158
|
+
f"[easy-coding:session-file:{display_path(root, session_path)}]",
|
|
1159
|
+
]
|
|
1160
|
+
)
|
|
1066
1161
|
return "\n".join(
|
|
1067
1162
|
[
|
|
1068
1163
|
build_status_line(root, session, agent, session_file),
|
|
@@ -1082,7 +1177,7 @@ def attach_status_context(
|
|
|
1082
1177
|
if session is None:
|
|
1083
1178
|
session = default_session()
|
|
1084
1179
|
context = build_status_context(root, session, agent, resolved_session_file)
|
|
1085
|
-
first_line = context.splitlines()[0] if context else ""
|
|
1180
|
+
first_line = context.splitlines()[0] if context.startswith("> ") else ""
|
|
1086
1181
|
enriched = dict(data)
|
|
1087
1182
|
enriched["status_line"] = first_line
|
|
1088
1183
|
enriched["status_context"] = context
|
|
@@ -1157,6 +1252,55 @@ def clear_current_task(root: Path, agent: str, session_file: str | Path | None =
|
|
|
1157
1252
|
return snapshot_state(root, session_file, session)
|
|
1158
1253
|
|
|
1159
1254
|
|
|
1255
|
+
def set_session_confirm_mode(
|
|
1256
|
+
root: Path,
|
|
1257
|
+
mode: str,
|
|
1258
|
+
agent: str,
|
|
1259
|
+
session_file: str | Path | None = None,
|
|
1260
|
+
) -> dict:
|
|
1261
|
+
if mode not in CONFIRM_MODES:
|
|
1262
|
+
raise StateError("Invalid confirm mode: expected approve, guard, or auto.")
|
|
1263
|
+
session = ensure_session(root, session_file)
|
|
1264
|
+
session["confirm_mode"] = mode
|
|
1265
|
+
session["last_agent"] = agent
|
|
1266
|
+
write_session(root, session, session_file)
|
|
1267
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
1268
|
+
snapshot["action"] = "set-confirm-mode"
|
|
1269
|
+
return snapshot
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def clear_session_confirm_mode(
|
|
1273
|
+
root: Path,
|
|
1274
|
+
agent: str,
|
|
1275
|
+
session_file: str | Path | None = None,
|
|
1276
|
+
) -> dict:
|
|
1277
|
+
session = ensure_session(root, session_file)
|
|
1278
|
+
session.pop("confirm_mode", None)
|
|
1279
|
+
session["last_agent"] = agent
|
|
1280
|
+
write_session(root, session, session_file)
|
|
1281
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
1282
|
+
snapshot["action"] = "clear-confirm-mode"
|
|
1283
|
+
return snapshot
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def set_harness_disabled(
|
|
1287
|
+
root: Path,
|
|
1288
|
+
disabled: bool,
|
|
1289
|
+
agent: str,
|
|
1290
|
+
session_file: str | Path | None = None,
|
|
1291
|
+
) -> dict:
|
|
1292
|
+
session = ensure_session(root, session_file)
|
|
1293
|
+
if disabled:
|
|
1294
|
+
session["harness_disabled"] = True
|
|
1295
|
+
else:
|
|
1296
|
+
session.pop("harness_disabled", None)
|
|
1297
|
+
session["last_agent"] = agent
|
|
1298
|
+
write_session(root, session, session_file)
|
|
1299
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
1300
|
+
snapshot["action"] = "disable-harness" if disabled else "enable-harness"
|
|
1301
|
+
return snapshot
|
|
1302
|
+
|
|
1303
|
+
|
|
1160
1304
|
def handoff_task(
|
|
1161
1305
|
root: Path,
|
|
1162
1306
|
agent: str,
|
|
@@ -1297,15 +1441,17 @@ def request_transition(
|
|
|
1297
1441
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
1298
1442
|
previous = str(task.get("status") or "idle")
|
|
1299
1443
|
task_type = str(task.get("type") or "")
|
|
1444
|
+
confirm_mode = resolve_confirm_mode(root, session)[2]
|
|
1300
1445
|
if previous == stage:
|
|
1301
1446
|
raise StateError(f"Transition target must differ from current stage: {stage}")
|
|
1302
1447
|
|
|
1303
1448
|
violation = validate_transition(previous, stage, task_type)
|
|
1304
1449
|
if violation:
|
|
1305
1450
|
raise StateError(violation)
|
|
1306
|
-
if is_automatic_transition(previous, stage, task_type):
|
|
1451
|
+
if is_automatic_transition(previous, stage, task_type, confirm_mode):
|
|
1307
1452
|
raise StateError(
|
|
1308
|
-
f"Transition {previous} -> {stage} is automatic
|
|
1453
|
+
f"Transition {previous} -> {stage} is automatic in {confirm_mode} mode; "
|
|
1454
|
+
"use auto-transition instead."
|
|
1309
1455
|
)
|
|
1310
1456
|
if previous == "ANALYSIS" and stage == "IMPLEMENT":
|
|
1311
1457
|
validate_analysis_readiness(root, resolved_task_id)
|
|
@@ -1382,11 +1528,14 @@ def auto_transition(
|
|
|
1382
1528
|
task_id: str | None = None,
|
|
1383
1529
|
session_file: str | Path | None = None,
|
|
1384
1530
|
) -> dict:
|
|
1385
|
-
|
|
1531
|
+
session, _, task = resolve_current_task(root, task_id, session_file)
|
|
1386
1532
|
previous = str(task.get("status") or "idle")
|
|
1387
1533
|
task_type = str(task.get("type") or "")
|
|
1388
|
-
|
|
1389
|
-
|
|
1534
|
+
confirm_mode = resolve_confirm_mode(root, session)[2]
|
|
1535
|
+
if not is_automatic_transition(previous, stage, task_type, confirm_mode):
|
|
1536
|
+
raise StateError(
|
|
1537
|
+
f"Automatic transition is not allowed in {confirm_mode} mode: {previous} -> {stage}."
|
|
1538
|
+
)
|
|
1390
1539
|
|
|
1391
1540
|
pending = task.get("pending_transition")
|
|
1392
1541
|
if isinstance(pending, dict) and (
|
|
@@ -1409,12 +1558,13 @@ def confirm_transition(
|
|
|
1409
1558
|
task_id: str | None = None,
|
|
1410
1559
|
session_file: str | Path | None = None,
|
|
1411
1560
|
) -> dict:
|
|
1412
|
-
|
|
1561
|
+
session, _, task = resolve_current_task(root, task_id, session_file)
|
|
1413
1562
|
pending = task.get("pending_transition")
|
|
1414
1563
|
if not isinstance(pending, dict):
|
|
1415
1564
|
raise StateError("No transition is pending user confirmation.")
|
|
1416
1565
|
previous = str(task.get("status") or "idle")
|
|
1417
1566
|
task_type = str(task.get("type") or "")
|
|
1567
|
+
confirm_mode = resolve_confirm_mode(root, session)[2]
|
|
1418
1568
|
source = str(pending.get("from") or "")
|
|
1419
1569
|
target = str(pending.get("to") or "")
|
|
1420
1570
|
if source != previous:
|
|
@@ -1423,9 +1573,10 @@ def confirm_transition(
|
|
|
1423
1573
|
)
|
|
1424
1574
|
if stage and stage != target:
|
|
1425
1575
|
raise StateError(f"Pending transition targets {target}, not {stage}.")
|
|
1426
|
-
if is_automatic_transition(source, target, task_type):
|
|
1576
|
+
if is_automatic_transition(source, target, task_type, confirm_mode):
|
|
1427
1577
|
raise StateError(
|
|
1428
|
-
f"Transition {source} -> {target} is automatic
|
|
1578
|
+
f"Transition {source} -> {target} is automatic in {confirm_mode} mode; "
|
|
1579
|
+
"use auto-transition instead."
|
|
1429
1580
|
)
|
|
1430
1581
|
|
|
1431
1582
|
snapshot = apply_transition(root, target, agent, task_id, session_file)
|
|
@@ -1703,6 +1854,19 @@ def main() -> int:
|
|
|
1703
1854
|
clear_current = subcommands.add_parser("clear-current", parents=[common])
|
|
1704
1855
|
clear_current.add_argument("--agent", required=True)
|
|
1705
1856
|
|
|
1857
|
+
set_confirm_mode_parser = subcommands.add_parser("set-confirm-mode", parents=[common])
|
|
1858
|
+
set_confirm_mode_parser.add_argument("--mode", required=True, choices=sorted(CONFIRM_MODES))
|
|
1859
|
+
set_confirm_mode_parser.add_argument("--agent", required=True)
|
|
1860
|
+
|
|
1861
|
+
clear_confirm_mode_parser = subcommands.add_parser("clear-confirm-mode", parents=[common])
|
|
1862
|
+
clear_confirm_mode_parser.add_argument("--agent", required=True)
|
|
1863
|
+
|
|
1864
|
+
disable_harness_parser = subcommands.add_parser("disable-harness", parents=[common])
|
|
1865
|
+
disable_harness_parser.add_argument("--agent", required=True)
|
|
1866
|
+
|
|
1867
|
+
enable_harness_parser = subcommands.add_parser("enable-harness", parents=[common])
|
|
1868
|
+
enable_harness_parser.add_argument("--agent", required=True)
|
|
1869
|
+
|
|
1706
1870
|
handoff = subcommands.add_parser("handoff-task", parents=[common])
|
|
1707
1871
|
handoff.add_argument("--agent", required=True)
|
|
1708
1872
|
handoff.add_argument("--summary", required=True)
|
|
@@ -1807,6 +1971,42 @@ def main() -> int:
|
|
|
1807
1971
|
session_file,
|
|
1808
1972
|
)
|
|
1809
1973
|
)
|
|
1974
|
+
elif command == "set-confirm-mode":
|
|
1975
|
+
emit(
|
|
1976
|
+
attach_status_context(
|
|
1977
|
+
root,
|
|
1978
|
+
set_session_confirm_mode(root, args.mode, args.agent, session_file),
|
|
1979
|
+
args.agent,
|
|
1980
|
+
session_file,
|
|
1981
|
+
)
|
|
1982
|
+
)
|
|
1983
|
+
elif command == "clear-confirm-mode":
|
|
1984
|
+
emit(
|
|
1985
|
+
attach_status_context(
|
|
1986
|
+
root,
|
|
1987
|
+
clear_session_confirm_mode(root, args.agent, session_file),
|
|
1988
|
+
args.agent,
|
|
1989
|
+
session_file,
|
|
1990
|
+
)
|
|
1991
|
+
)
|
|
1992
|
+
elif command == "disable-harness":
|
|
1993
|
+
emit(
|
|
1994
|
+
attach_status_context(
|
|
1995
|
+
root,
|
|
1996
|
+
set_harness_disabled(root, True, args.agent, session_file),
|
|
1997
|
+
args.agent,
|
|
1998
|
+
session_file,
|
|
1999
|
+
)
|
|
2000
|
+
)
|
|
2001
|
+
elif command == "enable-harness":
|
|
2002
|
+
emit(
|
|
2003
|
+
attach_status_context(
|
|
2004
|
+
root,
|
|
2005
|
+
set_harness_disabled(root, False, args.agent, session_file),
|
|
2006
|
+
args.agent,
|
|
2007
|
+
session_file,
|
|
2008
|
+
)
|
|
2009
|
+
)
|
|
1810
2010
|
elif command == "handoff-task":
|
|
1811
2011
|
emit(
|
|
1812
2012
|
attach_status_context(
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
-
from easy_coding_state import snapshot_state
|
|
7
|
+
from easy_coding_state import load_session, snapshot_state
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def configure_stdio() -> None:
|
|
@@ -54,7 +54,11 @@ def main() -> int:
|
|
|
54
54
|
if root is None:
|
|
55
55
|
return 0
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
session = load_session(root)
|
|
58
|
+
if session and session.get("harness_disabled") is True:
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
state = snapshot_state(root, session=session)
|
|
58
62
|
task_id = state.get("current_task")
|
|
59
63
|
context = [
|
|
60
64
|
"[easy-coding:subagent-guard]",
|