@hunter-harness/workflow-harness 0.2.69 → 0.2.70
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-plan/reference.md +31 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +20 -0
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-test/pitfalls.md +34 -3
- package/harness/bundles/general/claude-code/harness-test/reference.md +9 -0
- package/harness/bundles/general/claude-code/scripts/harness_context.py +27 -7
- package/harness/bundles/general/claude-code/scripts/harness_events.py +12 -3
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +127 -11
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +244 -5
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/general/claude-code/scripts/harness_service.py +69 -15
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +31 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +20 -0
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +34 -3
- package/harness/bundles/general/codebuddy/harness-test/reference.md +9 -0
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +27 -7
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +12 -3
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +127 -11
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +244 -5
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +69 -15
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-plan/reference.md +31 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-run/reference.md +20 -0
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-test/pitfalls.md +34 -3
- package/harness/bundles/general/codex/harness-test/reference.md +9 -0
- package/harness/bundles/general/codex/scripts/harness_context.py +27 -7
- package/harness/bundles/general/codex/scripts/harness_events.py +12 -3
- package/harness/bundles/general/codex/scripts/harness_gate.py +127 -11
- package/harness/bundles/general/codex/scripts/harness_ledger.py +244 -5
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/general/codex/scripts/harness_service.py +69 -15
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-plan/reference.md +31 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +20 -0
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-test/pitfalls.md +34 -3
- package/harness/bundles/general/cursor/harness-test/reference.md +9 -0
- package/harness/bundles/general/cursor/scripts/harness_context.py +27 -7
- package/harness/bundles/general/cursor/scripts/harness_events.py +12 -3
- package/harness/bundles/general/cursor/scripts/harness_gate.py +127 -11
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +244 -5
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/general/cursor/scripts/harness_service.py +69 -15
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-plan/reference.md +31 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-test/pitfalls.md +34 -3
- package/harness/bundles/java/claude-code/scripts/harness_context.py +27 -7
- package/harness/bundles/java/claude-code/scripts/harness_events.py +12 -3
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +127 -11
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +244 -5
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/java/claude-code/scripts/harness_service.py +69 -15
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +31 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +34 -3
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +27 -7
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +12 -3
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +127 -11
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +244 -5
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +69 -15
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-plan/reference.md +31 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-test/pitfalls.md +34 -3
- package/harness/bundles/java/codex/scripts/harness_context.py +27 -7
- package/harness/bundles/java/codex/scripts/harness_events.py +12 -3
- package/harness/bundles/java/codex/scripts/harness_gate.py +127 -11
- package/harness/bundles/java/codex/scripts/harness_ledger.py +244 -5
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/java/codex/scripts/harness_service.py +69 -15
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-plan/reference.md +31 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-test/pitfalls.md +34 -3
- package/harness/bundles/java/cursor/scripts/harness_context.py +27 -7
- package/harness/bundles/java/cursor/scripts/harness_events.py +12 -3
- package/harness/bundles/java/cursor/scripts/harness_gate.py +127 -11
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +244 -5
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/java/cursor/scripts/harness_service.py +69 -15
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +48 -1
- package/harness/manifests/general/claude-code.json +26 -26
- package/harness/manifests/general/codebuddy.json +26 -26
- package/harness/manifests/general/codex.json +26 -26
- package/harness/manifests/general/cursor.json +26 -26
- package/harness/manifests/java/claude-code.json +26 -26
- package/harness/manifests/java/codebuddy.json +26 -26
- package/harness/manifests/java/codex.json +26 -26
- package/harness/manifests/java/cursor.json +26 -26
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -728,7 +728,9 @@ def _validate_plan_start(
|
|
|
728
728
|
if not matching:
|
|
729
729
|
return _result_error(
|
|
730
730
|
"PHASE_START_MISSING",
|
|
731
|
-
"no matching plan phase.start event found for finalizer runId/attempt"
|
|
731
|
+
"no matching plan phase.start event found for finalizer runId/attempt; "
|
|
732
|
+
"append one with the same run id and attempt before finalizing, or use "
|
|
733
|
+
"the `republish` subcommand which allocates and appends both for you",
|
|
732
734
|
)
|
|
733
735
|
if len(matching) > 1:
|
|
734
736
|
return _result_error(
|
|
@@ -1299,6 +1301,72 @@ def _append_terminal(change_dir: Path, run_id: str, attempt: int) -> tuple[int,
|
|
|
1299
1301
|
return code, stderr.getvalue().strip()
|
|
1300
1302
|
|
|
1301
1303
|
|
|
1304
|
+
def _rollback_publish(
|
|
1305
|
+
created: list[Path], replaced: list[tuple[Path, bytes]]
|
|
1306
|
+
) -> None:
|
|
1307
|
+
"""Undo a failed publish: delete new files, restore overwritten ones."""
|
|
1308
|
+
for target in reversed(created):
|
|
1309
|
+
target.unlink(missing_ok=True)
|
|
1310
|
+
for target, original in reversed(replaced):
|
|
1311
|
+
try:
|
|
1312
|
+
target.write_bytes(original)
|
|
1313
|
+
except OSError:
|
|
1314
|
+
# Best effort: the caller already reports the publish failure.
|
|
1315
|
+
pass
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def _plan_attempts(change_dir: Path) -> list[int]:
|
|
1319
|
+
"""Every attempt number already used by a plan lifecycle event."""
|
|
1320
|
+
try:
|
|
1321
|
+
events = harness_events.load_events(harness_events.events_path(change_dir))
|
|
1322
|
+
except (OSError, ValueError):
|
|
1323
|
+
return []
|
|
1324
|
+
attempts: list[int] = []
|
|
1325
|
+
for event in events:
|
|
1326
|
+
if event.get("phase") != "plan":
|
|
1327
|
+
continue
|
|
1328
|
+
value = event.get("attempt")
|
|
1329
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
1330
|
+
attempts.append(value)
|
|
1331
|
+
return attempts
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def _run_id_in_use(change_dir: Path, run_id: str) -> bool:
|
|
1335
|
+
try:
|
|
1336
|
+
events = harness_events.load_events(harness_events.events_path(change_dir))
|
|
1337
|
+
except (OSError, ValueError):
|
|
1338
|
+
return False
|
|
1339
|
+
return any(event.get("run_id") == run_id for event in events)
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def _append_plan_start(
|
|
1343
|
+
change_dir: Path, run_id: str, attempt: int, reason: str
|
|
1344
|
+
) -> tuple[int, str]:
|
|
1345
|
+
stdout = io.StringIO()
|
|
1346
|
+
stderr = io.StringIO()
|
|
1347
|
+
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
1348
|
+
code = harness_events.main(
|
|
1349
|
+
[
|
|
1350
|
+
"append",
|
|
1351
|
+
"--change-dir",
|
|
1352
|
+
str(change_dir),
|
|
1353
|
+
"--phase",
|
|
1354
|
+
"plan",
|
|
1355
|
+
"--type",
|
|
1356
|
+
"phase.start",
|
|
1357
|
+
# phase.start accepts only `note` beyond provenance fields.
|
|
1358
|
+
"--note",
|
|
1359
|
+
reason,
|
|
1360
|
+
"--run-id",
|
|
1361
|
+
run_id,
|
|
1362
|
+
"--attempt",
|
|
1363
|
+
str(attempt),
|
|
1364
|
+
"--json",
|
|
1365
|
+
]
|
|
1366
|
+
)
|
|
1367
|
+
return code, stderr.getvalue().strip()
|
|
1368
|
+
|
|
1369
|
+
|
|
1302
1370
|
def finalize_plan(
|
|
1303
1371
|
change_dir: Path,
|
|
1304
1372
|
staging: Path,
|
|
@@ -1306,6 +1374,8 @@ def finalize_plan(
|
|
|
1306
1374
|
change_name: str,
|
|
1307
1375
|
run_id: str,
|
|
1308
1376
|
attempt: int,
|
|
1377
|
+
allow_amend: bool = False,
|
|
1378
|
+
amend_reason: str | None = None,
|
|
1309
1379
|
) -> dict[str, Any]:
|
|
1310
1380
|
change_dir = change_dir.resolve()
|
|
1311
1381
|
staging = staging.resolve()
|
|
@@ -1331,6 +1401,7 @@ def finalize_plan(
|
|
|
1331
1401
|
receipt_path = change_dir / "meta" / "plan-finalization.json"
|
|
1332
1402
|
lock_path = change_dir / "meta" / "plan-finalize.lock"
|
|
1333
1403
|
receipt: dict[str, Any] | None = None
|
|
1404
|
+
supersedes: dict[str, Any] | None = None
|
|
1334
1405
|
if receipt_path.is_file():
|
|
1335
1406
|
try:
|
|
1336
1407
|
loaded = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
|
|
@@ -1338,11 +1409,22 @@ def finalize_plan(
|
|
|
1338
1409
|
except (OSError, json.JSONDecodeError) as exc:
|
|
1339
1410
|
return _result_error("PLAN_FINALIZATION_RECEIPT_INVALID", str(exc))
|
|
1340
1411
|
if receipt and receipt.get("artifactsHash") != validation["artifactsHash"]:
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1412
|
+
if not allow_amend:
|
|
1413
|
+
return _result_error(
|
|
1414
|
+
"PLAN_FINALIZATION_HASH_CONFLICT",
|
|
1415
|
+
"finalizer was already invoked with a different artifact set; "
|
|
1416
|
+
"to publish an approved amendment run: harness_plan_finalize.py "
|
|
1417
|
+
"republish --change-dir <dir> --staging-dir <dir> --change "
|
|
1418
|
+
f"{change_name} --run-id <fresh-run-id> --reason \"<why>\"",
|
|
1419
|
+
)
|
|
1420
|
+
# Authorized amendment: keep the superseded identity in the new
|
|
1421
|
+
# receipt so the published history stays auditable.
|
|
1422
|
+
supersedes = {
|
|
1423
|
+
"artifactsHash": receipt.get("artifactsHash"),
|
|
1424
|
+
"runId": receipt.get("runId"),
|
|
1425
|
+
"attempt": receipt.get("attempt"),
|
|
1426
|
+
}
|
|
1427
|
+
elif (
|
|
1346
1428
|
receipt
|
|
1347
1429
|
and receipt.get("status") == "finalized"
|
|
1348
1430
|
and _terminal_exists(change_dir, run_id, attempt)
|
|
@@ -1365,21 +1447,33 @@ def finalize_plan(
|
|
|
1365
1447
|
return _result_error("PLAN_FINALIZATION_LOCKED", f"lock exists: {lock_path}")
|
|
1366
1448
|
|
|
1367
1449
|
created: list[Path] = []
|
|
1450
|
+
# Amendment overwrites replace already-published bytes. Keep the originals
|
|
1451
|
+
# so a failed publish restores them instead of deleting the published plan.
|
|
1452
|
+
replaced: list[tuple[Path, bytes]] = []
|
|
1368
1453
|
terminal_committed = False
|
|
1369
1454
|
try:
|
|
1370
1455
|
for rel_text in validation["files"]:
|
|
1371
1456
|
source = staging / rel_text
|
|
1372
1457
|
target = change_dir / rel_text
|
|
1373
|
-
if
|
|
1458
|
+
if (
|
|
1459
|
+
not allow_amend
|
|
1460
|
+
and target.exists()
|
|
1461
|
+
and target.read_bytes() != source.read_bytes()
|
|
1462
|
+
):
|
|
1374
1463
|
return _result_error(
|
|
1375
|
-
"PLAN_TARGET_CONFLICT",
|
|
1464
|
+
"PLAN_TARGET_CONFLICT",
|
|
1465
|
+
f"refusing to overwrite {rel_text}; publish an approved "
|
|
1466
|
+
"amendment with the `republish` subcommand instead",
|
|
1376
1467
|
)
|
|
1377
1468
|
|
|
1378
1469
|
for rel_text in validation["files"]:
|
|
1379
1470
|
source = staging / rel_text
|
|
1380
1471
|
target = change_dir / rel_text
|
|
1381
|
-
|
|
1382
|
-
|
|
1472
|
+
existed = target.exists()
|
|
1473
|
+
if existed:
|
|
1474
|
+
original = target.read_bytes()
|
|
1475
|
+
if not allow_amend or original == source.read_bytes():
|
|
1476
|
+
continue
|
|
1383
1477
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1384
1478
|
fd, raw_tmp = tempfile.mkstemp(
|
|
1385
1479
|
prefix=f".{target.name}.", suffix=".tmp", dir=str(target.parent)
|
|
@@ -1389,7 +1483,10 @@ def finalize_plan(
|
|
|
1389
1483
|
try:
|
|
1390
1484
|
shutil.copyfile(source, tmp)
|
|
1391
1485
|
os.replace(tmp, target)
|
|
1392
|
-
|
|
1486
|
+
if existed:
|
|
1487
|
+
replaced.append((target, original))
|
|
1488
|
+
else:
|
|
1489
|
+
created.append(target)
|
|
1393
1490
|
finally:
|
|
1394
1491
|
tmp.unlink(missing_ok=True)
|
|
1395
1492
|
|
|
@@ -1403,8 +1500,11 @@ def finalize_plan(
|
|
|
1403
1500
|
"tasks": tasks,
|
|
1404
1501
|
"foundationGate": "approved",
|
|
1405
1502
|
}
|
|
1503
|
+
if checkpoints_path.is_file():
|
|
1504
|
+
replaced.append((checkpoints_path, checkpoints_path.read_bytes()))
|
|
1505
|
+
else:
|
|
1506
|
+
created.append(checkpoints_path)
|
|
1406
1507
|
_atomic_write_json(checkpoints_path, checkpoints_payload)
|
|
1407
|
-
created.append(checkpoints_path)
|
|
1408
1508
|
|
|
1409
1509
|
# C9: write scenario-manifest.json with parsed scenarios.
|
|
1410
1510
|
scenarios = validation.get("scenarios") or []
|
|
@@ -1421,8 +1521,11 @@ def finalize_plan(
|
|
|
1421
1521
|
for scenario in scenarios
|
|
1422
1522
|
],
|
|
1423
1523
|
}
|
|
1524
|
+
if manifest_path.is_file():
|
|
1525
|
+
replaced.append((manifest_path, manifest_path.read_bytes()))
|
|
1526
|
+
else:
|
|
1527
|
+
created.append(manifest_path)
|
|
1424
1528
|
_atomic_write_json(manifest_path, manifest_payload)
|
|
1425
|
-
created.append(manifest_path)
|
|
1426
1529
|
|
|
1427
1530
|
pending_receipt = {
|
|
1428
1531
|
"schemaVersion": SCHEMA_VERSION,
|
|
@@ -1433,12 +1536,14 @@ def finalize_plan(
|
|
|
1433
1536
|
"runId": run_id,
|
|
1434
1537
|
"attempt": attempt,
|
|
1435
1538
|
}
|
|
1539
|
+
if supersedes is not None:
|
|
1540
|
+
pending_receipt["supersedes"] = supersedes
|
|
1541
|
+
pending_receipt["amendReason"] = amend_reason or ""
|
|
1436
1542
|
_atomic_write_json(receipt_path, pending_receipt)
|
|
1437
1543
|
terminal_code, terminal_error = _append_terminal(change_dir, run_id, attempt)
|
|
1438
1544
|
terminal_committed = _terminal_exists(change_dir, run_id, attempt)
|
|
1439
1545
|
if terminal_code != 0 and not terminal_committed:
|
|
1440
|
-
|
|
1441
|
-
target.unlink(missing_ok=True)
|
|
1546
|
+
_rollback_publish(created, replaced)
|
|
1442
1547
|
receipt_path.unlink(missing_ok=True)
|
|
1443
1548
|
return _result_error(
|
|
1444
1549
|
"PLAN_TERMINAL_APPEND_FAILED",
|
|
@@ -1446,9 +1551,9 @@ def finalize_plan(
|
|
|
1446
1551
|
)
|
|
1447
1552
|
pending_receipt["status"] = "finalized"
|
|
1448
1553
|
_atomic_write_json(receipt_path, pending_receipt)
|
|
1449
|
-
|
|
1554
|
+
result: dict[str, Any] = {
|
|
1450
1555
|
"ok": True,
|
|
1451
|
-
"action": "finalize",
|
|
1556
|
+
"action": "republish" if supersedes is not None else "finalize",
|
|
1452
1557
|
"idempotent": False,
|
|
1453
1558
|
"artifactsHash": validation["artifactsHash"],
|
|
1454
1559
|
"files": validation["files"],
|
|
@@ -1456,23 +1561,145 @@ def finalize_plan(
|
|
|
1456
1561
|
"artifactRef": "meta/plan-finalization.json",
|
|
1457
1562
|
"executionLogPath": str(harness_events.execution_log_path(change_dir)),
|
|
1458
1563
|
}
|
|
1564
|
+
if supersedes is not None:
|
|
1565
|
+
result["supersedes"] = supersedes
|
|
1566
|
+
result["scenarioCount"] = len(scenarios)
|
|
1567
|
+
result["taskCount"] = len(tasks)
|
|
1568
|
+
return result
|
|
1459
1569
|
except OSError as exc:
|
|
1460
1570
|
if terminal_committed or _terminal_exists(change_dir, run_id, attempt):
|
|
1461
1571
|
return _result_error(
|
|
1462
1572
|
"PLAN_FINALIZATION_RECOVERY_REQUIRED",
|
|
1463
1573
|
f"terminal committed; retry finalization to complete receipt: {exc}",
|
|
1464
1574
|
)
|
|
1465
|
-
|
|
1466
|
-
target.unlink(missing_ok=True)
|
|
1575
|
+
_rollback_publish(created, replaced)
|
|
1467
1576
|
receipt_path.unlink(missing_ok=True)
|
|
1468
1577
|
return _result_error("PLAN_FINALIZATION_IO_ERROR", str(exc))
|
|
1469
1578
|
finally:
|
|
1470
1579
|
lock_path.unlink(missing_ok=True)
|
|
1471
1580
|
|
|
1472
1581
|
|
|
1582
|
+
def republish_plan(
|
|
1583
|
+
change_dir: Path,
|
|
1584
|
+
staging: Path,
|
|
1585
|
+
*,
|
|
1586
|
+
change_name: str,
|
|
1587
|
+
run_id: str,
|
|
1588
|
+
reason: str,
|
|
1589
|
+
) -> dict[str, Any]:
|
|
1590
|
+
"""Publish an approved amendment to an already-finalized plan.
|
|
1591
|
+
|
|
1592
|
+
Amending a published plan (adding a scenario, correcting a task) used to
|
|
1593
|
+
require reverse-engineering a five-step lifecycle dance across three
|
|
1594
|
+
modules — new run id, incremented attempt, stale receipt removal, dangling
|
|
1595
|
+
attempt sealing — and every wrong turn produced a different error code
|
|
1596
|
+
(PLAN_FINALIZATION_HASH_CONFLICT / PHASE_START_MISSING /
|
|
1597
|
+
EVENT_ATTEMPT_CONFLICT / PHASE_ALREADY_CLOSED). Agents that gave up
|
|
1598
|
+
hand-edited meta/scenario-manifest.json, creating exactly the drift the
|
|
1599
|
+
hash guard exists to prevent.
|
|
1600
|
+
|
|
1601
|
+
This performs the whole sequence atomically and keeps the superseded
|
|
1602
|
+
identity in the receipt, so amendment stays auditable rather than silent.
|
|
1603
|
+
"""
|
|
1604
|
+
change_dir = change_dir.resolve()
|
|
1605
|
+
staging = staging.resolve()
|
|
1606
|
+
|
|
1607
|
+
if not str(reason).strip():
|
|
1608
|
+
return _result_error(
|
|
1609
|
+
"PLAN_AMEND_REASON_REQUIRED",
|
|
1610
|
+
"--reason is required: an amendment to a published plan must record why",
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
receipt_path = change_dir / "meta" / "plan-finalization.json"
|
|
1614
|
+
if not receipt_path.is_file():
|
|
1615
|
+
return _result_error(
|
|
1616
|
+
"PLAN_NOT_FINALIZED",
|
|
1617
|
+
"no plan-finalization.json to amend; use `finalize` for the first "
|
|
1618
|
+
"publication",
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
if _run_id_in_use(change_dir, run_id):
|
|
1622
|
+
return _result_error(
|
|
1623
|
+
"PLAN_AMEND_RUN_ID_IN_USE",
|
|
1624
|
+
f"run id {run_id} already appears in this change's events; "
|
|
1625
|
+
"an amendment needs a fresh run id",
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
# Nothing to amend is a no-op, not a new attempt: probe the staged hash
|
|
1629
|
+
# before writing any lifecycle event.
|
|
1630
|
+
reclassify = _reclassify_gate_policy(staging, change_name)
|
|
1631
|
+
if not reclassify.get("ok"):
|
|
1632
|
+
return _result_error(
|
|
1633
|
+
"CAPABILITY_GATE_DRIFT", reclassify.get("error", "reclassify failed")
|
|
1634
|
+
)
|
|
1635
|
+
probe = validate_staging(staging, change_name)
|
|
1636
|
+
if not probe["ok"]:
|
|
1637
|
+
return probe
|
|
1638
|
+
try:
|
|
1639
|
+
published = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
|
|
1640
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1641
|
+
return _result_error("PLAN_FINALIZATION_RECEIPT_INVALID", str(exc))
|
|
1642
|
+
if (
|
|
1643
|
+
isinstance(published, dict)
|
|
1644
|
+
and published.get("status") == "finalized"
|
|
1645
|
+
and published.get("artifactsHash") == probe["artifactsHash"]
|
|
1646
|
+
):
|
|
1647
|
+
return {
|
|
1648
|
+
"ok": True,
|
|
1649
|
+
"action": "republish",
|
|
1650
|
+
"idempotent": True,
|
|
1651
|
+
"artifactsHash": probe["artifactsHash"],
|
|
1652
|
+
"files": probe["files"],
|
|
1653
|
+
"receiptPath": str(receipt_path),
|
|
1654
|
+
"artifactRef": "meta/plan-finalization.json",
|
|
1655
|
+
"note": "published plan already matches staging; nothing to amend",
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
attempt = max(_plan_attempts(change_dir), default=0) + 1
|
|
1659
|
+
start_code, start_error = _append_plan_start(
|
|
1660
|
+
change_dir, run_id, attempt, f"plan amendment: {reason}"
|
|
1661
|
+
)
|
|
1662
|
+
if start_code != 0:
|
|
1663
|
+
return _result_error(
|
|
1664
|
+
"PLAN_AMEND_START_FAILED",
|
|
1665
|
+
start_error or "failed to append plan phase.start for the amendment",
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
result = finalize_plan(
|
|
1669
|
+
change_dir,
|
|
1670
|
+
staging,
|
|
1671
|
+
change_name=change_name,
|
|
1672
|
+
run_id=run_id,
|
|
1673
|
+
attempt=attempt,
|
|
1674
|
+
allow_amend=True,
|
|
1675
|
+
amend_reason=reason,
|
|
1676
|
+
)
|
|
1677
|
+
if isinstance(result, dict):
|
|
1678
|
+
result.setdefault("runId", run_id)
|
|
1679
|
+
result.setdefault("attempt", attempt)
|
|
1680
|
+
return result
|
|
1681
|
+
|
|
1682
|
+
|
|
1473
1683
|
def build_parser() -> argparse.ArgumentParser:
|
|
1474
1684
|
parser = argparse.ArgumentParser(prog="harness_plan_finalize.py")
|
|
1475
1685
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
1686
|
+
republish = sub.add_parser(
|
|
1687
|
+
"republish",
|
|
1688
|
+
help=(
|
|
1689
|
+
"publish an approved amendment to an already-finalized plan "
|
|
1690
|
+
"(allocates a fresh attempt and records what it supersedes)"
|
|
1691
|
+
),
|
|
1692
|
+
)
|
|
1693
|
+
republish.add_argument("--change-dir", required=True)
|
|
1694
|
+
republish.add_argument("--staging-dir", required=True)
|
|
1695
|
+
republish.add_argument("--change", required=True)
|
|
1696
|
+
republish.add_argument(
|
|
1697
|
+
"--run-id", required=True, help="a fresh run id, unused by this change"
|
|
1698
|
+
)
|
|
1699
|
+
republish.add_argument(
|
|
1700
|
+
"--reason", required=True, help="why the published plan is being amended"
|
|
1701
|
+
)
|
|
1702
|
+
republish.add_argument("--json", action="store_true")
|
|
1476
1703
|
finalize = sub.add_parser("finalize")
|
|
1477
1704
|
finalize.add_argument("--change-dir", required=True)
|
|
1478
1705
|
finalize.add_argument("--staging-dir", required=True)
|
|
@@ -1490,6 +1717,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1490
1717
|
args = build_parser().parse_args(argv)
|
|
1491
1718
|
if args.command == "verify":
|
|
1492
1719
|
result = verify_plan(Path(args.change_dir))
|
|
1720
|
+
elif args.command == "republish":
|
|
1721
|
+
result = republish_plan(
|
|
1722
|
+
Path(args.change_dir),
|
|
1723
|
+
Path(args.staging_dir),
|
|
1724
|
+
change_name=args.change,
|
|
1725
|
+
run_id=args.run_id,
|
|
1726
|
+
reason=args.reason,
|
|
1727
|
+
)
|
|
1493
1728
|
else:
|
|
1494
1729
|
result = finalize_plan(
|
|
1495
1730
|
Path(args.change_dir),
|
|
@@ -1462,6 +1462,10 @@ def _job_id_path(change_dir: Path) -> Path:
|
|
|
1462
1462
|
return _runtime_dir(change_dir) / "_harness_service.job.id"
|
|
1463
1463
|
|
|
1464
1464
|
|
|
1465
|
+
def _breakaway_note_path(change_dir: Path) -> Path:
|
|
1466
|
+
return _runtime_dir(change_dir) / "_harness_service.breakaway.json"
|
|
1467
|
+
|
|
1468
|
+
|
|
1465
1469
|
def _child_pid_path(change_dir: Path) -> Path:
|
|
1466
1470
|
return _runtime_dir(change_dir) / "_harness_service.child.pid"
|
|
1467
1471
|
|
|
@@ -1574,20 +1578,56 @@ def _start_detached_service_windows(
|
|
|
1574
1578
|
str(pid_path),
|
|
1575
1579
|
job_id,
|
|
1576
1580
|
]
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
)
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1581
|
+
base_flags = _DETACHED_PROCESS | _CREATE_NEW_PROCESS_GROUP | _CREATE_NO_WINDOW
|
|
1582
|
+
# CREATE_BREAKAWAY_FROM_JOB fails with ERROR_ACCESS_DENIED (WinError 5)
|
|
1583
|
+
# whenever the *caller* already runs inside a Job Object that was created
|
|
1584
|
+
# without JOB_OBJECT_LIMIT_BREAKAWAY_OK — which is exactly how agent CLIs
|
|
1585
|
+
# (Claude Code / CodeBuddy / Codex) contain their child processes. Breakaway
|
|
1586
|
+
# is an optimisation (the service outlives the agent session), not a
|
|
1587
|
+
# requirement, so fall back to spawning inside the caller's job rather than
|
|
1588
|
+
# failing the whole start.
|
|
1589
|
+
launcher_proc = None
|
|
1590
|
+
breakaway = True
|
|
1591
|
+
try:
|
|
1592
|
+
launcher_proc = subprocess.Popen(
|
|
1593
|
+
launcher_args,
|
|
1594
|
+
cwd=str(cwd),
|
|
1595
|
+
stdin=subprocess.DEVNULL,
|
|
1596
|
+
stdout=subprocess.DEVNULL,
|
|
1597
|
+
stderr=subprocess.DEVNULL,
|
|
1598
|
+
creationflags=base_flags | _CREATE_BREAKAWAY_FROM_JOB,
|
|
1599
|
+
close_fds=False,
|
|
1600
|
+
)
|
|
1601
|
+
except OSError as exc:
|
|
1602
|
+
if getattr(exc, "winerror", None) != 5:
|
|
1603
|
+
raise
|
|
1604
|
+
breakaway = False
|
|
1605
|
+
launcher_proc = subprocess.Popen(
|
|
1606
|
+
launcher_args,
|
|
1607
|
+
cwd=str(cwd),
|
|
1608
|
+
stdin=subprocess.DEVNULL,
|
|
1609
|
+
stdout=subprocess.DEVNULL,
|
|
1610
|
+
stderr=subprocess.DEVNULL,
|
|
1611
|
+
creationflags=base_flags,
|
|
1612
|
+
close_fds=False,
|
|
1613
|
+
)
|
|
1614
|
+
_breakaway_note_path(change_dir).write_text(
|
|
1615
|
+
json.dumps(
|
|
1616
|
+
{
|
|
1617
|
+
"breakawayFromJob": breakaway,
|
|
1618
|
+
"note": (
|
|
1619
|
+
None
|
|
1620
|
+
if breakaway
|
|
1621
|
+
else "spawned inside the caller's Job Object "
|
|
1622
|
+
"(CREATE_BREAKAWAY_FROM_JOB denied); the service is "
|
|
1623
|
+
"terminated when the agent session ends"
|
|
1624
|
+
),
|
|
1625
|
+
},
|
|
1626
|
+
ensure_ascii=False,
|
|
1627
|
+
)
|
|
1628
|
+
+ "\n",
|
|
1629
|
+
encoding="utf-8",
|
|
1630
|
+
newline="\n",
|
|
1591
1631
|
)
|
|
1592
1632
|
_launcher_pid_path(change_dir).write_text(str(launcher_proc.pid), encoding="utf-8")
|
|
1593
1633
|
_wait_for_child_pid(pid_path)
|
|
@@ -2400,7 +2440,21 @@ def _start_and_record(
|
|
|
2400
2440
|
}
|
|
2401
2441
|
)
|
|
2402
2442
|
write_json(mutation_state_path(change_dir), mutation_state)
|
|
2403
|
-
|
|
2443
|
+
if getattr(exc, "winerror", None) == 5:
|
|
2444
|
+
return emit_error(
|
|
2445
|
+
"failed to start service: Windows denied process creation "
|
|
2446
|
+
f"({exc}). The agent shell blocks detached spawns; start the "
|
|
2447
|
+
"service from a terminal the agent does not own (or use the "
|
|
2448
|
+
"documented nohup fallback) and re-run `ensure` so the harness "
|
|
2449
|
+
"adopts the running instance.",
|
|
2450
|
+
as_json=as_json,
|
|
2451
|
+
reasonCode="SERVICE_SPAWN_DENIED",
|
|
2452
|
+
)
|
|
2453
|
+
return emit_error(
|
|
2454
|
+
f"failed to start service: {exc}",
|
|
2455
|
+
as_json=as_json,
|
|
2456
|
+
reasonCode="SERVICE_START_FAILED",
|
|
2457
|
+
)
|
|
2404
2458
|
|
|
2405
2459
|
# Give the OS a moment to register the process before identity/create-time reads
|
|
2406
2460
|
time.sleep(0.15)
|
|
@@ -178,6 +178,32 @@ def _state_project_root(project: Path) -> Path:
|
|
|
178
178
|
return resolved.parent if resolved.name == ".git" else project
|
|
179
179
|
|
|
180
180
|
|
|
181
|
+
def _invalid_project_root(
|
|
182
|
+
action: str, project: Path | str, resolved: Path
|
|
183
|
+
) -> dict[str, Any] | None:
|
|
184
|
+
"""Reject a --project that is not an existing directory.
|
|
185
|
+
|
|
186
|
+
Passing the *project name* (``--project udp``) instead of its path used to
|
|
187
|
+
resolve to ``<cwd>/udp`` and then surface as SNAPSHOT_MISSING — an error
|
|
188
|
+
that points at the wrong thing entirely. Fail here with the resolved path
|
|
189
|
+
so the real mistake is visible.
|
|
190
|
+
"""
|
|
191
|
+
if resolved.is_dir():
|
|
192
|
+
return None
|
|
193
|
+
return _result(
|
|
194
|
+
False,
|
|
195
|
+
action,
|
|
196
|
+
"PROJECT_ROOT_INVALID",
|
|
197
|
+
[],
|
|
198
|
+
project=str(project),
|
|
199
|
+
resolvedProject=str(resolved),
|
|
200
|
+
hint=(
|
|
201
|
+
"--project takes a filesystem path to the project root "
|
|
202
|
+
"(use '.' when running from it), not the project name"
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
181
207
|
def _change_dir(project: Path, change_dir: Path | str) -> Path | None:
|
|
182
208
|
state_project = _state_project_root(project)
|
|
183
209
|
candidate = Path(change_dir)
|
|
@@ -556,6 +582,9 @@ def record(
|
|
|
556
582
|
) -> dict[str, Any]:
|
|
557
583
|
action = "record"
|
|
558
584
|
project_root = Path(project).resolve()
|
|
585
|
+
invalid_root = _invalid_project_root(action, project, project_root)
|
|
586
|
+
if invalid_root is not None:
|
|
587
|
+
return invalid_root
|
|
559
588
|
if not files:
|
|
560
589
|
return _result(False, action, "EMPTY_FILES", [])
|
|
561
590
|
if reason not in REASONS:
|
|
@@ -1452,6 +1481,9 @@ def _reconcile_close_manifest(
|
|
|
1452
1481
|
def begin(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
1453
1482
|
action = "begin"
|
|
1454
1483
|
project_root = Path(project).resolve()
|
|
1484
|
+
invalid_root = _invalid_project_root(action, project, project_root)
|
|
1485
|
+
if invalid_root is not None:
|
|
1486
|
+
return invalid_root
|
|
1455
1487
|
change_root = _change_dir(project_root, change_dir)
|
|
1456
1488
|
if change_root is None:
|
|
1457
1489
|
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
@@ -1545,12 +1577,24 @@ def begin(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
|
1545
1577
|
def close(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
1546
1578
|
action = "close"
|
|
1547
1579
|
project_root = Path(project).resolve()
|
|
1580
|
+
invalid_root = _invalid_project_root(action, project, project_root)
|
|
1581
|
+
if invalid_root is not None:
|
|
1582
|
+
return invalid_root
|
|
1548
1583
|
change_root = _change_dir(project_root, change_dir)
|
|
1549
1584
|
if change_root is None:
|
|
1550
1585
|
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
1551
1586
|
snapshot_path = _state_root(change_root) / SNAPSHOT_REL
|
|
1552
1587
|
if not snapshot_path.is_file():
|
|
1553
|
-
|
|
1588
|
+
# Name the exact path: this snapshot is evidence/test-guard-snapshot.json,
|
|
1589
|
+
# not meta/state-snapshot.json, and the two are easy to confuse.
|
|
1590
|
+
return _result(
|
|
1591
|
+
False,
|
|
1592
|
+
action,
|
|
1593
|
+
"SNAPSHOT_MISSING",
|
|
1594
|
+
[],
|
|
1595
|
+
expectedSnapshot=str(snapshot_path),
|
|
1596
|
+
hint="run `harness_test_guard.py begin` for this change first",
|
|
1597
|
+
)
|
|
1554
1598
|
try:
|
|
1555
1599
|
snapshot = _read_json(snapshot_path)
|
|
1556
1600
|
except (OSError, json.JSONDecodeError) as exc:
|
|
@@ -1752,6 +1796,9 @@ def mark(
|
|
|
1752
1796
|
def stage(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
1753
1797
|
action = "stage"
|
|
1754
1798
|
project_root = Path(project).resolve()
|
|
1799
|
+
invalid_root = _invalid_project_root(action, project, project_root)
|
|
1800
|
+
if invalid_root is not None:
|
|
1801
|
+
return invalid_root
|
|
1755
1802
|
change_root = _change_dir(project_root, change_dir)
|
|
1756
1803
|
if change_root is None:
|
|
1757
1804
|
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-archive — 变更归档
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-codebase-map — 代码库地图
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -8,7 +8,7 @@ disallowed-tools:
|
|
|
8
8
|
- Bash(git *)
|
|
9
9
|
- Bash(curl *)
|
|
10
10
|
---
|
|
11
|
-
<!-- generated by harness_deploy.py; core=
|
|
11
|
+
<!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=claude-code; do not edit -->
|
|
12
12
|
# harness-knowledge-ingest
|
|
13
13
|
|
|
14
14
|
知识 ingest 完全由 Hunter Platform 负责。归档完成时,客户端生成一个确定性 ZIP;
|
|
@@ -8,7 +8,7 @@ disallowed-tools:
|
|
|
8
8
|
- Bash(git *)
|
|
9
9
|
- Bash(curl *)
|
|
10
10
|
---
|
|
11
|
-
<!-- generated by harness_deploy.py; core=
|
|
11
|
+
<!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=claude-code; do not edit -->
|
|
12
12
|
# harness-knowledge-query
|
|
13
13
|
|
|
14
14
|
项目知识以 Hunter Platform 的服务端索引为唯一真源。客户端只提交查询并消费结果:
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-package — 增量模块打包(Java overlay)
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-plan — 需求规划
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -120,6 +120,8 @@ change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`
|
|
|
120
120
|
| 空目录 | 不得为“预留目录”生成 `.gitkeep`;只有产品明确需要跟踪空目录时才能创建,并在计划中说明业务原因 |
|
|
121
121
|
| 设计审批包 | 一次 blocking user confirmation 含 worktree(读 `harness.json` `defaultWorktree`) |
|
|
122
122
|
| 阶段 8 | 二选一且不得混用:**v2** = `hunter-harness plan finalize`(证据包 → 八 target + journal committed + plan-events.ndjson);**legacy** = 六项标准产物先进入 staging,仅 finalizer 校验成功后发布并写唯一 `phase.end`/log,随后 `verify` 确认 start/end、收据完整覆盖六项标准产物、哈希、全部任务表和非空场景清单一致。失败均不得手工补终态 |
|
|
123
|
+
| 发布后改产物 | 用 `harness_plan_finalize.py republish --run-id <全新> --reason "<why>"` 一次完成(新 attempt + 换收据 + 重新派生 manifest)。重跑 `finalize` 会报 `PLAN_FINALIZATION_HASH_CONFLICT`;**绝不手改 `meta/scenario-manifest.json`**(派生物,手改必致 `ARTIFACT_HASH_DRIFT`)→ `reference.md`「发布后修订计划」 |
|
|
124
|
+
| v2 输入骨架 | 不要猜 `plan-evidence-input.json` 结构,也不要去翻 TS 接口/npx 缓存:`npx hunter-harness plan evidence-pack --print-template` 直接给带占位符的完整骨架 |
|
|
123
125
|
| Plan 结束 | **禁止**询问执行模式;只提示 `/harness-run` |
|
|
124
126
|
| 知识查询 | 阶段 1 失败不得假装已读历史,也不得改用本地索引或其他执行入口 |
|
|
125
127
|
| 歧义优先检查 | 否定、对比、动作对象或范围存在多种合理解释时,最小取证后先给推荐理解并一次一问;确认前不深挖错误方向 |
|