@smeltjs/core 0.2.0 → 0.4.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.
- package/README.md +3 -3
- package/dist/agents/guide.d.ts +68 -0
- package/dist/agents/guide.d.ts.map +1 -0
- package/dist/agents/guide.js +72 -0
- package/dist/agents/guide.js.map +1 -0
- package/dist/agents/instructions.d.ts +133 -0
- package/dist/agents/instructions.d.ts.map +1 -0
- package/dist/agents/instructions.js +212 -0
- package/dist/agents/instructions.js.map +1 -0
- package/dist/agents/lint.d.ts +154 -0
- package/dist/agents/lint.d.ts.map +1 -0
- package/dist/agents/lint.js +761 -0
- package/dist/agents/lint.js.map +1 -0
- package/dist/agents/split.d.ts +113 -0
- package/dist/agents/split.d.ts.map +1 -0
- package/dist/agents/split.js +234 -0
- package/dist/agents/split.js.map +1 -0
- package/dist/apply.d.ts.map +1 -1
- package/dist/apply.js.map +1 -1
- package/dist/cache/prefix.d.ts.map +1 -1
- package/dist/cache/prefix.js.map +1 -1
- package/dist/cli/agents.d.ts +39 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +121 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/args.d.ts +17 -70
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +18 -338
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/config.d.ts +60 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +89 -2
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/hooks.d.ts +55 -51
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +206 -658
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts +30 -15
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +167 -38
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/report.d.ts +23 -0
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +142 -4
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/run.d.ts +29 -90
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +34 -342
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/shell.d.ts +106 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +100 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/subcommands/agents.d.ts +72 -0
- package/dist/cli/subcommands/agents.d.ts.map +1 -0
- package/dist/cli/subcommands/agents.js +162 -0
- package/dist/cli/subcommands/agents.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +144 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -0
- package/dist/cli/subcommands/flags.js +220 -0
- package/dist/cli/subcommands/flags.js.map +1 -0
- package/dist/cli/subcommands/hooks.d.ts +18 -0
- package/dist/cli/subcommands/hooks.d.ts.map +1 -0
- package/dist/cli/subcommands/hooks.js +80 -0
- package/dist/cli/subcommands/hooks.js.map +1 -0
- package/dist/cli/subcommands/init.d.ts +19 -0
- package/dist/cli/subcommands/init.d.ts.map +1 -0
- package/dist/cli/subcommands/init.js +44 -0
- package/dist/cli/subcommands/init.js.map +1 -0
- package/dist/cli/subcommands/map.d.ts +75 -0
- package/dist/cli/subcommands/map.d.ts.map +1 -0
- package/dist/cli/subcommands/map.js +132 -0
- package/dist/cli/subcommands/map.js.map +1 -0
- package/dist/cli/subcommands/registry.d.ts +81 -0
- package/dist/cli/subcommands/registry.d.ts.map +1 -0
- package/dist/cli/subcommands/registry.js +141 -0
- package/dist/cli/subcommands/registry.js.map +1 -0
- package/dist/cli/subcommands/retrieve.d.ts +74 -0
- package/dist/cli/subcommands/retrieve.d.ts.map +1 -0
- package/dist/cli/subcommands/retrieve.js +84 -0
- package/dist/cli/subcommands/retrieve.js.map +1 -0
- package/dist/cli/subcommands/smelt.d.ts +107 -0
- package/dist/cli/subcommands/smelt.d.ts.map +1 -0
- package/dist/cli/subcommands/smelt.js +304 -0
- package/dist/cli/subcommands/smelt.js.map +1 -0
- package/dist/cli/subcommands/stats.d.ts +43 -0
- package/dist/cli/subcommands/stats.d.ts.map +1 -0
- package/dist/cli/subcommands/stats.js +62 -0
- package/dist/cli/subcommands/stats.js.map +1 -0
- package/dist/cli/subcommands/subcommand.d.ts +113 -0
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -0
- package/dist/cli/subcommands/subcommand.js +3 -0
- package/dist/cli/subcommands/subcommand.js.map +1 -0
- package/dist/cli/usage.d.ts +3 -0
- package/dist/cli/usage.d.ts.map +1 -0
- package/dist/cli/usage.js +95 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js.map +1 -1
- package/dist/errors.d.ts +21 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +33 -1
- package/dist/errors.js.map +1 -1
- package/dist/harness/aider.d.ts +10 -0
- package/dist/harness/aider.d.ts.map +1 -0
- package/dist/harness/aider.js +21 -0
- package/dist/harness/aider.js.map +1 -0
- package/dist/harness/claude-code.d.ts +3 -0
- package/dist/harness/claude-code.d.ts.map +1 -0
- package/dist/harness/claude-code.js +68 -0
- package/dist/harness/claude-code.js.map +1 -0
- package/dist/harness/cline.d.ts +3 -0
- package/dist/harness/cline.d.ts.map +1 -0
- package/dist/harness/cline.js +54 -0
- package/dist/harness/cline.js.map +1 -0
- package/dist/harness/codex.d.ts +3 -0
- package/dist/harness/codex.d.ts.map +1 -0
- package/dist/harness/codex.js +92 -0
- package/dist/harness/codex.js.map +1 -0
- package/dist/harness/cursor.d.ts +3 -0
- package/dist/harness/cursor.d.ts.map +1 -0
- package/dist/harness/cursor.js +54 -0
- package/dist/harness/cursor.js.map +1 -0
- package/dist/harness/gemini.d.ts +3 -0
- package/dist/harness/gemini.d.ts.map +1 -0
- package/dist/harness/gemini.js +60 -0
- package/dist/harness/gemini.js.map +1 -0
- package/dist/harness/grok.d.ts +3 -0
- package/dist/harness/grok.d.ts.map +1 -0
- package/dist/harness/grok.js +47 -0
- package/dist/harness/grok.js.map +1 -0
- package/dist/harness/hermes.d.ts +3 -0
- package/dist/harness/hermes.d.ts.map +1 -0
- package/dist/harness/hermes.js +72 -0
- package/dist/harness/hermes.js.map +1 -0
- package/dist/harness/kilocode.d.ts +3 -0
- package/dist/harness/kilocode.d.ts.map +1 -0
- package/dist/harness/kilocode.js +35 -0
- package/dist/harness/kilocode.js.map +1 -0
- package/dist/harness/opencode.d.ts +3 -0
- package/dist/harness/opencode.d.ts.map +1 -0
- package/dist/harness/opencode.js +97 -0
- package/dist/harness/opencode.js.map +1 -0
- package/dist/harness/paths.d.ts +17 -0
- package/dist/harness/paths.d.ts.map +1 -0
- package/dist/harness/paths.js +45 -0
- package/dist/harness/paths.js.map +1 -0
- package/dist/harness/profile.d.ts +209 -0
- package/dist/harness/profile.d.ts.map +1 -0
- package/dist/harness/profile.js +41 -0
- package/dist/harness/profile.js.map +1 -0
- package/dist/harness/registry.d.ts +100 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +144 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/snippet.d.ts +19 -0
- package/dist/harness/snippet.d.ts.map +1 -0
- package/dist/harness/snippet.js +41 -0
- package/dist/harness/snippet.js.map +1 -0
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js.map +1 -1
- package/dist/hooks/guard-core.d.ts +14 -14
- package/dist/hooks/guard-core.d.ts.map +1 -1
- package/dist/hooks/guard-core.js +15 -37
- package/dist/hooks/guard-core.js.map +1 -1
- package/dist/hooks/shim.d.ts +95 -4
- package/dist/hooks/shim.d.ts.map +1 -1
- package/dist/hooks/shim.js +87 -2
- package/dist/hooks/shim.js.map +1 -1
- package/dist/hooks/shims/claude-code.d.ts +5 -18
- package/dist/hooks/shims/claude-code.d.ts.map +1 -1
- package/dist/hooks/shims/claude-code.js +9 -56
- package/dist/hooks/shims/claude-code.js.map +1 -1
- package/dist/hooks/shims/cline.d.ts +5 -12
- package/dist/hooks/shims/cline.d.ts.map +1 -1
- package/dist/hooks/shims/cline.js +9 -34
- package/dist/hooks/shims/cline.js.map +1 -1
- package/dist/hooks/shims/codex.d.ts +5 -18
- package/dist/hooks/shims/codex.d.ts.map +1 -1
- package/dist/hooks/shims/codex.js +9 -51
- package/dist/hooks/shims/codex.js.map +1 -1
- package/dist/hooks/shims/cursor.d.ts +5 -14
- package/dist/hooks/shims/cursor.d.ts.map +1 -1
- package/dist/hooks/shims/cursor.js +9 -42
- package/dist/hooks/shims/cursor.js.map +1 -1
- package/dist/hooks/shims/gemini.d.ts +5 -18
- package/dist/hooks/shims/gemini.d.ts.map +1 -1
- package/dist/hooks/shims/gemini.js +9 -48
- package/dist/hooks/shims/gemini.js.map +1 -1
- package/dist/hooks/shims/grok.d.ts +5 -13
- package/dist/hooks/shims/grok.d.ts.map +1 -1
- package/dist/hooks/shims/grok.js +9 -32
- package/dist/hooks/shims/grok.js.map +1 -1
- package/dist/hooks/shims/hermes.d.ts +5 -17
- package/dist/hooks/shims/hermes.d.ts.map +1 -1
- package/dist/hooks/shims/hermes.js +9 -45
- package/dist/hooks/shims/hermes.js.map +1 -1
- package/dist/index.d.ts +43 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -67
- package/dist/index.js.map +1 -1
- package/dist/lang/bash.d.ts.map +1 -1
- package/dist/lang/bash.js.map +1 -1
- package/dist/lang/c.d.ts.map +1 -1
- package/dist/lang/c.js.map +1 -1
- package/dist/lang/c_sharp.d.ts.map +1 -1
- package/dist/lang/c_sharp.js.map +1 -1
- package/dist/lang/cpp.d.ts.map +1 -1
- package/dist/lang/cpp.js.map +1 -1
- package/dist/lang/go.d.ts.map +1 -1
- package/dist/lang/go.js.map +1 -1
- package/dist/lang/java.d.ts.map +1 -1
- package/dist/lang/java.js.map +1 -1
- package/dist/lang/javascript.d.ts.map +1 -1
- package/dist/lang/javascript.js.map +1 -1
- package/dist/lang/kotlin.d.ts.map +1 -1
- package/dist/lang/kotlin.js.map +1 -1
- package/dist/lang/php.d.ts.map +1 -1
- package/dist/lang/php.js.map +1 -1
- package/dist/lang/profile.d.ts.map +1 -1
- package/dist/lang/profile.js.map +1 -1
- package/dist/lang/python.d.ts.map +1 -1
- package/dist/lang/python.js.map +1 -1
- package/dist/lang/registry.d.ts.map +1 -1
- package/dist/lang/registry.js.map +1 -1
- package/dist/lang/ruby.d.ts.map +1 -1
- package/dist/lang/ruby.js.map +1 -1
- package/dist/lang/rust.d.ts.map +1 -1
- package/dist/lang/rust.js.map +1 -1
- package/dist/lang/swift.d.ts.map +1 -1
- package/dist/lang/swift.js.map +1 -1
- package/dist/lang/tsx.d.ts.map +1 -1
- package/dist/lang/tsx.js.map +1 -1
- package/dist/lang/typescript.d.ts.map +1 -1
- package/dist/lang/typescript.js.map +1 -1
- package/dist/net/policy.d.ts +18 -1
- package/dist/net/policy.d.ts.map +1 -1
- package/dist/net/policy.js +6 -4
- package/dist/net/policy.js.map +1 -1
- package/dist/ops/index.d.ts +39 -0
- package/dist/ops/index.d.ts.map +1 -0
- package/dist/ops/index.js +37 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/ops/inputs.d.ts +170 -0
- package/dist/ops/inputs.d.ts.map +1 -0
- package/dist/ops/inputs.js +142 -0
- package/dist/ops/inputs.js.map +1 -0
- package/dist/ops/verbs.d.ts +136 -0
- package/dist/ops/verbs.d.ts.map +1 -0
- package/dist/ops/verbs.js +76 -0
- package/dist/ops/verbs.js.map +1 -0
- package/dist/plan/auto.d.ts +69 -0
- package/dist/plan/auto.d.ts.map +1 -0
- package/dist/plan/auto.js +70 -0
- package/dist/plan/auto.js.map +1 -0
- package/dist/plan/budget.d.ts +21 -0
- package/dist/plan/budget.d.ts.map +1 -0
- package/dist/plan/budget.js +26 -0
- package/dist/plan/budget.js.map +1 -0
- package/dist/plan/grammar.d.ts +14 -0
- package/dist/plan/grammar.d.ts.map +1 -1
- package/dist/plan/grammar.js +47 -6
- package/dist/plan/grammar.js.map +1 -1
- package/dist/plan/lexical.d.ts.map +1 -1
- package/dist/plan/lexical.js +1 -7
- package/dist/plan/lexical.js.map +1 -1
- package/dist/plan/planners.d.ts +35 -2
- package/dist/plan/planners.d.ts.map +1 -1
- package/dist/plan/planners.js +35 -2
- package/dist/plan/planners.js.map +1 -1
- package/dist/plan/structural.d.ts +19 -5
- package/dist/plan/structural.d.ts.map +1 -1
- package/dist/plan/structural.js +180 -33
- package/dist/plan/structural.js.map +1 -1
- package/dist/repomap/cache.d.ts +48 -0
- package/dist/repomap/cache.d.ts.map +1 -1
- package/dist/repomap/cache.js +112 -13
- package/dist/repomap/cache.js.map +1 -1
- package/dist/repomap/io.d.ts +24 -0
- package/dist/repomap/io.d.ts.map +1 -0
- package/dist/repomap/io.js +34 -0
- package/dist/repomap/io.js.map +1 -0
- package/dist/repomap/map.d.ts +69 -6
- package/dist/repomap/map.d.ts.map +1 -1
- package/dist/repomap/map.js +104 -22
- package/dist/repomap/map.js.map +1 -1
- package/dist/repomap/rank.d.ts +26 -2
- package/dist/repomap/rank.d.ts.map +1 -1
- package/dist/repomap/rank.js +20 -0
- package/dist/repomap/rank.js.map +1 -1
- package/dist/repomap/reader.d.ts +73 -0
- package/dist/repomap/reader.d.ts.map +1 -0
- package/dist/repomap/reader.js +30 -0
- package/dist/repomap/reader.js.map +1 -0
- package/dist/repomap/tags.d.ts.map +1 -1
- package/dist/repomap/tags.js.map +1 -1
- package/dist/retrieve.d.ts +8 -0
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +12 -1
- package/dist/retrieve.js.map +1 -1
- package/dist/smelter.d.ts +79 -0
- package/dist/smelter.d.ts.map +1 -0
- package/dist/smelter.js +65 -0
- package/dist/smelter.js.map +1 -0
- package/dist/stages.d.ts.map +1 -1
- package/dist/stages.js +2 -2
- package/dist/stages.js.map +1 -1
- package/dist/stats.d.ts +2 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +2 -1
- package/dist/stats.js.map +1 -1
- package/dist/store-dir.d.ts +43 -4
- package/dist/store-dir.d.ts.map +1 -1
- package/dist/store-dir.js +48 -5
- package/dist/store-dir.js.map +1 -1
- package/dist/store.d.ts +6 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +6 -0
- package/dist/store.js.map +1 -1
- package/dist/text/json-edit.d.ts +71 -0
- package/dist/text/json-edit.d.ts.map +1 -0
- package/dist/text/json-edit.js +219 -0
- package/dist/text/json-edit.js.map +1 -0
- package/dist/types.d.ts +35 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +11 -11
- package/dist/cli/resolve.d.ts +0 -103
- package/dist/cli/resolve.d.ts.map +0 -1
- package/dist/cli/resolve.js +0 -106
- package/dist/cli/resolve.js.map +0 -1
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { instructionSnippet } from './snippet.js';
|
|
2
|
+
/**
|
|
3
|
+
* KiloCode — ADVISORY tier. Instructions only, and honest about it.
|
|
4
|
+
*
|
|
5
|
+
* No first-class hook API (Kilo-Org/kilocode#5827), so nothing here is enforced: what
|
|
6
|
+
* ships is a rules file this harness owns whole — the shared snippet plus the two
|
|
7
|
+
* manual enforcement legs the snippet has no room for. `remove` deletes it.
|
|
8
|
+
*/
|
|
9
|
+
function kilocodeRulesSource(ctx) {
|
|
10
|
+
return `${instructionSnippet(ctx.thresholdBytes, ctx.budgetBytes)}
|
|
11
|
+
<!-- smelt:hooks v1 advisory notes -->
|
|
12
|
+
|
|
13
|
+
KiloCode has no first-class hook API (Kilo-Org/kilocode#5827), so nothing above is
|
|
14
|
+
enforced — it is advisory. Two manual legs make it harder to bypass:
|
|
15
|
+
|
|
16
|
+
1. Permissions: in your KiloCode per-tool permission config, set raw-read/execute
|
|
17
|
+
tools to "ask" so oversized reads surface for review instead of passing silently.
|
|
18
|
+
2. MCP: expose smelt through an MCP server and prefer its tools; a resident server
|
|
19
|
+
also keeps smelt's grammar cache warm across calls.
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
export const kilocode = {
|
|
23
|
+
id: 'kilocode',
|
|
24
|
+
name: 'KiloCode',
|
|
25
|
+
tier: 'advisory',
|
|
26
|
+
detect: ['.kilocode'],
|
|
27
|
+
detectHome: ['.config/kilo'],
|
|
28
|
+
instructionFile: '.kilocode/rules/smelt.md',
|
|
29
|
+
instructions: kilocodeRulesSource,
|
|
30
|
+
caveats: [
|
|
31
|
+
'no first-class hooks (Kilo-Org/kilocode#5827): enforcement is permissions config + MCP, both manual',
|
|
32
|
+
],
|
|
33
|
+
install: [],
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=kilocode.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { DENIED_WITHOUT_REASON, REWRITE_ANNOUNCEMENT_JOIN, REWRITE_ANNOUNCEMENT_OPENING, } from '../hooks/shim.js';
|
|
2
|
+
import { guardCoreScriptPath, portablePath } from './paths.js';
|
|
3
|
+
/**
|
|
4
|
+
* opencode — EXPERIMENTAL tier. Install and removal, end to end.
|
|
5
|
+
*
|
|
6
|
+
* The one harness with no shim script: its hook API is a JavaScript plugin, not a
|
|
7
|
+
* stdin schema, so what the installer writes *is* the adapter. The plugin imports the
|
|
8
|
+
* built guard core directly (the same module every shim calls) and maps
|
|
9
|
+
* `tool.execute.before` onto it, so all policy still lives in exactly one place.
|
|
10
|
+
*
|
|
11
|
+
* Its two announcements — the rewrite sentence and the reasonless deny — are spliced
|
|
12
|
+
* in from `hooks/shim.ts`'s constants rather than hand-typed into the template, where
|
|
13
|
+
* nothing could see them drift from what the shims print.
|
|
14
|
+
*/
|
|
15
|
+
function opencodePluginSource(ctx) {
|
|
16
|
+
const guardCore = portablePath(ctx.cwd, guardCoreScriptPath());
|
|
17
|
+
return `// smelt:hooks v1 — opencode plugin shim. EXPERIMENTAL tier: mapped from the
|
|
18
|
+
// capability matrix (docs/research/2026-09-02-harness-capability-matrix.md, opencode
|
|
19
|
+
// row; https://opencode.ai/docs/plugins/). This template's deny/pass/window paths
|
|
20
|
+
// were exercised directly against the built guard core (verified
|
|
21
|
+
// 2026-09-02), but a live opencode session has not been smoke-tested — that needs
|
|
22
|
+
// provider credentials. Caveat carried from the matrix: MCP tools can bypass plugin
|
|
23
|
+
// hooks (sst/opencode#2319) — this guard sees built-in tools only.
|
|
24
|
+
//
|
|
25
|
+
// Thin adapter: maps tool.execute.before onto the smelt guard core (zero
|
|
26
|
+
// dependencies), which owns every decision. Deny mode throws (opencode surfaces the
|
|
27
|
+
// reason to the model); rewrite mode substitutes the faithful replacement command —
|
|
28
|
+
// announced on stderr, because the plugin API has no reason channel on a rewrite
|
|
29
|
+
// and a substitution must never be silent. Both announcements below are spliced in
|
|
30
|
+
// from the shims' own constants, so this copy cannot drift from theirs.
|
|
31
|
+
import { pathToFileURL } from 'node:url';
|
|
32
|
+
|
|
33
|
+
const GUARD_CORE = ${JSON.stringify(guardCore)};
|
|
34
|
+
const core = await import(pathToFileURL(GUARD_CORE).href);
|
|
35
|
+
|
|
36
|
+
export const SmeltGuard = async () => ({
|
|
37
|
+
'tool.execute.before': async (input, output) => {
|
|
38
|
+
const tool = input?.tool;
|
|
39
|
+
const args = output?.args ?? {};
|
|
40
|
+
let request;
|
|
41
|
+
if (tool === 'read' && typeof args.filePath === 'string') {
|
|
42
|
+
request = {
|
|
43
|
+
tool: 'Read',
|
|
44
|
+
input: {
|
|
45
|
+
path: args.filePath,
|
|
46
|
+
offsetLimited: args.offset !== undefined || args.limit !== undefined,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
} else if (tool === 'bash' && typeof args.command === 'string') {
|
|
50
|
+
request = { tool: 'Bash', input: { command: args.command } };
|
|
51
|
+
} else {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const warn = (text) => process.stderr.write(text + '\\n');
|
|
55
|
+
const settings = core.readGuardSettings(process.cwd(), warn);
|
|
56
|
+
const decision = core.decide(request, settings, process.cwd());
|
|
57
|
+
if (decision.action !== 'deny') return;
|
|
58
|
+
if (
|
|
59
|
+
settings.enforcement === 'rewrite' &&
|
|
60
|
+
request.tool === 'Bash' &&
|
|
61
|
+
decision.suggestion !== undefined
|
|
62
|
+
) {
|
|
63
|
+
warn(
|
|
64
|
+
${JSON.stringify(REWRITE_ANNOUNCEMENT_OPENING)} +
|
|
65
|
+
decision.suggestion +
|
|
66
|
+
${JSON.stringify(REWRITE_ANNOUNCEMENT_JOIN)} +
|
|
67
|
+
(decision.reason ?? ''),
|
|
68
|
+
);
|
|
69
|
+
output.args.command = decision.suggestion;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(decision.reason ?? ${JSON.stringify(DENIED_WITHOUT_REASON)});
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
export const opencode = {
|
|
78
|
+
id: 'opencode',
|
|
79
|
+
name: 'opencode',
|
|
80
|
+
tier: 'experimental',
|
|
81
|
+
detect: ['.opencode', 'opencode.json'],
|
|
82
|
+
detectHome: ['.config/opencode'],
|
|
83
|
+
instructionFile: 'AGENTS.md',
|
|
84
|
+
instructions: 'snippet',
|
|
85
|
+
caveats: [
|
|
86
|
+
'MCP tools can bypass opencode plugin hooks (sst/opencode#2319) — the guard sees built-in tools only',
|
|
87
|
+
],
|
|
88
|
+
install: [
|
|
89
|
+
{
|
|
90
|
+
kind: 'own-file',
|
|
91
|
+
file: '.opencode/plugin/smelt-guard.js',
|
|
92
|
+
content: opencodePluginSource,
|
|
93
|
+
guardOnly: true,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=opencode.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ShimmedHarnessProfile } from './profile.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The shim script a harness's hook command runs. Takes a profile rather than an id,
|
|
4
|
+
* because only a profile that carries a hook schema (or a hand-written adapter) has a
|
|
5
|
+
* shim script on disk: a path for a harness that ships none would name a file the
|
|
6
|
+
* build never produced.
|
|
7
|
+
*/
|
|
8
|
+
export declare function shimScriptPath(profile: ShimmedHarnessProfile): string;
|
|
9
|
+
/** The guard core as a module: what the opencode plugin imports at hook time. */
|
|
10
|
+
export declare function guardCoreScriptPath(): string;
|
|
11
|
+
/** The `smelt` binary — quoted into the stats and map hook commands. */
|
|
12
|
+
export declare function smeltBinPath(): string;
|
|
13
|
+
/** Inside the project, a project-relative path travels with the repo; outside, absolute. */
|
|
14
|
+
export declare function portablePath(cwd: string, absolute: string): string;
|
|
15
|
+
/** `node "<script>"` — how every harness config invokes something of smelt's. */
|
|
16
|
+
export declare function nodeCommand(cwd: string, script: string, args?: string): string;
|
|
17
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/harness/paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAoB1D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAErE;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,wEAAwE;AACxE,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGlE;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,MAAM,CAE1E","sourcesContent":["import { dirname, isAbsolute, join, relative, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Where the scripts a harness config points at actually live, and how a profile names\n * one. Path facts only — nothing here reads or writes a file.\n */\n\n/**\n * The `dist` directory of this installed package — where the shipped guard core and\n * shim scripts live. Computed from this module's own location, which is\n * `<pkg>/dist/harness/` in every real run (the CLI executes from `dist`); under the\n * test runner it is `<pkg>/src/harness/`, and the substitution still points at `dist`,\n * which is where the scripts will exist once built — the paths are written into config\n * files for *node* to execute, never imported.\n */\nfunction packageDistDir(): string {\n const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/(dist|src)/harness\n return join(dirname(dirname(here)), 'dist');\n}\n\n/**\n * The shim script a harness's hook command runs. Takes a profile rather than an id,\n * because only a profile that carries a hook schema (or a hand-written adapter) has a\n * shim script on disk: a path for a harness that ships none would name a file the\n * build never produced.\n */\nexport function shimScriptPath(profile: ShimmedHarnessProfile): string {\n return join(packageDistDir(), 'hooks', 'shims', `${profile.id}.js`);\n}\n\n/** The guard core as a module: what the opencode plugin imports at hook time. */\nexport function guardCoreScriptPath(): string {\n return join(packageDistDir(), 'hooks', 'guard-core.js');\n}\n\n/** The `smelt` binary — quoted into the stats and map hook commands. */\nexport function smeltBinPath(): string {\n return join(packageDistDir(), 'cli', 'bin.js');\n}\n\n/** Inside the project, a project-relative path travels with the repo; outside, absolute. */\nexport function portablePath(cwd: string, absolute: string): string {\n const rel = relative(cwd, absolute);\n return rel.startsWith('..') || isAbsolute(rel) ? absolute : rel.split(sep).join('/');\n}\n\n/** `node \"<script>\"` — how every harness config invokes something of smelt's. */\nexport function nodeCommand(cwd: string, script: string, args = ''): string {\n return `node \"${portablePath(cwd, script)}\"${args === '' ? '' : ` ${args}`}`;\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
/**
|
|
4
|
+
* Where the scripts a harness config points at actually live, and how a profile names
|
|
5
|
+
* one. Path facts only — nothing here reads or writes a file.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The `dist` directory of this installed package — where the shipped guard core and
|
|
9
|
+
* shim scripts live. Computed from this module's own location, which is
|
|
10
|
+
* `<pkg>/dist/harness/` in every real run (the CLI executes from `dist`); under the
|
|
11
|
+
* test runner it is `<pkg>/src/harness/`, and the substitution still points at `dist`,
|
|
12
|
+
* which is where the scripts will exist once built — the paths are written into config
|
|
13
|
+
* files for *node* to execute, never imported.
|
|
14
|
+
*/
|
|
15
|
+
function packageDistDir() {
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/(dist|src)/harness
|
|
17
|
+
return join(dirname(dirname(here)), 'dist');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The shim script a harness's hook command runs. Takes a profile rather than an id,
|
|
21
|
+
* because only a profile that carries a hook schema (or a hand-written adapter) has a
|
|
22
|
+
* shim script on disk: a path for a harness that ships none would name a file the
|
|
23
|
+
* build never produced.
|
|
24
|
+
*/
|
|
25
|
+
export function shimScriptPath(profile) {
|
|
26
|
+
return join(packageDistDir(), 'hooks', 'shims', `${profile.id}.js`);
|
|
27
|
+
}
|
|
28
|
+
/** The guard core as a module: what the opencode plugin imports at hook time. */
|
|
29
|
+
export function guardCoreScriptPath() {
|
|
30
|
+
return join(packageDistDir(), 'hooks', 'guard-core.js');
|
|
31
|
+
}
|
|
32
|
+
/** The `smelt` binary — quoted into the stats and map hook commands. */
|
|
33
|
+
export function smeltBinPath() {
|
|
34
|
+
return join(packageDistDir(), 'cli', 'bin.js');
|
|
35
|
+
}
|
|
36
|
+
/** Inside the project, a project-relative path travels with the repo; outside, absolute. */
|
|
37
|
+
export function portablePath(cwd, absolute) {
|
|
38
|
+
const rel = relative(cwd, absolute);
|
|
39
|
+
return rel.startsWith('..') || isAbsolute(rel) ? absolute : rel.split(sep).join('/');
|
|
40
|
+
}
|
|
41
|
+
/** `node "<script>"` — how every harness config invokes something of smelt's. */
|
|
42
|
+
export function nodeCommand(cwd, script, args = '') {
|
|
43
|
+
return `node "${portablePath(cwd, script)}"${args === '' ? '' : ` ${args}`}`;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/harness/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC;;;GAGG;AAEH;;;;;;;GAOG;AACH,SAAS,cAAc;IACrB,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,2BAA2B;IACjF,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,OAA8B;IAC3D,OAAO,IAAI,CAAC,cAAc,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;AACtE,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,cAAc,EAAE,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC;AAC1D,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,YAAY;IAC1B,OAAO,IAAI,CAAC,cAAc,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AACjD,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,QAAgB;IACxD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACpC,OAAO,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvF,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,MAAc,EAAE,IAAI,GAAG,EAAE;IAChE,OAAO,SAAS,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AAC/E,CAAC","sourcesContent":["import { dirname, isAbsolute, join, relative, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { ShimmedHarnessProfile } from './profile.ts';\n\n/**\n * Where the scripts a harness config points at actually live, and how a profile names\n * one. Path facts only — nothing here reads or writes a file.\n */\n\n/**\n * The `dist` directory of this installed package — where the shipped guard core and\n * shim scripts live. Computed from this module's own location, which is\n * `<pkg>/dist/harness/` in every real run (the CLI executes from `dist`); under the\n * test runner it is `<pkg>/src/harness/`, and the substitution still points at `dist`,\n * which is where the scripts will exist once built — the paths are written into config\n * files for *node* to execute, never imported.\n */\nfunction packageDistDir(): string {\n const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/(dist|src)/harness\n return join(dirname(dirname(here)), 'dist');\n}\n\n/**\n * The shim script a harness's hook command runs. Takes a profile rather than an id,\n * because only a profile that carries a hook schema (or a hand-written adapter) has a\n * shim script on disk: a path for a harness that ships none would name a file the\n * build never produced.\n */\nexport function shimScriptPath(profile: ShimmedHarnessProfile): string {\n return join(packageDistDir(), 'hooks', 'shims', `${profile.id}.js`);\n}\n\n/** The guard core as a module: what the opencode plugin imports at hook time. */\nexport function guardCoreScriptPath(): string {\n return join(packageDistDir(), 'hooks', 'guard-core.js');\n}\n\n/** The `smelt` binary — quoted into the stats and map hook commands. */\nexport function smeltBinPath(): string {\n return join(packageDistDir(), 'cli', 'bin.js');\n}\n\n/** Inside the project, a project-relative path travels with the repo; outside, absolute. */\nexport function portablePath(cwd: string, absolute: string): string {\n const rel = relative(cwd, absolute);\n return rel.startsWith('..') || isAbsolute(rel) ? absolute : rel.split(sep).join('/');\n}\n\n/** `node \"<script>\"` — how every harness config invokes something of smelt's. */\nexport function nodeCommand(cwd: string, script: string, args = ''): string {\n return `node \"${portablePath(cwd, script)}\"${args === '' ? '' : ` ${args}`}`;\n}\n"]}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { HarnessHookSchema, ShimAdapter } from '../hooks/shim.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Everything smelt knows about one agent harness, in one place.
|
|
4
|
+
*
|
|
5
|
+
* Before this module existed, a harness's facts lived at seven edit sites: the
|
|
6
|
+
* `HarnessId` union, the `HARNESSES` array, a shim file, a case in `planInstall`, a
|
|
7
|
+
* case in `planRemove`, `MANAGED_EVENTS`, a test fixture — plus a hand-typed id list
|
|
8
|
+
* in the `--harness` help text, which could not be derived because the registry lived
|
|
9
|
+
* in `cli/hooks.ts` and `cli/hooks.ts` imports `CLI_NAME` from `cli/args.ts`. The
|
|
10
|
+
* registry was in the wrong module, and the cycle was the proof.
|
|
11
|
+
*
|
|
12
|
+
* A `HarnessProfile` is the single adapter carrying every per-harness fact, and the
|
|
13
|
+
* registry (`registry.ts`) is `Record<HarnessId, HarnessProfile>` so totality stays a
|
|
14
|
+
* compile error: a new `HarnessId` without a profile does not build. It imports
|
|
15
|
+
* nothing from `cli/`, so `cli/args.ts` derives the `--harness` list and every other
|
|
16
|
+
* rendered view is derived too — the managed event names, the files a re-run reads its
|
|
17
|
+
* toggles back from, the id list in an error message.
|
|
18
|
+
*
|
|
19
|
+
* Installing and removing are **data**, not switch statements: `install` is the list
|
|
20
|
+
* of artefacts `smelt hooks install` writes for this harness, and each artefact's kind
|
|
21
|
+
* is also how `smelt hooks remove` takes it back out — a JSON hook file is merged and
|
|
22
|
+
* strip-merged, a marker block is upserted and stripped, a file that is entirely ours
|
|
23
|
+
* is written and deleted. `planInstall`/`planRemove` fold over the list.
|
|
24
|
+
*
|
|
25
|
+
* The optional sections state capability, never invent it: a harness ships a shim
|
|
26
|
+
* exactly when it carries a {@link hooks} schema (or, for a harness a table cannot
|
|
27
|
+
* express, a hand-written {@link shim}), and only such a profile has a shim script
|
|
28
|
+
* path. What a profile does not claim, the installer does not write.
|
|
29
|
+
*/
|
|
30
|
+
export interface HarnessProfile {
|
|
31
|
+
readonly id: HarnessId;
|
|
32
|
+
/** The harness's own name, as its makers spell it. Shown wherever a tier is. */
|
|
33
|
+
readonly name: string;
|
|
34
|
+
/**
|
|
35
|
+
* The name a *list* of harnesses uses, where the maker's own spelling carries a
|
|
36
|
+
* category suffix a list does not need — "Codex CLI" is "Codex" in a tier clause,
|
|
37
|
+
* "Hermes Agent" is "Hermes". Optional, and defaulted by {@link harnessLabel} to
|
|
38
|
+
* {@link name}: only a harness whose prose name differs from its full name carries
|
|
39
|
+
* one, and it carries it here rather than in each of the four places a tier list is
|
|
40
|
+
* rendered. "Claude Code" has no suffix to drop, and so has none.
|
|
41
|
+
*/
|
|
42
|
+
readonly shortName?: string;
|
|
43
|
+
readonly tier: HarnessTier;
|
|
44
|
+
/** Paths (relative to the project) whose existence means "this harness is in use here". */
|
|
45
|
+
readonly detect: readonly string[];
|
|
46
|
+
/** Paths relative to the user's home directory — "installed on this machine". */
|
|
47
|
+
readonly detectHome: readonly string[];
|
|
48
|
+
/** The standing-instructions file this harness reads (capability matrix column d). */
|
|
49
|
+
readonly instructionFile: string;
|
|
50
|
+
/**
|
|
51
|
+
* The standing-instructions layer — belt and braces under every shim, and the *only*
|
|
52
|
+
* layer an advisory harness has:
|
|
53
|
+
*
|
|
54
|
+
* - `'snippet'`: upsert the shared marker-bracketed block into
|
|
55
|
+
* {@link instructionFile} (a file several harnesses read, like `AGENTS.md`, is
|
|
56
|
+
* planned once); `remove` strips the block and leaves every other byte.
|
|
57
|
+
* - a renderer: a whole file this harness owns, snippet included — KiloCode's rules
|
|
58
|
+
* file carries two manual enforcement legs the shared snippet has no room for;
|
|
59
|
+
* `remove` deletes it.
|
|
60
|
+
*/
|
|
61
|
+
readonly instructions: 'snippet' | HarnessFileContent;
|
|
62
|
+
/** Caveats carried from the capability matrix, shown at install time. */
|
|
63
|
+
readonly caveats: readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* Everything else `hooks install` writes for this harness, in the order it is
|
|
66
|
+
* written and listed. Empty means instructions only: the advisory tier, where
|
|
67
|
+
* nothing enforces anything and the output says so.
|
|
68
|
+
*/
|
|
69
|
+
readonly install: readonly HarnessInstallStep[];
|
|
70
|
+
/**
|
|
71
|
+
* This harness's native pre-tool hook schema, as data. Present exactly when the
|
|
72
|
+
* harness ships a shim script (`dist/hooks/shims/<id>.js`) — absent for the advisory
|
|
73
|
+
* tier, and for opencode, whose hook API is a JavaScript plugin rather than a stdin
|
|
74
|
+
* schema.
|
|
75
|
+
*/
|
|
76
|
+
readonly hooks?: HarnessHookSchema;
|
|
77
|
+
/**
|
|
78
|
+
* The escape hatch: a hand-written adapter, for a harness whose schema a table
|
|
79
|
+
* cannot express. Wins over {@link hooks} when both are present.
|
|
80
|
+
*/
|
|
81
|
+
readonly shim?: ShimAdapter;
|
|
82
|
+
}
|
|
83
|
+
/** How much smelt is willing to claim about a harness. */
|
|
84
|
+
export type HarnessTier = 'verified' | 'experimental' | 'advisory';
|
|
85
|
+
/**
|
|
86
|
+
* Every harness the preset knows. The registry is keyed by this union, so adding an id
|
|
87
|
+
* without writing its profile is a compile error — and the `--harness` help list, the
|
|
88
|
+
* wizard's table and every error message read the registry, never a second list.
|
|
89
|
+
*/
|
|
90
|
+
export type HarnessId = 'claude-code' | 'codex' | 'gemini' | 'grok' | 'hermes' | 'cursor' | 'opencode' | 'cline' | 'kilocode' | 'aider';
|
|
91
|
+
/** One line of honesty per tier, shown wherever a tier label appears. */
|
|
92
|
+
export declare const TIER_HONESTY: Record<HarnessTier, string>;
|
|
93
|
+
/**
|
|
94
|
+
* The tiers, in the order every rendered tier list walks them — most claimed first.
|
|
95
|
+
* {@link TIER_HONESTY}'s key order *is* that order, so a tier cannot exist without a
|
|
96
|
+
* line saying what it means, and the order lives once.
|
|
97
|
+
*/
|
|
98
|
+
export declare const HARNESS_TIERS: readonly HarnessTier[];
|
|
99
|
+
/**
|
|
100
|
+
* How a list of harnesses spells this one: {@link HarnessProfile.shortName} where the
|
|
101
|
+
* maker's own name carries a suffix a list does not need, else {@link
|
|
102
|
+
* HarnessProfile.name}. The help's tier clause, the wizard's sentences and the site's
|
|
103
|
+
* tier table all render through this, so they cannot spell a harness three ways.
|
|
104
|
+
*/
|
|
105
|
+
export declare function harnessLabel(profile: HarnessProfile): string;
|
|
106
|
+
/**
|
|
107
|
+
* What the wizard settled on, as the installer's renderers see it: the toggles, the
|
|
108
|
+
* guard's settings, and the project directory every path is relative to. A renderer
|
|
109
|
+
* takes this and returns bytes; nothing writes.
|
|
110
|
+
*/
|
|
111
|
+
export interface HarnessInstallContext {
|
|
112
|
+
/** Project directory: every path a renderer emits is portable relative to it. */
|
|
113
|
+
readonly cwd: string;
|
|
114
|
+
readonly guard: boolean;
|
|
115
|
+
readonly statsOnStop: boolean;
|
|
116
|
+
readonly mapOnStart: boolean;
|
|
117
|
+
/** Opt-in `smelt agents lint` at session start — shares SessionStart with the map. */
|
|
118
|
+
readonly lintOnStart: boolean;
|
|
119
|
+
readonly thresholdBytes: number;
|
|
120
|
+
/** The `--budget` every suggested command and the snippet quote. */
|
|
121
|
+
readonly budgetBytes: number;
|
|
122
|
+
}
|
|
123
|
+
/** A file's bytes, rendered from the wizard's choices. */
|
|
124
|
+
export type HarnessFileContent = (ctx: HarnessInstallContext) => string;
|
|
125
|
+
/**
|
|
126
|
+
* One artefact `hooks install` writes. The kind is also the un-write: `json-hooks` is
|
|
127
|
+
* merged in and strip-merged out, `marker-block` is upserted and stripped,
|
|
128
|
+
* `own-file` is written and deleted.
|
|
129
|
+
*/
|
|
130
|
+
export type HarnessInstallStep = HarnessJsonHooks | HarnessMarkerBlock | HarnessOwnFile;
|
|
131
|
+
/**
|
|
132
|
+
* A JSON settings/hooks file the harness reads. Our entries are merged in
|
|
133
|
+
* byte-faithfully and, on `remove`, stripped back out with everything foreign left
|
|
134
|
+
* exactly as it was.
|
|
135
|
+
*/
|
|
136
|
+
export interface HarnessJsonHooks {
|
|
137
|
+
readonly kind: 'json-hooks';
|
|
138
|
+
/** Project-relative path of the file. */
|
|
139
|
+
readonly file: string;
|
|
140
|
+
/** The pre-tool event, in this harness's spelling (`PreToolUse`, `BeforeTool`, …). */
|
|
141
|
+
readonly event: string;
|
|
142
|
+
/**
|
|
143
|
+
* One hook entry per matcher — the tool names the guard wants to see. `undefined`
|
|
144
|
+
* is a matcher-less entry, for a harness whose hook fires on every tool.
|
|
145
|
+
*/
|
|
146
|
+
readonly matchers: readonly (string | undefined)[];
|
|
147
|
+
/**
|
|
148
|
+
* The entry shape: `'command-list'` is Claude Code's `{matcher, hooks:[{type,
|
|
149
|
+
* command}]}`, which Codex, Gemini and Grok mirror; `'bare-command'` is Cursor's
|
|
150
|
+
* `{command}`.
|
|
151
|
+
*/
|
|
152
|
+
readonly entry: 'command-list' | 'bare-command';
|
|
153
|
+
/**
|
|
154
|
+
* True for a harness whose schema also carries the session-lifecycle events this
|
|
155
|
+
* preset offers — `smelt stats` on Stop, `smelt map` and `smelt agents lint` on
|
|
156
|
+
* SessionStart. The other
|
|
157
|
+
* harnesses wire the guard only, and the wizard's toggles say so.
|
|
158
|
+
*/
|
|
159
|
+
readonly lifecycle: boolean;
|
|
160
|
+
/** Top-level keys a *fresh* file must carry (Cursor's `version: 1`). */
|
|
161
|
+
readonly shape?: {
|
|
162
|
+
readonly version: number;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** A marker-bracketed block inside a file smelt shares with its owner. */
|
|
166
|
+
export interface HarnessMarkerBlock {
|
|
167
|
+
readonly kind: 'marker-block';
|
|
168
|
+
readonly file: string;
|
|
169
|
+
readonly block: HarnessFileContent;
|
|
170
|
+
/** The marker line opening the block — also how `remove` finds it. */
|
|
171
|
+
readonly start: string;
|
|
172
|
+
readonly end: string;
|
|
173
|
+
/**
|
|
174
|
+
* Refuse the file instead of editing it when it already carries `contains` and none
|
|
175
|
+
* of ours: Codex's `config.toml` may have a hand-written `[features]` table, and an
|
|
176
|
+
* installer that merged into it would be editing what it was never asked to.
|
|
177
|
+
*/
|
|
178
|
+
readonly skipWhen?: {
|
|
179
|
+
readonly contains: string;
|
|
180
|
+
readonly why: string;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** A file that is entirely smelt's: written whole, deleted whole. */
|
|
184
|
+
export interface HarnessOwnFile {
|
|
185
|
+
readonly kind: 'own-file';
|
|
186
|
+
readonly file: string;
|
|
187
|
+
readonly content: HarnessFileContent;
|
|
188
|
+
/** chmod after writing (Cline's hook must be executable). */
|
|
189
|
+
readonly mode?: number;
|
|
190
|
+
/** True when the file exists only to wire the guard — the guard toggle gates it. */
|
|
191
|
+
readonly guardOnly: boolean;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* A profile that ships a shim script. Only these have a shim path: `shimScriptPath`
|
|
195
|
+
* takes one, so a harness with no shim cannot name a script that was never built.
|
|
196
|
+
*/
|
|
197
|
+
export type ShimmedHarnessProfile = HarnessProfile & ({
|
|
198
|
+
readonly hooks: HarnessHookSchema;
|
|
199
|
+
} | {
|
|
200
|
+
readonly shim: ShimAdapter;
|
|
201
|
+
});
|
|
202
|
+
/** True for a profile that ships a shim script — the narrowing `shimScriptPath` needs. */
|
|
203
|
+
export declare function hasShim(profile: HarnessProfile): profile is ShimmedHarnessProfile;
|
|
204
|
+
/**
|
|
205
|
+
* The adapter this profile's shim script runs: the one its {@link HarnessProfile.hooks}
|
|
206
|
+
* schema describes, or the hand-written escape hatch where it carries one.
|
|
207
|
+
*/
|
|
208
|
+
export declare function shimAdapterOf(profile: ShimmedHarnessProfile): ShimAdapter;
|
|
209
|
+
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/harness/profile.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,sFAAsF;IACtF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,GAAG,kBAAkB,CAAC;IACtD,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAChD;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;CAC7B;AAED,0DAA0D;AAC1D,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,UAAU,CAAC;AAEnE;;;;GAIG;AACH,MAAM,MAAM,SAAS,GACjB,aAAa,GACb,OAAO,GACP,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,OAAO,GACP,UAAU,GACV,OAAO,CAAC;AAEZ,yEAAyE;AACzE,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAKpD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAA+C,CAAC;AAEhG;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,0DAA0D;AAC1D,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,qBAAqB,KAAK,MAAM,CAAC;AAExE;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,cAAc,CAAC;AAExF;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sFAAsF;IACtF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IACnD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAAC;IAChD;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;AAED,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,cAAc,GAChD,CAAC;IAAE,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC;AAE3E,0FAA0F;AAC1F,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,IAAI,qBAAqB,CAEjF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,WAAW,CAUzE","sourcesContent":["import { shimFromSchema } from '../hooks/shim.ts';\nimport type { HarnessHookSchema, ShimAdapter } from '../hooks/shim.ts';\n\n/**\n * Everything smelt knows about one agent harness, in one place.\n *\n * Before this module existed, a harness's facts lived at seven edit sites: the\n * `HarnessId` union, the `HARNESSES` array, a shim file, a case in `planInstall`, a\n * case in `planRemove`, `MANAGED_EVENTS`, a test fixture — plus a hand-typed id list\n * in the `--harness` help text, which could not be derived because the registry lived\n * in `cli/hooks.ts` and `cli/hooks.ts` imports `CLI_NAME` from `cli/args.ts`. The\n * registry was in the wrong module, and the cycle was the proof.\n *\n * A `HarnessProfile` is the single adapter carrying every per-harness fact, and the\n * registry (`registry.ts`) is `Record<HarnessId, HarnessProfile>` so totality stays a\n * compile error: a new `HarnessId` without a profile does not build. It imports\n * nothing from `cli/`, so `cli/args.ts` derives the `--harness` list and every other\n * rendered view is derived too — the managed event names, the files a re-run reads its\n * toggles back from, the id list in an error message.\n *\n * Installing and removing are **data**, not switch statements: `install` is the list\n * of artefacts `smelt hooks install` writes for this harness, and each artefact's kind\n * is also how `smelt hooks remove` takes it back out — a JSON hook file is merged and\n * strip-merged, a marker block is upserted and stripped, a file that is entirely ours\n * is written and deleted. `planInstall`/`planRemove` fold over the list.\n *\n * The optional sections state capability, never invent it: a harness ships a shim\n * exactly when it carries a {@link hooks} schema (or, for a harness a table cannot\n * express, a hand-written {@link shim}), and only such a profile has a shim script\n * path. What a profile does not claim, the installer does not write.\n */\nexport interface HarnessProfile {\n readonly id: HarnessId;\n /** The harness's own name, as its makers spell it. Shown wherever a tier is. */\n readonly name: string;\n /**\n * The name a *list* of harnesses uses, where the maker's own spelling carries a\n * category suffix a list does not need — \"Codex CLI\" is \"Codex\" in a tier clause,\n * \"Hermes Agent\" is \"Hermes\". Optional, and defaulted by {@link harnessLabel} to\n * {@link name}: only a harness whose prose name differs from its full name carries\n * one, and it carries it here rather than in each of the four places a tier list is\n * rendered. \"Claude Code\" has no suffix to drop, and so has none.\n */\n readonly shortName?: string;\n readonly tier: HarnessTier;\n /** Paths (relative to the project) whose existence means \"this harness is in use here\". */\n readonly detect: readonly string[];\n /** Paths relative to the user's home directory — \"installed on this machine\". */\n readonly detectHome: readonly string[];\n /** The standing-instructions file this harness reads (capability matrix column d). */\n readonly instructionFile: string;\n /**\n * The standing-instructions layer — belt and braces under every shim, and the *only*\n * layer an advisory harness has:\n *\n * - `'snippet'`: upsert the shared marker-bracketed block into\n * {@link instructionFile} (a file several harnesses read, like `AGENTS.md`, is\n * planned once); `remove` strips the block and leaves every other byte.\n * - a renderer: a whole file this harness owns, snippet included — KiloCode's rules\n * file carries two manual enforcement legs the shared snippet has no room for;\n * `remove` deletes it.\n */\n readonly instructions: 'snippet' | HarnessFileContent;\n /** Caveats carried from the capability matrix, shown at install time. */\n readonly caveats: readonly string[];\n /**\n * Everything else `hooks install` writes for this harness, in the order it is\n * written and listed. Empty means instructions only: the advisory tier, where\n * nothing enforces anything and the output says so.\n */\n readonly install: readonly HarnessInstallStep[];\n /**\n * This harness's native pre-tool hook schema, as data. Present exactly when the\n * harness ships a shim script (`dist/hooks/shims/<id>.js`) — absent for the advisory\n * tier, and for opencode, whose hook API is a JavaScript plugin rather than a stdin\n * schema.\n */\n readonly hooks?: HarnessHookSchema;\n /**\n * The escape hatch: a hand-written adapter, for a harness whose schema a table\n * cannot express. Wins over {@link hooks} when both are present.\n */\n readonly shim?: ShimAdapter;\n}\n\n/** How much smelt is willing to claim about a harness. */\nexport type HarnessTier = 'verified' | 'experimental' | 'advisory';\n\n/**\n * Every harness the preset knows. The registry is keyed by this union, so adding an id\n * without writing its profile is a compile error — and the `--harness` help list, the\n * wizard's table and every error message read the registry, never a second list.\n */\nexport type HarnessId =\n | 'claude-code'\n | 'codex'\n | 'gemini'\n | 'grok'\n | 'hermes'\n | 'cursor'\n | 'opencode'\n | 'cline'\n | 'kilocode'\n | 'aider';\n\n/** One line of honesty per tier, shown wherever a tier label appears. */\nexport const TIER_HONESTY: Record<HarnessTier, string> = {\n verified: 'schema verified against primary docs and pinned by fixtures',\n experimental:\n 'schema mapped from the 2026-09-02 capability matrix, not yet smoke-tested against the real binary',\n advisory: 'no usable hook API — instructions only, nothing enforces them',\n};\n\n/**\n * The tiers, in the order every rendered tier list walks them — most claimed first.\n * {@link TIER_HONESTY}'s key order *is* that order, so a tier cannot exist without a\n * line saying what it means, and the order lives once.\n */\nexport const HARNESS_TIERS: readonly HarnessTier[] = Object.keys(TIER_HONESTY) as HarnessTier[];\n\n/**\n * How a list of harnesses spells this one: {@link HarnessProfile.shortName} where the\n * maker's own name carries a suffix a list does not need, else {@link\n * HarnessProfile.name}. The help's tier clause, the wizard's sentences and the site's\n * tier table all render through this, so they cannot spell a harness three ways.\n */\nexport function harnessLabel(profile: HarnessProfile): string {\n return profile.shortName ?? profile.name;\n}\n\n/**\n * What the wizard settled on, as the installer's renderers see it: the toggles, the\n * guard's settings, and the project directory every path is relative to. A renderer\n * takes this and returns bytes; nothing writes.\n */\nexport interface HarnessInstallContext {\n /** Project directory: every path a renderer emits is portable relative to it. */\n readonly cwd: string;\n readonly guard: boolean;\n readonly statsOnStop: boolean;\n readonly mapOnStart: boolean;\n /** Opt-in `smelt agents lint` at session start — shares SessionStart with the map. */\n readonly lintOnStart: boolean;\n readonly thresholdBytes: number;\n /** The `--budget` every suggested command and the snippet quote. */\n readonly budgetBytes: number;\n}\n\n/** A file's bytes, rendered from the wizard's choices. */\nexport type HarnessFileContent = (ctx: HarnessInstallContext) => string;\n\n/**\n * One artefact `hooks install` writes. The kind is also the un-write: `json-hooks` is\n * merged in and strip-merged out, `marker-block` is upserted and stripped,\n * `own-file` is written and deleted.\n */\nexport type HarnessInstallStep = HarnessJsonHooks | HarnessMarkerBlock | HarnessOwnFile;\n\n/**\n * A JSON settings/hooks file the harness reads. Our entries are merged in\n * byte-faithfully and, on `remove`, stripped back out with everything foreign left\n * exactly as it was.\n */\nexport interface HarnessJsonHooks {\n readonly kind: 'json-hooks';\n /** Project-relative path of the file. */\n readonly file: string;\n /** The pre-tool event, in this harness's spelling (`PreToolUse`, `BeforeTool`, …). */\n readonly event: string;\n /**\n * One hook entry per matcher — the tool names the guard wants to see. `undefined`\n * is a matcher-less entry, for a harness whose hook fires on every tool.\n */\n readonly matchers: readonly (string | undefined)[];\n /**\n * The entry shape: `'command-list'` is Claude Code's `{matcher, hooks:[{type,\n * command}]}`, which Codex, Gemini and Grok mirror; `'bare-command'` is Cursor's\n * `{command}`.\n */\n readonly entry: 'command-list' | 'bare-command';\n /**\n * True for a harness whose schema also carries the session-lifecycle events this\n * preset offers — `smelt stats` on Stop, `smelt map` and `smelt agents lint` on\n * SessionStart. The other\n * harnesses wire the guard only, and the wizard's toggles say so.\n */\n readonly lifecycle: boolean;\n /** Top-level keys a *fresh* file must carry (Cursor's `version: 1`). */\n readonly shape?: { readonly version: number };\n}\n\n/** A marker-bracketed block inside a file smelt shares with its owner. */\nexport interface HarnessMarkerBlock {\n readonly kind: 'marker-block';\n readonly file: string;\n readonly block: HarnessFileContent;\n /** The marker line opening the block — also how `remove` finds it. */\n readonly start: string;\n readonly end: string;\n /**\n * Refuse the file instead of editing it when it already carries `contains` and none\n * of ours: Codex's `config.toml` may have a hand-written `[features]` table, and an\n * installer that merged into it would be editing what it was never asked to.\n */\n readonly skipWhen?: { readonly contains: string; readonly why: string };\n}\n\n/** A file that is entirely smelt's: written whole, deleted whole. */\nexport interface HarnessOwnFile {\n readonly kind: 'own-file';\n readonly file: string;\n readonly content: HarnessFileContent;\n /** chmod after writing (Cline's hook must be executable). */\n readonly mode?: number;\n /** True when the file exists only to wire the guard — the guard toggle gates it. */\n readonly guardOnly: boolean;\n}\n\n/**\n * A profile that ships a shim script. Only these have a shim path: `shimScriptPath`\n * takes one, so a harness with no shim cannot name a script that was never built.\n */\nexport type ShimmedHarnessProfile = HarnessProfile &\n ({ readonly hooks: HarnessHookSchema } | { readonly shim: ShimAdapter });\n\n/** True for a profile that ships a shim script — the narrowing `shimScriptPath` needs. */\nexport function hasShim(profile: HarnessProfile): profile is ShimmedHarnessProfile {\n return profile.hooks !== undefined || profile.shim !== undefined;\n}\n\n/**\n * The adapter this profile's shim script runs: the one its {@link HarnessProfile.hooks}\n * schema describes, or the hand-written escape hatch where it carries one.\n */\nexport function shimAdapterOf(profile: ShimmedHarnessProfile): ShimAdapter {\n const { hooks, shim } = profile;\n if (shim !== undefined) return shim;\n /* v8 ignore next 5 -- unreachable: a ShimmedHarnessProfile carries one or the other */\n if (hooks === undefined) {\n throw new Error(\n `smelt: harness \"${profile.id}\" claims a shim but carries neither a hook schema nor an adapter.`,\n );\n }\n return shimFromSchema(hooks);\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { shimFromSchema } from '../hooks/shim.js';
|
|
2
|
+
/** One line of honesty per tier, shown wherever a tier label appears. */
|
|
3
|
+
export const TIER_HONESTY = {
|
|
4
|
+
verified: 'schema verified against primary docs and pinned by fixtures',
|
|
5
|
+
experimental: 'schema mapped from the 2026-09-02 capability matrix, not yet smoke-tested against the real binary',
|
|
6
|
+
advisory: 'no usable hook API — instructions only, nothing enforces them',
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The tiers, in the order every rendered tier list walks them — most claimed first.
|
|
10
|
+
* {@link TIER_HONESTY}'s key order *is* that order, so a tier cannot exist without a
|
|
11
|
+
* line saying what it means, and the order lives once.
|
|
12
|
+
*/
|
|
13
|
+
export const HARNESS_TIERS = Object.keys(TIER_HONESTY);
|
|
14
|
+
/**
|
|
15
|
+
* How a list of harnesses spells this one: {@link HarnessProfile.shortName} where the
|
|
16
|
+
* maker's own name carries a suffix a list does not need, else {@link
|
|
17
|
+
* HarnessProfile.name}. The help's tier clause, the wizard's sentences and the site's
|
|
18
|
+
* tier table all render through this, so they cannot spell a harness three ways.
|
|
19
|
+
*/
|
|
20
|
+
export function harnessLabel(profile) {
|
|
21
|
+
return profile.shortName ?? profile.name;
|
|
22
|
+
}
|
|
23
|
+
/** True for a profile that ships a shim script — the narrowing `shimScriptPath` needs. */
|
|
24
|
+
export function hasShim(profile) {
|
|
25
|
+
return profile.hooks !== undefined || profile.shim !== undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The adapter this profile's shim script runs: the one its {@link HarnessProfile.hooks}
|
|
29
|
+
* schema describes, or the hand-written escape hatch where it carries one.
|
|
30
|
+
*/
|
|
31
|
+
export function shimAdapterOf(profile) {
|
|
32
|
+
const { hooks, shim } = profile;
|
|
33
|
+
if (shim !== undefined)
|
|
34
|
+
return shim;
|
|
35
|
+
/* v8 ignore next 5 -- unreachable: a ShimmedHarnessProfile carries one or the other */
|
|
36
|
+
if (hooks === undefined) {
|
|
37
|
+
throw new Error(`smelt: harness "${profile.id}" claims a shim but carries neither a hook schema nor an adapter.`);
|
|
38
|
+
}
|
|
39
|
+
return shimFromSchema(hooks);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/harness/profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAyGlD,yEAAyE;AACzE,MAAM,CAAC,MAAM,YAAY,GAAgC;IACvD,QAAQ,EAAE,6DAA6D;IACvE,YAAY,EACV,mGAAmG;IACrG,QAAQ,EAAE,+DAA+D;CAC1E,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAA2B,MAAM,CAAC,IAAI,CAAC,YAAY,CAAkB,CAAC;AAEhG;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,OAAuB;IAClD,OAAO,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;AAC3C,CAAC;AAiGD,0FAA0F;AAC1F,MAAM,UAAU,OAAO,CAAC,OAAuB;IAC7C,OAAO,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC;AACnE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAA8B;IAC1D,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IAChC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACpC,uFAAuF;IACvF,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,mBAAmB,OAAO,CAAC,EAAE,mEAAmE,CACjG,CAAC;IACJ,CAAC;IACD,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["import { shimFromSchema } from '../hooks/shim.ts';\nimport type { HarnessHookSchema, ShimAdapter } from '../hooks/shim.ts';\n\n/**\n * Everything smelt knows about one agent harness, in one place.\n *\n * Before this module existed, a harness's facts lived at seven edit sites: the\n * `HarnessId` union, the `HARNESSES` array, a shim file, a case in `planInstall`, a\n * case in `planRemove`, `MANAGED_EVENTS`, a test fixture — plus a hand-typed id list\n * in the `--harness` help text, which could not be derived because the registry lived\n * in `cli/hooks.ts` and `cli/hooks.ts` imports `CLI_NAME` from `cli/args.ts`. The\n * registry was in the wrong module, and the cycle was the proof.\n *\n * A `HarnessProfile` is the single adapter carrying every per-harness fact, and the\n * registry (`registry.ts`) is `Record<HarnessId, HarnessProfile>` so totality stays a\n * compile error: a new `HarnessId` without a profile does not build. It imports\n * nothing from `cli/`, so `cli/args.ts` derives the `--harness` list and every other\n * rendered view is derived too — the managed event names, the files a re-run reads its\n * toggles back from, the id list in an error message.\n *\n * Installing and removing are **data**, not switch statements: `install` is the list\n * of artefacts `smelt hooks install` writes for this harness, and each artefact's kind\n * is also how `smelt hooks remove` takes it back out — a JSON hook file is merged and\n * strip-merged, a marker block is upserted and stripped, a file that is entirely ours\n * is written and deleted. `planInstall`/`planRemove` fold over the list.\n *\n * The optional sections state capability, never invent it: a harness ships a shim\n * exactly when it carries a {@link hooks} schema (or, for a harness a table cannot\n * express, a hand-written {@link shim}), and only such a profile has a shim script\n * path. What a profile does not claim, the installer does not write.\n */\nexport interface HarnessProfile {\n readonly id: HarnessId;\n /** The harness's own name, as its makers spell it. Shown wherever a tier is. */\n readonly name: string;\n /**\n * The name a *list* of harnesses uses, where the maker's own spelling carries a\n * category suffix a list does not need — \"Codex CLI\" is \"Codex\" in a tier clause,\n * \"Hermes Agent\" is \"Hermes\". Optional, and defaulted by {@link harnessLabel} to\n * {@link name}: only a harness whose prose name differs from its full name carries\n * one, and it carries it here rather than in each of the four places a tier list is\n * rendered. \"Claude Code\" has no suffix to drop, and so has none.\n */\n readonly shortName?: string;\n readonly tier: HarnessTier;\n /** Paths (relative to the project) whose existence means \"this harness is in use here\". */\n readonly detect: readonly string[];\n /** Paths relative to the user's home directory — \"installed on this machine\". */\n readonly detectHome: readonly string[];\n /** The standing-instructions file this harness reads (capability matrix column d). */\n readonly instructionFile: string;\n /**\n * The standing-instructions layer — belt and braces under every shim, and the *only*\n * layer an advisory harness has:\n *\n * - `'snippet'`: upsert the shared marker-bracketed block into\n * {@link instructionFile} (a file several harnesses read, like `AGENTS.md`, is\n * planned once); `remove` strips the block and leaves every other byte.\n * - a renderer: a whole file this harness owns, snippet included — KiloCode's rules\n * file carries two manual enforcement legs the shared snippet has no room for;\n * `remove` deletes it.\n */\n readonly instructions: 'snippet' | HarnessFileContent;\n /** Caveats carried from the capability matrix, shown at install time. */\n readonly caveats: readonly string[];\n /**\n * Everything else `hooks install` writes for this harness, in the order it is\n * written and listed. Empty means instructions only: the advisory tier, where\n * nothing enforces anything and the output says so.\n */\n readonly install: readonly HarnessInstallStep[];\n /**\n * This harness's native pre-tool hook schema, as data. Present exactly when the\n * harness ships a shim script (`dist/hooks/shims/<id>.js`) — absent for the advisory\n * tier, and for opencode, whose hook API is a JavaScript plugin rather than a stdin\n * schema.\n */\n readonly hooks?: HarnessHookSchema;\n /**\n * The escape hatch: a hand-written adapter, for a harness whose schema a table\n * cannot express. Wins over {@link hooks} when both are present.\n */\n readonly shim?: ShimAdapter;\n}\n\n/** How much smelt is willing to claim about a harness. */\nexport type HarnessTier = 'verified' | 'experimental' | 'advisory';\n\n/**\n * Every harness the preset knows. The registry is keyed by this union, so adding an id\n * without writing its profile is a compile error — and the `--harness` help list, the\n * wizard's table and every error message read the registry, never a second list.\n */\nexport type HarnessId =\n | 'claude-code'\n | 'codex'\n | 'gemini'\n | 'grok'\n | 'hermes'\n | 'cursor'\n | 'opencode'\n | 'cline'\n | 'kilocode'\n | 'aider';\n\n/** One line of honesty per tier, shown wherever a tier label appears. */\nexport const TIER_HONESTY: Record<HarnessTier, string> = {\n verified: 'schema verified against primary docs and pinned by fixtures',\n experimental:\n 'schema mapped from the 2026-09-02 capability matrix, not yet smoke-tested against the real binary',\n advisory: 'no usable hook API — instructions only, nothing enforces them',\n};\n\n/**\n * The tiers, in the order every rendered tier list walks them — most claimed first.\n * {@link TIER_HONESTY}'s key order *is* that order, so a tier cannot exist without a\n * line saying what it means, and the order lives once.\n */\nexport const HARNESS_TIERS: readonly HarnessTier[] = Object.keys(TIER_HONESTY) as HarnessTier[];\n\n/**\n * How a list of harnesses spells this one: {@link HarnessProfile.shortName} where the\n * maker's own name carries a suffix a list does not need, else {@link\n * HarnessProfile.name}. The help's tier clause, the wizard's sentences and the site's\n * tier table all render through this, so they cannot spell a harness three ways.\n */\nexport function harnessLabel(profile: HarnessProfile): string {\n return profile.shortName ?? profile.name;\n}\n\n/**\n * What the wizard settled on, as the installer's renderers see it: the toggles, the\n * guard's settings, and the project directory every path is relative to. A renderer\n * takes this and returns bytes; nothing writes.\n */\nexport interface HarnessInstallContext {\n /** Project directory: every path a renderer emits is portable relative to it. */\n readonly cwd: string;\n readonly guard: boolean;\n readonly statsOnStop: boolean;\n readonly mapOnStart: boolean;\n /** Opt-in `smelt agents lint` at session start — shares SessionStart with the map. */\n readonly lintOnStart: boolean;\n readonly thresholdBytes: number;\n /** The `--budget` every suggested command and the snippet quote. */\n readonly budgetBytes: number;\n}\n\n/** A file's bytes, rendered from the wizard's choices. */\nexport type HarnessFileContent = (ctx: HarnessInstallContext) => string;\n\n/**\n * One artefact `hooks install` writes. The kind is also the un-write: `json-hooks` is\n * merged in and strip-merged out, `marker-block` is upserted and stripped,\n * `own-file` is written and deleted.\n */\nexport type HarnessInstallStep = HarnessJsonHooks | HarnessMarkerBlock | HarnessOwnFile;\n\n/**\n * A JSON settings/hooks file the harness reads. Our entries are merged in\n * byte-faithfully and, on `remove`, stripped back out with everything foreign left\n * exactly as it was.\n */\nexport interface HarnessJsonHooks {\n readonly kind: 'json-hooks';\n /** Project-relative path of the file. */\n readonly file: string;\n /** The pre-tool event, in this harness's spelling (`PreToolUse`, `BeforeTool`, …). */\n readonly event: string;\n /**\n * One hook entry per matcher — the tool names the guard wants to see. `undefined`\n * is a matcher-less entry, for a harness whose hook fires on every tool.\n */\n readonly matchers: readonly (string | undefined)[];\n /**\n * The entry shape: `'command-list'` is Claude Code's `{matcher, hooks:[{type,\n * command}]}`, which Codex, Gemini and Grok mirror; `'bare-command'` is Cursor's\n * `{command}`.\n */\n readonly entry: 'command-list' | 'bare-command';\n /**\n * True for a harness whose schema also carries the session-lifecycle events this\n * preset offers — `smelt stats` on Stop, `smelt map` and `smelt agents lint` on\n * SessionStart. The other\n * harnesses wire the guard only, and the wizard's toggles say so.\n */\n readonly lifecycle: boolean;\n /** Top-level keys a *fresh* file must carry (Cursor's `version: 1`). */\n readonly shape?: { readonly version: number };\n}\n\n/** A marker-bracketed block inside a file smelt shares with its owner. */\nexport interface HarnessMarkerBlock {\n readonly kind: 'marker-block';\n readonly file: string;\n readonly block: HarnessFileContent;\n /** The marker line opening the block — also how `remove` finds it. */\n readonly start: string;\n readonly end: string;\n /**\n * Refuse the file instead of editing it when it already carries `contains` and none\n * of ours: Codex's `config.toml` may have a hand-written `[features]` table, and an\n * installer that merged into it would be editing what it was never asked to.\n */\n readonly skipWhen?: { readonly contains: string; readonly why: string };\n}\n\n/** A file that is entirely smelt's: written whole, deleted whole. */\nexport interface HarnessOwnFile {\n readonly kind: 'own-file';\n readonly file: string;\n readonly content: HarnessFileContent;\n /** chmod after writing (Cline's hook must be executable). */\n readonly mode?: number;\n /** True when the file exists only to wire the guard — the guard toggle gates it. */\n readonly guardOnly: boolean;\n}\n\n/**\n * A profile that ships a shim script. Only these have a shim path: `shimScriptPath`\n * takes one, so a harness with no shim cannot name a script that was never built.\n */\nexport type ShimmedHarnessProfile = HarnessProfile &\n ({ readonly hooks: HarnessHookSchema } | { readonly shim: ShimAdapter });\n\n/** True for a profile that ships a shim script — the narrowing `shimScriptPath` needs. */\nexport function hasShim(profile: HarnessProfile): profile is ShimmedHarnessProfile {\n return profile.hooks !== undefined || profile.shim !== undefined;\n}\n\n/**\n * The adapter this profile's shim script runs: the one its {@link HarnessProfile.hooks}\n * schema describes, or the hand-written escape hatch where it carries one.\n */\nexport function shimAdapterOf(profile: ShimmedHarnessProfile): ShimAdapter {\n const { hooks, shim } = profile;\n if (shim !== undefined) return shim;\n /* v8 ignore next 5 -- unreachable: a ShimmedHarnessProfile carries one or the other */\n if (hooks === undefined) {\n throw new Error(\n `smelt: harness \"${profile.id}\" claims a shim but carries neither a hook schema nor an adapter.`,\n );\n }\n return shimFromSchema(hooks);\n}\n"]}
|