@hunter-harness/workflow-harness 0.2.20 → 0.2.21
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/CONTEXT.md +20 -0
- 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 +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/claude-code/scripts/harness_events.py +159 -1
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/CONTEXT.md +20 -0
- 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 +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +159 -1
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/CONTEXT.md +20 -0
- 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 +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codex/scripts/harness_events.py +159 -1
- package/harness/bundles/general/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/CONTEXT.md +20 -0
- 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 +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/cursor/scripts/harness_events.py +159 -1
- package/harness/bundles/general/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/CONTEXT.md +20 -0
- 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 +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +6 -0
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/claude-code/scripts/harness_events.py +159 -1
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/CONTEXT.md +20 -0
- 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 +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +6 -0
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +159 -1
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/CONTEXT.md +20 -0
- 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 +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +6 -0
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codex/scripts/harness_events.py +159 -1
- package/harness/bundles/java/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/CONTEXT.md +20 -0
- 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 +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +6 -0
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/cursor/scripts/harness_events.py +159 -1
- package/harness/bundles/java/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/manifests/general/claude-code.json +25 -17
- package/harness/manifests/general/codebuddy.json +25 -17
- package/harness/manifests/general/codex.json +25 -17
- package/harness/manifests/general/cursor.json +25 -17
- package/harness/manifests/java/claude-code.json +26 -18
- package/harness/manifests/java/codebuddy.json +26 -18
- package/harness/manifests/java/codex.json +26 -18
- package/harness/manifests/java/cursor.json +26 -18
- package/hunter-workflow-family.json +1 -1
- package/package.json +1 -1
|
@@ -1055,6 +1055,7 @@ def decide_can_reuse(
|
|
|
1055
1055
|
requested_toolchain_hash: str | None = None,
|
|
1056
1056
|
requested_profile_hash: str | None = None,
|
|
1057
1057
|
requested_environment_hash: str | None = None,
|
|
1058
|
+
requested_db_schema_hash: str | None = None,
|
|
1058
1059
|
) -> dict[str, Any]:
|
|
1059
1060
|
ledger, ledger_path = load_ledger(change_dir)
|
|
1060
1061
|
if ledger is None:
|
|
@@ -1183,6 +1184,7 @@ def decide_can_reuse(
|
|
|
1183
1184
|
("toolchainHash", requested_toolchain_hash, "TOOLCHAIN_CHANGED"),
|
|
1184
1185
|
("profileHash", requested_profile_hash, "PROFILE_CHANGED"),
|
|
1185
1186
|
("environmentHash", requested_environment_hash, "ENVIRONMENT_CHANGED"),
|
|
1187
|
+
("dbSchemaHash", requested_db_schema_hash, "DB_SCHEMA_CHANGED"),
|
|
1186
1188
|
):
|
|
1187
1189
|
stored = entry.get(field)
|
|
1188
1190
|
if (
|
|
@@ -1249,6 +1251,7 @@ def decide_can_reuse(
|
|
|
1249
1251
|
"reuse": True,
|
|
1250
1252
|
"reason": "reuse",
|
|
1251
1253
|
"code": "REUSED",
|
|
1254
|
+
"invalidationCode": None,
|
|
1252
1255
|
"verification": verification,
|
|
1253
1256
|
"ledger_path": str(ledger_path) if ledger_path else None,
|
|
1254
1257
|
"inputsHash": stored_hash,
|
|
@@ -1258,6 +1261,90 @@ def decide_can_reuse(
|
|
|
1258
1261
|
}
|
|
1259
1262
|
|
|
1260
1263
|
|
|
1264
|
+
_INVALIDATION_ALIASES = {
|
|
1265
|
+
"INPUTS_HASH_CHANGED": "INPUT_CHANGED",
|
|
1266
|
+
"ENVIRONMENT_CHANGED": "ENV_CHANGED",
|
|
1267
|
+
"MISSING_FIELDS": "MISSING_IDENTITY_FIELD",
|
|
1268
|
+
"MISSING_V2_FIELDS": "MISSING_IDENTITY_FIELD",
|
|
1269
|
+
"LEDGER_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1270
|
+
"VALIDATIONS_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1271
|
+
"VALIDATION_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def _attach_invalidation_code(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1276
|
+
"""Wave-2 H-9: expose stable invalidationCode alongside legacy code."""
|
|
1277
|
+
if payload.get("reuse") is True:
|
|
1278
|
+
payload.setdefault("invalidationCode", None)
|
|
1279
|
+
return payload
|
|
1280
|
+
code = str(payload.get("code") or "").strip()
|
|
1281
|
+
if code:
|
|
1282
|
+
payload["invalidationCode"] = _INVALIDATION_ALIASES.get(code, code)
|
|
1283
|
+
return payload
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def verification_graph_path(change_dir: Path) -> Path:
|
|
1287
|
+
return Path(change_dir) / "evidence" / "verification-graph.json"
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def upsert_verification_graph_node(
|
|
1291
|
+
change_dir: Path,
|
|
1292
|
+
*,
|
|
1293
|
+
verification: str,
|
|
1294
|
+
status: str,
|
|
1295
|
+
inputs_hash: str,
|
|
1296
|
+
command: str | None = None,
|
|
1297
|
+
toolchain_hash: str | None = None,
|
|
1298
|
+
environment_hash: str | None = None,
|
|
1299
|
+
db_schema_hash: str | None = None,
|
|
1300
|
+
diff_hash: str | None = None,
|
|
1301
|
+
) -> dict[str, Any]:
|
|
1302
|
+
"""Persist a verification graph node keyed by canonical identity (H-9)."""
|
|
1303
|
+
path = verification_graph_path(change_dir)
|
|
1304
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1305
|
+
graph: dict[str, Any]
|
|
1306
|
+
if path.is_file():
|
|
1307
|
+
try:
|
|
1308
|
+
loaded = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
1309
|
+
graph = loaded if isinstance(loaded, dict) else {"schemaVersion": 1, "nodes": []}
|
|
1310
|
+
except (OSError, json.JSONDecodeError):
|
|
1311
|
+
graph = {"schemaVersion": 1, "nodes": []}
|
|
1312
|
+
else:
|
|
1313
|
+
graph = {"schemaVersion": 1, "nodes": []}
|
|
1314
|
+
nodes = graph.get("nodes")
|
|
1315
|
+
if not isinstance(nodes, list):
|
|
1316
|
+
nodes = []
|
|
1317
|
+
identity = {
|
|
1318
|
+
"verification": verification,
|
|
1319
|
+
"inputsHash": inputs_hash,
|
|
1320
|
+
"command": command,
|
|
1321
|
+
"toolchainHash": toolchain_hash,
|
|
1322
|
+
"environmentHash": environment_hash,
|
|
1323
|
+
"dbSchemaHash": db_schema_hash,
|
|
1324
|
+
"diffHash": diff_hash,
|
|
1325
|
+
}
|
|
1326
|
+
node = {
|
|
1327
|
+
"identity": identity,
|
|
1328
|
+
"status": status,
|
|
1329
|
+
"recordedAt": dt.datetime.now().astimezone().isoformat(timespec="milliseconds"),
|
|
1330
|
+
}
|
|
1331
|
+
replaced = False
|
|
1332
|
+
for index, existing in enumerate(nodes):
|
|
1333
|
+
if not isinstance(existing, dict):
|
|
1334
|
+
continue
|
|
1335
|
+
existing_id = existing.get("identity")
|
|
1336
|
+
if isinstance(existing_id, dict) and existing_id == identity:
|
|
1337
|
+
nodes[index] = node
|
|
1338
|
+
replaced = True
|
|
1339
|
+
break
|
|
1340
|
+
if not replaced:
|
|
1341
|
+
nodes.append(node)
|
|
1342
|
+
graph["schemaVersion"] = 1
|
|
1343
|
+
graph["nodes"] = nodes
|
|
1344
|
+
write_ledger(path, graph) # atomic json write helper already used for ledger
|
|
1345
|
+
return {"ok": True, "path": str(path), "nodeCount": len(nodes), "replaced": replaced}
|
|
1346
|
+
|
|
1347
|
+
|
|
1261
1348
|
def cmd_hash(args: argparse.Namespace) -> int:
|
|
1262
1349
|
as_json = bool(args.json)
|
|
1263
1350
|
try:
|
|
@@ -1330,10 +1417,12 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
1330
1417
|
requested_toolchain_hash=getattr(args, "toolchain_hash", None),
|
|
1331
1418
|
requested_profile_hash=getattr(args, "profile_hash", None),
|
|
1332
1419
|
requested_environment_hash=getattr(args, "environment_hash", None),
|
|
1420
|
+
requested_db_schema_hash=getattr(args, "db_schema_hash", None),
|
|
1333
1421
|
)
|
|
1334
1422
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1335
1423
|
return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
|
|
1336
1424
|
|
|
1425
|
+
payload = _attach_invalidation_code(payload)
|
|
1337
1426
|
emit_compact_or_verbose(
|
|
1338
1427
|
payload, as_json=as_json, verbose=verbose,
|
|
1339
1428
|
compact_fn=_compact_can_reuse_payload,
|
|
@@ -1470,6 +1559,7 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1470
1559
|
("toolchainHash", "toolchain_hash"),
|
|
1471
1560
|
("profileHash", "profile_hash"),
|
|
1472
1561
|
("environmentHash", "environment_hash"),
|
|
1562
|
+
("dbSchemaHash", "db_schema_hash"),
|
|
1473
1563
|
):
|
|
1474
1564
|
val = getattr(args, attr, None)
|
|
1475
1565
|
if _nonempty_str(val):
|
|
@@ -1548,6 +1638,37 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1548
1638
|
extra={"missing": missing},
|
|
1549
1639
|
)
|
|
1550
1640
|
write_ledger(out_path, ledger)
|
|
1641
|
+
try:
|
|
1642
|
+
upsert_verification_graph_node(
|
|
1643
|
+
change_dir,
|
|
1644
|
+
verification=verification,
|
|
1645
|
+
status=str(status),
|
|
1646
|
+
inputs_hash=str(inputs_hash),
|
|
1647
|
+
command=str(entry.get("command") or "") or None,
|
|
1648
|
+
toolchain_hash=(
|
|
1649
|
+
str(entry.get("toolchainHash")).strip()
|
|
1650
|
+
if _nonempty_str(entry.get("toolchainHash"))
|
|
1651
|
+
else None
|
|
1652
|
+
),
|
|
1653
|
+
environment_hash=(
|
|
1654
|
+
str(entry.get("environmentHash")).strip()
|
|
1655
|
+
if _nonempty_str(entry.get("environmentHash"))
|
|
1656
|
+
else None
|
|
1657
|
+
),
|
|
1658
|
+
db_schema_hash=(
|
|
1659
|
+
str(entry.get("dbSchemaHash")).strip()
|
|
1660
|
+
if _nonempty_str(entry.get("dbSchemaHash"))
|
|
1661
|
+
else None
|
|
1662
|
+
),
|
|
1663
|
+
diff_hash=(
|
|
1664
|
+
str(ledger.get("diffHash")).strip()
|
|
1665
|
+
if _nonempty_str(ledger.get("diffHash"))
|
|
1666
|
+
else None
|
|
1667
|
+
),
|
|
1668
|
+
)
|
|
1669
|
+
except (OSError, ValueError, TypeError):
|
|
1670
|
+
# Graph is advisory persistence; ledger write already succeeded.
|
|
1671
|
+
pass
|
|
1551
1672
|
except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
|
|
1552
1673
|
return emit_error(f"record failed: {exc}", as_json=as_json)
|
|
1553
1674
|
|
|
@@ -1667,6 +1788,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1667
1788
|
default=None,
|
|
1668
1789
|
help="optional environment hash to compare against ledger entry (UT-017)",
|
|
1669
1790
|
)
|
|
1791
|
+
p_reuse.add_argument(
|
|
1792
|
+
"--db-schema-hash",
|
|
1793
|
+
default=None,
|
|
1794
|
+
help="optional DB schema hash to compare against ledger entry (H-9)",
|
|
1795
|
+
)
|
|
1670
1796
|
p_reuse.add_argument(
|
|
1671
1797
|
"--verbose",
|
|
1672
1798
|
action="store_true",
|
|
@@ -1708,6 +1834,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1708
1834
|
p_record.add_argument("--toolchain-hash", default=None)
|
|
1709
1835
|
p_record.add_argument("--profile-hash", default=None)
|
|
1710
1836
|
p_record.add_argument("--environment-hash", default=None)
|
|
1837
|
+
p_record.add_argument("--db-schema-hash", default=None)
|
|
1711
1838
|
p_record.add_argument("--deploy-artifact", default=None, help="package: built artifact path")
|
|
1712
1839
|
p_record.add_argument("--artifact-hash", default=None, help="package: artifact sha256")
|
|
1713
1840
|
p_record.add_argument(
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Migration head single-source consistency check (Wave-2 H-7).
|
|
3
|
+
|
|
4
|
+
Canonical file (default ``.harness/config/migration-head.json``)::
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"schemaVersion": 1,
|
|
8
|
+
"head": "005_example",
|
|
9
|
+
"sources": [
|
|
10
|
+
{"path": "backend/app/config.py", "pattern": "EXPECTED_ALEMBIC_HEAD\\s*=\\s*[\\\"']([^\\\"']+)[\\\"']"},
|
|
11
|
+
{"path": "integration/manifest.json", "jsonPointer": "/migrationHead"}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Each source must resolve to the same ``head`` value. This harness repo may not
|
|
16
|
+
ship alembic; consumer projects declare the canonical file and run::
|
|
17
|
+
|
|
18
|
+
python harness_migration_head.py check --project . --json
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
DEFAULT_REL = ".harness/config/migration-head.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def emit_json(payload: dict[str, Any], *, as_json: bool) -> None:
|
|
34
|
+
if as_json:
|
|
35
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
36
|
+
else:
|
|
37
|
+
code = payload.get("code") or ("OK" if payload.get("ok") else "FAIL")
|
|
38
|
+
sys.stdout.write(f"{code}\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def emit_error(code: str, message: str, *, as_json: bool, extra: dict[str, Any] | None = None) -> int:
|
|
42
|
+
payload: dict[str, Any] = {"ok": False, "code": code, "message": message}
|
|
43
|
+
if extra:
|
|
44
|
+
payload.update(extra)
|
|
45
|
+
stream = sys.stdout if as_json else sys.stderr
|
|
46
|
+
if as_json:
|
|
47
|
+
stream.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
48
|
+
else:
|
|
49
|
+
stream.write(f"{code}: {message}\n")
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read_json_pointer(data: Any, pointer: str) -> Any:
|
|
54
|
+
if not pointer.startswith("/"):
|
|
55
|
+
raise ValueError(f"jsonPointer must start with /: {pointer}")
|
|
56
|
+
cur = data
|
|
57
|
+
for part in pointer.lstrip("/").split("/"):
|
|
58
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
59
|
+
if isinstance(cur, dict) and part in cur:
|
|
60
|
+
cur = cur[part]
|
|
61
|
+
else:
|
|
62
|
+
raise KeyError(pointer)
|
|
63
|
+
return cur
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _extract_source(project: Path, source: dict[str, Any]) -> str:
|
|
67
|
+
rel = str(source.get("path") or "").strip().replace("\\", "/")
|
|
68
|
+
if not rel or rel.startswith("/") or ".." in rel.split("/"):
|
|
69
|
+
raise ValueError(f"invalid source path: {rel}")
|
|
70
|
+
target = (project / rel).resolve()
|
|
71
|
+
try:
|
|
72
|
+
target.relative_to(project.resolve())
|
|
73
|
+
except ValueError as exc:
|
|
74
|
+
raise ValueError(f"source escapes project: {rel}") from exc
|
|
75
|
+
if not target.is_file():
|
|
76
|
+
raise FileNotFoundError(rel)
|
|
77
|
+
text = target.read_text(encoding="utf-8-sig")
|
|
78
|
+
if "jsonPointer" in source and source["jsonPointer"]:
|
|
79
|
+
data = json.loads(text)
|
|
80
|
+
value = _read_json_pointer(data, str(source["jsonPointer"]))
|
|
81
|
+
if value is None or (isinstance(value, str) and not value.strip()):
|
|
82
|
+
raise ValueError(f"empty value at {source['jsonPointer']} in {rel}")
|
|
83
|
+
return str(value).strip()
|
|
84
|
+
pattern = str(source.get("pattern") or "").strip()
|
|
85
|
+
if not pattern:
|
|
86
|
+
raise ValueError(f"source {rel} requires pattern or jsonPointer")
|
|
87
|
+
match = re.search(pattern, text, flags=re.MULTILINE)
|
|
88
|
+
if not match:
|
|
89
|
+
raise ValueError(f"pattern did not match in {rel}")
|
|
90
|
+
return str(match.group(1) if match.lastindex else match.group(0)).strip()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def check_migration_head(
|
|
94
|
+
project: Path,
|
|
95
|
+
*,
|
|
96
|
+
config_rel: str = DEFAULT_REL,
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
project = project.resolve()
|
|
99
|
+
config_path = (project / config_rel).resolve()
|
|
100
|
+
try:
|
|
101
|
+
config_path.relative_to(project)
|
|
102
|
+
except ValueError:
|
|
103
|
+
return {
|
|
104
|
+
"ok": False,
|
|
105
|
+
"code": "MIGRATION_HEAD_CONFIG_ESCAPE",
|
|
106
|
+
"message": f"config path escapes project: {config_rel}",
|
|
107
|
+
}
|
|
108
|
+
if not config_path.is_file():
|
|
109
|
+
return {
|
|
110
|
+
"ok": False,
|
|
111
|
+
"code": "MIGRATION_HEAD_MISSING",
|
|
112
|
+
"message": f"canonical migration-head file missing: {config_rel}",
|
|
113
|
+
"configPath": str(config_path),
|
|
114
|
+
}
|
|
115
|
+
try:
|
|
116
|
+
data = json.loads(config_path.read_text(encoding="utf-8-sig"))
|
|
117
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
118
|
+
return {
|
|
119
|
+
"ok": False,
|
|
120
|
+
"code": "MIGRATION_HEAD_UNREADABLE",
|
|
121
|
+
"message": str(exc),
|
|
122
|
+
"configPath": str(config_path),
|
|
123
|
+
}
|
|
124
|
+
if not isinstance(data, dict) or data.get("schemaVersion") != 1:
|
|
125
|
+
return {
|
|
126
|
+
"ok": False,
|
|
127
|
+
"code": "MIGRATION_HEAD_SCHEMA_INVALID",
|
|
128
|
+
"message": "migration-head.json must be schemaVersion 1 object",
|
|
129
|
+
"configPath": str(config_path),
|
|
130
|
+
}
|
|
131
|
+
head = str(data.get("head") or "").strip()
|
|
132
|
+
sources = data.get("sources")
|
|
133
|
+
if not head:
|
|
134
|
+
return {
|
|
135
|
+
"ok": False,
|
|
136
|
+
"code": "MIGRATION_HEAD_EMPTY",
|
|
137
|
+
"message": "head must be a non-empty string",
|
|
138
|
+
"configPath": str(config_path),
|
|
139
|
+
}
|
|
140
|
+
if not isinstance(sources, list) or not sources:
|
|
141
|
+
return {
|
|
142
|
+
"ok": False,
|
|
143
|
+
"code": "MIGRATION_HEAD_SOURCES_EMPTY",
|
|
144
|
+
"message": "sources must be a non-empty array",
|
|
145
|
+
"configPath": str(config_path),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
observed: list[dict[str, Any]] = []
|
|
149
|
+
drifts: list[dict[str, Any]] = []
|
|
150
|
+
for index, source in enumerate(sources):
|
|
151
|
+
if not isinstance(source, dict):
|
|
152
|
+
return {
|
|
153
|
+
"ok": False,
|
|
154
|
+
"code": "MIGRATION_HEAD_SOURCE_INVALID",
|
|
155
|
+
"message": f"sources[{index}] must be an object",
|
|
156
|
+
}
|
|
157
|
+
try:
|
|
158
|
+
value = _extract_source(project, source)
|
|
159
|
+
except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc:
|
|
160
|
+
return {
|
|
161
|
+
"ok": False,
|
|
162
|
+
"code": "MIGRATION_HEAD_SOURCE_UNREADABLE",
|
|
163
|
+
"message": str(exc),
|
|
164
|
+
"index": index,
|
|
165
|
+
"path": source.get("path"),
|
|
166
|
+
}
|
|
167
|
+
item = {"path": source.get("path"), "value": value}
|
|
168
|
+
observed.append(item)
|
|
169
|
+
if value != head:
|
|
170
|
+
drifts.append({**item, "expected": head})
|
|
171
|
+
|
|
172
|
+
if drifts:
|
|
173
|
+
return {
|
|
174
|
+
"ok": False,
|
|
175
|
+
"code": "MIGRATION_HEAD_DRIFT",
|
|
176
|
+
"message": "one or more sources disagree with canonical head",
|
|
177
|
+
"head": head,
|
|
178
|
+
"drifts": drifts,
|
|
179
|
+
"observed": observed,
|
|
180
|
+
"configPath": str(config_path),
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
"ok": True,
|
|
184
|
+
"code": "MIGRATION_HEAD_OK",
|
|
185
|
+
"head": head,
|
|
186
|
+
"observed": observed,
|
|
187
|
+
"configPath": str(config_path),
|
|
188
|
+
"sourceCount": len(observed),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
193
|
+
parser = argparse.ArgumentParser(prog="harness_migration_head.py")
|
|
194
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
195
|
+
check = sub.add_parser("check", help="verify sources match canonical head")
|
|
196
|
+
check.add_argument("--project", required=True)
|
|
197
|
+
check.add_argument("--config", default=DEFAULT_REL)
|
|
198
|
+
check.add_argument("--json", action="store_true")
|
|
199
|
+
return parser
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main(argv: list[str] | None = None) -> int:
|
|
203
|
+
args = build_parser().parse_args(argv)
|
|
204
|
+
as_json = bool(getattr(args, "json", False))
|
|
205
|
+
if args.command == "check":
|
|
206
|
+
result = check_migration_head(Path(args.project), config_rel=str(args.config))
|
|
207
|
+
if not result.get("ok"):
|
|
208
|
+
return emit_error(
|
|
209
|
+
str(result.get("code") or "MIGRATION_HEAD_FAIL"),
|
|
210
|
+
str(result.get("message") or "check failed"),
|
|
211
|
+
as_json=as_json,
|
|
212
|
+
extra={k: v for k, v in result.items() if k not in {"ok", "code", "message"}},
|
|
213
|
+
)
|
|
214
|
+
emit_json(result, as_json=as_json)
|
|
215
|
+
return 0
|
|
216
|
+
return emit_error("UNKNOWN_COMMAND", f"unknown command: {args.command}", as_json=as_json)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
raise SystemExit(main())
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": ".harness-build.json",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "830e24478800623aa6989bbde0b7fd4cf85f1956647556fae2a75b6274ee938d"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"path": "agents/harness-evaluator.md",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "CONTEXT.md",
|
|
26
|
-
"sha256": "
|
|
26
|
+
"sha256": "4838854cc45e8bd47552fb34e41cadca3121eeaa4597196568f35e25534d5a33"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "contracts/ci-metrics.schema.json",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "harness-archive/SKILL.md",
|
|
50
|
-
"sha256": "
|
|
50
|
+
"sha256": "3a140ee15487b686c80ab0a92c3d1d3af196d60c1d625aeeb63daf60929668e1"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"path": "harness-archive/templates/render-summary.mjs",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"path": "harness-codebase-map/SKILL.md",
|
|
70
|
-
"sha256": "
|
|
70
|
+
"sha256": "3f5d3e6edaf5da133f4ec601d15fb4476a250256b62ba6c5e927411eb5eafcc0"
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"path": "harness-codebase-map/templates/ARCHITECTURE.md",
|
|
@@ -131,11 +131,11 @@
|
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"path": "harness-knowledge-ingest/SKILL.md",
|
|
134
|
-
"sha256": "
|
|
134
|
+
"sha256": "0cdb9ef728b4771ce1d5aa85dcb562143a5c25ae7504cb78ebeef5a5635c50a8"
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
"path": "harness-knowledge-query/SKILL.md",
|
|
138
|
-
"sha256": "
|
|
138
|
+
"sha256": "06c4bbe28d8752a318e8ca0d80ea74adb9c95b0d70002471345ad851be5333d0"
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
"path": "harness-plan/checklist.md",
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"path": "harness-plan/SKILL.md",
|
|
154
|
-
"sha256": "
|
|
154
|
+
"sha256": "26a7ae5329dd2aead892e3a7802fe50f67ffa3df4c9c83f3e48e1e25cd3803f4"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
"path": "harness-review/checklist.md",
|
|
@@ -167,11 +167,11 @@
|
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
169
|
"path": "harness-review/SKILL.md",
|
|
170
|
-
"sha256": "
|
|
170
|
+
"sha256": "ec6ae1bb0922c552f5390840995c294b8cb045860ba1583cd388c37e85a64757"
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"path": "harness-run/checklist.md",
|
|
174
|
-
"sha256": "
|
|
174
|
+
"sha256": "62063fd4c20a12eb9295fb5bade31e10adb2f5a4fd8600446b2109b959c82961"
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
"path": "harness-run/protocols.md",
|
|
@@ -183,11 +183,11 @@
|
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"path": "harness-run/SKILL.md",
|
|
186
|
-
"sha256": "
|
|
186
|
+
"sha256": "dd43063183bed8dc0b31c6595abbaa41e7de1409daf803c222fef094180fe025"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"path": "harness-submit/checklist.md",
|
|
190
|
-
"sha256": "
|
|
190
|
+
"sha256": "85f157b611b69f1c10ed26c06308ad5e46233b5b31f22ad511df60cbc0c221cb"
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
"path": "harness-submit/reference.md",
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
},
|
|
196
196
|
{
|
|
197
197
|
"path": "harness-submit/SKILL.md",
|
|
198
|
-
"sha256": "
|
|
198
|
+
"sha256": "95b370f04917ce20822c4f60855a8d6ce3d903cd67d874c28cffd2877fcdb1ea"
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
201
|
"path": "harness-sync/reference.md",
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
},
|
|
204
204
|
{
|
|
205
205
|
"path": "harness-sync/SKILL.md",
|
|
206
|
-
"sha256": "
|
|
206
|
+
"sha256": "04cab37d003d7f87824fc33281bb6404f7acb1d75a3f72565a9142f974e1f4b5"
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
209
|
"path": "harness-test/checklist.md",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
},
|
|
228
228
|
{
|
|
229
229
|
"path": "harness-test/SKILL.md",
|
|
230
|
-
"sha256": "
|
|
230
|
+
"sha256": "c7624934d596e906841f5afab27762887957ba54e30a590afbc8723fc693e4ef"
|
|
231
231
|
},
|
|
232
232
|
{
|
|
233
233
|
"path": "protocols/archive-report-protocol.md",
|
|
@@ -241,6 +241,10 @@
|
|
|
241
241
|
"path": "protocols/ledger-protocol.md",
|
|
242
242
|
"sha256": "50f9deffcf71de009c56bd2cdd44fb05c7b18faf19ec56b00ff76e8ce897ecf7"
|
|
243
243
|
},
|
|
244
|
+
{
|
|
245
|
+
"path": "protocols/model-routing-protocol.md",
|
|
246
|
+
"sha256": "1ce493d7dcc7a0c32b07b2d0118fdca40d72ca2487a63566f3c997293269f06e"
|
|
247
|
+
},
|
|
244
248
|
{
|
|
245
249
|
"path": "protocols/powershell-protocol.md",
|
|
246
250
|
"sha256": "f723f18eee08125e75f387e655f7e604eb603f636cd6809dd6b5948748f32d04"
|
|
@@ -287,7 +291,7 @@
|
|
|
287
291
|
},
|
|
288
292
|
{
|
|
289
293
|
"path": "scripts/harness_events.py",
|
|
290
|
-
"sha256": "
|
|
294
|
+
"sha256": "94880e69acde59e13afaa338d9ac80a5481e1c84abfbffaf7f5438089655d21a"
|
|
291
295
|
},
|
|
292
296
|
{
|
|
293
297
|
"path": "scripts/harness_gate.py",
|
|
@@ -295,11 +299,15 @@
|
|
|
295
299
|
},
|
|
296
300
|
{
|
|
297
301
|
"path": "scripts/harness_integration.py",
|
|
298
|
-
"sha256": "
|
|
302
|
+
"sha256": "78904ed2a6254be6671a2627863759c68a9576ef41584f423c5cde3faa6d6b25"
|
|
299
303
|
},
|
|
300
304
|
{
|
|
301
305
|
"path": "scripts/harness_ledger.py",
|
|
302
|
-
"sha256": "
|
|
306
|
+
"sha256": "11340d0e84fc3905d2d7383683ac94d2b9ab13655e56a26e460d7e9b0b515f1a"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"path": "scripts/harness_migration_head.py",
|
|
310
|
+
"sha256": "6bce06ce5c077185d6e40e1655c37ef98b2f8e6e39d64b2f3f3c45f12854ed51"
|
|
303
311
|
},
|
|
304
312
|
{
|
|
305
313
|
"path": "scripts/harness_paths.py",
|