T-LX04-activate-s0008-apps-adapter-validator
Status: closed/done · Impact: medium · Complexity: medium
S-0008-apps-consume-substrate-through-published-surfaces forbids apps/** and
packages/** from importing the substrate (plugin/lib/**) or the harness
surface (plugin/skills/**, plugin/.claude-plugin/**) directly — but it is
open/draft with no enforcement, so the rule is honor-system only. Now that
apps/ exists (T-CW4K), this task lands a deterministic import-discipline
validator and promotes S-0008 to open/active, turning a documented contract
into a pre-commit gate that blocks substrate-coupling imports with file:line
citations.
| Location | Role today |
|---|---|
docs/planning/standards/S-0008-apps-consume-substrate-through-published-surfaces.md | The standard, status: open/draft; its “How to apply” says to promote and name a validator “when the first application lands” — but no validator exists |
.claude/skills/project-check/ | Home of the deterministic check_*.ts gates; greps/scans for forbidden patterns and exits non-zero with file:line citations |
.claude/skills/project-check/check_no_legacy_script_paths.ts | The closest pattern to copy: regex-scans a file set per line, prints OK/FAIL per unit, exits 1 with file:line citations on any hit (2 on its own error) |
.claude/skills/project-check/tests/ | Sibling Bun test suite — one check_*.test.ts per gate, with fixtures/ dirs |
lefthook.yml#pre-commit | Wires each gate as a project-check-* command (bun run .claude/skills/project-check/check_*.ts); aborts on first non-zero exit, surfaces citations verbatim |
plugin/lib/model/ops/_update.ts | The entity-agnostic frontmatter-update op behind sdlc standard update; no dedicated standard activate verb exists — the open/draft → open/active flip is sdlc standard update --set '{"status":"open/active"}' |
apps/ is created by T-CW4K (apps/dashboard/); this validator scopes over
it alongside packages/**.
Proposed
Section titled “Proposed”A new check_apps_imports.ts gate under .claude/skills/project-check/ scans
every file under apps/** and packages/** for imports of the substrate
(plugin/lib/**, and src/** post-lift) or the harness surface
(plugin/skills/**, plugin/.claude-plugin/**). It prints OK/FAIL per
scanned file, exits non-zero with file:line citations on any forbidden
import, and is wired into lefthook.yml as a project-check-apps-imports
pre-commit command alongside the existing gates. A sibling
check_apps_imports.test.ts with fixtures covers a violating file and a
compliant file. S-0008 is flipped to status: open/active and its “How to
apply” bullet names this validator.
Approach
Section titled “Approach”- Author
.claude/skills/project-check/check_apps_imports.ts, mirroringcheck_no_legacy_script_paths.ts(repo-root resolution viaimport.meta.url,pyResolve,splitlines,displayPath,OK/FAILper-unit output, exit0/1/2). It walksapps/**andpackages/**(skipnode_modules/,dist/,.git/) and line-scans source files (.ts/.tsx/.js/.jsx/.vue— Vue SFC<script>blocks carry imports too) for import/require/dynamic-import()statements whose specifier resolves into a forbidden tree. - Define the forbidden-specifier match: any
import/export … from,require(…), orimport(…)whose module string points atplugin/lib/**,plugin/skills/**,plugin/.claude-plugin/**, or top-levelsrc/**— whether written as a relative path that climbs into those trees or via a path alias (e.g.@lib/…). Resolve relative specifiers against the importing file before matching so an alias-free../../plugin/lib/xis caught; treat the@lib/alias (used across the substrate) as aplugin/lib/**reference. Emit one citation per hit:apps/.../file.ts:NN: apps/** may not import the substrate (plugin/lib/**) — reach it through a defined API (sdlc CLI / JSON API / packages/<eco> API library) per S-0008. - Add
check_apps_imports.test.tsunder.claude/skills/project-check/tests/with afixtures/dir holding a violating file (e.g. anapps/-shaped file importing@lib/…orplugin/lib/…) and a compliant file (shells the CLI / imports only apackages/<eco>API library). Assert exit1+ citation on the former, exit0on the latter, mirroringcheck_no_legacy_script_paths.test.ts. - Wire the gate into
lefthook.ymlpre-commit.commandsasproject-check-apps-imports:runningbun run .claude/skills/project-check/check_apps_imports.ts, placed alongside the otherproject-check-*gates with a comment citing S-0008. - Promote the standard:
bun run plugin/cli/sdlc.ts standard update S-0008-apps-consume-substrate-through-published-surfaces --set '{"status":"open/active"}', then Edit the “How to apply”open/draftbullet in the standard to state the rule is now enforced by.claude/skills/project-check/check_apps_imports.ts.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
.claude/skills/project-check/check_apps_imports.ts | new | The import-discipline validator: scans apps/** + packages/**, exits non-zero with file:line citations on substrate/harness imports |
.claude/skills/project-check/tests/check_apps_imports.test.ts | new | Bun test: violating file fails with a citation, compliant file passes |
lefthook.yml | modify | Add project-check-apps-imports to pre-commit.commands, running the new gate |
docs/planning/standards/S-0008-apps-consume-substrate-through-published-surfaces.md | modify | status: open/draft → open/active (via standard update); name the validator in “How to apply” |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: An
apps/**file that importsplugin/lib/**(or via the@lib/alias) failsbun run .claude/skills/project-check/check_apps_imports.tswith exit code1and afile:linecitation naming the offending import. - AC-2: A
packages/**file importingplugin/skills/**orplugin/.claude-plugin/**likewise fails with afile:linecitation. - AC-3: A compliant
apps/**file (no substrate/harness import — reaching the substrate only through thesdlcCLI or apackages/<eco>API library) passes the gate with exit code0. - AC-4:
docs/planning/standards/S-0008-apps-consume-substrate-through-published-surfaces.mdhasstatus: open/activeand its “How to apply” section namescheck_apps_imports.tsas the enforcing validator. - AC-5:
lefthook.ymlrunsproject-check-apps-importsinpre-commit, so staging a violatingapps/**import aborts the commit.
Out of scope
Section titled “Out of scope”- The applications and packages themselves (
apps/dashboard/, anypackages/<eco>/API library) — created byT-CW4Kand follow-on tasks, not here. This task only validates their import discipline. - Enforcing the inverse arrow (substrate
plugin/lib/**not importing harness surface) — that is the future Standard flagged in S-0008’s Notes, separate work. - A dedicated
sdlc standard activateverb — the flip rides the existing genericstandard update --setop; minting a bespoke verb is not in scope.
Dependencies
Section titled “Dependencies”T-CW4K—apps/(and the dashboard app under it) must exist before there is anything for the validator to scope over and test against.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-06-20. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
check_apps_imports.test.tsasserts anapps/**file importingplugin/lib/**(relative../../../plugin/lib/…) and via the@lib/alias each fail with exit 1 + afile:linecitation. - AC-2: auto — the test asserts a
packages/**file importingplugin/skills/**andplugin/.claude-plugin/**(dynamicimport()) each fail with exit 1 + a citation. - AC-3: auto + agent-manual — the test asserts a compliant
apps/**fixture passes (exit 0); separately,bun run .claude/skills/project-check/check_apps_imports.tsagainst the live tree exits 0 over the realapps/dashboard/(4 files: env.d.ts, App.vue, main.ts, vite.config.ts — all npm/relative, no substrate import). - AC-4: agent-manual —
docs/planning/standards/S-0008-…mdfrontmatter isstatus: open/activeand its “How to apply” namescheck_apps_imports.tsas the enforcing validator (verified by reading the committed file). - AC-5: agent-manual —
lefthook.ymlrunsproject-check-apps-importsinpre-commit; confirmed the gate fired and passed (✔️ project-check-apps-imports) during the post-mortem commit.
What worked
Section titled “What worked”- Mirroring
check_no_legacy_script_paths.tsgave a clean, proven skeleton (repo-root resolution, per-unit OK/FAIL, exit 0/1/2) — the new validator only had to add specifier extraction + forbidden-tree matching. - The real
apps/dashboard/is genuinely compliant (relative/apifetch, npm imports only), so promoting S-0008 toopen/activeand arming the pre-commit gate is safe day one — the live gate passes on the first commit. - The
@cli/vs@lib/distinction was correct out of the box: the CLI is a defined API (allowed),@lib/is substrate internals (forbidden).
Friction and automation gaps
Section titled “Friction and automation gaps”- The verify/start task-state commits landed on the task BRANCH (stale SHAs)
instead of only on
main:start_task.tsreset the branch to localmain’s tip, then the parent had togit pull --rebasemain(origin/main had diverged under parallel WIP) which rewrote those commits’ SHAs — leaving the pre-rebase copies stranded on the branch. They trippedproject-check-task-state-originon every branch commit, forcing the implementation sub-agent to commit withLEFTHOOK=0(bypassing ALL gates). Resolved by rebasing the branch ontoorigin/main(git dropped the stale commits as already-applied), after which the parent re-verified every gate independently. Gap: when origin/main moves betweenstart_task’s branch-reset and the parent’s main-push, the branch carries stale state commits; task-work Step 5b should rebase the task branch ontoorigin/mainafter the main-push (orstart_taskshould reset against the post-push tip), so the implementer never sees the lint-state-origin false positive and never needsLEFTHOOK=0. → T-6R73-pre-commit-drift-hooks-gate-unconditionally-forcing-no - A sub-agent reaching for
LEFTHOOK=0to get unblocked silently skips the new validator + docs-drift + markdown-fmt gates; the parent then has to re-run the whole gate set by hand. The implementer brief should forbidLEFTHOOK=0and instead surface a hook block to the parent for resolution. → T-6R73-pre-commit-drift-hooks-gate-unconditionally-forcing-no - The baseline-diff quality gate again surfaced spurious
new-drift(parallel work’sT-44OO/T-BQRUmarkdown landed on origin/main after the Step 3a baseline was captured at a now-stale SHA, plus rumdl’s issue-count summary line) — the same gap tracked by T-BCNP-quality-gate-ignores-summary-and-corpus-lines. → T-BCNP-quality-gate-ignores-summary-and-corpus-lines
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-6R73-pre-commit-drift-hooks-gate-unconditionally-forcing-no — linked
(stale task-state commits trip
project-check-task-state-origin, forcingLEFTHOOK=0; and the implementer brief should forbidLEFTHOOK=0and surface hook blocks to the parent — both folded into T-6R73, the active tracker) - T-BCNP-quality-gate-ignores-summary-and-corpus-lines — linked (baseline-diff spurious new-drift from a stale baseline SHA + rumdl issue-count summary line)
Discovery context
Section titled “Discovery context”- The S-0008 promotion and validator landed once
T-CW4Kshippedapps/dashboard/; the validator’s compliant-case is exercised against that real app.
Depends on
Section titled “Depends on”T-CW4K