@canonical/pragma-cli 0.35.0 → 0.37.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 (295) hide show
  1. package/README.md +2 -2
  2. package/bundled-skills/add-standard/SKILL.md +338 -0
  3. package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
  4. package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
  5. package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
  6. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
  7. package/bundled-skills/anatomy-author/SKILL.md +1226 -0
  8. package/bundled-skills/design-auditor/SKILL.md +455 -0
  9. package/bundled-skills/implement-component/SKILL.md +333 -0
  10. package/bundled-skills/specify-component/SKILL.md +331 -0
  11. package/bundled-skills/specify-pattern/SKILL.md +292 -0
  12. package/dist/package.json +24 -20
  13. package/dist/pragma.conf.js +266 -34
  14. package/dist/pragma.conf.js.map +1 -1
  15. package/dist/src/bin.js +1 -1
  16. package/dist/src/bin.js.map +1 -1
  17. package/dist/src/capabilities/capabilities/hints.js +1 -1
  18. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  19. package/dist/src/capabilities/colophon/colophon.render.js +1 -1
  20. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  21. package/dist/src/capabilities/create/create.verb.js +2 -1
  22. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  23. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  24. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  25. package/dist/src/capabilities/create/mount.js +4 -3
  26. package/dist/src/capabilities/create/mount.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  28. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  29. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  30. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  32. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  33. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  34. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  35. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  36. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  37. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  38. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  39. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  40. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  41. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  42. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  43. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  44. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  45. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  46. package/dist/src/capabilities/graph/query.render.js +2 -2
  47. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  48. package/dist/src/capabilities/info/collectInfo.js +8 -2
  49. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  50. package/dist/src/capabilities/info/info.render.js +6 -2
  51. package/dist/src/capabilities/info/info.render.js.map +1 -1
  52. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  53. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  54. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  55. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  56. package/dist/src/capabilities/setup/mode.js +2 -2
  57. package/dist/src/capabilities/setup/operations/index.js +4 -4
  58. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  60. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
  64. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  65. package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
  66. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  67. package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
  68. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  69. package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
  70. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  71. package/dist/src/capabilities/setup/plan.js.map +1 -1
  72. package/dist/src/capabilities/setup/plan.render.js +144 -66
  73. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  74. package/dist/src/capabilities/setup/setup.render.js +1 -1
  75. package/dist/src/capabilities/setup/setup.verb.js +165 -63
  76. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  77. package/dist/src/capabilities/setup/targets.js +122 -51
  78. package/dist/src/capabilities/setup/targets.js.map +1 -1
  79. package/dist/src/capabilities/shared/index.js +7 -6
  80. package/dist/src/capabilities/shared/index.js.map +1 -1
  81. package/dist/src/capabilities/shared/packageManager.js +300 -54
  82. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  83. package/dist/src/capabilities/skill/discover.js +97 -11
  84. package/dist/src/capabilities/skill/discover.js.map +1 -1
  85. package/dist/src/capabilities/skill/render.js +2 -2
  86. package/dist/src/capabilities/skill/render.js.map +1 -1
  87. package/dist/src/capabilities/sources/installSkills.js +145 -3
  88. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  89. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  90. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  91. package/dist/src/capabilities/sources/update.verb.js +3 -3
  92. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  93. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  94. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  95. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  96. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  97. package/dist/src/constants.js +13 -2
  98. package/dist/src/constants.js.map +1 -1
  99. package/dist/src/kernel/index.js +11 -2
  100. package/dist/src/kernel/index.js.map +1 -1
  101. package/dist/src/kernel/interactivity.js +68 -0
  102. package/dist/src/kernel/interactivity.js.map +1 -0
  103. package/dist/src/kernel/packs/renderPack.js +3 -3
  104. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  105. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  106. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  107. package/dist/src/kernel/packs/runBodies.js +57 -4
  108. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  109. package/dist/src/kernel/packs/schema.js +54 -2
  110. package/dist/src/kernel/packs/schema.js.map +1 -1
  111. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  112. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
  114. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/types.js.map +1 -1
  116. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  117. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  118. package/dist/src/kernel/project/cli/dispatch.js +46 -24
  119. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  120. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  121. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  122. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  123. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  124. package/dist/src/kernel/project/cli/rootHelp.js +42 -9
  125. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  126. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  127. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  128. package/dist/src/kernel/render/renderers.js +1 -1
  129. package/dist/src/kernel/render/style.js +29 -9
  130. package/dist/src/kernel/render/style.js.map +1 -1
  131. package/dist/src/kernel/render/vocabulary.js +174 -0
  132. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  133. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  134. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  135. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  136. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  137. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  138. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  139. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  140. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  141. package/dist/src/kernel/runtime/graphpack/read.js +14 -9
  142. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  143. package/dist/src/kernel/runtime/loadSession.js +3 -1
  144. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  145. package/dist/src/kernel/runtime/probe.js +1 -1
  146. package/dist/src/kernel/runtime/probe.js.map +1 -1
  147. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  148. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  149. package/dist/src/kernel/runtime/refs/resolve.js +9 -3
  150. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  151. package/dist/src/kernel/spec/emitReference.js +1 -1
  152. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  153. package/package.json +24 -20
  154. package/pragma.conf.ts +272 -34
  155. package/src/bin.ts +8 -1
  156. package/src/capabilities/block.parity.test.ts +25 -11
  157. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  158. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  159. package/src/capabilities/capabilities/hints.ts +1 -1
  160. package/src/capabilities/colophon/colophon.render.ts +1 -2
  161. package/src/capabilities/colophon/colophon.test.ts +2 -2
  162. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  163. package/src/capabilities/create/byteEquality.test.ts +28 -1
  164. package/src/capabilities/create/create.test.ts +1 -0
  165. package/src/capabilities/create/create.verb.ts +2 -1
  166. package/src/capabilities/create/createGrammar.test.ts +5 -2
  167. package/src/capabilities/create/createSurface.generated.ts +23 -1
  168. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  169. package/src/capabilities/create/mount.ts +3 -3
  170. package/src/capabilities/distribution.parity.test.ts +4 -0
  171. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  172. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  173. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  174. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  175. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  176. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  177. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  178. package/src/capabilities/doctor/checks/index.ts +6 -6
  179. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  180. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  181. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  182. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  183. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  184. package/src/capabilities/doctor/doctor.render.ts +56 -80
  185. package/src/capabilities/doctor/doctor.test.ts +247 -35
  186. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  187. package/src/capabilities/doctor/runChecks.ts +14 -11
  188. package/src/capabilities/doctor/types.ts +15 -24
  189. package/src/capabilities/emptyRecovery.test.ts +10 -2
  190. package/src/capabilities/graph/query.render.ts +2 -2
  191. package/src/capabilities/info/collectInfo.ts +8 -1
  192. package/src/capabilities/info/info.render.ts +6 -2
  193. package/src/capabilities/info/info.test.ts +37 -1
  194. package/src/capabilities/info/types.ts +10 -2
  195. package/src/capabilities/lazy.test.ts +26 -2
  196. package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
  197. package/src/capabilities/prompt/prompt.render.ts +2 -2
  198. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  199. package/src/capabilities/resources/resources.test.ts +38 -2
  200. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  201. package/src/capabilities/setup/buildPlan.ts +45 -41
  202. package/src/capabilities/setup/index.ts +1 -1
  203. package/src/capabilities/setup/mode.ts +2 -2
  204. package/src/capabilities/setup/operations/index.ts +6 -1
  205. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  206. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  207. package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
  208. package/src/capabilities/setup/operations/setupLsp.ts +203 -38
  209. package/src/capabilities/setup/operations/setupMcp.ts +139 -62
  210. package/src/capabilities/setup/operations/setupSkills.ts +399 -60
  211. package/src/capabilities/setup/plan.render.ts +171 -76
  212. package/src/capabilities/setup/plan.ts +17 -12
  213. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  214. package/src/capabilities/setup/setup.render.test.ts +50 -36
  215. package/src/capabilities/setup/setup.render.ts +1 -1
  216. package/src/capabilities/setup/setup.test.ts +1126 -50
  217. package/src/capabilities/setup/setup.verb.ts +174 -68
  218. package/src/capabilities/setup/targets.ts +138 -60
  219. package/src/capabilities/setup/types.ts +10 -18
  220. package/src/capabilities/shared/index.ts +18 -6
  221. package/src/capabilities/shared/packageManager.test.ts +421 -6
  222. package/src/capabilities/shared/packageManager.ts +394 -59
  223. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  224. package/src/capabilities/skill/discover.ts +100 -11
  225. package/src/capabilities/skill/render.ts +2 -2
  226. package/src/capabilities/skill/skill.test.ts +17 -4
  227. package/src/capabilities/sources/installSkills.ts +181 -3
  228. package/src/capabilities/sources/runUpdate.ts +78 -2
  229. package/src/capabilities/sources/sources.test.ts +228 -0
  230. package/src/capabilities/sources/update.verb.ts +3 -3
  231. package/src/capabilities/surface.test.ts +14 -6
  232. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  233. package/src/capabilities/upgrade/types.ts +10 -2
  234. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  235. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  236. package/src/constants.ts +14 -1
  237. package/src/identity.test.ts +18 -5
  238. package/src/kernel/completion/entitySource.test.ts +15 -2
  239. package/src/kernel/completion/safety.test.ts +16 -9
  240. package/src/kernel/config/defaults.test.ts +19 -3
  241. package/src/kernel/config/readConfig.test.ts +1 -1
  242. package/src/kernel/index.ts +11 -2
  243. package/src/kernel/interactivity.test.ts +76 -0
  244. package/src/kernel/interactivity.ts +69 -0
  245. package/src/kernel/packs/compile.test.ts +92 -0
  246. package/src/kernel/packs/renderPack.ts +3 -3
  247. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  248. package/src/kernel/packs/resolveEntity.ts +88 -24
  249. package/src/kernel/packs/runBodies.ts +84 -19
  250. package/src/kernel/packs/schema.ts +60 -4
  251. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  252. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  253. package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
  254. package/src/kernel/packs/toolDescription.test.ts +35 -1
  255. package/src/kernel/packs/types.ts +125 -2
  256. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  257. package/src/kernel/project/cli/buildProgram.ts +8 -1
  258. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  259. package/src/kernel/project/cli/dispatch.ts +48 -25
  260. package/src/kernel/project/cli/globalFlags.ts +4 -1
  261. package/src/kernel/project/cli/helpFormat.ts +1 -2
  262. package/src/kernel/project/cli/rootHelp.ts +44 -8
  263. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.ts +1 -1
  266. package/src/kernel/render/style.ts +32 -9
  267. package/src/kernel/render/vocabulary.ts +276 -0
  268. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  269. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  270. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  271. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  272. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  273. package/src/kernel/runtime/graphpack/read.ts +14 -12
  274. package/src/kernel/runtime/loadSession.ts +3 -1
  275. package/src/kernel/runtime/probe.ts +1 -1
  276. package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
  277. package/src/kernel/runtime/refs/parseRef.ts +25 -1
  278. package/src/kernel/runtime/refs/resolve.ts +9 -3
  279. package/src/kernel/runtime/types.ts +17 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/types.ts +35 -6
  282. package/src/testing/PARITY_GAPS.ts +9 -3
  283. package/src/testing/behavioral/docExamples.test.ts +4 -2
  284. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  285. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  286. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  287. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  288. package/src/testing/eval/cases/readNouns.ts +245 -9
  289. package/src/testing/fixtures/blockGraph.ts +28 -13
  290. package/src/testing/fixtures/graph/canonical.ts +95 -12
  291. package/src/testing/helpers/runCli.test.ts +44 -0
  292. package/src/testing/helpers/runCli.ts +27 -3
  293. package/dist/src/capabilities/shared/bands.js +0 -16
  294. package/dist/src/capabilities/shared/bands.js.map +0 -1
  295. package/src/capabilities/shared/bands.ts +0 -16
@@ -3,6 +3,7 @@ import { afterAll, describe, expect, it, vi } from "vitest";
3
3
  import { VERSION } from "../../../constants.js";
4
4
  import { fixtureModule } from "../../../testing/fixtures/fixtureCapability.js";
5
5
  import { projectCli } from "../../../testing/helpers/projectCli.js";
6
+ import type { GlobalFlags } from "../../runtime/index.js";
6
7
  import type { CapabilityModule, VerbSpec } from "../../spec/types.js";
7
8
  import { formatRootHelp } from "./rootHelp.js";
8
9
 
@@ -486,8 +487,26 @@ describe("buildProgram — sub-verb-only noun invoked bare (U2)", () => {
486
487
  });
487
488
  });
488
489
 
489
- describe("formatRootHelp — grouping", () => {
490
- const help = formatRootHelp(
490
+ /** The global flags of a human at a terminal: plain output, nothing inferred. */
491
+ const HUMAN_FLAGS: GlobalFlags = {
492
+ llm: false,
493
+ format: "plain",
494
+ verbose: false,
495
+ };
496
+
497
+ /** A run whose stdout is captured: `llm`, inferred or asked for. */
498
+ const CAPTURED_FLAGS: GlobalFlags = {
499
+ llm: true,
500
+ autoLlm: true,
501
+ format: "plain",
502
+ verbose: false,
503
+ };
504
+
505
+ /** The first line of the wordmark art — present iff the wordmark printed. */
506
+ const WORDMARK_LINE = "(___,";
507
+
508
+ const rootHelpFor = (flags: GlobalFlags): string =>
509
+ formatRootHelp(
491
510
  "pragma",
492
511
  "pragma test",
493
512
  [
@@ -497,8 +516,60 @@ describe("formatRootHelp — grouping", () => {
497
516
  ],
498
517
  "1.2.3",
499
518
  "https://example.test/issues",
519
+ flags,
500
520
  );
501
521
 
522
+ describe("formatRootHelp — the wordmark is gated on the AUDIENCE", () => {
523
+ // Asserted on the ART, never on styling: `golden.ts` strips ANSI and this
524
+ // suite pins `chalk.level = 0`, so a colour-only change would be invisible
525
+ // here — and chalk emits no escapes off a TTY anyway, so it would save an
526
+ // agent nothing. The lines have to go.
527
+ it("prints the art for a human reading plain output", () => {
528
+ expect(rootHelpFor(HUMAN_FLAGS)).toContain(WORDMARK_LINE);
529
+ });
530
+
531
+ it("drops the art — every line of it — when stdout is captured", () => {
532
+ const help = rootHelpFor(CAPTURED_FLAGS);
533
+ expect(help).not.toContain(WORDMARK_LINE);
534
+ // No leading blank line either: the header IS the first line.
535
+ expect(help.startsWith("pragma v1.2.3 — pragma test")).toBe(true);
536
+ // Only the art goes; the page itself is untouched.
537
+ expect(help).toContain("Usage: pragma <command> [subcommand] [flags]");
538
+ expect(help).toContain("Global flags");
539
+ });
540
+
541
+ it("drops the art for `--format json` too", () => {
542
+ expect(
543
+ rootHelpFor({ llm: false, format: "json", verbose: false }),
544
+ ).not.toContain(WORDMARK_LINE);
545
+ });
546
+
547
+ it("keeps the art for an EXPLICIT `--format plain` down a pipe", () => {
548
+ // Explicit beats inference: asking for the human shape gets the human page,
549
+ // captured or not. (`parseGlobalFlags` leaves `llm` false whenever
550
+ // `--format` was requested.)
551
+ expect(
552
+ rootHelpFor({
553
+ llm: false,
554
+ autoLlm: false,
555
+ format: "plain",
556
+ verbose: false,
557
+ }),
558
+ ).toContain(WORDMARK_LINE);
559
+ });
560
+
561
+ it("differs ONLY by the wordmark block", () => {
562
+ const human = rootHelpFor(HUMAN_FLAGS).split("\n");
563
+ const captured = rootHelpFor(CAPTURED_FLAGS).split("\n");
564
+ // 10 art lines + the blank that separates them from the header.
565
+ expect(human.length - captured.length).toBe(11);
566
+ expect(human.slice(11)).toEqual(captured);
567
+ });
568
+ });
569
+
570
+ describe("formatRootHelp — grouping", () => {
571
+ const help = rootHelpFor(HUMAN_FLAGS);
572
+
502
573
  it("leads with the uncurated nouns, untitled, under the usage line", () => {
503
574
  // `block` is not in the kernel's curated table, so it leads the page with
504
575
  // no heading of its own — the header already carries the blurb, and the
@@ -298,7 +298,14 @@ export function buildProgram(
298
298
  program.enablePositionalOptions();
299
299
  program.exitOverride();
300
300
  useDesignedHelp(program, () =>
301
- formatRootHelp(programName, description, live, version, issuesUrl),
301
+ formatRootHelp(
302
+ programName,
303
+ description,
304
+ live,
305
+ version,
306
+ issuesUrl,
307
+ options.globalFlags,
308
+ ),
302
309
  );
303
310
 
304
311
  const groups = new Map<string, VerbSpec[]>();
@@ -5,10 +5,11 @@ import { $, fail, gen, log, mkdir, succeed, writeFile } from "@canonical/task";
5
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
6
  import { fixturePreviewModule } from "../../../testing/fixtures/fixtureCapability.js";
7
7
  import { PragmaError } from "../../error/PragmaError.js";
8
+ import { canPrompt } from "../../interactivity.js";
8
9
  import { bootRuntime } from "../../runtime/boot.js";
9
10
  import type { GlobalFlags } from "../../runtime/types.js";
10
11
  import type { ParamSpec, VerbSpec } from "../../spec/types.js";
11
- import { cliIsTTY, dispatch, executeVerb, extractParams } from "./dispatch.js";
12
+ import { dispatch, executeVerb, extractParams } from "./dispatch.js";
12
13
 
13
14
  /** The read-then-write mutation the honest-preview guards drive. */
14
15
  const graft = fixturePreviewModule.verbs[0] as VerbSpec;
@@ -218,7 +219,7 @@ describe("--quiet — success is silent, failure is not", () => {
218
219
  formatters: {
219
220
  ...passthroughFormatters,
220
221
  plain: () => "",
221
- emptyNotice: () => "No results.",
222
+ notice: () => "No results.",
222
223
  },
223
224
  },
224
225
  run: async () => [],
@@ -491,7 +492,7 @@ describe("executeVerb — interactivity gate (H3)", () => {
491
492
  expect(sink.isTTY).toBe(true);
492
493
  });
493
494
 
494
- it("cliIsTTY IS the stdin+stderr conjunction — the one gate both callers read", () => {
495
+ it("canPrompt IS the stdin+stderr conjunction — the one gate both callers read", () => {
495
496
  // The mount's create decision reads the same exported function this
496
497
  // describe drives through executeVerb, so pinning the truth table here
497
498
  // pins BOTH callers (nothing is left to keep two copies in step).
@@ -506,7 +507,7 @@ describe("executeVerb — interactivity gate (H3)", () => {
506
507
  setTTY(process.stdin, stdin);
507
508
  setTTY(process.stderr, stderr);
508
509
  setTTY(process.stdout, stdout);
509
- expect(cliIsTTY(), `stdin=${stdin} stderr=${stderr}`).toBe(expected);
510
+ expect(canPrompt(), `stdin=${stdin} stderr=${stderr}`).toBe(expected);
510
511
  }
511
512
  });
512
513
  });
@@ -25,6 +25,7 @@ import {
25
25
  renderErrorLlm,
26
26
  renderErrorPlain,
27
27
  } from "../../error/index.js";
28
+ import { canPrompt, stdoutIsCaptured } from "../../interactivity.js";
28
29
  import type { RenderContext } from "../../render/contracts.js";
29
30
  import { successEnvelope, writeStdout } from "../../render/index.js";
30
31
  import { bootRuntime } from "../../runtime/index.js";
@@ -43,18 +44,6 @@ export interface MutationFlags {
43
44
  readonly yes: boolean;
44
45
  }
45
46
 
46
- /**
47
- * THE CLI interactivity gate (H3): stdin AND stderr are TTYs. The Ink wizard
48
- * renders to stderr and reads stdin, so `<verb> 2>/dev/null` must be
49
- * non-interactive — gating on stdout would mount an invisible render that
50
- * blocks on stdin. One exported fact: the kernel's interaction context and a
51
- * mounted subtree's own decision both read it here, so the two callers can
52
- * never disagree about what "a TTY" means.
53
- */
54
- export function cliIsTTY(): boolean {
55
- return process.stdin.isTTY === true && process.stderr.isTTY === true;
56
- }
57
-
58
47
  /**
59
48
  * Route a Task's log effects to stderr. The interpreter otherwise falls back to
60
49
  * `console.log` (stdout), which would corrupt the `--format json` / MCP stdio
@@ -171,7 +160,7 @@ export function extractParams(
171
160
  * pipe would read as a record. `llm` and `json` keep their own empty shapes
172
161
  * on stdout: both are machine contracts whose consumers read one stream.
173
162
  *
174
- * @note Impure — reads `process.stdout.isTTY` for the render context.
163
+ * @note Impure — reads stdout's capture state for the render context.
175
164
  */
176
165
  function renderData(
177
166
  verb: VerbSpec,
@@ -181,8 +170,17 @@ function renderData(
181
170
  ): DispatchOutcome {
182
171
  if (flags.format === "json") {
183
172
  const projection = JSON.parse(verb.output.formatters.json(data));
173
+ // The calm notice rides `meta` on the machine surfaces, so an empty result
174
+ // is distinguishable from an unbuilt store or a mistyped filter, and an
175
+ // ambiguous lookup hit from an unambiguous one. `data` keeps its uniform
176
+ // shape, and MCP builds the same key from the same seam
177
+ // (`mcp/registerVerb.ts#noticeMeta`) — the two machine surfaces stay
178
+ // byte-equal.
179
+ const notice = verb.output.formatters.notice?.(data);
184
180
  return {
185
- stdout: `${JSON.stringify(successEnvelope(projection, meta))}\n`,
181
+ stdout: `${JSON.stringify(
182
+ successEnvelope(projection, notice ? { ...meta, notice } : meta),
183
+ )}\n`,
186
184
  exitCode: 0,
187
185
  };
188
186
  }
@@ -192,14 +190,12 @@ function renderData(
192
190
  }
193
191
  const context: RenderContext = {
194
192
  headers: flags.noHeaders !== true,
195
- stdoutIsTty: process.stdout.isTTY === true,
193
+ stdoutIsTty: !stdoutIsCaptured(),
196
194
  };
197
195
  const text = verb.output.formatters.plain(data, context);
198
- // The calm zero-record notice is success-path guidance — `--quiet` mutes it.
196
+ // The calm notice is success-path guidance — `--quiet` mutes it.
199
197
  const notice =
200
- flags.quiet === true
201
- ? undefined
202
- : verb.output.formatters.emptyNotice?.(data);
198
+ flags.quiet === true ? undefined : verb.output.formatters.notice?.(data);
203
199
  return {
204
200
  stdout: text ? `${text}\n` : "",
205
201
  ...(notice ? { stderr: `${notice}\n` } : {}),
@@ -332,8 +328,8 @@ export async function executeVerb(
332
328
  // block on input. `--undo` stays handler-free and untouched by this seam.
333
329
  const controller = new AbortController();
334
330
  const interaction: InteractionRuntime = {
335
- // The shared H3 gate (see cliIsTTY): stderr, never stdout.
336
- isTTY: cliIsTTY(),
331
+ // The shared H3 gate (see `canPrompt`): stdin and stderr, never stdout.
332
+ isTTY: canPrompt(),
337
333
  transport: "cli",
338
334
  yes: mutation.yes,
339
335
  signal: controller.signal,
@@ -415,22 +411,49 @@ export async function executeVerb(
415
411
  }
416
412
  }
417
413
  if (mutation.undo) {
418
- const { undoCount } = await runUndo(task, { onLog: logSink(flags) });
414
+ // The truth seam. `runUndo`'s collection phase drove the verb's task
415
+ // with every effect mocked, so anything the task body reported was a
416
+ // narration of the mocked walk, not of the reversals — which have only
417
+ // JUST run, each isolated, each with its own outcome. Those outcomes
418
+ // are the one honest record of what the undo did: a verb that reports
419
+ // per-target results sets `undoReport` (like `exec`/`planData`) and
420
+ // projects them onto its own rows; a throw from it renders as the
421
+ // run's error, exactly like a failed real run. Verbs without a
422
+ // projector of their own still may not exit 0 over a failed reversal,
423
+ // so the outcomes are re-checked here as the backstop.
424
+ const { undoCount, outcomes } = await runUndo(task, {
425
+ onLog: logSink(flags),
426
+ });
427
+ await mutationRuntime.undoReport?.(outcomes);
428
+ const failed = outcomes.filter((o) => o.status === "failed");
429
+ if (failed.length > 0) {
430
+ throw new PragmaError({
431
+ code: "UNSUPPORTED",
432
+ message: `${failed.length} of ${outcomes.length} undo step(s) did not complete: ${failed
433
+ .map((o) => o.error.message)
434
+ .join("; ")}`,
435
+ });
436
+ }
419
437
  return renderUndo(flags, undoCount);
420
438
  }
421
439
  // Real execution: spread the verb's runner options into the node
422
440
  // interpreter (prompt handler, stamping/progress callbacks, log routing,
423
- // signal). Teardown (e.g. unmount an Ink render) runs in `finally`.
441
+ // signal). Teardown (e.g. unmount an Ink render) runs in `finally` —
442
+ // BEFORE the outcome is rendered: an interactive session's closing frame
443
+ // flushes on its dispose, so printing the result first put the verb's
444
+ // stdout recap ABOVE a frame that then repainted stale beneath it. Tear
445
+ // the UI down, then report; the error path gets the same order for free.
424
446
  const exec = mutationRuntime.exec ?? {};
425
447
  const onSigint = (): void => controller.abort();
426
448
  process.once("SIGINT", onSigint);
449
+ let value: unknown;
427
450
  try {
428
- const value = await runTask(task, { onLog: logSink(flags), ...exec });
429
- return renderData(verb, flags, value, {});
451
+ value = await runTask(task, { onLog: logSink(flags), ...exec });
430
452
  } finally {
431
453
  process.removeListener("SIGINT", onSigint);
432
454
  await exec.dispose?.();
433
455
  }
456
+ return renderData(verb, flags, value, {});
434
457
  }
435
458
 
436
459
  const data = await Promise.resolve(
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import { DETAIL_LEVELS, type DetailLevel } from "../../../constants.js";
28
+ import { stdoutIsCaptured } from "../../interactivity.js";
28
29
  import type { GlobalFlags } from "../../runtime/index.js";
29
30
 
30
31
  /** The end-of-options separator: nothing after it is a flag of this program. */
@@ -153,7 +154,9 @@ export interface OutputEnvironment {
153
154
  /** Read the real stdout/env output environment. @note Impure — reads process. */
154
155
  function readOutputEnvironment(): OutputEnvironment {
155
156
  return {
156
- isTty: process.stdout.isTTY === true,
157
+ // The OUTPUT-SHAPE question, on stdout — not the prompting one. A run whose
158
+ // stdout is captured may still have a terminal to prompt on (`canPrompt`).
159
+ isTty: !stdoutIsCaptured(),
157
160
  noAutoLlm: Boolean(process.env.PRAGMA_NO_AUTO_LLM),
158
161
  };
159
162
  }
@@ -11,9 +11,8 @@
11
11
  */
12
12
 
13
13
  import chalk from "chalk";
14
+ import { INDENT } from "../../render/vocabulary.js";
14
15
 
15
- /** Two-space indent for every list body row. */
16
- const INDENT = " ";
17
16
  /** Two-space gap between a term column and its description. */
18
17
  const GAP = " ";
19
18
 
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { PROGRAM_LOGO } from "../../../constants.js";
14
+ import type { GlobalFlags } from "../../runtime/index.js";
14
15
  import type { VerbSpec } from "../../spec/index.js";
15
16
  import {
16
17
  helpColumns,
@@ -82,6 +83,14 @@ function buildKernelGroups(programName: string): readonly HelpGroup[] {
82
83
  },
83
84
  { noun: "info", summary: "Show version, config, and update status" },
84
85
  { noun: "version", summary: "Print the CLI version" },
86
+ // A colophon is the note at the back of a book: the typeface, the
87
+ // press, the paper. Nothing depends on it, which is the point — so
88
+ // this line does not argue for it. Every other summary in this column
89
+ // promises work done; this one just invites you to look. It sits with
90
+ // the other commands that describe the distribution rather than act on
91
+ // it; "For AI agents" is for surfaces an agent drives, and credits are
92
+ // for a person reading.
93
+ { noun: "colophon", summary: "Read the credits" },
85
94
  {
86
95
  noun: "upgrade",
87
96
  summary: `Upgrade the ${programName} CLI to the latest version`,
@@ -95,11 +104,6 @@ function buildKernelGroups(programName: string): readonly HelpGroup[] {
95
104
  noun: "capabilities",
96
105
  summary: "Discover conventions, tools, and the discovery sequence",
97
106
  },
98
- // A colophon is the note at the back of a book: the typeface, the
99
- // press, the paper. Nothing depends on it, which is the point — so
100
- // this line does not argue for it. Every other summary in this column
101
- // promises work done; this one just invites you to look.
102
- { noun: "colophon", summary: "Read the credits" },
103
107
  { noun: "skill", summary: "Browse agent skills from the active packs" },
104
108
  { noun: "prompt", summary: "Browse reusable prompt templates" },
105
109
  // Curated for PLACEMENT, not for existence: `mcp serve` is an
@@ -132,6 +136,27 @@ function summarizeNoun(noun: string, verbs: readonly VerbSpec[]): string {
132
136
  return (first?.summary ?? `${noun} commands`).replace(/\.$/, "");
133
137
  }
134
138
 
139
+ /**
140
+ * Whether this reader is worth spending the wordmark on.
141
+ *
142
+ * An AUDIENCE question, not a data question: the art is 11 lines and ~200
143
+ * tokens — roughly a third of what `--help` costs an agent — and it says
144
+ * nothing an agent can act on. Both captured shapes are excluded:
145
+ * `llm` (requested, or inferred from a non-interactive stdout — the shape an
146
+ * agent captures) and `json`. `--format plain` therefore keeps the art even
147
+ * down a pipe, and `--format llm` drops it even on a terminal: explicit beats
148
+ * inference, the same inferred-vs-explicit split `renderErrorForFormat` uses.
149
+ *
150
+ * The DECISION is passed in as data — both call sites already hold the parsed
151
+ * flags — so this module still reads no `process` state and stays testable.
152
+ *
153
+ * @param flags - The invocation's global flags.
154
+ * @returns `true` when a human is reading and the wordmark should print.
155
+ */
156
+ function wordmarkSuitsReader(flags: GlobalFlags): boolean {
157
+ return flags.llm !== true && flags.format !== "json";
158
+ }
159
+
135
160
  /**
136
161
  * Build the curated root help string.
137
162
  *
@@ -144,6 +169,9 @@ function summarizeNoun(noun: string, verbs: readonly VerbSpec[]): string {
144
169
  * read here so it is the SAME string `--version` prints: `buildProgram`
145
170
  * resolves `options.version ?? VERSION`, and a host that overrides one must
146
171
  * not be able to leave the other saying something else.
172
+ * @param globalFlags - The invocation's global flags, read only to decide who
173
+ * is reading (see {@link wordmarkSuitsReader}). Passed rather than probed: the
174
+ * help layer must not learn to read `process`.
147
175
  * @returns The formatted, colorized help text.
148
176
  */
149
177
  export function formatRootHelp(
@@ -152,6 +180,7 @@ export function formatRootHelp(
152
180
  verbs: readonly VerbSpec[],
153
181
  version: string,
154
182
  issuesUrl: string,
183
+ globalFlags: GlobalFlags,
155
184
  ): string {
156
185
  const present = nounsFrom(verbs);
157
186
  const kernel = buildKernelGroups(programName);
@@ -181,9 +210,16 @@ export function formatRootHelp(
181
210
  );
182
211
 
183
212
  const lines: string[] = [
184
- // The wordmark leads when the distribution declares one. A fork ships its
185
- // own art or none, so its front door is never branded with someone else's.
186
- ...(PROGRAM_LOGO.length > 0 ? [...PROGRAM_LOGO.map(helpLogo), ""] : []),
213
+ // The wordmark leads when the distribution declares one AND a human is
214
+ // reading it. A fork ships its own art or none, so its front door is never
215
+ // branded with someone else's; and a captured run drops the art entirely
216
+ // rather than charging an agent ~200 tokens for a picture of the name it
217
+ // just typed (see `wordmarkSuitsReader`). The LINES go, not their colour:
218
+ // chalk already emits no escapes off a TTY, so dimming would save nobody
219
+ // anything.
220
+ ...(PROGRAM_LOGO.length > 0 && wordmarkSuitsReader(globalFlags)
221
+ ? [...PROGRAM_LOGO.map(helpLogo), ""]
222
+ : []),
187
223
  // `<name> v<version> — <blurb>`, the spelling `info` and `capabilities`
188
224
  // already use, so the front door names the build the same way every other
189
225
  // surface does. Dimmed: it answers "which build am I on" for someone who
@@ -143,6 +143,22 @@ function withDetail(
143
143
  };
144
144
  }
145
145
 
146
+ /**
147
+ * The notice seam, projected into the envelope's `meta`.
148
+ *
149
+ * It existed with exactly ONE consumer: the CLI dispatcher, which writes it to
150
+ * stderr. An agent therefore could not tell an unbuilt store, a mistyped filter
151
+ * and a genuinely empty result apart — all three were
152
+ * `{"ok":true,"data":[],"meta":{}}` — and, later, could not tell an unambiguous
153
+ * lookup hit from one of three blocks sharing a name. Riding in `meta` keeps
154
+ * `data` its uniform shape while making both of those ≠ silence, and the CLI's
155
+ * `--format json` carries the same key so the two surfaces stay byte-equal.
156
+ */
157
+ function noticeMeta(verb: VerbSpec, data: unknown): Record<string, unknown> {
158
+ const notice = verb.output.formatters.notice?.(data as never);
159
+ return notice ? { notice } : {};
160
+ }
161
+
146
162
  /** The tool handler for a read verb: run, project, envelope. */
147
163
  function readHandler(verb: VerbSpec, runtime: PragmaRuntime) {
148
164
  return async (args: Record<string, unknown>): Promise<CallToolResult> => {
@@ -152,7 +168,10 @@ function readHandler(verb: VerbSpec, runtime: PragmaRuntime) {
152
168
  const result = await Promise.resolve(
153
169
  verb.run(params, withDetail(verb, runtime, args)) as Promise<unknown>,
154
170
  );
155
- return toolSuccess(JSON.parse(verb.output.formatters.json(result)));
171
+ return toolSuccess(
172
+ JSON.parse(verb.output.formatters.json(result)),
173
+ noticeMeta(verb, result),
174
+ );
156
175
  } catch (error) {
157
176
  return toolError(asPragmaError(error));
158
177
  }
@@ -13,6 +13,8 @@ describe("DEFAULT_PREFIX_MAP", () => {
13
13
  skos: "http://www.w3.org/2004/02/skos/core#",
14
14
  ds: "https://ds.canonical.com/",
15
15
  cs: "http://pragma.canonical.com/codestandards#",
16
+ dt: "https://dt.canonical.com/",
17
+ "w3c-tokens": "https://dt.canonical.com/w3c-tokens/",
16
18
  });
17
19
  });
18
20
 
@@ -85,7 +85,7 @@ function emptyBody<T>(options: RenderListOptions<T>): string {
85
85
  * The empty-list notice a dispatcher routes to STDERR (exit 0): zero rows is
86
86
  * a calm success, and stdout — the data stream — must not carry a human
87
87
  * sentence a pipe would read as a record. Exported for the formatter seam's
88
- * `emptyNotice`; the plain renderer itself no longer prints it.
88
+ * `notice`; the plain renderer itself no longer prints it.
89
89
  */
90
90
  export function renderListEmptyNotice<T>(
91
91
  options: RenderListOptions<T>,
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import chalk from "chalk";
15
+ import { stdoutIsCaptured } from "../interactivity.js";
15
16
 
16
17
  /** The palette + gate a renderer consults to beautify the human path. */
17
18
  export interface RenderStyle {
@@ -27,6 +28,8 @@ export interface RenderStyle {
27
28
  green(text: string): string;
28
29
  /** Tint a status that wants attention. */
29
30
  yellow(text: string): string;
31
+ /** Tint a real fault — a failed check or a failed row, nothing milder. */
32
+ red(text: string): string;
30
33
  }
31
34
 
32
35
  /** The identity styler — every function returns its input unchanged. */
@@ -37,6 +40,7 @@ const PLAIN_STYLE: RenderStyle = {
37
40
  cyan: (text) => text,
38
41
  green: (text) => text,
39
42
  yellow: (text) => text,
43
+ red: (text) => text,
40
44
  };
41
45
 
42
46
  /**
@@ -55,6 +59,7 @@ export function styleFor(enabled: boolean): RenderStyle {
55
59
  cyan: (text) => chalk.cyan(text),
56
60
  green: (text) => chalk.green(text),
57
61
  yellow: (text) => chalk.yellow(text),
62
+ red: (text) => chalk.red(text),
58
63
  };
59
64
  }
60
65
 
@@ -75,8 +80,8 @@ export function forbidsColor(env: NodeJS.ProcessEnv): boolean {
75
80
  }
76
81
 
77
82
  // Applied to the SHARED chalk instance at module load, not inside
78
- // `defaultStyle()`, because four modules reach for `chalk` directly rather than
79
- // through `RenderStyle` — help formatting, doctor, upgrade and the colophon's
83
+ // `defaultStyle()`, because three modules reach for `chalk` directly rather
84
+ // than through `RenderStyle` — help formatting, upgrade and the colophon's
80
85
  // Markdown renderer. Gating only the style seam would leave every one of them
81
86
  // still emitting escapes. Zeroing the level once covers them by construction,
82
87
  // including any site added later.
@@ -87,18 +92,36 @@ export function forbidsColor(env: NodeJS.ProcessEnv): boolean {
87
92
  // worked.
88
93
  if (forbidsColor(process.env)) chalk.level = 0;
89
94
 
95
+ /**
96
+ * Whether stdout may carry color: an attended terminal AND a usable chalk
97
+ * level (zeroed by the module-load check above under `NO_COLOR`).
98
+ *
99
+ * The third question of the interactivity vocabulary (`kernel/interactivity.ts`
100
+ * holds the other two, and its docblock says why this one lives here: it needs
101
+ * `chalk`, and that module is on the flag-parsing fast path). Both signals are
102
+ * required — `supports-color` enables color off a TTY in CI, which would paint
103
+ * escapes into a redirected stream every byte-comparison then has to strip.
104
+ *
105
+ * @returns True on a color-capable terminal.
106
+ * @note Impure — reads stdout's capture state and chalk's color level.
107
+ */
108
+ export function canColor(): boolean {
109
+ return !stdoutIsCaptured() && chalk.level > 0;
110
+ }
111
+
90
112
  /**
91
113
  * The active style for this process's stdout.
92
114
  *
93
- * Gated on BOTH signals: stdout must be an attended TTY, so piped / redirected /
94
- * MCP output stays byte-stable even where `supports-color` fires off a TTY (CI
95
- * sets `GITHUB_ACTIONS`, which chalk honors); AND chalk must report a usable
96
- * color level, which the module-load check above forces to zero under
97
- * `NO_COLOR`.
115
+ * Both signals, asked as one named question ({@link canColor}): stdout must be
116
+ * an attended TTY, so piped / redirected / MCP output stays byte-stable even
117
+ * where `supports-color` fires off a TTY (CI sets `GITHUB_ACTIONS`, which chalk
118
+ * honors); AND chalk must report a usable color level, which the module-load
119
+ * check above forces to zero under `NO_COLOR`.
98
120
  *
99
121
  * @returns A colorizing style on a color-capable TTY, else the identity styler.
100
- * @note Impure — reads `process.stdout.isTTY` and chalk's color level.
122
+ * @note Impure — {@link canColor} reads stdout's capture state and chalk's
123
+ * color level.
101
124
  */
102
125
  export function defaultStyle(): RenderStyle {
103
- return styleFor(process.stdout.isTTY === true && chalk.level > 0);
126
+ return styleFor(canColor());
104
127
  }