8000 fix(kit): adopted kits admit the host files design:os writes by jangtrinh · Pull Request #380 · jangtrinh/design-os · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## 2026-11-05 - adopted kits admit the host files design:os writes

### Fixed
- `ui init` and `ui agents init` in an adopted kit project (`ui ds kit adopt`) no longer make
`ui ds kit verify` fail with `DS_TAMPERED: kit integrity: Undeclared file`. Kit integrity now lets an
undeclared design:os host file coexist with the kit: the `ui init` adapter trees for all three runtimes,
an `AGENTS.md` holding the ease-design sentinel block, and `.claude/agents/*.md` files carrying the
design-os agent stamp. Declared kit content stays hash-bound, a host file the kit declared stays bound,
and every other undeclared file (a hand-written agent, `.claude/settings.json`, a stray source file)
still fails.
- `ui ds guidelines build | check` read the ontology glossary from `design/knowledge/glossary.yaml` when
the project root has no `ontology/glossary.yaml`, so an adopted kit can gain a glossary after adoption.
A root `ontology/glossary.yaml` added later still fails integrity, and the error now names
`design/knowledge/glossary.yaml`.
- `ui init --json` reports every file it writes: each `adapters[]` entry carries its `wrapper` script
(`.claude/design-os-model.sh` and peers), and the new `files` array lists the manifests, adapter tree,
wrappers and any `--with-agents` agents.

## 2026-10-30 - ease-design 0.6.2

The design-system usage release. `ui gate` can now fail a page that drifts from its own design system,
Expand Down
7 changes: 7 additions & 0 deletions knowledge/design-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ explicit `ui init --with-agents` flag exercises the opt-in in the same run
agents as they want; a project with zero agents is a fully supported state
(`agents check` reports it as a warning, never an error).

In an adopted kit project (`ui ds kit adopt`), generate agents straight into it and
leave them out of `kit.json` artifacts. Kit integrity admits an undeclared
`.claude/agents/*.md` only when it carries the design-os stamp (the same mark §6
reads), so a hand-written agent there still fails `ui ds kit verify`. Declaring a
generated agent binds its bytes, and the next `agents init --force` then reads as
tampering. See `ds-kit-onboarding.md` §5 for the full host-file rule.

## §5 Runtime scope

**This sc 10000 ope is about SUBAGENTS only** (`ui agents` → `.claude/agents/`) — NOT the
Expand Down
16 changes: 16 additions & 0 deletions knowledge/ds-kit-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,21 @@ The kit binding participates in the normal DS revision and owner-local lesson ch
Editing TSX, CSS, source, mappings, lock files or evidence outside sanctioned writes
fails integrity. An accepted lesson cannot float to another implementation revision.

Host files that design:os writes itself may sit beside the adopted kit undeclared:
`ui init` adapter trees (`.claude/` or `.agent/` manifest, `commands/ui/`,
`workflows/ui-*`, `skills/design-os-*`, `rules/design-os-routing.md`,
`design-os-model.sh`, `AGENTS.ease-design.json`), an `AGENTS.md` that holds the
ease-design sentinel block, and `.claude/agents/*.md` files carrying the design-os
agent stamp. They configure the agent host, and the CLI re-renders them, so do not
declare them in `kit.json`. A declared host file stays hash-bound like any other
content: re-rendering it then fails integrity until a new revision is adopted. Any
other undeclared file, including a hand-written agent or `.claude/settings.json`,
still fails. Owner knowledge added after adoption lives in the `design/` store: rulings
in `design/knowledge/rulings.json`, the ontology glossary in
`design/knowledge/glossary.yaml` (`ds guidelines build` reads it when the project
root has no `ontology/glossary.yaml`). A root glossary added later fails integrity,
and the error names this path.

## 6. Grow with the owner

Sanctioned token/role and registry writes preserve prior integrity, update the bound
Expand All @@ -239,4 +254,5 @@ the owner's actual review decision; successful adoption does not fabricate that
- **Coverage reduced to the starter floor.** A minimum count does not account for every captured ID. Keep all supplied mappings and observed options; unresolved items, duplicate owner identities and lost token/mode projections block completeness.
- **Screenshots show different code.** Bind the actual implementation exports, consumer CSS and dependency/import closure used by the harness. Preserved original source alone cannot justify a screenshot of adapted code.
- **Evidence treated as its own judge.** Hashes bind the declared subject and proof bytes; they cannot establish visual quality or authenticate a review author. Require independent source, render and behavior assessment before writing passed cases.
- **Generated host files declared as content.** Declaring `ui init` or `ui agents init` output in `kit.json` freezes it: the next re-render reads as tampering. Leave these files undeclared; integrity already admits them by their design:os path or mark.
- **Growth silently inherits an old pass.** Sanctioned changes create a new revision with stale proof; arbitrary source edits fail integrity. Stage and verify the new candidate rather than accepting lessons against stale proof or healing unauthorized edits.
12 changes: 11 additions & 1 deletion src/commands/ds-guidelines-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,23 @@ function readOptional(projectDir: string, abs: string): OptionalInput {
}
}

/**
* `ontology/glossary.yaml` at the project root wins; otherwise `design/knowledge/glossary.yaml`, next to the
* rulings. An adopted kit refuses undeclared root files, so the design/ store is its only post-adoption home.
*/
function glossaryPath(projectDir: string, designDir: string): string {
const root = join(projectDir, "ontology", "glossary.yaml");
const store = join(designDir, "knowledge", "glossary.yaml");
return !existsSync(root) && existsSync(store) ? store : root;
}

/** `rulingsFlag` (--rulings) overrides the default `design/knowledge/rulings.json`. */
export function readPackInputs(projectDir: string, designDir: string, rulingsFlag: string | undefined): PackInputs {
const rulingsAbs = rulingsFlag !== undefined ? resolve(rulingsFlag) : join(designDir, "knowledge", "rulings.jso 10BC8 n");
return {
rulings: readOptional(projectDir, rulingsAbs),
context: readOptional(projectDir, join(projectDir, "CONTEXT.md")),
glossary: readOptional(projectDir, join(projectDir, "ontology", "glossary.yaml")),
glossary: readOptional(projectDir, glossaryPath(projectDir, designDir)),
conventions: readOptional(projectDir, join(projectDir, "CONVENTIONS.md")),
soul: readOptional(projectDir, join(designDir, "soul.md")),
};
Expand Down
13 changes: 11 additions & 2 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,9 @@ export const initCommand = {
written: boolean;
replaced: boolean;
}[] = [];
const adapterResults: { runtime: Runtime; paths: string[] }[] = [];
// `paths` mirrors the manifest's adapters[] contract (wrapper excluded); `wrapper` names the
// model-adapter script so the JSON report still covers every file written.
const adapterResults: { runtime: Runtime; paths: string[]; wrapper: string }[] = [];

const allWrittenManifests: string[] = [];
// Per-runtime adapter artifacts that were successfully written (flat list
Expand Down Expand Up @@ -466,6 +468,7 @@ export const initCommand = {
adapterResults.push({
runtime: entry.runtime,
paths: nonWrapperArtifacts.map((a) => a.absPath),
wrapper: wrapperAbsPath,
});
}

Expand Down Expand Up @@ -508,7 +511,13 @@ export const initCommand = {

// ── Emit result ────────────────────────────────────────────────────────
if (useJson) {
const data: Record<string, unknown> = { manifests, adapters: adapterResults };
// `files`: every path this run wrote — manifests, adapter tree, wrapper scripts, agents.
const files = [
...manifests.map((m) => m.path),
...adapterResults.flatMap((a) => [...a.paths, a.wrapper]),
...(agentsWritten ?? []).filter((a) => a.written).map((a) => a.path),
];
const data: Record<string, unknown> = { manifests, adapters: adapterResults, files };
if (hint !== null) data.nextStep = hint.nextStep;
if (agentsWritten !== null) data.agents = agentsWritten;
return okJson(CMD, data);
Expand Down
2 changes: 1 addition & 1 deletion src/core/ds-guidelines-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export interface PackInputs {
rulings: OptionalInput;
/** Product glossary: `CONTEXT.md` at the project root. */
context: OptionalInput;
/** Ontology glossary: `ontology/glossary.yaml` at the project root. */
/** Ontology glossary: `ontology/glossary.yaml` at the project root, else `design/knowledge/glossary.yaml` (an adopted kit's home). */
glossary: OptionalInput;
/** Measured house style: `CONVENTIONS.md` at the project root (from `ui synthesize-conventions`). */
conventions: OptionalInput;
Expand Down
4 changes: 3 additions & 1 deletion src/core/ds-kit-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { KitError, type KitFile } from './ds-kit-types.js';
import { isGeneratedHostFile, undeclaredFileMessage } from './ds-kit-host-files.js';
export function hashKitBytes(buffer: Buffer): string { return `sha256:${createHash('sha256').update(buffer).digest('hex')}`; }
export function kitPath(path: unknown): string {
if (typeof path !== 'string' || !path || path.includes('\\') || [...path].some(c => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127 || c.codePointAt(0)! >= 0xD800 && c.codePointAt(0)! <= 0xDFFF) || /[<>:"|?*]/.test(path) || path.startsWith('/') || path.split('/').some(p => !p || p === '.' || p === '..' || /[. ]$/.test(p) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i.test(p)))
Expand Down Expand Up @@ -65,7 +66,8 @@ export function indexKitContent(root: string, artifacts: string[]): KitFile[] {
continue;
}
if (stat.isDirectory()) walk(rel);
else if (!stat.isFile() || !declared.has(rel)) throw new KitError('KIT_UNDECLARED', `Undeclared file: ${rel}`);
else if (!stat.isFile()) throw new KitError('KIT_UNDECLARED', `Undeclared file: ${rel}`);
else if (!declared.has(rel) && !isGeneratedHostFile(rel, () => readKitFile(root, rel))) throw new KitError('KIT_UNDECLARED', undeclaredFileMessage(rel));
}
};
try { walk(''); } catch (error) {
Expand Down
45 changes: 45 additions & 0 deletions src/core/ds-kit-host-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Host files design:os writes into a project itself — `ui init` adapter trees and
* `ui agents init` agents. They configure the agent host, never the design system,
* and the CLI re-renders them at will, so an adopted kit's integrity walk lets an
* UNDECLARED one coexist instead of calling it tampering. A host file the kit
* declared in `kit.json` artifacts stays hashed like any other content.
*
* Paths are design:os-owned namespaces, not a list of today's verbs: a project
* initialised by an older or newer `ui` must verify too. Agents and AGENTS.md
* share their folder with hand-written files, so those count only when they carry
* the generator's own mark (the agent stamp, the Codex sentinel block).
*/
import { parseAgentStamp } from "./agents-gen.js";
import { CODEX_SENTINEL_BEGIN } from "../adapters/wrapper-shapes-codex.js";

const OWNED_HOST_PATHS: readonly RegExp[] = [
/^(\.claude|\.agent)\/ease-design\.json$/,
/^AGENTS\.ease-design\.json$/,
/^\.claude\/commands\/ui\/[a-z0-9-]+\.md$/,
/^\.agent\/workflows\/ui-[a-z0-9-]+\.md$/,
/^(\.claude|\.agent)\/skills\/design-os-[a-z0-9-]+\/SKILL\.md$/,
/^(\.claude|\.agent)\/rules\/design-os-routing\.md$/,
/^((\.claude|\.agent)\/)?design-os-model\.sh$/,
];

/** True when `path` (kit-root-relative POSIX) is a host file design:os generated. `read` is only called for marked kinds. */
export function isGeneratedHostFile(path: string, read: () => Buffer): boolean {
if (OWNED_HOST_PATHS.some((pattern) => pattern.test(path))) return true;
if (/^\.claude\/agents\/[^/]+\.md$/.test(path)) return parseAgentStamp(read().toString("utf8")) !== null;
if (path === "AGENTS.md") return read().toString("utf8").includes(CODEX_SENTINEL_BEGIN);
return false;
}

/** Where an adopted kit takes a project input that would otherwise sit undeclared in the kit root. */
const ADOPTED_INPUT_HOMES: Readonly<Record<string, string>> = {
"ontology/glossary.yaml": "design/knowledge/glossary.yaml",
};

/** The undeclared-file message, naming the supported home when the file is a known project input. */
export function undeclaredFileMessage(path: string): string {
const home = ADOPTED_INPUT_HOMES[path];
return home === undefined
? `Undeclared file: ${path}`
: `Undeclared file: ${path} — an adopted kit reads it from ${home} (or declare it in kit.json artifacts before adoption)`;
}
Loading
Loading
0