@ai-substrate/engineering-harness 0.2.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 (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/harness/cli/bin/harness.js +12 -0
  4. package/harness/cli/dist/acts/docs.d.ts +17 -0
  5. package/harness/cli/dist/acts/docs.js +73 -0
  6. package/harness/cli/dist/acts/docs.js.map +1 -0
  7. package/harness/cli/dist/acts/doctor.d.ts +14 -0
  8. package/harness/cli/dist/acts/doctor.js +43 -0
  9. package/harness/cli/dist/acts/doctor.js.map +1 -0
  10. package/harness/cli/dist/acts/help.d.ts +14 -0
  11. package/harness/cli/dist/acts/help.js +29 -0
  12. package/harness/cli/dist/acts/help.js.map +1 -0
  13. package/harness/cli/dist/acts/init.d.ts +22 -0
  14. package/harness/cli/dist/acts/init.js +61 -0
  15. package/harness/cli/dist/acts/init.js.map +1 -0
  16. package/harness/cli/dist/acts/instructions.d.ts +21 -0
  17. package/harness/cli/dist/acts/instructions.js +75 -0
  18. package/harness/cli/dist/acts/instructions.js.map +1 -0
  19. package/harness/cli/dist/acts/new.d.ts +19 -0
  20. package/harness/cli/dist/acts/new.js +66 -0
  21. package/harness/cli/dist/acts/new.js.map +1 -0
  22. package/harness/cli/dist/acts/observe.d.ts +23 -0
  23. package/harness/cli/dist/acts/observe.js +129 -0
  24. package/harness/cli/dist/acts/observe.js.map +1 -0
  25. package/harness/cli/dist/acts/record.d.ts +24 -0
  26. package/harness/cli/dist/acts/record.js +93 -0
  27. package/harness/cli/dist/acts/record.js.map +1 -0
  28. package/harness/cli/dist/acts/skills.d.ts +32 -0
  29. package/harness/cli/dist/acts/skills.js +256 -0
  30. package/harness/cli/dist/acts/skills.js.map +1 -0
  31. package/harness/cli/dist/acts/update.d.ts +23 -0
  32. package/harness/cli/dist/acts/update.js +297 -0
  33. package/harness/cli/dist/acts/update.js.map +1 -0
  34. package/harness/cli/dist/acts/verb.d.ts +27 -0
  35. package/harness/cli/dist/acts/verb.js +56 -0
  36. package/harness/cli/dist/acts/verb.js.map +1 -0
  37. package/harness/cli/dist/adapters/clock/clock-port.d.ts +10 -0
  38. package/harness/cli/dist/adapters/clock/clock-port.js +2 -0
  39. package/harness/cli/dist/adapters/clock/clock-port.js.map +1 -0
  40. package/harness/cli/dist/adapters/clock/fake-clock.d.ts +15 -0
  41. package/harness/cli/dist/adapters/clock/fake-clock.js +25 -0
  42. package/harness/cli/dist/adapters/clock/fake-clock.js.map +1 -0
  43. package/harness/cli/dist/adapters/clock/system-clock.d.ts +5 -0
  44. package/harness/cli/dist/adapters/clock/system-clock.js +7 -0
  45. package/harness/cli/dist/adapters/clock/system-clock.js.map +1 -0
  46. package/harness/cli/dist/adapters/env/env-port.d.ts +17 -0
  47. package/harness/cli/dist/adapters/env/env-port.js +2 -0
  48. package/harness/cli/dist/adapters/env/env-port.js.map +1 -0
  49. package/harness/cli/dist/adapters/env/fake-env.d.ts +15 -0
  50. package/harness/cli/dist/adapters/env/fake-env.js +24 -0
  51. package/harness/cli/dist/adapters/env/fake-env.js.map +1 -0
  52. package/harness/cli/dist/adapters/env/node-env.d.ts +6 -0
  53. package/harness/cli/dist/adapters/env/node-env.js +16 -0
  54. package/harness/cli/dist/adapters/env/node-env.js.map +1 -0
  55. package/harness/cli/dist/adapters/exec/exec-port.d.ts +22 -0
  56. package/harness/cli/dist/adapters/exec/exec-port.js +2 -0
  57. package/harness/cli/dist/adapters/exec/exec-port.js.map +1 -0
  58. package/harness/cli/dist/adapters/exec/fake-exec.d.ts +25 -0
  59. package/harness/cli/dist/adapters/exec/fake-exec.js +25 -0
  60. package/harness/cli/dist/adapters/exec/fake-exec.js.map +1 -0
  61. package/harness/cli/dist/adapters/exec/node-exec.d.ts +14 -0
  62. package/harness/cli/dist/adapters/exec/node-exec.js +38 -0
  63. package/harness/cli/dist/adapters/exec/node-exec.js.map +1 -0
  64. package/harness/cli/dist/adapters/fs/fake-fs.d.ts +22 -0
  65. package/harness/cli/dist/adapters/fs/fake-fs.js +63 -0
  66. package/harness/cli/dist/adapters/fs/fake-fs.js.map +1 -0
  67. package/harness/cli/dist/adapters/fs/fs-port.d.ts +20 -0
  68. package/harness/cli/dist/adapters/fs/fs-port.js +2 -0
  69. package/harness/cli/dist/adapters/fs/fs-port.js.map +1 -0
  70. package/harness/cli/dist/adapters/fs/node-fs.d.ts +9 -0
  71. package/harness/cli/dist/adapters/fs/node-fs.js +30 -0
  72. package/harness/cli/dist/adapters/fs/node-fs.js.map +1 -0
  73. package/harness/cli/dist/adapters/git/exec-git.d.ts +6 -0
  74. package/harness/cli/dist/adapters/git/exec-git.js +21 -0
  75. package/harness/cli/dist/adapters/git/exec-git.js.map +1 -0
  76. package/harness/cli/dist/adapters/git/fake-git.d.ts +15 -0
  77. package/harness/cli/dist/adapters/git/fake-git.js +20 -0
  78. package/harness/cli/dist/adapters/git/fake-git.js.map +1 -0
  79. package/harness/cli/dist/adapters/git/git-port.d.ts +12 -0
  80. package/harness/cli/dist/adapters/git/git-port.js +2 -0
  81. package/harness/cli/dist/adapters/git/git-port.js.map +1 -0
  82. package/harness/cli/dist/adapters/loader/fake-loader.d.ts +13 -0
  83. package/harness/cli/dist/adapters/loader/fake-loader.js +25 -0
  84. package/harness/cli/dist/adapters/loader/fake-loader.js.map +1 -0
  85. package/harness/cli/dist/adapters/loader/jiti-loader.d.ts +15 -0
  86. package/harness/cli/dist/adapters/loader/jiti-loader.js +29 -0
  87. package/harness/cli/dist/adapters/loader/jiti-loader.js.map +1 -0
  88. package/harness/cli/dist/adapters/loader/module-loader-port.d.ts +12 -0
  89. package/harness/cli/dist/adapters/loader/module-loader-port.js +2 -0
  90. package/harness/cli/dist/adapters/loader/module-loader-port.js.map +1 -0
  91. package/harness/cli/dist/adapters/process/fake-process.d.ts +13 -0
  92. package/harness/cli/dist/adapters/process/fake-process.js +21 -0
  93. package/harness/cli/dist/adapters/process/fake-process.js.map +1 -0
  94. package/harness/cli/dist/adapters/process/node-process.d.ts +6 -0
  95. package/harness/cli/dist/adapters/process/node-process.js +17 -0
  96. package/harness/cli/dist/adapters/process/node-process.js.map +1 -0
  97. package/harness/cli/dist/adapters/process/process-port.d.ts +13 -0
  98. package/harness/cli/dist/adapters/process/process-port.js +2 -0
  99. package/harness/cli/dist/adapters/process/process-port.js.map +1 -0
  100. package/harness/cli/dist/adapters/version-lookup/fake-version-lookup.d.ts +13 -0
  101. package/harness/cli/dist/adapters/version-lookup/fake-version-lookup.js +21 -0
  102. package/harness/cli/dist/adapters/version-lookup/fake-version-lookup.js.map +1 -0
  103. package/harness/cli/dist/adapters/version-lookup/node-version-lookup.d.ts +18 -0
  104. package/harness/cli/dist/adapters/version-lookup/node-version-lookup.js +51 -0
  105. package/harness/cli/dist/adapters/version-lookup/node-version-lookup.js.map +1 -0
  106. package/harness/cli/dist/adapters/version-lookup/version-lookup-port.d.ts +19 -0
  107. package/harness/cli/dist/adapters/version-lookup/version-lookup-port.js +2 -0
  108. package/harness/cli/dist/adapters/version-lookup/version-lookup-port.js.map +1 -0
  109. package/harness/cli/dist/app.d.ts +70 -0
  110. package/harness/cli/dist/app.js +221 -0
  111. package/harness/cli/dist/app.js.map +1 -0
  112. package/harness/cli/dist/index.d.ts +2 -0
  113. package/harness/cli/dist/index.js +26 -0
  114. package/harness/cli/dist/index.js.map +1 -0
  115. package/harness/cli/dist/output/envelope.d.ts +68 -0
  116. package/harness/cli/dist/output/envelope.js +56 -0
  117. package/harness/cli/dist/output/envelope.js.map +1 -0
  118. package/harness/cli/dist/output/error-codes.d.ts +57 -0
  119. package/harness/cli/dist/output/error-codes.js +57 -0
  120. package/harness/cli/dist/output/error-codes.js.map +1 -0
  121. package/harness/cli/dist/output/exit.d.ts +29 -0
  122. package/harness/cli/dist/output/exit.js +36 -0
  123. package/harness/cli/dist/output/exit.js.map +1 -0
  124. package/harness/cli/dist/output/output-port.d.ts +54 -0
  125. package/harness/cli/dist/output/output-port.js +55 -0
  126. package/harness/cli/dist/output/output-port.js.map +1 -0
  127. package/harness/cli/dist/output/style.d.ts +33 -0
  128. package/harness/cli/dist/output/style.js +68 -0
  129. package/harness/cli/dist/output/style.js.map +1 -0
  130. package/harness/cli/dist/services/config/load-config.d.ts +27 -0
  131. package/harness/cli/dist/services/config/load-config.js +114 -0
  132. package/harness/cli/dist/services/config/load-config.js.map +1 -0
  133. package/harness/cli/dist/services/docs/contract.d.ts +41 -0
  134. package/harness/cli/dist/services/docs/contract.js +14 -0
  135. package/harness/cli/dist/services/docs/contract.js.map +1 -0
  136. package/harness/cli/dist/services/docs/docs-content.d.ts +37 -0
  137. package/harness/cli/dist/services/docs/docs-content.js +48 -0
  138. package/harness/cli/dist/services/docs/docs-content.js.map +1 -0
  139. package/harness/cli/dist/services/docs/docs-service.d.ts +26 -0
  140. package/harness/cli/dist/services/docs/docs-service.js +25 -0
  141. package/harness/cli/dist/services/docs/docs-service.js.map +1 -0
  142. package/harness/cli/dist/services/doctor/doctor-service.d.ts +69 -0
  143. package/harness/cli/dist/services/doctor/doctor-service.js +237 -0
  144. package/harness/cli/dist/services/doctor/doctor-service.js.map +1 -0
  145. package/harness/cli/dist/services/extensions/contract.d.ts +138 -0
  146. package/harness/cli/dist/services/extensions/contract.js +17 -0
  147. package/harness/cli/dist/services/extensions/contract.js.map +1 -0
  148. package/harness/cli/dist/services/extensions/discovery.d.ts +53 -0
  149. package/harness/cli/dist/services/extensions/discovery.js +116 -0
  150. package/harness/cli/dist/services/extensions/discovery.js.map +1 -0
  151. package/harness/cli/dist/services/extensions/registry.d.ts +63 -0
  152. package/harness/cli/dist/services/extensions/registry.js +165 -0
  153. package/harness/cli/dist/services/extensions/registry.js.map +1 -0
  154. package/harness/cli/dist/services/extensions/verb-context.d.ts +44 -0
  155. package/harness/cli/dist/services/extensions/verb-context.js +97 -0
  156. package/harness/cli/dist/services/extensions/verb-context.js.map +1 -0
  157. package/harness/cli/dist/services/help/help-service.d.ts +42 -0
  158. package/harness/cli/dist/services/help/help-service.js +108 -0
  159. package/harness/cli/dist/services/help/help-service.js.map +1 -0
  160. package/harness/cli/dist/services/init/governance-template.d.ts +27 -0
  161. package/harness/cli/dist/services/init/governance-template.js +72 -0
  162. package/harness/cli/dist/services/init/governance-template.js.map +1 -0
  163. package/harness/cli/dist/services/init/init-service.d.ts +38 -0
  164. package/harness/cli/dist/services/init/init-service.js +44 -0
  165. package/harness/cli/dist/services/init/init-service.js.map +1 -0
  166. package/harness/cli/dist/services/instructions/core-instructions.d.ts +11 -0
  167. package/harness/cli/dist/services/instructions/core-instructions.js +80 -0
  168. package/harness/cli/dist/services/instructions/core-instructions.js.map +1 -0
  169. package/harness/cli/dist/services/instructions/instructions-service.d.ts +52 -0
  170. package/harness/cli/dist/services/instructions/instructions-service.js +53 -0
  171. package/harness/cli/dist/services/instructions/instructions-service.js.map +1 -0
  172. package/harness/cli/dist/services/observe/buffer-codec.d.ts +51 -0
  173. package/harness/cli/dist/services/observe/buffer-codec.js +139 -0
  174. package/harness/cli/dist/services/observe/buffer-codec.js.map +1 -0
  175. package/harness/cli/dist/services/observe/observe-service.d.ts +87 -0
  176. package/harness/cli/dist/services/observe/observe-service.js +221 -0
  177. package/harness/cli/dist/services/observe/observe-service.js.map +1 -0
  178. package/harness/cli/dist/services/record/contract.d.ts +32 -0
  179. package/harness/cli/dist/services/record/contract.js +17 -0
  180. package/harness/cli/dist/services/record/contract.js.map +1 -0
  181. package/harness/cli/dist/services/record/core-types/retro.d.ts +20 -0
  182. package/harness/cli/dist/services/record/core-types/retro.js +55 -0
  183. package/harness/cli/dist/services/record/core-types/retro.js.map +1 -0
  184. package/harness/cli/dist/services/record/record-service.d.ts +38 -0
  185. package/harness/cli/dist/services/record/record-service.js +144 -0
  186. package/harness/cli/dist/services/record/record-service.js.map +1 -0
  187. package/harness/cli/dist/services/record/registry.d.ts +46 -0
  188. package/harness/cli/dist/services/record/registry.js +71 -0
  189. package/harness/cli/dist/services/record/registry.js.map +1 -0
  190. package/harness/cli/dist/services/scaffold/scaffold-service.d.ts +29 -0
  191. package/harness/cli/dist/services/scaffold/scaffold-service.js +88 -0
  192. package/harness/cli/dist/services/scaffold/scaffold-service.js.map +1 -0
  193. package/harness/cli/dist/services/scaffold/templates.d.ts +42 -0
  194. package/harness/cli/dist/services/scaffold/templates.js +178 -0
  195. package/harness/cli/dist/services/scaffold/templates.js.map +1 -0
  196. package/harness/cli/dist/services/shared/posix-path.d.ts +54 -0
  197. package/harness/cli/dist/services/shared/posix-path.js +94 -0
  198. package/harness/cli/dist/services/shared/posix-path.js.map +1 -0
  199. package/harness/cli/dist/services/shared/temp.d.ts +24 -0
  200. package/harness/cli/dist/services/shared/temp.js +29 -0
  201. package/harness/cli/dist/services/shared/temp.js.map +1 -0
  202. package/harness/cli/dist/services/skills/contract.d.ts +52 -0
  203. package/harness/cli/dist/services/skills/contract.js +55 -0
  204. package/harness/cli/dist/services/skills/contract.js.map +1 -0
  205. package/harness/cli/dist/services/skills/skills-service.d.ts +73 -0
  206. package/harness/cli/dist/services/skills/skills-service.js +132 -0
  207. package/harness/cli/dist/services/skills/skills-service.js.map +1 -0
  208. package/harness/cli/dist/services/update/banner.d.ts +26 -0
  209. package/harness/cli/dist/services/update/banner.js +28 -0
  210. package/harness/cli/dist/services/update/banner.js.map +1 -0
  211. package/harness/cli/dist/services/update/cache.d.ts +21 -0
  212. package/harness/cli/dist/services/update/cache.js +61 -0
  213. package/harness/cli/dist/services/update/cache.js.map +1 -0
  214. package/harness/cli/dist/services/update/constants.d.ts +9 -0
  215. package/harness/cli/dist/services/update/constants.js +10 -0
  216. package/harness/cli/dist/services/update/constants.js.map +1 -0
  217. package/harness/cli/dist/services/update/install.d.ts +26 -0
  218. package/harness/cli/dist/services/update/install.js +78 -0
  219. package/harness/cli/dist/services/update/install.js.map +1 -0
  220. package/harness/cli/dist/services/update/semver.d.ts +16 -0
  221. package/harness/cli/dist/services/update/semver.js +108 -0
  222. package/harness/cli/dist/services/update/semver.js.map +1 -0
  223. package/harness/cli/dist/services/update/update-service.d.ts +46 -0
  224. package/harness/cli/dist/services/update/update-service.js +91 -0
  225. package/harness/cli/dist/services/update/update-service.js.map +1 -0
  226. package/harness/cli/dist/version.d.ts +8 -0
  227. package/harness/cli/dist/version.js +15 -0
  228. package/harness/cli/dist/version.js.map +1 -0
  229. package/package.json +56 -0
@@ -0,0 +1,221 @@
1
+ import { Command } from 'commander';
2
+ import { registerDocsAct } from './acts/docs.js';
3
+ import { registerDoctorAct } from './acts/doctor.js';
4
+ import { registerHelpAct } from './acts/help.js';
5
+ import { registerInitAct } from './acts/init.js';
6
+ import { registerInstructionsAct } from './acts/instructions.js';
7
+ import { registerNewAct } from './acts/new.js';
8
+ import { registerObserveAct } from './acts/observe.js';
9
+ import { registerRecordAct } from './acts/record.js';
10
+ import { registerSkillsAct } from './acts/skills.js';
11
+ import { registerUpdateAct } from './acts/update.js';
12
+ import { registerVerbAct } from './acts/verb.js';
13
+ import { SystemClock } from './adapters/clock/system-clock.js';
14
+ import { NodeEnv } from './adapters/env/node-env.js';
15
+ import { NodeExec } from './adapters/exec/node-exec.js';
16
+ import { NodeFs } from './adapters/fs/node-fs.js';
17
+ import { ExecGit } from './adapters/git/exec-git.js';
18
+ import { JitiLoader } from './adapters/loader/jiti-loader.js';
19
+ import { NodeProcess } from './adapters/process/node-process.js';
20
+ import { formatError, formatOk } from './output/envelope.js';
21
+ import { ErrorCodes } from './output/error-codes.js';
22
+ import { exitWithEnvelope, setBannerDecorator } from './output/exit.js';
23
+ import { createOutputPort, processWriters, selectMode, } from './output/output-port.js';
24
+ import { helpStyleConfig, resolveUseColor } from './output/style.js';
25
+ import { validateVerbRegistry } from './services/config/load-config.js';
26
+ import { discoverExtensions } from './services/extensions/discovery.js';
27
+ import { buildExtensionRegistry, } from './services/extensions/registry.js';
28
+ import { buildRecordRegistry, coreRecordTypes, } from './services/record/registry.js';
29
+ import { buildBannerDecorator } from './services/update/banner.js';
30
+ import { readVersion } from './version.js';
31
+ /**
32
+ * Tri-state read of the output flag from argv. The entrypoint resolves this
33
+ * ONCE — commander collapses `--json`/`--no-json` to a single boolean and loses
34
+ * the "absent" state that lets env/TTY decide, so acts must never re-derive it.
35
+ */
36
+ export function jsonFlag(argv) {
37
+ if (argv.includes('--no-json')) {
38
+ return false;
39
+ }
40
+ if (argv.includes('--json')) {
41
+ return true;
42
+ }
43
+ return undefined;
44
+ }
45
+ /**
46
+ * Safe mode: skip extension discovery entirely (core commands only). Detected
47
+ * from raw argv (`--no-extensions`) or env (`HARNESS_NO_EXTENSIONS=1`) BEFORE
48
+ * parse, since the registry must be known before commander is built.
49
+ *
50
+ * NOTE (v1): the argv scan is a simple `includes`, so a `--no-extensions` placed
51
+ * after a verb name (as a verb's own option) would also trigger safe mode. The
52
+ * env var is the unambiguous path; revisit if a verb ever needs that flag.
53
+ */
54
+ export function isExtensionsDisabled(argv, env) {
55
+ return argv.includes('--no-extensions') || env.HARNESS_NO_EXTENSIONS === '1';
56
+ }
57
+ function orientationEnvelope(version) {
58
+ return formatOk('harness', {
59
+ version,
60
+ purpose: "Front door to this repo's engineering harness.",
61
+ next_steps: ['harness help', 'harness doctor'],
62
+ }, new SystemClock(), { next_action: 'Run `harness help` for the command surface.' });
63
+ }
64
+ /**
65
+ * Map a thrown commander error (raised because `exitOverride` is set) to an
66
+ * actionable envelope. Returns `null` for help/version display (commander
67
+ * already printed; the caller exits 0). Unknown command/option/missing-arg →
68
+ * `E108`; anything else (an unexpected bug) → `E100` — so no raw stack trace
69
+ * ever escapes.
70
+ */
71
+ export function commanderErrorEnvelope(err, clock) {
72
+ if (err.code === 'commander.helpDisplayed' ||
73
+ err.code === 'commander.version' ||
74
+ err.code === 'commander.help') {
75
+ return null;
76
+ }
77
+ const code = typeof err.code === 'string' && err.code.startsWith('commander.')
78
+ ? ErrorCodes.INVALID_ARGS
79
+ : ErrorCodes.UNKNOWN;
80
+ return formatError('harness', code, err.message ?? 'Unexpected error.', clock, {
81
+ next_action: 'Run `harness help` for usage.',
82
+ });
83
+ }
84
+ /** Last-resort envelope for an unexpected error before/around parse — routed through the kernel. */
85
+ function unexpectedEnvelope(err, clock) {
86
+ return formatError('harness', ErrorCodes.UNKNOWN, err instanceof Error ? err.message : String(err), clock, { next_action: 'This is an unexpected harness error; please report it.' });
87
+ }
88
+ /**
89
+ * Discover + load the repo's extensions into the extension registry (verbs +
90
+ * record types + provenance) in one pass, unless safe mode is on (then the
91
+ * registry is empty — core commands + core record types only). Core record-type
92
+ * names are reserved so an extension can never shadow them. Runs BEFORE parse so
93
+ * each verb is a registered command (WS-A Decision 6).
94
+ */
95
+ export async function loadRegistry(argv, env, deps, loader) {
96
+ if (isExtensionsDisabled(argv, env)) {
97
+ return { verbs: [], recordTypes: [], records: [] };
98
+ }
99
+ const discovery = discoverExtensions(deps.fs, deps.proc);
100
+ return buildExtensionRegistry(discovery.candidates, loader, {
101
+ reservedRecordTypes: new Set(coreRecordTypes.map((t) => t.type)),
102
+ rejected: discovery.rejected,
103
+ });
104
+ }
105
+ /**
106
+ * Build the composition root: global flags + core commands (incl. `record`, built
107
+ * from the merged record registry = core ∪ extension) + one subcommand per
108
+ * discovered verb, each registered with the pre-resolved `io` + injected ports. No
109
+ * business logic, no fs/process/git here.
110
+ */
111
+ export function buildProgram(version, io, deps, registry) {
112
+ const program = new Command()
113
+ .name('harness')
114
+ .description("The agent-friendly front door to this repo's engineering harness.")
115
+ .version(version, '-v, --version')
116
+ .option('--json', 'force JSON output')
117
+ .option('--no-json', 'force human output')
118
+ .option('--no-extensions', 'skip loading repo extensions (core commands only)')
119
+ // Core commands sit under the default `Commands:` heading; each extension
120
+ // verb overrides this with `Extensions:` (see registerVerbAct) so the two
121
+ // surfaces read as distinct sections in `--help`. configureHelp accents the
122
+ // headings; commander strips the ANSI itself on non-color output streams.
123
+ .commandsGroup('Commands:')
124
+ .configureHelp(helpStyleConfig())
125
+ .exitOverride();
126
+ const recordRegistry = buildRecordRegistry(coreRecordTypes, registry.recordTypes ?? []);
127
+ // Cross-cutting: register the update banner ONCE so every command's exit
128
+ // chokepoint surfaces a known update (JSON field + human stderr line) from a
129
+ // single sync cache read. No-op until the cache holds a newer version (AC9);
130
+ // with no resolvable home (test fakes) it never fires.
131
+ setBannerDecorator(buildBannerDecorator({
132
+ fs: deps.fs,
133
+ env: deps.env,
134
+ installed: version,
135
+ mode: io.mode,
136
+ writers: io.writers,
137
+ }));
138
+ registerHelpAct(program, io, registry, deps.fs);
139
+ registerDoctorAct(program, io, registry, recordRegistry);
140
+ registerInitAct(program, io, deps);
141
+ registerNewAct(program, io, deps);
142
+ registerDocsAct(program, io);
143
+ registerSkillsAct(program, io, deps);
144
+ registerUpdateAct(program, io, deps, version);
145
+ registerRecordAct(program, io, deps, recordRegistry);
146
+ registerObserveAct(program, io, deps);
147
+ registerInstructionsAct(program, io, { fs: deps.fs, clock: deps.clock }, registry);
148
+ for (const verb of registry.verbs) {
149
+ registerVerbAct(program, verb, deps, io);
150
+ }
151
+ // Bare `harness` (no subcommand) prints an orientation envelope.
152
+ program.action(() => {
153
+ exitWithEnvelope(orientationEnvelope(version), createOutputPort(io.mode, io.writers));
154
+ });
155
+ return program;
156
+ }
157
+ function defaultDeps() {
158
+ return {
159
+ exec: new NodeExec(),
160
+ fs: new NodeFs(),
161
+ env: new NodeEnv(),
162
+ git: new ExecGit(),
163
+ clock: new SystemClock(),
164
+ proc: new NodeProcess(),
165
+ };
166
+ }
167
+ /**
168
+ * The async composition root (WS-A Decision 6): resolve output mode, discover +
169
+ * load extensions, validate the assembled registry, then `await parseAsync`.
170
+ * Three distinct error boundaries keep the kernel the sole exit point:
171
+ * 1. an unexpected discovery/load error → `E100` (routed through the kernel);
172
+ * 2. a malformed registry → the `E120` validation envelope;
173
+ * 3. a commander parse error → `commanderErrorEnvelope` (help/version → exit 0).
174
+ */
175
+ export async function main(argv = process.argv, overrides = {}) {
176
+ const deps = overrides.deps ?? defaultDeps();
177
+ const loader = overrides.loader ?? new JitiLoader();
178
+ const env = overrides.env ?? process.env;
179
+ const isTty = overrides.isTty ?? Boolean(process.stdout.isTTY);
180
+ const writers = overrides.writers ?? processWriters;
181
+ const version = overrides.version ?? readVersion();
182
+ const clock = deps.clock;
183
+ const mode = selectMode({ json: jsonFlag(argv) }, env, isTty);
184
+ const io = { mode, writers, useColor: resolveUseColor({ mode, isTty, env }) };
185
+ const port = createOutputPort(io.mode, io.writers);
186
+ // Register the update banner BEFORE any exit — incl. the pre-build discovery /
187
+ // registry-validation error envelopes below, which exit before buildProgram
188
+ // (which re-registers it) runs (companion F004). Idempotent: same decorator.
189
+ setBannerDecorator(buildBannerDecorator({
190
+ fs: deps.fs,
191
+ env: deps.env,
192
+ installed: version,
193
+ mode: io.mode,
194
+ writers: io.writers,
195
+ }));
196
+ let registry;
197
+ try {
198
+ registry = await loadRegistry(argv, env, deps, loader);
199
+ }
200
+ catch (err) {
201
+ exitWithEnvelope(unexpectedEnvelope(err, clock), port);
202
+ return;
203
+ }
204
+ const check = validateVerbRegistry(registry.verbs, clock);
205
+ if (check.status === 'error') {
206
+ exitWithEnvelope(check, port);
207
+ return;
208
+ }
209
+ try {
210
+ await buildProgram(version, io, deps, registry).parseAsync(argv);
211
+ }
212
+ catch (err) {
213
+ const envelope = commanderErrorEnvelope(err, clock);
214
+ if (envelope === null) {
215
+ // help/version: commander already printed; returning lets Node exit 0.
216
+ return;
217
+ }
218
+ exitWithEnvelope(envelope, port);
219
+ }
220
+ }
221
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './app.js';
3
+ // Pipe-friendly raw output (`harness docs <id> | head`) can have stdout closed
4
+ // by the reader before we finish writing, which Node surfaces as EPIPE. Treat a
5
+ // broken pipe as a normal early-close (not a failure) so the raw-dump path never
6
+ // prints a stack trace; re-surface any other stream error unchanged. Uses
7
+ // process.exitCode (never process.exit) so the single-exit architecture holds.
8
+ process.stdout.on('error', (err) => {
9
+ if (err.code === 'EPIPE') {
10
+ process.exitCode = 0;
11
+ return;
12
+ }
13
+ throw err;
14
+ });
15
+ // Thin bin entry — always runs. All composition logic lives in app.ts (testable,
16
+ // never auto-runs). Called unconditionally so the npm/npx bin symlink works
17
+ // regardless of how the symlink resolves (companion F005).
18
+ //
19
+ // main() routes every expected failure through the exit kernel; this .catch() is
20
+ // a CATASTROPHIC-only net so the async bin can never float an unhandled promise
21
+ // rejection (which would bypass the kernel + the Envelope contract). KF-04.
22
+ main().catch((err) => {
23
+ process.exitCode = 1;
24
+ process.stderr.write(`harness: unexpected error: ${err instanceof Error ? err.message : String(err)}\n`);
25
+ });
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,+EAA+E;AAC/E,gFAAgF;AAChF,iFAAiF;AACjF,0EAA0E;AAC1E,+EAA+E;AAC/E,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;IACxD,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IACD,MAAM,GAAG,CAAC;AACZ,CAAC,CAAC,CAAC;AAEH,iFAAiF;AACjF,4EAA4E;AAC5E,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,4EAA4E;AAC5E,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,8BAA8B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CACnF,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,68 @@
1
+ import type { Clock } from '../adapters/clock/clock-port.js';
2
+ /** The four output states. `unconfigured` is the honest "no mapped behaviour yet". */
3
+ export type Status = 'ok' | 'error' | 'degraded' | 'unconfigured';
4
+ export interface Evidence {
5
+ /** Human label, e.g. "doctor report" or "coverage summary". */
6
+ label: string;
7
+ /** Repo-relative path where durable proof was written, if any. */
8
+ path?: string;
9
+ /** Set true when the command explicitly produced NO durable evidence. */
10
+ none?: boolean;
11
+ }
12
+ /**
13
+ * Optional, additive "an update is available" notice (plan 019). Set ONLY at the
14
+ * exit chokepoint (exit.ts), never by the format* constructors — so existing
15
+ * envelope snapshots / `toEqual` tests are unaffected when no update is known,
16
+ * and the MCP-stable seam stays additive (P4).
17
+ */
18
+ export interface UpdateAvailable {
19
+ /** Currently-installed CLI version. */
20
+ installed: string;
21
+ /** Latest version available on the registry. */
22
+ latest: string;
23
+ /** Exact command to run to update — pinned to "harness update". */
24
+ command: string;
25
+ }
26
+ export interface Envelope {
27
+ command: string;
28
+ status: Status;
29
+ /** ISO-8601, from an injected Clock (deterministic in tests). */
30
+ timestamp: string;
31
+ data?: unknown;
32
+ error?: {
33
+ code: string;
34
+ message: string;
35
+ details?: unknown;
36
+ };
37
+ evidence?: Evidence[];
38
+ next_action?: string;
39
+ /** Optional additive update notice (plan 019); set only at the exit chokepoint. */
40
+ update_available?: UpdateAvailable;
41
+ }
42
+ /**
43
+ * Success. `next_action` is optional for `ok` only — every non-`ok` status has a
44
+ * dedicated constructor below that REQUIRES `next_action` (workshop 001 field-presence rule).
45
+ */
46
+ export declare function formatOk<T>(command: string, data: T, clock: Clock, opts?: {
47
+ evidence?: Evidence[];
48
+ next_action?: string;
49
+ }): Envelope;
50
+ /**
51
+ * Succeeded with caveats (exit 0 by default). `next_action` is REQUIRED — the
52
+ * contract guarantees a non-`ok` envelope always tells an agent what to do next.
53
+ */
54
+ export declare function formatDegraded<T>(command: string, data: T, next_action: string, clock: Clock, opts?: {
55
+ evidence?: Evidence[];
56
+ }): Envelope;
57
+ /**
58
+ * Honest "not built". `next_action` REQUIRED; `data` is optional so a command can
59
+ * carry context (e.g. `harness run smoke --dry-run` → `{dry_run:true,slot,mapped_command:null}`,
60
+ * workshop 001 worked example #4). Always maps to exit 2.
61
+ */
62
+ export declare function formatUnconfigured(command: string, next_action: string, clock: Clock, opts?: {
63
+ data?: unknown;
64
+ }): Envelope;
65
+ export declare function formatError(command: string, code: string, message: string, clock: Clock, opts?: {
66
+ details?: unknown;
67
+ next_action?: string;
68
+ }): Envelope;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Success. `next_action` is optional for `ok` only — every non-`ok` status has a
3
+ * dedicated constructor below that REQUIRES `next_action` (workshop 001 field-presence rule).
4
+ */
5
+ export function formatOk(command, data, clock, opts) {
6
+ return {
7
+ command,
8
+ status: 'ok',
9
+ timestamp: clock.nowIso(),
10
+ data,
11
+ ...(opts?.evidence && { evidence: opts.evidence }),
12
+ ...(opts?.next_action && { next_action: opts.next_action }),
13
+ };
14
+ }
15
+ /**
16
+ * Succeeded with caveats (exit 0 by default). `next_action` is REQUIRED — the
17
+ * contract guarantees a non-`ok` envelope always tells an agent what to do next.
18
+ */
19
+ export function formatDegraded(command, data, next_action, clock, opts) {
20
+ return {
21
+ command,
22
+ status: 'degraded',
23
+ timestamp: clock.nowIso(),
24
+ data,
25
+ ...(opts?.evidence && { evidence: opts.evidence }),
26
+ next_action,
27
+ };
28
+ }
29
+ /**
30
+ * Honest "not built". `next_action` REQUIRED; `data` is optional so a command can
31
+ * carry context (e.g. `harness run smoke --dry-run` → `{dry_run:true,slot,mapped_command:null}`,
32
+ * workshop 001 worked example #4). Always maps to exit 2.
33
+ */
34
+ export function formatUnconfigured(command, next_action, clock, opts) {
35
+ return {
36
+ command,
37
+ status: 'unconfigured',
38
+ timestamp: clock.nowIso(),
39
+ ...(opts?.data !== undefined && { data: opts.data }),
40
+ next_action,
41
+ };
42
+ }
43
+ export function formatError(command, code, message, clock, opts) {
44
+ return {
45
+ command,
46
+ status: 'error',
47
+ timestamp: clock.nowIso(),
48
+ error: {
49
+ code,
50
+ message,
51
+ ...(opts?.details !== undefined && { details: opts.details }),
52
+ },
53
+ next_action: opts?.next_action ?? message,
54
+ };
55
+ }
56
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../src/output/envelope.ts"],"names":[],"mappings":"AA8CA;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,OAAe,EACf,IAAO,EACP,KAAY,EACZ,IAAsD;IAEtD,OAAO;QACL,OAAO;QACP,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE;QACzB,IAAI;QACJ,GAAG,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClD,GAAG,CAAC,IAAI,EAAE,WAAW,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;KAC5D,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAe,EACf,IAAO,EACP,WAAmB,EACnB,KAAY,EACZ,IAAgC;IAEhC,OAAO;QACL,OAAO;QACP,MAAM,EAAE,UAAU;QAClB,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE;QACzB,IAAI;QACJ,GAAG,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClD,WAAW;KACZ,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAe,EACf,WAAmB,EACnB,KAAY,EACZ,IAAyB;IAEzB,OAAO;QACL,OAAO;QACP,MAAM,EAAE,cAAc;QACtB,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE;QACzB,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;QACpD,WAAW;KACZ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,OAAe,EACf,IAAY,EACZ,OAAe,EACf,KAAY,EACZ,IAAkD;IAElD,OAAO;QACL,OAAO;QACP,MAAM,EAAE,OAAO;QACf,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE;QACzB,KAAK,EAAE;YACL,IAAI;YACJ,OAAO;YACP,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;SAC9D;QACD,WAAW,EAAE,IAAI,EAAE,WAAW,IAAI,OAAO;KAC1C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Central error-code table (grep `Exxx` to find call sites). Start minimal;
3
+ * grow as commands are added. Workshop 001 § Error code table.
4
+ */
5
+ export declare const ErrorCodes: {
6
+ /** Unclassified failure (last resort). */
7
+ readonly UNKNOWN: "E100";
8
+ /** Missing/invalid argument or flag. */
9
+ readonly INVALID_ARGS: "E108";
10
+ /** `.harness`/command-map config failed validation. */
11
+ readonly CONFIG_INVALID: "E120";
12
+ /** A doctor check raised an unexpected error (vs. reporting a failing layer). */
13
+ readonly DOCTOR_CHECK_FAILED: "E130";
14
+ /** An extension file couldn't be imported or failed shape validation (per-extension, non-fatal; surfaced by `doctor`). */
15
+ readonly EXTENSION_LOAD_FAILED: "E140";
16
+ /** A verb handler threw at invocation time, or returned an invalid result (isolated → error Envelope, never a raw stack). */
17
+ readonly EXTENSION_RUNTIME_ERROR: "E141";
18
+ /** Two extensions (or an extension + a reserved core name) declared the same verb name. */
19
+ readonly EXTENSION_VERB_CONFLICT: "E142";
20
+ /** A flat code file sits directly under `.harness/extensions/` — unsupported layout since plan 014; move it to `<name>/extension.ts`. */
21
+ readonly EXTENSION_FLAT_LAYOUT: "E143";
22
+ /** An extension folder is missing its conventional `instructions.md` agent briefing (doctor wails; the verb still runs). */
23
+ readonly EXTENSION_INSTRUCTIONS_MISSING: "E144";
24
+ /** `harness instructions <verb>`: the extension's `instructions.md` exists but could not be read. */
25
+ readonly INSTRUCTIONS_UNREADABLE: "E145";
26
+ /** `harness observe`: a bucket's session buffer exists but could not be read (never silent data loss). */
27
+ readonly OBSERVE_BUFFER_UNREADABLE: "E146";
28
+ /** `harness new`: the requested verb name fails the name rules (empty, spaces, separators, etc.). */
29
+ readonly SCAFFOLD_INVALID_NAME: "E150";
30
+ /** `harness new`: the requested name is reserved by a core command (`help`/`doctor`/`new`). */
31
+ readonly SCAFFOLD_NAME_RESERVED: "E151";
32
+ /** `harness new`: a file already exists at the target path and `--force` was not passed. */
33
+ readonly SCAFFOLD_FILE_EXISTS: "E152";
34
+ /** `harness new`: the directory create or file write itself failed (permissions, etc.). */
35
+ readonly SCAFFOLD_WRITE_FAILED: "E153";
36
+ /** `harness docs <id>`: no curated doc is registered under that id. */
37
+ readonly DOC_NOT_FOUND: "E160";
38
+ /** `harness skills install`: the underlying `npx skills add` pass-through failed (non-zero exit / spawn error). */
39
+ readonly SKILLS_INSTALL_FAILED: "E170";
40
+ /** `harness record <type>`: no such record type in the merged registry (core ∪ extension). */
41
+ readonly RECORD_TYPE_UNKNOWN: "E180";
42
+ /** `harness record <type>`: the records directory create or file write itself failed (permissions, etc.). */
43
+ readonly RECORD_WRITE_FAILED: "E181";
44
+ /** `harness init`: writing the governance-doc skeleton (`.harness/engineering-harness.md`) failed (permissions, etc.). */
45
+ readonly INIT_WRITE_FAILED: "E190";
46
+ /** `harness update`/`self-install`: the global npm install failed (generic / unclassified). */
47
+ readonly UPDATE_FAILED: "E200";
48
+ /** `harness update`/`self-install`: the npm registry rejected the install — unexpected auth on the public package (wrong registry / stale login), or it isn't published yet / the registry is unreachable. */
49
+ readonly UPDATE_AUTH_FAILED: "E201";
50
+ /** `harness update`/`self-install`: the global npm install was denied (filesystem permissions). */
51
+ readonly UPDATE_PERMISSION_DENIED: "E202";
52
+ /** `harness update`/`self-install`: npm was not found on PATH. */
53
+ readonly UPDATE_NPM_MISSING: "E203";
54
+ /** `harness update --pin`: the requested version is not published in the registry. */
55
+ readonly UPDATE_VERSION_NOT_FOUND: "E204";
56
+ };
57
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Central error-code table (grep `Exxx` to find call sites). Start minimal;
3
+ * grow as commands are added. Workshop 001 § Error code table.
4
+ */
5
+ export const ErrorCodes = {
6
+ /** Unclassified failure (last resort). */
7
+ UNKNOWN: 'E100',
8
+ /** Missing/invalid argument or flag. */
9
+ INVALID_ARGS: 'E108',
10
+ /** `.harness`/command-map config failed validation. */
11
+ CONFIG_INVALID: 'E120',
12
+ /** A doctor check raised an unexpected error (vs. reporting a failing layer). */
13
+ DOCTOR_CHECK_FAILED: 'E130',
14
+ /** An extension file couldn't be imported or failed shape validation (per-extension, non-fatal; surfaced by `doctor`). */
15
+ EXTENSION_LOAD_FAILED: 'E140',
16
+ /** A verb handler threw at invocation time, or returned an invalid result (isolated → error Envelope, never a raw stack). */
17
+ EXTENSION_RUNTIME_ERROR: 'E141',
18
+ /** Two extensions (or an extension + a reserved core name) declared the same verb name. */
19
+ EXTENSION_VERB_CONFLICT: 'E142',
20
+ /** A flat code file sits directly under `.harness/extensions/` — unsupported layout since plan 014; move it to `<name>/extension.ts`. */
21
+ EXTENSION_FLAT_LAYOUT: 'E143',
22
+ /** An extension folder is missing its conventional `instructions.md` agent briefing (doctor wails; the verb still runs). */
23
+ EXTENSION_INSTRUCTIONS_MISSING: 'E144',
24
+ /** `harness instructions <verb>`: the extension's `instructions.md` exists but could not be read. */
25
+ INSTRUCTIONS_UNREADABLE: 'E145',
26
+ /** `harness observe`: a bucket's session buffer exists but could not be read (never silent data loss). */
27
+ OBSERVE_BUFFER_UNREADABLE: 'E146',
28
+ /** `harness new`: the requested verb name fails the name rules (empty, spaces, separators, etc.). */
29
+ SCAFFOLD_INVALID_NAME: 'E150',
30
+ /** `harness new`: the requested name is reserved by a core command (`help`/`doctor`/`new`). */
31
+ SCAFFOLD_NAME_RESERVED: 'E151',
32
+ /** `harness new`: a file already exists at the target path and `--force` was not passed. */
33
+ SCAFFOLD_FILE_EXISTS: 'E152',
34
+ /** `harness new`: the directory create or file write itself failed (permissions, etc.). */
35
+ SCAFFOLD_WRITE_FAILED: 'E153',
36
+ /** `harness docs <id>`: no curated doc is registered under that id. */
37
+ DOC_NOT_FOUND: 'E160',
38
+ /** `harness skills install`: the underlying `npx skills add` pass-through failed (non-zero exit / spawn error). */
39
+ SKILLS_INSTALL_FAILED: 'E170',
40
+ /** `harness record <type>`: no such record type in the merged registry (core ∪ extension). */
41
+ RECORD_TYPE_UNKNOWN: 'E180',
42
+ /** `harness record <type>`: the records directory create or file write itself failed (permissions, etc.). */
43
+ RECORD_WRITE_FAILED: 'E181',
44
+ /** `harness init`: writing the governance-doc skeleton (`.harness/engineering-harness.md`) failed (permissions, etc.). */
45
+ INIT_WRITE_FAILED: 'E190',
46
+ /** `harness update`/`self-install`: the global npm install failed (generic / unclassified). */
47
+ UPDATE_FAILED: 'E200',
48
+ /** `harness update`/`self-install`: the npm registry rejected the install — unexpected auth on the public package (wrong registry / stale login), or it isn't published yet / the registry is unreachable. */
49
+ UPDATE_AUTH_FAILED: 'E201',
50
+ /** `harness update`/`self-install`: the global npm install was denied (filesystem permissions). */
51
+ UPDATE_PERMISSION_DENIED: 'E202',
52
+ /** `harness update`/`self-install`: npm was not found on PATH. */
53
+ UPDATE_NPM_MISSING: 'E203',
54
+ /** `harness update --pin`: the requested version is not published in the registry. */
55
+ UPDATE_VERSION_NOT_FOUND: 'E204',
56
+ };
57
+ //# sourceMappingURL=error-codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../../src/output/error-codes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,0CAA0C;IAC1C,OAAO,EAAE,MAAM;IACf,wCAAwC;IACxC,YAAY,EAAE,MAAM;IACpB,uDAAuD;IACvD,cAAc,EAAE,MAAM;IACtB,iFAAiF;IACjF,mBAAmB,EAAE,MAAM;IAC3B,0HAA0H;IAC1H,qBAAqB,EAAE,MAAM;IAC7B,6HAA6H;IAC7H,uBAAuB,EAAE,MAAM;IAC/B,2FAA2F;IAC3F,uBAAuB,EAAE,MAAM;IAC/B,yIAAyI;IACzI,qBAAqB,EAAE,MAAM;IAC7B,4HAA4H;IAC5H,8BAA8B,EAAE,MAAM;IACtC,qGAAqG;IACrG,uBAAuB,EAAE,MAAM;IAC/B,0GAA0G;IAC1G,yBAAyB,EAAE,MAAM;IACjC,qGAAqG;IACrG,qBAAqB,EAAE,MAAM;IAC7B,+FAA+F;IAC/F,sBAAsB,EAAE,MAAM;IAC9B,4FAA4F;IAC5F,oBAAoB,EAAE,MAAM;IAC5B,2FAA2F;IAC3F,qBAAqB,EAAE,MAAM;IAC7B,uEAAuE;IACvE,aAAa,EAAE,MAAM;IACrB,mHAAmH;IACnH,qBAAqB,EAAE,MAAM;IAC7B,8FAA8F;IAC9F,mBAAmB,EAAE,MAAM;IAC3B,6GAA6G;IAC7G,mBAAmB,EAAE,MAAM;IAC3B,0HAA0H;IAC1H,iBAAiB,EAAE,MAAM;IACzB,+FAA+F;IAC/F,aAAa,EAAE,MAAM;IACrB,8MAA8M;IAC9M,kBAAkB,EAAE,MAAM;IAC1B,mGAAmG;IACnG,wBAAwB,EAAE,MAAM;IAChC,kEAAkE;IAClE,kBAAkB,EAAE,MAAM;IAC1B,sFAAsF;IACtF,wBAAwB,EAAE,MAAM;CACxB,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { Envelope } from './envelope.js';
2
+ import type { OutputPort, Writers } from './output-port.js';
3
+ export declare function exitCodeFor(env: Envelope): number;
4
+ /**
5
+ * A decorator applied to every outgoing envelope at the single exit chokepoint,
6
+ * registered ONCE by the composition root (plan 019, T007). It may mutate the
7
+ * envelope (e.g. set the additive `update_available` field, which the JSON
8
+ * renderer then serializes) and/or write a side-channel line (e.g. the human-mode
9
+ * update banner to stderr).
10
+ *
11
+ * Centralising it here is deliberate (KF-09): `exitWithEnvelope` is the ONE path
12
+ * every command exits through (43 call sites), and the per-act bespoke human
13
+ * `{ emit }` ports bypass `renderHuman` — so decorating here is the only place
14
+ * that reaches EVERY command with zero per-site plumbing. Default null = no-op.
15
+ */
16
+ export type BannerDecorator = (env: Envelope) => void;
17
+ /** Register (or clear, with null) the exit-chokepoint banner decorator. */
18
+ export declare function setBannerDecorator(decorator: BannerDecorator | null): void;
19
+ /** Single exit point for the whole CLI — only the kernel calls process.exit. */
20
+ export declare function exitWithEnvelope(env: Envelope, io: OutputPort): never;
21
+ /**
22
+ * Verbatim passthrough exit: write raw text to stdout and let the process exit
23
+ * NATURALLY with `code` (0 by default) — set `process.exitCode` and return, never
24
+ * `process.exit`. A large raw payload (e.g. `harness docs <id>`) piped or
25
+ * redirected must not be truncated by an early `process.exit` that races the
26
+ * stdout flush; a natural return lets Node drain stdout first (companion F002).
27
+ * Envelope-bearing commands still use `exitWithEnvelope`.
28
+ */
29
+ export declare function emitRawAndExit(text: string, writers: Writers, code?: number): void;
@@ -0,0 +1,36 @@
1
+ /** Status → exit code (authoritative). Workshop 001 § Status → exit code mapping. */
2
+ const EXIT_BY_STATUS = {
3
+ ok: 0,
4
+ degraded: 0,
5
+ unconfigured: 2,
6
+ error: 1,
7
+ };
8
+ export function exitCodeFor(env) {
9
+ return EXIT_BY_STATUS[env.status];
10
+ }
11
+ let bannerDecorator = null;
12
+ /** Register (or clear, with null) the exit-chokepoint banner decorator. */
13
+ export function setBannerDecorator(decorator) {
14
+ bannerDecorator = decorator;
15
+ }
16
+ /** Single exit point for the whole CLI — only the kernel calls process.exit. */
17
+ export function exitWithEnvelope(env, io) {
18
+ // Decorate BEFORE emit so the JSON renderer serializes any field the decorator
19
+ // sets (e.g. update_available) and the human banner precedes the act's output.
20
+ bannerDecorator?.(env);
21
+ io.emit(env);
22
+ process.exit(exitCodeFor(env));
23
+ }
24
+ /**
25
+ * Verbatim passthrough exit: write raw text to stdout and let the process exit
26
+ * NATURALLY with `code` (0 by default) — set `process.exitCode` and return, never
27
+ * `process.exit`. A large raw payload (e.g. `harness docs <id>`) piped or
28
+ * redirected must not be truncated by an early `process.exit` that races the
29
+ * stdout flush; a natural return lets Node drain stdout first (companion F002).
30
+ * Envelope-bearing commands still use `exitWithEnvelope`.
31
+ */
32
+ export function emitRawAndExit(text, writers, code = 0) {
33
+ writers.out(text);
34
+ process.exitCode = code;
35
+ }
36
+ //# sourceMappingURL=exit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exit.js","sourceRoot":"","sources":["../../src/output/exit.ts"],"names":[],"mappings":"AAGA,qFAAqF;AACrF,MAAM,cAAc,GAA2B;IAC7C,EAAE,EAAE,CAAC;IACL,QAAQ,EAAE,CAAC;IACX,YAAY,EAAE,CAAC;IACf,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,GAAa;IACvC,OAAO,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAgBD,IAAI,eAAe,GAA2B,IAAI,CAAC;AAEnD,2EAA2E;AAC3E,MAAM,UAAU,kBAAkB,CAAC,SAAiC;IAClE,eAAe,GAAG,SAAS,CAAC;AAC9B,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,GAAa,EAAE,EAAc;IAC5D,+EAA+E;IAC/E,+EAA+E;IAC/E,eAAe,EAAE,CAAC,GAAG,CAAC,CAAC;IACvB,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAgB,EAAE,IAAI,GAAG,CAAC;IACrE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { Envelope } from './envelope.js';
2
+ /**
3
+ * The sink the kernel exits through. Concrete human/JSON renderers (output-port.ts
4
+ * § T008) implement this; defining it here keeps `exit.ts` free of renderer details.
5
+ */
6
+ export interface OutputPort {
7
+ emit(env: Envelope): void;
8
+ }
9
+ export type OutputMode = 'json' | 'human';
10
+ /**
11
+ * Resolved per-invocation I/O, computed ONCE by the entrypoint and threaded to
12
+ * acts. Acts must NOT re-derive the mode from `program.opts()` — commander
13
+ * collapses `--json`/`--no-json` to a boolean and loses the "flag absent" state
14
+ * that lets env/TTY decide. The entrypoint resolves it via `jsonFlag(argv)`.
15
+ */
16
+ export interface CliIo {
17
+ mode: OutputMode;
18
+ writers: Writers;
19
+ /**
20
+ * Whether the hand-rolled `harness help` renderer should emit ANSI color.
21
+ * Resolved ONCE by the entrypoint (human + interactive TTY, minus NO_COLOR);
22
+ * optional so test call sites that omit it default to plain text. Commander's
23
+ * own `--help` does its own color detection and ignores this.
24
+ */
25
+ useColor?: boolean;
26
+ }
27
+ /**
28
+ * Where rendered text goes. Injected so renderers are unit-testable without
29
+ * touching real process streams.
30
+ */
31
+ export interface Writers {
32
+ out(text: string): void;
33
+ err(text: string): void;
34
+ }
35
+ /** Default writers — the real process streams. */
36
+ export declare const processWriters: Writers;
37
+ /**
38
+ * Human-vs-JSON selection precedence (highest wins):
39
+ * 1. explicit --json / --no-json flag
40
+ * 2. HARNESS_JSON=1 env (CI, where TTY detection is unreliable)
41
+ * 3. TTY detection: piped (!isTty) => json, interactive => human
42
+ */
43
+ export declare function selectMode(flags: {
44
+ json?: boolean;
45
+ }, env: NodeJS.ProcessEnv, isTty: boolean): OutputMode;
46
+ /** JSON renderer — one parseable line to stdout. */
47
+ export declare function renderJson(env: Envelope, writers?: Writers): void;
48
+ /**
49
+ * Human renderer — progress/diagnostics (here, the next_action) go to stderr;
50
+ * the final one-line summary goes to stdout (so `harness ... | …` pipes the summary).
51
+ */
52
+ export declare function renderHuman(env: Envelope, writers?: Writers): void;
53
+ /** Build an OutputPort for a selected mode. */
54
+ export declare function createOutputPort(mode: OutputMode, writers?: Writers): OutputPort;