@momentiq/dark-factory-cli 2.7.0 → 2.8.0
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.
|
@@ -1036,6 +1036,111 @@ def terminal_status_error(cycle_id: str, doc: CycleDoc, pr_type: str, source: st
|
|
|
1036
1036
|
return None
|
|
1037
1037
|
|
|
1038
1038
|
|
|
1039
|
+
OBJECTIVE_ID_RE = re.compile(r"^(cycle\d+(\.\d+)*#ec\d+|issue\d+#ac\d+)$")
|
|
1040
|
+
OBJECTIVES_MANIFEST_PATH = ".darkfactory/objectives.yaml"
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _route_ids(repo_root: Path) -> set[str]:
|
|
1044
|
+
cfg = repo_root / ".agent-review" / "config.json"
|
|
1045
|
+
if not cfg.exists():
|
|
1046
|
+
return set()
|
|
1047
|
+
try:
|
|
1048
|
+
data = json.loads(cfg.read_text())
|
|
1049
|
+
except json.JSONDecodeError:
|
|
1050
|
+
return set()
|
|
1051
|
+
routes = (data.get("validation") or {}).get("verificationRoutes") or []
|
|
1052
|
+
return {r.get("id") for r in routes if isinstance(r, dict) and isinstance(r.get("id"), str)}
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _declared_refs(trailers: "Trailers") -> set[str]:
|
|
1056
|
+
# The PR's declared sources of intent, keyed "<kind>:<ref>" to match an
|
|
1057
|
+
# objective's source. Cycle trailer ref is normalized via normalize_cycle_id
|
|
1058
|
+
# so dotted ids like "318.4" and trailer values like "Cycle 318.4" compare
|
|
1059
|
+
# equal. Issue ref strips a leading "#" so "1234" and "#1234" both match
|
|
1060
|
+
# (mirroring the TS parseObjective's `ref.replace(/^#/, "")`).
|
|
1061
|
+
refs: set[str] = set()
|
|
1062
|
+
if trailers.cycle:
|
|
1063
|
+
cycle_id = normalize_cycle_id(trailers.cycle)
|
|
1064
|
+
if cycle_id:
|
|
1065
|
+
refs.add(f"cycle:{cycle_id}")
|
|
1066
|
+
if trailers.issue:
|
|
1067
|
+
refs.add(f"issue:{re.sub(r'^#', '', trailers.issue).strip()}")
|
|
1068
|
+
return refs
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def validate_objectives(repo_root: Path, trailers: "Trailers") -> list[str]:
|
|
1072
|
+
"""Validate .darkfactory/objectives.yaml against PR context. Empty list = ok.
|
|
1073
|
+
|
|
1074
|
+
v1 checks: id format, source linked by a PR trailer, attestedBy route exists.
|
|
1075
|
+
NO coverage check (the `enforced` flag is the future ratchet). Absent manifest
|
|
1076
|
+
is a no-op — objectives are optional in v1.
|
|
1077
|
+
|
|
1078
|
+
Uses a deferred ``import yaml`` so that a missing pyyaml installation only
|
|
1079
|
+
errors when a repo actually ships ``.darkfactory/objectives.yaml`` (the
|
|
1080
|
+
absent-manifest path is a no-op and never imports yaml). This preserves the
|
|
1081
|
+
validator's dependency-light boot on CI environments that have not installed
|
|
1082
|
+
pyyaml.
|
|
1083
|
+
"""
|
|
1084
|
+
manifest_path = repo_root / OBJECTIVES_MANIFEST_PATH
|
|
1085
|
+
if not manifest_path.exists():
|
|
1086
|
+
return []
|
|
1087
|
+
try:
|
|
1088
|
+
import yaml # deferred: pyyaml not guaranteed in all CI environments
|
|
1089
|
+
data = yaml.safe_load(manifest_path.read_text()) or {}
|
|
1090
|
+
except ImportError:
|
|
1091
|
+
return [
|
|
1092
|
+
f"{OBJECTIVES_MANIFEST_PATH}: pyyaml is required to validate objectives "
|
|
1093
|
+
"(pip install pyyaml)"
|
|
1094
|
+
]
|
|
1095
|
+
except yaml.YAMLError as exc:
|
|
1096
|
+
return [f"{OBJECTIVES_MANIFEST_PATH}: invalid YAML — {exc}"]
|
|
1097
|
+
if not isinstance(data, dict):
|
|
1098
|
+
return [f"{OBJECTIVES_MANIFEST_PATH}: top-level must be a mapping"]
|
|
1099
|
+
objectives = data.get("objectives")
|
|
1100
|
+
if not isinstance(objectives, list):
|
|
1101
|
+
return [f"{OBJECTIVES_MANIFEST_PATH}: 'objectives' must be a list"]
|
|
1102
|
+
|
|
1103
|
+
route_ids = _route_ids(repo_root)
|
|
1104
|
+
declared = _declared_refs(trailers)
|
|
1105
|
+
errors: list[str] = []
|
|
1106
|
+
for idx, obj in enumerate(objectives):
|
|
1107
|
+
loc = f"{OBJECTIVES_MANIFEST_PATH} objectives[{idx}]"
|
|
1108
|
+
if not isinstance(obj, dict):
|
|
1109
|
+
errors.append(f"{loc}: expected a mapping")
|
|
1110
|
+
continue
|
|
1111
|
+
oid = obj.get("id")
|
|
1112
|
+
if not isinstance(oid, str) or not OBJECTIVE_ID_RE.match(oid):
|
|
1113
|
+
errors.append(
|
|
1114
|
+
f"{loc}.id: expected 'cycle<N>#ec<k>' or 'issue<N>#ac<k>', got {oid!r}"
|
|
1115
|
+
)
|
|
1116
|
+
source = obj.get("source")
|
|
1117
|
+
if not isinstance(source, dict):
|
|
1118
|
+
errors.append(f"{loc}.source: expected a mapping, got {source!r}")
|
|
1119
|
+
continue
|
|
1120
|
+
kind, ref = source.get("kind"), source.get("ref")
|
|
1121
|
+
# Normalize refs to match _declared_refs: cycle ids go through
|
|
1122
|
+
# normalize_cycle_id (handles dotted ids and "Cycle N.N" prefixes);
|
|
1123
|
+
# issue refs strip a leading "#" so "1234" and "#1234" compare equal.
|
|
1124
|
+
if kind == "cycle" and isinstance(ref, str):
|
|
1125
|
+
normalized_ref = normalize_cycle_id(ref)
|
|
1126
|
+
elif kind == "issue" and isinstance(ref, str):
|
|
1127
|
+
normalized_ref = re.sub(r'^#', '', ref)
|
|
1128
|
+
else:
|
|
1129
|
+
normalized_ref = ref
|
|
1130
|
+
if f"{kind}:{normalized_ref}" not in declared:
|
|
1131
|
+
errors.append(
|
|
1132
|
+
f"{loc}.source: {kind} {ref!r} is not linked by any Cycle:/Closes #N trailer on this PR"
|
|
1133
|
+
)
|
|
1134
|
+
for j, binding in enumerate(obj.get("attestedBy") or []):
|
|
1135
|
+
if isinstance(binding, dict) and binding.get("kind") == "route":
|
|
1136
|
+
rid = binding.get("routeId")
|
|
1137
|
+
if rid not in route_ids:
|
|
1138
|
+
errors.append(
|
|
1139
|
+
f"{loc}.attestedBy[{j}].routeId: {rid!r} is not a verificationRoute in .agent-review/config.json"
|
|
1140
|
+
)
|
|
1141
|
+
return errors
|
|
1142
|
+
|
|
1143
|
+
|
|
1039
1144
|
def validate(
|
|
1040
1145
|
title: str,
|
|
1041
1146
|
body: str,
|
|
@@ -1057,6 +1162,7 @@ def validate(
|
|
|
1057
1162
|
return errors
|
|
1058
1163
|
|
|
1059
1164
|
trailers = parse_trailers(body)
|
|
1165
|
+
errors.extend(validate_objectives(REPO_ROOT, trailers))
|
|
1060
1166
|
plan = is_plan_pr(title, labels_list, changed_files)
|
|
1061
1167
|
pr_type = "plan-pr" if plan else "code-pr"
|
|
1062
1168
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momentiq/dark-factory-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Dark Factory OSS CLI — multi-vendor adversarial critic orchestration (Cursor, Codex, Gemini, Grok) with min-complete-quorum aggregation and trusted-surface rebind",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://momentiq.ai/dark-factory",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"@google/genai": "^2.2.0",
|
|
99
99
|
"@iarna/toml": "2.2.5",
|
|
100
100
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
101
|
-
"@momentiq/dark-factory-schemas": "0.
|
|
101
|
+
"@momentiq/dark-factory-schemas": "0.9.0",
|
|
102
102
|
"@openai/codex-sdk": "^0.130.0",
|
|
103
103
|
"@yarnpkg/lockfile": "1.1.0",
|
|
104
104
|
"ajv": "^8.20.0",
|