@momentiq/dark-factory-cli 2.18.0 → 3.0.1

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.
Files changed (119) hide show
  1. package/README.md +33 -15
  2. package/dist/adapters/cursor-sdk.d.ts.map +1 -1
  3. package/dist/adapters/cursor-sdk.js +14 -2
  4. package/dist/adapters/cursor-sdk.js.map +1 -1
  5. package/dist/adapters/index.d.ts +1 -0
  6. package/dist/adapters/index.d.ts.map +1 -1
  7. package/dist/adapters/index.js +6 -0
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/adapters/mode-scope.d.ts +86 -0
  10. package/dist/adapters/mode-scope.d.ts.map +1 -0
  11. package/dist/adapters/mode-scope.js +456 -0
  12. package/dist/adapters/mode-scope.js.map +1 -0
  13. package/dist/branch-protection/audit_branch_protection.py +152 -26
  14. package/dist/cli.d.ts +5 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +17 -1
  17. package/dist/cli.js.map +1 -1
  18. package/dist/commands/mode.d.ts.map +1 -1
  19. package/dist/commands/mode.js +135 -10
  20. package/dist/commands/mode.js.map +1 -1
  21. package/dist/commands/objectives.d.ts +27 -3
  22. package/dist/commands/objectives.d.ts.map +1 -1
  23. package/dist/commands/objectives.js +347 -53
  24. package/dist/commands/objectives.js.map +1 -1
  25. package/dist/cycle-doc-validator/validate_cycle_doc.py +128 -11
  26. package/dist/doctor.d.ts.map +1 -1
  27. package/dist/doctor.js +8 -4
  28. package/dist/doctor.js.map +1 -1
  29. package/dist/evidence/index.d.ts +1 -0
  30. package/dist/evidence/index.d.ts.map +1 -1
  31. package/dist/evidence/index.js +5 -0
  32. package/dist/evidence/index.js.map +1 -1
  33. package/dist/evidence/ui-visual/embed.d.ts +65 -0
  34. package/dist/evidence/ui-visual/embed.d.ts.map +1 -0
  35. package/dist/evidence/ui-visual/embed.js +176 -0
  36. package/dist/evidence/ui-visual/embed.js.map +1 -0
  37. package/dist/evidence/ui-visual/index.d.ts +2 -0
  38. package/dist/evidence/ui-visual/index.d.ts.map +1 -0
  39. package/dist/evidence/ui-visual/index.js +6 -0
  40. package/dist/evidence/ui-visual/index.js.map +1 -0
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/mcp/cycle-doc/parser.d.ts +3 -3
  46. package/dist/mcp/cycle-doc/parser.d.ts.map +1 -1
  47. package/dist/mcp/cycle-doc/parser.js +2 -2
  48. package/dist/mcp/cycle-doc/parser.js.map +1 -1
  49. package/dist/mcp/tools/doctor.d.ts +5 -0
  50. package/dist/mcp/tools/doctor.d.ts.map +1 -1
  51. package/dist/mcp/tools/doctor.js +10 -1
  52. package/dist/mcp/tools/doctor.js.map +1 -1
  53. package/dist/mcp/tools/review-bypass.d.ts +5 -0
  54. package/dist/mcp/tools/review-bypass.d.ts.map +1 -1
  55. package/dist/mcp/tools/review-bypass.js +9 -1
  56. package/dist/mcp/tools/review-bypass.js.map +1 -1
  57. package/dist/mode/apply.d.ts +8 -0
  58. package/dist/mode/apply.d.ts.map +1 -0
  59. package/dist/mode/apply.js +118 -0
  60. package/dist/mode/apply.js.map +1 -0
  61. package/dist/mode/context.d.ts +50 -0
  62. package/dist/mode/context.d.ts.map +1 -0
  63. package/dist/mode/context.js +127 -0
  64. package/dist/mode/context.js.map +1 -0
  65. package/dist/mode/critic-registration.d.ts +66 -0
  66. package/dist/mode/critic-registration.d.ts.map +1 -0
  67. package/dist/mode/critic-registration.js +279 -0
  68. package/dist/mode/critic-registration.js.map +1 -0
  69. package/dist/mode/guard.d.ts +15 -0
  70. package/dist/mode/guard.d.ts.map +1 -0
  71. package/dist/mode/guard.js +160 -0
  72. package/dist/mode/guard.js.map +1 -0
  73. package/dist/mode/init.d.ts.map +1 -1
  74. package/dist/mode/init.js +14 -5
  75. package/dist/mode/init.js.map +1 -1
  76. package/dist/mode/loadConfig.d.ts +34 -0
  77. package/dist/mode/loadConfig.d.ts.map +1 -0
  78. package/dist/mode/loadConfig.js +38 -0
  79. package/dist/mode/loadConfig.js.map +1 -0
  80. package/dist/mode/prompts/designer-baseline.md +47 -0
  81. package/dist/mode/resolve.d.ts +1 -1
  82. package/dist/mode/resolve.d.ts.map +1 -1
  83. package/dist/mode/resolve.js +19 -3
  84. package/dist/mode/resolve.js.map +1 -1
  85. package/dist/mode/scope-policy.d.ts +41 -0
  86. package/dist/mode/scope-policy.d.ts.map +1 -0
  87. package/dist/mode/scope-policy.js +97 -0
  88. package/dist/mode/scope-policy.js.map +1 -0
  89. package/dist/mode/status.d.ts.map +1 -1
  90. package/dist/mode/status.js +2 -9
  91. package/dist/mode/status.js.map +1 -1
  92. package/dist/onboard/validate.d.ts +24 -6
  93. package/dist/onboard/validate.d.ts.map +1 -1
  94. package/dist/onboard/validate.js +87 -14
  95. package/dist/onboard/validate.js.map +1 -1
  96. package/dist/skills/config.d.ts +2 -113
  97. package/dist/skills/config.d.ts.map +1 -1
  98. package/dist/skills/config.js +19 -62
  99. package/dist/skills/config.js.map +1 -1
  100. package/dist/skills/index.d.ts +1 -1
  101. package/dist/skills/index.d.ts.map +1 -1
  102. package/dist/skills/index.js +1 -1
  103. package/dist/skills/index.js.map +1 -1
  104. package/dist/skills/install.d.ts.map +1 -1
  105. package/dist/skills/install.js +7 -0
  106. package/dist/skills/install.js.map +1 -1
  107. package/package.json +2 -2
  108. package/skills/designer-brief/SKILL.md.tmpl +74 -0
  109. package/skills/designer-brief/skill.json +17 -0
  110. package/skills/designer-build/SKILL.md.tmpl +63 -0
  111. package/skills/designer-build/skill.json +17 -0
  112. package/skills/designer-handoff/SKILL.md.tmpl +64 -0
  113. package/skills/designer-handoff/skill.json +17 -0
  114. package/skills/designer-ship/SKILL.md.tmpl +77 -0
  115. package/skills/designer-ship/skill.json +17 -0
  116. package/skills/objectives/SKILL.md.tmpl +16 -1
  117. package/skills/verify/producer/ui-visual-surfaces.ts +261 -0
  118. package/skills/verify/producer/ui-visual.config.ts +22 -0
  119. package/skills/verify/producer/ui-visual.producer.spec.ts +137 -0
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: designer-brief
3
+ description: Use when a non-technical operator (designer, PM, or founder) in Designer Mode asks for a non-trivial UI change — capture their intent as a short brief (What, Why, Done-when, Out-of-bounds) at .darkfactory/briefs/ before building. Tiny, obvious single-shot asks skip the brief and go straight to /designer-build.
4
+ ---
5
+
6
+ # /designer-brief — capture intent before building
7
+
8
+ Designer Mode pairs a non-technical operator with an agent that owns every
9
+ technical decision. This skill is the **plan-time** step for {{REPO_NAME}}: it
10
+ turns a product ask into a one-glance brief the operator approves before you
11
+ write a line of code, so "done" has an agreed shape.
12
+
13
+ Everything below is **your** procedure. The operator never sees a brief
14
+ template, a file path, or this doctrine — they see one plain-language summary
15
+ and answer at most a question or two.
16
+
17
+ ## When to write a brief (and when to skip)
18
+
19
+ Write a brief when the ask is **non-trivial** — a new section, page, or flow, or
20
+ a change that spans several surfaces or has more than one "done" condition
21
+ ("add a testimonials section", "make onboarding feel calmer").
22
+
23
+ **Skip it** — the single-shot path — for tiny, unambiguous polish where the
24
+ change and its "done" are obvious in one look ("the pricing cards feel
25
+ cramped", "this heading is the wrong blue"). Go straight to `/designer-build`,
26
+ screenshot before/after, and confirm.
27
+
28
+ When unsure, prefer a brief — it is three sentences and a checklist, not a spec.
29
+
30
+ ## The brief
31
+
32
+ Write it to `.darkfactory/briefs/<date>-<slug>.md`, where `<date>` is today in
33
+ `YYYY-MM-DD` form and `<slug>` is a short kebab-case name for the ask
34
+ (`.darkfactory/briefs/2026-07-03-testimonials-section.md`). Four sections, all
35
+ in the operator's product language — no framework, file, or component names:
36
+
37
+ ```markdown
38
+ # <Title of the ask>
39
+
40
+ ## What
41
+ One sentence: the change, as the operator would describe it.
42
+
43
+ ## Why
44
+ The outcome they want — what a visitor or user feels or can now do.
45
+
46
+ ## Done-when
47
+ - [ ] An observable, checkable acceptance item in plain language.
48
+ - [ ] Another one. Each is something you can *show* on screen.
49
+ - [ ] …
50
+
51
+ ## Out-of-bounds
52
+ - What this deliberately does NOT touch (keeps scope honest and points at handoffs).
53
+ ```
54
+
55
+ The **Done-when checkboxes are the acceptance contract.** Write each as
56
+ something you can demonstrate with a screenshot or a visible behavior — they
57
+ become what `/designer-ship` verifies and what closeout proves. Vague items
58
+ ("looks nicer") are not acceptance items; make them observable ("the
59
+ testimonials read comfortably on a phone without pinching").
60
+
61
+ ## Filling gaps — ask product questions only
62
+
63
+ If the ask is underspecified, ask **only product or visual questions**, one at a
64
+ time, multiple-choice with 2–3 concrete options plus "you decide"
65
+ ("Should testimonials show a photo, or just a name and quote?"). Never ask about
66
+ frameworks, data, files, or state — decide those yourself from the repo's
67
+ existing conventions and `DESIGN.md`.
68
+
69
+ ## Approve in one glance
70
+
71
+ Show the operator the essence, not the file: *"Here's what I'll build — a
72
+ testimonials section with three quotes and photos, on the home page. It won't
73
+ touch sign-in or your data. Look right?"* On yes, hand off to `/designer-build`.
74
+ Keep the brief file updated if the ask shifts mid-build.
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "../skill-schema.json",
3
+ "name": "designer-brief",
4
+ "version": "1.0.0",
5
+ "summary": "Designer Mode: turn a non-technical operator's intent into a short brief (What/Why/Done-when/Out-of-bounds) at .darkfactory/briefs/ before building; tiny obvious asks skip straight to /designer-build.",
6
+ "originatingRepo": "momentiq-ai/dark-factory",
7
+ "files": [
8
+ { "template": "SKILL.md.tmpl", "target": "SKILL.md" }
9
+ ],
10
+ "variables": {
11
+ "REPO_NAME": {
12
+ "description": "Display name of the consumer repo (e.g. 'Taxpilot').",
13
+ "source": "darkfactory.yaml: repo.displayName",
14
+ "default": "this repo"
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: designer-build
3
+ description: Use when implementing a Designer Mode change within the mode's write scopes — reuse the repo's design system, run the dev server, and capture before/after screenshots each visual iteration until the operator says it looks right. Follows /designer-brief (or a tiny single-shot ask), precedes /designer-ship.
4
+ ---
5
+
6
+ # /designer-build — implement within scope, verify visually
7
+
8
+ This is the **build loop** of Designer Mode for {{REPO_NAME}}. You implement the
9
+ change, prove it visually, and iterate on the operator's product feedback —
10
+ never on code they have to read.
11
+
12
+ The operator sees screenshots and plain sentences. They never see diffs, file
13
+ trees, terminals, or component names.
14
+
15
+ ## Stay in scope
16
+
17
+ Work only inside the mode's `writeScopes`. The PreToolUse guard
18
+ (`df mode guard`) enforces the scopes and `protectedPaths` at the tool level —
19
+ if a call is denied, **do not retry or route around it.** A denial means the ask
20
+ needs an engineer: stop and use `/designer-handoff`. (`df mode status` confirms
21
+ the active mode and its editable areas.)
22
+
23
+ ## Reuse the design system — don't invent
24
+
25
+ `DESIGN.md` is the design source of truth (its tokens are injected into your
26
+ session context):
27
+
28
+ - Take every color, font size, and spacing value from `DESIGN.md` tokens. Do
29
+ not hand-pick hex values or one-off spacings.
30
+ - **Reuse existing components before creating anything new** — extend a
31
+ component, don't fork it. Match the patterns already in the codebase.
32
+ - Never remove accessibility affordances (focus rings, labels, alt text,
33
+ contrast). A change that would is out-of-bounds → `/designer-handoff`.
34
+
35
+ ## The visual loop (show, don't tell)
36
+
37
+ 1. **Baseline.** Start the dev server using the mode catalog's
38
+ `devServer.command` (e.g. `npm run dev`) at its configured URL. With the
39
+ Playwright MCP, screenshot the route(s) and state(s) you're about to change —
40
+ this is the **BEFORE**.
41
+ 2. **Change.** Implement one coherent step within scope.
42
+ 3. **AFTER.** Re-screenshot the same route(s) and state(s). Include the
43
+ responsive and empty/error states the Done-when items care about (e.g. a
44
+ phone width).
45
+ 4. **Show the operator** the before/after and describe the change in one
46
+ sentence: *"Here's the testimonials section — three quotes, easy to read on a
47
+ phone. Want the quotes bigger?"*
48
+ 5. **Iterate** on their product feedback ("make it warmer", "tighten the
49
+ spacing") — repeat steps 2–4 until they say it looks right. Keep each
50
+ iteration a small, screenshotted step.
51
+
52
+ Keep the final before/after screenshots — `/designer-ship` embeds them in the PR
53
+ so reviewers and the operator see the change, and the visual-evidence route
54
+ binds them to the shipped commit.
55
+
56
+ ## Translate, never dump
57
+
58
+ If something goes wrong (a build error, a failing check), say what it means in
59
+ one sentence and what you're doing about it — never paste the raw error. Decide
60
+ all implementation details yourself, following repo conventions; ask the
61
+ operator only product or visual questions, multiple-choice, one at a time.
62
+
63
+ When the operator says it looks right, hand off to `/designer-ship`.
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "../skill-schema.json",
3
+ "name": "designer-build",
4
+ "version": "1.0.0",
5
+ "summary": "Designer Mode: implement within the mode's write scopes, reuse the design system, and run a dev-server + Playwright before/after screenshot loop until the operator says it looks right.",
6
+ "originatingRepo": "momentiq-ai/dark-factory",
7
+ "files": [
8
+ { "template": "SKILL.md.tmpl", "target": "SKILL.md" }
9
+ ],
10
+ "variables": {
11
+ "REPO_NAME": {
12
+ "description": "Display name of the consumer repo (e.g. 'Taxpilot').",
13
+ "source": "darkfactory.yaml: repo.displayName",
14
+ "default": "this repo"
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: designer-handoff
3
+ description: Use when a Designer Mode ask needs an engineer — it is out-of-scope, touches a protected path, requires a dependency change, or a gate will not converge within the N=2 ceiling. Package a scrubbed, dual-register escalation with df handoff and tell the operator in one plain sentence, framed as progress.
4
+ ---
5
+
6
+ # /designer-handoff — escalate as progress, never as rejection
7
+
8
+ Some asks legitimately need an engineer. In Designer Mode for {{REPO_NAME}}, that
9
+ is a **first-class outcome**, not a failure — the product answer to a scope
10
+ boundary is a clean handoff that feels like momentum.
11
+
12
+ ## When to hand off
13
+
14
+ - A tool call was **denied by the mode guard** (outside `writeScopes`, or a
15
+ `protectedPaths` hit like auth, API, migrations, config, or CI). Do **not**
16
+ retry or work around a denial.
17
+ - The ask needs a **dependency change** under `dependencyPolicy: none`.
18
+ - A gate finding **won't converge within N=2** fix rounds, or the fix requires
19
+ work outside your scope.
20
+
21
+ ## Package the artifact with `df handoff`
22
+
23
+ `df handoff` is Issue-anchored: it reads the composed note on **stdin**, upserts
24
+ it as the dedicated handoff Issue's body, adds the `handoff` label, and leaves it
25
+ unassigned; it prints the Issue URL. Link the relevant PR or issue with
26
+ `--link`:
27
+
28
+ ```bash
29
+ df handoff --link pr:<PR#> <<'EOF'
30
+ <the composed, scrubbed note — see structure below>
31
+ EOF
32
+ ```
33
+
34
+ ## Dual-register: two audiences, one artifact
35
+
36
+ Write the note for the **engineer**, and speak a separate plain sentence to the
37
+ **operator**.
38
+
39
+ Engineer-facing note (structured, precise):
40
+
41
+ ```markdown
42
+ ## Intent
43
+ What the operator asked for, in their words.
44
+
45
+ ## Done so far
46
+ What is already built and where (branch or PR), and what is verified visually.
47
+
48
+ ## Blocked — why
49
+ The specific boundary: which scope, protected path, dependency, or gate — and
50
+ what an engineer needs to decide or do next.
51
+ ```
52
+
53
+ Operator-facing sentence (plain, framed as progress):
54
+
55
+ > *"That touches sign-in security, which needs an engineer's hands. I've written
56
+ > up exactly what you want and everything I built, and handed it to the team —
57
+ > here's the link. Want to keep going on something else meanwhile?"*
58
+
59
+ ## Scrub before you hand off
60
+
61
+ The note may be read outside the session — keep it clean. No secrets, no raw
62
+ diffs or stack traces, no internal jargon that leaks implementation the operator
63
+ never saw. State the intent and the blocker; link the PR for the code. Never
64
+ include anything you would not show the operator.
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "../skill-schema.json",
3
+ "name": "designer-handoff",
4
+ "version": "1.0.0",
5
+ "summary": "Designer Mode: package a scrubbed, dual-register df handoff artifact when an ask needs an engineer (out-of-scope, protected path, dependency change, or N=2 gate thrash), framed to the operator as progress.",
6
+ "originatingRepo": "momentiq-ai/dark-factory",
7
+ "files": [
8
+ { "template": "SKILL.md.tmpl", "target": "SKILL.md" }
9
+ ],
10
+ "variables": {
11
+ "REPO_NAME": {
12
+ "description": "Display name of the consumer repo (e.g. 'Taxpilot').",
13
+ "source": "darkfactory.yaml: repo.displayName",
14
+ "default": "this repo"
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: designer-ship
3
+ description: Use when a Designer Mode change is visually approved and ready to ship — confirm the brief's Done-when items, run gates, and open plus auto-merge a PR that carries the designer mode trailer and a Closes #N link with before/after screenshots embedded, relaying pipeline status to the operator in plain language.
4
+ ---
5
+
6
+ # /designer-ship — verify, ship, and relay in plain language
7
+
8
+ The change is visually approved. This skill takes it through the **unchanged**
9
+ production pipeline for {{REPO_NAME}} — branch, gates, PR, review, merge — and
10
+ narrates it to the operator as outcomes, never as plumbing.
11
+
12
+ The operator sees "it's in review" and "it's live". They never see git, a diff,
13
+ a PR number, or a critic finding.
14
+
15
+ ## 1. Confirm the acceptance contract
16
+
17
+ The brief's **Done-when checkboxes are the acceptance criteria.** Before
18
+ shipping, confirm each is satisfied and backed by the before/after screenshots
19
+ from `/designer-build` — a Done-when item with no visible proof is not done.
20
+
21
+ Make "done" proof-bound, not free text. Where the change has a
22
+ `.darkfactory/objectives.yaml`, verify it and read the closeout proof with
23
+ `df prove` (per-objective proven/pending/failed); see the `/objectives` skill
24
+ for authoring and binding mechanics. (Automated derivation of objectives
25
+ directly from a brief is a later Designer-Mode phase; today, treat the Done-when
26
+ list as the contract and verify each item visually.)
27
+
28
+ ## 2. Run the gates locally first
29
+
30
+ Run the repo's configured quality gates before pushing (build, tests, and
31
+ type-check as the repo defines them). `df gate-push` runs the local critic on
32
+ HEAD at push time and blocks on unresolved blockers — let it; never bypass a
33
+ gate or ask the operator to.
34
+
35
+ ## 3. Branch, commit, PR — with the mode trailers
36
+
37
+ Do the work on a branch (a worktree, per repo doctrine), then commit with the
38
+ Designer Mode trailers:
39
+
40
+ ```
41
+ Mode: designer
42
+ Closes #<N>
43
+ ```
44
+
45
+ The `Mode: designer` trailer tags the PR as designer-driven. Today, scope is
46
+ enforced live by the PreToolUse guard during the session, and the change still
47
+ faces the full multi-vendor critic quorum + merge queue like any other PR. The
48
+ deterministic mode-scope critic — which reads this trailer to validate the diff
49
+ against the catalog's writeScopes / protectedPaths / diffBudget and block an
50
+ out-of-scope diff at merge time, even from a jailbroken local session — lands in
51
+ a later Designer-Mode phase; the trailer is the label it will key on.
52
+ `Closes #<N>` links the tracking issue.
53
+
54
+ Open the PR and embed the **before/after screenshots** in the body so the change
55
+ is visible to reviewers and the operator:
56
+
57
+ ```bash
58
+ gh pr create --base main --title "<plain, conventional-commit title>" \
59
+ --body "<what changed, before/after screenshots, Closes #<N>>"
60
+ gh pr merge <PR#> --auto --squash
61
+ ```
62
+
63
+ Auto-merge carries the PR into the merge queue; the full critic quorum, required
64
+ checks, and review-thread resolution gate it. You do not admin-merge or bypass.
65
+
66
+ ## 4. Relay status in plain language
67
+
68
+ Translate pipeline state into outcomes for the operator. Check the PR's
69
+ automated checks (`gh pr checks`) and the merge-queue state, then say it plainly:
70
+
71
+ - In review: *"It's in review with our automated checks — a couple passed, one
72
+ still running. I'll tell you when it's live."*
73
+ - Merged: *"It's live."*
74
+ - A check found something: fix it within the N=2 iteration ceiling (re-verify
75
+ the visuals after each fix). If it won't converge, or the fix needs work
76
+ outside your scope, switch to `/designer-handoff` — never surface the raw
77
+ finding or critic jargon to the operator.
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "../skill-schema.json",
3
+ "name": "designer-ship",
4
+ "version": "1.0.0",
5
+ "summary": "Designer Mode: confirm the brief's Done-when items, run gates, and open + auto-merge a PR with the Mode: designer and Closes #N trailers and before/after screenshots, relaying pipeline status to the operator in plain language.",
6
+ "originatingRepo": "momentiq-ai/dark-factory",
7
+ "files": [
8
+ { "template": "SKILL.md.tmpl", "target": "SKILL.md" }
9
+ ],
10
+ "variables": {
11
+ "REPO_NAME": {
12
+ "description": "Display name of the consumer repo (e.g. 'Taxpilot').",
13
+ "source": "darkfactory.yaml: repo.displayName",
14
+ "default": "this repo"
15
+ }
16
+ }
17
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: objectives
3
- description: Use at PLAN TIME for any PR that implements a cycle's acceptance criteria — author the verifiable objectives contract before writing code. Run `df objectives derive --cycle <N> --apply` to generate .darkfactory/objectives.yaml from the linked cycle doc's Exit criteria, bind each objective's attestedBy to a real route or test, surface the manifest in your plan for agreement, then declare victory at closeout with `df prove` (not free-text "done").
3
+ description: Use at PLAN TIME for any PR that implements a cycle's (or designer brief's) acceptance criteria — author the verifiable objectives contract before writing code. Run `df objectives derive --cycle <N> --apply` (or `--brief <path>` for a designer brief's Done-when checkboxes) to generate .darkfactory/objectives.yaml from the linked source, bind each objective's attestedBy to a real route or test, surface the manifest in your plan for agreement, then declare victory at closeout with `df prove` (not free-text "done").
4
4
  ---
5
5
 
6
6
  # /objectives — plan-time verifiable objectives authoring
@@ -51,6 +51,21 @@ recognized. Accepted item forms:
51
51
  Each criterion must be **one physical line**. Multi-line criteria are
52
52
  rejected because only that single line is hashed into the source binding.
53
53
 
54
+ **Designer briefs (`--brief`):** for Designer Mode work, derive from a committed
55
+ design brief instead of a cycle doc — `--cycle` and `--brief` are mutually
56
+ exclusive sibling input sources:
57
+
58
+ ```bash
59
+ # One objective per `## Done-when` checkbox in .darkfactory/briefs/<slug>.md
60
+ df objectives derive --brief .darkfactory/briefs/<slug>.md --apply
61
+ ```
62
+
63
+ Each `- [ ]`/`- [x]` checkbox under `## Done-when` becomes one objective with id
64
+ `brief-<slug>#ac<k>` (`<slug>` from the brief filename), `source.kind: brief`, and
65
+ the same `text-hash` binding. The checkbox state is stripped before hashing, so
66
+ marking an item done never moves its hash. Bind / check / prove below are
67
+ identical to the cycle path.
68
+
54
69
  The emitted manifest looks like:
55
70
 
56
71
  ```yaml
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Auto-derived capture surfaces for the Dark Factory **`ui-visual`** verification
3
+ * route (Designer Mode, cycle 27 Phase C).
4
+ *
5
+ * The `playwright` route (`coverage.ts` + `ui-route.producer.spec.ts`) makes a
6
+ * human hand-author `SURFACES[]` — one entry per URL, with a `covers` glob and a
7
+ * copy-stable `requiredHeading`. That is right for an engineer who knows the app.
8
+ * A non-technical Designer-Mode operator cannot. `ui-visual` instead DERIVES the
9
+ * capture surfaces from the changed Next.js **app-router** files: a changed
10
+ * `app/pricing/page.tsx` maps to the URL `/pricing`, which the producer navigates
11
+ * and screenshots. No `SURFACES[]` to edit.
12
+ *
13
+ * This module is PURE (no browser, no fs, no Dark Factory / vendor dependency) and
14
+ * is the load-bearing half of the route — the path→URL mapping and the fail-closed
15
+ * classification — so it is unit-tested without a browser (mirroring `coverage.ts`).
16
+ *
17
+ * ── CONSUMER REFERENCE FILE — ships in the npm tarball next to `coverage.ts`;
18
+ * copy it into your repo alongside `ui-visual.producer.spec.ts` and own it.
19
+ * `df skills install verify` does NOT overwrite it.
20
+ */
21
+
22
+ /** One capture surface derived from a changed app-router file. */
23
+ export interface DerivedSurface {
24
+ /** URL path to navigate (relative to baseURL), e.g. "/", "/pricing". Dynamic
25
+ * routes carry a `:param` placeholder (e.g. "/blog/:slug") and are NOT
26
+ * directly navigable — see `dynamic`. */
27
+ path: string;
28
+ /** Filesystem-safe slug for the evidence subdir, e.g. "home", "pricing". */
29
+ slug: string;
30
+ /** The changed source path this surface is the evidence for. */
31
+ covers: string;
32
+ /** True when the route has a `[param]` / `[...catch]` segment: the producer
33
+ * cannot navigate it without a concrete value, so it is recorded (for human
34
+ * review) rather than auto-captured. */
35
+ dynamic: boolean;
36
+ }
37
+
38
+ export interface DerivedSurfacePartition {
39
+ /** Static, navigable surfaces — the producer captures each. De-duped by slug. */
40
+ surfaces: DerivedSurface[];
41
+ /** Dynamic routes touched by the change — recorded (need a concrete param),
42
+ * not auto-navigated. De-duped by slug. */
43
+ dynamic: DerivedSurface[];
44
+ /** Changed product-UI paths with NO derivable route (a component, a lib, a
45
+ * non-`app/` `*.tsx`). Auto-derivation cannot know which page renders a
46
+ * component, so these FAIL CLOSED — the change must also touch the page(s)
47
+ * that render them, or the operator adds an explicit surface. Mirrors the
48
+ * `coverage.ts` fail-closed contract. */
49
+ uncovered: string[];
50
+ }
51
+
52
+ // App-router files that render a routable surface. `route.*` (API route
53
+ // handlers) render no UI and are treated as non-visual (exempt). `default.*`
54
+ // (parallel-route fallbacks) render UI at their segment, so they count.
55
+ const ROUTE_FILE_BASENAMES = new Set(["page", "layout", "template", "default"]);
56
+ const ROUTE_FILE_RE = /\.(tsx|jsx|ts|js)$/;
57
+
58
+ /**
59
+ * Harness / non-UI files that are EXEMPT from the coverage requirement — the
60
+ * route's own machinery, styles, tests, type decls, config, data. A change to
61
+ * ONLY these does not require a capture (there is no rendered surface). Kept in
62
+ * sync with `coverage.ts`'s `NON_SURFACE_GLOBS` philosophy but self-contained.
63
+ */
64
+ export const UI_VISUAL_NON_SURFACE_SUFFIXES: readonly string[] = [
65
+ ".css",
66
+ ".scss",
67
+ ".json",
68
+ ".md",
69
+ ".mdx",
70
+ ".d.ts",
71
+ ".test.ts",
72
+ ".test.tsx",
73
+ ".spec.ts",
74
+ ".spec.tsx",
75
+ ];
76
+
77
+ function isNonSurface(path: string): boolean {
78
+ const lower = path.toLowerCase();
79
+ if (UI_VISUAL_NON_SURFACE_SUFFIXES.some((s) => lower.endsWith(s))) return true;
80
+ // The producer harness itself + Playwright config + test dirs.
81
+ if (/(^|\/)tests?(\/|$)/.test(lower)) return true;
82
+ if (/(^|\/)__tests__(\/|$)/.test(lower)) return true;
83
+ if (/(^|\/)(coverage|ui-visual-surfaces)\.ts$/.test(lower)) return true;
84
+ if (/playwright[^/]*\.config\.(ts|js|mjs|cjs)$/.test(lower)) return true;
85
+ if (/\.producer\.spec\.ts$/.test(lower)) return true;
86
+ return false;
87
+ }
88
+
89
+ /**
90
+ * Map one changed source path to a route surface, or `null` when it maps to no
91
+ * navigable route (a component, a private `_folder` colocation file, a file not
92
+ * under an `app/` router root, or an API `route.*` handler).
93
+ */
94
+ export function deriveSurface(changedPath: string): DerivedSurface | null {
95
+ if (ROUTE_FILE_RE.test(changedPath) === false) return null;
96
+ const segments = changedPath.split("/");
97
+ const file = segments[segments.length - 1] ?? "";
98
+ const dotIdx = file.indexOf(".");
99
+ const base = dotIdx >= 0 ? file.slice(0, dotIdx) : file;
100
+ if (!ROUTE_FILE_BASENAMES.has(base)) return null;
101
+
102
+ // Locate the app-router root: the LAST `app` segment (handles `web/app/…`,
103
+ // `apps/web/src/app/…`, `src/app/…`). No `app/` root ⇒ not a routed file.
104
+ const appIdx = segments.lastIndexOf("app");
105
+ if (appIdx < 0) return null;
106
+
107
+ const routeDirSegments = segments.slice(appIdx + 1, segments.length - 1);
108
+ const urlSegments: string[] = [];
109
+ let dynamic = false;
110
+ for (const seg of routeDirSegments) {
111
+ // Route groups `(marketing)` and intercepting markers `(.)`,`(..)` — removed
112
+ // from the URL.
113
+ if (seg.startsWith("(") && seg.endsWith(")")) continue;
114
+ // Parallel-route slots `@modal` — not part of the URL.
115
+ if (seg.startsWith("@")) continue;
116
+ // Private folders `_components` opt OUT of routing — the file is colocation,
117
+ // not a route.
118
+ if (seg.startsWith("_")) return null;
119
+ // Dynamic segments `[slug]`, `[...all]`, `[[...opt]]` → `:slug` placeholder.
120
+ const dyn = seg.match(/^\[+\.{0,3}(.+?)\]+$/);
121
+ if (dyn) {
122
+ dynamic = true;
123
+ urlSegments.push(`:${dyn[1]}`);
124
+ continue;
125
+ }
126
+ urlSegments.push(seg);
127
+ }
128
+
129
+ const path = urlSegments.length === 0 ? "/" : `/${urlSegments.join("/")}`;
130
+ const slug = slugForUrl(urlSegments);
131
+ return { path, slug, covers: changedPath, dynamic };
132
+ }
133
+
134
+ function sanitizeSlug(raw: string): string {
135
+ return raw
136
+ .replace(/:/g, "")
137
+ .replace(/[^a-z0-9-]+/gi, "-")
138
+ .replace(/-+/g, "-")
139
+ .replace(/^-|-$/g, "")
140
+ .toLowerCase();
141
+ }
142
+
143
+ function slugForUrl(urlSegments: readonly string[]): string {
144
+ if (urlSegments.length === 0) return "home";
145
+ return sanitizeSlug(urlSegments.join("-"));
146
+ }
147
+
148
+ /**
149
+ * Partition changed paths (already restricted to the route's trigger surface —
150
+ * `web/**` / `*.tsx` — by `playwright-route.sh`) into the surfaces to capture,
151
+ * the dynamic routes to record, and the uncovered product paths that FAIL CLOSED.
152
+ *
153
+ * An empty `changed` (a manual run with the env unset) yields nothing derivable;
154
+ * the producer falls back to capturing the home surface so evidence is never zero.
155
+ */
156
+ export function deriveSurfacesFromChangedPaths(
157
+ changed: readonly string[],
158
+ ): DerivedSurfacePartition {
159
+ const surfacesBySlug = new Map<string, DerivedSurface>();
160
+ const dynamicBySlug = new Map<string, DerivedSurface>();
161
+ const uncovered: string[] = [];
162
+
163
+ for (const raw of changed) {
164
+ const path = raw.trim();
165
+ if (!path) continue;
166
+ if (isNonSurface(path)) continue; // harness / non-UI — exempt.
167
+ const surface = deriveSurface(path);
168
+ if (surface === null) {
169
+ uncovered.push(path);
170
+ continue;
171
+ }
172
+ const bucket = surface.dynamic ? dynamicBySlug : surfacesBySlug;
173
+ if (!bucket.has(surface.slug)) bucket.set(surface.slug, surface);
174
+ }
175
+
176
+ return {
177
+ surfaces: [...surfacesBySlug.values()],
178
+ dynamic: [...dynamicBySlug.values()],
179
+ uncovered,
180
+ };
181
+ }
182
+
183
+ /**
184
+ * The home surface, smoke-captured when the change armed the route but derived NO
185
+ * navigable surface AND touched no dynamic/uncovered path (a harness-only or
186
+ * signal-less manual run) — so the route never passes with zero evidence.
187
+ */
188
+ export const UI_VISUAL_HOME_FALLBACK: DerivedSurface = {
189
+ path: "/",
190
+ slug: "home",
191
+ covers: "(smoke)",
192
+ dynamic: false,
193
+ };
194
+
195
+ /** The producer's capture decision: what to navigate, or why to fail closed. */
196
+ export interface CapturePlan {
197
+ /** Surfaces the producer captures (empty when `failClosed` is set). */
198
+ capture: DerivedSurface[];
199
+ /** Non-null when the route CANNOT produce real evidence and must fail the run
200
+ * (a dynamic page that can't be auto-navigated, or an uncovered product path).
201
+ * `null` means the plan is safe to capture. */
202
+ failClosed: { code: "uncovered" | "dynamic-unmapped"; paths: string[]; message: string } | null;
203
+ }
204
+
205
+ /**
206
+ * Decide what the `ui-visual` producer captures for a partition of changed paths,
207
+ * or whether it must FAIL CLOSED. The fail-closed contract (extracted here so it is
208
+ * unit-tested without a browser):
209
+ *
210
+ * - `uncovered` path(s) present → fail closed (a bare component can't be evidenced
211
+ * by rendering an unrelated surface).
212
+ * - `dynamic` route(s) present → fail closed. A `[slug]` / `[...all]` page has no
213
+ * auto-navigable URL, so this auto-derive route cannot evidence it; capturing
214
+ * anything else (a static sibling, or the home fallback) would be a false pass
215
+ * for the changed page. Dynamic pages belong to the engineer `playwright` route
216
+ * (hand-authored concrete surfaces). Fail closed even when static surfaces are
217
+ * also present — a run is all-or-nothing.
218
+ * - otherwise capture the derived static surfaces; if there are none, smoke-capture
219
+ * the home surface so evidence is never zero.
220
+ */
221
+ export function planCapture(partition: DerivedSurfacePartition): CapturePlan {
222
+ const { surfaces, dynamic, uncovered } = partition;
223
+
224
+ if (uncovered.length > 0) {
225
+ return {
226
+ capture: [],
227
+ failClosed: {
228
+ code: "uncovered",
229
+ paths: [...uncovered],
230
+ message:
231
+ "ui-visual route: the following changed UI path(s) map to NO app-router " +
232
+ "surface, so this route cannot produce real evidence for them and FAILS " +
233
+ "CLOSED:\n" +
234
+ uncovered.map((p) => ` - ${p}`).join("\n") +
235
+ "\n\nAuto-derivation captures app-router pages (e.g. app/foo/page.tsx → " +
236
+ "/foo). A bare component has no route: also change (or add a page under " +
237
+ "app/ that renders) the affected UI, so the capture is real.",
238
+ },
239
+ };
240
+ }
241
+
242
+ if (dynamic.length > 0) {
243
+ return {
244
+ capture: [],
245
+ failClosed: {
246
+ code: "dynamic-unmapped",
247
+ paths: dynamic.map((d) => d.path),
248
+ message:
249
+ "ui-visual route: dynamic app-router page(s) changed. This auto-derive " +
250
+ "route does NOT support dynamic pages (a `[slug]` / `[...all]` route has " +
251
+ "no concrete URL to auto-navigate), so it FAILS CLOSED:\n" +
252
+ dynamic.map((d) => ` - ${d.covers} → ${d.path}`).join("\n") +
253
+ "\n\nCapture dynamic pages with the engineer `playwright` route instead " +
254
+ "(its hand-authored SURFACES[] take real params).",
255
+ },
256
+ };
257
+ }
258
+
259
+ if (surfaces.length > 0) return { capture: [...surfaces], failClosed: null };
260
+ return { capture: [UI_VISUAL_HOME_FALLBACK], failClosed: null };
261
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Playwright config for the **`ui-visual` (Designer Mode) verification-route
3
+ * producer** (cycle 27 Phase C).
4
+ *
5
+ * DRY over the base UI-route config: it reuses every setting from
6
+ * `playwright.ui-route.config.ts` (one chromium project, no retries, optional
7
+ * storage-state, optional local-preview `webServer`, all the same env knobs) and
8
+ * changes ONLY `testMatch` so this config runs `ui-visual.producer.spec.ts`
9
+ * instead of `ui-route.producer.spec.ts`. Kept a separate file so the shared
10
+ * `playwright-route.sh` can select it via `DF_UI_ROUTE_CONFIG=ui-visual.config.ts`
11
+ * without disturbing the `playwright` route.
12
+ *
13
+ * ── CONSUMER REFERENCE FILE — copy into your repo next to the base config +
14
+ * the producer spec and own it. `df skills install verify` does NOT overwrite it.
15
+ */
16
+ import { defineConfig } from "@playwright/test";
17
+ import base from "./playwright.ui-route.config";
18
+
19
+ export default defineConfig({
20
+ ...base,
21
+ testMatch: /ui-visual\.producer\.spec\.ts$/,
22
+ });