diff --git a/Cargo.lock b/Cargo.lock index 0390154..7a1bc4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1232,7 +1232,7 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pk-cherry" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "chrono", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "pk-cli" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "clap", @@ -1272,7 +1272,7 @@ dependencies = [ [[package]] name = "pk-core" -version = "1.9.0" +version = "1.10.0" dependencies = [ "chrono", "dirs", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "pk-event-store" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "chrono", @@ -1301,7 +1301,7 @@ dependencies = [ [[package]] name = "pk-learning-worker" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "axum", @@ -1322,7 +1322,7 @@ dependencies = [ [[package]] name = "pk-librarian" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "chrono", @@ -1338,7 +1338,7 @@ dependencies = [ [[package]] name = "pk-mcp" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "axum", @@ -1359,7 +1359,7 @@ dependencies = [ [[package]] name = "pk-store" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "chrono", @@ -1378,7 +1378,7 @@ dependencies = [ [[package]] name = "pk-uar" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "async-trait", @@ -1395,7 +1395,7 @@ dependencies = [ [[package]] name = "pk-watcher" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 43a9f4e..0f692d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.9.0" +version = "1.10.0" edition = "2021" authors = ["Travis James "] license = "MIT" @@ -73,9 +73,9 @@ dirs = "5" fs2 = "0.4" # Internal crates -pk-core = { version = "1.9.0", path = "pk-core" } -pk-store = { version = "1.9.0", path = "pk-store" } -pk-watcher = { version = "1.9.0", path = "pk-watcher" } -pk-librarian = { version = "1.9.0", path = "pk-librarian" } -pk-mcp = { version = "1.9.0", path = "pk-mcp" } -pk-event-store = { version = "1.9.0", path = "pk-event-store" } +pk-core = { version = "1.10.0", path = "pk-core" } +pk-store = { version = "1.10.0", path = "pk-store" } +pk-watcher = { version = "1.10.0", path = "pk-watcher" } +pk-librarian = { version = "1.10.0", path = "pk-librarian" } +pk-mcp = { version = "1.10.0", path = "pk-mcp" } +pk-event-store = { version = "1.10.0", path = "pk-event-store" } diff --git a/openspec/changes/team-aware-recall-surfaces/design.md b/openspec/changes/team-aware-recall-surfaces/design.md new file mode 100644 index 0000000..29294ca --- /dev/null +++ b/openspec/changes/team-aware-recall-surfaces/design.md @@ -0,0 +1,15 @@ +## Release gate: the 1.9.0 `WikiEntry.sources` break + +The maintainers' semver rule forbids cutting a tag while a public-API break in pk-core, pk-store or pk-librarian is unresolved. It names the 1.9.0 `WikiEntry.sources` change (`Vec` → `Vec`) as the open case. + +What the code provides today, checked at `2c66c3e`: + +- **Builder path.** `WikiEntry::with_sources(impl IntoIterator>)`, plus `From for Source` and `From<&str> for Source`. Callers passing strings compile unchanged. +- **Wire and file path.** `Source`'s `Deserialize` accepts a bare string, a mapping with `resource`, and numbers or booleans read as text. Both v0.1 and v0.2 files load. +- **Remaining source break:** code that reads the field directly and expects `String`, for example `entry.sources[0].as_str()`. No consumer does this. prometheus-cli and forge-rs depend on pk-core, pk-store and pk-librarian, and `grep -rn '\.sources\|with_sources\|Source::'` over their sources finds nothing. The okf-v02-writer proposal already recorded that forge-rs uses no pk types. + +**Conclusion offered to the maintainers:** the break is mitigated by compatibility shims for every known consumer. Only direct field readers outside the estate would be affected. Whether that counts as "resolved" for the tag rule is the maintainers' decision. This change records the evidence; it does not make the decision. + +## Why `candidate_count` changed meaning instead of adding a new key + +The old meaning, "entries inspected (capped)", described the defect itself: a cap applied before scoring. After the fix, nothing is inspected and then dropped unscored, so the old meaning has no referent. `scored_count` reports what was inspected, and `candidate_count` keeps its name for what the cap bounds. diff --git a/openspec/changes/team-aware-recall-surfaces/proposal.md b/openspec/changes/team-aware-recall-surfaces/proposal.md new file mode 100644 index 0000000..d8223cf --- /dev/null +++ b/openspec/changes/team-aware-recall-surfaces/proposal.md @@ -0,0 +1,51 @@ +## Why + +The skill system's team-aware learning work needs recall to target a single agent role. +Three gaps prevented that: + +- **`pk context` never saw most of a large knowledge base.** It took `ceil(max_candidates / scopes)` entries per scope in snapshot order *before* scoring them. With 200 entries and default flags, a late-sorting match was never returned. +- **The learning worker wrote unattributed records and nothing recallable.** It always sent `agent_id: null`, and it never committed the prompt snapshot that `pk context` reads. +- **pk could not classify or filter entries by role, team or visibility.** + +This change records three surface changes that already merged: #31, #32 and #34. This repository requires an OpenSpec change naming the consumers for any change to a frozen surface, and those PRs were merged without one. This proposal is that record. + +## What Changes + +- **`pk context --format json` (frozen surface).** + - Every snapshot entry of every readable scope is scored. `--max-candidates` now caps the merged, ranked list. + - Ranking is deterministic: score descending, then scope priority, then id. + - New key `scored_count`. + - **Changed meaning:** `candidate_count` was "entries inspected (capped)". It is now "ranked candidates kept after the cap". + - `--format hook` output is unchanged. + - The contract test `pk-cli/tests/context.rs::candidate_budget_is_shared_across_requested_scopes` asserted the old inspected count. It now asserts `scored_count = 5` and `candidate_count = 1`. That is a semantic update, not a weakening: the budget is still enforced, now on the ranked list. +- **`pk context --tag ` (new flag, additive).** Repeatable, all-of, applied before scoring. +- **`pk ingest --type ` and `--tag ` (new flags, additive).** + - `--type` sets the OKF entry type. `Reference` remains the default. + - `--tag` adds tags, with caller tags first and duplicates removed. + - The rust API gains `Librarian::compile_with(raw, CompileOverrides)`. `compile()` delegates to it. +- **Learning job schema.** + - Optional `projectId`, `teamId` and `roleId`, all `serde(default)`. The job stays at schemaVersion 2, and older jobs still parse. + - The memory scope keys are never null: `user_id` = project, `agent_id` = `/`, else `@project`. Shared scope uses `@global`. + - The worker commits the prompt snapshot after each upsert. + - Hash-bound queued payloads are not rewritten. Only the legacy `add_task_step` form gains keys. + +## Consumers + +| Consumer | Affected surface | Effect | +|---|---|---| +| prometheus-skill-system (hooks, recall, memory bridge) | `pk context --format json`, `pk ingest`, learning job fields | Intended consumer: it reads `results` and now passes `--tag`/`--type` and the job identity fields. It reads no `candidate_count`. | +| prometheus-skills-mini (`lib/karpathy/transport.mjs`) | `pk ingest` | Additive flags only. Existing invocations are unchanged. | +| forge-rs | `pk ingest` (shells out) | Additive flags only. Existing invocations are unchanged. | +| prometheus-cli | pk-core / pk-store / pk-librarian crates | `compile_with` is additive. No type changed. | +| prometheus doctor | `status.json` | Unchanged. | + +## Capabilities + +### New Capabilities +- `team-aware-recall`: context scoring and budget, tag filtering, ingest classification, and attributed learning jobs. + +## Impact + +- **Code:** `pk-cli/src/main.rs`, `pk-librarian/src/librarian.rs` + `lib.rs`, `pk-learning-worker/src/main.rs`. +- **Tests:** `pk-cli/tests/context_scoring.rs`, `pk-cli/tests/tags_and_type.rs`, `pk-learning-worker/tests/attribution.rs`, and the updated assertion in `pk-cli/tests/context.rs`. +- **Release:** the workspace version goes from 1.9.0 to **1.10.0**, a minor bump. All the changes are additive except the `candidate_count` meaning, which no consumer reads. diff --git a/openspec/changes/team-aware-recall-surfaces/specs/team-aware-recall/spec.md b/openspec/changes/team-aware-recall-surfaces/specs/team-aware-recall/spec.md new file mode 100644 index 0000000..2fd9807 --- /dev/null +++ b/openspec/changes/team-aware-recall-surfaces/specs/team-aware-recall/spec.md @@ -0,0 +1,47 @@ +## ADDED Requirements + +### Requirement: Context scores every entry before applying the candidate cap +`pk context` SHALL score every entry of every readable scope, and SHALL apply `--max-candidates` only to the merged, de-duplicated, ranked list. A scope that fails SHALL NOT reserve any share of the cap. + +#### Scenario: A late-sorting entry in a large scope is recalled +- **WHEN** a project scope holds 200 committed entries and the only match sorts last by id +- **THEN** `pk context --format json`, with default flags, returns that entry, and `scored_count` is 200 + +#### Scenario: A failed scope does not reserve budget +- **WHEN** the project scope fails and the shared scope holds 60 matching entries, with `--max-candidates 60` +- **THEN** `candidate_count` is 60 + +### Requirement: Context output is deterministic +Results SHALL be ordered by score descending, then scope priority (project, shared, global), then entry id, and repeated runs over one snapshot SHALL produce byte-identical JSON. + +#### Scenario: Two runs are identical +- **WHEN** the same `pk context` command runs twice against unchanged snapshots +- **THEN** stdout is byte-identical + +### Requirement: Context filters by tags +`pk context --tag `, repeated, SHALL keep only entries that carry every given tag. The filter SHALL run before scoring. + +#### Scenario: All-of tag filter +- **WHEN** entries carry `role:api-dev`, and `role:ui-dev` together with `vis:team` +- **THEN** `--tag role:api-dev` returns only the first, `--tag role:ui-dev --tag vis:team` returns only the second, and `--tag role:api-dev --tag vis:team` returns nothing + +### Requirement: Ingest classifies entries +`pk ingest --type ` SHALL set the entry type, with `Reference` as the default when it is absent. `--tag `, repeated, SHALL add tags ahead of the model's tags, without duplicates. + +#### Scenario: Typed and tagged ingest +- **WHEN** a document is ingested with `--type Lesson --tag role:api-dev` +- **THEN** its frontmatter has `type: Lesson`, and its tags include `role:api-dev` and the model's tags + +### Requirement: Learning jobs produce attributed memory scope keys +The worker SHALL derive `user_id` from the job's `projectId`, falling back to the project scope. It SHALL derive `agent_id` as `/` when both are present, otherwise `@project`. Shared scope SHALL use `@global` for both. No queued memory operation the worker creates SHALL have a null `user_id` or `agent_id`. + +#### Scenario: Attributed job +- **WHEN** a job carries projectId `project:x`, teamId `t` and roleId `r` +- **THEN** its memory operation has `user_id = project:x` and `agent_id = t/r` + +### Requirement: The worker publishes recallable snapshots +After a successful upsert, the worker SHALL commit the prompt snapshot for the target knowledge base. + +#### Scenario: A session record is recallable +- **WHEN** the worker processes a job +- **THEN** the committed project prompt snapshot contains the job's session entry diff --git a/openspec/changes/team-aware-recall-surfaces/tasks.md b/openspec/changes/team-aware-recall-surfaces/tasks.md new file mode 100644 index 0000000..a3c2945 --- /dev/null +++ b/openspec/changes/team-aware-recall-surfaces/tasks.md @@ -0,0 +1,20 @@ +Records work that already merged. The evidence is each PR's integration test. + +## 1. Context scoring (#31) +- [x] 1.1 Score all entries and cap after ranking (`pk-cli/src/main.rs::run_context`). +- [x] 1.2 `pk-cli/tests/context_scoring.rs`: 3/3. Negative control: the pre-fix binary returned `results=[]` and `candidate_count=128` on the 200-entry fixture. +- [x] 1.3 Update the `candidate_budget_is_shared_across_requested_scopes` assertion to the new semantics (design.md). + +## 2. Worker attribution and snapshot (#32) +- [x] 2.1 Add the job identity fields and non-null scope keys. The legacy `add_task_step` form gets keys too. +- [x] 2.2 Commit the prompt snapshot after upsert. +- [x] 2.3 `pk-learning-worker/tests/attribution.rs`: 2/2. `worker.rs`: 5/5. + +## 3. Ingest classification and context tags (#34) +- [x] 3.1 `--type` and `--tag` on ingest, via `CompileOverrides`. +- [x] 3.2 `--tag` on context. +- [x] 3.3 `pk-cli/tests/tags_and_type.rs`: 1/1, end to end against a local model endpoint. + +## 4. Release 1.10.0 +- [x] 4.1 Bump the workspace version to 1.10.0 and refresh `Cargo.lock` for the workspace members. +- [ ] 4.2 Maintainers confirm the `WikiEntry.sources` gate (design.md), then tag `v1.10.0` on the merge commit.