diff --git a/.changeset/gather-applicability-fences.md b/.changeset/gather-applicability-fences.md new file mode 100644 index 00000000..15a9e8b6 --- /dev/null +++ b/.changeset/gather-applicability-fences.md @@ -0,0 +1,5 @@ +--- +"@design-intelligence/ghost": minor +--- + +Clarify gather's applicability contract in markdown and JSON, and make pulled material and Skeleton fences safe around nested backticks. diff --git a/CLAUDE.md b/CLAUDE.md index c0d8b498..9b1429d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,9 +71,9 @@ to implementation or assets the agent can inspect), and **how it is assembled** Altitude lives in the prose: a universal truth is stated plainly; a narrower truth names its **condition**, the situation it applies in, never a filing -destination. `ghost gather` emits the whole menu (every node's id, kind, -description, and material count); the agent selects just-in-time against the -actual task. `ghost pull` emits selected node bodies and materials. `ghost +destination. `ghost gather` emits the complete menu (every node's id, kind, +description, material count, and payload labels); the agent pulls every node +whose stated situation applies to the actual task. `ghost pull` emits selected node bodies and materials. `ghost review` reads a diff, matches touched files to node materials, offers relevant checks, and emits an advisory packet for the host agent to judge. @@ -103,7 +103,7 @@ Core workflow: | `ghost init` | Scaffold `.ghost/` with the skeleton starter: manifest, glossary, a `brand.md` cover, foundation chapters, context nodes, and the cliche floor. `--template minimal` writes a smaller cover-led starter; `--body vessel-light` installs a full inhabited fingerprint instead. `--with checks` also adds the checks directory. | | `ghost checks init` | Scaffold `.ghost/checks/` with an example review assertion. | | `ghost validate` | Validate the package: manifest shape, node validity, material locators, check references, and glossary kind prefixes. | -| `ghost gather [ask…]` | Emit the fingerprint menu for the agent to select from. | +| `ghost gather [ask…]` | Emit the complete guidance menu so the agent can pull applicable nodes. | | `ghost pull […]` | Emit selected nodes' bodies and materials; append the selection to the local `.ghost/.events` tape. | | `ghost review` | Emit an advisory review packet for a diff using material-backed nodes and checks (requires `.ghost/checks/`). | | `ghost pulse` | Summarize local gather/pull events from `.ghost/.events`. | diff --git a/apps/docs/.ghost/pattern.docs-index-card-grid.md b/apps/docs/.ghost/pattern.docs-index-card-grid.md index 13a73cd2..946fa03b 100644 --- a/apps/docs/.ghost/pattern.docs-index-card-grid.md +++ b/apps/docs/.ghost/pattern.docs-index-card-grid.md @@ -22,7 +22,7 @@ product and offers a small set of next reads or tools. title, one short description. The hover state can darken the border and invert a title underline, but it should not become a whole animated tile. - Card copy explains the job of the destination, not the feature in abstract. - Prefer "Emit the fingerprint menu with gather" over "Powerful context + Prefer "Emit Available guidance with gather" over "Powerful context discovery." - Icons are thin-line Lucide symbols at the existing sizes and stroke widths. They support scanning; they are not illustrations. diff --git a/apps/docs/src/app/tools/scan/page.tsx b/apps/docs/src/app/tools/scan/page.tsx index 43829114..47162a68 100644 --- a/apps/docs/src/app/tools/scan/page.tsx +++ b/apps/docs/src/app/tools/scan/page.tsx @@ -24,7 +24,7 @@ const cards: { name: "CLI reference", href: "/docs/cli", description: - "Emit the fingerprint menu with gather, read selected truths with pull, and tune with pulse.", + "Emit Available guidance with gather, pull selected truths with pull, and tune with pulse.", icon: , }, { @@ -47,7 +47,7 @@ export default function GhostScanLanding() {
@@ -141,13 +142,17 @@ ghost gather "checkout confirmation email" --format json This is the pre-generation step, and "the right context at the right time": the agent selects just-in-time against the actual task, then manifests the truths -into whatever it is building — product UI, marketing, email, a landing page. +into whatever it is building: product UI, marketing, email, a landing page. If +nothing applies, the agent should name the fingerprint's silence and avoid +inventing Ghost-backed guidance. ### Read the selected truths: `pull` -`pull` emits the named nodes' full prose bodies. It is how an agent reads the -truths it selected from the menu — preferred over opening the files directly, -because gather and pull append structured JSONL events to `.ghost/.events`. +`pull` emits the named nodes' full prose bodies and materials. It is how an +agent reads the truths it selected from the menu, preferred over opening the +files directly because it inlines small materials, emits inspect-pointers, +extracts Skeletons last, and appends structured JSONL events to +`.ghost/.events`. That file is an events tape for the fingerprint's author: tweak a node's description, re-run the task, and `ghost pulse` shows whether the truth appeared diff --git a/apps/docs/src/content/docs/fingerprint-authoring.mdx b/apps/docs/src/content/docs/fingerprint-authoring.mdx index 6dacc278..ce272b63 100644 --- a/apps/docs/src/content/docs/fingerprint-authoring.mdx +++ b/apps/docs/src/content/docs/fingerprint-authoring.mdx @@ -92,7 +92,7 @@ Node frontmatter carries only descriptive properties: | Property | What it does | | --- | --- | -| `description` | A short summary of the node — the retrieval payload the menu shows. | +| `description` | The retrieval payload the menu shows: what the node governs, the observable condition under which it applies, and what it contributes where useful. | | `materials` | Optional list of locators for the concrete materials the truth is about: repo-relative paths/globs or absolute HTTPS URLs. | | _passthrough_ | Free-form keys (`audience`, `stage`, …) are preserved for host tooling. | @@ -115,7 +115,8 @@ plainly. A narrower truth names its **condition** in the prose — the *situatio it applies in ("when a surface must show many items at once, carry hierarchy with weight, not color"), never a destination or filing bucket ("for dashboards:"). The agent reads the condition and decides when the truth applies; that -interpretation is the agent's job, not the filename's. +interpretation is the agent's job, not the filename's. Avoid broad universal +imperatives unless universal retrieval is truly intended. @@ -128,8 +129,8 @@ and *how it is assembled* (the patterns — layouts, flows, states, content patterns, behavior, visual arrangements). These are drafting prompts, never frontmatter fields, and a node may answer only one. Each answer lands as a steering dimension: stance prose is read first — weakest at moving output form -on its own, but the yardstick every later selection is measured against; materials -make a node concrete and drive pull ordering and review matching; patterns and +on its own, but the yardstick every later selection is measured against; +materials provide concrete payload and support review matching; patterns and Skeletons steer strongest, and Skeleton fences are extracted and emitted dead last so generation starts from them. diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx index 71f8d741..9d67ff89 100644 --- a/apps/docs/src/content/docs/getting-started.mdx +++ b/apps/docs/src/content/docs/getting-started.mdx @@ -32,8 +32,8 @@ through a short loop: ```bash ghost init # scaffold .ghost/ with the skeleton starter ghost validate # make sure the fingerprint is well-formed -ghost gather # emit the menu for this task; your agent selects the truths it needs -ghost pull # read the selected truths' full bodies +ghost gather # emit Available guidance; your agent selects applicable truths +ghost pull # pull selected truths' full bodies ghost pulse # inspect the local signal while tuning the fingerprint ``` @@ -122,20 +122,22 @@ applies in, never a filename destination. For a fuller human-agent workflow, rea -Before generating, gather the fingerprint menu. `gather` does no selection: it -emits every node's id, kind, and description, and your agent reads the ask -against it and pulls the truths it judges relevant: +Before generating, gather Available guidance from the Ghost brand fingerprint. +`gather` does no selection: it emits the complete, unfiltered, unranked list of +node ids, kinds, and descriptions. Your agent reads the ask against +applicability and pulls every node whose stated situation governs the work: ```bash ghost gather "checkout settings" ghost gather "checkout settings" --format json -ghost pull principle.trust voice # read the selected truths' full bodies +ghost pull principle.trust voice # pull selected truths' full bodies ``` This is "the right context at the right time": the agent selects just-in-time -against the actual task, then manifests the truths into whatever it is building. -Run it before generation, so the agent builds with the brand in hand rather than -discovering the gaps in review. +against the actual task. Topic overlap alone is not applicability; if nothing +applies, it should name the fingerprint's silence and avoid inventing +Ghost-backed guidance. Run it before generation, so the agent builds with the +brand in hand rather than discovering the gaps in review. Each `gather` and `pull` also adds a line to a private local log at `.ghost/.events`; it stays on your machine and never enters version control. diff --git a/apps/docs/src/content/docs/troubleshooting.mdx b/apps/docs/src/content/docs/troubleshooting.mdx index ca4084fa..4036e25b 100644 --- a/apps/docs/src/content/docs/troubleshooting.mdx +++ b/apps/docs/src/content/docs/troubleshooting.mdx @@ -26,15 +26,15 @@ do about each. -`gather` does no filtering or ranking; it always emits the whole menu. If your -agent reads the menu and pulls nothing useful, the problem is retrieval, not -missing coverage. +`gather` does no filtering or ranking; it always emits the whole guidance menu. +If your agent reads the menu and pulls nothing useful, the problem may be +retrieval, applicability, or true fingerprint silence. - **Check the description, not the body.** An agent selects against a node's `description` alone. A vague description (`Our trust principles.`) is invisible at selection time no matter how good the prose underneath is. - Rewrite it as a task-shaped phrase: what moment, surface, or question should - make an agent realize this node applies. + Rewrite it as a task-shaped phrase: what the node governs, what observable + condition makes it apply, and what it contributes where useful. - **Check `ghost pulse`.** It shows whether a node appeared on the menu, got pulled, was missed by an id the agent tried and failed, or stayed cold every time. A cold node with real content is a description problem, not a @@ -42,6 +42,9 @@ missing coverage. - **Don't add more nodes to fix a retrieval miss.** A weak description with a strong body is worse than a strong description with a thin body; the agent never reaches the body. +- **Respect silence.** If no description indicates an applicable situation, name + the fingerprint's silence, follow the cover silence posture, and do not claim + provisional local reasoning as Ghost-backed. ```bash ghost gather "checkout settings" --format json diff --git a/apps/docs/src/generated/cli-manifest.json b/apps/docs/src/generated/cli-manifest.json index cc889b1d..8c44fc8f 100644 --- a/apps/docs/src/generated/cli-manifest.json +++ b/apps/docs/src/generated/cli-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-14T04:00:29.832Z", + "generatedAt": "2026-07-15T12:11:34.119Z", "tools": [ { "tool": "ghost", @@ -96,11 +96,11 @@ "tool": "ghost", "name": "gather", "rawName": "gather [...ask]", - "description": "Emit the fingerprint menu — every node's id, kind, and description — for the agent to select from.", + "description": "Emit the complete available guidance menu so the agent can pull applicable nodes.", "group": "core", "defaultHelp": true, "compactName": "gather [ask]", - "summary": "Emit the fingerprint menu for the agent to select from.", + "summary": "Emit the complete guidance menu so the agent can pull applicable nodes.", "options": [ { "rawName": "--package ", diff --git a/docs/purposes.md b/docs/purposes.md index 89f96510..9f1536ab 100644 --- a/docs/purposes.md +++ b/docs/purposes.md @@ -46,8 +46,8 @@ into folders is a browsing convenience only. One resolution mechanism, read-only: - **The menu.** `ghost gather` emits every node's id, kind, description, and - material count. The agent reads the ask against descriptions and pulls the - truths it judges relevant. Ghost does no NLP and no selection. + material count. The agent reads the ask against descriptions and pulls every + truth whose stated situation applies. Ghost does no NLP and no selection. The optional `cover` in `manifest.yml` names the human-curated front door. `ghost gather` inlines that node before the menu and excludes it from the diff --git a/packages/context-control/lib/model.mjs b/packages/context-control/lib/model.mjs index 193e3e0a..53693378 100644 --- a/packages/context-control/lib/model.mjs +++ b/packages/context-control/lib/model.mjs @@ -92,11 +92,12 @@ following the ghost skill's recall recipe. You will get an ask, the cover already in context, and the ghost gather menu. Select only menu node ids against their descriptions. Do not select the cover. -- Pull a small set: 3-5 nodes is normal; 10 is a bad selection unless the - task is unusually broad. -- Prefer nodes with concrete material for the surface being made. -- Anti-goal nodes are review-critical negative space; include one when the - ask enters the territory its description names. +- Pull every node whose description indicates its stated situation applies and + whose truth, material, structure, or refusal governs the work. +- Skip inapplicable nodes. Topic overlap alone is not applicability. +- Do not add nodes for completeness or omit applicable nodes to meet a count. +- Anti-goal nodes are review-critical negative space; pull each one whose + description names territory the ask enters. Respond with ONLY a JSON array of node id strings, nothing else.`; diff --git a/packages/ghost/src/commands/command-discovery.ts b/packages/ghost/src/commands/command-discovery.ts index e851863e..5e9fa82c 100644 --- a/packages/ghost/src/commands/command-discovery.ts +++ b/packages/ghost/src/commands/command-discovery.ts @@ -124,7 +124,8 @@ const COMMAND_DISCOVERY = [ group: "core", defaultHelp: true, compactName: "gather [ask]", - summary: "Emit the fingerprint menu for the agent to select from.", + summary: + "Emit the complete guidance menu so the agent can pull applicable nodes.", }, { name: "pull", diff --git a/packages/ghost/src/commands/gather-command.ts b/packages/ghost/src/commands/gather-command.ts index 1edc52b6..cb8555ca 100644 --- a/packages/ghost/src/commands/gather-command.ts +++ b/packages/ghost/src/commands/gather-command.ts @@ -25,7 +25,7 @@ export function registerGatherCommand(cli: CAC): void { cli .command( "gather [...ask]", - "Emit the fingerprint menu — every node's id, kind, and description — for the agent to select from.", + "Emit the complete available guidance menu so the agent can pull applicable nodes.", ) .option( "--package ", @@ -65,17 +65,35 @@ export function registerGatherCommand(cli: CAC): void { menu: menu.map((entry) => entry.id), }); - // Ghost does no selection. It emits the catalog; the agent reads the - // ask against it and pulls the nodes it judges relevant. + // Ghost does no selection. It emits the complete catalog; the agent + // reads the ask against it and pulls the nodes whose described + // conditions apply. if (opts.format === "json") { process.stdout.write( `${JSON.stringify( { kind: "menu", ...(ask ? { ask } : {}), + source: { + artifact: "Ghost brand fingerprint", + list: "Available guidance", + }, + contract: gatherContract(ask), ...(coverNode - ? { cover: { id: coverNode.id, body: coverNode.body } } + ? { + cover: { + id: coverNode.id, + body: coverNode.body, + inContext: true, + selectable: false, + }, + } : {}), + next: { command: "ghost pull […]" }, + silence: { + ifNoneApply: + "Name the fingerprint's silence, follow the cover silence posture when present, and do not invent Ghost-backed guidance.", + }, coverage: menuCoverage(menu), ...(kinds.length > 0 ? { kinds } : {}), nodes: menu, @@ -141,16 +159,65 @@ interface FormatMenuOptions { }; } +interface GatherContract { + completeness: { + complete: true; + filtered: false; + ranked: false; + selectedByGhost: false; + }; + selection: { + basis: "applicability"; + instruction: string; + topicOverlapAloneIsApplicability: false; + addForCompleteness: false; + omitApplicableForCount: false; + }; + noAsk: string; +} + +function gatherContract(ask: string | undefined): GatherContract { + return { + completeness: { + complete: true, + filtered: false, + ranked: false, + selectedByGhost: false, + }, + selection: { + basis: "applicability", + instruction: ask + ? "Pull every node whose description indicates its stated situation applies and whose truth, material, structure, or refusal governs the work; skip inapplicable nodes." + : "Bare gather is catalog inspection. Do not treat the menu as task grounding until an ask is supplied; when grounding a task, pull every applicable node and skip inapplicable nodes.", + topicOverlapAloneIsApplicability: false, + addForCompleteness: false, + omitApplicableForCount: false, + }, + noAsk: + "Bare gather is catalog inspection and does not imply task grounding.", + }; +} + function menuCoverage(menu: CatalogMenuEntry[]): { nodes: number; concrete: number; + payloads: { + materials: number; + fencedExamples: number; + skeletons: number; + }; undescribed: number; } { return { nodes: menu.length, concrete: menu.filter((entry) => entry.concrete).length, + payloads: { + materials: menu.filter((entry) => entry.materials !== undefined).length, + fencedExamples: menu.filter((entry) => entry.hasFencedExample).length, + skeletons: menu.filter((entry) => entry.hasSkeleton).length, + }, // A node without a description is a bare id the agent cannot select - // against — surface the count where selection happens. + // against; surface the count where selection happens. undescribed: menu.filter( (entry) => !entry.description || entry.description.trim().length === 0, ).length, @@ -159,9 +226,14 @@ function menuCoverage(menu: CatalogMenuEntry[]): { function menuCoverageLine(menu: CatalogMenuEntry[]): string { const coverage = menuCoverage(menu); + const payloadParts = [ + `${coverage.payloads.materials} with materials`, + `${coverage.payloads.fencedExamples} with substantial fenced examples`, + `${coverage.payloads.skeletons} with Skeletons`, + ]; const parts = [ `${coverage.nodes} nodes`, - `${coverage.concrete} carry concrete material`, + `${coverage.concrete} carry payloads (${payloadParts.join(", ")})`, ]; if (coverage.undescribed > 0) { parts.push(`${coverage.undescribed} lack descriptions`); @@ -174,25 +246,36 @@ function formatMenuMarkdown( kinds: MenuKind[], options: FormatMenuOptions = {}, ): string { - const lines: string[] = [ - options.ask ? `# Ghost Nodes — for: ${options.ask}` : "# Ghost Nodes", - "", - ]; + const lines: string[] = ["# Ghost brand fingerprint", ""]; + if (options.ask) lines.push(`Ask: ${options.ask}`, ""); if (options.cover) { lines.push( - `## Cover — \`${options.cover.id}\``, + `## Cover in context: \`${options.cover.id}\``, "", options.cover.body, "", + "Cover status: already in context; outside selection; do not pull again.", + "", "---", "", ); } - lines.push( - "The fingerprint menu. Match the ask against these nodes and read the ones you judge relevant.", - menuCoverageLine(menu), - "", - ); + lines.push("## Available guidance", "", menuCoverageLine(menu), ""); + if (options.ask) { + lines.push( + "Complete, unfiltered, unranked list from the fingerprint. Ghost has not selected nodes for this ask.", + "Pull every node whose description indicates its stated situation applies and whose truth, material, structure, or refusal governs the work. Skip inapplicable nodes. Topic overlap alone is not applicability. Do not add nodes for completeness or omit applicable nodes to meet a count.", + "Next: `ghost pull […]`.", + "If nothing applies, name the fingerprint's silence, follow the cover silence posture, and do not invent Ghost-backed guidance.", + "", + ); + } else { + lines.push( + "Complete, unfiltered, unranked list from the fingerprint. Bare gather is catalog inspection; Ghost has not grounded a task or selected nodes.", + "When grounding an ask, pull every applicable node with `ghost pull […]`. Skip inapplicable nodes and do not invent Ghost-backed guidance when the fingerprint is silent.", + "", + ); + } if (kinds.length > 0) { lines.push("Kinds:", ""); for (const kind of kinds) { @@ -207,11 +290,18 @@ function formatMenuMarkdown( if (entry.materials !== undefined) { lines.push(` - materials: ${entry.materials}`); } - if (entry.concrete) { - lines.push( - ` - carries concrete material${entry.hasSkeleton ? " (Skeleton)" : ""}`, - ); + const payloadTypes = formatPayloadTypes(entry); + if (payloadTypes.length > 0) { + lines.push(` - payloads: ${payloadTypes.join(", ")}`); } } return `${lines.join("\n")}\n`; } + +function formatPayloadTypes(entry: CatalogMenuEntry): string[] { + const types: string[] = []; + if (entry.materials !== undefined) types.push("materials"); + if (entry.hasFencedExample) types.push("substantial fenced example"); + if (entry.hasSkeleton) types.push("Skeleton"); + return types; +} diff --git a/packages/ghost/src/commands/pull-command.ts b/packages/ghost/src/commands/pull-command.ts index eda74c85..0024b7e2 100644 --- a/packages/ghost/src/commands/pull-command.ts +++ b/packages/ghost/src/commands/pull-command.ts @@ -285,7 +285,7 @@ function formatPullMarkdown(nodes: PulledNode[]): string { for (const material of materials.materials) { if (material.inlined !== undefined) { const info = material.path ?? material.locator; - lines.push("", `\`\`\`${info}`, material.inlined.trimEnd(), "```"); + lines.push("", fencedMarkdown(material.inlined.trimEnd(), info)); } else if (material.reason === "binary inspect-pointer") { lines.push( `- inspect: ${material.path ?? material.locator} — view this image before generating`, @@ -310,11 +310,23 @@ function formatPullMarkdown(nodes: PulledNode[]): string { ]; for (const skeleton of skeletons) { lines.push("", `## From \`${skeleton.nodeId}\``, ""); - const info = skeleton.info ? skeleton.info : ""; - lines.push(`\`\`\`${info}`, skeleton.content.trimEnd(), "```"); + lines.push(fencedMarkdown(skeleton.content.trimEnd(), skeleton.info)); } sections.push(lines.join("\n")); } return `${sections.join("\n\n---\n\n")}\n`; } + +function fencedMarkdown(content: string, info?: string): string { + const fence = "`".repeat(Math.max(3, longestBacktickRun(content) + 1)); + return `${fence}${info ?? ""}\n${content}\n${fence}`; +} + +function longestBacktickRun(content: string): number { + let longest = 0; + for (const match of content.matchAll(/`+/g)) { + longest = Math.max(longest, match[0].length); + } + return longest; +} diff --git a/packages/ghost/src/ghost-core/catalog/assemble.ts b/packages/ghost/src/ghost-core/catalog/assemble.ts index 73e7744f..35fdf65c 100644 --- a/packages/ghost/src/ghost-core/catalog/assemble.ts +++ b/packages/ghost/src/ghost-core/catalog/assemble.ts @@ -1,6 +1,7 @@ import { carriesConcreteMaterial, extractSkeletonSections, + hasSubstantialFencedExample, } from "../node/steering.js"; import type { GhostNodeDocument } from "../node/types.js"; import type { GhostCatalog, GhostCatalogNode } from "./types.js"; @@ -45,6 +46,7 @@ export function assembleCatalog(input: AssembleCatalogInput): GhostCatalog { ...(fm.description !== undefined ? { description: fm.description } : {}), ...(fm.materials !== undefined ? { materials: fm.materials } : {}), concrete, + hasFencedExample: hasSubstantialFencedExample(placed.doc.body), hasSkeleton: extractSkeletonSections(placed.doc.body).length > 0, body: placed.doc.body, }); diff --git a/packages/ghost/src/ghost-core/catalog/menu.ts b/packages/ghost/src/ghost-core/catalog/menu.ts index 94326bc3..46c70d9d 100644 --- a/packages/ghost/src/ghost-core/catalog/menu.ts +++ b/packages/ghost/src/ghost-core/catalog/menu.ts @@ -2,9 +2,9 @@ import type { GhostCatalog } from "./types.js"; /** * One entry in the gather menu: a node presented as `id` + `kind` + - * `description` — the retrieval payload the agent selects against. The agent - * matches a natural-language ask against these and reads what it judges - * relevant; Ghost does no NLP and no selection. + * `description`, the retrieval payload the agent selects against. The agent + * matches a natural-language ask against these and pulls applicable nodes; + * Ghost does no NLP and no selection. */ export interface CatalogMenuEntry { id: string; @@ -13,8 +13,10 @@ export interface CatalogMenuEntry { description?: string; /** Count of material locators available after pulling this node. */ materials?: number; - /** True when this entry carries concrete material by derived structure. */ + /** True when this entry carries a material locator, fenced example, or Skeleton. */ concrete: boolean; + /** True when this entry includes a substantial fenced example. */ + hasFencedExample?: true; /** True when this entry includes a Skeleton section. */ hasSkeleton?: true; } @@ -36,6 +38,7 @@ export function buildCatalogMenu(catalog: GhostCatalog): CatalogMenuEntry[] { ? { materials: node.materials.length } : {}), concrete: node.concrete, + ...(node.hasFencedExample ? { hasFencedExample: true as const } : {}), ...(node.hasSkeleton ? { hasSkeleton: true as const } : {}), }); } diff --git a/packages/ghost/src/ghost-core/catalog/types.ts b/packages/ghost/src/ghost-core/catalog/types.ts index 3528c048..a8dba774 100644 --- a/packages/ghost/src/ghost-core/catalog/types.ts +++ b/packages/ghost/src/ghost-core/catalog/types.ts @@ -10,12 +10,14 @@ export interface GhostCatalogNode { kind?: string; /** Filename slug: bare name, or the part after the first dot. */ slug: string; - /** One-line "what this is / when to gather it" — the retrieval payload. */ + /** Retrieval payload shown in gather: what applies, when, and what it contributes. */ description?: string; /** Optional material locators carried by the authored node. */ materials?: string[]; - /** True when the node carries materials, a substantial fence, or a Skeleton. */ + /** True when the node carries a material locator, substantial fence, or Skeleton. */ concrete: boolean; + /** True when the node body carries a fenced block of at least 3 lines. */ + hasFencedExample: boolean; /** True when the node declares a `## Skeleton` section. */ hasSkeleton: boolean; body: string; diff --git a/packages/ghost/src/ghost-core/index.ts b/packages/ghost/src/ghost-core/index.ts index d45dd92a..86303b71 100644 --- a/packages/ghost/src/ghost-core/index.ts +++ b/packages/ghost/src/ghost-core/index.ts @@ -73,6 +73,7 @@ export { type GhostNodeLintIssue, type GhostNodeLintReport, type GhostNodeLintSeverity, + hasSubstantialFencedExample, lintGhostNode, NodeIdSchema, NodeRefSchema, diff --git a/packages/ghost/src/ghost-core/node/index.ts b/packages/ghost/src/ghost-core/node/index.ts index 6a1e0127..d4ac61f1 100644 --- a/packages/ghost/src/ghost-core/node/index.ts +++ b/packages/ghost/src/ghost-core/node/index.ts @@ -16,6 +16,7 @@ export { extractSkeletonFences, extractSkeletonSections, type FencedBlock, + hasSubstantialFencedExample, hasThreeLineFence, type SkeletonSection, stripSkeletonSections, diff --git a/packages/ghost/src/ghost-core/node/steering.ts b/packages/ghost/src/ghost-core/node/steering.ts index 002bb31e..8b944a96 100644 --- a/packages/ghost/src/ghost-core/node/steering.ts +++ b/packages/ghost/src/ghost-core/node/steering.ts @@ -19,11 +19,15 @@ export function carriesConcreteMaterial(input: { }): boolean { return ( (input.materials?.length ?? 0) > 0 || - hasThreeLineFence(input.body) || + hasSubstantialFencedExample(input.body) || extractSkeletonSections(input.body).length > 0 ); } +export function hasSubstantialFencedExample(body: string): boolean { + return hasThreeLineFence(stripSkeletonSections(body)); +} + export function hasThreeLineFence(body: string): boolean { for (const block of extractFencedBlocks(body)) { if (block.content.split(/\r?\n/).length >= 3) return true; diff --git a/packages/ghost/src/ghost-core/node/types.ts b/packages/ghost/src/ghost-core/node/types.ts index b930a65b..3f4ff75d 100644 --- a/packages/ghost/src/ghost-core/node/types.ts +++ b/packages/ghost/src/ghost-core/node/types.ts @@ -10,10 +10,9 @@ export interface GhostNodeFrontmatter { /** Free-form descriptive properties parsed from node frontmatter. */ [key: string]: unknown; /** - * One-line statement of what this node is and when to gather it — the - * retrieval payload. Together with the node's id (its path) it is how an - * agent selects a node, exactly like a tool's name + description. The body is - * the node's "implementation"; the description is what makes it discoverable. + * Retrieval payload shown by gather: what the node governs, the observable + * condition under which it applies, and what it contributes where useful. + * Together with the node's id, it is how an agent decides applicability. * Optional, but strongly encouraged on any node worth anchoring a task at. */ description?: string; diff --git a/packages/ghost/src/init-payloads/skeleton/brand.md b/packages/ghost/src/init-payloads/skeleton/brand.md index f83485d4..3d117868 100644 --- a/packages/ghost/src/init-payloads/skeleton/brand.md +++ b/packages/ghost/src/init-payloads/skeleton/brand.md @@ -1,9 +1,10 @@ --- -description: "The brand on one page — always in context; what this brand is, how it feels, and what only it refuses." +description: "The brand on one page; what this brand is, how it feels, and what only it refuses." --- -This cover is unwritten. It is the one page always in an agent's context, so -it carries only what cannot be retrieved by task: what this brand is about, +This cover is unwritten. Ghost gather always places this page in an agent's +context; that is delivery status, not a claim that every sentence applies to +every task. Use it for what cannot be retrieved by task: what this brand is about, in one paragraph, in the brand's own voice. The temperature its words and motion share. And the refusals only this brand makes — not generic don'ts (those live in each foundation's misuse list, and the model's cliches live diff --git a/packages/ghost/src/scan/fingerprint-package-lint.ts b/packages/ghost/src/scan/fingerprint-package-lint.ts index df4ecefb..8b9fe0d6 100644 --- a/packages/ghost/src/scan/fingerprint-package-lint.ts +++ b/packages/ghost/src/scan/fingerprint-package-lint.ts @@ -188,8 +188,8 @@ async function lintKindPrefixes( /** * The `description` is a node's entire retrieval payload: `gather` lists it as * the text the agent selects against. A node without one renders as a bare id - * and is effectively invisible to selection, so `validate` makes that loud. - * Warning, not error: an undescribed node is legal, just undiscoverable. + * and cannot show when it applies, so `validate` makes that loud. Warning, + * not error: an undescribed node is legal, just undiscoverable. */ function lintNodeDescriptions( catalog: GhostCatalog, @@ -203,7 +203,7 @@ function lintNodeDescriptions( severity: "warning", rule: "node-description-missing", message: - "node has no `description`, so `gather` lists it as a bare id the agent cannot select against; add a one-line description of what this truth is and when to pull it", + "node has no `description`, so `gather` lists it as a bare id without applicability context; add a one-line description of what this truth governs, when it applies, and what it contributes", path: `${node.id}.md`, }); } diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md index e8a5104c..d0a71ac2 100644 --- a/packages/ghost/src/skill-bundle/SKILL.md +++ b/packages/ghost/src/skill-bundle/SKILL.md @@ -55,23 +55,26 @@ true, and an agent reads the relevant truths before building. ghost init # scaffold .ghost/ with the steering starter ghost checks init # opt in to review assertions ghost validate # artifact shape + node/material/check validation -ghost gather # emit the fingerprint menu for this task -ghost pull # read selected node bodies and materials +ghost gather # emit Available guidance for this task +ghost pull # pull selected node bodies and materials ghost review # assemble diff + matched material-backed nodes + checks ghost export # package .ghost/ as a portable brand artifact ghost pulse # summarize local gather/pull events while tuning ``` -`gather` does no selection. It emits the menu and you read the ask against it, -then pull the truths you judge relevant. Its header includes a coverage line — -total nodes and nodes carrying concrete material — so an all-prose -fingerprint is visible before generation. +`gather` does no selection. It emits the complete, unfiltered, unranked menu +from the Ghost brand fingerprint. You read the ask against descriptions, then +pull every applicable truth and skip inapplicable nodes. Topic overlap alone is +not applicability. Its header includes a coverage line: total nodes and nodes +carrying concrete material. `gather` labels materials, substantial fenced +examples, and Skeletons separately, so an all-prose fingerprint is visible +before generation. Prefer `ghost pull` over reading files directly: it emits the same prose, inlines small local materials by default, turns binary materials into -inspect-pointers, orders the packet for steering (cover when selected, concrete nodes, -prose rules), extracts Skeletons dead last, and appends structured -events to `.ghost/.events` for local tuning. +inspect-pointers, orders the packet for steering (cover when selected, +concrete nodes, prose rules), extracts Skeletons dead last, and appends +structured events to `.ghost/.events` for local tuning. `review` does no grading. It assembles an advisory packet: touched files, matched material-backed nodes, offered checks, probe @@ -84,7 +87,7 @@ evidence, coverage gaps, and the diff. The host agent renders findings. | `ghost init` | Scaffold `.ghost/` with the skeleton starter: manifest, glossary, a `brand.md` cover, foundation chapters, context nodes, and the cliche floor. `--template minimal` writes a smaller cover-led starter. `--with checks` also adds the checks directory. | | `ghost checks init` | Scaffold `.ghost/checks/` with an example review assertion. | | `ghost validate [file-or-dir]` | Validate manifest, nodes, material locators, check references, and glossary kind prefixes. | -| `ghost gather [ask…] [--format json]` | Emit the node menu for selection plus coverage line; log exposed ids. | +| `ghost gather [ask…] [--format json]` | Emit the complete guidance menu plus coverage line; log exposed ids. | | `ghost pull […]` | Emit selected nodes' full bodies and materials in steering order; log selected/missed ids. | | `ghost review [--diff ] [--base ] [--format json] [--no-probes]` | Emit an advisory review packet for a diff (requires `.ghost/checks/`). | | `ghost export [--out ] [--no-checks] [--strict] [--format json]` | Package `.ghost/` as a portable brand artifact and report which material locators will not travel. | @@ -129,7 +132,7 @@ conventions, but durable brand truth should be curated by the human. A silent fingerprint does not require stopping. Proceed from nearby product surfaces, local conventions, and ordinary reasoning when safe, and label that -reasoning as provisional and non-Ghost-backed — unless the fingerprint itself +reasoning as provisional and non-Ghost-backed unless the fingerprint itself declares a stricter silence posture (check the cover), which overrides this default. Ask a human before high-risk, irreversible, privacy, security, legal, or brand-defining choices. diff --git a/packages/ghost/src/skill-bundle/references/blocks.md b/packages/ghost/src/skill-bundle/references/blocks.md index 6d6f31e0..3b44688a 100644 --- a/packages/ghost/src/skill-bundle/references/blocks.md +++ b/packages/ghost/src/skill-bundle/references/blocks.md @@ -26,11 +26,10 @@ whatever keeps each node purpose-coherent. ## Where it sits A fingerprint declares stance. Block and material nodes ground that stance in -concrete materials — they are what makes a node **concrete** in gather, pull -ordering, and review matching. A realizing agent **reads** them and matches -against them. Strip every block node and the fingerprint is still valid — it -just gives the agent less to draw on, and prose-only steering is weak steering. -The agent does the matching. +concrete material: materials, substantial fenced examples, or Skeletons. A +realizing agent **reads** them and matches against them. Strip every block node +and the fingerprint is still valid — it just gives the agent less to draw on, +and prose-only steering is weak steering. The agent does the matching. ## Concreteness is the grounding dial @@ -90,10 +89,10 @@ not a swappable implementation detail. ## How a match runs -The agent reads the fingerprint's stance, `gather`s the menu, ranks candidate -block nodes by description, separates near-neighbors on *not when* and -*never*, and assembles. The realizing surface authors the chosen blocks in its -medium. The fingerprint never named a component; the agent bridged via documented +The agent reads the fingerprint's stance, `gather`s the menu, pulls block nodes +whose descriptions apply, separates near-neighbors on *not when* and *never*, +and assembles. The realizing surface authors the chosen blocks in its medium. +The fingerprint never named a component; the agent bridged via documented purpose. ## Curation rule diff --git a/packages/ghost/src/skill-bundle/references/brief.md b/packages/ghost/src/skill-bundle/references/brief.md index f0e83145..679326e2 100644 --- a/packages/ghost/src/skill-bundle/references/brief.md +++ b/packages/ghost/src/skill-bundle/references/brief.md @@ -9,12 +9,14 @@ A brief is an ephemeral steering packet for the generating pass. It is not a new schema and is never written back into `.ghost/`. 1. Run `ghost gather --format json` and select against descriptions. -2. The cover is already in context. Pull a small set from the menu: **3–5 nodes - is normal; 10 is a bad selection** unless the task is unusually broad. -3. Prefer concrete nodes: `materials`, substantial fenced examples, or a - `## Skeleton`. If there is **no concrete material for this surface**, the - readiness ceiling is **Yellow**. -4. Keep provisional reasoning visibly separate from Ghost-backed claims. +2. The cover is already in context and outside selection. Pull every applicable + node with `ghost pull […]`; skip nodes whose stated situation does + not apply. Topic overlap alone is not applicability. +3. Note gather payload labels where present: `materials`, substantial fenced + examples, or a `## Skeleton`. If there is no concrete material for this + surface, the readiness ceiling is **Yellow**. +4. If no node applies, name the fingerprint's silence, follow the cover silence + posture, and keep provisional reasoning separate from Ghost-backed claims. ## The packet: five sections only @@ -40,8 +42,8 @@ Return this shape: Rules: - Do not add sections for every kind. Sections dilute instruction weight. -- Treat `ghost pull` ordering as signal: stance first, concrete material next, - prose rules, Skeletons dead last. +- Treat `ghost pull` ordering as packet structure: stance first, concrete-material + nodes next, prose rules, Skeletons dead last. - If a pulled Skeleton matches the surface, begin the artifact from it verbatim before filling. - Anti-goals are review-critical: state the positive replacement, not just the diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md index d39c066e..3d3572c2 100644 --- a/packages/ghost/src/skill-bundle/references/capture.md +++ b/packages/ghost/src/skill-bundle/references/capture.md @@ -177,14 +177,18 @@ Near the moment of payment, reduce felt risk. Proximity of reassurance to the action beats completeness... ``` -- **`description`** is how an agent finds the node: a one-line "what this is and - when to gather it." `ghost gather` emits the menu of id, kind, description, - concrete coverage, and material count; the agent matches the ask against it. +- **`description`** is how an agent finds the node: a compact retrieval payload + naming what the node governs, the observable condition under which it applies, + and what it contributes when useful. `ghost gather` emits id, kind, + description, concrete coverage, payload labels, and material count; the agent matches the ask + against applicability. - **Kind is the filename prefix** and must be a kind the glossary declares. A bare name (`voice.md`) has no kind. - **Altitude lives in the prose.** State a universal truth plainly; give a - narrower truth its **condition** — the situation it applies in — in the prose. - Never file a truth by destination (`for-emails.md`). + narrower truth its **condition** — the situation it applies in — in the prose + and usually in the description. Do not use broad universal imperatives unless + universal retrieval is intended. Never file a truth by destination + (`for-emails.md`). - **Concreteness is derived.** A node carries concrete material when it has `materials`, a substantial fenced example, or a `## Skeleton`. You do not declare a separate type. @@ -200,8 +204,8 @@ machinery already scores: - **why** lands as stance prose — read first in `ghost pull`, weakest at moving output form on its own; it is the yardstick every later selection is measured against. -- **with what** lands as `materials` and concreteness — drives pull ordering - and `ghost review` matching. See [blocks.md](blocks.md) for authoring +- **with what** lands as `materials` and concreteness — affects pull packet + order and `ghost review` matching. See [blocks.md](blocks.md) for authoring material-backed nodes. - **how it is assembled** lands as patterns and `## Skeleton` sections — the strongest steering; Skeleton fences are extracted and emitted dead last so diff --git a/packages/ghost/src/skill-bundle/references/recall.md b/packages/ghost/src/skill-bundle/references/recall.md index 596d03cb..0b9adbb1 100644 --- a/packages/ghost/src/skill-bundle/references/recall.md +++ b/packages/ghost/src/skill-bundle/references/recall.md @@ -8,7 +8,10 @@ description: Gather and pull the applicable Ghost brand truths for a task. 1. Run `ghost gather ` for the actual task. The cover is inlined by gather; do not pull it separately. Read the coverage line: all-prose fingerprints are weak steering. -2. Select against `description`; Ghost never selects for you. +2. Select against `description`; Ghost never selects for you. Pull every node + whose stated situation applies and whose truth, material, structure, or + refusal governs the work. Skip inapplicable nodes. Topic overlap alone is not + applicability. 3. Run `ghost pull […]`. Prefer `pull` over reading files directly: it orders the packet for steering, inlines small local materials, emits inspect-pointers for binary materials, extracts Skeletons last, and logs the @@ -23,10 +26,11 @@ description: Gather and pull the applicable Ghost brand truths for a task. 2. Concrete-material nodes: `materials`, substantial fenced examples, or `## Skeleton` sections, with materials inlined or pointed to inspect. 3. Prose-only rules: principles, conditions, decisions, and other rules without - concrete payload. + concrete material. 4. Skeleton fences dead last: if one matches the surface, start from that structure verbatim. Return the selected truths with node ids, how each applies, and where the -fingerprint is silent. If silence is safe, proceed provisionally and label it; +fingerprint is silent. If no node applies, say so and do not invent Ghost-backed +guidance. If silence is safe, proceed provisionally and label it; if it is brand-defining or high-risk, ask or author a node first. diff --git a/packages/ghost/src/skill-bundle/references/schema.md b/packages/ghost/src/skill-bundle/references/schema.md index 1f7f55a1..5500a49e 100644 --- a/packages/ghost/src/skill-bundle/references/schema.md +++ b/packages/ghost/src/skill-bundle/references/schema.md @@ -48,13 +48,17 @@ Use the full lockup when recognition matters. - Identity is the filename minus `.md`. - Kind is the first dotted segment of the filename. -- `description` is the retrieval payload shown by `ghost gather`. +- `description` is the retrieval payload shown by `ghost gather`: what the node + governs, the observable condition under which it applies, and what it + contributes where useful. Avoid broad universal wording unless universal + retrieval is intended. - `materials` accepts repo-relative paths/globs plus absolute HTTPS URLs. It is a locator list, not guidance. -Ghost derives whether a node carries concrete material from structure: non-empty -`materials`, a fenced code block of at least 3 lines, or a `## Skeleton` section. -This is reported in gather/pulse and used for pull ordering. +Ghost derives whether a node carries concrete material from structure: +non-empty `materials`, a fenced code block of at least 3 lines, or a +`## Skeleton` section. `gather` reports these payload labels for clarity; they +are not ranking signals. ## Skeleton convention @@ -101,8 +105,8 @@ probes are the same class as npm scripts; Git review is the boundary. ## Gather / Pull / Review -- `ghost gather` emits the cover above the node menu, then coverage counts. - Checks are invisible. +- `ghost gather` emits the cover above Available guidance, then coverage counts. + The guidance list is complete, unfiltered, and unranked. Checks are invisible. - `ghost pull` emits selected nodes in steering order and inlines small local materials. Binary local materials become inspect-pointers. - `ghost review` matches diff files to local node materials, offers relevant diff --git a/packages/ghost/src/skill-bundle/references/steering-audit.md b/packages/ghost/src/skill-bundle/references/steering-audit.md index 449ce996..1b8d8e7b 100644 --- a/packages/ghost/src/skill-bundle/references/steering-audit.md +++ b/packages/ghost/src/skill-bundle/references/steering-audit.md @@ -25,10 +25,12 @@ Report first: - **Concreteness coverage:** total nodes, concrete-material nodes, prose-only nodes. Concrete means non-empty `materials`, a fenced code block of at least 3 - lines, or a `## Skeleton` section. -- **Pulse by concreteness:** concrete exposure/pull rate vs prose-only - exposure/pull rate. This is the tuning instrument: if concrete nodes are not - pulled, descriptions or task selection are failing. + lines, or a `## Skeleton` section. `ghost gather` also breaks out materials, + substantial fenced examples, and Skeletons as payload labels. +- **Pulse by concreteness:** concrete-material exposure/pull rate vs prose-only + exposure/pull rate. In markdown this is the `Concrete material` row. This is + the tuning instrument: if concrete nodes are not pulled when applicable, + descriptions or task selection are failing. ## Corpus-level table diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts index ad1d7449..de05e5ab 100644 --- a/packages/ghost/test/cli.test.ts +++ b/packages/ghost/test/cli.test.ts @@ -708,9 +708,11 @@ describe("ghost CLI", () => { const markdown = await runCli(["gather"], dir); expect(markdown.code).toBe(0); - expect(markdown.stdout).toContain("## Cover — `brand`"); + expect(markdown.stdout).toContain("## Cover in context: `brand`"); expect(markdown.stdout).toContain("This cover is unwritten."); - expect(markdown.stdout).toContain("9 nodes · 0 carry concrete material"); + expect(markdown.stdout).toContain( + "9 nodes · 0 carry payloads (0 with materials, 0 with substantial fenced examples, 0 with Skeletons)", + ); expect(markdown.stdout).not.toContain("- `brand`"); const json = await runCli(["gather", "--format", "json"], dir); @@ -719,6 +721,8 @@ describe("ghost CLI", () => { expect(payload.cover).toMatchObject({ id: "brand", body: expect.stringContaining("This cover is unwritten."), + inContext: true, + selectable: false, }); expect(payload.nodes.map((node: { id: string }) => node.id)).not.toContain( "brand", @@ -726,6 +730,7 @@ describe("ghost CLI", () => { expect(payload.coverage).toEqual({ nodes: 9, concrete: 0, + payloads: { materials: 0, fencedExamples: 0, skeletons: 0 }, undescribed: 0, }); }); @@ -741,7 +746,9 @@ describe("ghost CLI", () => { expect(markdown.code).toBe(0); expect(markdown.stdout).not.toContain("## Cover"); // With no resolvable cover, brand stays a selectable menu node. - expect(markdown.stdout).toContain("10 nodes · 0 carry concrete material"); + expect(markdown.stdout).toContain( + "10 nodes · 0 carry payloads (0 with materials, 0 with substantial fenced examples, 0 with Skeletons)", + ); expect(markdown.stdout).toContain("- `brand`"); const json = await runCli(["gather", "--format", "json"], dir); @@ -891,10 +898,13 @@ describe("ghost CLI", () => { expect(JSON.parse(gather.stdout).coverage).toEqual({ nodes: 4, concrete: 1, + payloads: { materials: 1, fencedExamples: 0, skeletons: 0 }, undescribed: 0, }); const markdown = await runCli(["gather"], dir); - expect(markdown.stdout).toContain("4 nodes · 1 carry concrete material"); + expect(markdown.stdout).toContain( + "4 nodes · 1 carry payloads (1 with materials, 0 with substantial fenced examples, 0 with Skeletons)", + ); // No undescribed nodes: the coverage line stays quiet about them. expect(markdown.stdout).not.toContain("lack descriptions"); @@ -906,7 +916,7 @@ describe("ghost CLI", () => { ); const gatherMute = await runCli(["gather"], dir); expect(gatherMute.stdout).toContain( - "5 nodes · 1 carry concrete material · 1 lack descriptions", + "5 nodes · 1 carry payloads (1 with materials, 0 with substantial fenced examples, 0 with Skeletons) · 1 lack descriptions", ); const gatherMuteJson = await runCli(["gather", "--format", "json"], dir); expect(JSON.parse(gatherMuteJson.stdout).coverage.undescribed).toBe(1); @@ -929,6 +939,87 @@ describe("ghost CLI", () => { ); }); + it("gather names payload types without ranking them", async () => { + await runCli(["init", "--template", "minimal"], dir); + await writeFile( + join(dir, ".ghost", "asset.tokens.md"), + [ + "---", + "description: Token material.", + "materials:", + " - materials/tokens.css", + "---", + "", + "Use tokens.", + "", + ].join("\n"), + ); + await writeFile( + join(dir, ".ghost", "exemplar.copy.md"), + [ + "---", + "description: Copy exemplar.", + "---", + "", + "```txt", + "one", + "two", + "three", + "```", + "", + ].join("\n"), + ); + await writeFile( + join(dir, ".ghost", "pattern.card.md"), + [ + "---", + "description: Card pattern.", + "---", + "", + "## Skeleton", + "", + "```tsx", + "
", + "
", + "
", + "
", + "```", + "", + ].join("\n"), + ); + + const json = await runCli(["gather", "card", "--format", "json"], dir); + expect(json.code).toBe(0); + const payload = JSON.parse(json.stdout); + expect(payload.coverage.payloads).toEqual({ + materials: 1, + fencedExamples: 1, + skeletons: 1, + }); + const exemplar = payload.nodes.find( + (node: { id: string }) => node.id === "exemplar.copy", + ); + expect(exemplar).toMatchObject({ + concrete: true, + hasFencedExample: true, + }); + const pattern = payload.nodes.find( + (node: { id: string }) => node.id === "pattern.card", + ); + expect(pattern).toMatchObject({ + concrete: true, + hasSkeleton: true, + }); + expect(pattern.hasFencedExample).toBeUndefined(); + + const markdown = await runCli(["gather", "card"], dir); + expect(markdown.stdout).toContain("payloads: substantial fenced example"); + expect(markdown.stdout).toContain("payloads: Skeleton"); + expect(markdown.stdout).not.toContain( + "payloads: substantial fenced example, Skeleton", + ); + }); + it("pull extracts Skeletons last and validate warns on malformed Skeleton sections", async () => { await runCli(["init", "--template", "minimal"], dir); await writeFile( @@ -957,6 +1048,55 @@ describe("ghost CLI", () => { expect(validate.stdout).toContain("skeleton-fence-count"); }); + it("pull uses fences longer than inlined material and Skeleton backtick runs", async () => { + await runCli(["init", "--template", "minimal"], dir); + await mkdir(join(dir, "brand"), { recursive: true }); + await writeFile( + join(dir, "brand", "example.md"), + [ + "Before.", + "```ts", + "const value = `inside`;", + "```", + "````", + "four", + "````", + "After.", + "", + ].join("\n"), + ); + await writeFile( + join(dir, ".ghost", "pattern.safe.md"), + [ + "---", + "description: Fence safety.", + "materials:", + " - brand/example.md", + "---", + "", + "Pattern prose.", + "", + "## Skeleton", + "", + "```md", + "Wrapper text", + "````", + "inner four", + "````", + "```", + "", + ].join("\n"), + ); + + const pull = await runCli(["pull", "pattern.safe"], dir); + + expect(pull.code).toBe(0); + expect(pull.stdout).toContain("`````brand/example.md"); + expect(pull.stdout).toContain("`````md"); + expect(pull.stdout).toContain("````\nfour\n````"); + expect(pull.stdout).toContain("````\ninner four\n````"); + }); + it("pull emits binary materials as inspect-pointers in markdown and JSON", async () => { await runCli(["init", "--template", "minimal"], dir); await mkdir(join(dir, "brand"), { recursive: true }); @@ -999,14 +1139,34 @@ describe("ghost CLI", () => { expect(gather.code).toBe(0); const menuPayload = JSON.parse(gather.stdout); expect(menuPayload.ask).toBe("checkout confirmation"); + expect(menuPayload.source).toEqual({ + artifact: "Ghost brand fingerprint", + list: "Available guidance", + }); + expect(menuPayload.contract).toMatchObject({ + completeness: { + complete: true, + filtered: false, + ranked: false, + selectedByGhost: false, + }, + selection: { + basis: "applicability", + topicOverlapAloneIsApplicability: false, + addForCompleteness: false, + omitApplicableForCount: false, + }, + }); + expect(menuPayload.next.command).toBe("ghost pull […]"); + expect(menuPayload.silence.ifNoneApply).toContain("do not invent"); expect( menuPayload.nodes.some((n: { id: string }) => n.id === "voice"), ).toBe(true); const gatherMarkdown = await runCli(["gather", "checkout", "hero"], dir); - expect(gatherMarkdown.stdout).toContain( - "# Ghost Nodes — for: checkout hero", - ); + expect(gatherMarkdown.stdout).toContain("# Ghost brand fingerprint"); + expect(gatherMarkdown.stdout).toContain("Ask: checkout hero"); + expect(gatherMarkdown.stdout).toContain("## Available guidance"); const pull = await runCli(["pull", "principle.trust", "voice"], dir); expect(pull.code).toBe(0); diff --git a/packages/ghost/test/ghost-core/catalog-assemble.test.ts b/packages/ghost/test/ghost-core/catalog-assemble.test.ts index b5c6b3b8..958f6737 100644 --- a/packages/ghost/test/ghost-core/catalog-assemble.test.ts +++ b/packages/ghost/test/ghost-core/catalog-assemble.test.ts @@ -65,8 +65,11 @@ describe("assembleCatalog (flat catalog assembly)", () => { expect(catalog.nodes.get("asset.tokens")?.concrete).toBe(true); expect(catalog.nodes.get("principle.sample")?.concrete).toBe(true); + expect(catalog.nodes.get("principle.sample")?.hasFencedExample).toBe(true); expect(catalog.nodes.get("pattern.shell")?.concrete).toBe(true); + expect(catalog.nodes.get("pattern.shell")?.hasFencedExample).toBe(false); expect(catalog.nodes.get("pattern.shell")?.hasSkeleton).toBe(true); expect(catalog.nodes.get("principle.short")?.concrete).toBe(false); + expect(catalog.nodes.get("principle.short")?.hasFencedExample).toBe(false); }); });