@smeltjs/core 0.3.0 → 0.5.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 (146) hide show
  1. package/dist/agents/guide.d.ts +68 -0
  2. package/dist/agents/guide.d.ts.map +1 -0
  3. package/dist/agents/guide.js +72 -0
  4. package/dist/agents/guide.js.map +1 -0
  5. package/dist/agents/instructions.d.ts +133 -0
  6. package/dist/agents/instructions.d.ts.map +1 -0
  7. package/dist/agents/instructions.js +212 -0
  8. package/dist/agents/instructions.js.map +1 -0
  9. package/dist/agents/lint.d.ts +154 -0
  10. package/dist/agents/lint.d.ts.map +1 -0
  11. package/dist/agents/lint.js +761 -0
  12. package/dist/agents/lint.js.map +1 -0
  13. package/dist/agents/split.d.ts +113 -0
  14. package/dist/agents/split.d.ts.map +1 -0
  15. package/dist/agents/split.js +234 -0
  16. package/dist/agents/split.js.map +1 -0
  17. package/dist/cli/agents.d.ts +39 -0
  18. package/dist/cli/agents.d.ts.map +1 -0
  19. package/dist/cli/agents.js +121 -0
  20. package/dist/cli/agents.js.map +1 -0
  21. package/dist/cli/args.d.ts +1 -0
  22. package/dist/cli/args.d.ts.map +1 -1
  23. package/dist/cli/args.js.map +1 -1
  24. package/dist/cli/bin.d.ts.map +1 -1
  25. package/dist/cli/bin.js +9 -4
  26. package/dist/cli/bin.js.map +1 -1
  27. package/dist/cli/config.d.ts +20 -0
  28. package/dist/cli/config.d.ts.map +1 -1
  29. package/dist/cli/config.js +34 -1
  30. package/dist/cli/config.js.map +1 -1
  31. package/dist/cli/doctor.d.ts +66 -0
  32. package/dist/cli/doctor.d.ts.map +1 -0
  33. package/dist/cli/doctor.js +137 -0
  34. package/dist/cli/doctor.js.map +1 -0
  35. package/dist/cli/hooks.d.ts +61 -18
  36. package/dist/cli/hooks.d.ts.map +1 -1
  37. package/dist/cli/hooks.js +178 -241
  38. package/dist/cli/hooks.js.map +1 -1
  39. package/dist/cli/init.d.ts.map +1 -1
  40. package/dist/cli/init.js +8 -14
  41. package/dist/cli/init.js.map +1 -1
  42. package/dist/cli/installed.d.ts +56 -0
  43. package/dist/cli/installed.d.ts.map +1 -0
  44. package/dist/cli/installed.js +105 -0
  45. package/dist/cli/installed.js.map +1 -0
  46. package/dist/cli/lava.d.ts +31 -0
  47. package/dist/cli/lava.d.ts.map +1 -0
  48. package/dist/cli/lava.js +82 -0
  49. package/dist/cli/lava.js.map +1 -0
  50. package/dist/cli/report.d.ts +23 -0
  51. package/dist/cli/report.d.ts.map +1 -1
  52. package/dist/cli/report.js +140 -2
  53. package/dist/cli/report.js.map +1 -1
  54. package/dist/cli/run.d.ts +5 -3
  55. package/dist/cli/run.d.ts.map +1 -1
  56. package/dist/cli/run.js +2 -1
  57. package/dist/cli/run.js.map +1 -1
  58. package/dist/cli/setup.d.ts +87 -0
  59. package/dist/cli/setup.d.ts.map +1 -0
  60. package/dist/cli/setup.js +513 -0
  61. package/dist/cli/setup.js.map +1 -0
  62. package/dist/cli/shell.d.ts +11 -4
  63. package/dist/cli/shell.d.ts.map +1 -1
  64. package/dist/cli/shell.js.map +1 -1
  65. package/dist/cli/subcommands/agents.d.ts +72 -0
  66. package/dist/cli/subcommands/agents.d.ts.map +1 -0
  67. package/dist/cli/subcommands/agents.js +163 -0
  68. package/dist/cli/subcommands/agents.js.map +1 -0
  69. package/dist/cli/subcommands/doctor.d.ts +15 -0
  70. package/dist/cli/subcommands/doctor.d.ts.map +1 -0
  71. package/dist/cli/subcommands/doctor.js +43 -0
  72. package/dist/cli/subcommands/doctor.js.map +1 -0
  73. package/dist/cli/subcommands/flags.d.ts +16 -0
  74. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  75. package/dist/cli/subcommands/flags.js +40 -2
  76. package/dist/cli/subcommands/flags.js.map +1 -1
  77. package/dist/cli/subcommands/hooks.d.ts.map +1 -1
  78. package/dist/cli/subcommands/hooks.js +29 -5
  79. package/dist/cli/subcommands/hooks.js.map +1 -1
  80. package/dist/cli/subcommands/init.d.ts.map +1 -1
  81. package/dist/cli/subcommands/init.js +2 -1
  82. package/dist/cli/subcommands/init.js.map +1 -1
  83. package/dist/cli/subcommands/registry.d.ts +4 -1
  84. package/dist/cli/subcommands/registry.d.ts.map +1 -1
  85. package/dist/cli/subcommands/registry.js +9 -0
  86. package/dist/cli/subcommands/registry.js.map +1 -1
  87. package/dist/cli/subcommands/setup.d.ts +23 -0
  88. package/dist/cli/subcommands/setup.d.ts.map +1 -0
  89. package/dist/cli/subcommands/setup.js +87 -0
  90. package/dist/cli/subcommands/setup.js.map +1 -0
  91. package/dist/cli/subcommands/subcommand.d.ts +1 -1
  92. package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
  93. package/dist/cli/subcommands/subcommand.js.map +1 -1
  94. package/dist/cli/wizard.d.ts +72 -0
  95. package/dist/cli/wizard.d.ts.map +1 -0
  96. package/dist/cli/wizard.js +86 -0
  97. package/dist/cli/wizard.js.map +1 -0
  98. package/dist/harness/claude-code.d.ts.map +1 -1
  99. package/dist/harness/claude-code.js +10 -0
  100. package/dist/harness/claude-code.js.map +1 -1
  101. package/dist/harness/codex.d.ts.map +1 -1
  102. package/dist/harness/codex.js +2 -0
  103. package/dist/harness/codex.js.map +1 -1
  104. package/dist/harness/gemini.d.ts.map +1 -1
  105. package/dist/harness/gemini.js +1 -0
  106. package/dist/harness/gemini.js.map +1 -1
  107. package/dist/harness/grok.d.ts.map +1 -1
  108. package/dist/harness/grok.js +5 -1
  109. package/dist/harness/grok.js.map +1 -1
  110. package/dist/harness/hermes.d.ts.map +1 -1
  111. package/dist/harness/hermes.js +1 -0
  112. package/dist/harness/hermes.js.map +1 -1
  113. package/dist/harness/kilocode.d.ts.map +1 -1
  114. package/dist/harness/kilocode.js +1 -1
  115. package/dist/harness/kilocode.js.map +1 -1
  116. package/dist/harness/opencode.d.ts.map +1 -1
  117. package/dist/harness/opencode.js +10 -0
  118. package/dist/harness/opencode.js.map +1 -1
  119. package/dist/harness/profile.d.ts +49 -3
  120. package/dist/harness/profile.d.ts.map +1 -1
  121. package/dist/harness/profile.js +15 -0
  122. package/dist/harness/profile.js.map +1 -1
  123. package/dist/harness/registry.d.ts +55 -6
  124. package/dist/harness/registry.d.ts.map +1 -1
  125. package/dist/harness/registry.js +63 -6
  126. package/dist/harness/registry.js.map +1 -1
  127. package/dist/harness/snippet.d.ts +14 -1
  128. package/dist/harness/snippet.d.ts.map +1 -1
  129. package/dist/harness/snippet.js +18 -2
  130. package/dist/harness/snippet.js.map +1 -1
  131. package/dist/index.d.ts +22 -0
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +17 -0
  134. package/dist/index.js.map +1 -1
  135. package/dist/setup/recipe.d.ts +73 -0
  136. package/dist/setup/recipe.d.ts.map +1 -0
  137. package/dist/setup/recipe.js +76 -0
  138. package/dist/setup/recipe.js.map +1 -0
  139. package/dist/text/json-edit.d.ts +85 -0
  140. package/dist/text/json-edit.d.ts.map +1 -0
  141. package/dist/text/json-edit.js +290 -0
  142. package/dist/text/json-edit.js.map +1 -0
  143. package/dist/types.d.ts +0 -11
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js.map +1 -1
  146. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\n\nimport { CliUsageError } from '../errors.ts';\nimport { ENFORCEMENT_MODES } from '../hooks/guard-core.ts';\nimport type { EnforcementMode } from '../hooks/guard-core.ts';\nimport { isStrategy, STRATEGIES } from '../plan/planners.ts';\nimport type { Strategy } from '../plan/planners.ts';\n\nimport { CLI_NAME } from './shell.ts';\n\n/**\n * `smelt.config.json` — CLI defaults, and nothing more.\n *\n * This file exists so `smelt` can be run without retyping the same flags, and it is\n * deliberately a *CLI* concern: the programmatic API never reads it. A library that\n * reads config files off the caller's disk is a library whose behaviour depends on\n * where it was invoked from, which is exactly the class of invisible input smelt\n * refuses. `createSmelter()` takes explicit arguments; the CLI translates this file\n * into those arguments, and explicit flags always win over it.\n *\n * The schema is versioned (`\"smeltConfig\": 1`) for the same reason the marker and the\n * `--json` envelope are: files outlive the binaries that wrote them, and a mismatch\n * must be identifiable rather than half-understood. Parsing is strict — an unknown\n * key, a wrong type, or an unknown version is a {@link CliUsageError}, never a shrug.\n * A config the CLI silently ignored would be a budget the user *thought* they set.\n */\n\n/** The file name looked for, from the working directory upward. */\nexport const CONFIG_FILE_NAME = 'smelt.config.json';\n\n/** The schema version this build reads and writes. */\nexport const CONFIG_VERSION = 1;\n\n/** Where the CLI's elision store lives between runs. */\nexport type SmeltConfigStore =\n | { readonly kind: 'memory' }\n | {\n readonly kind: 'directory';\n /** Resolved relative to the directory holding the config file, not the cwd. */\n readonly path: string;\n };\n\n/**\n * The `hooks` block — settings for the harness guard (`smelt hooks install`).\n *\n * Parsed strictly here, like every other key: the CLI refuses a malformed config.\n * The guard core (`src/hooks/guard-core.ts`) reads the same file with its own\n * *tolerant* reader — a guard running inside somebody's session fails open where the\n * CLI correctly refuses — and `test/hooks-guard-core.test.ts` pins the two readers to\n * the same key names and defaults so they cannot drift apart.\n */\nexport interface SmeltConfigHooks {\n /** Reads at or under this many bytes pass the guard untouched. Default 8192. */\n readonly thresholdBytes?: number;\n /**\n * `'deny'` (default): oversized raw reads are refused with a reason naming the\n * exact replacement command. `'rewrite'`: on harnesses whose hooks can modify tool\n * input, the command is substituted instead (never silently — the reason says so).\n */\n readonly enforcement?: EnforcementMode;\n}\n\n/** The parsed shape of `smelt.config.json`. Every field beyond the version is optional. */\nexport interface SmeltConfig {\n readonly smeltConfig: typeof CONFIG_VERSION;\n /** Used when a `smelt` run omits `--budget`. UTF-8 bytes, like every smelt budget. */\n readonly defaultBudgetBytes?: number;\n /** Used when a run omits `--strategy`. Validated against the {@link PLANNERS} registry. */\n readonly strategy?: Strategy;\n /** Used for every run; there is no store flag. Defaults to memory when absent. */\n readonly store?: SmeltConfigStore;\n /** Settings for the harness guard. See {@link SmeltConfigHooks}. */\n readonly hooks?: SmeltConfigHooks;\n}\n\n/** A config plus where it was found — the path matters for resolving `store.path`. */\nexport interface LoadedConfig {\n readonly path: string;\n readonly config: SmeltConfig;\n}\n\n/**\n * The nearest `smelt.config.json`, walking up from `cwd` to the filesystem root —\n * the same discovery shape as `package.json`, so a config at a repo root covers the\n * whole tree. `undefined` when there is none, which is not an error: flags alone are\n * a complete interface.\n */\nexport function findConfigFile(cwd: string): string | undefined {\n let dir = resolve(cwd);\n for (;;) {\n const candidate = join(dir, CONFIG_FILE_NAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/**\n * Find and parse the nearest config. `undefined` when no file exists; a file that\n * exists but is malformed **throws** — see {@link parseConfig}. The distinction is the\n * point: \"no config\" is a fine state, \"a config you cannot have meant\" is not.\n *\n * @throws {CliUsageError} when a config file exists and is malformed.\n */\nexport function loadNearestConfig(cwd: string): LoadedConfig | undefined {\n const path = findConfigFile(cwd);\n if (path === undefined) return undefined;\n return { path, config: parseConfig(readFileSync(path, 'utf8'), path) };\n}\n\n/**\n * Parse and validate one config file, strictly.\n *\n * Strict means: unknown keys are refused, not skipped. A typo'd `defaultBudgetByte`\n * that parsed cleanly would leave the user believing a default was set while every\n * run ignored it — a config error whose failure mode is *silence*, which is the one\n * failure shape this project refuses everywhere else too.\n *\n * @throws {CliUsageError} naming the file and the exact problem.\n */\nexport function parseConfig(text: string, path: string): SmeltConfig {\n const bad = (why: string): CliUsageError =>\n new CliUsageError(\n `${CLI_NAME}: malformed ${CONFIG_FILE_NAME} at ${path}: ${why}\\n` +\n `Fix it or delete it — a config smelt cannot read is never silently ignored. ` +\n `\\`${CLI_NAME} init\\` can rewrite it.`,\n );\n\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw bad(`not JSON (${cause instanceof Error ? cause.message : String(cause)}).`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad('expected a JSON object at the top level.');\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['smeltConfig'] !== CONFIG_VERSION) {\n throw bad(\n `\"smeltConfig\" is ${JSON.stringify(fields['smeltConfig'])}; this build reads version ` +\n `${String(CONFIG_VERSION)}. The schema is versioned so a mismatch is visible ` +\n `instead of half-understood.`,\n );\n }\n\n const known = ['smeltConfig', 'defaultBudgetBytes', 'strategy', 'store', 'hooks'];\n const unknown = Object.keys(fields).filter((key) => !known.includes(key));\n if (unknown.length > 0) {\n throw bad(\n `unknown key${unknown.length === 1 ? '' : 's'} ${unknown.map((k) => `\"${k}\"`).join(', ')}. ` +\n `Known keys: ${known.join(', ')}. Unknown keys are refused because a typo that ` +\n `parsed cleanly would be a setting you believed was set.`,\n );\n }\n\n const budget = fields['defaultBudgetBytes'];\n if (budget !== undefined && (typeof budget !== 'number' || !Number.isInteger(budget))) {\n throw bad(`\"defaultBudgetBytes\" must be a whole number of UTF-8 bytes.`);\n }\n if (typeof budget === 'number' && budget <= 0) {\n throw bad(`\"defaultBudgetBytes\" must be greater than zero, got ${String(budget)}.`);\n }\n\n const strategy = fields['strategy'];\n if (strategy !== undefined && (typeof strategy !== 'string' || !isStrategy(strategy))) {\n throw bad(\n `\"strategy\" must be ${STRATEGIES.map((s) => `\"${s}\"`).join(' or ')}, ` +\n `got ${JSON.stringify(strategy)}.`,\n );\n }\n\n const store = parseStore(fields['store'], bad);\n const hooks = parseHooks(fields['hooks'], bad);\n\n return {\n smeltConfig: CONFIG_VERSION,\n ...(budget === undefined ? {} : { defaultBudgetBytes: budget }),\n ...(strategy === undefined ? {} : { strategy }),\n ...(store === undefined ? {} : { store }),\n ...(hooks === undefined ? {} : { hooks }),\n };\n}\n\n/**\n * Serialize a config back to the bytes of a `smelt.config.json` — {@link parseConfig}'s\n * inverse, and the **only** writer of this file.\n *\n * The module that owns the schema owns both directions. It used to own only the read:\n * two modules hand-built a config object and stringified it — the `init` wizard and\n * `hooks install` — and they had already drifted, one always emitting `strategy` and\n * the other only when it was carried. A sixth key would have been written by whichever\n * module its author was editing and silently dropped by the other, which is the\n * \"setting the user believed was in force\" failure this file refuses everywhere else.\n *\n * Key order is fixed here — the reader's own key order, top level and inside `store`\n * and `hooks` — so a re-run diffs cleanly no matter which verb wrote the file, and two\n * writers can never disagree about shape. What goes *into* the config stays each\n * verb's policy: the wizard's choices, and `hooks install`'s directory-store injection.\n * Absent keys are omitted rather than written as `null`, so\n * `parseConfig(renderConfig(c))` returns `c` field for field —\n * `test/guards/config-writer.test.ts` pins that round trip.\n */\nexport function renderConfig(config: SmeltConfig): string {\n const ordered: SmeltConfig = {\n smeltConfig: CONFIG_VERSION,\n ...(config.defaultBudgetBytes === undefined\n ? {}\n : { defaultBudgetBytes: config.defaultBudgetBytes }),\n ...(config.strategy === undefined ? {} : { strategy: config.strategy }),\n ...(config.store === undefined ? {} : { store: renderStore(config.store) }),\n ...(config.hooks === undefined ? {} : { hooks: renderHooks(config.hooks) }),\n };\n return `${JSON.stringify(ordered, null, 2)}\\n`;\n}\n\nfunction renderStore(store: SmeltConfigStore): SmeltConfigStore {\n return store.kind === 'memory' ? { kind: 'memory' } : { kind: 'directory', path: store.path };\n}\n\nfunction renderHooks(hooks: SmeltConfigHooks): SmeltConfigHooks {\n return {\n ...(hooks.thresholdBytes === undefined ? {} : { thresholdBytes: hooks.thresholdBytes }),\n ...(hooks.enforcement === undefined ? {} : { enforcement: hooks.enforcement }),\n };\n}\n\nfunction parseStore(\n value: unknown,\n bad: (why: string) => CliUsageError,\n): SmeltConfigStore | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad(`\"store\" must be an object like {\"kind\":\"memory\"} or {\"kind\":\"directory\",\"path\":…}.`);\n }\n const fields = value as Record<string, unknown>;\n const kind = fields['kind'];\n if (kind === 'memory') {\n const extra = Object.keys(fields).filter((key) => key !== 'kind');\n if (extra.length > 0) throw bad(`\"store\" of kind \"memory\" takes no other keys.`);\n return { kind: 'memory' };\n }\n if (kind === 'directory') {\n const path = fields['path'];\n if (typeof path !== 'string' || path === '') {\n throw bad(`\"store\" of kind \"directory\" needs a non-empty \"path\".`);\n }\n const extra = Object.keys(fields).filter((key) => key !== 'kind' && key !== 'path');\n if (extra.length > 0) throw bad(`\"store\" of kind \"directory\" takes only \"kind\" and \"path\".`);\n return { kind: 'directory', path };\n }\n throw bad(`\"store\".kind must be \"memory\" or \"directory\", got ${JSON.stringify(kind)}.`);\n}\n\nfunction parseHooks(\n value: unknown,\n bad: (why: string) => CliUsageError,\n): SmeltConfigHooks | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad(`\"hooks\" must be an object like {\"thresholdBytes\":8192,\"enforcement\":\"deny\"}.`);\n }\n const fields = value as Record<string, unknown>;\n const extra = Object.keys(fields).filter(\n (key) => key !== 'thresholdBytes' && key !== 'enforcement',\n );\n if (extra.length > 0) {\n throw bad(\n `\"hooks\" takes only \"thresholdBytes\" and \"enforcement\", ` +\n `got ${extra.map((k) => `\"${k}\"`).join(', ')}.`,\n );\n }\n const threshold = fields['thresholdBytes'];\n if (\n threshold !== undefined &&\n (typeof threshold !== 'number' || !Number.isInteger(threshold) || threshold <= 0)\n ) {\n throw bad(`\"hooks\".thresholdBytes must be a whole number of bytes greater than zero.`);\n }\n const enforcement = fields['enforcement'];\n if (\n enforcement !== undefined &&\n !(ENFORCEMENT_MODES as readonly unknown[]).includes(enforcement)\n ) {\n throw bad(\n `\"hooks\".enforcement must be ${ENFORCEMENT_MODES.map((m) => `\"${m}\"`).join(' or ')}, ` +\n `got ${JSON.stringify(enforcement)}.`,\n );\n }\n return {\n ...(threshold === undefined ? {} : { thresholdBytes: threshold as number }),\n ...(enforcement === undefined ? {} : { enforcement: enforcement as EnforcementMode }),\n };\n}\n\n/** `store.path` is relative to the config file, so the config works from any cwd. */\nexport function resolveStorePath(loaded: LoadedConfig, path: string): string {\n return resolve(dirname(loaded.path), path);\n}\n\n/**\n * The store decision a config carries, with `path` already resolved against the config\n * file's directory — one config serves every subdirectory it covers without scattering\n * store roots. `'memory'` is the built-in default: a fresh in-memory store per run.\n */\nexport type ConfiguredStore =\n { readonly kind: 'memory' } | { readonly kind: 'directory'; readonly path: string };\n\n/**\n * The store leg of every merge, read once here rather than in each verb that needs it.\n *\n * There is no store flag, so this leg is config-or-built-in only — which is exactly\n * why it belongs to the config module and not to a verb: `smelt` builds a store from\n * it, and `retrieve`/`stats` refuse when it is not a directory (see `resolveStoreRun`\n * in `subcommands/retrieve.ts`). Three verbs, one reading of the same key.\n */\nexport function configuredStore(config: LoadedConfig | undefined): ConfiguredStore {\n if (config?.config.store === undefined || config.config.store.kind === 'memory') {\n return { kind: 'memory' };\n }\n return { kind: 'directory', path: resolveStorePath(config, config.config.store.path) };\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\n\nimport { CliUsageError } from '../errors.ts';\nimport { ENFORCEMENT_MODES } from '../hooks/guard-core.ts';\nimport type { EnforcementMode } from '../hooks/guard-core.ts';\nimport { isStrategy, STRATEGIES } from '../plan/planners.ts';\nimport type { Strategy } from '../plan/planners.ts';\n\nimport { CLI_NAME } from './shell.ts';\n\n/**\n * `smelt.config.json` — CLI defaults, and nothing more.\n *\n * This file exists so `smelt` can be run without retyping the same flags, and it is\n * deliberately a *CLI* concern: the programmatic API never reads it. A library that\n * reads config files off the caller's disk is a library whose behaviour depends on\n * where it was invoked from, which is exactly the class of invisible input smelt\n * refuses. `createSmelter()` takes explicit arguments; the CLI translates this file\n * into those arguments, and explicit flags always win over it.\n *\n * The schema is versioned (`\"smeltConfig\": 1`) for the same reason the marker and the\n * `--json` envelope are: files outlive the binaries that wrote them, and a mismatch\n * must be identifiable rather than half-understood. Parsing is strict — an unknown\n * key, a wrong type, or an unknown version is a {@link CliUsageError}, never a shrug.\n * A config the CLI silently ignored would be a budget the user *thought* they set.\n */\n\n/** The file name looked for, from the working directory upward. */\nexport const CONFIG_FILE_NAME = 'smelt.config.json';\n\n/** The schema version this build reads and writes. */\nexport const CONFIG_VERSION = 1;\n\n/** Where the CLI's elision store lives between runs. */\nexport type SmeltConfigStore =\n | { readonly kind: 'memory' }\n | {\n readonly kind: 'directory';\n /** Resolved relative to the directory holding the config file, not the cwd. */\n readonly path: string;\n };\n\n/**\n * The `hooks` block — settings for the harness guard (`smelt hooks install`).\n *\n * Parsed strictly here, like every other key: the CLI refuses a malformed config.\n * The guard core (`src/hooks/guard-core.ts`) reads the same file with its own\n * *tolerant* reader — a guard running inside somebody's session fails open where the\n * CLI correctly refuses — and `test/hooks-guard-core.test.ts` pins the two readers to\n * the same key names and defaults so they cannot drift apart.\n */\nexport interface SmeltConfigHooks {\n /** Reads at or under this many bytes pass the guard untouched. Default 8192. */\n readonly thresholdBytes?: number;\n /**\n * `'deny'` (default): oversized raw reads are refused with a reason naming the\n * exact replacement command. `'rewrite'`: on harnesses whose hooks can modify tool\n * input, the command is substituted instead (never silently — the reason says so).\n */\n readonly enforcement?: EnforcementMode;\n}\n\n/**\n * The `agents` block — settings for `smelt agents lint`.\n *\n * One key, and it is the user's number: how many UTF-8 bytes of instruction file this\n * project is willing to pay on every request. **There is no default and there never\n * will be** — the same ruling as `--budget`, for the same reason. A budget smelt\n * invented would be smelt deciding how much of somebody's context window their own\n * house rules deserve, at a number nobody chose. The guide's cited \"~150-200\n * instructions\" figure is printed by the lint as a citation and compared to nothing.\n */\nexport interface SmeltConfigAgents {\n /**\n * The merged set's byte ceiling — every `AGENTS.md`/`CLAUDE.md`/`GEMINI.md` in the\n * tree, summed. Absent means unbudgeted: the lint measures and reports, and no\n * number can fail.\n */\n readonly budgetBytes?: number;\n}\n\n/** The parsed shape of `smelt.config.json`. Every field beyond the version is optional. */\nexport interface SmeltConfig {\n readonly smeltConfig: typeof CONFIG_VERSION;\n /** Used when a `smelt` run omits `--budget`. UTF-8 bytes, like every smelt budget. */\n readonly defaultBudgetBytes?: number;\n /** Used when a run omits `--strategy`. Validated against the {@link PLANNERS} registry. */\n readonly strategy?: Strategy;\n /** Used for every run; there is no store flag. Defaults to memory when absent. */\n readonly store?: SmeltConfigStore;\n /** Settings for the harness guard. See {@link SmeltConfigHooks}. */\n readonly hooks?: SmeltConfigHooks;\n /** Settings for `smelt agents lint`. See {@link SmeltConfigAgents}. */\n readonly agents?: SmeltConfigAgents;\n}\n\n/** A config plus where it was found — the path matters for resolving `store.path`. */\nexport interface LoadedConfig {\n readonly path: string;\n readonly config: SmeltConfig;\n}\n\n/**\n * The nearest `smelt.config.json`, walking up from `cwd` to the filesystem root —\n * the same discovery shape as `package.json`, so a config at a repo root covers the\n * whole tree. `undefined` when there is none, which is not an error: flags alone are\n * a complete interface.\n */\nexport function findConfigFile(cwd: string): string | undefined {\n let dir = resolve(cwd);\n for (;;) {\n const candidate = join(dir, CONFIG_FILE_NAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/**\n * Find and parse the nearest config. `undefined` when no file exists; a file that\n * exists but is malformed **throws** — see {@link parseConfig}. The distinction is the\n * point: \"no config\" is a fine state, \"a config you cannot have meant\" is not.\n *\n * @throws {CliUsageError} when a config file exists and is malformed.\n */\nexport function loadNearestConfig(cwd: string): LoadedConfig | undefined {\n const path = findConfigFile(cwd);\n if (path === undefined) return undefined;\n return { path, config: parseConfig(readFileSync(path, 'utf8'), path) };\n}\n\n/**\n * Parse and validate one config file, strictly.\n *\n * Strict means: unknown keys are refused, not skipped. A typo'd `defaultBudgetByte`\n * that parsed cleanly would leave the user believing a default was set while every\n * run ignored it — a config error whose failure mode is *silence*, which is the one\n * failure shape this project refuses everywhere else too.\n *\n * @throws {CliUsageError} naming the file and the exact problem.\n */\nexport function parseConfig(text: string, path: string): SmeltConfig {\n const bad = (why: string): CliUsageError =>\n new CliUsageError(\n `${CLI_NAME}: malformed ${CONFIG_FILE_NAME} at ${path}: ${why}\\n` +\n `Fix it or delete it — a config smelt cannot read is never silently ignored. ` +\n `\\`${CLI_NAME} init\\` can rewrite it.`,\n );\n\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw bad(`not JSON (${cause instanceof Error ? cause.message : String(cause)}).`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad('expected a JSON object at the top level.');\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['smeltConfig'] !== CONFIG_VERSION) {\n throw bad(\n `\"smeltConfig\" is ${JSON.stringify(fields['smeltConfig'])}; this build reads version ` +\n `${String(CONFIG_VERSION)}. The schema is versioned so a mismatch is visible ` +\n `instead of half-understood.`,\n );\n }\n\n const known = ['smeltConfig', 'defaultBudgetBytes', 'strategy', 'store', 'hooks', 'agents'];\n const unknown = Object.keys(fields).filter((key) => !known.includes(key));\n if (unknown.length > 0) {\n throw bad(\n `unknown key${unknown.length === 1 ? '' : 's'} ${unknown.map((k) => `\"${k}\"`).join(', ')}. ` +\n `Known keys: ${known.join(', ')}. Unknown keys are refused because a typo that ` +\n `parsed cleanly would be a setting you believed was set.`,\n );\n }\n\n const budget = fields['defaultBudgetBytes'];\n if (budget !== undefined && (typeof budget !== 'number' || !Number.isInteger(budget))) {\n throw bad(`\"defaultBudgetBytes\" must be a whole number of UTF-8 bytes.`);\n }\n if (typeof budget === 'number' && budget <= 0) {\n throw bad(`\"defaultBudgetBytes\" must be greater than zero, got ${String(budget)}.`);\n }\n\n const strategy = fields['strategy'];\n if (strategy !== undefined && (typeof strategy !== 'string' || !isStrategy(strategy))) {\n throw bad(\n `\"strategy\" must be ${STRATEGIES.map((s) => `\"${s}\"`).join(' or ')}, ` +\n `got ${JSON.stringify(strategy)}.`,\n );\n }\n\n const store = parseStore(fields['store'], bad);\n const hooks = parseHooks(fields['hooks'], bad);\n const agents = parseAgents(fields['agents'], bad);\n\n return {\n smeltConfig: CONFIG_VERSION,\n ...(budget === undefined ? {} : { defaultBudgetBytes: budget }),\n ...(strategy === undefined ? {} : { strategy }),\n ...(store === undefined ? {} : { store }),\n ...(hooks === undefined ? {} : { hooks }),\n ...(agents === undefined ? {} : { agents }),\n };\n}\n\n/**\n * Serialize a config back to the bytes of a `smelt.config.json` — {@link parseConfig}'s\n * inverse, and the **only** writer of this file.\n *\n * The module that owns the schema owns both directions. It used to own only the read:\n * two modules hand-built a config object and stringified it — the `init` wizard and\n * `hooks install` — and they had already drifted, one always emitting `strategy` and\n * the other only when it was carried. A sixth key would have been written by whichever\n * module its author was editing and silently dropped by the other, which is the\n * \"setting the user believed was in force\" failure this file refuses everywhere else.\n *\n * Key order is fixed here — the reader's own key order, top level and inside `store`\n * and `hooks` — so a re-run diffs cleanly no matter which verb wrote the file, and two\n * writers can never disagree about shape. What goes *into* the config stays each\n * verb's policy: the wizard's choices, and `hooks install`'s directory-store injection.\n * Absent keys are omitted rather than written as `null`, so\n * `parseConfig(renderConfig(c))` returns `c` field for field —\n * `test/guards/config-writer.test.ts` pins that round trip.\n */\nexport function renderConfig(config: SmeltConfig): string {\n const ordered: SmeltConfig = {\n smeltConfig: CONFIG_VERSION,\n ...(config.defaultBudgetBytes === undefined\n ? {}\n : { defaultBudgetBytes: config.defaultBudgetBytes }),\n ...(config.strategy === undefined ? {} : { strategy: config.strategy }),\n ...(config.store === undefined ? {} : { store: renderStore(config.store) }),\n ...(config.hooks === undefined ? {} : { hooks: renderHooks(config.hooks) }),\n ...(config.agents === undefined ? {} : { agents: renderAgents(config.agents) }),\n };\n return `${JSON.stringify(ordered, null, 2)}\\n`;\n}\n\nfunction renderStore(store: SmeltConfigStore): SmeltConfigStore {\n return store.kind === 'memory' ? { kind: 'memory' } : { kind: 'directory', path: store.path };\n}\n\nfunction renderHooks(hooks: SmeltConfigHooks): SmeltConfigHooks {\n return {\n ...(hooks.thresholdBytes === undefined ? {} : { thresholdBytes: hooks.thresholdBytes }),\n ...(hooks.enforcement === undefined ? {} : { enforcement: hooks.enforcement }),\n };\n}\n\nfunction renderAgents(agents: SmeltConfigAgents): SmeltConfigAgents {\n return agents.budgetBytes === undefined ? {} : { budgetBytes: agents.budgetBytes };\n}\n\nfunction parseStore(\n value: unknown,\n bad: (why: string) => CliUsageError,\n): SmeltConfigStore | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad(`\"store\" must be an object like {\"kind\":\"memory\"} or {\"kind\":\"directory\",\"path\":…}.`);\n }\n const fields = value as Record<string, unknown>;\n const kind = fields['kind'];\n if (kind === 'memory') {\n const extra = Object.keys(fields).filter((key) => key !== 'kind');\n if (extra.length > 0) throw bad(`\"store\" of kind \"memory\" takes no other keys.`);\n return { kind: 'memory' };\n }\n if (kind === 'directory') {\n const path = fields['path'];\n if (typeof path !== 'string' || path === '') {\n throw bad(`\"store\" of kind \"directory\" needs a non-empty \"path\".`);\n }\n const extra = Object.keys(fields).filter((key) => key !== 'kind' && key !== 'path');\n if (extra.length > 0) throw bad(`\"store\" of kind \"directory\" takes only \"kind\" and \"path\".`);\n return { kind: 'directory', path };\n }\n throw bad(`\"store\".kind must be \"memory\" or \"directory\", got ${JSON.stringify(kind)}.`);\n}\n\nfunction parseHooks(\n value: unknown,\n bad: (why: string) => CliUsageError,\n): SmeltConfigHooks | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad(`\"hooks\" must be an object like {\"thresholdBytes\":8192,\"enforcement\":\"deny\"}.`);\n }\n const fields = value as Record<string, unknown>;\n const extra = Object.keys(fields).filter(\n (key) => key !== 'thresholdBytes' && key !== 'enforcement',\n );\n if (extra.length > 0) {\n throw bad(\n `\"hooks\" takes only \"thresholdBytes\" and \"enforcement\", ` +\n `got ${extra.map((k) => `\"${k}\"`).join(', ')}.`,\n );\n }\n const threshold = fields['thresholdBytes'];\n if (\n threshold !== undefined &&\n (typeof threshold !== 'number' || !Number.isInteger(threshold) || threshold <= 0)\n ) {\n throw bad(`\"hooks\".thresholdBytes must be a whole number of bytes greater than zero.`);\n }\n const enforcement = fields['enforcement'];\n if (\n enforcement !== undefined &&\n !(ENFORCEMENT_MODES as readonly unknown[]).includes(enforcement)\n ) {\n throw bad(\n `\"hooks\".enforcement must be ${ENFORCEMENT_MODES.map((m) => `\"${m}\"`).join(' or ')}, ` +\n `got ${JSON.stringify(enforcement)}.`,\n );\n }\n return {\n ...(threshold === undefined ? {} : { thresholdBytes: threshold as number }),\n ...(enforcement === undefined ? {} : { enforcement: enforcement as EnforcementMode }),\n };\n}\n\n/**\n * The `agents` block, parsed as strictly as every other: one key, a whole number of\n * bytes greater than zero.\n *\n * A malformed `agents.budgetBytes` is refused rather than ignored for the reason the\n * whole file is parsed strictly — an ignored budget is a ceiling the user believed was\n * in force. And there is no coercion from a string: `\"budgetBytes\": \"4kb\"` is a\n * mistake with a right answer, and guessing it would be smelt inventing a number.\n */\nfunction parseAgents(\n value: unknown,\n bad: (why: string) => CliUsageError,\n): SmeltConfigAgents | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad(`\"agents\" must be an object like {\"budgetBytes\":2000}.`);\n }\n const fields = value as Record<string, unknown>;\n const extra = Object.keys(fields).filter((key) => key !== 'budgetBytes');\n if (extra.length > 0) {\n throw bad(`\"agents\" takes only \"budgetBytes\", got ${extra.map((k) => `\"${k}\"`).join(', ')}.`);\n }\n const budget = fields['budgetBytes'];\n if (\n budget !== undefined &&\n (typeof budget !== 'number' || !Number.isInteger(budget) || budget <= 0)\n ) {\n throw bad(`\"agents\".budgetBytes must be a whole number of bytes greater than zero.`);\n }\n return budget === undefined ? {} : { budgetBytes: budget as number };\n}\n\n/** `store.path` is relative to the config file, so the config works from any cwd. */\nexport function resolveStorePath(loaded: LoadedConfig, path: string): string {\n return resolve(dirname(loaded.path), path);\n}\n/**\n * The store decision a config carries, with `path` already resolved against the config\n * file's directory — one config serves every subdirectory it covers without scattering\n * store roots. `'memory'` is the built-in default: a fresh in-memory store per run.\n */\nexport type ConfiguredStore =\n { readonly kind: 'memory' } | { readonly kind: 'directory'; readonly path: string };\n\n/**\n * The store leg of every merge, read once here rather than in each verb that needs it.\n *\n * There is no store flag, so this leg is config-or-built-in only — which is exactly\n * why it belongs to the config module and not to a verb: `smelt` builds a store from\n * it, and `retrieve`/`stats` refuse when it is not a directory (see `resolveStoreRun`\n * in `subcommands/retrieve.ts`). Three verbs, one reading of the same key.\n */\nexport function configuredStore(config: LoadedConfig | undefined): ConfiguredStore {\n if (config?.config.store === undefined || config.config.store.kind === 'memory') {\n return { kind: 'memory' };\n }\n return { kind: 'directory', path: resolveStorePath(config, config.config.store.path) };\n}\n"]}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `smelt doctor` — the verdicts over InstalledState. The reading lives in
3
+ * `cli/installed.ts` (one reader, behind the three consumers); this module decides
4
+ * and reports: which blocks are behind the running binary, which pieces are orphans,
5
+ * what the repair is, and what the exit code means. Everything the writers wrote,
6
+ * this verb reads back and compares — and never writes a byte of (ADR-0003).
7
+ *
8
+ * When something is behind, the report ends with the exact repair command —
9
+ * `smelt setup`, per harness where a block is behind — and nothing more happens.
10
+ * The exit code carries the verdict, so the other-machine loop — upgrade, doctor,
11
+ * setup — needs no prose parsing.
12
+ */
13
+ export interface DoctorIo {
14
+ readonly output: (text: string) => void;
15
+ /** Where installed state is read: config discovery, instruction files, hook files. */
16
+ readonly cwd: string;
17
+ /** The running binary's version — what "current" is measured against. */
18
+ readonly version: string;
19
+ }
20
+ export interface DoctorOptions {
21
+ readonly json: boolean;
22
+ }
23
+ /** One instruction block found on disk, with the release that wrote it. */
24
+ export interface DoctorBlock {
25
+ readonly file: string;
26
+ /** The harness profiles whose instruction file this is (often exactly one). */
27
+ readonly harnesses: readonly string[];
28
+ /** The release that wrote it, or `undefined` when it predates stamping. */
29
+ readonly installedBy?: string;
30
+ readonly status: 'current' | 'behind' | 'unversioned';
31
+ }
32
+ /** The config as doctor saw it. A malformed config is a finding, not a crash. */
33
+ export interface DoctorConfig {
34
+ readonly present: boolean;
35
+ readonly malformed?: boolean;
36
+ readonly schemaVersion?: number;
37
+ readonly currentSchema?: boolean;
38
+ readonly budgetBytes?: number;
39
+ readonly store: {
40
+ readonly kind?: 'directory' | 'memory';
41
+ readonly path?: string;
42
+ readonly dirExists?: boolean;
43
+ };
44
+ }
45
+ /** One MCP registration found (or notably absent) on disk. */
46
+ export interface DoctorMcp {
47
+ readonly file: string;
48
+ readonly server: string;
49
+ readonly registered: boolean;
50
+ }
51
+ /** The machine receipt — `--json`. Everything doctor read, and the verdict. */
52
+ export interface DoctorReceipt {
53
+ readonly format: 'smelt.doctor.v1';
54
+ readonly version: string;
55
+ /** True when something is installed and nothing is behind and there are no orphans. */
56
+ readonly current: boolean;
57
+ readonly installed: boolean;
58
+ readonly config: DoctorConfig;
59
+ readonly blocks: readonly DoctorBlock[];
60
+ readonly hookFiles: readonly string[];
61
+ readonly mcp: readonly DoctorMcp[];
62
+ readonly orphans: readonly string[];
63
+ readonly repair: readonly string[];
64
+ }
65
+ export declare function runDoctor(options: DoctorOptions, io: DoctorIo): number;
66
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,sFAAsF;IACtF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,aAAa,CAAC;CACvD;AAED,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,QAAQ,CAAC;QACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;KAC9B,CAAC;CACH;AAED,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,GAAG,EAAE,SAAS,SAAS,EAAE,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,QAAQ,GAAG,MAAM,CA2ItE","sourcesContent":["import { existsSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { CONFIG_FILE_NAME, CONFIG_VERSION } from './config.ts';\nimport { readInstalledState } from './installed.ts';\nimport type { InstalledBlock } from './installed.ts';\nimport { CLI_NAME, EXIT } from './shell.ts';\n\n/**\n * `smelt doctor` — the verdicts over InstalledState. The reading lives in\n * `cli/installed.ts` (one reader, behind the three consumers); this module decides\n * and reports: which blocks are behind the running binary, which pieces are orphans,\n * what the repair is, and what the exit code means. Everything the writers wrote,\n * this verb reads back and compares — and never writes a byte of (ADR-0003).\n *\n * When something is behind, the report ends with the exact repair command —\n * `smelt setup`, per harness where a block is behind — and nothing more happens.\n * The exit code carries the verdict, so the other-machine loop — upgrade, doctor,\n * setup — needs no prose parsing.\n */\n\nexport interface DoctorIo {\n readonly output: (text: string) => void;\n /** Where installed state is read: config discovery, instruction files, hook files. */\n readonly cwd: string;\n /** The running binary's version — what \"current\" is measured against. */\n readonly version: string;\n}\n\nexport interface DoctorOptions {\n readonly json: boolean;\n}\n\n/** One instruction block found on disk, with the release that wrote it. */\nexport interface DoctorBlock {\n readonly file: string;\n /** The harness profiles whose instruction file this is (often exactly one). */\n readonly harnesses: readonly string[];\n /** The release that wrote it, or `undefined` when it predates stamping. */\n readonly installedBy?: string;\n readonly status: 'current' | 'behind' | 'unversioned';\n}\n\n/** The config as doctor saw it. A malformed config is a finding, not a crash. */\nexport interface DoctorConfig {\n readonly present: boolean;\n readonly malformed?: boolean;\n readonly schemaVersion?: number;\n readonly currentSchema?: boolean;\n readonly budgetBytes?: number;\n readonly store: {\n readonly kind?: 'directory' | 'memory';\n readonly path?: string;\n readonly dirExists?: boolean;\n };\n}\n\n/** One MCP registration found (or notably absent) on disk. */\nexport interface DoctorMcp {\n readonly file: string;\n readonly server: string;\n readonly registered: boolean;\n}\n\n/** The machine receipt — `--json`. Everything doctor read, and the verdict. */\nexport interface DoctorReceipt {\n readonly format: 'smelt.doctor.v1';\n readonly version: string;\n /** True when something is installed and nothing is behind and there are no orphans. */\n readonly current: boolean;\n readonly installed: boolean;\n readonly config: DoctorConfig;\n readonly blocks: readonly DoctorBlock[];\n readonly hookFiles: readonly string[];\n readonly mcp: readonly DoctorMcp[];\n readonly orphans: readonly string[];\n readonly repair: readonly string[];\n}\n\nexport function runDoctor(options: DoctorOptions, io: DoctorIo): number {\n const say = (text: string): void => {\n if (!options.json) io.output(text);\n };\n const orphans: string[] = [];\n const repair: string[] = [];\n\n const state = readInstalledState(io.cwd);\n\n // ── verdict: blocks vs the running binary ──\n const blocks: DoctorBlock[] = state.blocks.map((block) => ({\n file: block.file,\n harnesses: block.harnesses,\n ...(block.installedBy === undefined ? {} : { installedBy: block.installedBy }),\n status: blockStatus(block, io.version),\n }));\n const behindBlocks = blocks.filter((block) => block.status === 'behind');\n for (const block of behindBlocks) {\n repair.push(...block.harnesses.map((id) => `${CLI_NAME} setup --harness ${id}`));\n }\n\n // ── config detail + the store-directory orphan ──\n let config: DoctorConfig = { present: false, store: {} };\n if (state.config.present) {\n if (state.config.malformed === true || state.config.parsed === undefined) {\n config = { present: true, malformed: true, store: {} };\n orphans.push(\n `${CONFIG_FILE_NAME} is malformed: ${state.config.malformedWhy ?? 'unparseable JSON'}`,\n );\n repair.push(`${CLI_NAME} setup`);\n } else {\n const parsed = state.config.parsed;\n const configPath = state.config.path ?? join(io.cwd, CONFIG_FILE_NAME);\n const dirExists =\n parsed.store?.kind === 'directory'\n ? existsSync(join(dirname(configPath), parsed.store.path))\n : undefined;\n config = {\n present: true,\n schemaVersion: parsed.smeltConfig,\n currentSchema: parsed.smeltConfig === CONFIG_VERSION,\n ...(parsed.defaultBudgetBytes === undefined\n ? {}\n : { budgetBytes: parsed.defaultBudgetBytes }),\n store: {\n ...(parsed.store === undefined ? {} : { kind: parsed.store.kind }),\n ...(parsed.store?.kind === 'directory' ? { path: parsed.store.path } : {}),\n ...(dirExists === undefined ? {} : { dirExists }),\n },\n };\n if (parsed.store?.kind === 'directory' && dirExists === false) {\n orphans.push(\n `the store directory (${parsed.store.path}) does not exist — retrieves across processes would fail`,\n );\n repair.push(`${CLI_NAME} setup`);\n }\n }\n }\n\n // ── orphans: pieces whose partners are missing ──\n const wired = state.blocks.length > 0 || state.hookFiles.length > 0;\n if (state.mcp.some((one) => one.registered) && !wired) {\n orphans.push(\n 'an MCP registration is present but no hooks wiring is — the guard and the retrieval contract travel together',\n );\n repair.push(`${CLI_NAME} setup`);\n }\n if (wired && !state.config.present) {\n orphans.push(\n 'hooks are wired but there is no smelt.config.json — the store and budget the hooks promise live there',\n );\n repair.push(`${CLI_NAME} setup`);\n }\n\n // ── verdict ──\n const installed = wired || state.config.present || state.mcp.some((one) => one.registered);\n const current = installed && behindBlocks.length === 0 && orphans.length === 0;\n\n say(`${CLI_NAME} doctor — binary ${io.version}, reading ${io.cwd}\\n`);\n if (!installed) {\n say(`Nothing of smelt's is installed here. \\`${CLI_NAME} setup\\` would change that.\\n`);\n } else {\n if (config.present) {\n say(\n ` ${CONFIG_FILE_NAME}: ${\n config.malformed === true\n ? 'MALFORMED'\n : `schema ${String(config.schemaVersion)}, budget ${\n config.budgetBytes === undefined ? 'unset' : String(config.budgetBytes)\n }, store ${describeStore(config)}`\n }\\n`,\n );\n } else {\n say(` ${CONFIG_FILE_NAME}: absent\\n`);\n }\n for (const block of blocks) {\n say(\n ` ${block.file}: written by ${\n block.installedBy ?? 'a pre-stamping release (unversioned)'\n } [${block.status}] — ${block.harnesses.join(', ')}\\n`,\n );\n }\n for (const name of state.hookFiles) say(` ${name}: wired\\n`);\n for (const one of state.mcp) {\n if (one.registered) say(` ${one.file}: ${one.server} registered\\n`);\n }\n for (const orphan of orphans) say(` ORPHAN: ${orphan}\\n`);\n if (behindBlocks.length > 0) {\n say(\n `\\nBehind: the running binary is ${io.version}; re-run setup to bring the ` +\n `installed state to it:\\n` +\n [...new Set(repair)].map((command) => ` ${command}\\n`).join(''),\n );\n } else if (orphans.length > 0) {\n say(`\\nRepair:\\n${[...new Set(repair)].map((command) => ` ${command}\\n`).join('')}`);\n }\n say(\n current\n ? `Current: everything on disk agrees with binary ${io.version}.\\n`\n : `Not current — see above. Doctor never writes; ${CLI_NAME} setup is the repair.\\n`,\n );\n }\n\n if (options.json) {\n const receipt: DoctorReceipt = {\n format: 'smelt.doctor.v1',\n version: io.version,\n current,\n installed,\n config,\n blocks,\n hookFiles: [...state.hookFiles],\n mcp: [...state.mcp],\n orphans,\n repair: [...new Set(repair)],\n };\n io.output(JSON.stringify(receipt, null, 2) + '\\n');\n }\n return current || !installed ? EXIT.ok : EXIT.refused;\n}\n\n/** The verdict over one block: whole-owned files carry no stamp to compare. */\nfunction blockStatus(block: InstalledBlock, binaryVersion: string): DoctorBlock['status'] {\n if (!block.stampable) return 'unversioned';\n return block.installedBy === binaryVersion ? 'current' : 'behind';\n}\n\nfunction describeStore(config: DoctorConfig): string {\n if (config.store.kind === undefined) return 'unset';\n if (config.store.kind === 'memory') return 'memory';\n return `directory at ${config.store.path ?? ''} (${\n config.store.dirExists ? 'present' : 'MISSING'\n })`;\n}\n"]}
@@ -0,0 +1,137 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { CONFIG_FILE_NAME, CONFIG_VERSION } from './config.js';
4
+ import { readInstalledState } from './installed.js';
5
+ import { CLI_NAME, EXIT } from './shell.js';
6
+ export function runDoctor(options, io) {
7
+ const say = (text) => {
8
+ if (!options.json)
9
+ io.output(text);
10
+ };
11
+ const orphans = [];
12
+ const repair = [];
13
+ const state = readInstalledState(io.cwd);
14
+ // ── verdict: blocks vs the running binary ──
15
+ const blocks = state.blocks.map((block) => ({
16
+ file: block.file,
17
+ harnesses: block.harnesses,
18
+ ...(block.installedBy === undefined ? {} : { installedBy: block.installedBy }),
19
+ status: blockStatus(block, io.version),
20
+ }));
21
+ const behindBlocks = blocks.filter((block) => block.status === 'behind');
22
+ for (const block of behindBlocks) {
23
+ repair.push(...block.harnesses.map((id) => `${CLI_NAME} setup --harness ${id}`));
24
+ }
25
+ // ── config detail + the store-directory orphan ──
26
+ let config = { present: false, store: {} };
27
+ if (state.config.present) {
28
+ if (state.config.malformed === true || state.config.parsed === undefined) {
29
+ config = { present: true, malformed: true, store: {} };
30
+ orphans.push(`${CONFIG_FILE_NAME} is malformed: ${state.config.malformedWhy ?? 'unparseable JSON'}`);
31
+ repair.push(`${CLI_NAME} setup`);
32
+ }
33
+ else {
34
+ const parsed = state.config.parsed;
35
+ const configPath = state.config.path ?? join(io.cwd, CONFIG_FILE_NAME);
36
+ const dirExists = parsed.store?.kind === 'directory'
37
+ ? existsSync(join(dirname(configPath), parsed.store.path))
38
+ : undefined;
39
+ config = {
40
+ present: true,
41
+ schemaVersion: parsed.smeltConfig,
42
+ currentSchema: parsed.smeltConfig === CONFIG_VERSION,
43
+ ...(parsed.defaultBudgetBytes === undefined
44
+ ? {}
45
+ : { budgetBytes: parsed.defaultBudgetBytes }),
46
+ store: {
47
+ ...(parsed.store === undefined ? {} : { kind: parsed.store.kind }),
48
+ ...(parsed.store?.kind === 'directory' ? { path: parsed.store.path } : {}),
49
+ ...(dirExists === undefined ? {} : { dirExists }),
50
+ },
51
+ };
52
+ if (parsed.store?.kind === 'directory' && dirExists === false) {
53
+ orphans.push(`the store directory (${parsed.store.path}) does not exist — retrieves across processes would fail`);
54
+ repair.push(`${CLI_NAME} setup`);
55
+ }
56
+ }
57
+ }
58
+ // ── orphans: pieces whose partners are missing ──
59
+ const wired = state.blocks.length > 0 || state.hookFiles.length > 0;
60
+ if (state.mcp.some((one) => one.registered) && !wired) {
61
+ orphans.push('an MCP registration is present but no hooks wiring is — the guard and the retrieval contract travel together');
62
+ repair.push(`${CLI_NAME} setup`);
63
+ }
64
+ if (wired && !state.config.present) {
65
+ orphans.push('hooks are wired but there is no smelt.config.json — the store and budget the hooks promise live there');
66
+ repair.push(`${CLI_NAME} setup`);
67
+ }
68
+ // ── verdict ──
69
+ const installed = wired || state.config.present || state.mcp.some((one) => one.registered);
70
+ const current = installed && behindBlocks.length === 0 && orphans.length === 0;
71
+ say(`${CLI_NAME} doctor — binary ${io.version}, reading ${io.cwd}\n`);
72
+ if (!installed) {
73
+ say(`Nothing of smelt's is installed here. \`${CLI_NAME} setup\` would change that.\n`);
74
+ }
75
+ else {
76
+ if (config.present) {
77
+ say(` ${CONFIG_FILE_NAME}: ${config.malformed === true
78
+ ? 'MALFORMED'
79
+ : `schema ${String(config.schemaVersion)}, budget ${config.budgetBytes === undefined ? 'unset' : String(config.budgetBytes)}, store ${describeStore(config)}`}\n`);
80
+ }
81
+ else {
82
+ say(` ${CONFIG_FILE_NAME}: absent\n`);
83
+ }
84
+ for (const block of blocks) {
85
+ say(` ${block.file}: written by ${block.installedBy ?? 'a pre-stamping release (unversioned)'} [${block.status}] — ${block.harnesses.join(', ')}\n`);
86
+ }
87
+ for (const name of state.hookFiles)
88
+ say(` ${name}: wired\n`);
89
+ for (const one of state.mcp) {
90
+ if (one.registered)
91
+ say(` ${one.file}: ${one.server} registered\n`);
92
+ }
93
+ for (const orphan of orphans)
94
+ say(` ORPHAN: ${orphan}\n`);
95
+ if (behindBlocks.length > 0) {
96
+ say(`\nBehind: the running binary is ${io.version}; re-run setup to bring the ` +
97
+ `installed state to it:\n` +
98
+ [...new Set(repair)].map((command) => ` ${command}\n`).join(''));
99
+ }
100
+ else if (orphans.length > 0) {
101
+ say(`\nRepair:\n${[...new Set(repair)].map((command) => ` ${command}\n`).join('')}`);
102
+ }
103
+ say(current
104
+ ? `Current: everything on disk agrees with binary ${io.version}.\n`
105
+ : `Not current — see above. Doctor never writes; ${CLI_NAME} setup is the repair.\n`);
106
+ }
107
+ if (options.json) {
108
+ const receipt = {
109
+ format: 'smelt.doctor.v1',
110
+ version: io.version,
111
+ current,
112
+ installed,
113
+ config,
114
+ blocks,
115
+ hookFiles: [...state.hookFiles],
116
+ mcp: [...state.mcp],
117
+ orphans,
118
+ repair: [...new Set(repair)],
119
+ };
120
+ io.output(JSON.stringify(receipt, null, 2) + '\n');
121
+ }
122
+ return current || !installed ? EXIT.ok : EXIT.refused;
123
+ }
124
+ /** The verdict over one block: whole-owned files carry no stamp to compare. */
125
+ function blockStatus(block, binaryVersion) {
126
+ if (!block.stampable)
127
+ return 'unversioned';
128
+ return block.installedBy === binaryVersion ? 'current' : 'behind';
129
+ }
130
+ function describeStore(config) {
131
+ if (config.store.kind === undefined)
132
+ return 'unset';
133
+ if (config.store.kind === 'memory')
134
+ return 'memory';
135
+ return `directory at ${config.store.path ?? ''} (${config.store.dirExists ? 'present' : 'MISSING'})`;
136
+ }
137
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { CONFIG_FILE_NAME, CONFIG_VERSION } from './config.ts';\nimport { readInstalledState } from './installed.ts';\nimport type { InstalledBlock } from './installed.ts';\nimport { CLI_NAME, EXIT } from './shell.ts';\n\n/**\n * `smelt doctor` — the verdicts over InstalledState. The reading lives in\n * `cli/installed.ts` (one reader, behind the three consumers); this module decides\n * and reports: which blocks are behind the running binary, which pieces are orphans,\n * what the repair is, and what the exit code means. Everything the writers wrote,\n * this verb reads back and compares — and never writes a byte of (ADR-0003).\n *\n * When something is behind, the report ends with the exact repair command —\n * `smelt setup`, per harness where a block is behind — and nothing more happens.\n * The exit code carries the verdict, so the other-machine loop — upgrade, doctor,\n * setup — needs no prose parsing.\n */\n\nexport interface DoctorIo {\n readonly output: (text: string) => void;\n /** Where installed state is read: config discovery, instruction files, hook files. */\n readonly cwd: string;\n /** The running binary's version — what \"current\" is measured against. */\n readonly version: string;\n}\n\nexport interface DoctorOptions {\n readonly json: boolean;\n}\n\n/** One instruction block found on disk, with the release that wrote it. */\nexport interface DoctorBlock {\n readonly file: string;\n /** The harness profiles whose instruction file this is (often exactly one). */\n readonly harnesses: readonly string[];\n /** The release that wrote it, or `undefined` when it predates stamping. */\n readonly installedBy?: string;\n readonly status: 'current' | 'behind' | 'unversioned';\n}\n\n/** The config as doctor saw it. A malformed config is a finding, not a crash. */\nexport interface DoctorConfig {\n readonly present: boolean;\n readonly malformed?: boolean;\n readonly schemaVersion?: number;\n readonly currentSchema?: boolean;\n readonly budgetBytes?: number;\n readonly store: {\n readonly kind?: 'directory' | 'memory';\n readonly path?: string;\n readonly dirExists?: boolean;\n };\n}\n\n/** One MCP registration found (or notably absent) on disk. */\nexport interface DoctorMcp {\n readonly file: string;\n readonly server: string;\n readonly registered: boolean;\n}\n\n/** The machine receipt — `--json`. Everything doctor read, and the verdict. */\nexport interface DoctorReceipt {\n readonly format: 'smelt.doctor.v1';\n readonly version: string;\n /** True when something is installed and nothing is behind and there are no orphans. */\n readonly current: boolean;\n readonly installed: boolean;\n readonly config: DoctorConfig;\n readonly blocks: readonly DoctorBlock[];\n readonly hookFiles: readonly string[];\n readonly mcp: readonly DoctorMcp[];\n readonly orphans: readonly string[];\n readonly repair: readonly string[];\n}\n\nexport function runDoctor(options: DoctorOptions, io: DoctorIo): number {\n const say = (text: string): void => {\n if (!options.json) io.output(text);\n };\n const orphans: string[] = [];\n const repair: string[] = [];\n\n const state = readInstalledState(io.cwd);\n\n // ── verdict: blocks vs the running binary ──\n const blocks: DoctorBlock[] = state.blocks.map((block) => ({\n file: block.file,\n harnesses: block.harnesses,\n ...(block.installedBy === undefined ? {} : { installedBy: block.installedBy }),\n status: blockStatus(block, io.version),\n }));\n const behindBlocks = blocks.filter((block) => block.status === 'behind');\n for (const block of behindBlocks) {\n repair.push(...block.harnesses.map((id) => `${CLI_NAME} setup --harness ${id}`));\n }\n\n // ── config detail + the store-directory orphan ──\n let config: DoctorConfig = { present: false, store: {} };\n if (state.config.present) {\n if (state.config.malformed === true || state.config.parsed === undefined) {\n config = { present: true, malformed: true, store: {} };\n orphans.push(\n `${CONFIG_FILE_NAME} is malformed: ${state.config.malformedWhy ?? 'unparseable JSON'}`,\n );\n repair.push(`${CLI_NAME} setup`);\n } else {\n const parsed = state.config.parsed;\n const configPath = state.config.path ?? join(io.cwd, CONFIG_FILE_NAME);\n const dirExists =\n parsed.store?.kind === 'directory'\n ? existsSync(join(dirname(configPath), parsed.store.path))\n : undefined;\n config = {\n present: true,\n schemaVersion: parsed.smeltConfig,\n currentSchema: parsed.smeltConfig === CONFIG_VERSION,\n ...(parsed.defaultBudgetBytes === undefined\n ? {}\n : { budgetBytes: parsed.defaultBudgetBytes }),\n store: {\n ...(parsed.store === undefined ? {} : { kind: parsed.store.kind }),\n ...(parsed.store?.kind === 'directory' ? { path: parsed.store.path } : {}),\n ...(dirExists === undefined ? {} : { dirExists }),\n },\n };\n if (parsed.store?.kind === 'directory' && dirExists === false) {\n orphans.push(\n `the store directory (${parsed.store.path}) does not exist — retrieves across processes would fail`,\n );\n repair.push(`${CLI_NAME} setup`);\n }\n }\n }\n\n // ── orphans: pieces whose partners are missing ──\n const wired = state.blocks.length > 0 || state.hookFiles.length > 0;\n if (state.mcp.some((one) => one.registered) && !wired) {\n orphans.push(\n 'an MCP registration is present but no hooks wiring is — the guard and the retrieval contract travel together',\n );\n repair.push(`${CLI_NAME} setup`);\n }\n if (wired && !state.config.present) {\n orphans.push(\n 'hooks are wired but there is no smelt.config.json — the store and budget the hooks promise live there',\n );\n repair.push(`${CLI_NAME} setup`);\n }\n\n // ── verdict ──\n const installed = wired || state.config.present || state.mcp.some((one) => one.registered);\n const current = installed && behindBlocks.length === 0 && orphans.length === 0;\n\n say(`${CLI_NAME} doctor — binary ${io.version}, reading ${io.cwd}\\n`);\n if (!installed) {\n say(`Nothing of smelt's is installed here. \\`${CLI_NAME} setup\\` would change that.\\n`);\n } else {\n if (config.present) {\n say(\n ` ${CONFIG_FILE_NAME}: ${\n config.malformed === true\n ? 'MALFORMED'\n : `schema ${String(config.schemaVersion)}, budget ${\n config.budgetBytes === undefined ? 'unset' : String(config.budgetBytes)\n }, store ${describeStore(config)}`\n }\\n`,\n );\n } else {\n say(` ${CONFIG_FILE_NAME}: absent\\n`);\n }\n for (const block of blocks) {\n say(\n ` ${block.file}: written by ${\n block.installedBy ?? 'a pre-stamping release (unversioned)'\n } [${block.status}] — ${block.harnesses.join(', ')}\\n`,\n );\n }\n for (const name of state.hookFiles) say(` ${name}: wired\\n`);\n for (const one of state.mcp) {\n if (one.registered) say(` ${one.file}: ${one.server} registered\\n`);\n }\n for (const orphan of orphans) say(` ORPHAN: ${orphan}\\n`);\n if (behindBlocks.length > 0) {\n say(\n `\\nBehind: the running binary is ${io.version}; re-run setup to bring the ` +\n `installed state to it:\\n` +\n [...new Set(repair)].map((command) => ` ${command}\\n`).join(''),\n );\n } else if (orphans.length > 0) {\n say(`\\nRepair:\\n${[...new Set(repair)].map((command) => ` ${command}\\n`).join('')}`);\n }\n say(\n current\n ? `Current: everything on disk agrees with binary ${io.version}.\\n`\n : `Not current — see above. Doctor never writes; ${CLI_NAME} setup is the repair.\\n`,\n );\n }\n\n if (options.json) {\n const receipt: DoctorReceipt = {\n format: 'smelt.doctor.v1',\n version: io.version,\n current,\n installed,\n config,\n blocks,\n hookFiles: [...state.hookFiles],\n mcp: [...state.mcp],\n orphans,\n repair: [...new Set(repair)],\n };\n io.output(JSON.stringify(receipt, null, 2) + '\\n');\n }\n return current || !installed ? EXIT.ok : EXIT.refused;\n}\n\n/** The verdict over one block: whole-owned files carry no stamp to compare. */\nfunction blockStatus(block: InstalledBlock, binaryVersion: string): DoctorBlock['status'] {\n if (!block.stampable) return 'unversioned';\n return block.installedBy === binaryVersion ? 'current' : 'behind';\n}\n\nfunction describeStore(config: DoctorConfig): string {\n if (config.store.kind === undefined) return 'unset';\n if (config.store.kind === 'memory') return 'memory';\n return `directory at ${config.store.path ?? ''} (${\n config.store.dirExists ? 'present' : 'MISSING'\n })`;\n}\n"]}
@@ -14,20 +14,24 @@ import type { SmeltConfig, SmeltConfigHooks } from './config.ts';
14
14
  *
15
15
  * Every per-harness fact lives in that harness's {@link HarnessProfile}
16
16
  * (`src/harness/<id>.ts`), including what to write and how to take it back out. This
17
- * module owns only what is the *same* for every harness: the byte-faithful JSON merge,
18
- * the marker-block upsert, the wizard, and the two plans below — folds over
19
- * `profile.install`, with no case list of its own.
17
+ * module owns only what is the *same* for every harness: the hooks merge (which entries
18
+ * are ours, what a re-run replaces), the wizard, and the two plans below — folds over
19
+ * `profile.install`, with no case list of its own. The byte-faithful editing itself —
20
+ * one top-level JSON property, one delimited text block — is `src/text/json-edit.ts`,
21
+ * which knows nothing about harnesses.
20
22
  *
21
- * Harnesses come in three honesty tiers (docs/research/2026-09-02-harness-capability-matrix.md):
23
+ * Harnesses come in three honesty tiers (docs/research/2026-09-02-harness-capability-matrix.md),
24
+ * and which harness sits at which is `HarnessProfile.tier` — read through
25
+ * `harnessesByTier()`, never listed again here:
22
26
  *
23
- * - **verified** — Claude Code, Codex: schemas verified against primary docs and
24
- * exercised against recorded fixtures; first-class targets.
25
- * - **experimental** — Gemini, Grok, Hermes, Cursor, opencode, Cline: hook schemas
26
- * mapped from the capability matrix but not yet smoke-tested green against the
27
- * real binary. Labelled as such in code, docs, and this installer's output.
28
- * - **advisory** — KiloCode, Aider: no usable hook API, so what ships is
29
- * instructions (and, for KiloCode, a permissions/MCP sketch). Nothing enforces
30
- * them, and the output says so rather than implying a guard exists.
27
+ * - **verified** — schemas verified against primary docs and exercised against
28
+ * recorded fixtures; first-class targets.
29
+ * - **experimental** — hook schemas mapped from the capability matrix but not yet
30
+ * smoke-tested green against the real binary. Labelled as such in code, docs, and
31
+ * this installer's output.
32
+ * - **advisory** — no usable hook API, so what ships is instructions (and, for
33
+ * KiloCode, a permissions/MCP sketch). Nothing enforces them, and the output says
34
+ * so rather than implying a guard exists.
31
35
  *
32
36
  * The wizard discipline is `smelt init`'s, verbatim: every step accepts `back`,
33
37
  * nothing is written until a final confirm that lists every file, and an existing
@@ -47,10 +51,16 @@ export interface HooksIo {
47
51
  readonly cwd: string;
48
52
  /** Home directory for detection only. Tests point it at a temp dir; nothing writes here. */
49
53
  readonly home?: string;
54
+ /**
55
+ * The release running the install — stamped into the instruction block so
56
+ * `smelt doctor` can tell what wrote it. Absent (legacy callers) writes no stamp.
57
+ */
58
+ readonly version?: string;
50
59
  }
51
60
  export { instructionSnippet, SNIPPET_END_MD, SNIPPET_START_MD };
52
61
  /** A harness whose config directory exists in the project or the home directory. */
53
62
  export declare function detectedHarnesses(cwd: string, home: string): readonly HarnessProfile[];
63
+ export declare const AGENTS_LINT_ARGS = "agents lint .";
54
64
  /**
55
65
  * Merge our hook entries into a JSON settings file, preserving everything foreign
56
66
  * **byte-faithfully**: the merged `hooks` value is spliced into the original text, so
@@ -67,10 +77,6 @@ export declare function detectedHarnesses(cwd: string, home: string): readonly H
67
77
  export declare function mergeJsonHooks(existingText: string | undefined, events: Record<string, readonly unknown[]>, shape?: {
68
78
  readonly version?: number;
69
79
  }): string | undefined;
70
- /** Replace this installer's marker block in `existingText`, or append it. */
71
- export declare function upsertMarkerBlock(existingText: string | undefined, block: string, start: string, end: string): string;
72
- /** Remove the marker block. `undefined` when nothing (or only whitespace) remains. */
73
- export declare function stripMarkerBlock(existingText: string, start: string, end: string): string | undefined;
74
80
  interface PlannedFile {
75
81
  /** Display path, relative to the project. */
76
82
  readonly name: string;
@@ -94,9 +100,12 @@ interface PlannedRemoval {
94
100
  }
95
101
  export interface HooksChoices {
96
102
  harnesses: HarnessProfile[];
103
+ /** The release writing these bytes — stamped into the snippet for `smelt doctor`. */
104
+ writtenBy?: string;
97
105
  guard: boolean;
98
106
  statsOnStop: boolean;
99
107
  mapOnStart: boolean;
108
+ lintOnStart: boolean;
100
109
  enforcement: EnforcementMode;
101
110
  thresholdBytes: number;
102
111
  }
@@ -117,7 +126,7 @@ interface InstallPlan {
117
126
  */
118
127
  export declare function planInstall(cwd: string, choices: HooksChoices): InstallPlan;
119
128
  /** Where the installed config points the persistent store, relative to the config file. */
120
- export declare const DEFAULT_STORE_DIR = ".smelt/store";
129
+ export declare const DEFAULT_STORE_DIR: ".smelt/store";
121
130
  /**
122
131
  * Existing config re-rendered with the hooks block, other fields carried verbatim —
123
132
  * except that a config with **no** store block gains a directory store. The deny
@@ -131,6 +140,13 @@ export declare const DEFAULT_STORE_DIR = ".smelt/store";
131
140
  * That store injection is this verb's **policy**, which is why it lives here; the
132
141
  * bytes are written by `renderConfig` in `config.ts`, the one writer, so a key added
133
142
  * to the schema reaches this file and `init`'s together or not at all.
143
+ *
144
+ * "Carried verbatim" is spelled as a spread rather than as a list of the fields to
145
+ * copy, and that is load-bearing: the list version silently dropped every key nobody
146
+ * remembered to add to it — `agents` was added to the schema and this function kept
147
+ * writing configs without it, which is a setting the user believed was in force,
148
+ * caught by `test/guards/config-writer.test.ts`. Only the two fields this verb
149
+ * actually decides are named.
134
150
  */
135
151
  export declare function renderConfigWithHooks(existing: SmeltConfig | undefined, hooks: SmeltConfigHooks): string;
136
152
  /**
@@ -142,7 +158,34 @@ export declare function renderConfigWithHooks(existing: SmeltConfig | undefined,
142
158
  export declare function planRemove(cwd: string, harnesses: readonly HarnessProfile[]): readonly PlannedRemoval[];
143
159
  /**
144
160
  * `smelt hooks <install|remove>`, start to finish. The same testability pattern as
145
- * `runInit`: a pure function over an input/output pair, exit code returned.
161
+ * `runInit`: a pure function over an input/output pair, exit code returned. The ask
162
+ * adapter, the step machine and the confirms are the wizard kit's (`cli/wizard.ts`) —
163
+ * this file holds what is hooks' own: the steps, the plan, the per-file consent.
146
164
  */
147
165
  export declare function runHooks(action: 'install' | 'remove', harnessFlag: string | undefined, io: HooksIo): Promise<number>;
166
+ /**
167
+ * A re-run reads the toggles back off what is actually installed — every JSON hook
168
+ * file this installer writes, plus the guard-only shim files, both derived from the
169
+ * registry — so it edits instead of resetting. Harnesses that only wire the guard
170
+ * (gemini, grok, cursor, hermes, opencode, cline) persist no stats/map entries, so
171
+ * after a re-run scoped to them those toggles read back as off; the defaults below
172
+ * apply only when nothing of smelt's is installed at all.
173
+ *
174
+ * The two `SessionStart` toggles share one event, so they are told apart by **the
175
+ * command each entry runs**, not by the key it sits under. Reading `SessionStart` as
176
+ * one boolean would make a re-run with the map on and the lint off write both back —
177
+ * or neither — which is a toggle the user believed they had set.
178
+ *
179
+ * Exported for `smelt setup`, which applies the preset's *current* state the same way
180
+ * — read off what is installed — rather than keeping a second copy of the defaults.
181
+ */
182
+ /**
183
+ * Whether a JSON hook file's text carries entries of ours — the **one** predicate for
184
+ * this fact, shared by the readers (`smelt doctor`, `smelt setup`'s repair policy) and
185
+ * backed by the same `isOursEntry` the writer's strip-merge uses. The guard command
186
+ * itself carries only the shim path (no token), so a text-level `OURS_TOKEN` search
187
+ * would miss a guard-only install — the exact drift this exists to prevent.
188
+ */
189
+ export declare function jsonHooksContainOurs(text: string): boolean;
190
+ export declare function presetToggles(cwd: string): Pick<HooksChoices, 'guard' | 'statsOnStop' | 'mapOnStart' | 'lintOnStart'>;
148
191
  //# sourceMappingURL=hooks.d.ts.map