Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.
Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
* enhance(#4223): default-off interaction capture for gsd-ui-auditor via the chrome-devtools CLI
gsd-ui-auditor is chartered to audit interaction and handed a capture
driver with no interaction verb: `npx playwright screenshot` cannot
click, fill, hover, press or snapshot, so a hover state, an open menu,
a focus ring or a form's validation state never appears in its
evidence and every Experience Design finding degrades to code reading.
Implements the shape approved at triage, not a new capability:
- capabilities/ui/capability.json declares `workflow.ui_interaction_capture`
(boolean, default false) on the capability that already owns the
auditor (ADR-894 one-owner invariant); capability-registry.cjs regenerated.
- gsd-core/workflows/ui-review.md reads the key through gsd_run and hands
it to the auditor as `interaction_capture:` in the spawn <config> block —
the auditor carries no gsd_run resolver, so the key travels by value.
- agents/gsd-ui-auditor.md gains an anchored interaction-capture section
AFTER the static block. With the key on and a Chrome binary resolved it
starts the `chrome-devtools` CLI (chrome-devtools-mcp, floor ^1.8.0) on
an --isolated profile, opens the dev URL the static block reached,
takes the a11y snapshot for element uids, captures the baseline and a
Tab focus-ring state, drives the UI-SPEC's interactive components, saves
console output, and stops the daemon unconditionally. Key off, no dev
server, or no Chrome: one status line, and the Playwright-only static
path runs exactly as before — the static fence is untouched.
Needs only Bash: no MCP server, no tools: change. Chromium-only by
nature; Firefox/WebKit stay on Playwright. `wait_for` is MCP-only, so
readiness is polled through evaluate_script.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* test(#4223): bind the interaction-capture shape and containment
- manifest, generated registry, config schema and config-set/loadConfig
all know workflow.ui_interaction_capture as a default-off boolean, and
hand-written non-booleans fall to the slice default
- the orchestrator reads the key and hands it down; the auditor never
grows a gsd_run dependency
- the static fence stays Playwright-only and the interaction fence
chrome-devtools-only, so key-off is today's path
- the interaction fence runs under bash with a stub driver on PATH: key
off / absent / no dev server / no Chrome invoke nothing; the happy path
starts first and stops last on the [selected] pageId with the documented
flags; a failed capture is removed and not counted; new_page and start
failures still honour the stop-only-if-started rule; CHROME_BIN and
CHROME_DEVTOOLS_MCP_VERSION overrides flow through
- docs/CONFIGURATION.md row shape; registered in the docs-guard lane
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* docs(#4223): document workflow.ui_interaction_capture and its how-to
- docs/CONFIGURATION.md: one row in the workflow.* table, default-off
- docs/AGENTS.md: the gsd-ui-auditor entry names the key and what the
interaction-capture section adds, skips and never claims
- docs/how-to/enable-ui-interaction-capture.md: turn it on, read the
`**Interaction captures:**` outcomes, what it does not do, turn it off
- docs/README.md: index the how-to beside live-DOM verification
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* chore(#4223): add changeset
Added-type fragment; pr: carries the issue number until the PR exists.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* enhance(#4223): use the /gsd:ui-review namespace form in the auditor's prose
Claude-facing source (agents/, workflows/) uses the /gsd:<cmd> namespace;
the hyphen form is retired there and the slash-command-namespace guard
rejects it. docs/ keep the hyphen form by convention.
Emitted-Drift-Ack-Growth: gsd-ui-auditor.md — #4223: the anchored default-off interaction-capture section (prose + one bash fence) appended after the static Playwright block inside <screenshot_approach>, plus one `**Interaction captures:**` line in each of the two report templates, one completion-checklist line and one Step-3 sentence. The static fence is byte-identical to next; nothing was removed or reordered.
Emitted-Drift-Ack-Growth: ui-review.md — #4223: a two-line config-get read + true/false normalisation in step 0 and one `interaction_capture:` line in the spawn <config> block with a three-line note on why the value travels by prompt. No step, gate, or dispatch shape changed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* enhance(#4223): per-run daemon session, bounded navigation, and step failures that count
Three findings from the pre-file adversarial review of the interaction
fence, folded in:
- `--sessionId <epoch>-<pid>` on every driver call. `start` restarts
whatever daemon shares its session and --isolated isolates only the
browser profile, so two concurrent audits — or an audit beside the
operator's own CLI daemon — would otherwise stop each other. The CLI
accepts hex and dashes only; the id is validated by the test stub.
- `new_page --timeout 30000`: the one verb that takes a bound, placed
before every verb that does not, so a hung page is caught first.
- a failed take_snapshot or press_key now increments the failure count
and is named on stdout; two clean screenshots can no longer read as
`0 failed` after the step that gives the interactions their uids failed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* enhance(#4223): subshell-unique session id, CRLF-safe page-id parse, stale-snapshot removal
Second review round, both reviewers:
- session id is `<epoch>-<BASHPID>-<RANDOM>`: `$$` is inherited by a
subshell, so two audits forked from one parent in the same second
shared an id and could stop each other's daemon (driven by the reviewer)
- `tr -d '\r'` before the `[selected]` parse so a CRLF-emitting driver
under Git Bash still matches the `$` anchor, and `|| true` on the
assignment so a failed new_page cannot abort the block under
`set -e -o pipefail` before the unconditional stop
- a failed take_snapshot removes any snapshot.txt it left or inherited
from a reused directory, so stale uids never drive the interactions
- `<config>` placeholder is `{interaction_capture}`, lowercase like its
`{phase_dir}` / `{padded_phase}` siblings — the block is a prompt
template, not a bash heredoc
- how-to: the `not captured` row no longer claims the daemon started
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* enhance(#4223): check new_page's exit status before parsing its output; regression cases for the edges
Third review round:
- a new_page that prints a page line and then exits non-zero is a failed
navigation, not a page id: the exit status is checked in an `if` before
the output is parsed (driven by the reviewer against the previous
`|| true`, which masked exactly that)
- regression cases for what the last two rounds added: CRLF driver
output, a stale snapshot removed on failure, partial-output new_page
failure, and the whole fence under `set -e -o pipefail` (both the
failed-navigation path and the happy path)
- the harness whitelist gains `date`; the session-id assertion now
requires all three parts, so a silently empty epoch cannot hide again
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* enhance(#4223): keep gsd-ui-auditor under the DEFAULT-tier size cap; changeset pr placeholder
- the three review folds pushed agents/gsd-ui-auditor.md to 25179 bytes,
over the 24576-byte hard cap tests/agent-size-budget.test.cjs enforces;
the interaction section's comments are tightened to the same content
in fewer bytes (23559 now). No bash changed — the fence's own tests and
the real-browser run are unchanged.
- .changeset/vivid-yaks-fly.md carries the policy placeholder `pr: 0`, which
the post-create backfill rewrites to the PR's own number.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* chore(#4223): set changeset fragment pr to 4477
* test(#4223): compare the fence's status path with the separator the fence uses
On the windows-latest lane the happy-path case failed on `\interaction` vs
`/interaction` alone: the fence joins "$SCREENSHOT_DIR/interaction" with a
literal slash, and the assertion built its expectation with path.join. Every
other case in the file passed on that lane, including the CRLF and
errexit/pipefail ones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanfY8KaLb4RVVubcUoGP6
* test(#4223): drop the inert file-header allow-test-rule marker
Review round 1 on #4477: the `source-text-is-the-product` marker sat at
line 2, outside no-source-grep's 8-line lookahead of every readFileSync
site (the first is ~60 lines down), so it suppressed nothing. It was also
unnecessary: every read in this file is a .md/.json path, which the rule
does not trigger on. Deleted rather than relocated — there is no site to
relocate it to. Negative control: `eslint` on the file is clean without it.
* chore(#4223): regenerate the platform-conformance-tier lists for the new test
Review round 3 on #4477. `next` gained chore(#4591)'s platform-conformance-tier
gate after this branch opened; its two committed lists must name every file
under tests/, and this PR's tests/ui-interaction-capture.test.cjs had never
been in them. Once the branch was updated against next the lists were stale
and three jobs went red on head 575667dd: lint-tests (gen-platform-conformance-tier
--check), conformance test (macos-latest) at 546 !== 547, and shard 1/3's
fragment-single-edit-propagation, which sees the same staleness as regen:derived
touching files beyond the fragment edit under test.
Regenerated with the repo's own generators, no hand-editing. The general tier
goes 546 -> 547 and the macOS tier 196 -> 197, each by exactly this one entry;
both --check arms are clean. Verified the red is this PR's own file and not
base drift: at upstream/next both generators report "list matches" (546 / 196),
and our committed copies were byte-identical to next's before this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CBQTeGX1JYHF5DRWp4wvZ
* fix(#4223): bound, confine and trap the chrome-devtools driver fence
Round 4 — three findings in one fence, interleaved on the same lines, so one
commit:
- Every driver call is time-bounded. `cdt <ceiling> <verb>` runs the client as a
background job in its own process group (`set -m`) under a watchdog that kills
the whole group at the ceiling — TERM, then KILL two seconds later. One pid is
not enough: npm forwards SIGTERM only to its direct child, so killing `npx`
alone leaves the client holding the fence's stdout and a `$(cdt … new_page)`
capture blocked past the ceiling (driven against a real npx tree by the
round's adversarial review; the pid-only first cut of this commit had exactly
that hole). The watchdog is an exec'd bash (`"$BASH" -c`), never a `( … )`
subshell: a subshell inherits bash's saved copies of the caller's stdio (the
fds ≥10 a function-level `>/dev/null` redirect leaves behind) and holds them
open, so a runner waiting for EOF waited out the whole 60 s ceiling whenever a
watchdog outlived its kill — measured as the intermittent 30 s test run the
review flagged; 0/60 after. It polls the job's process GROUP (`kill -0 --
-pgid`, every 0.1 s) and stands down by itself once the group is empty;
nothing ever signals it. The group, not the leader pid: a child can outlive
the leader while holding the `$(cdt … new_page)` pipe, and a leader-pid poll
stood down at once and left the substitution open-ended (driven by the
round's adversarial review at 6× the ceiling; a pgid cannot be reused while
any member lives, which a bare pid can). The daemon `start` launches is
spawned detached (its own session) and never in that group. Two platforms
forced the never-signalled shape. Under bash 3.2.57 the earlier `trap … TERM;
sleep & wait $!` form ignored its TERM in 3 of 300 fast calls and slept out
the whole ceiling — CI's macos job hanging 30 s right after `start`. On Git
Bash a signal to a watchdog still starting up hung the fence's `wait` for it:
18 of 20 fence tests at the harness's 30 s cap in 3 of 3 full-file runs,
while a fence slowed by xtrace, or three tests run alone, never hit it (a
startup race; the mechanism is not pinned further). Polling: 0/300 slow calls
and 0 orphaned sleeps under 3.2.57 and 5.2, the fence suite 20/20 in 3 of 3
full-file runs on Git Bash 5.2.37 (fractional `sleep 0.1`: driven on GNU,
msys and busybox sleep; BSD sleep documents it). A clock that cannot launch
(`sleep … || exit 0`) stands the watchdog down rather than firing at once
and killing a healthy call — by design that leaves a hung call unbounded,
the pre-round-4 behaviour, instead of failing a healthy one. A hung call
returns once its group is gone: at the ceiling, plus up to the 2 s
TERM-to-KILL grace. The KILL after the grace is sent only to a group that
is still alive: a pgid freed during the grace can be reused, and an
unconditional KILL could hit an unrelated group (the round's review).
`start` (npx fetch + Chrome launch) gets CHROME_DEVTOOLS_START_TIMEOUT
(180 s), every verb CHROME_DEVTOOLS_STEP_TIMEOUT (60 s). timeout(1) is absent
on macOS and this agent carries no gsd-tools resolver, hence a bash watchdog
rather than either.
- --allowUnrestrictedPaths -> --workspace "$INTERACTION_DIR": the driver may
write under the run's interaction/ directory and nowhere else. Relative, like
every --filePath (unchanged from rounds 1-3): the daemon resolves both against
one cwd (chrome-devtools-mcp 1.9.0 spawns it with cwd: process.cwd() and
path.resolve()s both), and a relative path needs no dialect translation — an
absolute `pwd -P` path is an msys path on Git Bash, which a Windows-native
daemon cannot resolve (CI's windows conformance shard caught the first cut).
--workspace is a 1.9.0 flag (absent from 1.8.0's `start --help`, verified),
so the documented floor moves from ^1.8.0 to ^1.9.0, where
--allowUnrestrictedPaths is deprecated.
- `stop` is owed by an EXIT trap after a successful `start`, not by position
(it replaces any earlier EXIT trap — none exists in this file); the explicit
call keeps it in order, a flag makes the trap a no-op afterwards, and only the
shell that installed the trap may act: a subshell copy of the fence state
carries CDT_STARTED=1 and, under a timing race CI's ubuntu job hit (reproduced
locally at 3/40 under load: the second `stop` came from a subshell pid, never
main), issued a second `stop`. The identity is `$(exec /bin/sh -c 'echo
"$PPID"')`, not $BASHPID — macOS ships bash 3.2, where BASHPID does not exist
and CI's macos conformance job showed the guard comparing empty to empty. The
fence was driven under bash 3.2.57 for the injected-subshell, errexit
failed-new_page, errexit failed-resize, hung-start, hung-new_page and happy
paths. A failed resize_page is a counted failed step now, not the one bare
command an errexit runner could abort on.
Prose in the section is tightened to pay for the mechanism: 23559 -> 24517
bytes against the 24576 DEFAULT-tier cap.
Tests: the stub driver hangs as a real child tree (sh waiting on a child that
holds stdout — never an exec), so a pid-only kill fails the new
aHungNewPageWhoseChildHoldsStdoutIsStillCutOffAtTheCeiling test (negative-
controlled: it blocks for the harness's whole cap on the old wrapper). A hung
start and a hung capture are cut off within ceiling + grace + slack and still
reach stop; an injected bare failure under errexit reaches stop through the
trap, exactly once; an injected subshell call of cdt_stop issues nothing; the
happy path issues exactly one stop; every driver call site names a ceiling and
the only bare $CDT is the wrapper's own spawn; the start line carries
--workspace with the capture directory, every --filePath lies under it, and no
code line carries --allowUnrestrictedPaths. A driver whose leader exits at
once while a child keeps holding the capture pipe is still cut off at the
ceiling (negative-controlled: a leader-pid poll blocks for the harness's whole
cap). A watchdog whose clock cannot launch leaves a 300 ms driver call alone
(negative-controlled: the trap form kills `start` in under 20 ms). The harness
EXPORTS its stub-only PATH — unexported, the exec'd
watchdog fell through to bash's compiled-in default PATH and never saw the stub
dir — and ships `sleep` there as an exec-wrapper script (portable to Git Bash,
pid-preserving).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgNMb2G67rAJfFQRHEBTAj
* fix(#4223): gitignore gate covers the capture directory, and upgrades an existing file
Round 4 Blocker. The gate enumerated image extensions, so snapshot.txt (the
accessibility tree, with entered form values) and console.txt (which can carry
tokens) were committable by `git add .`. The gate now ignores `interaction/` as
a directory — the next artifact type is covered by construction — and it appends
whatever an existing .gitignore lacks instead of writing once. The write-once
form was the same defect one step later: every project that had already run an
audit would never have received the new pattern at all.
Tests run the gate fence under bash: a fresh file carries every pattern; an
image-only file from an earlier audit gains interaction/ and keeps its own
header without duplicating present lines; a second run appends nothing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgNMb2G67rAJfFQRHEBTAj
* test(#4223): declare the interaction-capture anchor as a comment marker
The #4324 colon-token gate (slash-command-namespace) landed on next after this
branch was opened and reads `<!-- gsd:ui-interaction-capture -->` as an
unconvertible /gsd: command token. It is a section anchor of the same family as
gsd:live-dom-families and gsd:write-continue, so it is declared in
COMMENT_MARKER_TOKENS rather than renamed. Found by running the base-added
gates against the merged tree; CI at ca8d2508 predates the gate.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgNMb2G67rAJfFQRHEBTAj
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
Co-authored-by: CI Rebase Check <ci@gsd-redux>
* fix(release): finalize job calls sync-next-version.cjs to bump next after final release (#2423)
The release pipeline's 'finalize' job shipped X.Y.0 to npm 'latest' and
merged to main, but never bumped 'next' to match. 'scripts/sync-next-version.cjs'
exists exactly for this — its docstring promises to run 'for every release
type (rc / hotfix / final)' — but it was wired only into the 'rc' job
(release.yml:479), not 'finalize'. As a result, after 1.7.0 shipped on
2026-07-15, 'next' stayed at 1.7.0-rc.6 and every npm script banner on
'next' (and feature branches cut from it) reported the stale rc version.
Regression of #1104 — closed incomplete (covered rc only, not final).
This patch:
- adds a 'Sync next branch to the published release' step to the
'finalize' job, mirroring the rc job's pattern at line 479 (uses
VERSION from inputs.version rather than PRE_VERSION from steps.prerelease,
since finalize does not run the prerelease step);
- gates it on !inputs.dry_run + continue-on-error:true (matches rc);
- adds tests/release-finalize-syncs-next-version.test.cjs — a structural
YAML assertion that fails against the pre-fix workflow and passes after.
Failing-first demonstrated during development; the test parses job blocks
by indentation rather than grep so it stays valid as the file grows.
Root-cause diagnosis: scripts/sync-next-version.cjs:14 docstring admits
'used by release.yml's rc job, which has no next-targeting PR of its own'.
release.yml:506-685 (finalize job) had no sync-next-version step before
this patch. Every prior rc.N release has a matching 'chore: sync next
package version to 1.7.0-rc.N' commit; there is no such commit for 1.7.0.
* chore(release): sync next package version to 1.7.0 (#2423)
Replays the canonical 'chore: sync next package version to <v>' commit
that the release pipeline's rc job auto-produces via scripts/sync-next-version.cjs,
for the 1.7.0 final release that shipped on 2026-07-15 (commit dd4c90f82
'chore: finalize v1.7.0' on main). Without this, 'next' (and every feature
branch cut from it) carried 1.7.0-rc.6 indefinitely and reported it in
every npm script banner (e.g. 'lint:ci').
Bumps 43 synchronized manifests via the npm 'version' lifecycle hook
(scripts/sync-manifest-versions.cjs --stage + scripts/gen-capability-registry.cjs
--write), matching the file set of commit 27f69cc48 ('chore: sync next
package version to 1.7.0-rc.6') and commit dd4c90f82 ('chore: finalize
v1.7.0').
This is the immediate Layer-1 repair for #2423. Layer-2 (prevent recurrence)
is the workflow patch in the previous commit; Layer-3 (regression test)
ships with it. Future X.Y.0 final releases will produce this commit
automatically once the workflow fix lands.
* test(release): tighten #2423 dry-run gate assertion to the sync step
Code review of fix/2423 found that test #3 ('gates sync-next-version on
!inputs.dry_run') asserted too loosely: it scanned the entire finalize
block for any '!inputs.dry_run' line, so it would still pass if the
gate were stripped from the sync-next-version step specifically — the
exact regression the test name promises to catch. The finalize job has
multiple steps with their own !inputs.dry_run gates (e.g. Verify
publish), so the loose version masked the very bug it claimed to detect.
Tighten by extracting the specific YAML step block containing
'scripts/sync-next-version.cjs' and asserting the gate appears within
THAT step's lines, not anywhere in the job. Verified the tightened test:
- PASSES against the post-fix workflow (sync step has its own gate)
- FAILS when the sync step's gate is stripped (even when other steps
in finalize retain their own !inputs.dry_run gates) — the exact
regression that previously slipped through
Adds extractStepBlockContaining(jobBlock, marker) helper alongside the
existing extractJobBlock(text, jobName). Reuses the same indentation-
based parsing, so it stays valid as the file grows.
* chore(changeset): backfill pr:2437 in .changeset/sturdy-ibex-jump.md
CLAUDE.md changeset convention: 'Use placeholder pr:0 during initial commit.
Backfill immediately after gh api POST /pulls returns the real number.'
PR #2437 created from branch fix/2423-release-finalize-sync-next-version.
* fix(#2423): add see #2423 to allow-test-rule exemption per ADR-456
CI lint-allow-test-rule-refs failed on PR #2437: ADR-456 requires new
allow-test-rule exemptions added after the ADR's acceptance to include a
tracking issue number in the comment, in the form
// allow-test-rule: <reason> (see #NNN)
The exemption added in commit 976c8b0a2 lacked this ref. Fixed.
Verified locally:
node scripts/lint-allow-test-rule-refs.cjs
→ ok lint-allow-test-rule-refs: 173 grandfathered exemption(s) tracked, no novel untracked offenders
* feat(#1430): versioned capability manifest + native stamping (ADR-1244 Phase 1)
Make the capability manifest versioned — the data substrate the Capability
Ecosystem (ADR-1244) keys off:
- capability.json gains a REQUIRED semver `version` plus the optional
ecosystem envelope (`engines.gsd`, `compatVersions`, `integrity`,
`provenance`); the build-time conformance validator enforces them via a new
`validateVersionEnvelope()` (exported for the Phase 2 runtime overlay).
- All 32 native capabilities stamped with `version` (= package version,
lockstep) + `engines.gsd`; `sync-manifest-versions.cjs` gains a glob sweep
that keeps them in sync, and the issue-844 regression guard is extended.
- Strict SemVer 2.0.0 grammar blocks metacharacter/space/unicode smuggling in
version strings; range/integrity fields are shape-validated (satisfaction
and the load-time gate are deferred to Phase 2/4).
- Capability rel-paths emitted forward-slash for cross-platform git correctness.
Closes#1430
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1430): add changeset for versioned capability manifest
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Replace plan-phase.md §5.6 (a 6-branch inline UI gate) with a capability-driven
loop.render-hooks plan:pre dispatch — the FIRST gate dispatch in any workflow.
A step (ui-phase, when:workflow.ui_phase) + a new blocking gate
(when:workflow.ui_safety_gate). New ui.plan-gate check verb returns
{frontend, hasUiSpec, block}; the dispatch runs it unconditionally then fires
the active step (pipeline) or halts on the active blocking gate (manual). The
gate-handling (run check.query; halt if blocking+block) is the reusable
phase-6 template for blocking-gate cutovers.
Config semantics fixed per #1022 + maintainer call: ui_phase gates plan-time
UI-SPEC generation, ui_safety_gate gates the planning block. Common case + all
ui_phase=false cases are equivalence-preserving; the one intended change is
{ui_phase:true, ui_safety_gate:false} now auto-generating in pipelines.
Review found it broken twice (non-generic dispatch, phase-lookup divergence,
then the step-only check nested in a gate loop) — fixed; final Codex pass
verified all 8 (ui_phase,ui_safety_gate)x{pipeline,manual} cases correct.
gsd-ui-phase skill + autonomous §3a.5 untouched (§3a.5 deferred).
getRoadmapPhaseWithFallback mirrors cmdRoadmapGetPhase for lookup parity.
Closes#1026
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Make install + surface read the registry's derived profileMembership/
capabilityClusters so a capability's tier drives what installs + surfaces.
resolveProfile (when given the registry) unions capability skills for the
profiles its tier implies before the requires: closure; resolveSurface merges
capabilityClusters into the cluster map. bin/install.js, /gsd:surface, and the
capability-state resolver all thread the registry.
Shipped as a proven no-op: the UI capability is reconciled to tier:full (its
skills were full-only in the hand-authored profiles), so it contributes only to
the full profile (already the '*' sentinel) and core/standard are unchanged.
Equivalence tests prove resolveProfile/resolveSurface/listSurface/staging/
capability-state are identical with vs without the registry; the core-alias
staging path is verified equivalent (empty manifest → raw PROFILES.core).
Closes#949
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#896): Capability Registry generator + UI pilot (ADR-857 phase 3a-impl)
First phase-3 code: the Capability Registry generation pipeline, built against
the ADR-894 contract and NOT wired into the live loop (registry-only, per the
staged-cutover design).
- capabilities/ui/capability.json — the UI pilot (ADR-894 worked example): 2
skills, 2 agents, 3 config keys, 2 steps + 1 gate, with `when` activation.
- scripts/gen-capability-registry.cjs — --write/--check generator. Hand-rolled
schema validation (envelope + role-typed feature/runtime bodies + typed
steps/contributions/gates + when + gate-check variants); cross-capability
invariants (single ownership; requires exist+acyclic+tier-monotone; config-key
ownership exclusive, collision-vs-central as a pending-migration warning);
hooks validated against an inline LOOP_HOST_CONTRACT (3a-impl-2 swaps its
source to the generated-from-workflows contract); GLOBAL point-ordered
consumes-satisfiability; materialized byLoopPoint ordering (produces/consumes
topo-sort); emits gsd-core/bin/lib/capability-registry.cjs (role-partitioned
indexes + requiresClosure). Prototype-pollution guards (Object.create(null) +
inline literal key checks) + fragment.path traversal guard.
- gsd-core/bin/lib/capability-registry.cjs — committed generated artifact
(mirrors package-identity.cjs: script-generated, tracked, linted, regenerated
on build, drift-tested), wired via the new `gen:capability-registry` build step.
- tests/capability-registry.test.cjs — 72 tests: schema + invariant + hook +
ordering + adversarial (path-traversal, proto-pollution, runtime body,
self-consume, cycles, collisions) + committed-file staleness guard.
New-CLI-module checklist (INVENTORY 97->98, MANIFEST, ARCHITECTURE), CONTEXT.md
"Capability Registry" un-[Planned]'d. Nothing wired into install/surface/loop.
Gates: lint, code-review (4 bugs fixed), security-review (path-traversal +
prototype-pollution fixed), codex adversarial-review ×3 (8+ findings fixed,
confirmed sound), clean-build docker 13190 pass / 0 fail.
Closes#896
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#896): CRLF-agnostic --check for capability-registry staleness (Windows)
The committed capability-registry.cjs staleness guard failed on Windows CI only:
git checks out the committed .cjs as CRLF (autocrlf, no .gitattributes) while the
generator emits LF, so the byte-for-byte --check comparison mismatched. Normalize
line endings on both sides of the --check comparison (no .gitattributes change,
no change to the LF the generator writes). Adds a regression test simulating the
Windows CRLF checkout.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>