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
143 changes: 143 additions & 0 deletions packages/agents/content/skills/capture-lede-decision/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
name: capture-lede-decision
description: Record the author's decision about a merged pull request's lede — accepted as the agent wrote it, or revised — into the lede-decision corpus. Use after a merge, or to record a pull request merged outside the merge flow.
user-invocable: true
---

# Capture a lede decision

Record what the author decided about a lede: that the agent's `## What` shipped as written, or that it was rewritten before merge. A bundled helper does the mechanical work — it reads the lede the agent published and the lede that merged from the ticket's own artifacts, fingerprints the doctrine that governed the first, and writes one event record. You present the pair and relay the author's decision.

**Announce at start:** "Using capture-lede-decision to record the lede decision for #{pr}."

## The corpus stores positive signals only

A record exists because the author looked at the lede and decided. There are exactly two decisions, and no third:

- **`accepted`** — the author read the agent's lede and shipped it as written.
- **`revised`** — the author rewrote it before merge.

Declining to decide writes nothing. **The absence of a record carries no meaning, and in particular is not an acceptance**: a merge nobody evaluated is indistinguishable from a merge this skill never ran on. Never infer a verdict, and never record one the author did not give — a lede that shipped unchanged under time pressure is not an accepted lede, and recording it as one is the single failure that would make the corpus useless.

For the same reason, the corpus is outcome-selected: it holds only changes someone chose to evaluate. It is the right population for reading what good looks like and what typically fails, and the wrong one for measuring whether guidance helps. A comparison's fixture draw must never read it.

## Arguments

| Argument | Description | Required |
| -------------------- | --------------------------------------------------------------------------------------- | -------- |
| `--artifact-dir` | The ticket's artifact directory, holding the pull-request and merge artifacts. | Yes |
| `--pr` | The pull-request number. | Yes |
| `--merge-commit` | The merge commit's SHA. | Yes |
| `--inspect` | Resolve and report the episode without writing. Mutually exclusive with `--verdict`. | Mode |
| `--verdict` | The author's decision: `accepted` or `revised`. Mutually exclusive with `--inspect`. | Mode |
| `--store` | Registry name of the event store, or `@default` for the `default_kb`. Needed to record. | Yes |
| `--type` | Work type. Falls back to the change summary's frontmatter. | No |
| `--scope` | Package or surface scope. Falls back to the change summary's frontmatter. | No |
| `--ticket` | Ticket id. Falls back to the change summary's frontmatter. | No |
| `--merged-lede-file` | File holding the merged lede, for a pull request that wrote no merge artifact. | No |
| `--agent-lede-file` | File holding the agent's lede, for a pull request that wrote no pull-request artifact. | No |
| `--harness` | The agent platform (`claude`, `rovodev`); install-injected — keep as-is. | Injected |

Exactly one of `--inspect` and `--verdict` must appear. The author's comment is read from stdin to EOF; an empty comment is allowed and records no comment section.

## Runtime dependencies

- **`node` ≥ 24** — the bundled helper inherits the Node version floor of `@codeassembly/kb`.

## Process

### 1. Inspect the episode

```bash
node {harness_home_dir}/skills/capture-lede-decision/capture-lede-decision.mjs \
--inspect \
--artifact-dir <ticket artifact directory> \
--pr <number> \
--merge-commit <sha> \
[--type <key>] [--scope <name>] [--ticket <id>]
```

The helper prints a JSON object to stdout: `ok: true` with `episode` on success, or `ok: false` with `error` and `message`. Inspecting writes nothing and needs no store, so it can never block or alter a merge that already happened.

On `ok: false`, report the `message` on one line and stop. The merge has already succeeded — do not present this as a merge failure, and do not retry.

### 2. Present the pair and ask

Read `episode.differ`. Present the ledes and ask, following [option format](#option-format):

When `differ` is `true`, show the agent's lede and the merged lede, then ask:

1. ■■□ Record it as a revision (add a comment to explain what was wrong, if you want)
2. ■□□ Skip — this was a content change, or not a decision worth recording

When `differ` is `false`, show the single lede and ask:

1. ■■□ Record it as accepted — you read it and shipped it as written
2. ■□□ Skip — you did not evaluate it

Ask once. A skip is a complete answer, not a prompt to re-ask or to persuade: the corpus is better off one record smaller than holding a decision the author did not make.

### 3. Record the decision

On a skip, write nothing and say nothing further.

On a decision, pipe the author's comment (empty when they gave none) to the helper:

```bash
cat <<'EOF' | node {harness_home_dir}/skills/capture-lede-decision/capture-lede-decision.mjs \
--verdict <accepted|revised> \
--store <name|@default> \
--harness {harness_id} \
--artifact-dir <ticket artifact directory> \
--pr <number> \
--merge-commit <sha> \
[--type <key>] [--scope <name>] [--ticket <id>]
<the author's comment, verbatim; may be empty and may span multiple lines>
EOF
```

Relay the comment verbatim. It is free text on purpose: naming which doctrine rule the fix invoked is the refinement pass's job, and a rule list offered at capture time would presuppose which rules matter, which is the question the corpus exists to answer.

Report the written `path` on success.

### Recording a pull request merged outside the merge flow

Such a pull request wrote no merge artifact, so the merged lede has to be supplied. A lede file is read whole and recorded as the lede, with none of the heading extraction the artifact path applies: a file holding the entire pull-request body records the entire body as the lede. Extract the `## What` section as the file is written:

```bash
gh pr view <number> --json body --jq '.body' \
| awk '{ sub(/\r$/, "") }
tolower($0) ~ /^## what[[:space:]]*$/ { capturing = 1; next }
/^## / { capturing = 0 }
capturing' \
> "$TMPDIR/merged-lede.md"
```

Pass that file to `--merged-lede-file` and continue from step 2. Everything else resolves from the ticket's artifacts as usual.

## The record

One event per decision, in the named store:

- **Tags** — `lede-decision`, `type:{work type}`, and the verdict. Recall the corpus as a group with `kb-retrieve-events --tag lede-decision`, and by work type with `--tag type:feat`.
- **Frontmatter** — the work type, tier, and scope; the pull-request number, merge commit, and ticket; `doctrine-hash`, a digest of the lede doctrine in force when the agent wrote; and `agents-version` when the install manifest supplies one.
- **Body** — `## Agent lede`, then `## Merged lede` whenever the two texts differ, then `## Comment` when one was given.

`doctrine-hash` is what groups records by doctrine generation. Nothing is recorded at install time to make that work: the mapping from a digest back to the commit that introduced it stays recoverable by re-hashing the doctrine file's own history.

## Handling failures

Route by the `error` code:

- `no-artifact-dir`, `no-agent-lede`, `no-merged-lede` — the ticket's artifacts do not carry both ledes. Report and stop; supply `--agent-lede-file` or `--merged-lede-file` only when the text is genuinely in hand.
- `no-doctrine` — the installed doctrine file is unreadable. Report it as an install problem.
- `unresolved-identity` — the work type, tier, or scope could not be resolved. The message names which; pass the corresponding flag.
- `invalid-args` — surface the message and propose a corrected invocation.
- `missing-store`, `store-not-registered`, `readonly-store`, `no-default-store` — the destination could not be resolved; the message lists the registered stores.
- `schema-validation` — surface the `errors`.

## Completion

Either one written record at the reported path, or nothing at all. There is no third outcome, and no record is ever written without the author's decision.

<!-- include: ../_partials/option-format.md / -->
22 changes: 21 additions & 1 deletion packages/agents/content/skills/merge-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,27 @@ Pass the following inputs to the selected delegate per the delegate interface:

The orchestrator never passes ambiguous-status dimensions or `prompt` sentinels to the delegate — all values are concrete by this point.

After the delegate returns, emit `skill.completed` (payload `{"outcome":"merged"}`, or `{"outcome":"stopped: <reason>"}` when the delegate stopped or failed) per [Lifecycle events](#lifecycle-events).
If the delegate stopped or failed, emit `skill.completed` (payload `{"outcome":"stopped: <reason>"}`) per [Lifecycle events](#lifecycle-events) and stop. Otherwise capture the merge commit SHA from the delegate's completion report, if it carries one, and continue.

### 10. Record the lede decision

Skip this step when the delegate's completion report carries no merge commit SHA: nothing merged, so there is no shipped lede to decide about. The Bitbucket delegate is the standing case, since it prints the resolved values and exits successfully without merging. Emit `skill.completed` (payload `{"outcome":"not merged"}`) per [Lifecycle events](#lifecycle-events) and stop.

Otherwise the merge has already happened, so this step can only add a record. Declining costs a data point and nothing else, and nothing here can undo or re-run the merge — never present a failure at this step as a merge failure.

Invoke `{skill:capture-lede-decision}` with:

| Input | Value |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `--artifact-dir` | `{artifact_base_dir}/projects/{project_slug}/tickets/{ticket_id}/` |
| `--pr` | Resolved PR number |
| `--merge-commit` | The merge commit SHA from the delegate's completion report |
| `--type`, `--scope` | The values resolved in step 3, as settled at the approval gate |
| `--store` | `codeassembly` — the project's agent-guidance KB. Pass a different store only when the user directs the record elsewhere. |

That skill owns the prompt and the record: it asks once, writes one event on a decision, and writes nothing on a skip. Do not ask again, and do not infer a verdict from whether the ledes differ — a lede that shipped unchanged under time pressure is not an accepted lede.

Then emit `skill.completed` (payload `{"outcome":"merged"}`) per [Lifecycle events](#lifecycle-events).

## Important

Expand Down
4 changes: 4 additions & 0 deletions packages/agents/scripts/bundle-skill-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export const targets: BundleTarget[] = [
entry: 'src/capture-event/cli.ts',
outFile: 'content/skills/capture-event/capture-event.mjs',
},
{
entry: 'src/capture-lede-decision/cli.ts',
outFile: 'content/skills/capture-lede-decision/capture-lede-decision.mjs',
},
{
entry: 'src/kb-update-events/cli.ts',
outFile: 'content/skills/kb-update-events/kb-update-events.mjs',
Expand Down
24 changes: 1 addition & 23 deletions packages/agents/src/capture-event/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '@codeassembly/kb/records';
import { ulid } from 'ulid';

import { formatMissingStoreMessage } from '../kb-shared/format-missing-store.ts';
import { formatUtcTimestamp, isSafeEventId } from '../kb-shared/note-helpers.ts';
import { resolveCaptureTarget } from '../kb-shared/resolve-capture-target.ts';
import { parseTagList } from '../kb-shared/tag-helpers.ts';
Expand Down Expand Up @@ -302,29 +303,6 @@ function amendRecord(existing: KbEvent, args: ParsedArgs, body: string): KbEvent
};
}

/**
* Builds the agent-facing error message for an omitted `--store`, naming the registered stores and, when configured,
* the registry default reachable as `--store @default`.
*/
function formatMissingStoreMessage(resolved: {
registeredStores: string[];
defaultName?: string;
registryError?: string;
}): string {
if (resolved.registryError !== undefined) {
return `--store is required, but the kb.yaml registry could not be loaded: ${resolved.registryError}`;
}
if (resolved.registeredStores.length === 0) {
return '--store is required, but no stores are registered in kb.yaml';
}
const stores = resolved.registeredStores.join(', ');
const defaultHint =
resolved.defaultName !== undefined
? `the registry default is "${resolved.defaultName}", reachable as --store @default`
: 'no default_kb is configured';
return `--store is required. Registered stores: ${stores}. Pass --store <name> to choose one; ${defaultHint}.`;
}

/**
* Returns true when this module is the process entry point. Both sides are resolved through `realpathSync`, so a
* symlinked invocation path still matches. On a `realpathSync` failure the function emits a warning and returns
Expand Down
Loading
Loading