@event4u/agent-config 10.3.0 → 11.0.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +147 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze.md +2 -1
- package/dist/agent-src/commands/council/debate.md +13 -4
- package/dist/agent-src/commands/council/default.md +26 -11
- package/dist/agent-src/commands/council.md +1 -1
- package/dist/agent-src/commands/design-system/capture.md +44 -0
- package/dist/agent-src/commands/design-system/generate.md +75 -0
- package/dist/agent-src/commands/design-system/import.md +63 -0
- package/dist/agent-src/commands/design-system.md +66 -0
- package/dist/agent-src/commands/pr/create.md +51 -26
- package/dist/agent-src/commands/worktree/create.md +5 -3
- package/dist/agent-src/rules/settings-ask-protocol.md +14 -9
- package/dist/agent-src/skills/ai-council/SKILL.md +24 -6
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +12 -1
- package/dist/agent-src/skills/corpus-grounding/scripts/decision_engine.ts +215 -7
- package/dist/agent-src/skills/corpus-grounding/scripts/ground.ts +39 -1
- package/dist/agent-src/skills/design-intelligence/ATTRIBUTION.md +19 -2
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +36 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +9 -0
- package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +70 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +9 -3
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +9 -6
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -21
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
- package/dist/cli-delegate/{chunk-BV3WR67D.js → chunk-55WZN5VF.js} +3 -6
- package/dist/cli-delegate/{chunk-X3VOARZD.js → chunk-L565YFXH.js} +3 -3
- package/dist/cli-delegate/{chunk-JMQZ7MM5.js → chunk-MNWRDMR5.js} +1 -1
- package/dist/cli-delegate/{chunk-IR3IVRVC.js → chunk-NLP54U3J.js} +2 -1
- package/dist/cli-delegate/{chunk-FV34EGEX.js → chunk-NN4KW4HZ.js} +1 -1
- package/dist/cli-delegate/{chunk-6ENSXSBW.js → chunk-PPQO4NGR.js} +1 -1
- package/dist/cli-delegate/chunk-RLN73PR5.js +157 -0
- package/dist/cli-delegate/{chunk-6LOPHJLD.js → chunk-RYNADUFG.js} +1 -1
- package/dist/cli-delegate/{chunk-I5EAFRII.js → chunk-ZPSXJTLI.js} +2 -154
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +673 -215
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +10 -8
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +6 -4
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +127 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +9 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +8 -4
- package/dist/hooks/dispatch.js +994 -399
- package/dist/install/install.mjs +5 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1104 -593
- package/dist/scripts/_lib/agent_settings.js +8 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/server/schemas/settings.js +3 -4
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/{index-5fXOHoVh.js → index-B0aMJnuz.js} +2 -2
- package/dist/ui/assets/{index-5fXOHoVh.js.map → index-B0aMJnuz.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +12 -9
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
- package/src/config/agent-settings.template.yml +15 -18
- package/src/config/gate-coverage.yml +33 -2
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/pack-size-budget.json +3 -2
- package/src/scripts/_lib/agent_settings.ts +8 -0
- package/src/scripts/_lib/design_system_import.ts +987 -0
- package/src/scripts/check_branch_freshness.ts +238 -6
- package/src/scripts/design_system_import.ts +264 -0
- package/src/scripts/external_sources_denylist.json +7 -2
- package/src/scripts/hook_manifest.yaml +71 -2
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +10 -0
- package/src/scripts/hooks/host_semantics.ts +2 -0
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +212 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +723 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +39 -2
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +194 -13
- package/src/scripts/session_register_hook.ts +120 -10
- package/src/server/schemas/settings.ts +3 -6
- package/tests/fixtures/hooks/subagent_start.json +7 -0
- package/tests/fixtures/hooks/subagent_stop.json +8 -0
|
@@ -68,52 +68,77 @@ gate.
|
|
|
68
68
|
|
|
69
69
|
Run, in order:
|
|
70
70
|
|
|
71
|
-
1.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
1. **Ask the gate, never hand-roll the comparison:**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
./scripts-run src/scripts/check_branch_freshness
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It resolves the base itself — explicit `--base`, else the base of the **open
|
|
78
|
+
PR for this branch** as the forge reports it, else the repo default from
|
|
79
|
+
`origin/HEAD`. Do **not** substitute `git rev-list --count HEAD..origin/main`:
|
|
80
|
+
that hardcodes a base this branch may not target, and it reads the local
|
|
81
|
+
tracking ref, which is a fetch from earlier in the session — a memory, not a
|
|
82
|
+
check (`direct-answers` Iron Law 2). Exit `0` = current, `1` = behind, and the
|
|
83
|
+
refusal names the base **and where that base came from**.
|
|
84
|
+
|
|
85
|
+
2. `gh pr list --state open --base {resolved-base} --limit 20 --json number,headRefName,files`
|
|
86
|
+
— open PRs targeting the same base, for the overlap question below.
|
|
76
87
|
|
|
77
88
|
**Decision matrix:**
|
|
78
89
|
|
|
79
|
-
|
|
|
90
|
+
| Gate | Overlapping open PR touches same files? | Action |
|
|
80
91
|
|---|---|---|
|
|
81
|
-
| `0` | — | Proceed to Step 2 |
|
|
82
|
-
| `1
|
|
83
|
-
| `1
|
|
92
|
+
| exit `0` | — | Proceed to Step 2 |
|
|
93
|
+
| exit `1` | No | **Merge the base in** — `git fetch origin && git merge origin/{resolved-base} --no-edit` — then run the regeneration set below, then proceed. No need to ask; state that you did it. |
|
|
94
|
+
| exit `1` | **Yes** | STOP — surface the overlapping PR number, ask: stack on top of it / wait for it to land / proceed-anyway-and-accept-conflicts / cancel |
|
|
95
|
+
| warns `could not ask the forge` | — | The **default** base was checked and an open PR against a different base was **not** ruled out. Say so; do not report it as a clean freshness pass. |
|
|
84
96
|
|
|
85
|
-
A branch behind its base is **not** current — bring
|
|
97
|
+
A branch behind its base is **not** current — bring the base in **before** the PR
|
|
86
98
|
exists, never after. Auto-merge when there is no file overlap (the common case);
|
|
87
99
|
only ask when an overlapping open PR makes the base genuinely ambiguous. Never
|
|
88
100
|
improvise the base or silently proceed when behind **and** overlapping. The
|
|
89
101
|
10-second fetch beats hours of rebase reconciliation after the parent PR lands.
|
|
90
102
|
|
|
91
|
-
**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
**After EVERY merge — not only on conflict — regenerate the derived files.**
|
|
104
|
+
This is the half that gets skipped, because a merge that lands cleanly *looks*
|
|
105
|
+
finished. It is not: these files are **generated**, so a clean auto-merge of
|
|
106
|
+
them produces a file that describes neither side's sources. The dashboard is the
|
|
107
|
+
usual casualty — it merges without a conflict marker and then reports a roadmap
|
|
108
|
+
set that no longer exists.
|
|
109
|
+
|
|
110
|
+
1. `./agent-config roadmap:progress` → the roadmap dashboard.
|
|
111
|
+
2. `./scripts-run src/scripts/build_proof` → `docs/proof.md`.
|
|
112
|
+
3. `bash src/scripts/condense.sh --sync`, then `--check` → re-projects
|
|
113
|
+
`dist/agent-src/` from the merged source.
|
|
101
114
|
4. Stage the regenerated files, complete the merge, and re-run the touched
|
|
102
115
|
verification before pushing.
|
|
103
116
|
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
On a **conflict**, the same four steps are the resolution — regenerate from the
|
|
118
|
+
merged sources, never hand-pick hunks. Only a conflict in **hand-authored**
|
|
119
|
+
content (source code, prose you or the other PR wrote) is surfaced to the user;
|
|
120
|
+
a generated-file conflict never is.
|
|
106
121
|
|
|
107
122
|
### 1b-ii. Update freshness — MANDATORY on EVERY later push to an open PR
|
|
108
123
|
|
|
109
124
|
The gate above is not creation-only. **Every subsequent push** to a branch with
|
|
110
125
|
an open PR (a CI fix, a review response, a follow-up commit) re-runs the same
|
|
111
|
-
sequence first: `
|
|
112
|
-
`origin/{
|
|
126
|
+
sequence first: `check_branch_freshness` → on exit `1`, merge
|
|
127
|
+
`origin/{resolved-base}` in → regenerate the derived files → verify → push.
|
|
113
128
|
A PR that sits open while its base advances goes stale silently; keeping the
|
|
114
129
|
base merged **at every touch** means it stays `mergeStateStatus: CLEAN` instead
|
|
115
|
-
of accumulating conflicts for the moment the user wants to merge. If the
|
|
116
|
-
|
|
130
|
+
of accumulating conflicts for the moment the user wants to merge. If the gate
|
|
131
|
+
exits `0`, the step costs one `ls-remote` and nothing else.
|
|
132
|
+
|
|
133
|
+
Once the PR exists, this is also when the resolution gets *more* precise rather
|
|
134
|
+
than less: the gate can now read the PR's real `baseRefName` from the forge, so
|
|
135
|
+
a stacked or release-line PR is measured against the branch it will actually
|
|
136
|
+
merge into instead of against the repo default.
|
|
137
|
+
|
|
138
|
+
Where the project wires this gate into its pre-push chain, a push that goes out
|
|
139
|
+
through the hook is covered without a separate step. Run it by hand when you push
|
|
140
|
+
with the hook bypassed, or when you are about to open the PR some time after the
|
|
141
|
+
last push — that gap is exactly where a base moves unseen.
|
|
117
142
|
|
|
118
143
|
### 1c. PR-gate — archive completed roadmaps (MANDATORY)
|
|
119
144
|
|
|
@@ -26,7 +26,7 @@ Thin orchestration — mechanics live in the skills; do not restate them.
|
|
|
26
26
|
### 1. Gate + create
|
|
27
27
|
|
|
28
28
|
Run [`using-git-worktrees`](../../../skills/using-git-worktrees/SKILL.md)
|
|
29
|
-
in full:
|
|
29
|
+
in full: instruction-only pre-flight, existing-worktree inspection,
|
|
30
30
|
directory convention, ignore-safety check, worktree creation, the
|
|
31
31
|
[§ 4b seeding allow/deny list](../../../skills/using-git-worktrees/SKILL.md#4b-seed-the-worktree--allow--deny-list),
|
|
32
32
|
dependency install + clean baseline. Its Iron Law (no worktree without
|
|
@@ -61,5 +61,7 @@ name it as the suggested next action — suggest, never auto-run.
|
|
|
61
61
|
### Rules
|
|
62
62
|
|
|
63
63
|
- **Do NOT commit or push.**
|
|
64
|
-
- Worktree creation
|
|
65
|
-
|
|
64
|
+
- Worktree creation is instruction-only (ADR-229). Invoking this command
|
|
65
|
+
**is** the explicit request, so the skill's § 0 pre-flight passes and no
|
|
66
|
+
further permission question is raised. Nothing here creates a worktree
|
|
67
|
+
the user did not ask for.
|
|
@@ -77,12 +77,16 @@ conservative default (`""`, `false`, `false`), so absent is indistinguishable
|
|
|
77
77
|
from *no* and never from *yes*.
|
|
78
78
|
|
|
79
79
|
**Class C carrying an `ask` value in its own enum** — a runtime question whose
|
|
80
|
-
answer dies with the run: `
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
answer dies with the run: `tokens.rich_skills`,
|
|
81
|
+
`subagents.adversarial_council`, `decision_engine.on_block`. **None of them
|
|
82
|
+
ships as `ask`** — a human sets it, so the whole class is opt-in and no shipped
|
|
83
|
+
default routes a question through this protocol at all.
|
|
84
|
+
|
|
85
|
+
Two deletions got the list here. Always-on orchestration
|
|
86
|
+
(road-to-always-on-orchestration Phase 1) removed `subagents.auto` and
|
|
87
|
+
`subagents.budget_routing`. ADR-229 removed `worktrees.mode`, which was the last
|
|
88
|
+
key that *shipped* as `ask` — worktree creation is instruction-only now, so
|
|
89
|
+
there is no setting and nothing to ask about.
|
|
86
90
|
|
|
87
91
|
## The budget
|
|
88
92
|
|
|
@@ -153,9 +157,10 @@ by `settings:set` and only *displayed* by the GUI.
|
|
|
153
157
|
- Class A — resolves to its default, never asked.
|
|
154
158
|
- Class C **not** set to `ask` — no question; a change is a human edit.
|
|
155
159
|
- Already decided on any resolution layer, or the run completes without it.
|
|
156
|
-
- **The `ask` routes to a permission gate
|
|
157
|
-
`scope-control`
|
|
158
|
-
|
|
160
|
+
- **The `ask` routes to a permission gate** — a permission ask about an
|
|
161
|
+
*action*, which `scope-control` owns; a storage line there would store
|
|
162
|
+
nothing. `worktrees.mode: ask` was the worked example until ADR-229 deleted
|
|
163
|
+
the key; the carve-out is kept because the shape recurs, not the key.
|
|
159
164
|
- **The `ask` is code, not agent-carried.** `decision_engine.on_block: ask` is a
|
|
160
165
|
TTY prompt in `work_engine/hooks/builtin/decision_gate.ts`.
|
|
161
166
|
|
|
@@ -328,9 +328,25 @@ not a default.
|
|
|
328
328
|
2. **Bundle + redact.** Call `scripts/ai_council/bundler.ts` to produce
|
|
329
329
|
a redacted artefact bundle. If `BundleTooLarge` fires, surface the
|
|
330
330
|
size and ask the user to narrow scope — do NOT truncate silently.
|
|
331
|
-
3. **
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
3. **Clear the spend bound.** The council is standing-authorized — there is
|
|
332
|
+
no per-invocation approval. What still binds is a *bound*, resolved in
|
|
333
|
+
this order:
|
|
334
|
+
- **No billable member** (every member is `mode: cli` under
|
|
335
|
+
subscription auth — `billable=False`) → nothing is spent. Render the
|
|
336
|
+
estimate table as information and fan out. No gate.
|
|
337
|
+
- **Billable members with a configured ceiling** (`cost_budget.max_total_usd`
|
|
338
|
+
non-zero, or `daily_limit_usd` non-zero) → the ceiling **is** the
|
|
339
|
+
authorization the user already gave. Render the estimate and fan out.
|
|
340
|
+
`on_overrun` still fires per member on breach (below) — that is where
|
|
341
|
+
the user regains the decision.
|
|
342
|
+
- **Billable members with no ceiling at all** (`max_total_usd: 0` AND
|
|
343
|
+
`daily_limit_usd: 0`) → unbounded paid spend, so surface the estimate
|
|
344
|
+
and require an explicit `1`. Nothing bounds the call, so the user must.
|
|
345
|
+
|
|
346
|
+
Autonomy settings do not *create* a bound and do not lift the last case.
|
|
347
|
+
Consumers who want the old per-run gate back set a small
|
|
348
|
+
`cost_budget.max_total_usd`: every call then breaches and `on_overrun`
|
|
349
|
+
asks, per member.
|
|
334
350
|
4. **Fan out.** Dispatch the bundle to each enabled council member via
|
|
335
351
|
`scripts/ai_council/orchestrator.ts`. Each member receives the
|
|
336
352
|
neutrality preamble from `prompts.ts` plus the artefact — nothing
|
|
@@ -594,8 +610,9 @@ overrides the table regardless of how the lens resolved.
|
|
|
594
610
|
identity — that primes the reviewer and collapses diversity.
|
|
595
611
|
- Do NOT silently truncate a too-large bundle — surface the size and
|
|
596
612
|
ask for narrower scope.
|
|
597
|
-
- Do NOT
|
|
598
|
-
|
|
613
|
+
- Do NOT spend against an unbounded budget without asking — a billable
|
|
614
|
+
member with neither `max_total_usd` nor `daily_limit_usd` set has no
|
|
615
|
+
ceiling, and autonomy is not a ceiling.
|
|
599
616
|
- Do NOT reuse SDK clients across invocations — re-load keys via
|
|
600
617
|
`load_*_key()` each call.
|
|
601
618
|
|
|
@@ -620,7 +637,8 @@ Real failure modes seen in the wild:
|
|
|
620
637
|
| Paste the host-agent identity ("I am Augment / Claude Code…") | Identity primes the reviewer's model. | Neutrality preamble in `prompts.ts` already handles this. |
|
|
621
638
|
| Silently truncate a too-large bundle. | Misleads the reviewer into thinking they saw the whole thing. | Bundler raises `BundleTooLarge`; surface and ask for narrower scope. |
|
|
622
639
|
| Reuse the same SDK client across calls without re-loading the key. | Leaks the key in long-lived process state. | Each invocation builds fresh clients from `load_*_key()`. |
|
|
623
|
-
|
|
|
640
|
+
| Spend against a billable member with no configured ceiling because "the council is standing-authorized". | Standing authorization is a *bound* the user set once, not a blank cheque. No ceiling = nothing was authorized. | Surface the estimate and ask, exactly in that one case (Procedure § 3). |
|
|
641
|
+
| Re-ask per invocation once a ceiling exists. | The ceiling already carried the decision; re-asking is the approval burden this default removed. | Fan out; let `on_overrun` ask on breach. |
|
|
624
642
|
| Forward council convergence to the user as numbered options without a host verdict. | Convergence ≠ correctness; the council never saw the codebase. | Apply the *Critical evaluation* lens; tag every finding `accept` / `accept-with-modification` / `reject` / `needs-input` with one-line reason. |
|
|
625
643
|
| Reject a finding on preference, not evidence. | "I don't like this" is not a verdict. | Cite the file, line, ADR, or contract that justifies the rejection — or surface as `needs-input`. |
|
|
626
644
|
| Paraphrase council output into the host's own analysis to defend a verdict. | Strips attribution, breaches `direct-answers` no-invented-facts. | Verdict cites host evidence (file:line); council output stays attributed in the per-member sections. |
|
|
@@ -71,7 +71,8 @@ Scripts resolve all paths **skill-relative** (per
|
|
|
71
71
|
|
|
72
72
|
./scripts-run <skills-root>/corpus-grounding/scripts/ground ground \
|
|
73
73
|
--manifest <skills-root>/<domain-skill>/data/manifest.json \
|
|
74
|
-
"luxury e-commerce" [--context '{"data_heavy": true}'] [--persist DIR]
|
|
74
|
+
"luxury e-commerce" [--context '{"data_heavy": true}'] [--persist DIR] \
|
|
75
|
+
[--variance 1-10] [--motion 1-10] [--density 1-10]
|
|
75
76
|
|
|
76
77
|
./scripts-run <skills-root>/corpus-grounding/scripts/ground validate --manifest …
|
|
77
78
|
```
|
|
@@ -79,6 +80,16 @@ Scripts resolve all paths **skill-relative** (per
|
|
|
79
80
|
`<skills-root>` is wherever skills are deployed (`~/.claude/skills/` for
|
|
80
81
|
Claude Code installs; `src/skills/` inside this repo).
|
|
81
82
|
|
|
83
|
+
**The three `ground` dials are optional 1-10 preferences, and unset means
|
|
84
|
+
unset** — a dial you do not pass changes nothing, so the no-flag result is
|
|
85
|
+
byte-identical to what it was before they existed. `--variance` biases which
|
|
86
|
+
style is **selected** from the rows the corpus returned (never which rows come
|
|
87
|
+
back), and when it changes the pick away from the grounding rule's own choice
|
|
88
|
+
the run says so in its evidence gaps. `--motion` needs a `gsap` domain in the
|
|
89
|
+
manifest and reports when a tier matches nothing. `--density` overrides the
|
|
90
|
+
spacing scale. All three render in the text output and in a persisted
|
|
91
|
+
`MASTER.md`, not only under `--json`.
|
|
92
|
+
|
|
82
93
|
## Output format
|
|
83
94
|
|
|
84
95
|
**Interface contract v1 — stability promise.** Per ADR-061 §2 ("Opus
|
|
@@ -531,16 +531,76 @@ function _select_best_match(
|
|
|
531
531
|
|
|
532
532
|
// ── grounding ────────────────────────────────────────────────────────────
|
|
533
533
|
|
|
534
|
+
/**
|
|
535
|
+
* The three optional 1-10 design dials.
|
|
536
|
+
*
|
|
537
|
+
* Adapted from the upstream generator's tier table (MIT; see
|
|
538
|
+
* `design-intelligence/ATTRIBUTION.md` and `provenance/borrows.jsonl`). The
|
|
539
|
+
* TIER BOUNDARIES and the values they carry are the upstream's data; the
|
|
540
|
+
* plumbing below is this engine's own, because upstream threads them through a
|
|
541
|
+
* `generate()` this tree does not have and ours has `ground()` instead.
|
|
542
|
+
*
|
|
543
|
+
* Unset means unset: a dial the caller did not pass changes nothing, which is
|
|
544
|
+
* the upstream's own contract and the only thing that keeps the no-flag path
|
|
545
|
+
* byte-identical to the behaviour before this existed.
|
|
546
|
+
*/
|
|
547
|
+
export const DIAL_TIERS: Record<string, Array<[number, number, ResultDict]>> = {
|
|
548
|
+
variance: [
|
|
549
|
+
[1, 3, { label: 'Centered / Minimal', style_keywords: ['Minimalism', 'Exaggerated Minimalism', 'centered', 'symmetric', 'grid-based'] }],
|
|
550
|
+
[4, 7, { label: 'Balanced / Modern', style_keywords: ['modern', 'structured', 'balanced'] }],
|
|
551
|
+
[8, 10, { label: 'Bold / Asymmetric', style_keywords: ['Brutalism', 'Bento Grids', 'asymmetric', 'experimental'] }],
|
|
552
|
+
],
|
|
553
|
+
motion: [
|
|
554
|
+
[1, 3, { label: 'Subtle', tier: 'Subtle' }],
|
|
555
|
+
[4, 7, { label: 'Standard', tier: 'Standard' }],
|
|
556
|
+
[8, 10, { label: 'Complex', tier: 'Complex' }],
|
|
557
|
+
],
|
|
558
|
+
density: [
|
|
559
|
+
[1, 3, { label: 'Spacious', spacing: { xs: '4px', sm: '8px', md: '24px', lg: '32px', xl: '48px', '2xl': '64px', '3xl': '96px' } }],
|
|
560
|
+
[4, 7, { label: 'Standard', spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', '2xl': '48px', '3xl': '64px' } }],
|
|
561
|
+
[8, 10, { label: 'Dense / Dashboard', spacing: { xs: '2px', sm: '4px', md: '8px', lg: '12px', xl: '16px', '2xl': '24px', '3xl': '32px' } }],
|
|
562
|
+
],
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/** The dial values a caller may pass; `null`/absent means "do not engage". */
|
|
566
|
+
export interface Dials {
|
|
567
|
+
variance?: number | null;
|
|
568
|
+
motion?: number | null;
|
|
569
|
+
density?: number | null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Bucket a 1-10 dial value into its tier config, or `null` when unset.
|
|
574
|
+
*
|
|
575
|
+
* Out-of-range input is clamped rather than rejected: the dial is a coarse
|
|
576
|
+
* preference, and refusing an 11 would turn a typo into a failed run for no
|
|
577
|
+
* gain in meaning.
|
|
578
|
+
*/
|
|
579
|
+
export function resolve_dial(dial_name: string, value: number | null | undefined): ResultDict | null {
|
|
580
|
+
if (value === null || value === undefined || Number.isNaN(value)) return null;
|
|
581
|
+
const tiers = DIAL_TIERS[dial_name];
|
|
582
|
+
if (tiers === undefined) return null;
|
|
583
|
+
const v = Math.max(1, Math.min(10, Math.trunc(value)));
|
|
584
|
+
for (const [lo, hi, info] of tiers) {
|
|
585
|
+
if (v >= lo && v <= hi) return { ...info, value: v };
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
534
590
|
/**
|
|
535
591
|
* Conditional grounding: category → rules → planned multi-domain search.
|
|
536
592
|
*
|
|
537
593
|
* Returns the interface-v1 grounded dict (see Python docstring). Async because
|
|
538
594
|
* `_load_rules_callable` is async (dynamic import).
|
|
595
|
+
*
|
|
596
|
+
* `dials` is optional and additive: with none passed, the returned dict is
|
|
597
|
+
* exactly what it was before dials existed.
|
|
539
598
|
*/
|
|
540
599
|
export async function ground(
|
|
541
600
|
manifest: Manifest,
|
|
542
601
|
query: string,
|
|
543
602
|
context: Record<string, unknown> | null = null,
|
|
603
|
+
dials: Dials | null = null,
|
|
544
604
|
): Promise<ResultDict> {
|
|
545
605
|
const reasoning = manifest.reasoning as Record<string, unknown> | null | undefined;
|
|
546
606
|
if (!reasoning) {
|
|
@@ -607,6 +667,29 @@ export async function ground(
|
|
|
607
667
|
.filter((s) => s.length > 0);
|
|
608
668
|
}
|
|
609
669
|
|
|
670
|
+
// 4b — dials.
|
|
671
|
+
//
|
|
672
|
+
// Variance biases SELECTION, never RETRIEVAL, and never silently.
|
|
673
|
+
//
|
|
674
|
+
// An earlier version prepended the dial's keywords to `priority` outright.
|
|
675
|
+
// That looked like "biasing" and was not: `priority.slice(0, 2)` augments
|
|
676
|
+
// the query, so the rule's own corpus-grounded keywords were pushed out of
|
|
677
|
+
// the retrieval entirely, and five dial keywords then outscored two rule
|
|
678
|
+
// keywords in the match. The dial did not bias the evidence — it replaced
|
|
679
|
+
// it, and nothing said so.
|
|
680
|
+
//
|
|
681
|
+
// Split instead. `priority` stays the rule's, so what comes BACK from the
|
|
682
|
+
// corpus is unchanged by the dial. `selection_priority` carries the dial in
|
|
683
|
+
// front for choosing among those rows — which is what a preference dial is
|
|
684
|
+
// legitimately for. And when that choice differs from the one the rule's own
|
|
685
|
+
// keywords would have made, it is reported rather than left invisible.
|
|
686
|
+
const variance_info = resolve_dial('variance', dials?.variance);
|
|
687
|
+
const motion_info = resolve_dial('motion', dials?.motion);
|
|
688
|
+
const density_info = resolve_dial('density', dials?.density);
|
|
689
|
+
const selection_priority = variance_info
|
|
690
|
+
? [...(variance_info.style_keywords as string[]), ...priority]
|
|
691
|
+
: priority;
|
|
692
|
+
|
|
610
693
|
// 5 — planned multi-domain search.
|
|
611
694
|
const selections: ResultDict = {};
|
|
612
695
|
const domain_confidences: number[] = [];
|
|
@@ -621,12 +704,28 @@ export async function ground(
|
|
|
621
704
|
}
|
|
622
705
|
const result = search_domain(manifest, q, domain_name, _int(max_results));
|
|
623
706
|
const results = (result.results as Row[] | null) || [];
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
707
|
+
let best: Row;
|
|
708
|
+
if (domain_name === priority_domain) {
|
|
709
|
+
best = _select_best_match(results, selection_priority, name_cols[domain_name]);
|
|
710
|
+
if (variance_info) {
|
|
711
|
+
// What the rule alone would have picked from the SAME rows.
|
|
712
|
+
// Reporting the divergence is the whole difference between a
|
|
713
|
+
// dial that biases and a dial that quietly overrules.
|
|
714
|
+
const ungated = _select_best_match(results, priority, name_cols[domain_name]);
|
|
715
|
+
if (ungated !== best) {
|
|
716
|
+
const col = name_cols[domain_name];
|
|
717
|
+
const nameOf = (r: Row): string =>
|
|
718
|
+
col && r[col] !== undefined ? pyStr(r[col]) : '(unnamed)';
|
|
719
|
+
gaps.push(
|
|
720
|
+
`--variance ${String(variance_info.value)} changed the ${domain_name} pick to ` +
|
|
721
|
+
`'${nameOf(best)}'; the rule's own keywords would have selected ` +
|
|
722
|
+
`'${nameOf(ungated)}' — the dial biased the choice, it did not come from the corpus`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
best = results.length > 0 ? (results[0] as Row) : {};
|
|
728
|
+
}
|
|
630
729
|
selections[domain_name] = {
|
|
631
730
|
best,
|
|
632
731
|
// Python: [r for r in results if r is not best] — identity compare.
|
|
@@ -641,6 +740,53 @@ export async function ground(
|
|
|
641
740
|
domain_confidences.push(score instanceof PyFloat ? score.value : (score as number | undefined) ?? 0.0);
|
|
642
741
|
}
|
|
643
742
|
|
|
743
|
+
// 5b — the motion dial reads a corpus, which is why it needs `motion.csv`
|
|
744
|
+
// and the other two do not. Rows are filtered by the `Intensity Tier`
|
|
745
|
+
// column; a tier with no rows yields an evidence gap rather than a silent
|
|
746
|
+
// empty block, because "the dial did nothing" and "the corpus has nothing
|
|
747
|
+
// at this tier" are different facts to the reader.
|
|
748
|
+
let motion_selection: ResultDict | null = null;
|
|
749
|
+
if (motion_info) {
|
|
750
|
+
const tier = motion_info.tier as string;
|
|
751
|
+
const motion_domain = (manifest.domains as Record<string, ResultDict>)['gsap'];
|
|
752
|
+
if (motion_domain === undefined) {
|
|
753
|
+
gaps.push(
|
|
754
|
+
"the --motion dial needs a 'gsap' search domain (motion.csv); this manifest has none, so the dial had no effect",
|
|
755
|
+
);
|
|
756
|
+
} else {
|
|
757
|
+
// FILTER FIRST, then rank. Retrieving the global top-N and filtering
|
|
758
|
+
// afterwards drops rows at the requested tier that lost the global
|
|
759
|
+
// cut to rows of another tier — the dial then silently returns less
|
|
760
|
+
// than the corpus holds for it. `search_domain` already merges a
|
|
761
|
+
// filters dict (it is what the `--filter` flag uses), and the row
|
|
762
|
+
// count comes from the manifest like every other domain rather than
|
|
763
|
+
// from a literal here.
|
|
764
|
+
const motion_max = motion_domain['max_results'];
|
|
765
|
+
const motion_result = search_domain(
|
|
766
|
+
manifest,
|
|
767
|
+
query,
|
|
768
|
+
'gsap',
|
|
769
|
+
typeof motion_max === 'number' ? motion_max : null,
|
|
770
|
+
{ 'Intensity Tier': tier },
|
|
771
|
+
);
|
|
772
|
+
const rows = (motion_result.results as Row[] | null) || [];
|
|
773
|
+
if (rows.length === 0) {
|
|
774
|
+
// No block at all, not an empty one: `motion: {best: {}}` reads
|
|
775
|
+
// as "the dial ran and found nothing worth showing", which is
|
|
776
|
+
// indistinguishable from a corpus that has nothing at this tier.
|
|
777
|
+
// The gap says which, and the absent key says the rest.
|
|
778
|
+
gaps.push(`no motion row at tier '${tier}' matched the query — the dial narrowed to nothing`);
|
|
779
|
+
} else {
|
|
780
|
+
motion_selection = {
|
|
781
|
+
tier,
|
|
782
|
+
best: rows[0] as Row,
|
|
783
|
+
alternatives: rows.slice(1),
|
|
784
|
+
confidence: motion_result.confidence,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
644
790
|
// 6 — aggregate confidence (weakest link wins).
|
|
645
791
|
const numeric = domain_confidences.length > 0 ? pyRound(Math.min(...domain_confidences), 3) : 0.0;
|
|
646
792
|
const label = numeric >= 0.4 ? 'high' : numeric >= 0.15 ? 'medium' : 'low';
|
|
@@ -653,7 +799,7 @@ export async function ground(
|
|
|
653
799
|
}
|
|
654
800
|
}
|
|
655
801
|
|
|
656
|
-
|
|
802
|
+
const out: ResultDict = {
|
|
657
803
|
domain: manifest.domain,
|
|
658
804
|
query,
|
|
659
805
|
category,
|
|
@@ -664,6 +810,24 @@ export async function ground(
|
|
|
664
810
|
evidence_gap:
|
|
665
811
|
gaps.length > 0 ? gaps : ['none — every planned domain returned a scored match'],
|
|
666
812
|
};
|
|
813
|
+
|
|
814
|
+
// Dial output is ADDITIVE and omitted entirely when no dial was passed, so
|
|
815
|
+
// the no-flag result stays byte-identical to what it was before dials
|
|
816
|
+
// existed. `spacing_scale` is a top-level override because it replaces a
|
|
817
|
+
// scale rather than informing one.
|
|
818
|
+
if (variance_info || motion_info || density_info) {
|
|
819
|
+
out.dials = {
|
|
820
|
+
variance: variance_info ? variance_info.value : null,
|
|
821
|
+
variance_label: variance_info ? variance_info.label : null,
|
|
822
|
+
motion: motion_info ? motion_info.value : null,
|
|
823
|
+
motion_label: motion_info ? motion_info.label : null,
|
|
824
|
+
density: density_info ? density_info.value : null,
|
|
825
|
+
density_label: density_info ? density_info.label : null,
|
|
826
|
+
};
|
|
827
|
+
if (motion_selection) out.motion = motion_selection;
|
|
828
|
+
if (density_info) out.spacing_scale = density_info.spacing;
|
|
829
|
+
}
|
|
830
|
+
return out;
|
|
667
831
|
}
|
|
668
832
|
|
|
669
833
|
/** Python int(x) for a plan value (int or numeric string). */
|
|
@@ -745,6 +909,50 @@ export function _render_markdown(grounded: ResultDict, master = false): string {
|
|
|
745
909
|
`- **Confidence:** ${pyStr(conf.label ?? '?')} ` + `(${_confScore(conf.score)})`,
|
|
746
910
|
'',
|
|
747
911
|
);
|
|
912
|
+
|
|
913
|
+
// Dials render HERE, not only under --json. This is the whole consumption
|
|
914
|
+
// path for two of the three output paths: the default text render and the
|
|
915
|
+
// persisted MASTER.md both come through this function, and `--density`'s
|
|
916
|
+
// only payload is `spacing_scale` — omitted here, that dial is inert
|
|
917
|
+
// everywhere a human actually reads the result.
|
|
918
|
+
const dials = (grounded.dials as ResultDict | null) || null;
|
|
919
|
+
if (dials) {
|
|
920
|
+
const dialLines: string[] = [];
|
|
921
|
+
for (const name of ['variance', 'motion', 'density']) {
|
|
922
|
+
const value = dials[name];
|
|
923
|
+
if (value === null || value === undefined) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const label = dials[`${name}_label`];
|
|
927
|
+
dialLines.push(
|
|
928
|
+
`- **${name[0]?.toUpperCase()}${name.slice(1)}:** ${pyStr(value)}/10` +
|
|
929
|
+
(label ? ` — ${pyStr(label)}` : ''),
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
if (dialLines.length > 0) {
|
|
933
|
+
lines.push('### Dials', '', ...dialLines, '');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const spacing = (grounded.spacing_scale as ResultDict | null) || null;
|
|
938
|
+
if (spacing && Object.keys(spacing).length > 0) {
|
|
939
|
+
lines.push('### Spacing scale (density override)', '');
|
|
940
|
+
for (const step of Object.keys(spacing)) {
|
|
941
|
+
lines.push(`- **${step}:** ${pyStr(spacing[step])}`);
|
|
942
|
+
}
|
|
943
|
+
lines.push('');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const motion = (grounded.motion as ResultDict | null) || null;
|
|
947
|
+
const motionBest = motion ? ((motion.best as Row | null) || {}) : {};
|
|
948
|
+
if (motion && Object.keys(motionBest).length > 0) {
|
|
949
|
+
lines.push(`### Motion (tier: ${pyStr(motion.tier ?? '')})`, '');
|
|
950
|
+
for (const key of Object.keys(motionBest)) {
|
|
951
|
+
lines.push(`- **${key}:** ${pyStr(motionBest[key])}`);
|
|
952
|
+
}
|
|
953
|
+
lines.push('');
|
|
954
|
+
}
|
|
955
|
+
|
|
748
956
|
const selections = (grounded.selections as ResultDict | null) || {};
|
|
749
957
|
for (const domain of Object.keys(selections)) {
|
|
750
958
|
const sel = (selections[domain] as ResultDict | null) || {};
|
|
@@ -125,6 +125,28 @@ interface Args {
|
|
|
125
125
|
persist?: string | null;
|
|
126
126
|
project_name?: string | null;
|
|
127
127
|
page?: string | null;
|
|
128
|
+
/** Optional 1-10 design dials; `null` means "not passed", never a default. */
|
|
129
|
+
variance?: number | null;
|
|
130
|
+
motion?: number | null;
|
|
131
|
+
density?: number | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse a 1-10 dial value.
|
|
136
|
+
*
|
|
137
|
+
* A non-numeric value is rejected rather than clamped: `--motion high` is a
|
|
138
|
+
* mistake the caller can fix, and silently reading it as "no dial" would run
|
|
139
|
+
* the whole grounding pass and return a result that quietly ignored the flag.
|
|
140
|
+
* Out-of-RANGE numbers are a different case and are clamped downstream — 11 is
|
|
141
|
+
* a typo with an obvious intent, `high` is not a number at all.
|
|
142
|
+
*/
|
|
143
|
+
function _dial(flag: string, raw: string): number {
|
|
144
|
+
const n = Number(raw);
|
|
145
|
+
if (!Number.isFinite(n)) {
|
|
146
|
+
process.stderr.write(`argument ${flag}: expected an integer 1-10, got '${raw}'\n`);
|
|
147
|
+
process.exit(2);
|
|
148
|
+
}
|
|
149
|
+
return Math.trunc(n);
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
/**
|
|
@@ -212,6 +234,18 @@ function _parseArgs(argv: string[]): Args {
|
|
|
212
234
|
args.page = eat();
|
|
213
235
|
} else if (a.startsWith('--page=')) {
|
|
214
236
|
args.page = a.slice('--page='.length);
|
|
237
|
+
} else if (a === '--variance') {
|
|
238
|
+
args.variance = _dial('--variance', eat());
|
|
239
|
+
} else if (a.startsWith('--variance=')) {
|
|
240
|
+
args.variance = _dial('--variance', a.slice('--variance='.length));
|
|
241
|
+
} else if (a === '--motion') {
|
|
242
|
+
args.motion = _dial('--motion', eat());
|
|
243
|
+
} else if (a.startsWith('--motion=')) {
|
|
244
|
+
args.motion = _dial('--motion', a.slice('--motion='.length));
|
|
245
|
+
} else if (a === '--density') {
|
|
246
|
+
args.density = _dial('--density', eat());
|
|
247
|
+
} else if (a.startsWith('--density=')) {
|
|
248
|
+
args.density = _dial('--density', a.slice('--density='.length));
|
|
215
249
|
} else {
|
|
216
250
|
positionals.push(a);
|
|
217
251
|
}
|
|
@@ -293,7 +327,11 @@ export async function main(argv: string[] | null = null): Promise<number> {
|
|
|
293
327
|
|
|
294
328
|
// ground
|
|
295
329
|
const context = args.context ? (JSON.parse(args.context) as Record<string, unknown>) : {};
|
|
296
|
-
const grounded = await run_ground(manifest, args.query as string, context
|
|
330
|
+
const grounded = await run_ground(manifest, args.query as string, context, {
|
|
331
|
+
variance: args.variance ?? null,
|
|
332
|
+
motion: args.motion ?? null,
|
|
333
|
+
density: args.density ?? null,
|
|
334
|
+
});
|
|
297
335
|
if (args.persist) {
|
|
298
336
|
const info = persist_grounding(
|
|
299
337
|
grounded,
|
|
@@ -8,9 +8,26 @@ upstream repository below. Keep this file shipping with the skill; update the
|
|
|
8
8
|
|
|
9
9
|
- Repository: `https://github.com/nextlevelbuilder/ui-ux-pro-max-skill`
|
|
10
10
|
- Commit SHA: `b7e3af80f6e331f6fb456667b82b12cade7c9d35`
|
|
11
|
-
- Last checked: 2026-
|
|
11
|
+
- Last checked: 2026-08-13
|
|
12
12
|
- Upstream version: `skill.json` v2.5.x (npm `uipro-cli`)
|
|
13
13
|
|
|
14
|
+
### Partial adoptions from a later upstream commit
|
|
15
|
+
|
|
16
|
+
The base pin above still describes the corpus as a whole. Two assets were
|
|
17
|
+
adopted **selectively** from `97eb2a20f...` on 2026-08-13, and the base pin is
|
|
18
|
+
deliberately NOT bumped: at that commit nine files are absent from our `data/`
|
|
19
|
+
and eleven more have drifted, so claiming the newer SHA wholesale would assert
|
|
20
|
+
an adoption that did not happen (six of the absent files are desktop-UI stacks
|
|
21
|
+
outside this pack's scope, and `google-fonts.csv` is declined by ADR-061 § 8).
|
|
22
|
+
|
|
23
|
+
| Asset | From | Why |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `data/motion.csv` (16 rows) | `97eb2a20` | Consumed by the `gsap` search domain and by the `--motion` design dial, which filters its `Intensity Tier` column. Without it the dial is inert. |
|
|
26
|
+
| The three design dials (`variance` / `motion` / `density`) in `corpus-grounding/scripts/decision_engine.ts` + `ground.ts` | `97eb2a20`, `scripts/design_system.py` | Re-derived, not copied — see `provenance/borrows.jsonl`. |
|
|
27
|
+
|
|
28
|
+
Licence re-verified at `97eb2a20` before adopting: MIT, same copyright holder,
|
|
29
|
+
unchanged from the base pin.
|
|
30
|
+
|
|
14
31
|
## Licenses
|
|
15
32
|
|
|
16
33
|
### MIT — corpus + core engine (root `LICENSE`)
|
|
@@ -18,7 +35,7 @@ upstream repository below. Keep this file shipping with the skill; update the
|
|
|
18
35
|
Copyright (c) 2024 **Next Level Builder**. The tabular corpus
|
|
19
36
|
(`ui-reasoning`, `products`, `colors`, `styles`, `typography`, `charts`,
|
|
20
37
|
`landing`, `icons`, `ux-guidelines`, `react-performance`, `app-interface`,
|
|
21
|
-
`stacks/*`) and the Python engine sources (`core.py`, `search.py`,
|
|
38
|
+
`motion`, `stacks/*`) and the Python engine sources (`core.py`, `search.py`,
|
|
22
39
|
`design_system.py`) are MIT-licensed. Obligation: **retain this notice**.
|
|
23
40
|
|
|
24
41
|
### Apache-2.0 — `ui-styling`-derived material ("claudekit")
|