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
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ recursive-include taxonomies *.json
recursive-include examples *.json
recursive-include examples/buggy *
recursive-exclude examples/buggy .ancp/* native-output*.txt
prune examples/buggy/*/.ancp
prune examples/buggy/*/*/.ancp
recursive-exclude examples/buggy *.pyc native-output*.txt
recursive-include docs *.md
recursive-include spec *.md
recursive-include tools *.py
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ compiler / linter / test / LSP / build tool
agents, IDEs, CI, repair bots
```

For agent terminals, ANCP can also render compact raw text from the same JSON:
For agent terminals, ANCP can also render ultra-minimal raw text from the same JSON:

```text
normal compiler command
Expand All @@ -71,11 +71,11 @@ normal compiler command
ANCP result.check JSON + raw native logs
|
v
minimal root-cause text for agents
one-line root-cause text for agents
```

See [Compact Signal Layer](docs/compact-signal-layer.md) for `ancp enable`,
`auto-compact`, token savings telemetry, and raw-output fallback behavior.
`auto-ultra`, token savings telemetry, `ancp raw`, and `ancp off -- ...`.

## Design Principles

Expand Down
38 changes: 34 additions & 4 deletions docs/cli-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ ancp repair --apply <plan-file> --json
ancp verify --json
ancp graph --json
ancp skills --json
ancp render --from check-result.json --format text
ancp render --from check-result.json --format ultra
ancp raw
ancp off -- <native-command>
ancp enable
ancp disable
ancp status
Expand Down Expand Up @@ -242,11 +244,39 @@ Renders ANCP JSON into a user- or agent-facing text format.
```bash
ancp render --from check-result.json --format markdown
ancp render --from check-result.json --format text --budget 800
ancp render --from check-result.json --format ultra --budget 200
```

Markdown output is intended for human inspection. Text output is the compact
agent signal format: no Markdown fences, no tables, root-cause groups first,
raw-output fallback path included when available.
agent signal format. Ultra output is the default terminal failure style:
no protocol banner, no raw log path, no token stats, no Markdown fences, and no
guidance unless explicitly requested elsewhere.

## `raw`

Prints raw native output captured by the latest or selected ANCP check result.

```bash
ancp raw
ancp raw --path
ancp raw --stream stderr
ancp raw --from .ancp/last-check.json
```

Raw logs are saved by proxy/shim mode but not printed in ultra terminal output.
`raw` is the explicit retrieval command.

## `off`

Runs a native command while bypassing ANCP interception.

```bash
ancp off -- python -m py_compile app.py
ancp off -- cargo check
```

This command resolves the real executable while ignoring ANCP shim scripts, then
prints native stdout/stderr exactly as the underlying tool produced them.

## `enable`, `disable`, `uninstall`, and `status`

Expand All @@ -262,7 +292,7 @@ ancp uninstall
ancp status
```

`enable` installs compiler/tool shims and configures `auto-compact` output by
`enable` installs compiler/tool shims and configures `auto-ultra` output by
default. On Windows, `--scope user` prepends the ANCP shim directory to the user
PATH. `--scope session` prints activation commands without mutating persistent
PATH.
Expand Down
84 changes: 59 additions & 25 deletions docs/compact-signal-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ smaller and more stable signal:
- primary source location,
- root-cause grouping,
- repair direction,
- raw output fallback path,
- token/byte savings telemetry.
- raw output fallback available by command,
- token/byte savings telemetry in JSON.

The compact signal layer does not replace the ANCP JSON contract. It renders the
JSON into minimal raw text for agent context.
Expand All @@ -43,42 +43,47 @@ raw stdout/stderr saved under .ancp/runs/<run-id>/
result.check JSON written to .ancp/last-check.json
|
v
compact raw text emitted to the agent when the command fails
ultra-minimal error text emitted to the agent when the command fails
```

Successful commands pass through by default in `auto-compact` mode. Failed
commands emit compact ANCP text and keep full native output on disk.
Successful commands pass through by default in `auto-ultra` mode. Failed
commands emit surgical one-line ANCP text and keep full native output on disk.

## Output Modes

| Mode | Behavior |
|---|---|
| `passthrough` | Print native stdout/stderr exactly as emitted. |
| `auto-compact` | Print native output for successful commands; print compact ANCP text for failed commands. |
| `auto-ultra` | Print native output for successful commands; print the shortest useful ANCP text for failed commands. |
| `auto-compact` | Compatibility alias for auto-ultra failure rendering. |
| `ultra` | Always print ultra-minimal ANCP text. |
| `compact` | Always print compact ANCP raw text. |
| `json` | Print the full `result.check` JSON document. |
| `both` | Print native output and append compact ANCP text. |

Project-local shims default to `passthrough` unless configured otherwise.
`ancp enable` defaults to `auto-compact`.
`ancp enable` defaults to `auto-ultra`.

## Minimal Text Format
## Ultra Text Format

The compact text intentionally avoids Markdown syntax. It is designed for agent
context, not documentation.
The default failure output intentionally avoids Markdown syntax, protocol names,
raw log paths, token stats, and guidance. It is designed to be smaller than the
useful portion of the native error when possible.

```text
ANCP result.check failed diagnostics=1
exit=1 durationMs=92
raw=C:\repo\.ancp\runs\sha256-abc\native.log
tokens native~2400 compact~180 saved~92%
summary severity=error:1 kind=syntax:1
root_causes=1
1. code=ancp.diag.syntax.invalid native=SyntaxError kind=syntax count=1
at=C:/repo/app.py:15:1
msg=SyntaxError: expected ':'
fix=Fix Python syntax [review_required] c=0.40
agent_next=fix root_causes first; rerun native command before claiming verified
SyntaxError src/app.py:15 expected ':' fix:fix syntax
```

For repeated failures, ANCP groups by root cause:

```text
TS2304 src/app.ts:8 Cannot find name 'user' x14 fix:import symbol
```

The richer compact renderer is still available when wanted:

```powershell
ancp render --from .ancp\last-check.json --format text --budget 800
```

## Global Windows Enablement
Expand Down Expand Up @@ -128,7 +133,7 @@ $env:PATH="$HOME\.ancp\bin;$env:PATH"
Render compact text from any ANCP JSON document:

```powershell
ancp render --from .ancp\last-check.json --format text --budget 800
ancp render --from .ancp\last-check.json --format ultra --budget 200
```

Render Markdown for human inspection:
Expand All @@ -137,6 +142,35 @@ Render Markdown for human inspection:
ancp render --from .ancp\last-check.json --format markdown
```

## Raw Output And Bypass

Raw logs are saved but not printed in ultra output. Show the latest native log:

```powershell
ancp raw
```

Print only the raw log path:

```powershell
ancp raw --path
```

Show raw stderr from the latest run:

```powershell
ancp raw --stream stderr
```

Run a command without ANCP interception:

```powershell
ancp off -- python -m py_compile app.py
```

This is the intended manual escape hatch when an agent or user wants native
errors exactly as the compiler produced them.

## Raw Output Contract

Every proxied run records raw output metadata in `data.rawOutput`:
Expand All @@ -158,9 +192,9 @@ Every proxied run also records `data.signalMetrics`:
"nativeBytes": 2048,
"compactBytes": 420,
"estimatedNativeTokens": 512,
"estimatedCompactTokens": 105,
"estimatedSavingsPercent": 79,
"renderer": "raw-text"
"estimatedCompactTokens": 20,
"estimatedSavingsPercent": 96,
"renderer": "ultra"
}
```

Expand Down
4 changes: 2 additions & 2 deletions docs/toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ Python and PowerShell usually exist on Windows machines already, but they are st
| --- | --- |
| Python | `python` |
| TypeScript | `tsc` |
| JavaScript | `eslint` |
| JavaScript | `eslint` preferred, `node --check` fallback |
| Rust | `cargo`, `rustc` |
| Go | `go` |
| C | `gcc` or `clang` |
| C++ | `g++` or `clang++` |
| Java | `javac` |
| Kotlin | `kotlinc` |
| C#/.NET | `dotnet` |
| C#/.NET | .NET SDK via `dotnet` |
| Swift | `swift` |
| Zig | `zig` |
| Ruby | `ruby` |
Expand Down
5 changes: 5 additions & 0 deletions examples/buggy/javascript/broken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function broken(value) {
if (value > 1) {
console.log(value)
}

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ ancp = "ancp.cli:main"
ancp-validate = "ancp.cli:validate_main"
ancp-tsc = "ancp.proxy:tsc_main"
ancp-eslint = "ancp.proxy:eslint_main"
ancp-node = "ancp.proxy:node_main"
ancp-pyright = "ancp.proxy:pyright_main"
ancp-ruff = "ancp.proxy:ruff_main"
ancp-python = "ancp.proxy:python_main"
Expand Down
66 changes: 66 additions & 0 deletions src/ancp/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,30 @@ class JavaScriptAdapter(Adapter):
tools = [
ToolSpec("eslint", "linter", ["eslint", "--format", "json", "."]),
ToolSpec("eslint", "linter", ["npx", "--no-install", "eslint", "--format", "json", "."], version_args=["npx", "--no-install", "eslint", "--version"]),
ToolSpec("node", "compiler", ["node", "--check"], version_args=["node", "--version"]),
]

def run_check(self, root: pathlib.Path, tool: ToolSpec, timeout: int) -> CommandResult:
if tool.name != "node":
return run_command(tool.command, root, timeout)
files = [path for path in list_files(root, self.file_extensions, limit=100) if path.suffix.lower() in {".js", ".mjs", ".cjs"}]
if not files:
return run_command(["node", "--version"], root, timeout=timeout)
stdout: list[str] = []
stderr: list[str] = []
last: CommandResult | None = None
for path in files:
last = run_command(["node", "--check", str(path)], root, timeout=timeout)
stdout.append(last.stdout)
stderr.append(last.stderr)
if last.exit_code not in (0, None):
break
assert last is not None
return CommandResult(last.argv, root, last.started_at, last.ended_at, last.duration_ms, last.exit_code, "\n".join(stdout), "\n".join(stderr))

def parse_result(self, root: pathlib.Path, result: CommandResult, tool: ToolSpec) -> list[dict[str, Any]]:
if tool.name == "node":
return self._parse_node_check(root, result.stderr + "\n" + result.stdout)
import json

try:
Expand Down Expand Up @@ -312,6 +333,43 @@ def parse_result(self, root: pathlib.Path, result: CommandResult, tool: ToolSpec
)
return diagnostics

def _parse_node_check(self, root: pathlib.Path, text: str) -> list[dict[str, Any]]:
import re

diagnostics: list[dict[str, Any]] = []
pattern = re.compile(r"^(?P<file>.+?\.(?:cjs|mjs|js)):(?P<line>\d+)$")
lines = text.splitlines()
for index, line in enumerate(lines):
match = pattern.match(line.strip())
if not match:
continue
message = "JavaScript syntax error"
native_code = "SyntaxError"
for candidate in lines[index + 1 : index + 10]:
stripped = candidate.strip()
if stripped.startswith("SyntaxError:"):
message = stripped.removeprefix("SyntaxError:").strip()
break
path = pathlib.Path(match.group("file"))
if not path.is_absolute():
path = (root / path).resolve()
canonical, kind, hints = canonical_for_native(native_code, message)
diagnostics.append(
doc.diagnostic(
f"diag-node-{len(diagnostics) + 1:04d}",
canonical,
native_code,
"error",
kind,
message,
doc.location(path, "javascript", int(match.group("line")) - 1, 0),
"node",
hints,
{"raw": "\n".join(lines[index : index + 8])},
)
)
return diagnostics


class PythonAdapter(Adapter):
key = "python"
Expand Down Expand Up @@ -500,6 +558,14 @@ class DotnetAdapter(Adapter):
markers = {".sln", ".csproj", ".fsproj", ".vbproj", "Directory.Build.props"}
tools = [ToolSpec("dotnet", "build", ["dotnet", "build", "--nologo"])]

def available_tool(self) -> ToolSpec | None:
if not find_executable("dotnet"):
return None
result = run_command(["dotnet", "--list-sdks"], pathlib.Path.cwd(), timeout=10)
if result.exit_code in (0, None) and result.stdout.strip():
return self.tools[0]
return None

def parse_result(self, root: pathlib.Path, result: CommandResult, tool: ToolSpec) -> list[dict[str, Any]]:
return parse_text_lines(result.stdout + "\n" + result.stderr, DOTNET_RE, root, "csharp", "dotnet", "diag-dotnet")

Expand Down
Loading
Loading