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
7 changes: 7 additions & 0 deletions .changeset/capture-codex-exec-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@donadiosolutions/lcm": patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. @donadiosolutions/lcm patch bump too low 📘 Rule violation § Compliance

The changeset declares a patch release, but the PR introduces new user-facing behavior (native
Codex exec capture and new connector diagnostics). Per the checklist, this should be at least a
minor bump.
Agent Prompt
## Issue description
The changeset uses a `patch` bump even though the change adds new user-facing behavior.

## Issue Context
This PR adds native Codex `functions.exec` / `functions.exec_command` capture behavior and new `lcm connectors doctor codex` diagnostics, which is more than a patch-level internal fix.

## Fix Focus Areas
- .changeset/capture-codex-exec-context.md[1-3]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

---

Capture bounded semantic context from Codex `functions.exec` and
`functions.exec_command` PostToolUse events, and validate the installed Codex
hook with structural and no-write functional connector checks.
33 changes: 33 additions & 0 deletions bin/lcm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2380,6 +2380,7 @@ export async function runCli(

const installed = listConnectors(opts.global ? homedir() : process.cwd());
console.log("\n Connector health:\n");
let failures = 0;
for (const agent of agents) {
const agentConnectors = installed.filter((c: any) => c.agentId === (agent as any).id);
if ((agentConnectors as any[]).length === 0) {
Expand All @@ -2389,8 +2390,40 @@ export async function runCli(
console.log(` ✓ ${(agent as any).name}: ${c.type} at ${c.path}`);
}
}

if ((agent as any).id !== "codex" || agentName === undefined) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Undocumented as any cast 📘 Rule violation ⚙ Maintainability

A new (agent as any) cast was added without an adjacent justification comment. This violates the
requirement that each as any usage in changed TypeScript code be explicitly justified.
Agent Prompt
## Issue description
A newly added `as any` type assertion is missing the required justification comment.

## Issue Context
The checklist requires an immediately-adjacent comment explaining why `as any` is necessary.

## Fix Focus Areas
- bin/lcm.ts[2393-2395]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Codex health check skipped without explicit agent 🐞 Bug ≡ Correctness

The connectors doctor command's new Codex structural/functional check is guarded by `agentName ===
undefined, so running the documented broad lcm connectors doctor` (no agent argument) silently
skips both the exact PostToolUse structural inspection and the native-exec functional probe, even
though the loop still iterates Codex within the AGENTS list. This causes the new health
verification introduced by this PR to be effectively unreachable for the command's most common
invocation form.
Agent Prompt
## Issue description
The Codex-specific PostToolUse structural/functional health check added in this PR is skipped whenever `lcm connectors doctor` is invoked without an explicit agent name (broad mode over all agents), because of an added `agentName === undefined` condition in the skip check. This means the new diagnostics never run for the most common/documented usage of `lcm connectors doctor`.

## Issue Context
`agents` is populated with all agents (including Codex) when `agentName` is undefined (broad mode). The loop over `agents` should run the Codex-specific checks whenever the current `agent.id === "codex"`, regardless of whether the user passed an explicit agent name.

## Fix Focus Areas
- bin/lcm.ts[2394-2394]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


const {
inspectCodexPostToolHook,
resolveCodexHooksPath,
} = await import("../src/connectors/codex-hooks.js");
const { codexPostToolFunctionalCoverage } = await import("../src/hooks/post-tool-normalization.js");
const inspection = inspectCodexPostToolHook(
resolveCodexHooksPath(opts.global ? homedir() : process.cwd()),
);
Comment on lines +2401 to +2403

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Doctor ignores hooks feature flag 🐞 Bug ≡ Correctness

Targeted Codex doctor declares PostToolUse installed after checking only hooks.json, while Codex
also requires [features].hooks = true in config.toml. If that flag is missing or disabled, the
pure adapter probe still passes and doctor reports healthy even though Codex will not dispatch the
hook.
Agent Prompt
## Issue description
`lcm connectors doctor codex` checks the PostToolUse entry and an in-memory adapter probe but does not verify that Codex hook execution is enabled in `config.toml`. Extend structural health validation to require `[features].hooks = true`, reporting failure and skipping the functional probe when the runtime feature is absent or disabled.

## Issue Context
Codex connector installation enables the feature separately from writing `hooks.json`, so either file can drift independently. Resolve and inspect the canonical config path using the same installation scope and keep the check pure/no-write.

## Fix Focus Areas
- bin/lcm.ts[2396-2422]
- src/connectors/codex-hooks.ts[273-307]
- src/connectors/codex-hooks.ts[173-238]
- test/connectors/codex-hooks.test.ts[104-154]
- test/bin/lcm-run-cli.test.ts[886-946]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


if (inspection.state === "installed") {
console.log(" ✓ Codex: PostToolUse hook installed");
let functional = false;
try {
functional = codexPostToolFunctionalCoverage();
} catch {
functional = false;
}
if (functional) {
console.log(" ✓ Codex: native exec capture functional");
} else {
console.log(" ✗ Codex: native exec capture functional");
failures += 1;
}
} else {
console.log(` ✗ Codex: PostToolUse hook ${inspection.state}`);
console.log(" Codex: native exec capture functional check skipped");
failures += 1;
}
}
console.log();
if (failures > 0) exit(1);
});

program.addCommand(connectorsCmd);
Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ component_management:
- component_id: "unit-hooks"
name: "Unit - Hooks"
paths:
# Includes native hook adapters under the existing hooks directory ownership.
- "src/hooks/"
- component_id: "unit-daemon-core"
name: "Unit - Daemon Core"
Expand Down
72 changes: 72 additions & 0 deletions docs/hook-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,78 @@ The `daemon_port` payload field is ignored. PostToolUse never sends the daemon
bearer token or captured event data to a payload-selected listener; queued
events are collected by the daemon's bounded background processing instead.

### Codex native PostToolUse capture

The Codex connector uses the following exact hook entry in the canonical
`~/.codex/hooks.json` file (or the equivalent path selected by the existing
connector install scope):

```json
{
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "lcm post-tool --client codex"
}
]
}
]
}
```
Comment on lines +171 to +185

The `matcher`, hook `type`, and command are structural contract values. The
installed connector may also retain its timeout and status-message metadata,
but the command must remain exactly `lcm post-tool --client codex`; extra
arguments do not satisfy the contract. Install it with:

```bash
lcm connectors install codex
lcm connectors doctor codex
```

The `--global` option selects the existing global connector scope; no new
configuration option is required for native command capture, and the 2,000-
character adapter bound is fixed rather than configurable.

Codex sends native tool names `functions.exec` and `functions.exec_command`.
For those names, lcm accepts only the bounded semantic command (`command`, or
`cmd` when `command` is absent) and a direct status projection. `tool_output` is
checked before `tool_response`; status fields are considered in this order:
`isError`, `is_error`, `exit_code`, and `exitCode`. Boolean values are used
directly, while finite numeric exit codes map zero to success and nonzero to an
error. A valid false or zero value is authoritative. Nested or invalid values
are ignored.

The adapter does not persist raw Codex responses, stdout, stderr, or unknown
fields, and it does not infer file events from shell text or unrecognized
file-like fields. The existing event truncation and scrubbing pipeline still
runs on derived event data. Commands whose trimmed text begins with `lcm
store` are suppressed to prevent LCM's own writes from feeding back into
passive learning.

`lcm connectors doctor codex` performs two checks for the targeted Codex
connector. It first verifies the exact structural hook contract above. Only
when that check passes does it run the native-exec functional probe. The probe
is pure and in-memory: it exercises normalization and extraction without
invoking the PostToolUse handler, opening an EventsDb, appending sidecar
events, writing hook files, or creating a database. A structurally absent or
incomplete hook never reports functional success; its functional result is
reported as:

```text
Codex: native exec capture functional check skipped
```

When the exact structure and the pure probe both pass, doctor reports:

```text
✓ Codex: PostToolUse hook installed
✓ Codex: native exec capture functional
```

## SessionSnapshot Hook

**Command:** `lcm session-snapshot`
Expand Down
36 changes: 34 additions & 2 deletions docs/passive-learning.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Passive learning captures insights from your Claude Code sessions automatically

Two hooks capture events during your session:

- **PostToolUse** — fires after every tool call. Extracts structured metadata (tool name, command, file path) from tool inputs. Never captures raw tool output.
- **PostToolUse** — fires after every tool call. Extracts structured metadata (tool name, command, file path) from tool inputs. Never captures raw tool output. For Codex, the native `functions.exec` and `functions.exec_command` calls are adapted into the existing Bash event semantics.
- **UserPromptSubmit** — fires on each user prompt. Detects decisions ("always use X"), role statements ("I'm a data scientist"), and intent patterns.

Events are written to a **sidecar SQLite database**
Expand All @@ -34,6 +34,36 @@ new global sequences during their transactional schema upgrade.

### What Gets Captured

#### Codex native command capture

The Codex connector recognizes only PostToolUse payloads marked with
`client: "codex"` whose `tool_name` is exactly `functions.exec` or
`functions.exec_command`. The adapter accepts the string `tool_input.command`
first, or the string `tool_input.cmd` when `command` is not a string. Other
command-like, path-like, and file-like fields are ignored; a blank `command`
does not fall back to `cmd`.

The adapter passes only bounded semantic information into the existing Bash
extractor:

- A command is limited to 2,000 characters at the adapter boundary; longer
commands are clipped with `...` before event classification.
- The existing event-data truncation and event scrubbing still run after
classification, including the normal sensitive-path redaction rules.
- Status is read only from direct, top-level fields. `tool_output` wins over
`tool_response` when it contains a valid status. Within either object, the
precedence is `isError`, `is_error`, `exit_code`, then `exitCode`; boolean
values are used directly and finite numeric exit codes treat zero as
success and any other value as an error. Invalid, nested, string, `NaN`, and
infinite values are ignored.
- The raw Codex response, stdout, stderr, and unknown output fields are never
copied into the normalized event. Shell text is not parsed into file events.
- A command whose trimmed text begins with `lcm store` is suppressed so LCM's
own storage activity cannot create a passive-learning feedback loop.

Only events recognized by the existing Bash extractor are queued. The command
itself is not stored as a transcript.

| Category | Examples | Priority |
|----------|----------|----------|
| Decisions | User answers to AskUserQuestion, "always use TypeScript" | 1 (immediate) |
Expand All @@ -47,7 +77,9 @@ new global sequences during their transactional schema upgrade.

### What Is NOT Captured

- Raw tool payload contents such as file contents and command stdout/stderr (only tool metadata and brief user answers are stored)
- Raw tool payload contents such as file contents, command stdout/stderr, and
unknown Codex response fields (only bounded semantic metadata and brief user
answers are stored)
- Sensitive file paths (`.env`, `.ssh/`, `credentials`, `.npmrc`)
- LCM's own `lcm_store` calls (prevents feedback loops)

Expand Down
40 changes: 23 additions & 17 deletions docs/superpowers/plans/2026-08-11-codex-post-tool-capture.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
- Start only after the CLI PR for #602/#603 merges. Fetch updated `origin/main`, create a fresh isolated worker workspace at `UPDATED_MAIN=$(git rev-parse --verify 'origin/main^{commit}')`, require `test "$(git rev-parse HEAD)" = "$UPDATED_MAIN"`, persist it with `git update-ref refs/lcm/implementation-bases/issue-604-codex-post-tool "$UPDATED_MAIN"`, and create branch `fix/604-codex-post-tool-capture` from that durable ref; never use a `codex/` prefix.
- Do not use, clean, stage, or modify the coordinator worktree or pre-existing files outside this branch.
- Add no dependency and preserve exact pins and lockfile integrity.
- Recognize only `client: "codex"` payloads with `functions.exec` or `functions.exec_command` names and explicit bounded command fields.
- Never serialize raw `tool_input`, `tool_response`, stdout, stderr, or unknown output into an event.
- Recognize only `client: "codex"` payloads with `functions.exec` or `functions.exec_command` names and explicit bounded command fields. Copy at most 2,000 command characters plus a literal `...` truncation marker into the canonical in-memory shape; no raw command object survives normalization.
- Never serialize raw `tool_input`, `tool_response`, stdout, stderr, or unknown output into an event. The bounded command is used only by the allowlisted semantic extractor, and extracted event data passes through the existing built-in and project-aware scrubber before enqueue.
- Preserve feedback-loop exclusions, built-in/project scrubbing, truncation, and append-before-project-metadata ordering.
- Functional doctor is pure/no-write and must not touch the user's event database.
- The existing `unit-hooks` directory path already exclusively owns the new production file. Update `codecov.yml` atomically with the count test by documenting that the directory path owns native hook adapters, but do not add a redundant exact-file path; update the literal expected production-file count in `test/codecov-config.test.ts`.
Expand Down Expand Up @@ -57,7 +57,7 @@ Create table-driven tests for:

Normalize each fixture, pass it to `extractPostToolEvents`, and assert the existing event type/category/priority. Add negative fixtures for non-Codex clients, unknown `functions.*` names, non-string `cmd`/`command`, lcm-store feedback-loop names, and response objects containing a sentinel secret. Assert the secret never appears in normalized input or event data.

Add status fixtures with this exact policy: inspect only top-level status fields in record-valued `tool_output` and `tool_response`; prefer `tool_output` over `tool_response`; within one record prefer boolean `isError`, then boolean `is_error`, then finite numeric `exit_code`, then finite numeric `exitCode`. A boolean maps directly, numeric zero maps to false, and every other finite number maps to true. Strings, nested objects, `NaN`, and infinities are ignored. An explicit higher-precedence false overrides a lower-precedence nonzero code. If no recognized field exists, omit canonical `tool_output`. Add conflict fixtures proving every precedence rule.
Add status fixtures with this exact policy: inspect only top-level status fields in record-valued `tool_output` and `tool_response`, consulting `tool_output` first. Within one record, select the first valid field in this precedence order: boolean `isError`, boolean `is_error`, finite numeric `exit_code`, finite numeric `exitCode`. A boolean maps directly, numeric zero maps to false, and every other finite number maps to true. A valid false/zero wins. Strings, nested objects, `NaN`, and infinities are invalid and ignored. Fall back to `tool_response` only when `tool_output` contains no valid recognized field. If neither source contains one, omit canonical `tool_output`. Add conflict and invalid-field fixtures proving every source and field precedence rule.

No captured structured Codex file-operation shape is available in #604. Add negative fixtures proving nested/unknown `operation`, `path`, and file-like fields cannot produce file events, and shell text such as `cat`, `sed`, `rm`, or redirects is never parsed into file events. Do not invent a structured mapping without a real captured payload.

Expand Down Expand Up @@ -93,23 +93,29 @@ export function normalizePostToolInput(input: RawPostToolInput): PostToolInput {
}
```

Keep command selection deterministic: prefer `command` when it is a string,
otherwise `cmd`; trim only to decide whether a command is empty. An empty or
missing command produces an inert canonical input and no event regardless of
status, while the original non-empty command string is retained for existing
bounded extractor semantics.
Keep command selection deterministic: when `command` is a string it wins even
when blank; only if it is not a string may a string `cmd` be selected. Trim only
to decide whether the selected command is empty and to apply the native
feedback-loop matcher; do not fall back from a blank string `command` to `cmd`.
An empty or missing selected command produces an inert canonical input and no
event regardless of status. Bound a non-empty selected command before extraction
to its first 2,000 UTF-16 code units plus `...` when truncated. This mirrors the
existing event-data soft cap while preventing an unbounded raw command from
entering the extractor. Extracted data is then scrubbed by the existing
project-aware event scrubber before any durable write.

Implement functional coverage from fixed benign fixtures:

```ts
export function codexPostToolFunctionalCoverage(): boolean {
return [
{ tool_name: "functions.exec", tool_input: { command: "git branch" } },
{ tool_name: "functions.exec_command", tool_input: { cmd: "npm install probe" } },
].every(fixture => extractPostToolEvents(normalizePostToolInput({
client: "codex",
...fixture,
})).length === 1);
const fixtures = [
{ tool_name: "functions.exec", tool_input: { command: "git branch" }, expected: "git_branch" },
{ tool_name: "functions.exec_command", tool_input: { cmd: "npm install probe" }, expected: "env_install" },
];
return fixtures.every(({ expected, ...fixture }) => {
const events = extractPostToolEvents(normalizePostToolInput({ client: "codex", ...fixture }));
return events.length === 1 && events[0]?.type === expected;
});
}
```

Expand Down Expand Up @@ -223,7 +229,7 @@ For targeted installed Codex connector doctor, require output that distinguishes
✓ Codex: native exec capture functional
```

Targeted doctor must inspect the canonical `~/.codex/hooks.json` path using the same default/`--global` resolution as installation even when broad discovery finds only a partial installation. Preserve existing connector path output, then print structural and functional lines. Distinguish absent, incomplete, and installed-but-nonfunctional states; never print functional success when structure is absent/incomplete. Mock functional failure and assert actionable output plus exit 1.
Targeted doctor must inspect the canonical `~/.codex/hooks.json` path using the same default/`--global` resolution as installation even when broad discovery finds only a partial installation. Preserve existing connector path output, then print structural and functional lines. Distinguish absent, incomplete, and installed-but-nonfunctional states; never print functional success when structure is absent/incomplete. Print `Codex: native exec capture functional check skipped` when structure is absent or incomplete. Mock functional failure and assert actionable output plus exit 1.

Make the pure probe mockable before module import through an injected dependency or module mock. Assert doctor does not call `appendLocalHookEvents`, instantiate `EventsDb`, write hook files, or create an event database.

Expand All @@ -239,7 +245,7 @@ Expected: existing broad `hasCodexHooks` accepts partial hook files and doctor o

- [ ] **Step 4: Implement exact structural inspection and no-write probe**

Keep broad connector discovery compatibility unchanged. Targeted health calls the exact structural inspector and only then the in-memory functional probe. Aggregate failures and call `exit(1)` only after printing all requested agent results. Non-Codex connector behavior remains unchanged.
Keep broad connector discovery compatibility and its existing output unchanged. Targeted health calls the exact structural inspector and only then the in-memory functional probe. Aggregate failures and call `exit(1)` only after printing all requested agent results. The probe imports only the pure normalizer/extractor path: it must not import or call `handlePostToolUse`, `appendLocalHookEvents`, `EventsDb`, or filesystem setup. Non-Codex connector behavior remains unchanged.

- [ ] **Step 5: Run GREEN**

Expand Down
Loading
Loading