@smeltjs/core 0.4.0 → 0.6.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/dist/apply.d.ts.map +1 -1
- package/dist/apply.js +8 -2
- package/dist/apply.js.map +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js +9 -4
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/doctor.d.ts +66 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +137 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/hooks.d.ts +36 -2
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +108 -64
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +12 -16
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/installed.d.ts +56 -0
- package/dist/cli/installed.d.ts.map +1 -0
- package/dist/cli/installed.js +105 -0
- package/dist/cli/installed.js.map +1 -0
- package/dist/cli/lava.d.ts +31 -0
- package/dist/cli/lava.d.ts.map +1 -0
- package/dist/cli/lava.js +82 -0
- package/dist/cli/lava.js.map +1 -0
- package/dist/cli/report.d.ts +10 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +16 -1
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/setup.d.ts +87 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +513 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/shell.d.ts +11 -4
- package/dist/cli/shell.d.ts.map +1 -1
- package/dist/cli/shell.js.map +1 -1
- package/dist/cli/subcommands/agents.d.ts.map +1 -1
- package/dist/cli/subcommands/agents.js +2 -1
- package/dist/cli/subcommands/agents.js.map +1 -1
- package/dist/cli/subcommands/doctor.d.ts +15 -0
- package/dist/cli/subcommands/doctor.d.ts.map +1 -0
- package/dist/cli/subcommands/doctor.js +43 -0
- package/dist/cli/subcommands/doctor.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +16 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -1
- package/dist/cli/subcommands/flags.js +43 -4
- package/dist/cli/subcommands/flags.js.map +1 -1
- package/dist/cli/subcommands/hooks.d.ts.map +1 -1
- package/dist/cli/subcommands/hooks.js +10 -2
- package/dist/cli/subcommands/hooks.js.map +1 -1
- package/dist/cli/subcommands/init.d.ts.map +1 -1
- package/dist/cli/subcommands/init.js +2 -1
- package/dist/cli/subcommands/init.js.map +1 -1
- package/dist/cli/subcommands/registry.d.ts +3 -1
- package/dist/cli/subcommands/registry.d.ts.map +1 -1
- package/dist/cli/subcommands/registry.js +6 -0
- package/dist/cli/subcommands/registry.js.map +1 -1
- package/dist/cli/subcommands/setup.d.ts +23 -0
- package/dist/cli/subcommands/setup.d.ts.map +1 -0
- package/dist/cli/subcommands/setup.js +87 -0
- package/dist/cli/subcommands/setup.js.map +1 -0
- package/dist/cli/subcommands/smelt.d.ts +3 -0
- package/dist/cli/subcommands/smelt.d.ts.map +1 -1
- package/dist/cli/subcommands/smelt.js +5 -1
- package/dist/cli/subcommands/smelt.js.map +1 -1
- package/dist/cli/subcommands/stats.d.ts +8 -3
- package/dist/cli/subcommands/stats.d.ts.map +1 -1
- package/dist/cli/subcommands/stats.js +15 -5
- package/dist/cli/subcommands/stats.js.map +1 -1
- package/dist/cli/subcommands/subcommand.d.ts +1 -1
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
- package/dist/cli/subcommands/subcommand.js.map +1 -1
- package/dist/cli/wizard.d.ts +72 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +86 -0
- package/dist/cli/wizard.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/harness/claude-code.d.ts.map +1 -1
- package/dist/harness/claude-code.js +10 -0
- package/dist/harness/claude-code.js.map +1 -1
- package/dist/harness/codex.d.ts.map +1 -1
- package/dist/harness/codex.js +1 -0
- package/dist/harness/codex.js.map +1 -1
- package/dist/harness/grok.d.ts.map +1 -1
- package/dist/harness/grok.js +4 -1
- package/dist/harness/grok.js.map +1 -1
- package/dist/harness/kilocode.d.ts.map +1 -1
- package/dist/harness/kilocode.js +1 -1
- package/dist/harness/kilocode.js.map +1 -1
- package/dist/harness/opencode.d.ts.map +1 -1
- package/dist/harness/opencode.js +10 -0
- package/dist/harness/opencode.js.map +1 -1
- package/dist/harness/profile.d.ts +23 -2
- package/dist/harness/profile.d.ts.map +1 -1
- package/dist/harness/profile.js.map +1 -1
- package/dist/harness/snippet.d.ts +14 -1
- package/dist/harness/snippet.d.ts.map +1 -1
- package/dist/harness/snippet.js +18 -2
- package/dist/harness/snippet.js.map +1 -1
- package/dist/hooks/focus-terms.d.ts +57 -0
- package/dist/hooks/focus-terms.d.ts.map +1 -0
- package/dist/hooks/focus-terms.js +230 -0
- package/dist/hooks/focus-terms.js.map +1 -0
- package/dist/hooks/guard-core.d.ts +8 -16
- package/dist/hooks/guard-core.d.ts.map +1 -1
- package/dist/hooks/guard-core.js +22 -120
- package/dist/hooks/guard-core.js.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/ops/index.d.ts +3 -3
- package/dist/ops/index.d.ts.map +1 -1
- package/dist/ops/index.js +2 -2
- package/dist/ops/index.js.map +1 -1
- package/dist/ops/verbs.d.ts +57 -1
- package/dist/ops/verbs.d.ts.map +1 -1
- package/dist/ops/verbs.js +49 -1
- package/dist/ops/verbs.js.map +1 -1
- package/dist/plan/auto.d.ts +9 -2
- package/dist/plan/auto.d.ts.map +1 -1
- package/dist/plan/auto.js +14 -2
- package/dist/plan/auto.js.map +1 -1
- package/dist/plan/diff.d.ts +45 -0
- package/dist/plan/diff.d.ts.map +1 -0
- package/dist/plan/diff.js +284 -0
- package/dist/plan/diff.js.map +1 -0
- package/dist/plan/json.d.ts +37 -0
- package/dist/plan/json.d.ts.map +1 -0
- package/dist/plan/json.js +181 -0
- package/dist/plan/json.js.map +1 -0
- package/dist/plan/kind.d.ts +28 -0
- package/dist/plan/kind.d.ts.map +1 -0
- package/dist/plan/kind.js +57 -0
- package/dist/plan/kind.js.map +1 -0
- package/dist/plan/offsets.d.ts +8 -0
- package/dist/plan/offsets.d.ts.map +1 -0
- package/dist/plan/offsets.js +19 -0
- package/dist/plan/offsets.js.map +1 -0
- package/dist/plan/planners.d.ts +15 -5
- package/dist/plan/planners.d.ts.map +1 -1
- package/dist/plan/planners.js +13 -5
- package/dist/plan/planners.js.map +1 -1
- package/dist/plan/structural.d.ts.map +1 -1
- package/dist/plan/structural.js +106 -14
- package/dist/plan/structural.js.map +1 -1
- package/dist/retrieve.d.ts +22 -1
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +59 -0
- package/dist/retrieve.js.map +1 -1
- package/dist/setup/recipe.d.ts +73 -0
- package/dist/setup/recipe.d.ts.map +1 -0
- package/dist/setup/recipe.js +76 -0
- package/dist/setup/recipe.js.map +1 -0
- package/dist/smelter.d.ts +4 -0
- package/dist/smelter.d.ts.map +1 -1
- package/dist/smelter.js +4 -0
- package/dist/smelter.js.map +1 -1
- package/dist/stats.d.ts +16 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +29 -0
- package/dist/stats.js.map +1 -1
- package/dist/store-dir.d.ts +9 -2
- package/dist/store-dir.d.ts.map +1 -1
- package/dist/store-dir.js +48 -7
- package/dist/store-dir.js.map +1 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +12 -2
- package/dist/store.js.map +1 -1
- package/dist/text/json-edit.d.ts +15 -1
- package/dist/text/json-edit.d.ts.map +1 -1
- package/dist/text/json-edit.js +76 -5
- package/dist/text/json-edit.js.map +1 -1
- package/dist/types.d.ts +116 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/harness/profile.ts"],"names":[],"mappings":"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","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"]}
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/harness/profile.ts"],"names":[],"mappings":"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","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 /** The release writing these bytes — stamped into shared blocks for `smelt doctor`. */\n readonly writtenBy?: 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, `mcp-registration` is nested-merged in and\n * lifted back out.\n */\nexport type HarnessInstallStep =\n HarnessJsonHooks | HarnessMarkerBlock | HarnessOwnFile | HarnessMcpRegistration;\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 * An MCP server registration inside a JSON config the harness reads — Claude Code's\n * `.mcp.json` (`mcpServers.smelt`), opencode's `opencode.json` (`mcp.smelt`). The\n * entry is merged in byte-faithfully beside any other servers the user registered,\n * and on `remove` it is lifted back out; a container this install created empty is\n * removed with it, so a file that never carried the key round-trips byte-identical.\n * Harnesses whose registration is TOML (Codex, Grok) do not declare this step — a\n * hand-edit into TOML is exactly the edit `text/json-edit.ts` exists to refuse.\n */\nexport interface HarnessMcpRegistration {\n readonly kind: 'mcp-registration';\n /** Project-relative path of the config file. */\n readonly file: string;\n /** The container key, then the server's name: `['mcpServers', 'smelt']`. */\n readonly path: readonly [string, string];\n /** The server entry as a JSON value — the bytes are the editor's. */\n readonly entry: (ctx: HarnessInstallContext) => unknown;\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"]}
|
|
@@ -1 +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"]}
|
|
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;AAwHD,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 /** The release writing these bytes — stamped into shared blocks for `smelt doctor`. */\n readonly writtenBy?: 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, `mcp-registration` is nested-merged in and\n * lifted back out.\n */\nexport type HarnessInstallStep =\n HarnessJsonHooks | HarnessMarkerBlock | HarnessOwnFile | HarnessMcpRegistration;\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 * An MCP server registration inside a JSON config the harness reads — Claude Code's\n * `.mcp.json` (`mcpServers.smelt`), opencode's `opencode.json` (`mcp.smelt`). The\n * entry is merged in byte-faithfully beside any other servers the user registered,\n * and on `remove` it is lifted back out; a container this install created empty is\n * removed with it, so a file that never carried the key round-trips byte-identical.\n * Harnesses whose registration is TOML (Codex, Grok) do not declare this step — a\n * hand-edit into TOML is exactly the edit `text/json-edit.ts` exists to refuse.\n */\nexport interface HarnessMcpRegistration {\n readonly kind: 'mcp-registration';\n /** Project-relative path of the config file. */\n readonly file: string;\n /** The container key, then the server's name: `['mcpServers', 'smelt']`. */\n readonly path: readonly [string, string];\n /** The server entry as a JSON value — the bytes are the editor's. */\n readonly entry: (ctx: HarnessInstallContext) => unknown;\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"]}
|
|
@@ -10,10 +10,23 @@ export declare const SNIPPET_START_HASH = "# smelt:hooks v1 start";
|
|
|
10
10
|
export declare const SNIPPET_END_HASH = "# smelt:hooks v1 end";
|
|
11
11
|
/** Substring that identifies a file (or JSON hook entry) as written by this installer. */
|
|
12
12
|
export declare const OURS_TOKEN = "smelt:hooks";
|
|
13
|
+
/**
|
|
14
|
+
* The version stamp, as the block's second line: which release wrote these bytes.
|
|
15
|
+
* The markers above stay byte-stable across releases — an upgrade must find the old
|
|
16
|
+
* block to replace it — so the version travels *inside* the block, where replacing
|
|
17
|
+
* the block replaces it. `smelt doctor` reads it back with {@link snippetStampVersion};
|
|
18
|
+
* a block without the line was written before stamping existed, and doctor says so
|
|
19
|
+
* instead of guessing a version nobody recorded.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SNIPPET_STAMP_LINE: (version: string) => string;
|
|
22
|
+
/** The version a block was written by, or `undefined` when it predates stamping. */
|
|
23
|
+
export declare function snippetStampVersion(text: string): string | undefined;
|
|
13
24
|
/**
|
|
14
25
|
* The instruction snippet — belt and braces under every shim, and the *only* layer
|
|
15
26
|
* for advisory harnesses. It teaches the three commands, and in particular what to do
|
|
16
27
|
* after a guard deny: run the named replacement, then `smelt retrieve` per marker.
|
|
28
|
+
* `writtenBy` stamps the block for `smelt doctor`; omitted (legacy callers) the block
|
|
29
|
+
* simply carries no version line.
|
|
17
30
|
*/
|
|
18
|
-
export declare function instructionSnippet(thresholdBytes: number, budgetBytes: number): string;
|
|
31
|
+
export declare function instructionSnippet(thresholdBytes: number, budgetBytes: number, writtenBy?: string): string;
|
|
19
32
|
//# sourceMappingURL=snippet.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snippet.d.ts","sourceRoot":"","sources":["../../src/harness/snippet.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,kCAAkC,CAAC;AAChE,eAAO,MAAM,cAAc,gCAAgC,CAAC;AAC5D,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD,0FAA0F;AAC1F,eAAO,MAAM,UAAU,gBAAgB,CAAC;AAExC
|
|
1
|
+
{"version":3,"file":"snippet.d.ts","sourceRoot":"","sources":["../../src/harness/snippet.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,kCAAkC,CAAC;AAChE,eAAO,MAAM,cAAc,gCAAgC,CAAC;AAC5D,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD,0FAA0F;AAC1F,eAAO,MAAM,UAAU,gBAAgB,CAAC;AAExC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,YAAa,MAAM,KAAG,MACO,CAAC;AAE7D,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIpE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAuBR","sourcesContent":["/**\n * The text every harness shares: the marker lines that bracket a block this installer\n * owns inside somebody else's file, the token that identifies a hook entry as ours,\n * and the instruction snippet itself.\n */\n\n/** Marker lines bracketing every block this installer owns inside a shared file. */\nexport const SNIPPET_START_MD = '<!-- smelt:hooks v1 start -->';\nexport const SNIPPET_END_MD = '<!-- smelt:hooks v1 end -->';\nexport const SNIPPET_START_HASH = '# smelt:hooks v1 start';\nexport const SNIPPET_END_HASH = '# smelt:hooks v1 end';\n\n/** Substring that identifies a file (or JSON hook entry) as written by this installer. */\nexport const OURS_TOKEN = 'smelt:hooks';\n\n/**\n * The version stamp, as the block's second line: which release wrote these bytes.\n * The markers above stay byte-stable across releases — an upgrade must find the old\n * block to replace it — so the version travels *inside* the block, where replacing\n * the block replaces it. `smelt doctor` reads it back with {@link snippetStampVersion};\n * a block without the line was written before stamping existed, and doctor says so\n * instead of guessing a version nobody recorded.\n */\nexport const SNIPPET_STAMP_LINE = (version: string): string =>\n `<!-- smelt:hooks written-by @smeltjs/core ${version} -->`;\n\n/** The version a block was written by, or `undefined` when it predates stamping. */\nexport function snippetStampVersion(text: string): string | undefined {\n return /<!-- smelt:hooks written-by @smeltjs\\/core (\\d+\\.\\d+\\.\\d+)(?:[-+][^>]*)? -->/u.exec(\n text,\n )?.[1];\n}\n\n/**\n * The instruction snippet — belt and braces under every shim, and the *only* layer\n * for advisory harnesses. It teaches the three commands, and in particular what to do\n * after a guard deny: run the named replacement, then `smelt retrieve` per marker.\n * `writtenBy` stamps the block for `smelt doctor`; omitted (legacy callers) the block\n * simply carries no version line.\n */\nexport function instructionSnippet(\n thresholdBytes: number,\n budgetBytes: number,\n writtenBy?: string,\n): string {\n const stamp = writtenBy === undefined ? '' : `${SNIPPET_STAMP_LINE(writtenBy)}\\n`;\n return `${SNIPPET_START_MD}\n${stamp}\n## smelt — context discipline\n\nThis project uses [smelt](https://github.com/smeltjs/smelt) to keep large tool output\nout of the context window, reversibly.\n\n- Do not read files over ${String(thresholdBytes)} bytes raw. Run\n \\`smelt <file> --budget ${String(budgetBytes)} --focus <what you are looking for>\\`\n instead (repeat \\`--focus\\` per term). Focused regions survive verbatim; everything\n else collapses into a one-line marker stating what was removed.\n- Every marker ends in \\`retrieve(\"hash\")\\`. \\`smelt retrieve <hash>\\` prints the\n exact original bytes back. Retrieve what you actually need — retrievals are counted,\n and \\`smelt stats\\` reports the honest expansion rate.\n- For orientation, \\`smelt map . --budget ${String(budgetBytes)}\\` prints a ranked\n symbol map of the repository.\n- If a smelt guard hook denies a raw read, run the exact replacement command named in\n the denial, then \\`smelt retrieve\\` any marker you need expanded.\n\n${SNIPPET_END_MD}\n`;\n}\n"]}
|
package/dist/harness/snippet.js
CHANGED
|
@@ -10,14 +10,30 @@ export const SNIPPET_START_HASH = '# smelt:hooks v1 start';
|
|
|
10
10
|
export const SNIPPET_END_HASH = '# smelt:hooks v1 end';
|
|
11
11
|
/** Substring that identifies a file (or JSON hook entry) as written by this installer. */
|
|
12
12
|
export const OURS_TOKEN = 'smelt:hooks';
|
|
13
|
+
/**
|
|
14
|
+
* The version stamp, as the block's second line: which release wrote these bytes.
|
|
15
|
+
* The markers above stay byte-stable across releases — an upgrade must find the old
|
|
16
|
+
* block to replace it — so the version travels *inside* the block, where replacing
|
|
17
|
+
* the block replaces it. `smelt doctor` reads it back with {@link snippetStampVersion};
|
|
18
|
+
* a block without the line was written before stamping existed, and doctor says so
|
|
19
|
+
* instead of guessing a version nobody recorded.
|
|
20
|
+
*/
|
|
21
|
+
export const SNIPPET_STAMP_LINE = (version) => `<!-- smelt:hooks written-by @smeltjs/core ${version} -->`;
|
|
22
|
+
/** The version a block was written by, or `undefined` when it predates stamping. */
|
|
23
|
+
export function snippetStampVersion(text) {
|
|
24
|
+
return /<!-- smelt:hooks written-by @smeltjs\/core (\d+\.\d+\.\d+)(?:[-+][^>]*)? -->/u.exec(text)?.[1];
|
|
25
|
+
}
|
|
13
26
|
/**
|
|
14
27
|
* The instruction snippet — belt and braces under every shim, and the *only* layer
|
|
15
28
|
* for advisory harnesses. It teaches the three commands, and in particular what to do
|
|
16
29
|
* after a guard deny: run the named replacement, then `smelt retrieve` per marker.
|
|
30
|
+
* `writtenBy` stamps the block for `smelt doctor`; omitted (legacy callers) the block
|
|
31
|
+
* simply carries no version line.
|
|
17
32
|
*/
|
|
18
|
-
export function instructionSnippet(thresholdBytes, budgetBytes) {
|
|
33
|
+
export function instructionSnippet(thresholdBytes, budgetBytes, writtenBy) {
|
|
34
|
+
const stamp = writtenBy === undefined ? '' : `${SNIPPET_STAMP_LINE(writtenBy)}\n`;
|
|
19
35
|
return `${SNIPPET_START_MD}
|
|
20
|
-
|
|
36
|
+
${stamp}
|
|
21
37
|
## smelt — context discipline
|
|
22
38
|
|
|
23
39
|
This project uses [smelt](https://github.com/smeltjs/smelt) to keep large tool output
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snippet.js","sourceRoot":"","sources":["../../src/harness/snippet.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,+BAA+B,CAAC;AAChE,MAAM,CAAC,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAC3D,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEvD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG,aAAa,CAAC;AAExC
|
|
1
|
+
{"version":3,"file":"snippet.js","sourceRoot":"","sources":["../../src/harness/snippet.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,+BAA+B,CAAC;AAChE,MAAM,CAAC,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAC3D,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEvD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG,aAAa,CAAC;AAExC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAe,EAAU,EAAE,CAC5D,6CAA6C,OAAO,MAAM,CAAC;AAE7D,oFAAoF;AACpF,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,+EAA+E,CAAC,IAAI,CACzF,IAAI,CACL,EAAE,CAAC,CAAC,CAAC,CAAC;AACT,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,cAAsB,EACtB,WAAmB,EACnB,SAAkB;IAElB,MAAM,KAAK,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,SAAS,CAAC,IAAI,CAAC;IAClF,OAAO,GAAG,gBAAgB;EAC1B,KAAK;;;;;;2BAMoB,MAAM,CAAC,cAAc,CAAC;4BACrB,MAAM,CAAC,WAAW,CAAC;;;;;;4CAMH,MAAM,CAAC,WAAW,CAAC;;;;;EAK7D,cAAc;CACf,CAAC;AACF,CAAC","sourcesContent":["/**\n * The text every harness shares: the marker lines that bracket a block this installer\n * owns inside somebody else's file, the token that identifies a hook entry as ours,\n * and the instruction snippet itself.\n */\n\n/** Marker lines bracketing every block this installer owns inside a shared file. */\nexport const SNIPPET_START_MD = '<!-- smelt:hooks v1 start -->';\nexport const SNIPPET_END_MD = '<!-- smelt:hooks v1 end -->';\nexport const SNIPPET_START_HASH = '# smelt:hooks v1 start';\nexport const SNIPPET_END_HASH = '# smelt:hooks v1 end';\n\n/** Substring that identifies a file (or JSON hook entry) as written by this installer. */\nexport const OURS_TOKEN = 'smelt:hooks';\n\n/**\n * The version stamp, as the block's second line: which release wrote these bytes.\n * The markers above stay byte-stable across releases — an upgrade must find the old\n * block to replace it — so the version travels *inside* the block, where replacing\n * the block replaces it. `smelt doctor` reads it back with {@link snippetStampVersion};\n * a block without the line was written before stamping existed, and doctor says so\n * instead of guessing a version nobody recorded.\n */\nexport const SNIPPET_STAMP_LINE = (version: string): string =>\n `<!-- smelt:hooks written-by @smeltjs/core ${version} -->`;\n\n/** The version a block was written by, or `undefined` when it predates stamping. */\nexport function snippetStampVersion(text: string): string | undefined {\n return /<!-- smelt:hooks written-by @smeltjs\\/core (\\d+\\.\\d+\\.\\d+)(?:[-+][^>]*)? -->/u.exec(\n text,\n )?.[1];\n}\n\n/**\n * The instruction snippet — belt and braces under every shim, and the *only* layer\n * for advisory harnesses. It teaches the three commands, and in particular what to do\n * after a guard deny: run the named replacement, then `smelt retrieve` per marker.\n * `writtenBy` stamps the block for `smelt doctor`; omitted (legacy callers) the block\n * simply carries no version line.\n */\nexport function instructionSnippet(\n thresholdBytes: number,\n budgetBytes: number,\n writtenBy?: string,\n): string {\n const stamp = writtenBy === undefined ? '' : `${SNIPPET_STAMP_LINE(writtenBy)}\\n`;\n return `${SNIPPET_START_MD}\n${stamp}\n## smelt — context discipline\n\nThis project uses [smelt](https://github.com/smeltjs/smelt) to keep large tool output\nout of the context window, reversibly.\n\n- Do not read files over ${String(thresholdBytes)} bytes raw. Run\n \\`smelt <file> --budget ${String(budgetBytes)} --focus <what you are looking for>\\`\n instead (repeat \\`--focus\\` per term). Focused regions survive verbatim; everything\n else collapses into a one-line marker stating what was removed.\n- Every marker ends in \\`retrieve(\"hash\")\\`. \\`smelt retrieve <hash>\\` prints the\n exact original bytes back. Retrieve what you actually need — retrievals are counted,\n and \\`smelt stats\\` reports the honest expansion rate.\n- For orientation, \\`smelt map . --budget ${String(budgetBytes)}\\` prints a ranked\n symbol map of the repository.\n- If a smelt guard hook denies a raw read, run the exact replacement command named in\n the denial, then \\`smelt retrieve\\` any marker you need expanded.\n\n${SNIPPET_END_MD}\n`;\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus terms, derived from the command that produced a blob — one derivation, zero
|
|
3
|
+
* imports, shared by the hooks guard and the ops seam.
|
|
4
|
+
*
|
|
5
|
+
* The guard is the producer expert: to decide anything about a `grep` it has already
|
|
6
|
+
* parsed the pattern out of the command. Before this file that knowledge died inside
|
|
7
|
+
* the guard — the deny reason printed `--focus <?>` and the model reinvented what the
|
|
8
|
+
* guard knew. Now the derivation lives here, beside the guard and importing nothing
|
|
9
|
+
* (the guard's latency budget is a stat and an exit, so this must stay a
|
|
10
|
+
* zero-dependency sibling rather than an exception to its no-library-import rule),
|
|
11
|
+
* and `smeltBlob` applies the same function to a caller's `producer` hint — so the
|
|
12
|
+
* guard's rewrite wrap, `smelt --producer` and the `smelt_file` tool cannot disagree
|
|
13
|
+
* about which terms a command names.
|
|
14
|
+
*
|
|
15
|
+
* The question it answers is narrower than "what did the command search for". It is:
|
|
16
|
+
* **which terms, if any, distinguish the output lines the task is about from the ones
|
|
17
|
+
* it is not.** Focus is what the lexical planner keeps; a term every output line
|
|
18
|
+
* carries keeps everything and cuts nothing, exactly when the output is large. So:
|
|
19
|
+
*
|
|
20
|
+
* - a plain `grep`/`rg` prints only matching lines — every line has the pattern, the
|
|
21
|
+
* pattern distinguishes nothing, and the answer is no terms (the planner's
|
|
22
|
+
* head-and-tail rule is the right cut);
|
|
23
|
+
* - a search with context (`-C`, `-A`, `-B`, `--context`…) prints non-matching lines
|
|
24
|
+
* around each hit, and there the pattern is exactly the focus;
|
|
25
|
+
* - a listing search (`-l`, `-c`, `--files-with-matches`…) prints no matching lines
|
|
26
|
+
* at all, so its pattern names nothing in the output;
|
|
27
|
+
* - a producer that states no term (`cat`, `git diff`, `sed`) yields none, and so
|
|
28
|
+
* does a command this parser cannot see whole — unsure means no terms, the same
|
|
29
|
+
* fail-open rule the guard lives under.
|
|
30
|
+
*
|
|
31
|
+
* Every rule above is a fact about what the command prints, stated as data in
|
|
32
|
+
* {@link SEARCH_PROGRAMS}, {@link CONTEXT_FLAGS} and {@link LISTING_FLAGS} — never a
|
|
33
|
+
* guess about the text.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* The terms a producer command names that distinguish output lines the task is about.
|
|
37
|
+
* `[]` whenever the honest answer is "none" — see the module doc. Never throws.
|
|
38
|
+
*/
|
|
39
|
+
export declare function focusTermsFor(command: string | undefined): readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* Split a command into words IF it is one simple command: no pipes, no logic, no
|
|
42
|
+
* redirects, no substitutions, no expansions this code would have to model. Anything
|
|
43
|
+
* else returns `undefined` and the caller treats the command as unknowable — the guard
|
|
44
|
+
* allows, this derivation names no terms.
|
|
45
|
+
*/
|
|
46
|
+
export declare function simpleCommandWords(command: string): readonly string[] | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Every pattern a grep/rg invocation searches for: each explicit `-e`/`--regexp`
|
|
49
|
+
* value if any are given, else the first word that is not a flag or a flag's value.
|
|
50
|
+
* Empty when the parse is not sure.
|
|
51
|
+
*/
|
|
52
|
+
export declare function searchPatterns(words: readonly string[]): readonly string[];
|
|
53
|
+
/** The first pattern of {@link searchPatterns}; `undefined` when there is none. */
|
|
54
|
+
export declare function searchPattern(words: readonly string[]): string | undefined;
|
|
55
|
+
/** Single-quote a value for `sh` unless it is plainly safe bare. */
|
|
56
|
+
export declare function shellQuote(value: string): string;
|
|
57
|
+
//# sourceMappingURL=focus-terms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus-terms.d.ts","sourceRoot":"","sources":["../../src/hooks/focus-terms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA2BH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,MAAM,EAAE,CAS5E;AA0BD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAuCjF;AAoCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CA6B1E;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAE1E;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhD","sourcesContent":["/**\n * Focus terms, derived from the command that produced a blob — one derivation, zero\n * imports, shared by the hooks guard and the ops seam.\n *\n * The guard is the producer expert: to decide anything about a `grep` it has already\n * parsed the pattern out of the command. Before this file that knowledge died inside\n * the guard — the deny reason printed `--focus <?>` and the model reinvented what the\n * guard knew. Now the derivation lives here, beside the guard and importing nothing\n * (the guard's latency budget is a stat and an exit, so this must stay a\n * zero-dependency sibling rather than an exception to its no-library-import rule),\n * and `smeltBlob` applies the same function to a caller's `producer` hint — so the\n * guard's rewrite wrap, `smelt --producer` and the `smelt_file` tool cannot disagree\n * about which terms a command names.\n *\n * The question it answers is narrower than \"what did the command search for\". It is:\n * **which terms, if any, distinguish the output lines the task is about from the ones\n * it is not.** Focus is what the lexical planner keeps; a term every output line\n * carries keeps everything and cuts nothing, exactly when the output is large. So:\n *\n * - a plain `grep`/`rg` prints only matching lines — every line has the pattern, the\n * pattern distinguishes nothing, and the answer is no terms (the planner's\n * head-and-tail rule is the right cut);\n * - a search with context (`-C`, `-A`, `-B`, `--context`…) prints non-matching lines\n * around each hit, and there the pattern is exactly the focus;\n * - a listing search (`-l`, `-c`, `--files-with-matches`…) prints no matching lines\n * at all, so its pattern names nothing in the output;\n * - a producer that states no term (`cat`, `git diff`, `sed`) yields none, and so\n * does a command this parser cannot see whole — unsure means no terms, the same\n * fail-open rule the guard lives under.\n *\n * Every rule above is a fact about what the command prints, stated as data in\n * {@link SEARCH_PROGRAMS}, {@link CONTEXT_FLAGS} and {@link LISTING_FLAGS} — never a\n * guess about the text.\n */\n\n/** The search programs whose first non-flag word (or every `-e`) is a pattern. */\nconst SEARCH_PROGRAMS: ReadonlySet<string> = new Set(['grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack']);\n\n/** Flags under which a search prints lines that do *not* match — where focus pays. */\nconst CONTEXT_FLAGS: ReadonlySet<string> = new Set([\n '-A',\n '-B',\n '-C',\n '--after-context',\n '--before-context',\n '--context',\n]);\n\n/** Flags under which a search prints no matching lines at all. */\nconst LISTING_FLAGS: ReadonlySet<string> = new Set([\n '-l',\n '-L',\n '-c',\n '--files-with-matches',\n '--files-without-match',\n '--count',\n '--count-matches',\n '--files',\n]);\n\n/**\n * The terms a producer command names that distinguish output lines the task is about.\n * `[]` whenever the honest answer is \"none\" — see the module doc. Never throws.\n */\nexport function focusTermsFor(command: string | undefined): readonly string[] {\n if (command === undefined) return [];\n const words = simpleCommandWords(command.trim());\n if (words === undefined || words.length === 0) return [];\n const search = searchWords(words);\n if (search === undefined) return [];\n if (!printsContext(search)) return [];\n if (isListing(search)) return [];\n return searchPatterns(search);\n}\n\n/**\n * The words of the search invocation, with `git grep` normalised to `grep`, or\n * `undefined` when the program is not a search at all.\n */\nfunction searchWords(words: readonly string[]): readonly string[] | undefined {\n const program = words[0]!.split('/').at(-1)!;\n if (program === 'git' && words[1] === 'grep') return ['grep', ...words.slice(2)];\n return SEARCH_PROGRAMS.has(program) ? words : undefined;\n}\n\n/** True when a context flag is present — spaced (`-C 3`), compact (`-C3`) or `=`-joined. */\nfunction printsContext(words: readonly string[]): boolean {\n return words.slice(1).some((word) => {\n if (CONTEXT_FLAGS.has(word)) return true;\n const long = word.startsWith('--') ? word.split('=')[0]! : undefined;\n if (long !== undefined) return CONTEXT_FLAGS.has(long);\n return /^-[ABC]\\d+$/.test(word);\n });\n}\n\nfunction isListing(words: readonly string[]): boolean {\n return words.slice(1).some((word) => LISTING_FLAGS.has(word.split('=')[0]!));\n}\n\n/**\n * Split a command into words IF it is one simple command: no pipes, no logic, no\n * redirects, no substitutions, no expansions this code would have to model. Anything\n * else returns `undefined` and the caller treats the command as unknowable — the guard\n * allows, this derivation names no terms.\n */\nexport function simpleCommandWords(command: string): readonly string[] | undefined {\n const words: string[] = [];\n let current = '';\n let started = false;\n let i = 0;\n const push = (): void => {\n if (started) words.push(current);\n current = '';\n started = false;\n };\n while (i < command.length) {\n const ch = command[i]!;\n if ('|&;<>()`$\\\\\\n*?~{}!'.includes(ch)) return undefined; // shell would interpret it\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n i += 1;\n started = true;\n while (i < command.length && command[i] !== quote) {\n if (quote === '\"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\\\')) {\n return undefined; // expansions inside double quotes — not simple\n }\n current += command[i]!;\n i += 1;\n }\n if (i >= command.length) return undefined; // unterminated quote\n i += 1;\n continue;\n }\n if (ch === ' ' || ch === '\\t') {\n push();\n i += 1;\n continue;\n }\n current += ch;\n started = true;\n i += 1;\n }\n push();\n return words;\n}\n\n/** Flags that consume the next word, so it is never mistaken for the pattern. */\nconst TAKES_VALUE: ReadonlySet<string> = new Set([\n '-e',\n '--regexp',\n '-f',\n '--file',\n '-m',\n '--max-count',\n '-A',\n '--after-context',\n '-B',\n '--before-context',\n '-C',\n '--context',\n '-d',\n '--directories',\n '-D',\n '--devices',\n '--include',\n '--exclude',\n '--exclude-dir',\n '-t',\n '--type',\n '-T',\n '--type-not',\n '-g',\n '--glob',\n '--iglob',\n '-j',\n '--threads',\n '--color',\n '--colour',\n]);\n\n/**\n * Every pattern a grep/rg invocation searches for: each explicit `-e`/`--regexp`\n * value if any are given, else the first word that is not a flag or a flag's value.\n * Empty when the parse is not sure.\n */\nexport function searchPatterns(words: readonly string[]): readonly string[] {\n const explicit: string[] = [];\n let positional: string | undefined;\n let i = 1;\n while (i < words.length) {\n const word = words[i]!;\n if (word === '--') {\n positional ??= words[i + 1];\n break;\n }\n if (word === '-e' || word === '--regexp') {\n const value = words[i + 1];\n if (value !== undefined) explicit.push(value);\n i += 2;\n continue;\n }\n if (word.startsWith('--') && word.includes('=')) {\n i += 1;\n continue;\n }\n if (word.startsWith('-') && word.length > 1) {\n i += TAKES_VALUE.has(word) ? 2 : 1;\n continue;\n }\n if (positional === undefined && explicit.length === 0) positional = word;\n i += 1;\n }\n if (explicit.length > 0) return explicit;\n return positional === undefined ? [] : [positional];\n}\n\n/** The first pattern of {@link searchPatterns}; `undefined` when there is none. */\nexport function searchPattern(words: readonly string[]): string | undefined {\n return searchPatterns(words)[0];\n}\n\n/** Single-quote a value for `sh` unless it is plainly safe bare. */\nexport function shellQuote(value: string): string {\n if (/^[A-Za-z0-9_./:=-]+$/.test(value)) return value;\n return `'${value.replaceAll(\"'\", `'\"'\"'`)}'`;\n}\n"]}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus terms, derived from the command that produced a blob — one derivation, zero
|
|
3
|
+
* imports, shared by the hooks guard and the ops seam.
|
|
4
|
+
*
|
|
5
|
+
* The guard is the producer expert: to decide anything about a `grep` it has already
|
|
6
|
+
* parsed the pattern out of the command. Before this file that knowledge died inside
|
|
7
|
+
* the guard — the deny reason printed `--focus <?>` and the model reinvented what the
|
|
8
|
+
* guard knew. Now the derivation lives here, beside the guard and importing nothing
|
|
9
|
+
* (the guard's latency budget is a stat and an exit, so this must stay a
|
|
10
|
+
* zero-dependency sibling rather than an exception to its no-library-import rule),
|
|
11
|
+
* and `smeltBlob` applies the same function to a caller's `producer` hint — so the
|
|
12
|
+
* guard's rewrite wrap, `smelt --producer` and the `smelt_file` tool cannot disagree
|
|
13
|
+
* about which terms a command names.
|
|
14
|
+
*
|
|
15
|
+
* The question it answers is narrower than "what did the command search for". It is:
|
|
16
|
+
* **which terms, if any, distinguish the output lines the task is about from the ones
|
|
17
|
+
* it is not.** Focus is what the lexical planner keeps; a term every output line
|
|
18
|
+
* carries keeps everything and cuts nothing, exactly when the output is large. So:
|
|
19
|
+
*
|
|
20
|
+
* - a plain `grep`/`rg` prints only matching lines — every line has the pattern, the
|
|
21
|
+
* pattern distinguishes nothing, and the answer is no terms (the planner's
|
|
22
|
+
* head-and-tail rule is the right cut);
|
|
23
|
+
* - a search with context (`-C`, `-A`, `-B`, `--context`…) prints non-matching lines
|
|
24
|
+
* around each hit, and there the pattern is exactly the focus;
|
|
25
|
+
* - a listing search (`-l`, `-c`, `--files-with-matches`…) prints no matching lines
|
|
26
|
+
* at all, so its pattern names nothing in the output;
|
|
27
|
+
* - a producer that states no term (`cat`, `git diff`, `sed`) yields none, and so
|
|
28
|
+
* does a command this parser cannot see whole — unsure means no terms, the same
|
|
29
|
+
* fail-open rule the guard lives under.
|
|
30
|
+
*
|
|
31
|
+
* Every rule above is a fact about what the command prints, stated as data in
|
|
32
|
+
* {@link SEARCH_PROGRAMS}, {@link CONTEXT_FLAGS} and {@link LISTING_FLAGS} — never a
|
|
33
|
+
* guess about the text.
|
|
34
|
+
*/
|
|
35
|
+
/** The search programs whose first non-flag word (or every `-e`) is a pattern. */
|
|
36
|
+
const SEARCH_PROGRAMS = new Set(['grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack']);
|
|
37
|
+
/** Flags under which a search prints lines that do *not* match — where focus pays. */
|
|
38
|
+
const CONTEXT_FLAGS = new Set([
|
|
39
|
+
'-A',
|
|
40
|
+
'-B',
|
|
41
|
+
'-C',
|
|
42
|
+
'--after-context',
|
|
43
|
+
'--before-context',
|
|
44
|
+
'--context',
|
|
45
|
+
]);
|
|
46
|
+
/** Flags under which a search prints no matching lines at all. */
|
|
47
|
+
const LISTING_FLAGS = new Set([
|
|
48
|
+
'-l',
|
|
49
|
+
'-L',
|
|
50
|
+
'-c',
|
|
51
|
+
'--files-with-matches',
|
|
52
|
+
'--files-without-match',
|
|
53
|
+
'--count',
|
|
54
|
+
'--count-matches',
|
|
55
|
+
'--files',
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* The terms a producer command names that distinguish output lines the task is about.
|
|
59
|
+
* `[]` whenever the honest answer is "none" — see the module doc. Never throws.
|
|
60
|
+
*/
|
|
61
|
+
export function focusTermsFor(command) {
|
|
62
|
+
if (command === undefined)
|
|
63
|
+
return [];
|
|
64
|
+
const words = simpleCommandWords(command.trim());
|
|
65
|
+
if (words === undefined || words.length === 0)
|
|
66
|
+
return [];
|
|
67
|
+
const search = searchWords(words);
|
|
68
|
+
if (search === undefined)
|
|
69
|
+
return [];
|
|
70
|
+
if (!printsContext(search))
|
|
71
|
+
return [];
|
|
72
|
+
if (isListing(search))
|
|
73
|
+
return [];
|
|
74
|
+
return searchPatterns(search);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The words of the search invocation, with `git grep` normalised to `grep`, or
|
|
78
|
+
* `undefined` when the program is not a search at all.
|
|
79
|
+
*/
|
|
80
|
+
function searchWords(words) {
|
|
81
|
+
const program = words[0].split('/').at(-1);
|
|
82
|
+
if (program === 'git' && words[1] === 'grep')
|
|
83
|
+
return ['grep', ...words.slice(2)];
|
|
84
|
+
return SEARCH_PROGRAMS.has(program) ? words : undefined;
|
|
85
|
+
}
|
|
86
|
+
/** True when a context flag is present — spaced (`-C 3`), compact (`-C3`) or `=`-joined. */
|
|
87
|
+
function printsContext(words) {
|
|
88
|
+
return words.slice(1).some((word) => {
|
|
89
|
+
if (CONTEXT_FLAGS.has(word))
|
|
90
|
+
return true;
|
|
91
|
+
const long = word.startsWith('--') ? word.split('=')[0] : undefined;
|
|
92
|
+
if (long !== undefined)
|
|
93
|
+
return CONTEXT_FLAGS.has(long);
|
|
94
|
+
return /^-[ABC]\d+$/.test(word);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function isListing(words) {
|
|
98
|
+
return words.slice(1).some((word) => LISTING_FLAGS.has(word.split('=')[0]));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Split a command into words IF it is one simple command: no pipes, no logic, no
|
|
102
|
+
* redirects, no substitutions, no expansions this code would have to model. Anything
|
|
103
|
+
* else returns `undefined` and the caller treats the command as unknowable — the guard
|
|
104
|
+
* allows, this derivation names no terms.
|
|
105
|
+
*/
|
|
106
|
+
export function simpleCommandWords(command) {
|
|
107
|
+
const words = [];
|
|
108
|
+
let current = '';
|
|
109
|
+
let started = false;
|
|
110
|
+
let i = 0;
|
|
111
|
+
const push = () => {
|
|
112
|
+
if (started)
|
|
113
|
+
words.push(current);
|
|
114
|
+
current = '';
|
|
115
|
+
started = false;
|
|
116
|
+
};
|
|
117
|
+
while (i < command.length) {
|
|
118
|
+
const ch = command[i];
|
|
119
|
+
if ('|&;<>()`$\\\n*?~{}!'.includes(ch))
|
|
120
|
+
return undefined; // shell would interpret it
|
|
121
|
+
if (ch === "'" || ch === '"') {
|
|
122
|
+
const quote = ch;
|
|
123
|
+
i += 1;
|
|
124
|
+
started = true;
|
|
125
|
+
while (i < command.length && command[i] !== quote) {
|
|
126
|
+
if (quote === '"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\')) {
|
|
127
|
+
return undefined; // expansions inside double quotes — not simple
|
|
128
|
+
}
|
|
129
|
+
current += command[i];
|
|
130
|
+
i += 1;
|
|
131
|
+
}
|
|
132
|
+
if (i >= command.length)
|
|
133
|
+
return undefined; // unterminated quote
|
|
134
|
+
i += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ch === ' ' || ch === '\t') {
|
|
138
|
+
push();
|
|
139
|
+
i += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
current += ch;
|
|
143
|
+
started = true;
|
|
144
|
+
i += 1;
|
|
145
|
+
}
|
|
146
|
+
push();
|
|
147
|
+
return words;
|
|
148
|
+
}
|
|
149
|
+
/** Flags that consume the next word, so it is never mistaken for the pattern. */
|
|
150
|
+
const TAKES_VALUE = new Set([
|
|
151
|
+
'-e',
|
|
152
|
+
'--regexp',
|
|
153
|
+
'-f',
|
|
154
|
+
'--file',
|
|
155
|
+
'-m',
|
|
156
|
+
'--max-count',
|
|
157
|
+
'-A',
|
|
158
|
+
'--after-context',
|
|
159
|
+
'-B',
|
|
160
|
+
'--before-context',
|
|
161
|
+
'-C',
|
|
162
|
+
'--context',
|
|
163
|
+
'-d',
|
|
164
|
+
'--directories',
|
|
165
|
+
'-D',
|
|
166
|
+
'--devices',
|
|
167
|
+
'--include',
|
|
168
|
+
'--exclude',
|
|
169
|
+
'--exclude-dir',
|
|
170
|
+
'-t',
|
|
171
|
+
'--type',
|
|
172
|
+
'-T',
|
|
173
|
+
'--type-not',
|
|
174
|
+
'-g',
|
|
175
|
+
'--glob',
|
|
176
|
+
'--iglob',
|
|
177
|
+
'-j',
|
|
178
|
+
'--threads',
|
|
179
|
+
'--color',
|
|
180
|
+
'--colour',
|
|
181
|
+
]);
|
|
182
|
+
/**
|
|
183
|
+
* Every pattern a grep/rg invocation searches for: each explicit `-e`/`--regexp`
|
|
184
|
+
* value if any are given, else the first word that is not a flag or a flag's value.
|
|
185
|
+
* Empty when the parse is not sure.
|
|
186
|
+
*/
|
|
187
|
+
export function searchPatterns(words) {
|
|
188
|
+
const explicit = [];
|
|
189
|
+
let positional;
|
|
190
|
+
let i = 1;
|
|
191
|
+
while (i < words.length) {
|
|
192
|
+
const word = words[i];
|
|
193
|
+
if (word === '--') {
|
|
194
|
+
positional ??= words[i + 1];
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (word === '-e' || word === '--regexp') {
|
|
198
|
+
const value = words[i + 1];
|
|
199
|
+
if (value !== undefined)
|
|
200
|
+
explicit.push(value);
|
|
201
|
+
i += 2;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (word.startsWith('--') && word.includes('=')) {
|
|
205
|
+
i += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (word.startsWith('-') && word.length > 1) {
|
|
209
|
+
i += TAKES_VALUE.has(word) ? 2 : 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (positional === undefined && explicit.length === 0)
|
|
213
|
+
positional = word;
|
|
214
|
+
i += 1;
|
|
215
|
+
}
|
|
216
|
+
if (explicit.length > 0)
|
|
217
|
+
return explicit;
|
|
218
|
+
return positional === undefined ? [] : [positional];
|
|
219
|
+
}
|
|
220
|
+
/** The first pattern of {@link searchPatterns}; `undefined` when there is none. */
|
|
221
|
+
export function searchPattern(words) {
|
|
222
|
+
return searchPatterns(words)[0];
|
|
223
|
+
}
|
|
224
|
+
/** Single-quote a value for `sh` unless it is plainly safe bare. */
|
|
225
|
+
export function shellQuote(value) {
|
|
226
|
+
if (/^[A-Za-z0-9_./:=-]+$/.test(value))
|
|
227
|
+
return value;
|
|
228
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=focus-terms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus-terms.js","sourceRoot":"","sources":["../../src/hooks/focus-terms.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Focus terms, derived from the command that produced a blob — one derivation, zero\n * imports, shared by the hooks guard and the ops seam.\n *\n * The guard is the producer expert: to decide anything about a `grep` it has already\n * parsed the pattern out of the command. Before this file that knowledge died inside\n * the guard — the deny reason printed `--focus <?>` and the model reinvented what the\n * guard knew. Now the derivation lives here, beside the guard and importing nothing\n * (the guard's latency budget is a stat and an exit, so this must stay a\n * zero-dependency sibling rather than an exception to its no-library-import rule),\n * and `smeltBlob` applies the same function to a caller's `producer` hint — so the\n * guard's rewrite wrap, `smelt --producer` and the `smelt_file` tool cannot disagree\n * about which terms a command names.\n *\n * The question it answers is narrower than \"what did the command search for\". It is:\n * **which terms, if any, distinguish the output lines the task is about from the ones\n * it is not.** Focus is what the lexical planner keeps; a term every output line\n * carries keeps everything and cuts nothing, exactly when the output is large. So:\n *\n * - a plain `grep`/`rg` prints only matching lines — every line has the pattern, the\n * pattern distinguishes nothing, and the answer is no terms (the planner's\n * head-and-tail rule is the right cut);\n * - a search with context (`-C`, `-A`, `-B`, `--context`…) prints non-matching lines\n * around each hit, and there the pattern is exactly the focus;\n * - a listing search (`-l`, `-c`, `--files-with-matches`…) prints no matching lines\n * at all, so its pattern names nothing in the output;\n * - a producer that states no term (`cat`, `git diff`, `sed`) yields none, and so\n * does a command this parser cannot see whole — unsure means no terms, the same\n * fail-open rule the guard lives under.\n *\n * Every rule above is a fact about what the command prints, stated as data in\n * {@link SEARCH_PROGRAMS}, {@link CONTEXT_FLAGS} and {@link LISTING_FLAGS} — never a\n * guess about the text.\n */\n\n/** The search programs whose first non-flag word (or every `-e`) is a pattern. */\nconst SEARCH_PROGRAMS: ReadonlySet<string> = new Set(['grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack']);\n\n/** Flags under which a search prints lines that do *not* match — where focus pays. */\nconst CONTEXT_FLAGS: ReadonlySet<string> = new Set([\n '-A',\n '-B',\n '-C',\n '--after-context',\n '--before-context',\n '--context',\n]);\n\n/** Flags under which a search prints no matching lines at all. */\nconst LISTING_FLAGS: ReadonlySet<string> = new Set([\n '-l',\n '-L',\n '-c',\n '--files-with-matches',\n '--files-without-match',\n '--count',\n '--count-matches',\n '--files',\n]);\n\n/**\n * The terms a producer command names that distinguish output lines the task is about.\n * `[]` whenever the honest answer is \"none\" — see the module doc. Never throws.\n */\nexport function focusTermsFor(command: string | undefined): readonly string[] {\n if (command === undefined) return [];\n const words = simpleCommandWords(command.trim());\n if (words === undefined || words.length === 0) return [];\n const search = searchWords(words);\n if (search === undefined) return [];\n if (!printsContext(search)) return [];\n if (isListing(search)) return [];\n return searchPatterns(search);\n}\n\n/**\n * The words of the search invocation, with `git grep` normalised to `grep`, or\n * `undefined` when the program is not a search at all.\n */\nfunction searchWords(words: readonly string[]): readonly string[] | undefined {\n const program = words[0]!.split('/').at(-1)!;\n if (program === 'git' && words[1] === 'grep') return ['grep', ...words.slice(2)];\n return SEARCH_PROGRAMS.has(program) ? words : undefined;\n}\n\n/** True when a context flag is present — spaced (`-C 3`), compact (`-C3`) or `=`-joined. */\nfunction printsContext(words: readonly string[]): boolean {\n return words.slice(1).some((word) => {\n if (CONTEXT_FLAGS.has(word)) return true;\n const long = word.startsWith('--') ? word.split('=')[0]! : undefined;\n if (long !== undefined) return CONTEXT_FLAGS.has(long);\n return /^-[ABC]\\d+$/.test(word);\n });\n}\n\nfunction isListing(words: readonly string[]): boolean {\n return words.slice(1).some((word) => LISTING_FLAGS.has(word.split('=')[0]!));\n}\n\n/**\n * Split a command into words IF it is one simple command: no pipes, no logic, no\n * redirects, no substitutions, no expansions this code would have to model. Anything\n * else returns `undefined` and the caller treats the command as unknowable — the guard\n * allows, this derivation names no terms.\n */\nexport function simpleCommandWords(command: string): readonly string[] | undefined {\n const words: string[] = [];\n let current = '';\n let started = false;\n let i = 0;\n const push = (): void => {\n if (started) words.push(current);\n current = '';\n started = false;\n };\n while (i < command.length) {\n const ch = command[i]!;\n if ('|&;<>()`$\\\\\\n*?~{}!'.includes(ch)) return undefined; // shell would interpret it\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n i += 1;\n started = true;\n while (i < command.length && command[i] !== quote) {\n if (quote === '\"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\\\')) {\n return undefined; // expansions inside double quotes — not simple\n }\n current += command[i]!;\n i += 1;\n }\n if (i >= command.length) return undefined; // unterminated quote\n i += 1;\n continue;\n }\n if (ch === ' ' || ch === '\\t') {\n push();\n i += 1;\n continue;\n }\n current += ch;\n started = true;\n i += 1;\n }\n push();\n return words;\n}\n\n/** Flags that consume the next word, so it is never mistaken for the pattern. */\nconst TAKES_VALUE: ReadonlySet<string> = new Set([\n '-e',\n '--regexp',\n '-f',\n '--file',\n '-m',\n '--max-count',\n '-A',\n '--after-context',\n '-B',\n '--before-context',\n '-C',\n '--context',\n '-d',\n '--directories',\n '-D',\n '--devices',\n '--include',\n '--exclude',\n '--exclude-dir',\n '-t',\n '--type',\n '-T',\n '--type-not',\n '-g',\n '--glob',\n '--iglob',\n '-j',\n '--threads',\n '--color',\n '--colour',\n]);\n\n/**\n * Every pattern a grep/rg invocation searches for: each explicit `-e`/`--regexp`\n * value if any are given, else the first word that is not a flag or a flag's value.\n * Empty when the parse is not sure.\n */\nexport function searchPatterns(words: readonly string[]): readonly string[] {\n const explicit: string[] = [];\n let positional: string | undefined;\n let i = 1;\n while (i < words.length) {\n const word = words[i]!;\n if (word === '--') {\n positional ??= words[i + 1];\n break;\n }\n if (word === '-e' || word === '--regexp') {\n const value = words[i + 1];\n if (value !== undefined) explicit.push(value);\n i += 2;\n continue;\n }\n if (word.startsWith('--') && word.includes('=')) {\n i += 1;\n continue;\n }\n if (word.startsWith('-') && word.length > 1) {\n i += TAKES_VALUE.has(word) ? 2 : 1;\n continue;\n }\n if (positional === undefined && explicit.length === 0) positional = word;\n i += 1;\n }\n if (explicit.length > 0) return explicit;\n return positional === undefined ? [] : [positional];\n}\n\n/** The first pattern of {@link searchPatterns}; `undefined` when there is none. */\nexport function searchPattern(words: readonly string[]): string | undefined {\n return searchPatterns(words)[0];\n}\n\n/** Single-quote a value for `sh` unless it is plainly safe bare. */\nexport function shellQuote(value: string): string {\n if (/^[A-Za-z0-9_./:=-]+$/.test(value)) return value;\n return `'${value.replaceAll(\"'\", `'\"'\"'`)}'`;\n}\n"]}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command parsing lives in `./focus-terms.ts` — a zero-import sibling, so the
|
|
3
|
+
* guard's no-library-import rule holds — and is re-exported here because this module
|
|
4
|
+
* is the published `hooks/guard-core` subpath every shim and the opencode plugin load.
|
|
5
|
+
*/
|
|
6
|
+
export { focusTermsFor, searchPattern, searchPatterns, shellQuote, simpleCommandWords, } from './focus-terms.ts';
|
|
1
7
|
/**
|
|
2
8
|
* The guard core — one zero-dependency node module, shared by every harness shim.
|
|
3
9
|
*
|
|
@@ -16,7 +22,8 @@
|
|
|
16
22
|
* A guard that can brick a session on bad input is worse than no guard; the agent
|
|
17
23
|
* loses nothing but the optimization, and the warning says so.
|
|
18
24
|
* - **No library import on any path.** This module imports node builtins only —
|
|
19
|
-
* never `../index.ts`, never a planner, never web-tree-sitter
|
|
25
|
+
* never `../index.ts`, never a planner, never web-tree-sitter — plus its one
|
|
26
|
+
* zero-import sibling `./focus-terms.ts`, which owns the command parsing. The allow case is
|
|
20
27
|
* a stat and an exit; the research note
|
|
21
28
|
* (docs/research/2026-09-02-agent-enforcement.md § 5) budgets the always-on guard
|
|
22
29
|
* at tens of milliseconds, and loading grammar machinery here would spend that
|
|
@@ -142,21 +149,6 @@ export declare function decide(request: GuardRequest, settings: GuardSettings, c
|
|
|
142
149
|
size: number;
|
|
143
150
|
isFile: boolean;
|
|
144
151
|
} | undefined): GuardDecision;
|
|
145
|
-
/**
|
|
146
|
-
* Split a command into words IF it is one simple command: no pipes, no logic, no
|
|
147
|
-
* redirects, no substitutions, no expansions this code would have to model. Anything
|
|
148
|
-
* else returns `undefined` and the caller allows — the guard judges only what it can
|
|
149
|
-
* see whole.
|
|
150
|
-
*/
|
|
151
|
-
export declare function simpleCommandWords(command: string): readonly string[] | undefined;
|
|
152
|
-
/**
|
|
153
|
-
* The pattern a grep/rg invocation searches for: an explicit `-e`/`--regexp` value if
|
|
154
|
-
* given, else the first word that is not a flag or a flag's value. `undefined` when
|
|
155
|
-
* the parse is not sure — and unsure means allow, like everything else here.
|
|
156
|
-
*/
|
|
157
|
-
export declare function searchPattern(words: readonly string[]): string | undefined;
|
|
158
|
-
/** Single-quote a value for `sh` unless it is plainly safe bare. */
|
|
159
|
-
export declare function shellQuote(value: string): string;
|
|
160
152
|
/** True when this module is the file node was asked to run, not an import. */
|
|
161
153
|
export declare function isMainModule(moduleUrl: string): boolean;
|
|
162
154
|
/** Every byte of fd 0 to EOF, retrying EAGAIN — the same shape `cli/bin.ts` uses. */
|