Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,46 @@ Switch modes with `interlinked mode <name>`:
Team-shared policy lives in `.interlinked/guard-rules.json`. Personal
overrides go in `.interlinked/guard-rules.local.json` (gitignored).

## Metacoder (per-prompt overlay)

On every `UserPromptSubmit`, the **metacoder** runs a peer of the coding
agent — **Opus 4.7 max-effort** for Claude Code sessions, **GPT-5.5
xhigh** for Codex sessions — that reads your prompt + project
`AGENTS.md` / `CLAUDE.md` and emits a session-scoped overlay of guard
rules. The overlay merges on top of the floor for the lifetime of the
prompt: prompt-specific blocks the floor rules don't know about (e.g.
"this prompt is about payments, so don't touch `src/auth/`"), enforced
the same hard-block way the built-in rules are.

Both transports use **your existing Claude Code / Codex CLI
subscription** (`claude -p` / `codex exec`). No `ANTHROPIC_API_KEY` or
`OPENAI_API_KEY` needed.

**Trade-off:** 5–30 s of added latency on every UserPromptSubmit
before the coding agent starts, and ~$0.05–$0.30 of subscription credit
per prompt at the maximum reasoning tier. Fail-open everywhere — if the
subprocess can't reach the subscription (quota hit, CLI not installed,
timeout), the overlay is skipped and the prompt reaches the agent
against floor rules only.

```bash
interlinked metacoder status # current state + recent audit
interlinked metacoder disable --reason "burn rate" # off, audited
interlinked metacoder enable # back on
```

Or hand-edit `.interlinked/guard-rules.local.json`:

```json
{ "metacoder": { "enabled": false } }
```

The harness hot-reloads on the next file-watcher tick (~2 s) — no
restart needed. Full design and operational notes:
[docs/design/metacoding-agent-plan.md](./docs/design/metacoding-agent-plan.md).
Per-field config reference:
[docs/generated/configuration.md § Metacoder](./docs/generated/configuration.md#metacoder-per-prompt-overlay).

## Privacy

- Harness decisions and activity capture are local by default. Data leaves
Expand Down
828 changes: 828 additions & 0 deletions docs/design/metacoding-agent-plan.md

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions docs/generated/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Commands:
login [options] Authenticate with the server (opens browser)
logout [options] Clear authentication credentials (preserves other config)
logs [options] View local activity log (offline, no server needed)
metacoder Per-prompt overlay generator — toggle, inspect, audit
mode [options] [name] Show current enforcement mode, or switch to balanced / strict / lenient
multi-edit [options] [path] Apply N old/new string edits atomically to one or more files. Gate runs once on final content. Ambiguity evaluated after prior edits.
mutation Per-file mutation-score ratchet — fails on any file whose mutation score drops
Expand Down Expand Up @@ -749,6 +750,66 @@ Options:
-h, --help display help for command
```

## Metacoder

```
Usage: interlinked metacoder [options] [command]

Per-prompt overlay generator — toggle, inspect, audit

Options:
-h, --help display help for command

Commands:
enable [options] Enable the per-prompt metacoder overlay generator
disable [options] Disable the metacoder. The exact timestamp is recorded in
the audit log.
status [options] Show metacoder enable state + recent toggle audit
help [command] display help for command
```

### metacoder enable

```
Usage: interlinked metacoder enable [options]

Enable the per-prompt metacoder overlay generator

Options:
--reason <text> Why — recorded in metacoder.audit.jsonl
--json Machine-readable output
--short One-line summary
-h, --help display help for command
```

### metacoder disable

```
Usage: interlinked metacoder disable [options]

Disable the metacoder. The exact timestamp is recorded in the audit log.

Options:
--reason <text> Why — recorded in metacoder.audit.jsonl
--json Machine-readable output
--short One-line summary
-h, --help display help for command
```

### metacoder status

```
Usage: interlinked metacoder status [options]

Show metacoder enable state + recent toggle audit

Options:
--json Machine-readable output
--short One-line summary
--full Detailed output (full audit log)
-h, --help display help for command
```

## Other Commands

### enable
Expand Down
33 changes: 33 additions & 0 deletions docs/generated/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,36 @@ Controls which checks suppress pre-existing findings when editing files.
| `test_first` | `true` | Nudge agent to write/run tests before editing source files |
| `cross_file_switch_discriminant` | `true` | Flags the same switch discriminant (.kind/.type/.tag) appearing in multiple files — usually a polymorphism opportunity |
| `single_implementation_interface` | `true` | Flags exported interfaces with exactly one implementor — possible premature abstraction |

## Metacoder (per-prompt overlay)

On every `UserPromptSubmit` the metacoder runs a peer of the coding agent (Opus 4.7 max-effort for Claude sessions, GPT-5.5 xhigh for Codex sessions) to emit a session-scoped overlay of guard rules. Both transports use the user's existing **Claude Code / Codex CLI subscription** — no API key required. Plan: [docs/design/metacoding-agent-plan.md](../design/metacoding-agent-plan.md).

**Toggle from the CLI:**

```
interlinked metacoder status # show current state + recent audit
interlinked metacoder enable # turn on (default)
interlinked metacoder disable --reason "burn rate" # turn off, with audit
```

**Or hand-edit `.interlinked/guard-rules.local.json`:**

```json
{
"metacoder": { "enabled": false }
}
```

| Setting | Default | Description |
|---------|---------|-------------|
| `enabled` | `true` | Master switch for the per-prompt metacoder |
| `timeout_ms` | `30000` | Metacoder LLM call timeout. Clamped at merge to keep < hook timeout (33000 ms). |
| `max_rules` | `20` | Hard cap on overlay rule count per emission |
| `max_pattern_length` | `200` | Per-pattern regex char cap (bounds ReDoS / compile cost) |
| `max_patterns_per_rule` | `10` | Patterns-per-rule cap |
| `max_addendum_chars` | `2000` | `system_prompt_addendum` length cap |

**Hook timeout coordination:** the generated hook waits 35000 ms (5 s buffer past the metacoder's internal timeout) so the harness can return a clean "metacoder timed out, allow" decision before the hook cold-falls back. Both `hook-entry.ts` (adapter path) and the generated `.mjs` (legacy path) honor this budget.

**Latency / cost:** 5–30 s per prompt, $0.05–$0.30 per prompt (Opus 4.7 max-effort). Fail-open: any subprocess failure (CLI not installed, subscription quota hit, timeout) skips the overlay and lets the prompt reach the agent against floor rules only.
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,18 @@
},
"packageManager": "npm@10.0.0",
"dependencies": {
"commander": "^12.0.0"
"commander": "12.1.0"
},
"optionalDependencies": {
"@typescript/native-preview": "7.0.0-dev.20260421.2"
},
"devDependencies": {
"@biomejs/biome": "^2.4.13",
"@types/node": "^20.0.0",
"fast-check": "^4.7.0",
"tsup": "^8.0.0",
"tsx": "^4.0.0",
"typescript": "^5.5.0",
"vitest": "^3.0.0"
"@biomejs/biome": "2.4.13",
"@types/node": "20.19.33",
"fast-check": "4.7.0",
"tsup": "8.5.1",
"tsx": "4.21.0",
"typescript": "5.9.3",
"vitest": "3.2.4"
}
}
44 changes: 44 additions & 0 deletions scripts/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { join } from "node:path";
// Import data structures from the harness
import { getDefaultConfig, getBuiltinRules } from "../src/harness/rules-loader.js";
import { STRUCTURAL_CHECK_META } from "../src/harness/check-metadata.js";
import {
DEFAULT_METACODER_CONFIG,
USER_PROMPT_HOOK_TIMEOUT_MS,
} from "../src/harness/metacoder/types.js";

const OUT_DIR = join(import.meta.dirname, "..", "docs", "generated");
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
Expand Down Expand Up @@ -194,6 +198,44 @@ function generateConfigReference(): string {
}
}

lines.push("");
lines.push("## Metacoder (per-prompt overlay)");
lines.push("");
lines.push(
"On every `UserPromptSubmit` the metacoder runs a peer of the coding agent (Opus 4.7 max-effort for Claude sessions, GPT-5.5 xhigh for Codex sessions) to emit a session-scoped overlay of guard rules. Both transports use the user's existing **Claude Code / Codex CLI subscription** — no API key required. Plan: [docs/design/metacoding-agent-plan.md](../design/metacoding-agent-plan.md).",
);
lines.push("");
lines.push("**Toggle from the CLI:**");
lines.push("");
lines.push("```");
lines.push("interlinked metacoder status # show current state + recent audit");
lines.push("interlinked metacoder enable # turn on (default)");
lines.push("interlinked metacoder disable --reason \"burn rate\" # turn off, with audit");
lines.push("```");
lines.push("");
lines.push("**Or hand-edit `.interlinked/guard-rules.local.json`:**");
lines.push("");
lines.push("```json");
lines.push("{");
lines.push(" \"metacoder\": { \"enabled\": false }");
lines.push("}");
lines.push("```");
lines.push("");
lines.push("| Setting | Default | Description |");
lines.push("|---------|---------|-------------|");
const m = DEFAULT_METACODER_CONFIG;
lines.push(`| \`enabled\` | \`${m.enabled}\` | Master switch for the per-prompt metacoder |`);
lines.push(`| \`timeout_ms\` | \`${m.timeout_ms}\` | Metacoder LLM call timeout. Clamped at merge to keep < hook timeout (${USER_PROMPT_HOOK_TIMEOUT_MS - 2_000} ms). |`);
lines.push(`| \`max_rules\` | \`${m.max_rules}\` | Hard cap on overlay rule count per emission |`);
lines.push(`| \`max_pattern_length\` | \`${m.max_pattern_length}\` | Per-pattern regex char cap (bounds ReDoS / compile cost) |`);
lines.push(`| \`max_patterns_per_rule\` | \`${m.max_patterns_per_rule}\` | Patterns-per-rule cap |`);
lines.push(`| \`max_addendum_chars\` | \`${m.max_addendum_chars}\` | \`system_prompt_addendum\` length cap |`);
lines.push("");
lines.push(
`**Hook timeout coordination:** the generated hook waits ${USER_PROMPT_HOOK_TIMEOUT_MS} ms (5 s buffer past the metacoder's internal timeout) so the harness can return a clean "metacoder timed out, allow" decision before the hook cold-falls back. Both \`hook-entry.ts\` (adapter path) and the generated \`.mjs\` (legacy path) honor this budget.`,
);
lines.push("");
lines.push("**Latency / cost:** 5–30 s per prompt, $0.05–$0.30 per prompt (Opus 4.7 max-effort). Fail-open: any subprocess failure (CLI not installed, subscription quota hit, timeout) skips the overlay and lets the prompt reach the agent against floor rules only.");
lines.push("");
return lines.join("\n");
}
Expand Down Expand Up @@ -255,6 +297,7 @@ function generateCliReference(): string {
"trace",
"guard",
"git",
"metacoder",
];

// Commands with subcommands — mirror the groups registered in src/index.ts.
Expand All @@ -272,6 +315,7 @@ function generateCliReference(): string {
coverage: ["check", "baseline"],
mutation: ["check", "baseline"],
completions: ["bash", "zsh", "fish"],
metacoder: ["enable", "disable", "status"],
};

for (const cmd of commandNames) {
Expand Down
50 changes: 27 additions & 23 deletions src/commands/__tests__/verify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,31 +141,35 @@ describe("scored suggestions", () => {
});

describe("suppression detection", () => {
it("ignores suppression markers that only appear inside string literals", async () => {
const { verifyCommand } = await import("../verify.js");
it(
"ignores suppression markers that only appear inside string literals",
async () => {
const { verifyCommand } = await import("../verify.js");

// Build the literal token at runtime so this test file's own source
// doesn't contain a raw "@ts-expect-error" — the suppressions check would
// (correctly) nag every edit if it did. The fixture file written below
// still contains the literal token, which is the point of the test.
const tsIgnore = `@ts-${"ignore"}`;
writeFileSync(
join(tempDir, "fixture.ts"),
[
"export function buildFixture() {",
` const code = "// ${tsIgnore}\\nconst x = 1;";`,
` return code.includes("${tsIgnore}");`,
"}",
"",
].join("\n"),
);
// Build the literal token at runtime so this test file's own source
// doesn't contain a raw "@ts-expect-error" — the suppressions check would
// (correctly) nag every edit if it did. The fixture file written below
// still contains the literal token, which is the point of the test.
const tsIgnore = `@ts-${"ignore"}`;
writeFileSync(
join(tempDir, "fixture.ts"),
[
"export function buildFixture() {",
` const code = "// ${tsIgnore}\\nconst x = 1;";`,
` return code.includes("${tsIgnore}");`,
"}",
"",
].join("\n"),
);

const captured = await captureStd(async () => {
await verifyCommand({ target: tempDir, json: true });
});
const result = JSON.parse(captured.stdout);
expect(result.suppressions.issues).toBe(0);
});
const captured = await captureStd(async () => {
await verifyCommand({ target: tempDir, json: true });
});
const result = JSON.parse(captured.stdout);
expect(result.suppressions.issues).toBe(0);
},
60_000,
);
});

// Pins the invariant for the tail "X / Y files flagged" summary:
Expand Down
Loading
Loading