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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ packages/agents/content/skills/kb-add/kb-add.mjs
packages/agents/content/skills/kb-curate/kb-curate.mjs
packages/agents/content/skills/kb-edit/kb-edit.mjs
packages/agents/content/skills/kb-retrieve/kb-retrieve.mjs
packages/agents/content/skills/kb-update-events/kb-update-events.mjs
packages/agents/content/skills/update-jira-ticket/update-jira-ticket.mjs

# Credentials
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck vendor file
// Complete implementation of condition-based waiting utilities
// From: Lace test infrastructure improvements (2025-10-03)
// Context: Fixed 15 flaky tests by replacing arbitrary timeouts
Expand Down
75 changes: 75 additions & 0 deletions packages/agents/content/skills/kb-update-events/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
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.
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`.

**Announce at start:** "Using kb-update-events to {mark|retag} {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 |

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.

### Store selection

`--store` is required: every edit names its store. The helper resolves the store by registry name only and never walks the working directory for a `.kb/` folder. The store must be registered in `kb.yaml`. Pass `--store <name>` for a named store, or `--store @default` for the registry's `default_kb`. Omitting `--store` is refused with an error that lists the registered stores.

## Runtime dependencies

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

## Process

### 1. Gather the event ids

Collect the ids of the events to edit (typically from a prior recall). Each id is the event's ULID — the filename stem under `content/events/`.

### 2. Invoke the helper

```bash
node {harness_home_dir}/skills/kb-update-events/kb-update-events.mjs \
--store <name|@default> \
--add-addressed-by <ref[,ref...]> \
<event-id> [<event-id> ...]
```

Use `--retag <tag[,tag...]>` in place of `--add-addressed-by` to retag instead.

The helper prints a JSON object to stdout:

- `ok: true` with `operation`, `store`, and a `results` array — one entry per id, in order. Each entry is either `{ ok: true, id, path }` or `{ ok: false, id, error, message }`.
- `ok: false` with `error` and `message` on an invocation-level failure (nothing was written).

### 3. Handle the result

On `ok: true`, report the per-event outcomes. A per-event `error` is one of:

- `invalid-id` — the id is not a bare filename stem (contains a path separator). Correct the id.
- `not-found` — no event at the resolved path. Confirm the id and store.
- `parse` — the file is not a valid event record. Inspect it.
- `validation` — the rendered record failed re-validation (unexpected); surface the message.

On `ok: false`, route by the `error` code:

- `invalid-args` — surface the message and propose a corrected invocation.
- `missing-store` — `--store` was omitted; the message lists the registered stores.
- `store-not-registered` — the named store is not in `kb.yaml`.
- `readonly-store` — the store is marked readonly; edits are refused.
- `no-default-store` — `--store @default` was given but no `default_kb` is configured.

## 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.
11 changes: 1 addition & 10 deletions packages/agents/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,5 @@ import baseConfig from '../../eslint.config.js';
export default [
...baseConfig,
// Generated esbuild bundles and shipped harness content, not lintable source.
globalIgnores([
'content/skills/_harnesses/**',
'content/skills/capture-event/capture-event.mjs',
'content/skills/derive-session-context/derive-session-context.mjs',
'content/skills/kb-add/kb-add.mjs',
'content/skills/kb-curate/kb-curate.mjs',
'content/skills/kb-edit/kb-edit.mjs',
'content/skills/kb-retrieve/kb-retrieve.mjs',
'content/skills/update-jira-ticket/update-jira-ticket.mjs',
]),
globalIgnores(['content/skills/**/*.mjs', 'content/skills/**/*-example.ts']),
];
97 changes: 97 additions & 0 deletions packages/agents/scripts/bundle-skill-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ export const targets: BundleTarget[] = [
outFile: 'content/skills/capture-event/capture-event.mjs',
smokeTest: makeCaptureEventSmokeTest(),
},
{
entry: 'src/kb-update-events/cli.ts',
outFile: 'content/skills/kb-update-events/kb-update-events.mjs',
smokeTest: makeKbUpdateEventsSmokeTest(),
},
];

/**
Expand Down Expand Up @@ -169,6 +174,98 @@ function assertCaptureEventSmokeResult(result: unknown): void {
}
}

/**
* Stands up an event store carrying a seed event plus an isolated home registering it as `default_kb`, then returns a
* `SmokeTestInvocation` that marks the event `addressed-by` a reference with `--store @default`. Exercises the full
* `@default` resolution → read → parse → mutate → atomic write pipeline, the only path that wires the bundled resolver,
* the per-type record layer, and the note-io writer together. The assertion confirms the reference landed and that no
* `title`/`created`/`updated` was injected onto the event.
*/
function makeKbUpdateEventsSmokeTest(): SmokeTestInvocation {
const storePath = mkdtempSync(path.join(tmpdir(), 'kb-update-events-store-'));
mkdirSync(path.join(storePath, '.kb'), { recursive: true });
writeFileSync(
path.join(storePath, '.kb', 'schema.yaml'),
[
'recordTypes:',
' event:',
' recall: recurrence-recency',
' required: [id, captured-at, session, cwd, summary]',
' optional: [repo, skill, model, harness, tags, addressed-by]',
'',
].join('\n'),
'utf8',
);

const eventsDir = path.join(storePath, 'content', 'events');
mkdirSync(eventsDir, { recursive: true });
const eventPath = path.join(eventsDir, 'smoke-event.md');
writeFileSync(
eventPath,
[
'---',
'recordType: event',
'id: smoke-event',
'captured-at: 2026-06-18T09:41:02Z',
'session: smoke',
'cwd: /tmp/smoke',
'summary: Smoke event',
'---',
'',
'Body.',
'',
].join('\n'),
'utf8',
);

const home = mkdtempSync(path.join(tmpdir(), 'kb-update-events-home-'));
mkdirSync(path.join(home, '.agents'), { recursive: true });
writeFileSync(
path.join(home, '.agents', 'kb.yaml'),
`default_kb: codeassembly\nkbs:\n codeassembly:\n path: ${storePath}\n`,
'utf8',
);

return {
args: ['--store', '@default', '--add-addressed-by', '#849', 'smoke-event'],
env: { ...process.env, HOME: home },
assertResult: (result) => assertKbUpdateEventsSmokeResult(result, eventPath),
};
}

/**
* Assert the kb-update-events smoke produced an ok batch whose one event updated, with the reference written to its
* `addressed-by` list and no assertion fields injected.
*/
function assertKbUpdateEventsSmokeResult(result: unknown, eventPath: string): void {
if (!isRecord(result)) {
throw new TypeError('expected object result from kb-update-events');
}
if (result.ok !== true) {
throw new Error(`expected ok: true, got ${JSON.stringify(result)}`);
}
if (result.operation !== 'add-addressed-by') {
throw new Error(`expected operation 'add-addressed-by', got ${JSON.stringify(result.operation)}`);
}
if (!Array.isArray(result.results) || result.results.length !== 1) {
throw new Error(`expected one per-event result, got ${JSON.stringify(result.results)}`);
}
const entry: unknown = result.results[0];
if (!isRecord(entry) || entry.ok !== true) {
throw new Error(`expected the event to update, got ${JSON.stringify(entry)}`);
}
const written = readFileSync(eventPath, 'utf8');
if (!/^addressed-by:/m.test(written)) {
throw new Error(`expected the written event to carry addressed-by, got:\n${written}`);
}
if (!written.includes('#849')) {
throw new Error(`expected the written event to reference #849, got:\n${written}`);
}
if (/^(title|created|updated):/m.test(written)) {
throw new Error(`expected no assertion fields injected, got:\n${written}`);
}
}

/**
* Builds a fixture directory containing a minimal preferences file and returns a `SmokeTestInvocation`
* that drives the deriver against it with a known branch name. The deriver's output depends on the
Expand Down
Loading
Loading