@canonical/pragma-cli 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +14 -13
  4. package/src/bin.ts +2 -2
  5. package/src/capabilities/block/blockList.test.ts +79 -0
  6. package/src/capabilities/block/blockList.verb.ts +18 -5
  7. package/src/capabilities/block/pack.ts +12 -1
  8. package/src/capabilities/block/parity.test.ts +9 -2
  9. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  10. package/src/capabilities/capabilities/capabilities.verb.ts +1 -1
  11. package/src/capabilities/capabilities/catalog.ts +7 -3
  12. package/src/capabilities/capabilities/hints.ts +8 -17
  13. package/src/capabilities/capabilities/types.ts +2 -0
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +4 -1
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/pragmaColophon.ts +4 -4
  20. package/src/capabilities/colophon/types.ts +1 -1
  21. package/src/capabilities/config/fields.ts +21 -92
  22. package/src/capabilities/config/set.test.ts +6 -6
  23. package/src/capabilities/config/set.verb.ts +5 -5
  24. package/src/capabilities/config/show.render.test.ts +65 -0
  25. package/src/capabilities/config/show.render.ts +56 -10
  26. package/src/capabilities/config/show.verb.ts +5 -5
  27. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  28. package/src/capabilities/create/byteEquality.test.ts +7 -4
  29. package/src/capabilities/create/compiledCreate.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/create.test.ts +32 -3
  31. package/src/capabilities/create/create.verb.ts +69 -11
  32. package/src/capabilities/doctor/checks/checkConfigFile.ts +1 -1
  33. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  34. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +10 -4
  35. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  36. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  37. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  38. package/src/capabilities/doctor/doctor.render.ts +116 -34
  39. package/src/capabilities/doctor/doctor.test.ts +31 -1
  40. package/src/capabilities/doctor/runChecks.ts +28 -4
  41. package/src/capabilities/doctor/types.ts +15 -0
  42. package/src/capabilities/ontology/index.ts +10 -3
  43. package/src/capabilities/ontology/ontology.test.ts +157 -5
  44. package/src/capabilities/ontology/verbs.ts +192 -93
  45. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  46. package/src/capabilities/prompt/prompt.render.ts +23 -6
  47. package/src/capabilities/prompt/prompt.test.ts +61 -0
  48. package/src/capabilities/reference.test.ts +32 -0
  49. package/src/capabilities/resources/provider.ts +28 -14
  50. package/src/capabilities/resources/resources.test.ts +99 -0
  51. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  52. package/src/capabilities/setup/index.ts +7 -1
  53. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  54. package/src/capabilities/setup/operations/setupGenerator.ts +157 -61
  55. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  56. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  57. package/src/capabilities/setup/operations/setupSkills.ts +7 -1
  58. package/src/capabilities/setup/setup.render.test.ts +109 -0
  59. package/src/capabilities/setup/setup.render.ts +61 -14
  60. package/src/capabilities/setup/setup.test.ts +352 -1
  61. package/src/capabilities/setup/setup.verb.ts +57 -5
  62. package/src/capabilities/setup/types.ts +59 -2
  63. package/src/capabilities/shared/bands.ts +16 -0
  64. package/src/capabilities/sources/collectStatus.ts +19 -4
  65. package/src/capabilities/sources/runUpdate.ts +58 -1
  66. package/src/capabilities/sources/sources.test.ts +187 -1
  67. package/src/capabilities/sources/status.render.test.ts +65 -0
  68. package/src/capabilities/sources/status.render.ts +50 -23
  69. package/src/capabilities/surface.test.ts +34 -9
  70. package/src/capabilities/tier/lookup.render.test.ts +72 -0
  71. package/src/capabilities/tier/lookup.render.ts +33 -8
  72. package/src/capabilities/tier/lookup.test.ts +15 -0
  73. package/src/capabilities/tier/runLookup.ts +28 -0
  74. package/src/constants.test.ts +1 -1
  75. package/src/constants.ts +2 -2
  76. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +13 -14
  77. package/src/kernel/completion/bashDrive.test.ts +3 -3
  78. package/src/kernel/completion/complete.test.ts +3 -5
  79. package/src/kernel/completion/entitySource.ts +14 -6
  80. package/src/kernel/completion/model.test.ts +2 -2
  81. package/src/kernel/completion/model.ts +0 -1
  82. package/src/kernel/completion/parse.test.ts +2 -4
  83. package/src/kernel/error/PragmaError.test.ts +4 -4
  84. package/src/kernel/error/renderError.ts +1 -1
  85. package/src/kernel/packs/compile.ts +1 -1
  86. package/src/kernel/project/cli/buildProgram.test.ts +51 -7
  87. package/src/kernel/project/cli/buildProgram.ts +11 -0
  88. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  89. package/src/kernel/project/cli/globalFlags.ts +14 -14
  90. package/src/kernel/project/cli/rootHelp.ts +4 -2
  91. package/src/kernel/project/mcp/instructions.ts +1 -1
  92. package/src/kernel/project/mcp/mcpError.ts +52 -0
  93. package/src/kernel/project/mcp/prompts/prompts.test.ts +8 -0
  94. package/src/kernel/project/mcp/prompts/provider.ts +36 -6
  95. package/src/kernel/render/formatters.ts +5 -4
  96. package/src/kernel/render/renderers.test.ts +34 -0
  97. package/src/kernel/render/renderers.ts +25 -7
  98. package/src/kernel/render/style.test.ts +31 -0
  99. package/src/kernel/render/style.ts +74 -0
  100. package/src/kernel/runtime/graphpack/build.ts +28 -0
  101. package/src/kernel/runtime/graphpack/buildIndex.test.ts +103 -0
  102. package/src/kernel/runtime/graphpack/buildIndex.ts +124 -36
  103. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +3 -6
  104. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -2
  105. package/src/kernel/runtime/graphpack/graphpack.test.ts +69 -1
  106. package/src/kernel/runtime/graphpack/read.ts +28 -13
  107. package/src/kernel/runtime/graphpack/types.ts +14 -0
  108. package/src/kernel/runtime/loadSession.ts +4 -12
  109. package/src/kernel/runtime/refs/resolve.ts +63 -3
  110. package/src/kernel/runtime/storeReadiness.ts +61 -0
  111. package/src/kernel/runtime/types.ts +1 -1
  112. package/src/kernel/runtime/wasmEmbed.test.ts +3 -1
  113. package/src/kernel/spec/emitReference.test.ts +196 -0
  114. package/src/kernel/spec/emitReference.ts +491 -0
  115. package/src/kernel/spec/emitSurface.ts +4 -2
  116. package/src/kernel/spec/index.ts +2 -0
  117. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  118. package/src/testing/PARITY_GAPS.ts +1 -1
  119. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  120. package/src/testing/behavioral/completion.test.ts +2 -2
  121. package/src/testing/behavioral/disclosure.test.ts +12 -0
  122. package/src/testing/behavioral/docExamples.test.ts +283 -0
  123. package/src/testing/behavioral/errorMatrix.storeless.test.ts +2 -1
  124. package/src/testing/behavioral/journeys.cli.test.ts +3 -1
  125. package/src/testing/behavioral/journeys.defaultPack.test.ts +571 -0
  126. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  127. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  128. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +176 -0
  129. package/src/testing/behavioral/mcpSurface.test.ts +1 -1
  130. package/src/testing/behavioral/staleCommands.test.ts +76 -0
  131. package/src/testing/eval/__snapshots__/eval.test.ts.snap +2 -2
  132. package/src/testing/eval/cases/assembly.ts +3 -3
  133. package/src/testing/eval/cases/effectful.ts +6 -3
  134. package/src/testing/eval/cases/readNouns.ts +4 -1
  135. package/src/testing/fixtures/fixtureCapability.ts +156 -0
  136. package/src/testing/fixtures/graph/defaultPack.ts +178 -0
  137. package/src/testing/fixtures/graph/owlExport.ts +95 -0
  138. package/src/testing/helpers/extractSnippets.ts +29 -0
  139. package/src/testing/helpers/golden.ts +1 -1
  140. package/src/capabilities/config/field.test.ts +0 -188
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @canonical/pragma-cli
2
+
3
+ `pragma` is a command-line tool **and** a Model Context Protocol (MCP) server over Canonical's design-system knowledge graph. A single grammar of capabilities is projected two ways — as CLI commands for humans and as MCP tools for agents — both reading the same local, content-addressed store built from your design-system packages.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @canonical/pragma-cli
9
+ ```
10
+
11
+ This installs the `pragma` binary: a prebuilt, standalone executable (`dist/pragma`). The package publishes for **Linux x64 only** (`"os": ["linux"]`, `"cpu": ["x64"]`) — other platforms are not yet shipped.
12
+
13
+ Check your environment and confirm the install:
14
+
15
+ ```bash
16
+ pragma doctor
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ ```bash
22
+ pragma sources update
23
+ pragma sources status
24
+ pragma block list
25
+ pragma block lookup Button
26
+ pragma capabilities
27
+ ```
28
+
29
+ `pragma sources update` resolves the design-system packages named in your `pragma.config.ts`, builds one local store, and writes `pragma.lock.json`. Once built, every read command answers from the store with no network access.
30
+
31
+ To register pragma with a detected AI harness as an MCP server:
32
+
33
+ ```bash
34
+ pragma setup mcp
35
+ ```
36
+
37
+ See [docs/mcp-integration.md](./docs/mcp-integration.md) for the full MCP surface, and the [command & tool reference](./docs/reference/index.md) for every command and tool.
38
+
39
+ ## Two planes
40
+
41
+ pragma is extended along two independent planes:
42
+
43
+ - **Data plane — the content.** Your design system is a set of semantic packages listed in `pragma.config.ts` (`packages: [...]`). `pragma sources update` compiles them into the local knowledge graph the read commands query. Point pragma at different packages and every `list` / `lookup` / `sample` command answers from that graph.
44
+ - **Behaviour plane — the capabilities.** Every command and MCP tool is one entry in a single capability grammar, projected to both the CLI and the MCP server. The [reference](./docs/reference/index.md) is generated from that grammar, so it can never drift from the code.
45
+
46
+ ## Relationship to summon
47
+
48
+ `@canonical/summon` is a separate scaffolding product. pragma reuses summon's generator core so that its scaffolding commands —
49
+
50
+ ```bash
51
+ pragma create component src/components/Button --framework react
52
+ ```
53
+
54
+ — produce output byte-identical to summon's own `component`, `package`, and `application` generators. summon is not a runtime dependency: the generators are embedded in the `pragma` binary, so `pragma create component` runs from a clean install.
55
+
56
+ ## MCP
57
+
58
+ Run pragma as an MCP server over stdio via `pragma setup mcp` (automatic harness registration) or the manual `pragma mcp` entry point. The server exposes the read and scaffold tools, a `pragma:{+uri}` resource surface for entity reads, the design system's workflow prompts, and handshake instructions describing the discovery sequence. Mutating tools are plan-first — they return the plan they would apply until called with `confirm: true`. See [docs/mcp-integration.md](./docs/mcp-integration.md).
59
+
60
+ ## Documentation
61
+
62
+ - [Getting started](./docs/getting-started.md)
63
+ - [MCP integration](./docs/mcp-integration.md)
64
+ - [Configuration model](./docs/config-model.md)
65
+ - [Architecture](./docs/architecture.md)
66
+ - [Skills](./docs/skills.md)
67
+ - [Command & tool reference](./docs/reference/index.md)
68
+ - [Changelog](./CHANGELOG.md)
69
+
70
+ ## License
71
+
72
+ GPL-3.0
package/dist/pragma CHANGED
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@canonical/pragma-cli",
3
3
  "description": "Next-generation CLI and MCP kernel for Canonical's design system (pragma v2).",
4
- "version": "0.32.0",
4
+ "version": "0.33.0",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",
7
7
  "types": "src/index.ts",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "scripts": {
38
38
  "build": "bun run scripts/build.ts",
39
+ "gen:reference": "bun run scripts/genReference.ts",
39
40
  "build:compile": "bun build --compile --minify --target=bun-linux-x64 src/bin.ts --outfile dist/pragma",
40
41
  "check": "bun run check:biome && bun run check:ts && bun run check:webarchitect",
41
42
  "check:webarchitect": "webarchitect tool-ts",
@@ -51,14 +52,14 @@
51
52
  "eval": "bun run src/testing/eval/report.ts"
52
53
  },
53
54
  "dependencies": {
54
- "@canonical/harnesses": "^0.32.0",
55
- "@canonical/ke": "^0.32.0",
56
- "@canonical/ke-graphql": "^0.32.0",
57
- "@canonical/summon-application": "^0.32.0",
58
- "@canonical/summon-component": "^0.32.0",
59
- "@canonical/summon-core": "^0.32.0",
60
- "@canonical/summon-package": "^0.32.0",
61
- "@canonical/task": "^0.32.0",
55
+ "@canonical/harnesses": "^0.33.0",
56
+ "@canonical/ke": "^0.33.0",
57
+ "@canonical/ke-graphql": "^0.33.0",
58
+ "@canonical/summon-application": "^0.33.0",
59
+ "@canonical/summon-component": "^0.33.0",
60
+ "@canonical/summon-core": "^0.33.0",
61
+ "@canonical/summon-package": "^0.33.0",
62
+ "@canonical/task": "^0.33.0",
62
63
  "@modelcontextprotocol/sdk": "^1.12.0",
63
64
  "chalk": "^5.6.2",
64
65
  "commander": "^14.0.3",
@@ -68,14 +69,14 @@
68
69
  },
69
70
  "devDependencies": {
70
71
  "@biomejs/biome": "2.4.9",
71
- "@canonical/biome-config": "^0.32.0",
72
- "@canonical/typescript-config": "^0.32.0",
73
- "@canonical/webarchitect": "^0.32.0",
72
+ "@canonical/biome-config": "^0.33.0",
73
+ "@canonical/typescript-config": "^0.33.0",
74
+ "@canonical/webarchitect": "^0.33.0",
74
75
  "@types/bun": "^1.3.10",
75
76
  "@types/node": "^24.12.0",
76
77
  "@vitest/coverage-v8": "^4.0.18",
77
78
  "typescript": "^5.9.3",
78
79
  "vitest": "^4.0.18"
79
80
  },
80
- "gitHead": "5b279d8150e3988c68c6c2a741046bcd425624ca"
81
+ "gitHead": "fca2bc4440fff2d511fc8db6686f9b19222fdbc7"
81
82
  }
package/src/bin.ts CHANGED
@@ -77,7 +77,7 @@ async function main(): Promise<void> {
77
77
  if (
78
78
  !explicitHelp &&
79
79
  rawFormat !== undefined &&
80
- !["plain", "json", "text"].includes(rawFormat)
80
+ !["plain", "llm", "json", "text"].includes(rawFormat)
81
81
  ) {
82
82
  const [
83
83
  { PragmaError },
@@ -89,7 +89,7 @@ async function main(): Promise<void> {
89
89
  import("./kernel/project/cli/exitCodes.js"),
90
90
  ]);
91
91
  const error = PragmaError.invalidInput("format", rawFormat, {
92
- validOptions: ["plain", "json"],
92
+ validOptions: ["plain", "llm", "json"],
93
93
  });
94
94
  const rendered =
95
95
  globalFlags.format === "json"
@@ -0,0 +1,79 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { verbKey } from "../../kernel/packs/uniqueness.js";
3
+ import type { PragmaRuntime } from "../../kernel/runtime/types.js";
4
+ import type { VerbSpec } from "../../kernel/spec/types.js";
5
+ import { OWL_EXPORT_TTL } from "../../testing/fixtures/graph/owlExport.js";
6
+ import { bootFixtureRuntime } from "../../testing/helpers/fixtureGraph.js";
7
+ import type { BlockRow } from "./blockList.verb.js";
8
+ import { blockModule } from "./index.js";
9
+
10
+ const listVerb = blockModule.verbs.find(
11
+ (verb) => verbKey(verb.path) === "block list",
12
+ ) as VerbSpec;
13
+
14
+ const names = (rows: BlockRow[]): string[] =>
15
+ rows.map((row) => row.name).sort();
16
+
17
+ /**
18
+ * The OWL-export fixture ships an UNTIERED `ds:Component` (`ds:datePicker`, no
19
+ * `ds:tier`) beside a tiered one (`ds:button`). The pre-fix `block list` gated
20
+ * on a REQUIRED `ds:tier` triple pattern, so `--all-tiers` — which drops the
21
+ * FILTER but not the pattern — still hid the untiered block, contradicting the
22
+ * flag's promise to show every block (A2). A tier-scoped view legitimately
23
+ * omits an untiered block (it joins the list through its tier); `--all-tiers`
24
+ * is the escape that reveals it.
25
+ */
26
+ describe("block list — untiered blocks under --all-tiers (A2)", () => {
27
+ describe("no tier configured", () => {
28
+ let runtime: PragmaRuntime;
29
+ let dispose: () => Promise<void>;
30
+
31
+ beforeAll(async () => {
32
+ const fixture = await bootFixtureRuntime({ ttl: OWL_EXPORT_TTL });
33
+ runtime = fixture.runtime;
34
+ dispose = fixture.dispose;
35
+ });
36
+ afterAll(() => dispose());
37
+
38
+ it("the default view still omits the untiered block", async () => {
39
+ const rows = (await listVerb.run({}, runtime)) as BlockRow[];
40
+ expect(names(rows)).toEqual(["Button"]);
41
+ });
42
+
43
+ it("--all-tiers reveals the untiered block", async () => {
44
+ const rows = (await listVerb.run(
45
+ { allTiers: true },
46
+ runtime,
47
+ )) as BlockRow[];
48
+ expect(names(rows)).toEqual(["Button", "Date Picker"]);
49
+ });
50
+ });
51
+
52
+ describe("a tier IS configured", () => {
53
+ let runtime: PragmaRuntime;
54
+ let dispose: () => Promise<void>;
55
+
56
+ beforeAll(async () => {
57
+ const fixture = await bootFixtureRuntime({
58
+ ttl: OWL_EXPORT_TTL,
59
+ config: { tier: "global", channel: "normal" },
60
+ });
61
+ runtime = fixture.runtime;
62
+ dispose = fixture.dispose;
63
+ });
64
+ afterAll(() => dispose());
65
+
66
+ it("a scoped tier lists its tiered block, not the untiered one", async () => {
67
+ const rows = (await listVerb.run({}, runtime)) as BlockRow[];
68
+ expect(names(rows)).toEqual(["Button"]);
69
+ });
70
+
71
+ it("--all-tiers overrides the scope and reveals the untiered block", async () => {
72
+ const rows = (await listVerb.run(
73
+ { allTiers: true },
74
+ runtime,
75
+ )) as BlockRow[];
76
+ expect(names(rows)).toContain("Date Picker");
77
+ });
78
+ });
79
+ });
@@ -84,14 +84,25 @@ function buildQuery(
84
84
  ): string {
85
85
  const tierFilter = allTiers ? "" : buildTierFilter(tier);
86
86
  const channelFilter = buildChannelFilter(channel);
87
+ // Under --all-tiers, `ds:name`/`ds:tier` become OPTIONAL so a real block that
88
+ // ships without either is no longer hidden — the flag promises "all blocks",
89
+ // yet the pre-fix query gated on those as REQUIRED patterns, dropping the
90
+ // FILTER but not the pattern (A2). In the scoped/default view they stay
91
+ // required: a block joins the list through its tier, so an untiered block is
92
+ // intentionally out of a tier-scoped view (and --all-tiers is the escape).
93
+ const nameTierPatterns = allTiers
94
+ ? [
95
+ " OPTIONAL { ?component ds:name ?name }",
96
+ " OPTIONAL { ?component ds:tier ?tier }",
97
+ ]
98
+ : [" ?component ds:name ?name ; ds:tier ?tier ."];
87
99
  return [
88
100
  "SELECT ?component ?type ?name ?tier",
89
101
  ' (GROUP_CONCAT(DISTINCT ?modName; separator=", ") AS ?modifiers)',
90
102
  "WHERE {",
91
103
  " VALUES ?type { ds:Component ds:Pattern ds:Layout ds:Subcomponent }",
92
- " ?component a ?type ;",
93
- " ds:name ?name ;",
94
- " ds:tier ?tier .",
104
+ " ?component a ?type .",
105
+ ...nameTierPatterns,
95
106
  tierFilter ? ` ${tierFilter}` : "",
96
107
  ` ${channelFilter}`,
97
108
  " OPTIONAL { ?component ds:hasModifierFamily ?mod . ?mod ds:name ?modName }",
@@ -118,7 +129,7 @@ const listVerb: VerbSpec<Record<string, unknown>, BlockRow[]> = {
118
129
  examples: [
119
130
  { cmd: "pragma block list" },
120
131
  { cmd: "pragma block list --all-tiers" },
121
- { cmd: "pragma block list --llm" },
132
+ { cmd: "pragma block list --format llm" },
122
133
  ],
123
134
  capability: {
124
135
  needsStore: true,
@@ -138,7 +149,9 @@ const listVerb: VerbSpec<Record<string, unknown>, BlockRow[]> = {
138
149
  const rows = await runSelect(rt, query, "block");
139
150
  return rows.map((row) => ({
140
151
  uri: row.component ?? "",
141
- name: row.name ?? "",
152
+ // An unnamed block still needs a token to show — fall back to its IRI's
153
+ // local name rather than rendering a blank row (A2).
154
+ name: row.name ?? localName(row.component ?? ""),
142
155
  type: normalizeType(row.type),
143
156
  tier: localName(row.tier ?? ""),
144
157
  modifiers: row.modifiers ?? "",
@@ -9,6 +9,14 @@
9
9
  * which rendered anatomy and modifiers without a flag. A derived name that maps
10
10
  * onto no schema field is omitted (OPTIONAL parity), so a graph lacking
11
11
  * whenToUse/whenNotToUse degrades gracefully.
12
+ *
13
+ * Disclosure declares the FULL canonical ladder `[summary, standard, detailed]`
14
+ * (B4), the same set `standard` declares, so a config `detail=standard` names a
15
+ * level `block` advertises rather than one it silently accepts-then-degrades.
16
+ * Block carries no `standard`-tier content of its own — gating is by canonical
17
+ * index, so `standard` resolves to the base view — but the declared set now
18
+ * matches its sibling reads. The per-noun DEFAULT stays domain-tuned (`block`
19
+ * rich-by-default, `standard` terse-by-default); only the level SET is aligned.
12
20
  */
13
21
 
14
22
  import type { PackDefinition } from "../../kernel/packs/types.js";
@@ -145,7 +153,10 @@ export const blockPack: PackDefinition = {
145
153
  ],
146
154
  },
147
155
  ],
148
- disclosure: { levels: ["summary", "detailed"], default: "detailed" },
156
+ disclosure: {
157
+ levels: ["summary", "standard", "detailed"],
158
+ default: "detailed",
159
+ },
149
160
  sample: {
150
161
  fixedCount: true,
151
162
  toolDescription:
@@ -173,8 +173,15 @@ describe("block list — hand-written tier/channel filtering", () => {
173
173
  ]);
174
174
  });
175
175
 
176
- it("--all-tiers returns the same global blocks (no tier configured)", async () => {
176
+ it("--all-tiers also reveals the untiered subcomponent (A2)", async () => {
177
+ // Button Icon is a ds:Subcomponent with NO ds:tier: the scoped/default view
178
+ // omits it (a block joins the list through its tier), but --all-tiers must
179
+ // surface every block, including untiered ones.
177
180
  const rows = (await listVerb.run({ allTiers: true }, rt)) as BlockRow[];
178
- expect(rows.map((r) => r.name).sort()).toEqual(["Button", "Modal"]);
181
+ expect(rows.map((r) => r.name).sort()).toEqual([
182
+ "Button",
183
+ "Button Icon",
184
+ "Modal",
185
+ ]);
179
186
  });
180
187
  });
@@ -99,10 +99,19 @@ describe("capabilities catalog — grammar-derived, drift-guarded (PROTECTED)",
99
99
  expect(data.limits.condensed_available).toBe(false);
100
100
  });
101
101
 
102
- it("carries the three orientation conventions", () => {
102
+ it("carries the four orientation conventions", () => {
103
103
  expect(data.conventions.system.length).toBeGreaterThan(0);
104
104
  expect(data.conventions.model).toContain("tier");
105
105
  expect(data.conventions.querying).toContain("RDF");
106
+ expect(data.conventions.mutations.length).toBeGreaterThan(0);
107
+ });
108
+
109
+ it("surfaces the plan-first/confirm convention so the confirm gate isn't trial-and-error (D2)", () => {
110
+ // A mutating tool without `confirm` returns a plan and writes nothing; agents
111
+ // must learn this from orientation, not by tripping the gate.
112
+ const { mutations } = data.conventions;
113
+ expect(mutations).toMatch(/plan/i);
114
+ expect(mutations).toContain("confirm");
106
115
  });
107
116
  });
108
117
 
@@ -22,7 +22,7 @@ const capabilitiesVerb: VerbSpec<Record<string, unknown>, CapabilitiesData> = {
22
22
  path: ["capabilities"],
23
23
  summary:
24
24
  "Discover pragma conventions, the annotated tool catalog, and the discovery sequence.",
25
- doc: "Storeless orientation for agents. Returns the conventions (KG / tier-channel / SPARQL model), a 3-stage discovery sequence, and every live tool with a behavioural use_when hint and category — all derived from the live grammar, so it never drifts. Call it first at session start.",
25
+ doc: "Storeless orientation for agents. Returns the conventions (KG / tier-channel / SPARQL model), a four-stage discovery sequence, and every live tool with a behavioural use_when hint and category — all derived from the live grammar, so it never drifts. Call it first at session start.",
26
26
  params: [],
27
27
  output: { formatters: capabilitiesFormatters },
28
28
  examples: [
@@ -26,16 +26,20 @@ import type {
26
26
  } from "./types.js";
27
27
 
28
28
  /**
29
- * The three orientation conventions, verbatim from the old shell. They describe
30
- * the KG / tier-channel / SPARQL model and are still accurate for v2.
29
+ * The four orientation conventions. The first three (system/model/querying) are
30
+ * verbatim from the old shell — they describe the KG / tier-channel / SPARQL
31
+ * model and are still accurate for v2; `mutations` is new in v2, surfacing the
32
+ * plan-first/confirm gate.
31
33
  */
32
34
  export const CONVENTIONS = {
33
35
  system:
34
36
  "Pragma is a CLI and MCP server for querying a design system knowledge graph — blocks, tokens, modifiers, standards, and ontologies.",
35
37
  model:
36
- "Data is scoped by tier (hierarchical, e.g. global > apps > apps/lxd) and channel (normal, experimental, prerelease). Set these via config_tier and config_channel.",
38
+ "Data is scoped by tier (hierarchical, e.g. global > apps > apps/lxd) and channel (normal, experimental, prerelease). Set these via config_set (e.g. config_set tier apps/lxd, config_set channel experimental).",
37
39
  querying:
38
40
  "All queries run against an RDF triple store. Prefixed IRIs (e.g. ds:global.component.button) identify entities. Use ontology_list to discover namespaces.",
41
+ mutations:
42
+ "Mutating tools are plan-first: call once WITHOUT confirm to get a plan (meta.planOnly, no writes), then repeat the call with confirm: true to execute.",
39
43
  } as const;
40
44
 
41
45
  /** The output modes v2 renders (dropped "text" → "plain"; condensed retired). */
@@ -7,7 +7,7 @@
7
7
  * exists, or a hint's category disagrees with the tool's real `mutates` flag.
8
8
  *
9
9
  * `use_when` strings for the surviving tools are ported verbatim from the old
10
- * shell's `domains/llm/data/toolCatalog.ts`; the net-new v2 tools (config_detail,
10
+ * shell's `domains/llm/data/toolCatalog.ts`; the net-new v2 tools (config_set,
11
11
  * graph_query, setup, upgrade, doctor, capabilities, prompt_*, token_add-config,
12
12
  * sources_*, tier_lookup, create_*) are authored here.
13
13
  */
@@ -93,10 +93,15 @@ export const TOOL_HINTS: Record<string, ToolHint> = {
93
93
  use_when:
94
94
  "Discovering loaded namespaces, prefixes, and class/property counts",
95
95
  },
96
+ ontology_lookup: {
97
+ category: "read",
98
+ use_when:
99
+ "Exploring the full schema of a namespace by name — classes, properties, and hierarchy",
100
+ },
96
101
  ontology_show: {
97
102
  category: "read",
98
103
  use_when:
99
- "Exploring the full schema of a namespace — classes, properties, and hierarchy",
104
+ "Deprecated alias of ontology_lookup — exploring a namespace's schema (prefer ontology_lookup)",
100
105
  },
101
106
  prompt_list: {
102
107
  category: "read",
@@ -166,24 +171,10 @@ export const TOOL_HINTS: Record<string, ToolHint> = {
166
171
  },
167
172
 
168
173
  // — Write ——————————————————————————————————————————————————————————————————
169
- config_tier: {
170
- category: "write",
171
- use_when: "Setting or resetting the active tier filter",
172
- },
173
- config_channel: {
174
- category: "write",
175
- use_when:
176
- "Setting the release channel (normal, experimental, or prerelease)",
177
- },
178
- config_detail: {
179
- category: "write",
180
- use_when:
181
- "Setting the default progressive-disclosure level (summary, standard, detailed)",
182
- },
183
174
  config_set: {
184
175
  category: "write",
185
176
  use_when:
186
- "Setting any config field by name in one call — tier, channel, or detail",
177
+ "Setting any config field by name — tier, channel, or detail (e.g. `config set tier apps/lxd`)",
187
178
  },
188
179
  create_component: {
189
180
  category: "write",
@@ -47,6 +47,8 @@ export interface CapabilitiesData {
47
47
  readonly system: string;
48
48
  readonly model: string;
49
49
  readonly querying: string;
50
+ /** The plan-first/confirm gate every mutating tool follows (D2). */
51
+ readonly mutations: string;
50
52
  };
51
53
  readonly discovery_sequence: readonly DiscoveryStage[];
52
54
  readonly tools: readonly CatalogTool[];
@@ -78,10 +78,37 @@ describe("colophon render golden — color-free (chalk.level = 0)", () => {
78
78
  });
79
79
  });
80
80
 
81
- describe("colophon render — color ON", () => {
82
- it("plain emits ANSI escapes when chalk color is enabled", () => {
81
+ /** Run `body` with stdout's `isTTY` forced to `value`, then restore it. */
82
+ function withStdoutTty(value: boolean | undefined, body: () => void): void {
83
+ const stream = process.stdout as { isTTY?: boolean };
84
+ const saved = stream.isTTY;
85
+ stream.isTTY = value;
86
+ try {
87
+ body();
88
+ } finally {
89
+ stream.isTTY = saved;
90
+ }
91
+ }
92
+
93
+ describe("colophon render — color ON (attended TTY)", () => {
94
+ it("plain emits ANSI escapes on a color-capable TTY", () => {
83
95
  chalk.level = 1;
84
- // biome-ignore lint/suspicious/noControlCharactersInRegex: asserting the literal ESC byte is the point
85
- expect(colophonFormatters.plain(FIXTURE)).toMatch(/\x1b\[/);
96
+ withStdoutTty(true, () => {
97
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: asserting the literal ESC byte is the point
98
+ expect(colophonFormatters.plain(FIXTURE)).toMatch(/\x1b\[/);
99
+ });
100
+ });
101
+ });
102
+
103
+ describe("colophon render — piped output is ANSI-free (F1)", () => {
104
+ it("plain emits ZERO ANSI off a TTY even when chalk reports color (CI/FORCE_COLOR)", () => {
105
+ // The CI leak: `supports-color` sets a non-zero level off a TTY under
106
+ // GITHUB_ACTIONS / FORCE_COLOR, so gating on `chalk.level` alone bled ANSI
107
+ // into `colophon --format plain | tee`. The isTTY gate closes it.
108
+ chalk.level = 3;
109
+ withStdoutTty(undefined, () => {
110
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: asserting NO ESC byte survives
111
+ expect(colophonFormatters.plain(FIXTURE)).not.toMatch(/\x1b\[/);
112
+ });
86
113
  });
87
114
  });
@@ -9,23 +9,32 @@
9
9
  * (which carries no leading H1) is never double-titled.
10
10
  */
11
11
 
12
+ import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
12
13
  import type { Formatters } from "../../kernel/spec/types.js";
13
14
  import { renderMarkdownToTerminal } from "./markdownTerminal.js";
14
15
  import type { ColophonData, ColophonSection } from "./types.js";
15
16
 
16
17
  /** Render one section (title as H1 + body) as styled terminal Markdown. */
17
- function plainSection(section: ColophonSection): string {
18
- return renderMarkdownToTerminal(`# ${section.title}\n\n${section.markdown}`);
18
+ function plainSection(section: ColophonSection, style: RenderStyle): string {
19
+ return renderMarkdownToTerminal(
20
+ `# ${section.title}\n\n${section.markdown}`,
21
+ style,
22
+ );
19
23
  }
20
24
 
21
- /** Render one section as condensed Markdown for `--llm` (summary preferred). */
25
+ /** Render one section as condensed Markdown for `--format llm` (summary preferred). */
22
26
  function llmSection(section: ColophonSection): string {
23
27
  return `## ${section.title}\n\n${section.summary ?? section.markdown}`;
24
28
  }
25
29
 
26
30
  export const colophonFormatters: Formatters<ColophonData> = {
27
31
  plain(data) {
28
- return data.sections.map(plainSection).join("\n\n");
32
+ // Resolve the TTY decision ONCE (impure: reads stdout.isTTY + chalk level),
33
+ // then render every section through the same inert-off-a-TTY styler.
34
+ const style = defaultStyle();
35
+ return data.sections
36
+ .map((section) => plainSection(section, style))
37
+ .join("\n\n");
29
38
  },
30
39
 
31
40
  llm(data) {
@@ -4,7 +4,7 @@
4
4
  * Pins the covenant-exact emitted slice (`{ v:"colophon", mcp:"colophon" }`),
5
5
  * proves the collector is storeless and combines pragma's built-in section with
6
6
  * the active pack's (the bundled `block` design-system colophon), exercises the
7
- * three formatter modes + `--format`/`--llm` precedence, holds CLI-json ≡ MCP
7
+ * three formatter modes + `--format` precedence, holds CLI-json ≡ MCP
8
8
  * parity, and checks the pack-grammar accepts a `colophon` field (rejecting a
9
9
  * non-string).
10
10
  */
@@ -47,7 +47,7 @@ const FLAGS: GlobalFlags = {
47
47
  verbose: false,
48
48
  };
49
49
  const FLAGS_JSON: GlobalFlags = { ...FLAGS, format: "json" };
50
- const FLAGS_LLM: GlobalFlags = { ...FLAGS, llm: true };
50
+ const FLAGS_LLM: GlobalFlags = { ...FLAGS, format: "llm", llm: true };
51
51
  const NO_MUT = { dryRun: false, undo: false, yes: false };
52
52
 
53
53
  /** A deterministic two-section fixture for pure formatter assertions. */
@@ -170,7 +170,7 @@ describe("colophon — formatter modes", () => {
170
170
  expect((envelope.data as ColophonData).sections[0]?.kind).toBe("pragma");
171
171
  });
172
172
 
173
- it("--llm selects the condensed Markdown form", async () => {
173
+ it("--format llm selects the condensed Markdown form", async () => {
174
174
  const outcome = await executeVerb(
175
175
  colophonVerb,
176
176
  {},
@@ -20,7 +20,10 @@ const colophonVerb: VerbSpec<Record<string, unknown>, ColophonData> = {
20
20
  output: { formatters: colophonFormatters },
21
21
  examples: [
22
22
  { cmd: "pragma colophon", note: "the toolchain + active domain story" },
23
- { cmd: "pragma colophon --llm", note: "condensed Markdown for agents" },
23
+ {
24
+ cmd: "pragma colophon --format llm",
25
+ note: "condensed Markdown for agents",
26
+ },
24
27
  ],
25
28
  capability: {
26
29
  needsStore: false,