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
23 changes: 13 additions & 10 deletions packages/agents/content/skills/capture-event/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,23 @@ A **skill-caused mistake** — an error a clearer skill definition would have pr

## Arguments

| Argument | Description | Required |
| ----------- | ------------------------------------------------------------------------ | -------- |
| `--summary` | A human-readable one-line summary; becomes the record's label on recall. | Yes |
| `--store` | Registry name of the event store, or `@default` for the `default_kb`. | Yes |
| `--skill` | The skill the event relates to. | No |
| `--model` | The model identifier in play. | No |
| `--harness` | The agent platform (`claude`, `rovodev`); install-injected — keep as-is. | Injected |
| `--tags` | Comma-separated tag list. | No |
| Argument | Description | Required |
| ----------- | --------------------------------------------------------------------------------- | -------- |
| `--summary` | A human-readable one-line summary; becomes the record's label on recall. | Yes |
| `--store` | Registry name of the event store, or `@default` for the `default_kb`. | Yes |
| `--skill` | The skill the event relates to. | No |
| `--model` | The model identifier in play. | No |
| `--harness` | The agent platform (`claude`, `rovodev`); install-injected — keep as-is. | Injected |
| `--tags` | Comma-separated tag list. | No |
| `--impact` | Impact rating: one of `low`, `medium`, `high`, `critical`. Omit to leave unrated. | No |

A value-bearing flag accepts both `--summary text` and `--summary=text`. The event body is read from stdin to EOF; an empty body is allowed.

### Auto-filled vs agent-supplied

- **Auto-filled by the helper:** `recordType` (`event`), `id` (ULID), `captured-at`, `session` (`CLAUDE_CODE_SESSION_ID`), `cwd`, and `repo` (the `owner/name` git remote at `cwd`, best-effort — omitted silently when no remote resolves).
- **Template-injected:** `harness` — `codeassembly-agents` writes the agent platform (`claude` or `rovodev`) into the `--harness` flag when it installs this skill. Unlike `model`, which varies per session and is self-reported, the harness is fixed at install time; keep the injected `--harness` flag verbatim rather than filling in a value yourself.
- **Agent-supplied:** `summary`, the optional `skill`/`model`/`tags`, and the body.
- **Agent-supplied:** `summary`, the optional `skill`/`model`/`tags`/`impact`, and the body.

### Store selection

Expand All @@ -55,6 +56,8 @@ Choose the destination deliberately. When the lesson is specific to a project, p

Write a one-line `--summary` that reads well on its own (it is the record's recall label). Put the detail — context, the problem and its resolution, the pattern and its refinement — in the body on stdin. Capture enough context that the event is intelligible months later without the surrounding conversation.

Optionally rate `--impact` (`low`, `medium`, `high`, or `critical`): your subjective read of how much addressing the event matters, which applies equally to a bug and to a beneficial change. Omit it when you have no clear read — an unrated event is left for a later triage pass. The rating is revisable later with `kb-update-events`.

### 2. Invoke the helper

Pipe the body to the bundled helper. A heredoc keeps multi-line bodies legible:
Expand All @@ -64,7 +67,7 @@ cat <<'EOF' | node {harness_home_dir}/skills/capture-event/capture-event.mjs \
--summary "<one-line summary>" \
--store <name|@default> \
--harness {harness_id} \
[--skill <skill>] [--model <model>] [--tags <comma,separated>]
[--skill <skill>] [--model <model>] [--tags <comma,separated>] [--impact <level>]
<event body, may span multiple lines and contain any characters>
EOF
```
Expand Down
1 change: 1 addition & 0 deletions packages/agents/content/skills/capture-feedback/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Invoke the `capture-event` skill to append an immutable record, composing its ar
- `--store codeassembly` — the project's agent-guidance KB. Pass a different `--store` only when the user directs the record elsewhere.
- `--tags feedback` — always. Add `,mistake` when existing guidance was misapplied (step 1, "Yes").
- `--skill <slug>` — when the refinement target is a skill.
- `--impact <level>` — optionally rate how much addressing this feedback would improve the agent's future behavior: `low`, `medium`, `high`, or `critical`. Omit it when you have no clear read; the rating is revisable later with `kb-update-events`.
- `--summary` — a one-line recall label, for example "Agent title-cased a heading; sentence case is the rule."
- **Body** — the generalized lesson, only to the extent needed to act on it later:
- The **error→correction pair** (misapplied-guidance mode) or the **desired behavior** (no-guidance mode), generalized — not the raw artifact or diff.
Expand Down
25 changes: 13 additions & 12 deletions packages/agents/content/skills/kb-update-events/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
---
name: kb-update-events
description: Edit existing events in the knowledge store — mark one or more events addressed-by a reference, or retag them — in a single batch invocation. The event mutable set only; events stay otherwise write-once.
description: Edit existing events in the knowledge store — mark one or more events addressed-by a reference, retag them, or set their impact — in a single batch invocation. The event mutable set only; events stay otherwise write-once.
user-invocable: true
---

# Update existing events

Apply one mutation to one or more existing event records in a single invocation. A bundled helper does the mechanical work — it resolves the event store by name, resolves each id to its record, reads it through the type-blind note I/O layer, parses it to a typed `KbEvent`, applies the operation, and writes it back atomically. You supply the store, the operation, and the event ids.

The operation surface is the **event mutable set** only: `addressed-by` (mark an event as addressed by a reference) and `tags` (retag). Events are otherwise write-once: there is no body edit, and marking stamps no timestamp — `addressed-by` and `tags` are append-only/curatorial annotations, not substantive edits. For new events, use `capture-event`. For editing assertions, use `kb-edit`.
The operation surface is the **event mutable set** only: `addressed-by` (mark an event as addressed by a reference), `tags` (retag), and `impact` (set the impact rating). Events are otherwise write-once: there is no body edit, and these mutations stamp no timestamp — they are curatorial annotations, not substantive edits. Impact is a subjective assessment that may legitimately change, which is why it lives in the mutable set rather than the immutable spine. For new events, use `capture-event`. For editing assertions, use `kb-edit`.

**Announce at start:** "Using kb-update-events to {mark|retag} {N} event(s)."
**Announce at start:** "Using kb-update-events to {mark|retag|rate} {N} event(s)."

## Arguments

| Argument | Description | Required |
| --------------------------- | ------------------------------------------------------------------------- | -------- |
| `--store` | Registry name of the event store, or `@default` for the `default_kb`. | Yes |
| `--add-addressed-by <refs>` | Append comma-separated reference(s) to each event's `addressed-by` list. | One op |
| `--retag <list>` | Replace each event's `tags` with the comma-separated list. Canonicalizes. | One op |
| `<event-id>` | One or more event ids; each resolves to `{store}/content/events/{id}.md`. | Yes |
| Argument | Description | Required |
| --------------------------- | -------------------------------------------------------------------------------------------------- | -------- |
| `--store` | Registry name of the event store, or `@default` for the `default_kb`. | Yes |
| `--add-addressed-by <refs>` | Append comma-separated reference(s) to each event's `addressed-by` list. | One op |
| `--retag <list>` | Replace each event's `tags` with the comma-separated list. Canonicalizes. | One op |
| `--set-impact <level>` | Set each event's `impact` to one of `low`, `medium`, `high`, `critical`. Replaces any prior value. | One op |
| `<event-id>` | One or more event ids; each resolves to `{store}/content/events/{id}.md`. | Yes |

A value-bearing flag accepts both `--retag fix,observation` and `--retag=fix,observation`. Exactly one operation flag is required per invocation; combining the two is rejected with `invalid-args`. References are free-form (a KB wikilink or relative path, a commit SHA, a PR/issue ref, or a URL); they are stored verbatim and de-duplicated after any existing entries. A reference that begins with `--` is otherwise read as the next flag, so pass it with the inline `--add-addressed-by=<ref>` form.
A value-bearing flag accepts both `--retag fix,observation` and `--retag=fix,observation`. Exactly one operation flag is required per invocation; combining more than one is rejected with `invalid-args`. References are free-form (a KB wikilink or relative path, a commit SHA, a PR/issue ref, or a URL); they are stored verbatim and de-duplicated after any existing entries. A reference that begins with `--` is otherwise read as the next flag, so pass it with the inline `--add-addressed-by=<ref>` form.

### Store selection

Expand All @@ -46,7 +47,7 @@ node {harness_home_dir}/skills/kb-update-events/kb-update-events.mjs \
<event-id> [<event-id> ...]
```

Use `--retag <tag[,tag...]>` in place of `--add-addressed-by` to retag instead.
Use `--retag <tag[,tag...]>` or `--set-impact <level>` in place of `--add-addressed-by` for those operations.

The helper prints a JSON object to stdout:

Expand All @@ -72,4 +73,4 @@ On `ok: false`, route by the `error` code:

## Completion

Each named event updated in place and re-validated, written atomically. A mixed batch is partial by design: succeeded events are written; failed ids are reported and left untouched. Events remain write-once apart from these `addressed-by`/`tags` annotations.
Each named event updated in place and re-validated, written atomically. A mixed batch is partial by design: succeeded events are written; failed ids are reported and left untouched. Events remain write-once apart from these `addressed-by`/`tags`/`impact` annotations.
30 changes: 29 additions & 1 deletion packages/agents/src/capture-event/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const EVENT_SCHEMA = `recordTypes:
event:
recall: recurrence-recency
required: [id, captured-at, session, cwd, summary]
optional: [repo, skill, model, harness, tags, correction, owner, locality, severity]
optional: [repo, skill, model, harness, tags, correction, owner, locality, severity, impact]
`;

function bodyStream(body: string): Readable {
Expand Down Expand Up @@ -70,6 +70,8 @@ describe(parseArgs, () => {
'claude',
'--tags',
'one, two,three',
'--impact',
'high',
]);

expect(parsed).toEqual({
Expand All @@ -79,6 +81,7 @@ describe(parseArgs, () => {
model: 'claude-opus-4-8',
harness: 'claude',
tags: ['one', 'two', 'three'],
impact: 'high',
});
});

Expand All @@ -90,6 +93,11 @@ describe(parseArgs, () => {
expect(parsed.model).toBeNull();
expect(parsed.harness).toBeNull();
expect(parsed.tags).toEqual([]);
expect(parsed.impact).toBeNull();
});

it('throws on an out-of-enum --impact', () => {
expect(() => parseArgs(['--summary', 'x', '--impact', 'urgent'])).toThrow(/--impact must be one of/);
});

it('throws when --summary is missing', () => {
Expand Down Expand Up @@ -182,6 +190,26 @@ describe(runCapture, () => {
}
});

it('writes the impact field when --impact is supplied', async () => {
const { home } = await makeStore('codeassembly');
const repo = await makeRepoWithRemote('git@github.com:williamthorsen/codeassembly.git');

const result = await runCapture({
argv: ['--store', '@default', '--summary', 'Noticed a thing', '--impact', 'high'],
stdin: bodyStream('Body text.'),
cwd: repo,
env: { CLAUDE_CODE_SESSION_ID: 'session-xyz' },
now: NOW,
home,
});

expect(result.ok).toBe(true);
if (result.ok) {
const written = await readFile(result.path, 'utf8');
expect(written).toMatch(/^impact: high$/m);
}
});

it('writes the harness field when --harness is supplied', async () => {
const { home } = await makeStore('codeassembly');
const repo = await makeRepoWithRemote('git@github.com:williamthorsen/codeassembly.git');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const EVENT_SCHEMA = `recordTypes:
event:
recall: recurrence-recency
required: [id, captured-at, session, cwd, summary]
optional: [repo, skill, model, harness, tags, correction, owner, locality, severity]
optional: [repo, skill, model, harness, tags, correction, owner, locality, severity, impact]
`;

const ID = '01HZZZZZZZZZZZZZZZZZZZZZZZZ';
Expand All @@ -29,6 +29,7 @@ function argsFor(overrides: Partial<ParsedArgs>): ParsedArgs {
model: null,
harness: null,
tags: [],
impact: null,
...overrides,
};
}
Expand Down Expand Up @@ -145,4 +146,36 @@ describe(prepareEvent, () => {
expect(result.prepared.content).toContain('tags: [recall, kb]');
}
});

it('renders the supplied impact into the record', () => {
const result = prepareEvent({
args: argsFor({ impact: 'high' }),
context: CONTEXT,
id: ID,
capturedAt: CAPTURED_AT,
schema,
body: '',
});

expect(result.ok).toBe(true);
if (result.ok) {
expect(result.prepared.content).toMatch(/^impact: high$/m);
}
});

it('omits impact when none is supplied', () => {
const result = prepareEvent({
args: argsFor({}),
context: CONTEXT,
id: ID,
capturedAt: CAPTURED_AT,
schema,
body: '',
});

expect(result.ok).toBe(true);
if (result.ok) {
expect(result.prepared.content).not.toMatch(/^impact:/m);
}
});
});
16 changes: 14 additions & 2 deletions packages/agents/src/capture-event/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';

import type { KbRoot } from '@codeassembly/kb';
import { EVENT_IMPACT_LEVELS, type EventImpact, isEventImpact } from '@codeassembly/kb/records';
import { loadSchema } from '@codeassembly/kb/schema';
import { ulid } from 'ulid';

Expand All @@ -32,6 +33,7 @@ const FLAGS: readonly FlagSpec[] = [
{ name: 'model', takesValue: true },
{ name: 'harness', takesValue: true },
{ name: 'tags', takesValue: true },
{ name: 'impact', takesValue: true },
];

/** Executes the helper from `process.argv` and writes the JSON result to stdout. */
Expand Down Expand Up @@ -156,8 +158,9 @@ export async function runCapture(input: {

/**
* Parses the helper's argv. Each value-bearing flag accepts both `--flag value` and `--flag=value`; `--tags` accepts a
* comma-separated list. Unknown flags, an unexpected positional, an empty value for any flag, or a missing `--summary`
* throw with a usage-style message. The body comes from stdin rather than the command line, so the layout is flag-only.
* comma-separated list and `--impact` accepts one declared impact level. Unknown flags, an unexpected positional, an
* empty value for any flag, a missing `--summary`, or an out-of-enum `--impact` throw with a usage-style message. The
* body comes from stdin rather than the command line, so the layout is flag-only.
*
* @internal - Exported to allow testing.
*/
Expand All @@ -178,13 +181,22 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
throw new Error('--summary is required');
}

let impact: EventImpact | null = null;
if (raw.impact !== undefined) {
if (!isEventImpact(raw.impact)) {
throw new Error(`--impact must be one of ${EVENT_IMPACT_LEVELS.join(', ')}`);
}
impact = raw.impact;
}

return {
store: raw.store ?? null,
summary,
skill: raw.skill ?? null,
model: raw.model ?? null,
harness: raw.harness ?? null,
tags: raw.tags === undefined ? [] : parseTagList(raw.tags),
impact,
};
}

Expand Down
6 changes: 5 additions & 1 deletion packages/agents/src/capture-event/prepare-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export type PrepareOutcome = PrepareSuccess | PrepareFailure;
* Assembles an immutable event record from agent-supplied args and auto-filled context, renders it to a note string,
* and validates the result against the store's schema via `frontmatterRule`. The record carries the stored
* `recordType: event` discriminant and the event spine (`id`, `captured-at`, `session`, `cwd`, `repo`, `summary`) plus
* any supplied `skill`/`model`/`harness`/`tags`. No `updated`/`last-verified` field is written: events are write-once.
* any supplied `skill`/`model`/`harness`/`tags`/`impact`. No `updated`/`last-verified` field is written: events are
* write-once.
*
* Validation round-trips the rendered note through `parseNoteContent` and `runRules`, mirroring `kb-add`'s
* prepare-then-validate flow. When any finding has `severity: 'error'`, the outcome is `{ ok: false, findings }` and
Expand Down Expand Up @@ -73,6 +74,9 @@ export function prepareEvent(input: {
if (args.tags.length > 0) {
fields.push(['tags', args.tags]);
}
if (args.impact !== null) {
fields.push(['impact', args.impact]);
}

const content = renderEventNote(fields, body);

Expand Down
3 changes: 3 additions & 0 deletions packages/agents/src/capture-event/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// (out-of-disk, permission denied) are out of band: they print to stderr and exit non-zero.

import type { Finding } from '@codeassembly/kb';
import type { EventImpact } from '@codeassembly/kb/records';

/** Parsed command-line invocation of the capture-event helper. */
export interface ParsedArgs {
Expand All @@ -24,6 +25,8 @@ export interface ParsedArgs {
harness: string | null;
/** Optional tag list, in the order the agent supplied them. */
tags: string[];
/** Optional impact rating; `null` when `--impact` is omitted. */
impact: EventImpact | null;
}

/** The auto-filled context an event carries beyond the agent-supplied fields. */
Expand Down
Loading
Loading