Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
11 changes: 11 additions & 0 deletions .changeset/systems-language-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"agentic-debug-mode": minor
---

Add probe templates for five more languages — C, C++, Rust, Java, and Kotlin — bringing the
advertised helper/runtime pairs from nine to fourteen (all file ingest). Java and Kotlin build
structured values with at-source redaction; C, C++, and Rust use a lightweight serialized-JSON
interface: the call site passes a complete JSON value (from the application's own serializer, or
the helper's `json_string` text fallback), the daemon performs canonical redaction, and the helper
stays small — envelope, timestamp, size cap, control-character/framing guard, and secure append
only.
56 changes: 51 additions & 5 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,53 @@ The first supported combinations are:
- PowerShell + file
- C# + file
- Swift + file
- Rust + file
- C++ + file
- C + file
- Java + file
- Kotlin + file

Every advertised combination must pass a live end-to-end test with its real runtime. Rust, Java,
Kotlin, C, C++, and shell are not advertised until a safe serializer contract is defined.
Every advertised combination must pass a live end-to-end test with its real runtime. Shell is
not advertised until a safe serializer contract is defined.

#### Data encoding

Each template declares how its call site supplies the `data` field:

- `native-json-value`: the call site passes a native language value, and the helper serializes and
redacts it client-side. JavaScript, TypeScript, and the languages with a safe standard JSON
facility (Python, Go, Ruby, PHP, PowerShell, C#, Swift, Java, Kotlin) keep this encoding. Their
data placeholder is `__DATA_EXPRESSION__`.
- `serialized-json`: the call site passes an expression that already evaluates to one complete
serialized JSON value, inserted verbatim after `"data":`. Rust, C++, and C use this encoding
because their languages have no standard structured JSON value; embedding a recursive serializer
and redactor in every instrumented file was disproportionately large. Their data placeholder is
`__DATA_JSON_EXPRESSION__`.

Serialized-JSON helpers expose one small fallback, `json_string(text)` (C++/Rust
`agent_debug_mode::json_string`, C `agent_debug_json_string`), that encodes arbitrary text as a
JSON string when the application has no serializer. The agent must never concatenate unescaped
strings into raw JSON by hand.

#### Placement

Each template declares machine-readable placement so the helper lands at a legal location:

- `helper: "file-start"` — C and C++ (their includes and `#define`s must precede declarations).
- `helper: "top-level"` — Rust and every other language (module or file scope).
- `call: "statement"` — every language inserts each call in statement position.

The agent inserts the helper once at the required location and wraps every call and the helper in
its own foldable `// #region agent log` / `// #endregion` region (or the language's line-comment
equivalent) so instrumentation is easy to find and remove.

#### Secret responsibility

The call site is responsible for excluding secrets: the agent chooses the smallest diagnostic value
that tests a hypothesis and never places credentials, tokens, or unnecessary personally
identifiable information in `data`. Helpers do not scan or redact before transport. The daemon still
validates and redacts accepted JSON before canonical persistence, so daemon redaction is
defense-in-depth, not permission to send secrets.

### `debug-mode reset --session <id>`

Expand Down Expand Up @@ -277,11 +321,13 @@ is stored once in session metadata.

```text
POST /ingest/<sessionId>
Content-Type: application/json
Content-Type: application/x-ndjson
```

There is no separate token, capability terminology, session header, or session field in the body.
The loopback route determines the session.
The HTTP helpers post newline-delimited JSON, so the actual request content type is
`application/x-ndjson`; the daemon parses the body as one-or-more NDJSON records. There is no
separate token, capability terminology, session header, or session field in the body. The loopback
route determines the session.

### File

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ reproduces the failure, and proves the root cause with runtime evidence before i

Inspired from [Cursor's debug mode](https://cursor.com/blog/debug-mode) and built upon it: the same
hypothesis-driven core, made agent-agnostic (any coding agent that can run a CLI), and extended
with a session-scoped evidence store, hypothesis-tagged probes across nine languages, embedded
with a session-scoped evidence store, hypothesis-tagged probes across fourteen languages, embedded
structured queries over captured events, at-source secret redaction, and bounded reads that keep
large evidence sets token-cheap.

Expand Down Expand Up @@ -86,6 +86,11 @@ runtime:
| PowerShell | file |
| C# | file |
| Swift | file |
| Rust | file |
| C++ | file |
| C | file |
| Java | file |
| Kotlin | file |

## Development

Expand Down
25 changes: 24 additions & 1 deletion scripts/build-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,30 @@ const outputDirectory = join(root, "dist");
const executableName = process.platform === "win32" ? "debug-mode.exe" : "debug-mode";
const executable = join(outputDirectory, executableName);

await rm(outputDirectory, { force: true, recursive: true });
// On Windows a just-exited process (e.g. a debug-mode daemon that ran the
// previous test step's dist/debug-mode.exe) can keep the executable's file
// handle open for a short window after it terminates, so removing dist races
// that release and fails with EACCES/EPERM/EBUSY. Retry with a short backoff
// until the handle is released; other platforms release handles on exit and
// succeed on the first attempt.
async function removeOutputDirectory(): Promise<void> {
const maxAttempts = 20;
for (let attempt = 1; ; attempt += 1) {
try {
await rm(outputDirectory, { force: true, recursive: true });
return;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
const retriable = code === "EACCES" || code === "EPERM" || code === "EBUSY";
if (!retriable || attempt >= maxAttempts) {
throw error;
}
await Bun.sleep(250);
}
}
}

await removeOutputDirectory();
await mkdir(outputDirectory, { recursive: true });

const processHandle = Bun.spawn(
Expand Down
60 changes: 60 additions & 0 deletions skills/agentic-debug-mode/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,66 @@ debug-mode status --session <id>
After the fix, `reset`, reproduce, and re-run the grouping query. When the skipped cluster is gone
and expected tasks appear, remove the regions and `debug-mode stop`.

## Example 3 — Rust worker computes a wrong queue depth (serialized JSON, file)

**Symptom.** A background worker occasionally pops from an empty queue. Hypothesis: `H1` the depth
snapshot is stale by the time the pop runs.

```bash
debug-mode create
debug-mode template --language rust --ingest file
```

Rust, C++, and C are `serialized-json` templates: the call placeholder is `__DATA_JSON_EXPRESSION__`,
and you pass an expression that already evaluates to one complete JSON value. Insert the helper once
at module scope (its declared placement is `top-level`), then a call region per hypothesis. Prefer
the application's serializer — here `serde_json` — and keep the emit inside the region:

```rust
// #region agent log
if let Ok(__agent_debug_data) = serde_json::to_string(&serde_json::json!({
"queueDepth": queue.len(),
"ready": ready,
})) {
agent_debug_mode::emit(
"H1",
&format!("{}:{}", file!(), line!()),
"Depth snapshot before pop",
&__agent_debug_data,
);
}
// #endregion
```

When no serializer is available, fall back to the helper's `json_string` on a concise text summary
rather than hand-building JSON (this is also how C uses `agent_debug_json_string`, and C++
`agent_debug_mode::json_string`):

```rust
// #region agent log
agent_debug_mode::emit(
"H1",
&format!("{}:{}", file!(), line!()),
"Depth snapshot before pop",
&agent_debug_mode::json_string(&format!("queueDepth={} ready={}", queue.len(), ready)),
);
// #endregion
```

The `data` field is then a JSON string: you can still filter by hypothesis, location, message, and
timestamp, but not by fields inside the text. Because these helpers do no client-side redaction,
choose the smallest diagnostic value and never place secrets in `data`.

```bash
debug-mode reset --session <id>
cargo run --bin worker
debug-mode query --session <id> 'select(.hypothesisId == "H1" and .data.queueDepth == 0)'
```

Rows where `queueDepth == 0` immediately before a pop confirm the stale snapshot. `H1` **CONFIRMED**.
Fix, record the baseline, `reset`, reproduce, and confirm the rows are gone before removing the
regions and `debug-mode stop`.

## Recovering a lost session

If you no longer have the Session ID:
Expand Down
24 changes: 24 additions & 0 deletions skills/agentic-debug-mode/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,34 @@ schema** for one language and transport. Templates never take `--session`.
| PowerShell | `powershell` (`pwsh`) | `file` |
| C# | `csharp` (`cs`, `c#`) | `file` |
| Swift | `swift` | `file` |
| Rust | `rust` (`rs`) | `file` |
| C++ | `cpp` (`c++`, `cxx`) | `file` |
| C | `c` | `file` |
| Java | `java` | `file` |
| Kotlin | `kotlin` (`kt`) | `file` |

HTTP templates use an ingest-URL placeholder; file templates use an append-path placeholder. Other
languages are unsupported until a safe serializer contract is defined.

The template output includes machine-readable **data encoding** and **placement** metadata:

- **Data encoding.** `native-json-value` templates take a native value in `__DATA_EXPRESSION__` and
serialize plus redact it client-side (JavaScript, TypeScript, Python, Go, Ruby, PHP, PowerShell,
C#, Swift, Java, Kotlin). `serialized-json` templates (Rust, C++, C) take `__DATA_JSON_EXPRESSION__`,
an expression already evaluating to one complete serialized JSON value inserted verbatim after
`"data":`. These languages have no standard structured JSON value, so the recursive serializer and
redactor were removed from the instrumented file; use the application's serializer, or the helper's
`json_string(text)` fallback (`agent_debug_mode::json_string` for C++/Rust, `agent_debug_json_string`
for C) to encode plain text. Never concatenate unescaped strings into raw JSON.
- **Placement.** `helper: "file-start"` for C and C++ (includes and `#define`s precede
declarations); `helper: "top-level"` for Rust and every other language; `call: "statement"` for
all. Each call and the helper belong in their own `agent log` region.

Regardless of encoding, the call site is responsible for excluding secrets and choosing the smallest
diagnostic value. Serialized-JSON helpers perform no client-side redaction; the background service
still validates and redacts accepted JSON before canonical persistence (defense-in-depth). HTTP
helpers post newline-delimited JSON with content type `application/x-ndjson`.

### `debug-mode reset --session <id>`

Clears events, diagnostics, and sequence state for the session while preserving the session ID,
Expand Down
35 changes: 23 additions & 12 deletions skills/agentic-debug-mode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ Terms used throughout, defined once:
track these; the CLI never registers, declares, or validates them. It only groups and filters the
labels it sees in evidence.
- **Helper template** — a block of code, inserted **once** per runtime, that owns transport,
serialization, size limits, secret redaction, and failure suppression. Treat it as opaque.
- **Call template** — a small block copied **once per observation**. You replace only its
placeholders.
envelope construction, size limits, and failure suppression. Treat it as opaque. Its declared
**placement** says where it must go: at file start for C and C++ (their includes must precede
declarations), at module or file scope for every other language.
- **Call template** — a small block copied **once per observation**, always in statement position.
You replace only its placeholders. Most languages take a native value in `__DATA_EXPRESSION__`
and serialize it for you; Rust, C++, and C instead take `__DATA_JSON_EXPRESSION__`, an expression
that already evaluates to one complete serialized JSON value (use the application's serializer, or
the helper's `json_string(text)` fallback for plain text — never hand-concatenate raw JSON).
- **Observation** — one `agent log` region emitting one event that tests one hypothesis.
- **Ingest URL** — the loopback address an HTTP helper posts to. The session is in the URL path;
it is not a secret and needs no header.
Expand Down Expand Up @@ -114,7 +119,9 @@ with the Append Path. Advertised combinations, each verified end-to-end:
| TypeScript | http | PHP | file |
| Python | file | PowerShell | file |
| Go | file | C# | file |
| Swift | file | | |
| Swift | file | Rust | file |
| C++ | file | C | file |
| Java | file | Kotlin | file |

The output has four sections: **HELPER TEMPLATE**, **CALL TEMPLATE**, **PLACEHOLDERS**, and
**EVENT SCHEMA**.
Expand All @@ -138,9 +145,10 @@ __agentDebugEmit({
// #endregion
```

Python and other file runtimes use `# region agent log` / `# endregion`. Never put production
behavior inside a region, never `await` an observation, and never instrument a generated file whose
syntax cannot hold the markers.
Python and Ruby use `# region agent log` / `# endregion`; Rust, C, C++, Go, Java, Kotlin, C#, and
Swift use the `// #region agent log` / `// #endregion` line-comment form. Keep whichever markers the
template prints. Never put production behavior inside a region, never `await` an observation, and
never instrument a generated file whose syntax cannot hold the markers.

### Event schema

Expand All @@ -155,11 +163,14 @@ Every observation carries exactly five fields:
Stored evidence adds `id`, `sequence` (order within the reset cycle), and `receivedAt` (receipt
time, also Unix epoch milliseconds). When timestamps tie, `sequence` breaks the tie.

Keep `message` constant and put changing values in `data`. Keep `data` small and bounded. Never
record passwords, cookies, authorization headers, private keys, full request bodies, or unrelated
personal data — the helper redacts obvious secrets, but you choose what to observe. A failed
observation must never change application control flow; the helper isolates transport failures, so
missing events mean an unexecuted path or a failed run, never a crash.
Keep `message` constant and put changing values in `data`. Keep `data` small and bounded — choose
the smallest diagnostic value that tests the hypothesis. Excluding secrets is your responsibility:
never record passwords, cookies, authorization headers, private keys, full request bodies, or
unrelated personal data. Redaction before stored evidence is a safety net, not permission to send
secrets — and the Rust, C++, and C helpers do no client-side redaction at all, so whatever the call
site passes is what leaves the process. A failed observation must never change application control
flow; the helper isolates transport failures, so missing events mean an unexecuted path or a failed
run, never a crash.

### 3. Reset and reproduce

Expand Down
37 changes: 31 additions & 6 deletions specs/building-a-debug-mode-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,29 @@ advertised, end-to-end-tested combinations are:
- PowerShell + file
- C# + file
- Swift + file
- Rust + file
- C++ + file
- C + file
- Java + file
- Kotlin + file

Other languages are not advertised until a safe serializer contract is defined.

The template also reports **data encoding** and **placement** metadata:

- **Data encoding.** `native-json-value` templates take a native value in `__DATA_EXPRESSION__` and
serialize plus redact it client-side (JavaScript, TypeScript, Python, Go, Ruby, PHP, PowerShell,
C#, Swift, Java, Kotlin). `serialized-json` templates (Rust, C++, C) take `__DATA_JSON_EXPRESSION__`,
an expression already evaluating to one complete serialized JSON value inserted verbatim after
`"data":`; these languages have no standard structured JSON value, so the recursive serializer and
redactor no longer live in the instrumented file. Use the application's serializer or the helper's
`json_string(text)` fallback (`agent_debug_mode::json_string` for C++/Rust, `agent_debug_json_string`
for C), and never hand-concatenate raw JSON.
- **Placement.** `helper: "file-start"` for C and C++ (includes and `#define`s precede
declarations), `helper: "top-level"` for Rust and the rest, and `call: "statement"` for every
language. Consumers should read placeholder names from the template's placeholder map rather than
assuming a fixed name, because the serialized-JSON rename is deliberately breaking.

#### `debug-mode reset --session <id>`

Clears events, diagnostics, and sequence state while preserving the session ID, append path, and
Expand Down Expand Up @@ -234,10 +254,14 @@ tie-breaker. The schema version is stored once in session metadata.

### Data rules

- Use a native JSON serializer; never build JSON by string interpolation.
- Keep `message` constant; put changing values in `data`.
- Never record passwords, tokens, cookies, authorization headers, private keys, full request
bodies, or unrelated personal data. Secrets are redacted before canonical persistence.
- Use a native serializer, or the serialized-JSON helper's `json_string(text)` fallback for plain
text; never build JSON by string interpolation.
- Keep `message` constant; put changing values in `data`, and choose the smallest diagnostic value
that tests the hypothesis.
- Excluding secrets is the call site's responsibility: never record passwords, tokens, cookies,
authorization headers, private keys, full request bodies, or unrelated personal data. Redaction
before canonical persistence is defense-in-depth, not permission to send secrets, and the Rust,
C++, and C helpers do no client-side redaction at all.
- Bound strings, arrays, and object depth. A failed observation must never change control flow.

## 4. Ingestion
Expand All @@ -246,9 +270,10 @@ tie-breaker. The schema version is stored once in session metadata.

```text
POST /ingest/<sessionId>
Content-Type: application/json
Content-Type: application/x-ndjson
```

The HTTP helpers post newline-delimited JSON, so the actual content type is `application/x-ndjson`.
The loopback route determines the session. There is no separate token, header, or session field in
the body.

Expand All @@ -266,7 +291,7 @@ visually distinct, foldable, mechanically removable, and auditable as complete b

| Language/file type | Open | Close |
| --------------------------------- | ---------------------- | --------------- |
| JavaScript, TypeScript, Go, Swift | `// #region agent log` | `// #endregion` |
| JavaScript, TypeScript, Go, Swift, Rust, C++, C, Java, Kotlin | `// #region agent log` | `// #endregion` |
| C#, PowerShell | `#region agent log` | `#endregion` |
| Python, Ruby | `# region agent log` | `# endregion` |
| PHP | `// #region agent log` | `// #endregion` |
Expand Down
Loading
Loading