@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
@@ -0,0 +1,87 @@
1
+ import process from 'node:process';
2
+ import { CliUsageError } from '../../errors.js';
3
+ import { HARNESSES, harnessById } from '../../harness/registry.js';
4
+ import { SETUP_RECIPE } from '../../setup/recipe.js';
5
+ import { runSetup } from '../setup.js';
6
+ import { colorize } from '../lava.js';
7
+ import { CLI_NAME } from '../shell.js';
8
+ export const setupCommand = {
9
+ name: 'setup',
10
+ flags: ['harness', 'yes', 'no-mcp', 'json'],
11
+ refusal: `setup applies the recipe; answer it with --yes (and --harness, --no-mcp, --json) or let it ask.`,
12
+ usage: {
13
+ synopsis: [],
14
+ occasional: ['setup [--harness <id>]... [--yes] [--no-mcp] [--json]'],
15
+ section: {
16
+ heading: 'SETUP',
17
+ body: ` ${CLI_NAME} setup applies the whole recipe in one command: smelt.config.json, the\n` +
18
+ ` hooks preset for the harnesses you name, the MCP registration step, and a real\n` +
19
+ ` smelt → retrieve round trip to prove the loop. Interactive from a terminal; for\n` +
20
+ ` an agent, answer everything up front:\n\n` +
21
+ ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]\n\n` +
22
+ ` The defaults are the recipe's: budget ${SETUP_RECIPE.recommendedBudgetBytes} bytes\n` +
23
+ ` (written only when the config carries none), a directory store at\n` +
24
+ ` ${SETUP_RECIPE.store.defaultDir} (only when the config carries none). Existing\n` +
25
+ ` files are never overwritten — they are skipped with a note; hooks install edits\n` +
26
+ ` them, and it asks per file. Re-running on a current machine writes nothing and\n` +
27
+ ` exits 0. --json prints a receipt: every file, every check, the exit's meaning.`,
28
+ },
29
+ },
30
+ parse(values, positionals) {
31
+ if (positionals.length > 1) {
32
+ throw new CliUsageError(`${CLI_NAME}: setup takes no further arguments, got ` +
33
+ `${positionals.slice(1).join(', ')}.`);
34
+ }
35
+ const harnessIds = (values.harness ?? []).map((id) => {
36
+ if (harnessById(id) === undefined) {
37
+ throw new CliUsageError(`${CLI_NAME} setup: unknown harness "${id}". ` +
38
+ `Known: ${HARNESSES.map((harness) => harness.id).join(', ')}.`);
39
+ }
40
+ return id;
41
+ });
42
+ const yes = values.yes === true;
43
+ const json = values.json === true;
44
+ if (json && !yes) {
45
+ throw new CliUsageError(`${CLI_NAME}: --json prints a machine receipt — pair it with --yes. ` +
46
+ `The interactive flow's output is for humans.`);
47
+ }
48
+ return {
49
+ mode: 'setup',
50
+ harnessIds,
51
+ yes,
52
+ noMcp: values['no-mcp'] === true,
53
+ json,
54
+ };
55
+ },
56
+ /**
57
+ * Nothing to merge: the flow reads `smelt.config.json` itself. A *missing* config
58
+ * is the thing setup writes; a *malformed* one is a loud refusal — doctor names it,
59
+ * the user fixes it, and setup never guesses around bytes it cannot parse.
60
+ */
61
+ resolve(invocation) {
62
+ return invocation;
63
+ },
64
+ /**
65
+ * Interactive unless `--yes` answered everything, so it needs the wizard stream —
66
+ * and the refusal below is the agent-facing interface documentation.
67
+ */
68
+ async run(resolved, io) {
69
+ if (!resolved.yes && io.initInput === undefined) {
70
+ throw new CliUsageError(`${CLI_NAME}: setup is interactive unless you answer it up front. ` +
71
+ `Non-interactive:\n` +
72
+ ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]`);
73
+ }
74
+ return await runSetup(resolved, {
75
+ // `input` stays absent for `--yes` — exactOptionalPropertyTypes means "absent"
76
+ // is a decision, not a field carrying undefined.
77
+ ...(io.initInput === undefined ? {} : { input: io.initInput }),
78
+ output: (text) => io.stdout(colorize(text, io.color === true && !resolved.yes && !resolved.json)),
79
+ cwd: io.cwd ?? process.cwd(),
80
+ version: io.version,
81
+ // The lava renderer is for the human at a terminal: --yes and --json are the
82
+ // machine paths, and their bytes stay plain however pretty the screen is.
83
+ ...(io.color === true && !resolved.yes && !resolved.json ? { color: true } : {}),
84
+ });
85
+ },
86
+ };
87
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/cli/subcommands/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESSES, harnessById } from '../../harness/registry.ts';\nimport { SETUP_RECIPE } from '../../setup/recipe.ts';\nimport { runSetup } from '../setup.ts';\nimport { colorize } from '../lava.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt setup` — the one-command front door for the whole recipe. The flow itself is\n * `cli/setup.ts`, a pure function over an injected input/output pair; this file is\n * only the verb.\n *\n * Interactive from a terminal, like `init` and `hooks`; for an agent, the flags are\n * the whole interface — `--yes` answers everything from the recipe, `--harness`\n * (repeatable) picks the harnesses, `--no-mcp` skips the registration step, and\n * `--json` (with `--yes`) turns the run into a machine-readable receipt. The refusal\n * below is how an agent with no TTY learns that interface, which is the same trick\n * `init`'s refusal uses.\n */\n\n/** `smelt setup [--harness <id>]... [--yes] [--no-mcp] [--json]` — parsed. */\nexport interface SetupInvocation {\n readonly mode: 'setup';\n readonly harnessIds: readonly string[];\n readonly yes: boolean;\n readonly noMcp: boolean;\n readonly json: boolean;\n}\n\nexport const setupCommand: Subcommand<SetupInvocation, SetupInvocation> = {\n name: 'setup',\n flags: ['harness', 'yes', 'no-mcp', 'json'],\n refusal: `setup applies the recipe; answer it with --yes (and --harness, --no-mcp, --json) or let it ask.`,\n usage: {\n synopsis: [],\n occasional: ['setup [--harness <id>]... [--yes] [--no-mcp] [--json]'],\n section: {\n heading: 'SETUP',\n body:\n ` ${CLI_NAME} setup applies the whole recipe in one command: smelt.config.json, the\\n` +\n ` hooks preset for the harnesses you name, the MCP registration step, and a real\\n` +\n ` smelt → retrieve round trip to prove the loop. Interactive from a terminal; for\\n` +\n ` an agent, answer everything up front:\\n\\n` +\n ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]\\n\\n` +\n ` The defaults are the recipe's: budget ${SETUP_RECIPE.recommendedBudgetBytes} bytes\\n` +\n ` (written only when the config carries none), a directory store at\\n` +\n ` ${SETUP_RECIPE.store.defaultDir} (only when the config carries none). Existing\\n` +\n ` files are never overwritten — they are skipped with a note; hooks install edits\\n` +\n ` them, and it asks per file. Re-running on a current machine writes nothing and\\n` +\n ` exits 0. --json prints a receipt: every file, every check, the exit's meaning.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SetupInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: setup takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}.`,\n );\n }\n const harnessIds = (values.harness ?? []).map((id) => {\n if (harnessById(id) === undefined) {\n throw new CliUsageError(\n `${CLI_NAME} setup: unknown harness \"${id}\". ` +\n `Known: ${HARNESSES.map((harness) => harness.id).join(', ')}.`,\n );\n }\n return id;\n });\n const yes = values.yes === true;\n const json = values.json === true;\n if (json && !yes) {\n throw new CliUsageError(\n `${CLI_NAME}: --json prints a machine receipt — pair it with --yes. ` +\n `The interactive flow's output is for humans.`,\n );\n }\n return {\n mode: 'setup',\n harnessIds,\n yes,\n noMcp: values['no-mcp'] === true,\n json,\n };\n },\n\n /**\n * Nothing to merge: the flow reads `smelt.config.json` itself. A *missing* config\n * is the thing setup writes; a *malformed* one is a loud refusal — doctor names it,\n * the user fixes it, and setup never guesses around bytes it cannot parse.\n */\n resolve(invocation: SetupInvocation): SetupInvocation {\n return invocation;\n },\n\n /**\n * Interactive unless `--yes` answered everything, so it needs the wizard stream —\n * and the refusal below is the agent-facing interface documentation.\n */\n async run(resolved: SetupInvocation, io: CliIo): Promise<number> {\n if (!resolved.yes && io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: setup is interactive unless you answer it up front. ` +\n `Non-interactive:\\n` +\n ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]`,\n );\n }\n return await runSetup(resolved, {\n // `input` stays absent for `--yes` — exactOptionalPropertyTypes means \"absent\"\n // is a decision, not a field carrying undefined.\n ...(io.initInput === undefined ? {} : { input: io.initInput }),\n output: (text) =>\n io.stdout(colorize(text, io.color === true && !resolved.yes && !resolved.json)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n // The lava renderer is for the human at a terminal: --yes and --json are the\n // machine paths, and their bytes stay plain however pretty the screen is.\n ...(io.color === true && !resolved.yes && !resolved.json ? { color: true } : {}),\n });\n },\n};\n"]}
@@ -9,7 +9,7 @@ import type { FlagValues, VerbFlag } from './flags.ts';
9
9
  * Order is meaningful: it is the order `--help` renders every derived list in, and the
10
10
  * order a refusal names owners in. Keep it stable and append new verbs at the end.
11
11
  */
12
- export type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks';
12
+ export type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';
13
13
  /** The verb a bare invocation selects — a file or stdin, with no subcommand word. */
14
14
  export declare const DEFAULT_VERB = "smelt";
15
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;AAE7E,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAyB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,YAAY,GAAG,SAAS,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7D;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,CAAC;IAChD,wEAAwE;IACxE,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
1
+ {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GACd,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5F,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAyB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,YAAY,GAAG,SAAS,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7D;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,CAAC;IAChD,wEAAwE;IACxE,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb =\n 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"subcommand.js","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAeA,qFAAqF;AACrF,MAAM,CAAC,MAAM,YAAY,GAAG,OAAsB,CAAC","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
1
+ {"version":3,"file":"subcommand.js","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAgBA,qFAAqF;AACrF,MAAM,CAAC,MAAM,YAAY,GAAG,OAAsB,CAAC","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb =\n 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
@@ -0,0 +1,72 @@
1
+ import type { AnswerStream } from './shell.ts';
2
+ /**
3
+ * The wizard kit — the stream machinery every interactive verb shares, extracted
4
+ * once because its third copy was the one that raced: `setup` re-typed the ask
5
+ * adapter without awaiting a step, and two prompts were on screen at once. A kit is
6
+ * not an abstraction here; it is the deletion of two copies of a thing that had
7
+ * already drifted (review II, KOT-255).
8
+ *
9
+ * What lives here, and nothing else:
10
+ *
11
+ * - {@link wizardAsk}: the ask adapter — `answerReader`, the prompt echo, the trim,
12
+ * and the EOF refusal, whose message is each verb's own (init, hooks and setup
13
+ * owe the reader different last sentences).
14
+ * - {@link walkSteps}: the step machine with real back-navigation — the loop
15
+ * `hooks` always had, `init` mirrored, and `setup` faked with "lands on the last
16
+ * question". A step is `(ask) => 'ok' | 'back'`; back at the first step is
17
+ * answered, not ignored.
18
+ * - {@link confirmLoop} / {@link confirmYesNo}: the confirm prompts, retry copy
19
+ * included, because two verbs spelling "yes to…" differently is drift.
20
+ * - {@link listPlannedFiles} / {@link writePlannedFile}: the plan listing and the
21
+ * one file-write mechanic (mkdir, write, chmod) every apply loop performs.
22
+ *
23
+ * Pure IO plumbing: no verb knowledge, no domain facts, no rendering opinions — the
24
+ * lava adapter stays outside, at the verb boundary, where one switch styles them all.
25
+ */
26
+ /** One question at a time, each answered by a line. What every wizard's `ask` is. */
27
+ export type Ask = (prompt: string) => Promise<string>;
28
+ /** One wizard step: `'ok'` advances, `'back'` returns to the previous step. */
29
+ export type Step = (ask: Ask) => Promise<'ok' | 'back'>;
30
+ /**
31
+ * The ask adapter over an injected stream. `eofMessage` is the refusal's tail — the
32
+ * one sentence that differs per verb, and the only one allowed to: setup's teaches
33
+ * the non-interactive flags, hooks' says what happens to already-confirmed writes,
34
+ * init's states the plain fact.
35
+ */
36
+ export declare function wizardAsk(input: AnswerStream, output: (text: string) => void, eofMessage: string): {
37
+ ask: Ask;
38
+ release: () => Promise<void>;
39
+ };
40
+ /**
41
+ * The step machine: steps in order, `back` moving one step back. The first step's
42
+ * back is answered where the user can read it — there is nothing before it, and
43
+ * pretending otherwise is how a wizard eats an answer. `startAt` is where a confirm's
44
+ * `back` lands: the last step, not the first.
45
+ */
46
+ export declare function walkSteps(steps: readonly Step[], ask: Ask, say: (text: string) => void, startAt?: number): Promise<void>;
47
+ /**
48
+ * `confirm (yes / no / back)> ` — the three-way confirm. `retryCopy` completes the
49
+ * "yes to …, no to …, back to …" sentence, which is verb knowledge.
50
+ */
51
+ export declare function confirmLoop(ask: Ask, retryCopy: string): Promise<'yes' | 'no' | 'back'>;
52
+ /** `confirm (yes / no)> ` — the two-way confirm, for flows with no step to return to. */
53
+ export declare function confirmYesNo(ask: Ask, retryCopy: string): Promise<'yes' | 'no'>;
54
+ /** One planned file, as the listing and the write mechanic both see it. */
55
+ export interface PlannedFileLike {
56
+ readonly name: string;
57
+ readonly path: string;
58
+ readonly content: string;
59
+ readonly exists: boolean;
60
+ readonly unchanged: boolean;
61
+ readonly mode?: number;
62
+ }
63
+ /** One skipped file, with the reason the plan refused it. */
64
+ export interface PlannedSkipLike {
65
+ readonly name: string;
66
+ readonly why: string;
67
+ }
68
+ /** The ` name…padEnd(32) (fate)` listing every confirm prints. */
69
+ export declare function listPlannedFiles(say: (text: string) => void, files: readonly PlannedFileLike[], skipped: readonly PlannedSkipLike[], fate: (file: PlannedFileLike) => string): void;
70
+ /** The one write mechanic: mkdir, write, chmod — in that order, everywhere. */
71
+ export declare function writePlannedFile(file: PlannedFileLike): void;
72
+ //# sourceMappingURL=wizard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wizard.d.ts","sourceRoot":"","sources":["../../src/cli/wizard.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,qFAAqF;AACrF,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEtD,+EAA+E;AAC/E,MAAM,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;AAExD;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC9B,UAAU,EAAE,MAAM,GACjB;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAS5C;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC3B,OAAO,SAAI,GACV,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,GAAG,MAAM,CAAC,CAQ7F;AAED,yFAAyF;AACzF,wBAAsB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAOrF;AAED,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,mEAAmE;AACnE,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC3B,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,IAAI,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,MAAM,GACtC,IAAI,CAON;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAI5D","sourcesContent":["import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport { CliUsageError } from '../errors.ts';\nimport { answerReader } from './shell.ts';\nimport type { AnswerStream } from './shell.ts';\n\n/**\n * The wizard kit — the stream machinery every interactive verb shares, extracted\n * once because its third copy was the one that raced: `setup` re-typed the ask\n * adapter without awaiting a step, and two prompts were on screen at once. A kit is\n * not an abstraction here; it is the deletion of two copies of a thing that had\n * already drifted (review II, KOT-255).\n *\n * What lives here, and nothing else:\n *\n * - {@link wizardAsk}: the ask adapter — `answerReader`, the prompt echo, the trim,\n * and the EOF refusal, whose message is each verb's own (init, hooks and setup\n * owe the reader different last sentences).\n * - {@link walkSteps}: the step machine with real back-navigation — the loop\n * `hooks` always had, `init` mirrored, and `setup` faked with \"lands on the last\n * question\". A step is `(ask) => 'ok' | 'back'`; back at the first step is\n * answered, not ignored.\n * - {@link confirmLoop} / {@link confirmYesNo}: the confirm prompts, retry copy\n * included, because two verbs spelling \"yes to…\" differently is drift.\n * - {@link listPlannedFiles} / {@link writePlannedFile}: the plan listing and the\n * one file-write mechanic (mkdir, write, chmod) every apply loop performs.\n *\n * Pure IO plumbing: no verb knowledge, no domain facts, no rendering opinions — the\n * lava adapter stays outside, at the verb boundary, where one switch styles them all.\n */\n\n/** One question at a time, each answered by a line. What every wizard's `ask` is. */\nexport type Ask = (prompt: string) => Promise<string>;\n\n/** One wizard step: `'ok'` advances, `'back'` returns to the previous step. */\nexport type Step = (ask: Ask) => Promise<'ok' | 'back'>;\n\n/**\n * The ask adapter over an injected stream. `eofMessage` is the refusal's tail — the\n * one sentence that differs per verb, and the only one allowed to: setup's teaches\n * the non-interactive flags, hooks' says what happens to already-confirmed writes,\n * init's states the plain fact.\n */\nexport function wizardAsk(\n input: AnswerStream,\n output: (text: string) => void,\n eofMessage: string,\n): { ask: Ask; release: () => Promise<void> } {\n const lines = answerReader(input);\n const ask: Ask = async (prompt) => {\n output(prompt);\n const next = await lines.next();\n if (next === undefined) throw new CliUsageError(eofMessage);\n return next.trim();\n };\n return { ask, release: () => lines.release() };\n}\n\n/**\n * The step machine: steps in order, `back` moving one step back. The first step's\n * back is answered where the user can read it — there is nothing before it, and\n * pretending otherwise is how a wizard eats an answer. `startAt` is where a confirm's\n * `back` lands: the last step, not the first.\n */\nexport async function walkSteps(\n steps: readonly Step[],\n ask: Ask,\n say: (text: string) => void,\n startAt = 0,\n): Promise<void> {\n let index = startAt;\n while (index < steps.length) {\n const outcome = await steps[index]!(ask);\n if (outcome === 'back') {\n if (index === 0) say(`This is the first step — there is nothing before it.\\n`);\n else index -= 1;\n } else {\n index += 1;\n }\n }\n}\n\n/**\n * `confirm (yes / no / back)> ` — the three-way confirm. `retryCopy` completes the\n * \"yes to …, no to …, back to …\" sentence, which is verb knowledge.\n */\nexport async function confirmLoop(ask: Ask, retryCopy: string): Promise<'yes' | 'no' | 'back'> {\n for (;;) {\n const answer = await ask(`confirm (yes / no / back)> `);\n if (answer === 'back') return 'back';\n if (answer === 'no') return 'no';\n if (answer === 'yes') return 'yes';\n await ask(`${retryCopy}\\n`);\n }\n}\n\n/** `confirm (yes / no)> ` — the two-way confirm, for flows with no step to return to. */\nexport async function confirmYesNo(ask: Ask, retryCopy: string): Promise<'yes' | 'no'> {\n for (;;) {\n const answer = await ask(`confirm (yes / no)> `);\n if (answer === 'no') return 'no';\n if (answer === 'yes') return 'yes';\n await ask(`${retryCopy}\\n`);\n }\n}\n\n/** One planned file, as the listing and the write mechanic both see it. */\nexport interface PlannedFileLike {\n readonly name: string;\n readonly path: string;\n readonly content: string;\n readonly exists: boolean;\n readonly unchanged: boolean;\n readonly mode?: number;\n}\n\n/** One skipped file, with the reason the plan refused it. */\nexport interface PlannedSkipLike {\n readonly name: string;\n readonly why: string;\n}\n\n/** The ` name…padEnd(32) (fate)` listing every confirm prints. */\nexport function listPlannedFiles(\n say: (text: string) => void,\n files: readonly PlannedFileLike[],\n skipped: readonly PlannedSkipLike[],\n fate: (file: PlannedFileLike) => string,\n): void {\n for (const file of files) {\n say(` ${file.name.padEnd(32)} (${fate(file)})\\n`);\n }\n for (const skip of skipped) {\n say(` ${skip.name.padEnd(32)} (SKIPPED: ${skip.why})\\n`);\n }\n}\n\n/** The one write mechanic: mkdir, write, chmod — in that order, everywhere. */\nexport function writePlannedFile(file: PlannedFileLike): void {\n mkdirSync(dirname(file.path), { recursive: true });\n writeFileSync(file.path, file.content);\n if (file.mode !== undefined) chmodSync(file.path, file.mode);\n}\n"]}
@@ -0,0 +1,86 @@
1
+ import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { CliUsageError } from '../errors.js';
4
+ import { answerReader } from './shell.js';
5
+ /**
6
+ * The ask adapter over an injected stream. `eofMessage` is the refusal's tail — the
7
+ * one sentence that differs per verb, and the only one allowed to: setup's teaches
8
+ * the non-interactive flags, hooks' says what happens to already-confirmed writes,
9
+ * init's states the plain fact.
10
+ */
11
+ export function wizardAsk(input, output, eofMessage) {
12
+ const lines = answerReader(input);
13
+ const ask = async (prompt) => {
14
+ output(prompt);
15
+ const next = await lines.next();
16
+ if (next === undefined)
17
+ throw new CliUsageError(eofMessage);
18
+ return next.trim();
19
+ };
20
+ return { ask, release: () => lines.release() };
21
+ }
22
+ /**
23
+ * The step machine: steps in order, `back` moving one step back. The first step's
24
+ * back is answered where the user can read it — there is nothing before it, and
25
+ * pretending otherwise is how a wizard eats an answer. `startAt` is where a confirm's
26
+ * `back` lands: the last step, not the first.
27
+ */
28
+ export async function walkSteps(steps, ask, say, startAt = 0) {
29
+ let index = startAt;
30
+ while (index < steps.length) {
31
+ const outcome = await steps[index](ask);
32
+ if (outcome === 'back') {
33
+ if (index === 0)
34
+ say(`This is the first step — there is nothing before it.\n`);
35
+ else
36
+ index -= 1;
37
+ }
38
+ else {
39
+ index += 1;
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * `confirm (yes / no / back)> ` — the three-way confirm. `retryCopy` completes the
45
+ * "yes to …, no to …, back to …" sentence, which is verb knowledge.
46
+ */
47
+ export async function confirmLoop(ask, retryCopy) {
48
+ for (;;) {
49
+ const answer = await ask(`confirm (yes / no / back)> `);
50
+ if (answer === 'back')
51
+ return 'back';
52
+ if (answer === 'no')
53
+ return 'no';
54
+ if (answer === 'yes')
55
+ return 'yes';
56
+ await ask(`${retryCopy}\n`);
57
+ }
58
+ }
59
+ /** `confirm (yes / no)> ` — the two-way confirm, for flows with no step to return to. */
60
+ export async function confirmYesNo(ask, retryCopy) {
61
+ for (;;) {
62
+ const answer = await ask(`confirm (yes / no)> `);
63
+ if (answer === 'no')
64
+ return 'no';
65
+ if (answer === 'yes')
66
+ return 'yes';
67
+ await ask(`${retryCopy}\n`);
68
+ }
69
+ }
70
+ /** The ` name…padEnd(32) (fate)` listing every confirm prints. */
71
+ export function listPlannedFiles(say, files, skipped, fate) {
72
+ for (const file of files) {
73
+ say(` ${file.name.padEnd(32)} (${fate(file)})\n`);
74
+ }
75
+ for (const skip of skipped) {
76
+ say(` ${skip.name.padEnd(32)} (SKIPPED: ${skip.why})\n`);
77
+ }
78
+ }
79
+ /** The one write mechanic: mkdir, write, chmod — in that order, everywhere. */
80
+ export function writePlannedFile(file) {
81
+ mkdirSync(dirname(file.path), { recursive: true });
82
+ writeFileSync(file.path, file.content);
83
+ if (file.mode !== undefined)
84
+ chmodSync(file.path, file.mode);
85
+ }
86
+ //# sourceMappingURL=wizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wizard.js","sourceRoot":"","sources":["../../src/cli/wizard.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport { CliUsageError } from '../errors.ts';\nimport { answerReader } from './shell.ts';\nimport type { AnswerStream } from './shell.ts';\n\n/**\n * The wizard kit — the stream machinery every interactive verb shares, extracted\n * once because its third copy was the one that raced: `setup` re-typed the ask\n * adapter without awaiting a step, and two prompts were on screen at once. A kit is\n * not an abstraction here; it is the deletion of two copies of a thing that had\n * already drifted (review II, KOT-255).\n *\n * What lives here, and nothing else:\n *\n * - {@link wizardAsk}: the ask adapter — `answerReader`, the prompt echo, the trim,\n * and the EOF refusal, whose message is each verb's own (init, hooks and setup\n * owe the reader different last sentences).\n * - {@link walkSteps}: the step machine with real back-navigation — the loop\n * `hooks` always had, `init` mirrored, and `setup` faked with \"lands on the last\n * question\". A step is `(ask) => 'ok' | 'back'`; back at the first step is\n * answered, not ignored.\n * - {@link confirmLoop} / {@link confirmYesNo}: the confirm prompts, retry copy\n * included, because two verbs spelling \"yes to…\" differently is drift.\n * - {@link listPlannedFiles} / {@link writePlannedFile}: the plan listing and the\n * one file-write mechanic (mkdir, write, chmod) every apply loop performs.\n *\n * Pure IO plumbing: no verb knowledge, no domain facts, no rendering opinions — the\n * lava adapter stays outside, at the verb boundary, where one switch styles them all.\n */\n\n/** One question at a time, each answered by a line. What every wizard's `ask` is. */\nexport type Ask = (prompt: string) => Promise<string>;\n\n/** One wizard step: `'ok'` advances, `'back'` returns to the previous step. */\nexport type Step = (ask: Ask) => Promise<'ok' | 'back'>;\n\n/**\n * The ask adapter over an injected stream. `eofMessage` is the refusal's tail — the\n * one sentence that differs per verb, and the only one allowed to: setup's teaches\n * the non-interactive flags, hooks' says what happens to already-confirmed writes,\n * init's states the plain fact.\n */\nexport function wizardAsk(\n input: AnswerStream,\n output: (text: string) => void,\n eofMessage: string,\n): { ask: Ask; release: () => Promise<void> } {\n const lines = answerReader(input);\n const ask: Ask = async (prompt) => {\n output(prompt);\n const next = await lines.next();\n if (next === undefined) throw new CliUsageError(eofMessage);\n return next.trim();\n };\n return { ask, release: () => lines.release() };\n}\n\n/**\n * The step machine: steps in order, `back` moving one step back. The first step's\n * back is answered where the user can read it — there is nothing before it, and\n * pretending otherwise is how a wizard eats an answer. `startAt` is where a confirm's\n * `back` lands: the last step, not the first.\n */\nexport async function walkSteps(\n steps: readonly Step[],\n ask: Ask,\n say: (text: string) => void,\n startAt = 0,\n): Promise<void> {\n let index = startAt;\n while (index < steps.length) {\n const outcome = await steps[index]!(ask);\n if (outcome === 'back') {\n if (index === 0) say(`This is the first step — there is nothing before it.\\n`);\n else index -= 1;\n } else {\n index += 1;\n }\n }\n}\n\n/**\n * `confirm (yes / no / back)> ` — the three-way confirm. `retryCopy` completes the\n * \"yes to …, no to …, back to …\" sentence, which is verb knowledge.\n */\nexport async function confirmLoop(ask: Ask, retryCopy: string): Promise<'yes' | 'no' | 'back'> {\n for (;;) {\n const answer = await ask(`confirm (yes / no / back)> `);\n if (answer === 'back') return 'back';\n if (answer === 'no') return 'no';\n if (answer === 'yes') return 'yes';\n await ask(`${retryCopy}\\n`);\n }\n}\n\n/** `confirm (yes / no)> ` — the two-way confirm, for flows with no step to return to. */\nexport async function confirmYesNo(ask: Ask, retryCopy: string): Promise<'yes' | 'no'> {\n for (;;) {\n const answer = await ask(`confirm (yes / no)> `);\n if (answer === 'no') return 'no';\n if (answer === 'yes') return 'yes';\n await ask(`${retryCopy}\\n`);\n }\n}\n\n/** One planned file, as the listing and the write mechanic both see it. */\nexport interface PlannedFileLike {\n readonly name: string;\n readonly path: string;\n readonly content: string;\n readonly exists: boolean;\n readonly unchanged: boolean;\n readonly mode?: number;\n}\n\n/** One skipped file, with the reason the plan refused it. */\nexport interface PlannedSkipLike {\n readonly name: string;\n readonly why: string;\n}\n\n/** The ` name…padEnd(32) (fate)` listing every confirm prints. */\nexport function listPlannedFiles(\n say: (text: string) => void,\n files: readonly PlannedFileLike[],\n skipped: readonly PlannedSkipLike[],\n fate: (file: PlannedFileLike) => string,\n): void {\n for (const file of files) {\n say(` ${file.name.padEnd(32)} (${fate(file)})\\n`);\n }\n for (const skip of skipped) {\n say(` ${skip.name.padEnd(32)} (SKIPPED: ${skip.why})\\n`);\n }\n}\n\n/** The one write mechanic: mkdir, write, chmod — in that order, everywhere. */\nexport function writePlannedFile(file: PlannedFileLike): void {\n mkdirSync(dirname(file.path), { recursive: true });\n writeFileSync(file.path, file.content);\n if (file.mode !== undefined) chmodSync(file.path, file.mode);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../src/harness/claude-code.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAiD1D,eAAO,MAAM,UAAU,EAAE,qBAoBxB,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Claude Code — VERIFIED tier. Schema, install and removal, end to end.\n *\n * Schema per <https://code.claude.com/docs/en/hooks> (verified 2026-09-02; the deep\n * dive is docs/research/2026-09-02-agent-enforcement.md § 1):\n *\n * - stdin: `{ hook_event_name: \"PreToolUse\", tool_name, tool_input, cwd }`. For\n * `Read`, `tool_input.file_path` is already absolute and `offset`/`limit` mark a\n * windowed read; for `Bash`, `tool_input.command` is the full command string, and\n * a relative path in it resolves against the payload's `cwd` — the *session's*\n * working directory, which after the model `cd`s differs from the hook process's\n * own cwd.\n * - deny: `hookSpecificOutput.permissionDecision: \"deny\"` with\n * `permissionDecisionReason` — which is **shown to the model**, so the guard's\n * reason (the exact replacement command, the `smelt retrieve` contract) lands in\n * the transcript as steering.\n * - rewrite (opt-in, `hooks.enforcement: \"rewrite\"`): `updatedInput` replaces the\n * entire input object of the *same* tool (v2.0.10+), so a Bash command can be\n * substituted but a Read can never become a Bash call — Reads deny in every mode.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read'],\n bashTools: ['Bash'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n cwdKey: 'cwd',\n deny: (reason) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n }),\n rewrite: {\n document: ({ input, announcement }) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'allow',\n permissionDecisionReason: announcement,\n // updatedInput replaces the whole input object — unchanged fields ride along.\n updatedInput: input,\n },\n }),\n announce: 'reason',\n },\n};\n\nexport const claudeCode: ShimmedHarnessProfile = {\n id: 'claude-code',\n name: 'Claude Code',\n tier: 'verified',\n detect: ['.claude'],\n detectHome: ['.claude'],\n instructionFile: 'CLAUDE.md',\n instructions: 'snippet',\n caveats: [],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.claude/settings.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: true,\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../src/harness/claude-code.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAqD1D,eAAO,MAAM,UAAU,EAAE,qBA0BxB,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport { MCP_RUN_ARGS } from '../setup/recipe.ts';\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Claude Code — VERIFIED tier. Schema, install and removal, end to end.\n *\n * Schema per <https://code.claude.com/docs/en/hooks> (verified 2026-09-02; the deep\n * dive is docs/research/2026-09-02-agent-enforcement.md § 1):\n *\n * - stdin: `{ hook_event_name: \"PreToolUse\", tool_name, tool_input, cwd }`. For\n * `Read`, `tool_input.file_path` is already absolute and `offset`/`limit` mark a\n * windowed read; for `Bash`, `tool_input.command` is the full command string, and\n * a relative path in it resolves against the payload's `cwd` — the *session's*\n * working directory, which after the model `cd`s differs from the hook process's\n * own cwd.\n * - deny: `hookSpecificOutput.permissionDecision: \"deny\"` with\n * `permissionDecisionReason` — which is **shown to the model**, so the guard's\n * reason (the exact replacement command, the `smelt retrieve` contract) lands in\n * the transcript as steering.\n * - rewrite (opt-in, `hooks.enforcement: \"rewrite\"`): `updatedInput` replaces the\n * entire input object of the *same* tool (v2.0.10+), so a Bash command can be\n * substituted but a Read can never become a Bash call — Reads deny in every mode.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read'],\n bashTools: ['Bash'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n cwdKey: 'cwd',\n deny: (reason) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n }),\n rewrite: {\n document: ({ input, announcement }) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'allow',\n permissionDecisionReason: announcement,\n // updatedInput replaces the whole input object — unchanged fields ride along.\n updatedInput: input,\n },\n }),\n announce: 'reason',\n },\n};\n\n// The registration's bytes come from the SetupRecipe's run command (MCP_RUN_ARGS,\n// derived once beside the fact) — so the CLI command the README teaches and the file\n// setup writes cannot disagree.\n\nexport const claudeCode: ShimmedHarnessProfile = {\n id: 'claude-code',\n name: 'Claude Code',\n tier: 'verified',\n detect: ['.claude'],\n detectHome: ['.claude'],\n instructionFile: 'CLAUDE.md',\n instructions: 'snippet',\n caveats: [],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.claude/settings.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: true,\n },\n {\n kind: 'mcp-registration',\n file: '.mcp.json',\n path: ['mcpServers', 'smelt'],\n entry: () => ({ command: MCP_RUN_ARGS[0], args: MCP_RUN_ARGS.slice(1) }),\n },\n ],\n};\n"]}
@@ -1,3 +1,4 @@
1
+ import { MCP_RUN_ARGS } from '../setup/recipe.js';
1
2
  /**
2
3
  * Claude Code — VERIFIED tier. Schema, install and removal, end to end.
3
4
  *
@@ -44,6 +45,9 @@ const HOOKS = {
44
45
  announce: 'reason',
45
46
  },
46
47
  };
48
+ // The registration's bytes come from the SetupRecipe's run command (MCP_RUN_ARGS,
49
+ // derived once beside the fact) — so the CLI command the README teaches and the file
50
+ // setup writes cannot disagree.
47
51
  export const claudeCode = {
48
52
  id: 'claude-code',
49
53
  name: 'Claude Code',
@@ -63,6 +67,12 @@ export const claudeCode = {
63
67
  entry: 'command-list',
64
68
  lifecycle: true,
65
69
  },
70
+ {
71
+ kind: 'mcp-registration',
72
+ file: '.mcp.json',
73
+ path: ['mcpServers', 'smelt'],
74
+ entry: () => ({ command: MCP_RUN_ARGS[0], args: MCP_RUN_ARGS.slice(1) }),
75
+ },
66
76
  ],
67
77
  };
68
78
  //# sourceMappingURL=claude-code.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/harness/claude-code.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,KAAK,GAAsB;IAC/B,SAAS,EAAE,CAAC,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,MAAM,CAAC;IACnB,YAAY,EAAE,CAAC,WAAW,CAAC;IAC3B,aAAa,EAAE,CAAC,YAAY,CAAC;IAC7B,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjB,kBAAkB,EAAE;YAClB,aAAa,EAAE,YAAY;YAC3B,kBAAkB,EAAE,MAAM;YAC1B,wBAAwB,EAAE,MAAM;SACjC;KACF,CAAC;IACF,OAAO,EAAE;QACP,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;YACtC,kBAAkB,EAAE;gBAClB,aAAa,EAAE,YAAY;gBAC3B,kBAAkB,EAAE,OAAO;gBAC3B,wBAAwB,EAAE,YAAY;gBACtC,8EAA8E;gBAC9E,YAAY,EAAE,KAAK;aACpB;SACF,CAAC;QACF,QAAQ,EAAE,QAAQ;KACnB;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAA0B;IAC/C,EAAE,EAAE,aAAa;IACjB,IAAI,EAAE,aAAa;IACnB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,CAAC,SAAS,CAAC;IACnB,UAAU,EAAE,CAAC,SAAS,CAAC;IACvB,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE,EAAE;IACX,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE;QACP;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,uBAAuB;YAC7B,KAAK,EAAE,YAAY;YACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;YAC1B,KAAK,EAAE,cAAc;YACrB,SAAS,EAAE,IAAI;SAChB;KACF;CACF,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Claude Code — VERIFIED tier. Schema, install and removal, end to end.\n *\n * Schema per <https://code.claude.com/docs/en/hooks> (verified 2026-09-02; the deep\n * dive is docs/research/2026-09-02-agent-enforcement.md § 1):\n *\n * - stdin: `{ hook_event_name: \"PreToolUse\", tool_name, tool_input, cwd }`. For\n * `Read`, `tool_input.file_path` is already absolute and `offset`/`limit` mark a\n * windowed read; for `Bash`, `tool_input.command` is the full command string, and\n * a relative path in it resolves against the payload's `cwd` — the *session's*\n * working directory, which after the model `cd`s differs from the hook process's\n * own cwd.\n * - deny: `hookSpecificOutput.permissionDecision: \"deny\"` with\n * `permissionDecisionReason` — which is **shown to the model**, so the guard's\n * reason (the exact replacement command, the `smelt retrieve` contract) lands in\n * the transcript as steering.\n * - rewrite (opt-in, `hooks.enforcement: \"rewrite\"`): `updatedInput` replaces the\n * entire input object of the *same* tool (v2.0.10+), so a Bash command can be\n * substituted but a Read can never become a Bash call — Reads deny in every mode.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read'],\n bashTools: ['Bash'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n cwdKey: 'cwd',\n deny: (reason) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n }),\n rewrite: {\n document: ({ input, announcement }) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'allow',\n permissionDecisionReason: announcement,\n // updatedInput replaces the whole input object — unchanged fields ride along.\n updatedInput: input,\n },\n }),\n announce: 'reason',\n },\n};\n\nexport const claudeCode: ShimmedHarnessProfile = {\n id: 'claude-code',\n name: 'Claude Code',\n tier: 'verified',\n detect: ['.claude'],\n detectHome: ['.claude'],\n instructionFile: 'CLAUDE.md',\n instructions: 'snippet',\n caveats: [],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.claude/settings.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: true,\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/harness/claude-code.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,KAAK,GAAsB;IAC/B,SAAS,EAAE,CAAC,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,MAAM,CAAC;IACnB,YAAY,EAAE,CAAC,WAAW,CAAC;IAC3B,aAAa,EAAE,CAAC,YAAY,CAAC;IAC7B,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjB,kBAAkB,EAAE;YAClB,aAAa,EAAE,YAAY;YAC3B,kBAAkB,EAAE,MAAM;YAC1B,wBAAwB,EAAE,MAAM;SACjC;KACF,CAAC;IACF,OAAO,EAAE;QACP,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;YACtC,kBAAkB,EAAE;gBAClB,aAAa,EAAE,YAAY;gBAC3B,kBAAkB,EAAE,OAAO;gBAC3B,wBAAwB,EAAE,YAAY;gBACtC,8EAA8E;gBAC9E,YAAY,EAAE,KAAK;aACpB;SACF,CAAC;QACF,QAAQ,EAAE,QAAQ;KACnB;CACF,CAAC;AAEF,kFAAkF;AAClF,qFAAqF;AACrF,gCAAgC;AAEhC,MAAM,CAAC,MAAM,UAAU,GAA0B;IAC/C,EAAE,EAAE,aAAa;IACjB,IAAI,EAAE,aAAa;IACnB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,CAAC,SAAS,CAAC;IACnB,UAAU,EAAE,CAAC,SAAS,CAAC;IACvB,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE,EAAE;IACX,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE;QACP;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,uBAAuB;YAC7B,KAAK,EAAE,YAAY;YACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;YAC1B,KAAK,EAAE,cAAc;YACrB,SAAS,EAAE,IAAI;SAChB;QACD;YACE,IAAI,EAAE,kBAAkB;YACxB,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC;YAC7B,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;SACzE;KACF;CACF,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport { MCP_RUN_ARGS } from '../setup/recipe.ts';\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Claude Code — VERIFIED tier. Schema, install and removal, end to end.\n *\n * Schema per <https://code.claude.com/docs/en/hooks> (verified 2026-09-02; the deep\n * dive is docs/research/2026-09-02-agent-enforcement.md § 1):\n *\n * - stdin: `{ hook_event_name: \"PreToolUse\", tool_name, tool_input, cwd }`. For\n * `Read`, `tool_input.file_path` is already absolute and `offset`/`limit` mark a\n * windowed read; for `Bash`, `tool_input.command` is the full command string, and\n * a relative path in it resolves against the payload's `cwd` — the *session's*\n * working directory, which after the model `cd`s differs from the hook process's\n * own cwd.\n * - deny: `hookSpecificOutput.permissionDecision: \"deny\"` with\n * `permissionDecisionReason` — which is **shown to the model**, so the guard's\n * reason (the exact replacement command, the `smelt retrieve` contract) lands in\n * the transcript as steering.\n * - rewrite (opt-in, `hooks.enforcement: \"rewrite\"`): `updatedInput` replaces the\n * entire input object of the *same* tool (v2.0.10+), so a Bash command can be\n * substituted but a Read can never become a Bash call — Reads deny in every mode.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read'],\n bashTools: ['Bash'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n cwdKey: 'cwd',\n deny: (reason) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n }),\n rewrite: {\n document: ({ input, announcement }) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'allow',\n permissionDecisionReason: announcement,\n // updatedInput replaces the whole input object — unchanged fields ride along.\n updatedInput: input,\n },\n }),\n announce: 'reason',\n },\n};\n\n// The registration's bytes come from the SetupRecipe's run command (MCP_RUN_ARGS,\n// derived once beside the fact) — so the CLI command the README teaches and the file\n// setup writes cannot disagree.\n\nexport const claudeCode: ShimmedHarnessProfile = {\n id: 'claude-code',\n name: 'Claude Code',\n tier: 'verified',\n detect: ['.claude'],\n detectHome: ['.claude'],\n instructionFile: 'CLAUDE.md',\n instructions: 'snippet',\n caveats: [],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.claude/settings.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: true,\n },\n {\n kind: 'mcp-registration',\n file: '.mcp.json',\n path: ['mcpServers', 'smelt'],\n entry: () => ({ command: MCP_RUN_ARGS[0], args: MCP_RUN_ARGS.slice(1) }),\n },\n ],\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/harness/codex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AA4D1D,eAAO,MAAM,KAAK,EAAE,qBAiCnB,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\nimport { SNIPPET_END_HASH, SNIPPET_START_HASH } from './snippet.ts';\n\n/**\n * Codex CLI — VERIFIED tier. Schema, install and removal, end to end.\n *\n * Codex's experimental hooks (`features.hooks`, rust-v0.114.0+, on by default from\n * 0.150.1) deliberately mirror Claude Code's schema — the survey is\n * docs/research/2026-09-02-agent-enforcement.md § 3, primary source\n * <https://developers.openai.com/codex/hooks>:\n *\n * - stdin: `{ tool_name, tool_input }`; shell input arrives as `tool_name: \"Bash\"`,\n * `tool_input.command`.\n * - deny: `hookSpecificOutput.permissionDecision: \"deny\"` + reason (or exit 2).\n * - rewrite: `permissionDecision: \"allow\"` with `updatedInput` — for Bash,\n * `updatedInput` **must include a string `command` field**, which the shared splice\n * always does.\n *\n * Two documented differences from Claude Code, both honoured here:\n * `permissionDecision: \"ask\"` is NOT supported (a parse error — the tool proceeds),\n * and this schema never emits it; project-layer hooks only run once the project is\n * trusted, which the installer's output says out loud, and which the `[features]`\n * block below is what enables.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read'],\n bashTools: ['Bash', 'shell'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n }),\n rewrite: {\n document: ({ input, announcement }) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'allow',\n permissionDecisionReason: announcement,\n updatedInput: input,\n },\n }),\n announce: 'reason',\n },\n};\n\n/** The Codex `config.toml` block: enables the hooks feature, marker-bracketed. */\nfunction codexConfigTomlBlock(): string {\n return `${SNIPPET_START_HASH}\n# Enables Codex's hooks feature so .codex/hooks.json is honored. Project-level hooks\n# run only once this project is trusted. Written by \\`smelt hooks install\\`.\n[features]\nhooks = true\n${SNIPPET_END_HASH}\n`;\n}\n\nexport const codex: ShimmedHarnessProfile = {\n id: 'codex',\n name: 'Codex CLI',\n tier: 'verified',\n detect: ['.codex'],\n detectHome: ['.codex'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'project-level Codex hooks run only once the project is trusted (features.hooks; see docs/research/2026-09-02-agent-enforcement.md § 3)',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.codex/hooks.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: true,\n },\n {\n kind: 'marker-block',\n file: '.codex/config.toml',\n block: codexConfigTomlBlock,\n start: SNIPPET_START_HASH,\n end: SNIPPET_END_HASH,\n skipWhen: {\n contains: '[features]',\n why: 'already has a [features] table — add `hooks = true` to it yourself',\n },\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/harness/codex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AA4D1D,eAAO,MAAM,KAAK,EAAE,qBAmCnB,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\nimport { SNIPPET_END_HASH, SNIPPET_START_HASH } from './snippet.ts';\n\n/**\n * Codex CLI — VERIFIED tier. Schema, install and removal, end to end.\n *\n * Codex's experimental hooks (`features.hooks`, rust-v0.114.0+, on by default from\n * 0.150.1) deliberately mirror Claude Code's schema — the survey is\n * docs/research/2026-09-02-agent-enforcement.md § 3, primary source\n * <https://developers.openai.com/codex/hooks>:\n *\n * - stdin: `{ tool_name, tool_input }`; shell input arrives as `tool_name: \"Bash\"`,\n * `tool_input.command`.\n * - deny: `hookSpecificOutput.permissionDecision: \"deny\"` + reason (or exit 2).\n * - rewrite: `permissionDecision: \"allow\"` with `updatedInput` — for Bash,\n * `updatedInput` **must include a string `command` field**, which the shared splice\n * always does.\n *\n * Two documented differences from Claude Code, both honoured here:\n * `permissionDecision: \"ask\"` is NOT supported (a parse error — the tool proceeds),\n * and this schema never emits it; project-layer hooks only run once the project is\n * trusted, which the installer's output says out loud, and which the `[features]`\n * block below is what enables.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read'],\n bashTools: ['Bash', 'shell'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n }),\n rewrite: {\n document: ({ input, announcement }) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'allow',\n permissionDecisionReason: announcement,\n updatedInput: input,\n },\n }),\n announce: 'reason',\n },\n};\n\n/** The Codex `config.toml` block: enables the hooks feature, marker-bracketed. */\nfunction codexConfigTomlBlock(): string {\n return `${SNIPPET_START_HASH}\n# Enables Codex's hooks feature so .codex/hooks.json is honored. Project-level hooks\n# run only once this project is trusted. Written by \\`smelt hooks install\\`.\n[features]\nhooks = true\n${SNIPPET_END_HASH}\n`;\n}\n\nexport const codex: ShimmedHarnessProfile = {\n id: 'codex',\n name: 'Codex CLI',\n shortName: 'Codex',\n tier: 'verified',\n detect: ['.codex'],\n detectHome: ['.codex'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'project-level Codex hooks run only once the project is trusted (features.hooks; see docs/research/2026-09-02-agent-enforcement.md § 3)',\n 'MCP registration is manual for Codex (TOML) — packages/mcp/README.md has the snippet; setup does not edit TOML',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.codex/hooks.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: true,\n },\n {\n kind: 'marker-block',\n file: '.codex/config.toml',\n block: codexConfigTomlBlock,\n start: SNIPPET_START_HASH,\n end: SNIPPET_END_HASH,\n skipWhen: {\n contains: '[features]',\n why: 'already has a [features] table — add `hooks = true` to it yourself',\n },\n },\n ],\n};\n"]}
@@ -57,6 +57,7 @@ ${SNIPPET_END_HASH}
57
57
  export const codex = {
58
58
  id: 'codex',
59
59
  name: 'Codex CLI',
60
+ shortName: 'Codex',
60
61
  tier: 'verified',
61
62
  detect: ['.codex'],
62
63
  detectHome: ['.codex'],
@@ -64,6 +65,7 @@ export const codex = {
64
65
  instructions: 'snippet',
65
66
  caveats: [
66
67
  'project-level Codex hooks run only once the project is trusted (features.hooks; see docs/research/2026-09-02-agent-enforcement.md § 3)',
68
+ 'MCP registration is manual for Codex (TOML) — packages/mcp/README.md has the snippet; setup does not edit TOML',
67
69
  ],
68
70
  hooks: HOOKS,
69
71
  install: [
@@ -1 +1 @@
1
- {"version":3,"file":"codex.js","sourceRoot":"","sources":["../../src/harness/codex.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,KAAK,GAAsB;IAC/B,SAAS,EAAE,CAAC,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,CAAC,WAAW,CAAC;IAC3B,aAAa,EAAE,CAAC,YAAY,CAAC;IAC7B,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjB,kBAAkB,EAAE;YAClB,aAAa,EAAE,YAAY;YAC3B,kBAAkB,EAAE,MAAM;YAC1B,wBAAwB,EAAE,MAAM;SACjC;KACF,CAAC;IACF,OAAO,EAAE;QACP,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;YACtC,kBAAkB,EAAE;gBAClB,aAAa,EAAE,YAAY;gBAC3B,kBAAkB,EAAE,OAAO;gBAC3B,wBAAwB,EAAE,YAAY;gBACtC,YAAY,EAAE,KAAK;aACpB;SACF,CAAC;QACF,QAAQ,EAAE,QAAQ;KACnB;CACF,CAAC;AAEF,kFAAkF;AAClF,SAAS,oBAAoB;IAC3B,OAAO,GAAG,kBAAkB;;;;;EAK5B,gBAAgB;CACjB,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,KAAK,GAA0B;IAC1C,EAAE,EAAE,OAAO;IACX,IAAI,EAAE,WAAW;IACjB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,CAAC;IACtB,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE;QACP,wIAAwI;KACzI;IACD,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE;QACP;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,mBAAmB;YACzB,KAAK,EAAE,YAAY;YACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;YAC1B,KAAK,EAAE,cAAc;YACrB,SAAS,EAAE,IAAI;SAChB;QACD;YACE,IAAI,EAAE,cAAc;YACpB,IAAI,EAAE,oBAAoB;YAC1B,KAAK,EAAE,oBAAoB;YAC3B,KAAK,EAAE,kBAAkB;YACzB,GAAG,EAAE,gBAAgB;YACrB,QAAQ,EAAE;gBACR,QAAQ,EAAE,YAAY;gBACtB,GAAG,EAAE,oEAAoE;aAC1E;SACF;KACF;CACF,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\nimport { SNIPPET_END_HASH, SNIPPET_START_HASH } from './snippet.ts';\n\n/**\n * Codex CLI — VERIFIED tier. Schema, install and removal, end to end.\n *\n * Codex's experimental hooks (`features.hooks`, rust-v0.114.0+, on by default from\n * 0.150.1) deliberately mirror Claude Code's schema — the survey is\n * docs/research/2026-09-02-agent-enforcement.md § 3, primary source\n * <https://developers.openai.com/codex/hooks>:\n *\n * - stdin: `{ tool_name, tool_input }`; shell input arrives as `tool_name: \"Bash\"`,\n * `tool_input.command`.\n * - deny: `hookSpecificOutput.permissionDecision: \"deny\"` + reason (or exit 2).\n * - rewrite: `permissionDecision: \"allow\"` with `updatedInput` — for Bash,\n * `updatedInput` **must include a string `command` field**, which the shared splice\n * always does.\n *\n * Two documented differences from Claude Code, both honoured here:\n * `permissionDecision: \"ask\"` is NOT supported (a parse error — the tool proceeds),\n * and this schema never emits it; project-layer hooks only run once the project is\n * trusted, which the installer's output says out loud, and which the `[features]`\n * block below is what enables.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read'],\n bashTools: ['Bash', 'shell'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n }),\n rewrite: {\n document: ({ input, announcement }) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'allow',\n permissionDecisionReason: announcement,\n updatedInput: input,\n },\n }),\n announce: 'reason',\n },\n};\n\n/** The Codex `config.toml` block: enables the hooks feature, marker-bracketed. */\nfunction codexConfigTomlBlock(): string {\n return `${SNIPPET_START_HASH}\n# Enables Codex's hooks feature so .codex/hooks.json is honored. Project-level hooks\n# run only once this project is trusted. Written by \\`smelt hooks install\\`.\n[features]\nhooks = true\n${SNIPPET_END_HASH}\n`;\n}\n\nexport const codex: ShimmedHarnessProfile = {\n id: 'codex',\n name: 'Codex CLI',\n tier: 'verified',\n detect: ['.codex'],\n detectHome: ['.codex'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'project-level Codex hooks run only once the project is trusted (features.hooks; see docs/research/2026-09-02-agent-enforcement.md § 3)',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.codex/hooks.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: true,\n },\n {\n kind: 'marker-block',\n file: '.codex/config.toml',\n block: codexConfigTomlBlock,\n start: SNIPPET_START_HASH,\n end: SNIPPET_END_HASH,\n skipWhen: {\n contains: '[features]',\n why: 'already has a [features] table — add `hooks = true` to it yourself',\n },\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"codex.js","sourceRoot":"","sources":["../../src/harness/codex.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,KAAK,GAAsB;IAC/B,SAAS,EAAE,CAAC,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,CAAC,WAAW,CAAC;IAC3B,aAAa,EAAE,CAAC,YAAY,CAAC;IAC7B,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjB,kBAAkB,EAAE;YAClB,aAAa,EAAE,YAAY;YAC3B,kBAAkB,EAAE,MAAM;YAC1B,wBAAwB,EAAE,MAAM;SACjC;KACF,CAAC;IACF,OAAO,EAAE;QACP,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;YACtC,kBAAkB,EAAE;gBAClB,aAAa,EAAE,YAAY;gBAC3B,kBAAkB,EAAE,OAAO;gBAC3B,wBAAwB,EAAE,YAAY;gBACtC,YAAY,EAAE,KAAK;aACpB;SACF,CAAC;QACF,QAAQ,EAAE,QAAQ;KACnB;CACF,CAAC;AAEF,kFAAkF;AAClF,SAAS,oBAAoB;IAC3B,OAAO,GAAG,kBAAkB;;;;;EAK5B,gBAAgB;CACjB,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,KAAK,GAA0B;IAC1C,EAAE,EAAE,OAAO;IACX,IAAI,EAAE,WAAW;IACjB,SAAS,EAAE,OAAO;IAClB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,CAAC;IACtB,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE;QACP,wIAAwI;QACxI,gHAAgH;KACjH;IACD,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE;QACP;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,mBAAmB;YACzB,KAAK,EAAE,YAAY;YACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;YAC1B,KAAK,EAAE,cAAc;YACrB,SAAS,EAAE,IAAI;SAChB;QACD;YACE,IAAI,EAAE,cAAc;YACpB,IAAI,EAAE,oBAAoB;YAC1B,KAAK,EAAE,oBAAoB;YAC3B,KAAK,EAAE,kBAAkB;YACzB,GAAG,EAAE,gBAAgB;YACrB,QAAQ,EAAE;gBACR,QAAQ,EAAE,YAAY;gBACtB,GAAG,EAAE,oEAAoE;aAC1E;SACF;KACF;CACF,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\nimport { SNIPPET_END_HASH, SNIPPET_START_HASH } from './snippet.ts';\n\n/**\n * Codex CLI — VERIFIED tier. Schema, install and removal, end to end.\n *\n * Codex's experimental hooks (`features.hooks`, rust-v0.114.0+, on by default from\n * 0.150.1) deliberately mirror Claude Code's schema — the survey is\n * docs/research/2026-09-02-agent-enforcement.md § 3, primary source\n * <https://developers.openai.com/codex/hooks>:\n *\n * - stdin: `{ tool_name, tool_input }`; shell input arrives as `tool_name: \"Bash\"`,\n * `tool_input.command`.\n * - deny: `hookSpecificOutput.permissionDecision: \"deny\"` + reason (or exit 2).\n * - rewrite: `permissionDecision: \"allow\"` with `updatedInput` — for Bash,\n * `updatedInput` **must include a string `command` field**, which the shared splice\n * always does.\n *\n * Two documented differences from Claude Code, both honoured here:\n * `permissionDecision: \"ask\"` is NOT supported (a parse error — the tool proceeds),\n * and this schema never emits it; project-layer hooks only run once the project is\n * trusted, which the installer's output says out loud, and which the `[features]`\n * block below is what enables.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read'],\n bashTools: ['Bash', 'shell'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n }),\n rewrite: {\n document: ({ input, announcement }) => ({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'allow',\n permissionDecisionReason: announcement,\n updatedInput: input,\n },\n }),\n announce: 'reason',\n },\n};\n\n/** The Codex `config.toml` block: enables the hooks feature, marker-bracketed. */\nfunction codexConfigTomlBlock(): string {\n return `${SNIPPET_START_HASH}\n# Enables Codex's hooks feature so .codex/hooks.json is honored. Project-level hooks\n# run only once this project is trusted. Written by \\`smelt hooks install\\`.\n[features]\nhooks = true\n${SNIPPET_END_HASH}\n`;\n}\n\nexport const codex: ShimmedHarnessProfile = {\n id: 'codex',\n name: 'Codex CLI',\n shortName: 'Codex',\n tier: 'verified',\n detect: ['.codex'],\n detectHome: ['.codex'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'project-level Codex hooks run only once the project is trusted (features.hooks; see docs/research/2026-09-02-agent-enforcement.md § 3)',\n 'MCP registration is manual for Codex (TOML) — packages/mcp/README.md has the snippet; setup does not edit TOML',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.codex/hooks.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: true,\n },\n {\n kind: 'marker-block',\n file: '.codex/config.toml',\n block: codexConfigTomlBlock,\n start: SNIPPET_START_HASH,\n end: SNIPPET_END_HASH,\n skipWhen: {\n contains: '[features]',\n why: 'already has a [features] table — add `hooks = true` to it yourself',\n },\n },\n ],\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/harness/gemini.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAsC1D,eAAO,MAAM,MAAM,EAAE,qBAsBpB,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Gemini CLI — EXPERIMENTAL tier. Schema, install and removal, end to end.\n *\n * The schema below is mapped from the capability matrix\n * (docs/research/2026-09-02-harness-capability-matrix.md, Gemini CLI row; primary\n * source <https://geminicli.com/docs/hooks/reference/>), and the field names were\n * additionally checked 2026-09-02 against the shipped `@google/gemini-cli` 0.58.0\n * bundle (installed headlessly for the shim's verification pass): the BeforeTool stdin\n * payload is `{ tool_name, tool_input }`, `read_file` takes `file_path` with\n * `start_line`/`end_line` windows, the shell tool is `run_shell_command` with\n * `command`, denial is `{ \"decision\": \"deny\", \"reason\": … }`, and\n * `hookSpecificOutput.tool_input` substitutes the input. What has NOT run is a live\n * end-to-end session (needs credentials this environment does not have), so the tier\n * stays experimental — treat surprises as shim bugs and please report them.\n *\n * Carried caveat from the matrix: Gemini's policy engine has an open bug where an\n * `allow` policy is ignored in non-interactive runs\n * (<https://github.com/google-gemini/gemini-cli/issues/20469>) — this preset uses\n * hooks, not the policy engine, but non-interactive behaviour is the least-tested\n * corner; verify before relying on it in CI.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['read_file', 'ReadFile', 'Read'],\n bashTools: ['run_shell_command', 'Shell', 'Bash'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({ decision: 'deny', reason }),\n rewrite: {\n document: ({ input }) => ({\n hookSpecificOutput: { hookEventName: 'BeforeTool', tool_input: input },\n }),\n // The BeforeTool rewrite shape carries no reason field — announce on stderr.\n announce: 'stderr',\n },\n};\n\nexport const gemini: ShimmedHarnessProfile = {\n id: 'gemini',\n name: 'Gemini CLI',\n tier: 'experimental',\n detect: ['.gemini'],\n detectHome: ['.gemini'],\n instructionFile: 'GEMINI.md',\n instructions: 'snippet',\n caveats: [\n 'Gemini policy-engine allow rules are ignored in non-interactive runs (google-gemini/gemini-cli#20469) — verify hook behaviour in CI before relying on it',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.gemini/settings.json',\n event: 'BeforeTool',\n matchers: ['read_file', 'run_shell_command'],\n entry: 'command-list',\n lifecycle: false,\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/harness/gemini.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAsC1D,eAAO,MAAM,MAAM,EAAE,qBAuBpB,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Gemini CLI — EXPERIMENTAL tier. Schema, install and removal, end to end.\n *\n * The schema below is mapped from the capability matrix\n * (docs/research/2026-09-02-harness-capability-matrix.md, Gemini CLI row; primary\n * source <https://geminicli.com/docs/hooks/reference/>), and the field names were\n * additionally checked 2026-09-02 against the shipped `@google/gemini-cli` 0.58.0\n * bundle (installed headlessly for the shim's verification pass): the BeforeTool stdin\n * payload is `{ tool_name, tool_input }`, `read_file` takes `file_path` with\n * `start_line`/`end_line` windows, the shell tool is `run_shell_command` with\n * `command`, denial is `{ \"decision\": \"deny\", \"reason\": … }`, and\n * `hookSpecificOutput.tool_input` substitutes the input. What has NOT run is a live\n * end-to-end session (needs credentials this environment does not have), so the tier\n * stays experimental — treat surprises as shim bugs and please report them.\n *\n * Carried caveat from the matrix: Gemini's policy engine has an open bug where an\n * `allow` policy is ignored in non-interactive runs\n * (<https://github.com/google-gemini/gemini-cli/issues/20469>) — this preset uses\n * hooks, not the policy engine, but non-interactive behaviour is the least-tested\n * corner; verify before relying on it in CI.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['read_file', 'ReadFile', 'Read'],\n bashTools: ['run_shell_command', 'Shell', 'Bash'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({ decision: 'deny', reason }),\n rewrite: {\n document: ({ input }) => ({\n hookSpecificOutput: { hookEventName: 'BeforeTool', tool_input: input },\n }),\n // The BeforeTool rewrite shape carries no reason field — announce on stderr.\n announce: 'stderr',\n },\n};\n\nexport const gemini: ShimmedHarnessProfile = {\n id: 'gemini',\n name: 'Gemini CLI',\n shortName: 'Gemini',\n tier: 'experimental',\n detect: ['.gemini'],\n detectHome: ['.gemini'],\n instructionFile: 'GEMINI.md',\n instructions: 'snippet',\n caveats: [\n 'Gemini policy-engine allow rules are ignored in non-interactive runs (google-gemini/gemini-cli#20469) — verify hook behaviour in CI before relying on it',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.gemini/settings.json',\n event: 'BeforeTool',\n matchers: ['read_file', 'run_shell_command'],\n entry: 'command-list',\n lifecycle: false,\n },\n ],\n};\n"]}
@@ -36,6 +36,7 @@ const HOOKS = {
36
36
  export const gemini = {
37
37
  id: 'gemini',
38
38
  name: 'Gemini CLI',
39
+ shortName: 'Gemini',
39
40
  tier: 'experimental',
40
41
  detect: ['.gemini'],
41
42
  detectHome: ['.gemini'],
@@ -1 +1 @@
1
- {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/harness/gemini.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,KAAK,GAAsB;IAC/B,SAAS,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC;IAC5C,SAAS,EAAE,CAAC,mBAAmB,EAAE,OAAO,EAAE,MAAM,CAAC;IACjD,YAAY,EAAE,CAAC,WAAW,CAAC;IAC3B,aAAa,EAAE,CAAC,YAAY,CAAC;IAC7B,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAChD,OAAO,EAAE;QACP,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;YACxB,kBAAkB,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE;SACvE,CAAC;QACF,6EAA6E;QAC7E,QAAQ,EAAE,QAAQ;KACnB;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAA0B;IAC3C,EAAE,EAAE,QAAQ;IACZ,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,CAAC,SAAS,CAAC;IACnB,UAAU,EAAE,CAAC,SAAS,CAAC;IACvB,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE;QACP,0JAA0J;KAC3J;IACD,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE;QACP;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,uBAAuB;YAC7B,KAAK,EAAE,YAAY;YACnB,QAAQ,EAAE,CAAC,WAAW,EAAE,mBAAmB,CAAC;YAC5C,KAAK,EAAE,cAAc;YACrB,SAAS,EAAE,KAAK;SACjB;KACF;CACF,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Gemini CLI — EXPERIMENTAL tier. Schema, install and removal, end to end.\n *\n * The schema below is mapped from the capability matrix\n * (docs/research/2026-09-02-harness-capability-matrix.md, Gemini CLI row; primary\n * source <https://geminicli.com/docs/hooks/reference/>), and the field names were\n * additionally checked 2026-09-02 against the shipped `@google/gemini-cli` 0.58.0\n * bundle (installed headlessly for the shim's verification pass): the BeforeTool stdin\n * payload is `{ tool_name, tool_input }`, `read_file` takes `file_path` with\n * `start_line`/`end_line` windows, the shell tool is `run_shell_command` with\n * `command`, denial is `{ \"decision\": \"deny\", \"reason\": … }`, and\n * `hookSpecificOutput.tool_input` substitutes the input. What has NOT run is a live\n * end-to-end session (needs credentials this environment does not have), so the tier\n * stays experimental — treat surprises as shim bugs and please report them.\n *\n * Carried caveat from the matrix: Gemini's policy engine has an open bug where an\n * `allow` policy is ignored in non-interactive runs\n * (<https://github.com/google-gemini/gemini-cli/issues/20469>) — this preset uses\n * hooks, not the policy engine, but non-interactive behaviour is the least-tested\n * corner; verify before relying on it in CI.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['read_file', 'ReadFile', 'Read'],\n bashTools: ['run_shell_command', 'Shell', 'Bash'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({ decision: 'deny', reason }),\n rewrite: {\n document: ({ input }) => ({\n hookSpecificOutput: { hookEventName: 'BeforeTool', tool_input: input },\n }),\n // The BeforeTool rewrite shape carries no reason field — announce on stderr.\n announce: 'stderr',\n },\n};\n\nexport const gemini: ShimmedHarnessProfile = {\n id: 'gemini',\n name: 'Gemini CLI',\n tier: 'experimental',\n detect: ['.gemini'],\n detectHome: ['.gemini'],\n instructionFile: 'GEMINI.md',\n instructions: 'snippet',\n caveats: [\n 'Gemini policy-engine allow rules are ignored in non-interactive runs (google-gemini/gemini-cli#20469) — verify hook behaviour in CI before relying on it',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.gemini/settings.json',\n event: 'BeforeTool',\n matchers: ['read_file', 'run_shell_command'],\n entry: 'command-list',\n lifecycle: false,\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/harness/gemini.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,KAAK,GAAsB;IAC/B,SAAS,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC;IAC5C,SAAS,EAAE,CAAC,mBAAmB,EAAE,OAAO,EAAE,MAAM,CAAC;IACjD,YAAY,EAAE,CAAC,WAAW,CAAC;IAC3B,aAAa,EAAE,CAAC,YAAY,CAAC;IAC7B,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAChD,OAAO,EAAE;QACP,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;YACxB,kBAAkB,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE;SACvE,CAAC;QACF,6EAA6E;QAC7E,QAAQ,EAAE,QAAQ;KACnB;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAA0B;IAC3C,EAAE,EAAE,QAAQ;IACZ,IAAI,EAAE,YAAY;IAClB,SAAS,EAAE,QAAQ;IACnB,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,CAAC,SAAS,CAAC;IACnB,UAAU,EAAE,CAAC,SAAS,CAAC;IACvB,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE;QACP,0JAA0J;KAC3J;IACD,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE;QACP;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,uBAAuB;YAC7B,KAAK,EAAE,YAAY;YACnB,QAAQ,EAAE,CAAC,WAAW,EAAE,mBAAmB,CAAC;YAC5C,KAAK,EAAE,cAAc;YACrB,SAAS,EAAE,KAAK;SACjB;KACF;CACF,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Gemini CLI — EXPERIMENTAL tier. Schema, install and removal, end to end.\n *\n * The schema below is mapped from the capability matrix\n * (docs/research/2026-09-02-harness-capability-matrix.md, Gemini CLI row; primary\n * source <https://geminicli.com/docs/hooks/reference/>), and the field names were\n * additionally checked 2026-09-02 against the shipped `@google/gemini-cli` 0.58.0\n * bundle (installed headlessly for the shim's verification pass): the BeforeTool stdin\n * payload is `{ tool_name, tool_input }`, `read_file` takes `file_path` with\n * `start_line`/`end_line` windows, the shell tool is `run_shell_command` with\n * `command`, denial is `{ \"decision\": \"deny\", \"reason\": … }`, and\n * `hookSpecificOutput.tool_input` substitutes the input. What has NOT run is a live\n * end-to-end session (needs credentials this environment does not have), so the tier\n * stays experimental — treat surprises as shim bugs and please report them.\n *\n * Carried caveat from the matrix: Gemini's policy engine has an open bug where an\n * `allow` policy is ignored in non-interactive runs\n * (<https://github.com/google-gemini/gemini-cli/issues/20469>) — this preset uses\n * hooks, not the policy engine, but non-interactive behaviour is the least-tested\n * corner; verify before relying on it in CI.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['read_file', 'ReadFile', 'Read'],\n bashTools: ['run_shell_command', 'Shell', 'Bash'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({ decision: 'deny', reason }),\n rewrite: {\n document: ({ input }) => ({\n hookSpecificOutput: { hookEventName: 'BeforeTool', tool_input: input },\n }),\n // The BeforeTool rewrite shape carries no reason field — announce on stderr.\n announce: 'stderr',\n },\n};\n\nexport const gemini: ShimmedHarnessProfile = {\n id: 'gemini',\n name: 'Gemini CLI',\n shortName: 'Gemini',\n tier: 'experimental',\n detect: ['.gemini'],\n detectHome: ['.gemini'],\n instructionFile: 'GEMINI.md',\n instructions: 'snippet',\n caveats: [\n 'Gemini policy-engine allow rules are ignored in non-interactive runs (google-gemini/gemini-cli#20469) — verify hook behaviour in CI before relying on it',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.gemini/settings.json',\n event: 'BeforeTool',\n matchers: ['read_file', 'run_shell_command'],\n entry: 'command-list',\n lifecycle: false,\n },\n ],\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"grok.d.ts","sourceRoot":"","sources":["../../src/harness/grok.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AA2B1D,eAAO,MAAM,IAAI,EAAE,qBAoBlB,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Grok CLI — EXPERIMENTAL tier. Schema, install and removal, end to end.\n *\n * Schema mapped from the capability matrix\n * (docs/research/2026-09-02-harness-capability-matrix.md, Grok CLI row; primary source\n * <https://docs.x.ai/build/features/hooks>), not yet smoke-tested against the real\n * binary. Note the matrix's provenance warning: the official CLI is\n * `xai-org/grok-build` (binary `grok`) — `superagent-ai/grok-cli` is a third-party\n * clone with a different hook surface.\n *\n * - event: `PreToolUse`; stdin `{ tool_name, tool_input }` (Claude-Code-like tool\n * names).\n * - deny: `{ \"decision\": \"deny\", \"reason\": … }` or exit 2. **Deny-only**: the input\n * is read-only to hooks, so there is no `rewrite` here — under\n * `hooks.enforcement: \"rewrite\"` this harness falls back to the deny, whose reason\n * still carries the exact replacement pipeline.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read', 'read_file', 'ReadFile'],\n bashTools: ['Bash', 'shell', 'run_shell_command'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({ decision: 'deny', reason }),\n};\n\nexport const grok: ShimmedHarnessProfile = {\n id: 'grok',\n name: 'Grok CLI',\n tier: 'experimental',\n detect: ['.grok'],\n detectHome: ['.grok'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: ['deny-only hooks: input rewrite is not supported, so rewrite mode falls back to deny'],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.grok/hooks.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: false,\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"grok.d.ts","sourceRoot":"","sources":["../../src/harness/grok.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AA2B1D,eAAO,MAAM,IAAI,EAAE,qBAwBlB,CAAC","sourcesContent":["import type { HarnessHookSchema } from '../hooks/shim.ts';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Grok CLI — EXPERIMENTAL tier. Schema, install and removal, end to end.\n *\n * Schema mapped from the capability matrix\n * (docs/research/2026-09-02-harness-capability-matrix.md, Grok CLI row; primary source\n * <https://docs.x.ai/build/features/hooks>), not yet smoke-tested against the real\n * binary. Note the matrix's provenance warning: the official CLI is\n * `xai-org/grok-build` (binary `grok`) — `superagent-ai/grok-cli` is a third-party\n * clone with a different hook surface.\n *\n * - event: `PreToolUse`; stdin `{ tool_name, tool_input }` (Claude-Code-like tool\n * names).\n * - deny: `{ \"decision\": \"deny\", \"reason\": … }` or exit 2. **Deny-only**: the input\n * is read-only to hooks, so there is no `rewrite` here — under\n * `hooks.enforcement: \"rewrite\"` this harness falls back to the deny, whose reason\n * still carries the exact replacement pipeline.\n */\nconst HOOKS: HarnessHookSchema = {\n readTools: ['Read', 'read_file', 'ReadFile'],\n bashTools: ['Bash', 'shell', 'run_shell_command'],\n toolNameKeys: ['tool_name'],\n toolInputKeys: ['tool_input'],\n deny: (reason) => ({ decision: 'deny', reason }),\n};\n\nexport const grok: ShimmedHarnessProfile = {\n id: 'grok',\n name: 'Grok CLI',\n shortName: 'Grok',\n tier: 'experimental',\n detect: ['.grok'],\n detectHome: ['.grok'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'deny-only hooks: input rewrite is not supported, so rewrite mode falls back to deny',\n 'MCP registration is manual for Grok (TOML) — packages/mcp/README.md has the snippet; setup does not edit TOML',\n ],\n hooks: HOOKS,\n install: [\n {\n kind: 'json-hooks',\n file: '.grok/hooks.json',\n event: 'PreToolUse',\n matchers: ['Read', 'Bash'],\n entry: 'command-list',\n lifecycle: false,\n },\n ],\n};\n"]}