@smeltjs/core 0.4.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 (96) hide show
  1. package/dist/cli/bin.d.ts.map +1 -1
  2. package/dist/cli/bin.js +9 -4
  3. package/dist/cli/bin.js.map +1 -1
  4. package/dist/cli/doctor.d.ts +66 -0
  5. package/dist/cli/doctor.d.ts.map +1 -0
  6. package/dist/cli/doctor.js +137 -0
  7. package/dist/cli/doctor.js.map +1 -0
  8. package/dist/cli/hooks.d.ts +36 -2
  9. package/dist/cli/hooks.d.ts.map +1 -1
  10. package/dist/cli/hooks.js +108 -64
  11. package/dist/cli/hooks.js.map +1 -1
  12. package/dist/cli/init.d.ts.map +1 -1
  13. package/dist/cli/init.js +8 -14
  14. package/dist/cli/init.js.map +1 -1
  15. package/dist/cli/installed.d.ts +56 -0
  16. package/dist/cli/installed.d.ts.map +1 -0
  17. package/dist/cli/installed.js +105 -0
  18. package/dist/cli/installed.js.map +1 -0
  19. package/dist/cli/lava.d.ts +31 -0
  20. package/dist/cli/lava.d.ts.map +1 -0
  21. package/dist/cli/lava.js +82 -0
  22. package/dist/cli/lava.js.map +1 -0
  23. package/dist/cli/setup.d.ts +87 -0
  24. package/dist/cli/setup.d.ts.map +1 -0
  25. package/dist/cli/setup.js +513 -0
  26. package/dist/cli/setup.js.map +1 -0
  27. package/dist/cli/shell.d.ts +11 -4
  28. package/dist/cli/shell.d.ts.map +1 -1
  29. package/dist/cli/shell.js.map +1 -1
  30. package/dist/cli/subcommands/agents.d.ts.map +1 -1
  31. package/dist/cli/subcommands/agents.js +2 -1
  32. package/dist/cli/subcommands/agents.js.map +1 -1
  33. package/dist/cli/subcommands/doctor.d.ts +15 -0
  34. package/dist/cli/subcommands/doctor.d.ts.map +1 -0
  35. package/dist/cli/subcommands/doctor.js +43 -0
  36. package/dist/cli/subcommands/doctor.js.map +1 -0
  37. package/dist/cli/subcommands/flags.d.ts +13 -0
  38. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  39. package/dist/cli/subcommands/flags.js +27 -1
  40. package/dist/cli/subcommands/flags.js.map +1 -1
  41. package/dist/cli/subcommands/hooks.d.ts.map +1 -1
  42. package/dist/cli/subcommands/hooks.js +10 -2
  43. package/dist/cli/subcommands/hooks.js.map +1 -1
  44. package/dist/cli/subcommands/init.d.ts.map +1 -1
  45. package/dist/cli/subcommands/init.js +2 -1
  46. package/dist/cli/subcommands/init.js.map +1 -1
  47. package/dist/cli/subcommands/registry.d.ts +3 -1
  48. package/dist/cli/subcommands/registry.d.ts.map +1 -1
  49. package/dist/cli/subcommands/registry.js +6 -0
  50. package/dist/cli/subcommands/registry.js.map +1 -1
  51. package/dist/cli/subcommands/setup.d.ts +23 -0
  52. package/dist/cli/subcommands/setup.d.ts.map +1 -0
  53. package/dist/cli/subcommands/setup.js +87 -0
  54. package/dist/cli/subcommands/setup.js.map +1 -0
  55. package/dist/cli/subcommands/subcommand.d.ts +1 -1
  56. package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
  57. package/dist/cli/subcommands/subcommand.js.map +1 -1
  58. package/dist/cli/wizard.d.ts +72 -0
  59. package/dist/cli/wizard.d.ts.map +1 -0
  60. package/dist/cli/wizard.js +86 -0
  61. package/dist/cli/wizard.js.map +1 -0
  62. package/dist/harness/claude-code.d.ts.map +1 -1
  63. package/dist/harness/claude-code.js +10 -0
  64. package/dist/harness/claude-code.js.map +1 -1
  65. package/dist/harness/codex.d.ts.map +1 -1
  66. package/dist/harness/codex.js +1 -0
  67. package/dist/harness/codex.js.map +1 -1
  68. package/dist/harness/grok.d.ts.map +1 -1
  69. package/dist/harness/grok.js +4 -1
  70. package/dist/harness/grok.js.map +1 -1
  71. package/dist/harness/kilocode.d.ts.map +1 -1
  72. package/dist/harness/kilocode.js +1 -1
  73. package/dist/harness/kilocode.js.map +1 -1
  74. package/dist/harness/opencode.d.ts.map +1 -1
  75. package/dist/harness/opencode.js +10 -0
  76. package/dist/harness/opencode.js.map +1 -1
  77. package/dist/harness/profile.d.ts +23 -2
  78. package/dist/harness/profile.d.ts.map +1 -1
  79. package/dist/harness/profile.js.map +1 -1
  80. package/dist/harness/snippet.d.ts +14 -1
  81. package/dist/harness/snippet.d.ts.map +1 -1
  82. package/dist/harness/snippet.js +18 -2
  83. package/dist/harness/snippet.js.map +1 -1
  84. package/dist/index.d.ts +12 -0
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +9 -0
  87. package/dist/index.js.map +1 -1
  88. package/dist/setup/recipe.d.ts +73 -0
  89. package/dist/setup/recipe.d.ts.map +1 -0
  90. package/dist/setup/recipe.js +76 -0
  91. package/dist/setup/recipe.js.map +1 -0
  92. package/dist/text/json-edit.d.ts +15 -1
  93. package/dist/text/json-edit.d.ts.map +1 -1
  94. package/dist/text/json-edit.js +76 -5
  95. package/dist/text/json-edit.js.map +1 -1
  96. package/package.json +1 -1
@@ -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":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAkC1C;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACvB,KAAmB,EACnB,MAA8B,EAC9B,UAAkB;IAElB,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,GAAG,GAAQ,KAAK,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,CAAC,MAAM,CAAC,CAAC;QACf,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,IAAI,aAAa,CAAC,UAAU,CAAC,CAAC;QAC5D,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC,CAAC;IACF,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAsB,EACtB,GAAQ,EACR,GAA2B,EAC3B,OAAO,GAAG,CAAC;IAEX,IAAI,KAAK,GAAG,OAAO,CAAC;IACpB,OAAO,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACvB,IAAI,KAAK,KAAK,CAAC;gBAAE,GAAG,CAAC,wDAAwD,CAAC,CAAC;;gBAC1E,KAAK,IAAI,CAAC,CAAC;QAClB,CAAC;aAAM,CAAC;YACN,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAQ,EAAE,SAAiB;IAC3D,SAAS,CAAC;QACR,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,6BAA6B,CAAC,CAAC;QACxD,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,MAAM,CAAC;QACrC,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QACjC,IAAI,MAAM,KAAK,KAAK;YAAE,OAAO,KAAK,CAAC;QACnC,MAAM,GAAG,CAAC,GAAG,SAAS,IAAI,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAED,yFAAyF;AACzF,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAQ,EAAE,SAAiB;IAC5D,SAAS,CAAC;QACR,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,sBAAsB,CAAC,CAAC;QACjD,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QACjC,IAAI,MAAM,KAAK,KAAK;YAAE,OAAO,KAAK,CAAC;QACnC,MAAM,GAAG,CAAC,GAAG,SAAS,IAAI,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAkBD,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAC9B,GAA2B,EAC3B,KAAiC,EACjC,OAAmC,EACnC,IAAuC;IAEvC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrD,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,cAAc,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,IAAqB;IACpD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;AAC/D,CAAC","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,qBAkCnB,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 ],\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"]}
@@ -65,6 +65,7 @@ export const codex = {
65
65
  instructions: 'snippet',
66
66
  caveats: [
67
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',
68
69
  ],
69
70
  hooks: HOOKS,
70
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,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;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 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 ],\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":"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,qBAqBlB,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: ['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"]}
@@ -31,7 +31,10 @@ export const grok = {
31
31
  detectHome: ['.grok'],
32
32
  instructionFile: 'AGENTS.md',
33
33
  instructions: 'snippet',
34
- caveats: ['deny-only hooks: input rewrite is not supported, so rewrite mode falls back to deny'],
34
+ caveats: [
35
+ 'deny-only hooks: input rewrite is not supported, so rewrite mode falls back to deny',
36
+ 'MCP registration is manual for Grok (TOML) — packages/mcp/README.md has the snippet; setup does not edit TOML',
37
+ ],
35
38
  hooks: HOOKS,
36
39
  install: [
37
40
  {
@@ -1 +1 @@
1
- {"version":3,"file":"grok.js","sourceRoot":"","sources":["../../src/harness/grok.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,KAAK,GAAsB;IAC/B,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,UAAU,CAAC;IAC5C,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,mBAAmB,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;CACjD,CAAC;AAEF,MAAM,CAAC,MAAM,IAAI,GAA0B;IACzC,EAAE,EAAE,MAAM;IACV,IAAI,EAAE,UAAU;IAChB,SAAS,EAAE,MAAM;IACjB,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,CAAC,OAAO,CAAC;IACjB,UAAU,EAAE,CAAC,OAAO,CAAC;IACrB,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE,CAAC,qFAAqF,CAAC;IAChG,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE;QACP;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,kBAAkB;YACxB,KAAK,EAAE,YAAY;YACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;YAC1B,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 * 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: ['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.js","sourceRoot":"","sources":["../../src/harness/grok.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,KAAK,GAAsB;IAC/B,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,UAAU,CAAC;IAC5C,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,mBAAmB,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;CACjD,CAAC;AAEF,MAAM,CAAC,MAAM,IAAI,GAA0B;IACzC,EAAE,EAAE,MAAM;IACV,IAAI,EAAE,UAAU;IAChB,SAAS,EAAE,MAAM;IACjB,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,CAAC,OAAO,CAAC;IACjB,UAAU,EAAE,CAAC,OAAO,CAAC;IACrB,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE;QACP,qFAAqF;QACrF,+GAA+G;KAChH;IACD,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE;QACP;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,kBAAkB;YACxB,KAAK,EAAE,YAAY;YACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;YAC1B,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 * 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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"kilocode.d.ts","sourceRoot":"","sources":["../../src/harness/kilocode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAE,MAAM,cAAc,CAAC;AAwB1E,eAAO,MAAM,QAAQ,EAAE,cAYtB,CAAC","sourcesContent":["import type { HarnessInstallContext, HarnessProfile } from './profile.ts';\nimport { instructionSnippet } from './snippet.ts';\n\n/**\n * KiloCode — ADVISORY tier. Instructions only, and honest about it.\n *\n * No first-class hook API (Kilo-Org/kilocode#5827), so nothing here is enforced: what\n * ships is a rules file this harness owns whole — the shared snippet plus the two\n * manual enforcement legs the snippet has no room for. `remove` deletes it.\n */\nfunction kilocodeRulesSource(ctx: HarnessInstallContext): string {\n return `${instructionSnippet(ctx.thresholdBytes, ctx.budgetBytes)}\n<!-- smelt:hooks v1 advisory notes -->\n\nKiloCode has no first-class hook API (Kilo-Org/kilocode#5827), so nothing above is\nenforced — it is advisory. Two manual legs make it harder to bypass:\n\n1. Permissions: in your KiloCode per-tool permission config, set raw-read/execute\n tools to \"ask\" so oversized reads surface for review instead of passing silently.\n2. MCP: expose smelt through an MCP server and prefer its tools; a resident server\n also keeps smelt's grammar cache warm across calls.\n`;\n}\n\nexport const kilocode: HarnessProfile = {\n id: 'kilocode',\n name: 'KiloCode',\n tier: 'advisory',\n detect: ['.kilocode'],\n detectHome: ['.config/kilo'],\n instructionFile: '.kilocode/rules/smelt.md',\n instructions: kilocodeRulesSource,\n caveats: [\n 'no first-class hooks (Kilo-Org/kilocode#5827): enforcement is permissions config + MCP, both manual',\n ],\n install: [],\n};\n"]}
1
+ {"version":3,"file":"kilocode.d.ts","sourceRoot":"","sources":["../../src/harness/kilocode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAE,MAAM,cAAc,CAAC;AAwB1E,eAAO,MAAM,QAAQ,EAAE,cAYtB,CAAC","sourcesContent":["import type { HarnessInstallContext, HarnessProfile } from './profile.ts';\nimport { instructionSnippet } from './snippet.ts';\n\n/**\n * KiloCode — ADVISORY tier. Instructions only, and honest about it.\n *\n * No first-class hook API (Kilo-Org/kilocode#5827), so nothing here is enforced: what\n * ships is a rules file this harness owns whole — the shared snippet plus the two\n * manual enforcement legs the snippet has no room for. `remove` deletes it.\n */\nfunction kilocodeRulesSource(ctx: HarnessInstallContext): string {\n return `${instructionSnippet(ctx.thresholdBytes, ctx.budgetBytes, ctx.writtenBy)}\n<!-- smelt:hooks v1 advisory notes -->\n\nKiloCode has no first-class hook API (Kilo-Org/kilocode#5827), so nothing above is\nenforced — it is advisory. Two manual legs make it harder to bypass:\n\n1. Permissions: in your KiloCode per-tool permission config, set raw-read/execute\n tools to \"ask\" so oversized reads surface for review instead of passing silently.\n2. MCP: expose smelt through an MCP server and prefer its tools; a resident server\n also keeps smelt's grammar cache warm across calls.\n`;\n}\n\nexport const kilocode: HarnessProfile = {\n id: 'kilocode',\n name: 'KiloCode',\n tier: 'advisory',\n detect: ['.kilocode'],\n detectHome: ['.config/kilo'],\n instructionFile: '.kilocode/rules/smelt.md',\n instructions: kilocodeRulesSource,\n caveats: [\n 'no first-class hooks (Kilo-Org/kilocode#5827): enforcement is permissions config + MCP, both manual',\n ],\n install: [],\n};\n"]}
@@ -7,7 +7,7 @@ import { instructionSnippet } from './snippet.js';
7
7
  * manual enforcement legs the snippet has no room for. `remove` deletes it.
8
8
  */
9
9
  function kilocodeRulesSource(ctx) {
10
- return `${instructionSnippet(ctx.thresholdBytes, ctx.budgetBytes)}
10
+ return `${instructionSnippet(ctx.thresholdBytes, ctx.budgetBytes, ctx.writtenBy)}
11
11
  <!-- smelt:hooks v1 advisory notes -->
12
12
 
13
13
  KiloCode has no first-class hook API (Kilo-Org/kilocode#5827), so nothing above is
@@ -1 +1 @@
1
- {"version":3,"file":"kilocode.js","sourceRoot":"","sources":["../../src/harness/kilocode.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,GAA0B;IACrD,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,cAAc,EAAE,GAAG,CAAC,WAAW,CAAC;;;;;;;;;;CAUlE,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAmB;IACtC,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,CAAC,WAAW,CAAC;IACrB,UAAU,EAAE,CAAC,cAAc,CAAC;IAC5B,eAAe,EAAE,0BAA0B;IAC3C,YAAY,EAAE,mBAAmB;IACjC,OAAO,EAAE;QACP,qGAAqG;KACtG;IACD,OAAO,EAAE,EAAE;CACZ,CAAC","sourcesContent":["import type { HarnessInstallContext, HarnessProfile } from './profile.ts';\nimport { instructionSnippet } from './snippet.ts';\n\n/**\n * KiloCode — ADVISORY tier. Instructions only, and honest about it.\n *\n * No first-class hook API (Kilo-Org/kilocode#5827), so nothing here is enforced: what\n * ships is a rules file this harness owns whole — the shared snippet plus the two\n * manual enforcement legs the snippet has no room for. `remove` deletes it.\n */\nfunction kilocodeRulesSource(ctx: HarnessInstallContext): string {\n return `${instructionSnippet(ctx.thresholdBytes, ctx.budgetBytes)}\n<!-- smelt:hooks v1 advisory notes -->\n\nKiloCode has no first-class hook API (Kilo-Org/kilocode#5827), so nothing above is\nenforced — it is advisory. Two manual legs make it harder to bypass:\n\n1. Permissions: in your KiloCode per-tool permission config, set raw-read/execute\n tools to \"ask\" so oversized reads surface for review instead of passing silently.\n2. MCP: expose smelt through an MCP server and prefer its tools; a resident server\n also keeps smelt's grammar cache warm across calls.\n`;\n}\n\nexport const kilocode: HarnessProfile = {\n id: 'kilocode',\n name: 'KiloCode',\n tier: 'advisory',\n detect: ['.kilocode'],\n detectHome: ['.config/kilo'],\n instructionFile: '.kilocode/rules/smelt.md',\n instructions: kilocodeRulesSource,\n caveats: [\n 'no first-class hooks (Kilo-Org/kilocode#5827): enforcement is permissions config + MCP, both manual',\n ],\n install: [],\n};\n"]}
1
+ {"version":3,"file":"kilocode.js","sourceRoot":"","sources":["../../src/harness/kilocode.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,GAA0B;IACrD,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,cAAc,EAAE,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,SAAS,CAAC;;;;;;;;;;CAUjF,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAmB;IACtC,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,CAAC,WAAW,CAAC;IACrB,UAAU,EAAE,CAAC,cAAc,CAAC;IAC5B,eAAe,EAAE,0BAA0B;IAC3C,YAAY,EAAE,mBAAmB;IACjC,OAAO,EAAE;QACP,qGAAqG;KACtG;IACD,OAAO,EAAE,EAAE;CACZ,CAAC","sourcesContent":["import type { HarnessInstallContext, HarnessProfile } from './profile.ts';\nimport { instructionSnippet } from './snippet.ts';\n\n/**\n * KiloCode — ADVISORY tier. Instructions only, and honest about it.\n *\n * No first-class hook API (Kilo-Org/kilocode#5827), so nothing here is enforced: what\n * ships is a rules file this harness owns whole — the shared snippet plus the two\n * manual enforcement legs the snippet has no room for. `remove` deletes it.\n */\nfunction kilocodeRulesSource(ctx: HarnessInstallContext): string {\n return `${instructionSnippet(ctx.thresholdBytes, ctx.budgetBytes, ctx.writtenBy)}\n<!-- smelt:hooks v1 advisory notes -->\n\nKiloCode has no first-class hook API (Kilo-Org/kilocode#5827), so nothing above is\nenforced — it is advisory. Two manual legs make it harder to bypass:\n\n1. Permissions: in your KiloCode per-tool permission config, set raw-read/execute\n tools to \"ask\" so oversized reads surface for review instead of passing silently.\n2. MCP: expose smelt through an MCP server and prefer its tools; a resident server\n also keeps smelt's grammar cache warm across calls.\n`;\n}\n\nexport const kilocode: HarnessProfile = {\n id: 'kilocode',\n name: 'KiloCode',\n tier: 'advisory',\n detect: ['.kilocode'],\n detectHome: ['.config/kilo'],\n instructionFile: '.kilocode/rules/smelt.md',\n instructions: kilocodeRulesSource,\n caveats: [\n 'no first-class hooks (Kilo-Org/kilocode#5827): enforcement is permissions config + MCP, both manual',\n ],\n install: [],\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../src/harness/opencode.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAyB,cAAc,EAAE,MAAM,cAAc,CAAC;AA6E1E,eAAO,MAAM,QAAQ,EAAE,cAmBtB,CAAC","sourcesContent":["import {\n DENIED_WITHOUT_REASON,\n REWRITE_ANNOUNCEMENT_JOIN,\n REWRITE_ANNOUNCEMENT_OPENING,\n} from '../hooks/shim.ts';\n\nimport { guardCoreScriptPath, portablePath } from './paths.ts';\nimport type { HarnessInstallContext, HarnessProfile } from './profile.ts';\n\n/**\n * opencode — EXPERIMENTAL tier. Install and removal, end to end.\n *\n * The one harness with no shim script: its hook API is a JavaScript plugin, not a\n * stdin schema, so what the installer writes *is* the adapter. The plugin imports the\n * built guard core directly (the same module every shim calls) and maps\n * `tool.execute.before` onto it, so all policy still lives in exactly one place.\n *\n * Its two announcements — the rewrite sentence and the reasonless deny — are spliced\n * in from `hooks/shim.ts`'s constants rather than hand-typed into the template, where\n * nothing could see them drift from what the shims print.\n */\nfunction opencodePluginSource(ctx: HarnessInstallContext): string {\n const guardCore = portablePath(ctx.cwd, guardCoreScriptPath());\n return `// smelt:hooks v1 — opencode plugin shim. EXPERIMENTAL tier: mapped from the\n// capability matrix (docs/research/2026-09-02-harness-capability-matrix.md, opencode\n// row; https://opencode.ai/docs/plugins/). This template's deny/pass/window paths\n// were exercised directly against the built guard core (verified\n// 2026-09-02), but a live opencode session has not been smoke-tested — that needs\n// provider credentials. Caveat carried from the matrix: MCP tools can bypass plugin\n// hooks (sst/opencode#2319) — this guard sees built-in tools only.\n//\n// Thin adapter: maps tool.execute.before onto the smelt guard core (zero\n// dependencies), which owns every decision. Deny mode throws (opencode surfaces the\n// reason to the model); rewrite mode substitutes the faithful replacement command —\n// announced on stderr, because the plugin API has no reason channel on a rewrite\n// and a substitution must never be silent. Both announcements below are spliced in\n// from the shims' own constants, so this copy cannot drift from theirs.\nimport { pathToFileURL } from 'node:url';\n\nconst GUARD_CORE = ${JSON.stringify(guardCore)};\nconst core = await import(pathToFileURL(GUARD_CORE).href);\n\nexport const SmeltGuard = async () => ({\n 'tool.execute.before': async (input, output) => {\n const tool = input?.tool;\n const args = output?.args ?? {};\n let request;\n if (tool === 'read' && typeof args.filePath === 'string') {\n request = {\n tool: 'Read',\n input: {\n path: args.filePath,\n offsetLimited: args.offset !== undefined || args.limit !== undefined,\n },\n };\n } else if (tool === 'bash' && typeof args.command === 'string') {\n request = { tool: 'Bash', input: { command: args.command } };\n } else {\n return;\n }\n const warn = (text) => process.stderr.write(text + '\\\\n');\n const settings = core.readGuardSettings(process.cwd(), warn);\n const decision = core.decide(request, settings, process.cwd());\n if (decision.action !== 'deny') return;\n if (\n settings.enforcement === 'rewrite' &&\n request.tool === 'Bash' &&\n decision.suggestion !== undefined\n ) {\n warn(\n ${JSON.stringify(REWRITE_ANNOUNCEMENT_OPENING)} +\n decision.suggestion +\n ${JSON.stringify(REWRITE_ANNOUNCEMENT_JOIN)} +\n (decision.reason ?? ''),\n );\n output.args.command = decision.suggestion;\n return;\n }\n throw new Error(decision.reason ?? ${JSON.stringify(DENIED_WITHOUT_REASON)});\n },\n});\n`;\n}\n\nexport const opencode: HarnessProfile = {\n id: 'opencode',\n name: 'opencode',\n tier: 'experimental',\n detect: ['.opencode', 'opencode.json'],\n detectHome: ['.config/opencode'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'MCP tools can bypass opencode plugin hooks (sst/opencode#2319) — the guard sees built-in tools only',\n ],\n install: [\n {\n kind: 'own-file',\n file: '.opencode/plugin/smelt-guard.js',\n content: opencodePluginSource,\n guardOnly: true,\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../src/harness/opencode.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAyB,cAAc,EAAE,MAAM,cAAc,CAAC;AAiF1E,eAAO,MAAM,QAAQ,EAAE,cAyBtB,CAAC","sourcesContent":["import {\n DENIED_WITHOUT_REASON,\n REWRITE_ANNOUNCEMENT_JOIN,\n REWRITE_ANNOUNCEMENT_OPENING,\n} from '../hooks/shim.ts';\n\nimport { MCP_RUN_ARGS } from '../setup/recipe.ts';\nimport { guardCoreScriptPath, portablePath } from './paths.ts';\nimport type { HarnessInstallContext, HarnessProfile } from './profile.ts';\n\n/**\n * opencode — EXPERIMENTAL tier. Install and removal, end to end.\n *\n * The one harness with no shim script: its hook API is a JavaScript plugin, not a\n * stdin schema, so what the installer writes *is* the adapter. The plugin imports the\n * built guard core directly (the same module every shim calls) and maps\n * `tool.execute.before` onto it, so all policy still lives in exactly one place.\n *\n * Its two announcements — the rewrite sentence and the reasonless deny — are spliced\n * in from `hooks/shim.ts`'s constants rather than hand-typed into the template, where\n * nothing could see them drift from what the shims print.\n */\nfunction opencodePluginSource(ctx: HarnessInstallContext): string {\n const guardCore = portablePath(ctx.cwd, guardCoreScriptPath());\n return `// smelt:hooks v1 — opencode plugin shim. EXPERIMENTAL tier: mapped from the\n// capability matrix (docs/research/2026-09-02-harness-capability-matrix.md, opencode\n// row; https://opencode.ai/docs/plugins/). This template's deny/pass/window paths\n// were exercised directly against the built guard core (verified\n// 2026-09-02), but a live opencode session has not been smoke-tested — that needs\n// provider credentials. Caveat carried from the matrix: MCP tools can bypass plugin\n// hooks (sst/opencode#2319) — this guard sees built-in tools only.\n//\n// Thin adapter: maps tool.execute.before onto the smelt guard core (zero\n// dependencies), which owns every decision. Deny mode throws (opencode surfaces the\n// reason to the model); rewrite mode substitutes the faithful replacement command —\n// announced on stderr, because the plugin API has no reason channel on a rewrite\n// and a substitution must never be silent. Both announcements below are spliced in\n// from the shims' own constants, so this copy cannot drift from theirs.\nimport { pathToFileURL } from 'node:url';\n\nconst GUARD_CORE = ${JSON.stringify(guardCore)};\nconst core = await import(pathToFileURL(GUARD_CORE).href);\n\nexport const SmeltGuard = async () => ({\n 'tool.execute.before': async (input, output) => {\n const tool = input?.tool;\n const args = output?.args ?? {};\n let request;\n if (tool === 'read' && typeof args.filePath === 'string') {\n request = {\n tool: 'Read',\n input: {\n path: args.filePath,\n offsetLimited: args.offset !== undefined || args.limit !== undefined,\n },\n };\n } else if (tool === 'bash' && typeof args.command === 'string') {\n request = { tool: 'Bash', input: { command: args.command } };\n } else {\n return;\n }\n const warn = (text) => process.stderr.write(text + '\\\\n');\n const settings = core.readGuardSettings(process.cwd(), warn);\n const decision = core.decide(request, settings, process.cwd());\n if (decision.action !== 'deny') return;\n if (\n settings.enforcement === 'rewrite' &&\n request.tool === 'Bash' &&\n decision.suggestion !== undefined\n ) {\n warn(\n ${JSON.stringify(REWRITE_ANNOUNCEMENT_OPENING)} +\n decision.suggestion +\n ${JSON.stringify(REWRITE_ANNOUNCEMENT_JOIN)} +\n (decision.reason ?? ''),\n );\n output.args.command = decision.suggestion;\n return;\n }\n throw new Error(decision.reason ?? ${JSON.stringify(DENIED_WITHOUT_REASON)});\n },\n});\n`;\n}\n\n// Same derivation as claude-code's, from the one place it lives (MCP_RUN_ARGS):\n// the recipe's run command in opencode's spawn shape (a single `command` array,\n// `type: \"local\"`).\n\nexport const opencode: HarnessProfile = {\n id: 'opencode',\n name: 'opencode',\n tier: 'experimental',\n detect: ['.opencode', 'opencode.json'],\n detectHome: ['.config/opencode'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'MCP tools can bypass opencode plugin hooks (sst/opencode#2319) — the guard sees built-in tools only',\n ],\n install: [\n {\n kind: 'own-file',\n file: '.opencode/plugin/smelt-guard.js',\n content: opencodePluginSource,\n guardOnly: true,\n },\n {\n kind: 'mcp-registration',\n file: 'opencode.json',\n path: ['mcp', 'smelt'],\n entry: () => ({ type: 'local', command: [...MCP_RUN_ARGS] }),\n },\n ],\n};\n"]}
@@ -1,4 +1,5 @@
1
1
  import { DENIED_WITHOUT_REASON, REWRITE_ANNOUNCEMENT_JOIN, REWRITE_ANNOUNCEMENT_OPENING, } from '../hooks/shim.js';
2
+ import { MCP_RUN_ARGS } from '../setup/recipe.js';
2
3
  import { guardCoreScriptPath, portablePath } from './paths.js';
3
4
  /**
4
5
  * opencode — EXPERIMENTAL tier. Install and removal, end to end.
@@ -74,6 +75,9 @@ export const SmeltGuard = async () => ({
74
75
  });
75
76
  `;
76
77
  }
78
+ // Same derivation as claude-code's, from the one place it lives (MCP_RUN_ARGS):
79
+ // the recipe's run command in opencode's spawn shape (a single `command` array,
80
+ // `type: "local"`).
77
81
  export const opencode = {
78
82
  id: 'opencode',
79
83
  name: 'opencode',
@@ -92,6 +96,12 @@ export const opencode = {
92
96
  content: opencodePluginSource,
93
97
  guardOnly: true,
94
98
  },
99
+ {
100
+ kind: 'mcp-registration',
101
+ file: 'opencode.json',
102
+ path: ['mcp', 'smelt'],
103
+ entry: () => ({ type: 'local', command: [...MCP_RUN_ARGS] }),
104
+ },
95
105
  ],
96
106
  };
97
107
  //# sourceMappingURL=opencode.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../../src/harness/opencode.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/D;;;;;;;;;;;GAWG;AACH,SAAS,oBAAoB,CAAC,GAA0B;IACtD,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,mBAAmB,EAAE,CAAC,CAAC;IAC/D,OAAO;;;;;;;;;;;;;;;;qBAgBY,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA+BpC,IAAI,CAAC,SAAS,CAAC,4BAA4B,CAAC;;YAE1C,IAAI,CAAC,SAAS,CAAC,yBAAyB,CAAC;;;;;;yCAMZ,IAAI,CAAC,SAAS,CAAC,qBAAqB,CAAC;;;CAG7E,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAmB;IACtC,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,CAAC,WAAW,EAAE,eAAe,CAAC;IACtC,UAAU,EAAE,CAAC,kBAAkB,CAAC;IAChC,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE;QACP,qGAAqG;KACtG;IACD,OAAO,EAAE;QACP;YACE,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,iCAAiC;YACvC,OAAO,EAAE,oBAAoB;YAC7B,SAAS,EAAE,IAAI;SAChB;KACF;CACF,CAAC","sourcesContent":["import {\n DENIED_WITHOUT_REASON,\n REWRITE_ANNOUNCEMENT_JOIN,\n REWRITE_ANNOUNCEMENT_OPENING,\n} from '../hooks/shim.ts';\n\nimport { guardCoreScriptPath, portablePath } from './paths.ts';\nimport type { HarnessInstallContext, HarnessProfile } from './profile.ts';\n\n/**\n * opencode — EXPERIMENTAL tier. Install and removal, end to end.\n *\n * The one harness with no shim script: its hook API is a JavaScript plugin, not a\n * stdin schema, so what the installer writes *is* the adapter. The plugin imports the\n * built guard core directly (the same module every shim calls) and maps\n * `tool.execute.before` onto it, so all policy still lives in exactly one place.\n *\n * Its two announcements — the rewrite sentence and the reasonless deny — are spliced\n * in from `hooks/shim.ts`'s constants rather than hand-typed into the template, where\n * nothing could see them drift from what the shims print.\n */\nfunction opencodePluginSource(ctx: HarnessInstallContext): string {\n const guardCore = portablePath(ctx.cwd, guardCoreScriptPath());\n return `// smelt:hooks v1 — opencode plugin shim. EXPERIMENTAL tier: mapped from the\n// capability matrix (docs/research/2026-09-02-harness-capability-matrix.md, opencode\n// row; https://opencode.ai/docs/plugins/). This template's deny/pass/window paths\n// were exercised directly against the built guard core (verified\n// 2026-09-02), but a live opencode session has not been smoke-tested — that needs\n// provider credentials. Caveat carried from the matrix: MCP tools can bypass plugin\n// hooks (sst/opencode#2319) — this guard sees built-in tools only.\n//\n// Thin adapter: maps tool.execute.before onto the smelt guard core (zero\n// dependencies), which owns every decision. Deny mode throws (opencode surfaces the\n// reason to the model); rewrite mode substitutes the faithful replacement command —\n// announced on stderr, because the plugin API has no reason channel on a rewrite\n// and a substitution must never be silent. Both announcements below are spliced in\n// from the shims' own constants, so this copy cannot drift from theirs.\nimport { pathToFileURL } from 'node:url';\n\nconst GUARD_CORE = ${JSON.stringify(guardCore)};\nconst core = await import(pathToFileURL(GUARD_CORE).href);\n\nexport const SmeltGuard = async () => ({\n 'tool.execute.before': async (input, output) => {\n const tool = input?.tool;\n const args = output?.args ?? {};\n let request;\n if (tool === 'read' && typeof args.filePath === 'string') {\n request = {\n tool: 'Read',\n input: {\n path: args.filePath,\n offsetLimited: args.offset !== undefined || args.limit !== undefined,\n },\n };\n } else if (tool === 'bash' && typeof args.command === 'string') {\n request = { tool: 'Bash', input: { command: args.command } };\n } else {\n return;\n }\n const warn = (text) => process.stderr.write(text + '\\\\n');\n const settings = core.readGuardSettings(process.cwd(), warn);\n const decision = core.decide(request, settings, process.cwd());\n if (decision.action !== 'deny') return;\n if (\n settings.enforcement === 'rewrite' &&\n request.tool === 'Bash' &&\n decision.suggestion !== undefined\n ) {\n warn(\n ${JSON.stringify(REWRITE_ANNOUNCEMENT_OPENING)} +\n decision.suggestion +\n ${JSON.stringify(REWRITE_ANNOUNCEMENT_JOIN)} +\n (decision.reason ?? ''),\n );\n output.args.command = decision.suggestion;\n return;\n }\n throw new Error(decision.reason ?? ${JSON.stringify(DENIED_WITHOUT_REASON)});\n },\n});\n`;\n}\n\nexport const opencode: HarnessProfile = {\n id: 'opencode',\n name: 'opencode',\n tier: 'experimental',\n detect: ['.opencode', 'opencode.json'],\n detectHome: ['.config/opencode'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'MCP tools can bypass opencode plugin hooks (sst/opencode#2319) — the guard sees built-in tools only',\n ],\n install: [\n {\n kind: 'own-file',\n file: '.opencode/plugin/smelt-guard.js',\n content: opencodePluginSource,\n guardOnly: true,\n },\n ],\n};\n"]}
1
+ {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../../src/harness/opencode.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/D;;;;;;;;;;;GAWG;AACH,SAAS,oBAAoB,CAAC,GAA0B;IACtD,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,mBAAmB,EAAE,CAAC,CAAC;IAC/D,OAAO;;;;;;;;;;;;;;;;qBAgBY,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA+BpC,IAAI,CAAC,SAAS,CAAC,4BAA4B,CAAC;;YAE1C,IAAI,CAAC,SAAS,CAAC,yBAAyB,CAAC;;;;;;yCAMZ,IAAI,CAAC,SAAS,CAAC,qBAAqB,CAAC;;;CAG7E,CAAC;AACF,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,oBAAoB;AAEpB,MAAM,CAAC,MAAM,QAAQ,GAAmB;IACtC,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,CAAC,WAAW,EAAE,eAAe,CAAC;IACtC,UAAU,EAAE,CAAC,kBAAkB,CAAC;IAChC,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE;QACP,qGAAqG;KACtG;IACD,OAAO,EAAE;QACP;YACE,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,iCAAiC;YACvC,OAAO,EAAE,oBAAoB;YAC7B,SAAS,EAAE,IAAI;SAChB;QACD;YACE,IAAI,EAAE,kBAAkB;YACxB,IAAI,EAAE,eAAe;YACrB,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC;YACtB,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,YAAY,CAAC,EAAE,CAAC;SAC7D;KACF;CACF,CAAC","sourcesContent":["import {\n DENIED_WITHOUT_REASON,\n REWRITE_ANNOUNCEMENT_JOIN,\n REWRITE_ANNOUNCEMENT_OPENING,\n} from '../hooks/shim.ts';\n\nimport { MCP_RUN_ARGS } from '../setup/recipe.ts';\nimport { guardCoreScriptPath, portablePath } from './paths.ts';\nimport type { HarnessInstallContext, HarnessProfile } from './profile.ts';\n\n/**\n * opencode — EXPERIMENTAL tier. Install and removal, end to end.\n *\n * The one harness with no shim script: its hook API is a JavaScript plugin, not a\n * stdin schema, so what the installer writes *is* the adapter. The plugin imports the\n * built guard core directly (the same module every shim calls) and maps\n * `tool.execute.before` onto it, so all policy still lives in exactly one place.\n *\n * Its two announcements — the rewrite sentence and the reasonless deny — are spliced\n * in from `hooks/shim.ts`'s constants rather than hand-typed into the template, where\n * nothing could see them drift from what the shims print.\n */\nfunction opencodePluginSource(ctx: HarnessInstallContext): string {\n const guardCore = portablePath(ctx.cwd, guardCoreScriptPath());\n return `// smelt:hooks v1 — opencode plugin shim. EXPERIMENTAL tier: mapped from the\n// capability matrix (docs/research/2026-09-02-harness-capability-matrix.md, opencode\n// row; https://opencode.ai/docs/plugins/). This template's deny/pass/window paths\n// were exercised directly against the built guard core (verified\n// 2026-09-02), but a live opencode session has not been smoke-tested — that needs\n// provider credentials. Caveat carried from the matrix: MCP tools can bypass plugin\n// hooks (sst/opencode#2319) — this guard sees built-in tools only.\n//\n// Thin adapter: maps tool.execute.before onto the smelt guard core (zero\n// dependencies), which owns every decision. Deny mode throws (opencode surfaces the\n// reason to the model); rewrite mode substitutes the faithful replacement command —\n// announced on stderr, because the plugin API has no reason channel on a rewrite\n// and a substitution must never be silent. Both announcements below are spliced in\n// from the shims' own constants, so this copy cannot drift from theirs.\nimport { pathToFileURL } from 'node:url';\n\nconst GUARD_CORE = ${JSON.stringify(guardCore)};\nconst core = await import(pathToFileURL(GUARD_CORE).href);\n\nexport const SmeltGuard = async () => ({\n 'tool.execute.before': async (input, output) => {\n const tool = input?.tool;\n const args = output?.args ?? {};\n let request;\n if (tool === 'read' && typeof args.filePath === 'string') {\n request = {\n tool: 'Read',\n input: {\n path: args.filePath,\n offsetLimited: args.offset !== undefined || args.limit !== undefined,\n },\n };\n } else if (tool === 'bash' && typeof args.command === 'string') {\n request = { tool: 'Bash', input: { command: args.command } };\n } else {\n return;\n }\n const warn = (text) => process.stderr.write(text + '\\\\n');\n const settings = core.readGuardSettings(process.cwd(), warn);\n const decision = core.decide(request, settings, process.cwd());\n if (decision.action !== 'deny') return;\n if (\n settings.enforcement === 'rewrite' &&\n request.tool === 'Bash' &&\n decision.suggestion !== undefined\n ) {\n warn(\n ${JSON.stringify(REWRITE_ANNOUNCEMENT_OPENING)} +\n decision.suggestion +\n ${JSON.stringify(REWRITE_ANNOUNCEMENT_JOIN)} +\n (decision.reason ?? ''),\n );\n output.args.command = decision.suggestion;\n return;\n }\n throw new Error(decision.reason ?? ${JSON.stringify(DENIED_WITHOUT_REASON)});\n },\n});\n`;\n}\n\n// Same derivation as claude-code's, from the one place it lives (MCP_RUN_ARGS):\n// the recipe's run command in opencode's spawn shape (a single `command` array,\n// `type: \"local\"`).\n\nexport const opencode: HarnessProfile = {\n id: 'opencode',\n name: 'opencode',\n tier: 'experimental',\n detect: ['.opencode', 'opencode.json'],\n detectHome: ['.config/opencode'],\n instructionFile: 'AGENTS.md',\n instructions: 'snippet',\n caveats: [\n 'MCP tools can bypass opencode plugin hooks (sst/opencode#2319) — the guard sees built-in tools only',\n ],\n install: [\n {\n kind: 'own-file',\n file: '.opencode/plugin/smelt-guard.js',\n content: opencodePluginSource,\n guardOnly: true,\n },\n {\n kind: 'mcp-registration',\n file: 'opencode.json',\n path: ['mcp', 'smelt'],\n entry: () => ({ type: 'local', command: [...MCP_RUN_ARGS] }),\n },\n ],\n};\n"]}
@@ -111,6 +111,8 @@ export declare function harnessLabel(profile: HarnessProfile): string;
111
111
  export interface HarnessInstallContext {
112
112
  /** Project directory: every path a renderer emits is portable relative to it. */
113
113
  readonly cwd: string;
114
+ /** The release writing these bytes — stamped into shared blocks for `smelt doctor`. */
115
+ readonly writtenBy?: string;
114
116
  readonly guard: boolean;
115
117
  readonly statsOnStop: boolean;
116
118
  readonly mapOnStart: boolean;
@@ -125,9 +127,10 @@ export type HarnessFileContent = (ctx: HarnessInstallContext) => string;
125
127
  /**
126
128
  * One artefact `hooks install` writes. The kind is also the un-write: `json-hooks` is
127
129
  * merged in and strip-merged out, `marker-block` is upserted and stripped,
128
- * `own-file` is written and deleted.
130
+ * `own-file` is written and deleted, `mcp-registration` is nested-merged in and
131
+ * lifted back out.
129
132
  */
130
- export type HarnessInstallStep = HarnessJsonHooks | HarnessMarkerBlock | HarnessOwnFile;
133
+ export type HarnessInstallStep = HarnessJsonHooks | HarnessMarkerBlock | HarnessOwnFile | HarnessMcpRegistration;
131
134
  /**
132
135
  * A JSON settings/hooks file the harness reads. Our entries are merged in
133
136
  * byte-faithfully and, on `remove`, stripped back out with everything foreign left
@@ -190,6 +193,24 @@ export interface HarnessOwnFile {
190
193
  /** True when the file exists only to wire the guard — the guard toggle gates it. */
191
194
  readonly guardOnly: boolean;
192
195
  }
196
+ /**
197
+ * An MCP server registration inside a JSON config the harness reads — Claude Code's
198
+ * `.mcp.json` (`mcpServers.smelt`), opencode's `opencode.json` (`mcp.smelt`). The
199
+ * entry is merged in byte-faithfully beside any other servers the user registered,
200
+ * and on `remove` it is lifted back out; a container this install created empty is
201
+ * removed with it, so a file that never carried the key round-trips byte-identical.
202
+ * Harnesses whose registration is TOML (Codex, Grok) do not declare this step — a
203
+ * hand-edit into TOML is exactly the edit `text/json-edit.ts` exists to refuse.
204
+ */
205
+ export interface HarnessMcpRegistration {
206
+ readonly kind: 'mcp-registration';
207
+ /** Project-relative path of the config file. */
208
+ readonly file: string;
209
+ /** The container key, then the server's name: `['mcpServers', 'smelt']`. */
210
+ readonly path: readonly [string, string];
211
+ /** The server entry as a JSON value — the bytes are the editor's. */
212
+ readonly entry: (ctx: HarnessInstallContext) => unknown;
213
+ }
193
214
  /**
194
215
  * A profile that ships a shim script. Only these have a shim path: `shimScriptPath`
195
216
  * takes one, so a harness with no shim cannot name a script that was never built.