@momentiq/dark-factory-cli 3.0.0 → 3.0.2
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/README.md
CHANGED
|
@@ -42,8 +42,8 @@ relevant) as `df` subcommands:
|
|
|
42
42
|
|
|
43
43
|
The package also ships five reusable GitHub Actions workflows
|
|
44
44
|
(`.github/workflows/*.yml`) that consumers wire up via `uses:`. See the
|
|
45
|
-
[root README](
|
|
46
|
-
pattern.
|
|
45
|
+
[root README](https://github.com/momentiq-ai/dark-factory/blob/main/README.md#reusable-workflows)
|
|
46
|
+
for the consumer wiring pattern.
|
|
47
47
|
|
|
48
48
|
## Status
|
|
49
49
|
|
|
@@ -163,22 +163,19 @@ used when:
|
|
|
163
163
|
hooks locally.
|
|
164
164
|
- A hook bypass landed and the CI gate needs to re-evaluate.
|
|
165
165
|
- The developer hasn't logged in to a vendor CLI yet
|
|
166
|
-
(`cursor login` / `codex login` / Claude desktop OAuth).
|
|
166
|
+
(`cursor-agent login` / `codex login` / Claude desktop OAuth).
|
|
167
167
|
|
|
168
168
|
### `.agent-review/config.json` — the profile that pins subscription auth
|
|
169
169
|
|
|
170
170
|
```json
|
|
171
171
|
{
|
|
172
172
|
"version": 2,
|
|
173
|
-
"critics": [
|
|
174
|
-
{ "id": "cursor-local-chief-engineer", "adapter": "cursor-sdk", ... },
|
|
175
|
-
{ "id": "codex-local-chief-engineer", "adapter": "codex-sdk", ... }
|
|
176
|
-
],
|
|
177
173
|
"profiles": {
|
|
178
174
|
"local": {
|
|
179
|
-
"criticIds": ["cursor-
|
|
180
|
-
"quorum":
|
|
175
|
+
"criticIds": ["cursor-cli-chief-engineer", "codex-local-chief-engineer"],
|
|
176
|
+
"quorum": 2,
|
|
181
177
|
"auth": {
|
|
178
|
+
"cursor-cli-chief-engineer": "chatgpt",
|
|
182
179
|
"codex-local-chief-engineer": "chatgpt"
|
|
183
180
|
}
|
|
184
181
|
},
|
|
@@ -193,10 +190,22 @@ used when:
|
|
|
193
190
|
}
|
|
194
191
|
```
|
|
195
192
|
|
|
196
|
-
The
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
The block shows the **`profiles`** map — the local/cloud profile wiring. The
|
|
194
|
+
ids it references (e.g. `cursor-cli-chief-engineer`) are critics defined in the
|
|
195
|
+
config's top-level `critics` array — each with `id`, `name`, `adapter`,
|
|
196
|
+
`required`, `runtime`, and `model` — omitted here to keep the focus on the auth
|
|
197
|
+
wiring. Step 2 of the setup checklist below points to the complete, schema-valid
|
|
198
|
+
file.
|
|
199
|
+
|
|
200
|
+
The `local` profile wires Cursor to the **`cursor-cli`** subscription
|
|
201
|
+
adapter (`cursor-agent login`, no API key) and pins both
|
|
202
|
+
`cursor-cli-chief-engineer` and `codex-local-chief-engineer` to
|
|
203
|
+
`"chatgpt"` — each vendor CLI uses its logged-in subscription session
|
|
204
|
+
(`~/.codex/auth.json` for Codex) and does **NOT** fall back to
|
|
205
|
+
`CURSOR_API_KEY` / `CODEX_API_KEY` even if set in env. This is the
|
|
206
|
+
firewall against accidental API-token billing. The `cloud` profile
|
|
207
|
+
deliberately keeps the API-key `cursor-sdk` critic — CI runners have no
|
|
208
|
+
subscription login.
|
|
200
209
|
|
|
201
210
|
`df doctor --profile local` validates the configured subscription source
|
|
202
211
|
is reachable. Run it after first-time setup.
|
|
@@ -253,10 +262,19 @@ API — see `packages/cli/src/doppler-bootstrap.ts` for the
|
|
|
253
262
|
### First-time setup checklist
|
|
254
263
|
|
|
255
264
|
1. `npm install --ignore-scripts @momentiq/dark-factory-cli`
|
|
256
|
-
2.
|
|
265
|
+
2. Create `.agent-review/config.json`: copy the canonical
|
|
266
|
+
[`.agent-review/config.json`](https://github.com/momentiq-ai/dark-factory/blob/main/.agent-review/config.json)
|
|
267
|
+
from the `momentiq-ai/dark-factory` repo and adjust it to your source layout
|
|
268
|
+
(see the [consumer-adoption guide](https://github.com/momentiq-ai/dark-factory/blob/main/docs/CONSUMER-ADOPTION.md)
|
|
269
|
+
§6). To avoid drift, swap `blob/main` in those links for the tag matching
|
|
270
|
+
your installed CLI version (e.g. `blob/dark-factory-cli-v3.0.1`); `df doctor`
|
|
271
|
+
flags a mismatch. No API key needed — it pins the `local`
|
|
272
|
+
profile to your Cursor / Codex subscription logins. (Optional: with an
|
|
273
|
+
`ANTHROPIC_API_KEY` set, `df onboard --apply --profile local` generates a
|
|
274
|
+
repo-tailored config for you.)
|
|
257
275
|
3. Add `.husky/post-commit` + `.husky/pre-push` (samples above).
|
|
258
276
|
4. `git config --local core.hooksPath .husky`
|
|
259
|
-
5. `cursor login` / `codex login` (or Claude desktop OAuth) on the workstation.
|
|
277
|
+
5. `cursor-agent login` / `codex login` (or Claude desktop OAuth) on the workstation.
|
|
260
278
|
6. Run `df doctor --profile local` — should report all OK.
|
|
261
279
|
7. Commit something — observe `.git/agent-reviews/<sha>.json` arrives.
|
|
262
280
|
8. (Optional) Set up CI with `CURSOR_API_KEY` / `CODEX_API_KEY` /
|
|
@@ -59,7 +59,11 @@ Environment:
|
|
|
59
59
|
|
|
60
60
|
Outputs:
|
|
61
61
|
|
|
62
|
-
exit 0 — spec == live, no divergence
|
|
62
|
+
exit 0 — spec == live, no divergence. May still print advisory
|
|
63
|
+
``[branch-protection-audit] WARNING: ...`` lines above the OK
|
|
64
|
+
banner — e.g. a required EXTERNAL reusable-workflow context whose
|
|
65
|
+
inner job name cannot be resolved offline is skipped-with-warning
|
|
66
|
+
(issue #341). Warnings never change the exit code;
|
|
63
67
|
exit 1 — divergence (structured field-by-field report on stdout);
|
|
64
68
|
exit 2 — usage / configuration error (e.g., spec.yaml missing).
|
|
65
69
|
"""
|
|
@@ -124,20 +128,33 @@ class Divergence:
|
|
|
124
128
|
@dataclass
|
|
125
129
|
class AuditReport:
|
|
126
130
|
divergences: list[Divergence] = field(default_factory=list)
|
|
131
|
+
warnings: list[str] = field(default_factory=list)
|
|
127
132
|
|
|
128
133
|
@property
|
|
129
134
|
def ok(self) -> bool:
|
|
135
|
+
# Warnings are advisory (e.g. an unverifiable external reusable-workflow
|
|
136
|
+
# context, issue #341); only divergences fail the audit.
|
|
130
137
|
return not self.divergences
|
|
131
138
|
|
|
132
139
|
def add(self, *args: Any, **kwargs: Any) -> None:
|
|
133
140
|
self.divergences.append(Divergence(*args, **kwargs))
|
|
134
141
|
|
|
142
|
+
def warn(self, message: str) -> None:
|
|
143
|
+
self.warnings.append(message)
|
|
144
|
+
|
|
135
145
|
def render(self) -> str:
|
|
146
|
+
warning_block = "".join(
|
|
147
|
+
f"[branch-protection-audit] WARNING: {w}\n" for w in self.warnings
|
|
148
|
+
)
|
|
136
149
|
if self.ok:
|
|
137
|
-
return
|
|
150
|
+
return (
|
|
151
|
+
warning_block
|
|
152
|
+
+ "[branch-protection-audit] OK: spec.yaml matches live ruleset.\n"
|
|
153
|
+
)
|
|
138
154
|
body = "\n".join(d.render() for d in self.divergences)
|
|
139
155
|
return (
|
|
140
|
-
|
|
156
|
+
warning_block
|
|
157
|
+
+ "[branch-protection-audit] FAIL: spec.yaml vs live ruleset divergence:\n"
|
|
141
158
|
f"{body}\n"
|
|
142
159
|
)
|
|
143
160
|
|
|
@@ -152,37 +169,126 @@ def load_spec(path: Path) -> dict[str, Any]:
|
|
|
152
169
|
return data
|
|
153
170
|
|
|
154
171
|
|
|
155
|
-
|
|
172
|
+
@dataclass
|
|
173
|
+
class EmittedContexts:
|
|
174
|
+
"""Status-check contexts a workflow directory statically emits.
|
|
175
|
+
|
|
176
|
+
``static`` — contexts resolvable offline: each job's ID, its static
|
|
177
|
+
``name:``, and — for a job that calls a LOCAL reusable workflow
|
|
178
|
+
(``uses: ./.github/workflows/<wf>.yml``) — the compound context
|
|
179
|
+
``<caller-job-id> / <callee-job-name>`` for every top-level job of that
|
|
180
|
+
reusable workflow (GitHub emits one status check per inner job, prefixed
|
|
181
|
+
by the CALLER JOB ID; CONSUMER-ADOPTION.md §8).
|
|
182
|
+
|
|
183
|
+
``unresolvable_external_callers`` — maps a reusable-caller job's ID to its
|
|
184
|
+
``uses:`` ref, for callers that invoke an EXTERNAL reusable workflow
|
|
185
|
+
(``<owner>/<repo>/.github/workflows/..@<ref>``). Their inner job names live
|
|
186
|
+
in another repo and cannot be resolved offline, so a required compound
|
|
187
|
+
context ``<caller-job-id> / <x>`` is UNVERIFIABLE here — skipped with a
|
|
188
|
+
warning rather than failed (issue #341).
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
static: set[str] = field(default_factory=set)
|
|
192
|
+
unresolvable_external_callers: dict[str, str] = field(default_factory=dict)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _job_display_label(job_id: str, job: dict[str, Any]) -> str:
|
|
196
|
+
"""A job's display name: its static ``name:`` when set (and free of
|
|
197
|
+
unresolved ``${{ }}`` expressions), else the job ID. For a reusable
|
|
198
|
+
workflow's inner job this is the ``<callee-job-name>`` segment of the
|
|
199
|
+
emitted check (it defaults to the job id when no ``name:`` is set). The
|
|
200
|
+
CALLER segment of a reusable context is always the caller JOB ID, not this
|
|
201
|
+
label (CONSUMER-ADOPTION.md §8)."""
|
|
202
|
+
name = job.get("name")
|
|
203
|
+
if isinstance(name, str) and name and "${{" not in name:
|
|
204
|
+
return name
|
|
205
|
+
return job_id
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _read_workflow_jobs(path: Path) -> dict[str, Any] | None:
|
|
209
|
+
"""Parse a workflow file and return its ``jobs`` mapping, or ``None`` if the
|
|
210
|
+
file is missing, unreadable, malformed, or declares no jobs mapping."""
|
|
211
|
+
try:
|
|
212
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
213
|
+
except (OSError, yaml.YAMLError):
|
|
214
|
+
return None
|
|
215
|
+
if not isinstance(data, dict):
|
|
216
|
+
return None
|
|
217
|
+
jobs = data.get("jobs")
|
|
218
|
+
return jobs if isinstance(jobs, dict) else None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def extract_workflow_emitted_contexts(workflows_dir: Path) -> EmittedContexts:
|
|
156
222
|
"""Collect status-check contexts that workflow files statically emit.
|
|
157
223
|
|
|
158
|
-
A
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
224
|
+
A normal job emits a context equal to its ``name:`` if declared, else its
|
|
225
|
+
job ID. A job that calls a reusable workflow (a job-level ``uses:``) emits
|
|
226
|
+
one status check per inner job of that reusable workflow, named
|
|
227
|
+
``<caller-job-id> / <callee-job-name>`` — the caller segment is the job id,
|
|
228
|
+
not a caller ``name:`` override (CONSUMER-ADOPTION.md §8):
|
|
229
|
+
|
|
230
|
+
* LOCAL reusable (``uses: ./.github/workflows/<wf>.yml``) — reusable
|
|
231
|
+
workflows must live in ``.github/workflows/``, i.e. the directory being
|
|
232
|
+
scanned, so the referenced file is read by basename and every top-level
|
|
233
|
+
inner job's compound context is added to ``static`` (full offline
|
|
234
|
+
verification; a bogus ``<caller-job-id> / <x>`` still fails).
|
|
235
|
+
* EXTERNAL reusable (``<owner>/<repo>/.github/workflows/..@<ref>``) — the
|
|
236
|
+
inner jobs live in another repo, so the caller job id is recorded in
|
|
237
|
+
``unresolvable_external_callers`` and its compound contexts are
|
|
238
|
+
skipped-with-warning instead of failed-closed (issue #341).
|
|
239
|
+
|
|
240
|
+
Matrix job names are still **not** expanded (they require runtime
|
|
241
|
+
evaluation); a required matrix-expanded context still surfaces as a
|
|
242
|
+
divergence.
|
|
163
243
|
"""
|
|
164
|
-
|
|
244
|
+
result = EmittedContexts()
|
|
165
245
|
if not workflows_dir.exists():
|
|
166
|
-
return
|
|
246
|
+
return result
|
|
167
247
|
for ext in ("*.yml", "*.yaml"):
|
|
168
248
|
for path in workflows_dir.glob(ext):
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
except (OSError, yaml.YAMLError):
|
|
172
|
-
continue
|
|
173
|
-
if not isinstance(data, dict):
|
|
174
|
-
continue
|
|
175
|
-
jobs = data.get("jobs") or {}
|
|
176
|
-
if not isinstance(jobs, dict):
|
|
249
|
+
jobs = _read_workflow_jobs(path)
|
|
250
|
+
if jobs is None:
|
|
177
251
|
continue
|
|
178
252
|
for job_id, job in jobs.items():
|
|
179
253
|
if not isinstance(job, dict):
|
|
180
254
|
continue
|
|
181
|
-
|
|
255
|
+
result.static.add(job_id)
|
|
182
256
|
job_name = job.get("name")
|
|
183
257
|
if isinstance(job_name, str) and job_name:
|
|
184
|
-
|
|
185
|
-
|
|
258
|
+
result.static.add(job_name)
|
|
259
|
+
|
|
260
|
+
uses = job.get("uses")
|
|
261
|
+
if not isinstance(uses, str) or not uses.strip():
|
|
262
|
+
continue # normal job — bare job_id/name already recorded.
|
|
263
|
+
|
|
264
|
+
# GitHub prefixes a reusable workflow's emitted checks with the
|
|
265
|
+
# CALLER JOB ID — never the caller's `name:` display override:
|
|
266
|
+
# `<caller-job-id> / <callee-job-name>` (CONSUMER-ADOPTION.md §8;
|
|
267
|
+
# a caller `name:` broke this contract in issue #27).
|
|
268
|
+
if uses.startswith("./"):
|
|
269
|
+
# LOCAL reusable workflow. Resolve by basename within the
|
|
270
|
+
# workflows dir (reusable workflows must live there) and add
|
|
271
|
+
# every inner job's compound context to `static`.
|
|
272
|
+
inner_path = workflows_dir / Path(uses.split("@", 1)[0]).name
|
|
273
|
+
inner_jobs = _read_workflow_jobs(inner_path)
|
|
274
|
+
if inner_jobs is None:
|
|
275
|
+
# Genuinely unresolvable local ref (missing/broken
|
|
276
|
+
# file). Add nothing so a required compound context
|
|
277
|
+
# falls through to the normal fail path, preserving the
|
|
278
|
+
# #168/#175 never-goes-green guard.
|
|
279
|
+
continue
|
|
280
|
+
for inner_id, inner_job in inner_jobs.items():
|
|
281
|
+
if not isinstance(inner_job, dict):
|
|
282
|
+
continue
|
|
283
|
+
callee_name = _job_display_label(inner_id, inner_job)
|
|
284
|
+
result.static.add(f"{job_id} / {callee_name}")
|
|
285
|
+
else:
|
|
286
|
+
# EXTERNAL reusable workflow — inner jobs are in another
|
|
287
|
+
# repo, unresolvable offline. Key on the caller JOB ID (the
|
|
288
|
+
# documented context prefix) so
|
|
289
|
+
# compare_emitted_status_checks can skip-with-warning.
|
|
290
|
+
result.unresolvable_external_callers[job_id] = uses
|
|
291
|
+
return result
|
|
186
292
|
|
|
187
293
|
|
|
188
294
|
def compare_emitted_status_checks(
|
|
@@ -200,9 +306,28 @@ def compare_emitted_status_checks(
|
|
|
200
306
|
return
|
|
201
307
|
emitted = extract_workflow_emitted_contexts(workflows_dir)
|
|
202
308
|
for context in required_contexts:
|
|
203
|
-
if context in emitted:
|
|
309
|
+
if context in emitted.static:
|
|
204
310
|
continue
|
|
205
|
-
|
|
311
|
+
# Reusable-workflow caller (issue #341): a compound context
|
|
312
|
+
# ``<caller> / <inner>`` whose ``<caller>`` invokes an EXTERNAL reusable
|
|
313
|
+
# workflow is unverifiable offline — skip WITH A WARNING, do not fail.
|
|
314
|
+
# LOCAL reusable callers are already fully enumerated in `static`, and a
|
|
315
|
+
# ``<caller>`` that is a normal/nonexistent job is NOT in this set, so
|
|
316
|
+
# both still fall through to the fail path below (non-weakening).
|
|
317
|
+
caller = context.partition(" / ")[0] if " / " in context else None
|
|
318
|
+
if caller and caller in emitted.unresolvable_external_callers:
|
|
319
|
+
uses_ref = emitted.unresolvable_external_callers[caller]
|
|
320
|
+
report.warn(
|
|
321
|
+
f"Required context {context!r} is emitted by reusable-workflow "
|
|
322
|
+
f"caller job {caller!r} (uses: {uses_ref}), whose inner job name "
|
|
323
|
+
f"lives in an external repo and cannot be resolved offline. "
|
|
324
|
+
f"Emission is UNVERIFIED here — skipped, not failed (issue #341). "
|
|
325
|
+
f"Confirm that reusable workflow emits this check at runtime."
|
|
326
|
+
)
|
|
327
|
+
continue
|
|
328
|
+
closest = difflib.get_close_matches(
|
|
329
|
+
context, sorted(emitted.static), n=1, cutoff=0.5
|
|
330
|
+
)
|
|
206
331
|
closest_name = closest[0] if closest else None
|
|
207
332
|
report.add(
|
|
208
333
|
field="required_status_checks.contexts.emitted",
|
|
@@ -211,8 +336,9 @@ def compare_emitted_status_checks(
|
|
|
211
336
|
detail=(
|
|
212
337
|
f"Required context {context!r} is not emitted by any job ID or "
|
|
213
338
|
f"job name in {workflows_dir / '*.yml'} or {workflows_dir / '*.yaml'}. "
|
|
214
|
-
f"Matrix-expanded names
|
|
215
|
-
f"
|
|
339
|
+
f"Matrix-expanded names are not statically checked; reusable-workflow "
|
|
340
|
+
f"caller contexts are verified against LOCAL reusable workflows and "
|
|
341
|
+
f"skipped-with-warning only for EXTERNAL ones."
|
|
216
342
|
),
|
|
217
343
|
)
|
|
218
344
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momentiq/dark-factory-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
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",
|
|
@@ -27,18 +27,30 @@ unassigned; it prints the Issue URL. Link the relevant PR or issue with
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
df handoff --link pr:<PR#> <<'EOF'
|
|
30
|
-
<the composed, scrubbed note
|
|
30
|
+
<the composed, scrubbed note from "Dual-register" below —
|
|
31
|
+
INCLUDING its <!-- agent-context:v1 --> … <!-- /agent-context:v1 --> markers>
|
|
31
32
|
EOF
|
|
32
33
|
```
|
|
33
34
|
|
|
35
|
+
The note **must** be wrapped in the `<!-- agent-context:v1 -->` …
|
|
36
|
+
`<!-- /agent-context:v1 -->` markers (they anchor the Issue-body upsert);
|
|
37
|
+
`df handoff` refuses a note that is missing them.
|
|
38
|
+
|
|
34
39
|
## Dual-register: two audiences, one artifact
|
|
35
40
|
|
|
36
41
|
Write the note for the **engineer**, and speak a separate plain sentence to the
|
|
37
42
|
**operator**.
|
|
38
43
|
|
|
39
|
-
Engineer-facing note (structured, precise):
|
|
44
|
+
Engineer-facing note (structured, precise). Keep the `agent-context:v1` markers
|
|
45
|
+
around the whole note — copy this verbatim into the heredoc above:
|
|
40
46
|
|
|
41
47
|
```markdown
|
|
48
|
+
<!-- agent-context:v1 -->
|
|
49
|
+
> 🤖 **Handoff context** — transient working memory, not a source of truth.
|
|
50
|
+
> _Updated: <YYYY-MM-DD> by <your session>_
|
|
51
|
+
|
|
52
|
+
**Work-stream:** `<the ask, in one line>` · the handoff Issue's title.
|
|
53
|
+
|
|
42
54
|
## Intent
|
|
43
55
|
What the operator asked for, in their words.
|
|
44
56
|
|
|
@@ -48,6 +60,7 @@ What is already built and where (branch or PR), and what is verified visually.
|
|
|
48
60
|
## Blocked — why
|
|
49
61
|
The specific boundary: which scope, protected path, dependency, or gate — and
|
|
50
62
|
what an engineer needs to decide or do next.
|
|
63
|
+
<!-- /agent-context:v1 -->
|
|
51
64
|
```
|
|
52
65
|
|
|
53
66
|
Operator-facing sentence (plain, framed as progress):
|
|
@@ -18,12 +18,14 @@ The brief's **Done-when checkboxes are the acceptance criteria.** Before
|
|
|
18
18
|
shipping, confirm each is satisfied and backed by the before/after screenshots
|
|
19
19
|
from `/designer-build` — a Done-when item with no visible proof is not done.
|
|
20
20
|
|
|
21
|
-
Make "done" proof-bound, not free text.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
Make "done" proof-bound, not free text. Derive an objectives contract straight
|
|
22
|
+
from the brief with **`df objectives derive --brief <brief-path> --apply`** — it
|
|
23
|
+
writes one objective per `## Done-when` checkbox to `.darkfactory/objectives.yaml`,
|
|
24
|
+
each text-hash-bound to its source line (id form `brief-<slug>#ac<k>`). Verify the
|
|
25
|
+
bindings and read the closeout proof with `df prove` (per-objective
|
|
26
|
+
proven/pending/failed); see the `/objectives` skill for authoring and binding
|
|
27
|
+
mechanics. Still confirm each Done-when item **visually** — the before/after
|
|
28
|
+
screenshots are the evidence behind the proof.
|
|
27
29
|
|
|
28
30
|
## 2. Run the gates locally first
|
|
29
31
|
|
|
@@ -42,14 +44,15 @@ Mode: designer
|
|
|
42
44
|
Closes #<N>
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
The `Mode: designer` trailer
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`Closes #<N>` links
|
|
47
|
+
The `Mode: designer` trailer is what the **deterministic mode-scope critic**
|
|
48
|
+
keys on: it reads the trailer, loads the catalog, and validates the diff against
|
|
49
|
+
the mode's `writeScopes` / `protectedPaths` / `diffBudget` / `dependencyPolicy`,
|
|
50
|
+
blocking an out-of-scope diff at merge time — even from a jailbroken local
|
|
51
|
+
session, because CI re-derives scope from the *committed* trailer + catalog
|
|
52
|
+
independently of whatever happened locally. Scope is *also* enforced live
|
|
53
|
+
in-session by the PreToolUse guard, and the change still faces the full
|
|
54
|
+
multi-vendor critic quorum + merge queue like any other PR. `Closes #<N>` links
|
|
55
|
+
the tracking issue.
|
|
53
56
|
|
|
54
57
|
Open the PR and embed the **before/after screenshots** in the body so the change
|
|
55
58
|
is visible to reviewers and the operator:
|