create-pmos 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  **A product manager's operating system you can adopt in five minutes — no coding expertise
4
4
  required.** One command puts a disciplined, eval-driven PM workflow inside your own repo:
5
- initiative → PRD → eval rubric → agreed contract → build → three gates. Your AI coding agent
6
- (e.g. Claude Code) does the building. You do the judging. Everything stays in your folder — no
7
- database, no account, no network calls, no telemetry.
5
+ initiative → PRD → eval rubric → agreed contract → build → three gates. **Any capable coding
6
+ agent** does the building — Claude Code, Codex/ChatGPT, Cursor, Gemini, Copilot; PMOS is
7
+ provider- and model-agnostic. You do the judging. Everything stays in your folder — no database,
8
+ no account, no network calls, no telemetry.
8
9
 
9
10
  ## Whose task is what
10
11
 
@@ -43,7 +44,7 @@ What you'll see:
43
44
 
44
45
  Next steps in /home/you/my-product:
45
46
  1. cp pmos/templates/pmos-gate.yml .github/workflows/pmos-gate.yml (only if on GitHub)
46
- 2. Add to the repo's CLAUDE.md: "This product is PMOS-managed (file mode) — read pmos/AGENTS.md before any work."
47
+ 2. Wire PMOS into your agent's instruction file (CLAUDE.md, AGENTS.md, .cursorrules, GEMINI.md, or copilot-instructions): "This product is PMOS-managed (file mode) — read pmos/AGENTS.md before any work."
47
48
  3. Commit the pmos/ folder. Then follow pmos/AGENTS.md for every run.
48
49
  ```
49
50
 
@@ -249,6 +250,36 @@ Refreshes the skills, concepts, and scripts only. Your records — `state/`, `pl
249
250
  `okf/product/` — are yours, and an update never touches them. `pmos/KIT-VERSION` always says
250
251
  exactly what you're running.
251
252
 
253
+ ## What's new in 0.5.0
254
+
255
+ 0.4.0 opened the front of the loop. 0.5.0 makes explicit what was always the intent: **the kit is
256
+ provider- and model-agnostic**, and the harness kernel now says so in its own words.
257
+
258
+ - **Any capable coding agent runs the loop — not just Claude Code.** The runtime amendment
259
+ (D09/D34, ratified 2026-07-23) generalizes the evaluator, ingest, and design skills from
260
+ "runs in Claude Code" to "runs in the interactive agent runtime — any capable coding agent;
261
+ PMOS-self happens to use Claude Code, but the runtime is not Claude-specific." Point Codex,
262
+ Cursor, Gemini, or Copilot at `pmos/AGENTS.md` and the same protocol holds.
263
+
264
+ > Works the same in any of them: open your product folder in the agent, then
265
+ > "Read pmos/AGENTS.md and follow it." The gates, records, and metrics don't care which model
266
+ > wrote the code.
267
+
268
+ - **Per-model calibration.** When you log a run or an eval you can now name the model that did the
269
+ work (`model`) or the judging (`evaluator_model`), so the evaluator's calibration is read *per
270
+ model* instead of silently pooling providers — `scripts/metrics.py` warns when labeled evals span
271
+ more than one evaluator model, because a trust score one model earned isn't inherited by another.
272
+ The design surface is documented as pluggable too (any surface that can take a brief and export a
273
+ component library), not tied to one vendor.
274
+
275
+ - **A stated security posture that travels with the kit** (`pmos/SECURITY.md`), a `frontier.sh`
276
+ metrics helper, a file-mode `ticket-template.md`, and a sharper `SKILL-FORMAT.md` — all now part
277
+ of the vendored kernel.
278
+
279
+ **Upgrade note:** `npx create-pmos@latest --update` brings all of this in and never touches your
280
+ `state/`, `planning/`, or `okf/product/` records. Purely additive — no change to how the gate
281
+ behaves on your existing initiatives.
282
+
252
283
  ## What's new in 0.4.0
253
284
 
254
285
  0.3.0 sharpened the judging. 0.4.0 adds the **front of the loop** — deciding *what* to build and
@@ -21,8 +21,10 @@ const PKG = require(path.join(__dirname, '..', 'package.json'));
21
21
  const KERNEL = [
22
22
  'AGENTS.md',
23
23
  'README.md',
24
+ 'SECURITY.md',
24
25
  'KIT-VERSION',
25
26
  'skills',
27
+ 'skills-native',
26
28
  'okf/core',
27
29
  'scripts',
28
30
  'templates',
@@ -59,7 +61,8 @@ function vendor(targetRoot) {
59
61
  Next steps in ${targetRoot}:
60
62
  1. If the repo is not git yet: git init
61
63
  2. If it lives on GitHub: cp pmos/templates/pmos-gate.yml .github/workflows/pmos-gate.yml
62
- 3. Add to the repo's CLAUDE.md: "This product is PMOS-managed (file mode) — read pmos/AGENTS.md before any work."
64
+ 3. Wire PMOS into your agent's instruction file (CLAUDE.md, AGENTS.md, .cursorrules, GEMINI.md, or
65
+ .github/copilot-instructions.md): "This product is PMOS-managed (file mode) — read pmos/AGENTS.md before any work."
63
66
  4. Commit the pmos/ folder. Then follow pmos/AGENTS.md for every run.`);
64
67
  }
65
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
package/payload/AGENTS.md CHANGED
@@ -24,8 +24,8 @@ yourself wanting an MCP tool, use the state adapter below instead.
24
24
  | `get_skill` | read `pmos/skills/<name>.skill` |
25
25
  | `get_okf_concepts` | read `pmos/okf/…` (index-first, then the concept body — progressive disclosure) |
26
26
  | `get_eval_rubric` | read `pmos/planning/evals/<id>-eval-rubric.md` |
27
- | `log_agent_run` | append one JSON line to `pmos/state/runs.jsonl` (`{run_id, initiative_id, pre_run_contract, self_eval, human_corrections, date}`) |
28
- | `save_eval` | append one JSON line to `pmos/state/evals.jsonl` (`{run_id, initiative_id, rubric_path, score, verdict, dimensions, date}`) |
27
+ | `log_agent_run` | append one JSON line to `pmos/state/runs.jsonl` (`{run_id, initiative_id, pre_run_contract, self_eval, human_corrections, date, model}`) — record `model` = the model that produced the run so metrics stay honest across model switches |
28
+ | `save_eval` | append one JSON line to `pmos/state/evals.jsonl` (`{run_id, initiative_id, rubric_path, score, verdict, dimensions, date, evaluator_model}`) — record `evaluator_model` = the model that rendered the verdict; κ is calibrated **per evaluator model** (a model swap does not inherit a prior κ) |
29
29
  | `record_acceptance` | append one JSON line to `pmos/state/acceptances.jsonl` (`{run_id, initiative_id, accepted, pm_label, note, date}`) — **written only on the PM's explicit word** |
30
30
  | `record_outcome_check` | append one JSON line to `pmos/state/outcome_checks.jsonl` (`{initiative_id, disposition, evidence, date}`) — the D57 post-acceptance outcome verdict (`validated | refuted | inconclusive`; evidence required for the first two). **PM's word only — never auto-dispose from metrics**; a re-judgment is a new line |
31
31
  | `save_okf_doc` | write the doc under `pmos/okf/product/…` marked as a DRAFT; the PM approves it via PR review (the file-mode form of draft→approved) |
@@ -93,6 +93,22 @@ honest.
93
93
  not install the pmos gate, or mirror the initiative file locally** — a vendored sibling gate
94
94
  fails closed on a missing local record by design (S5 Review Gate finding).
95
95
 
96
+ ## Decomposing an initiative into tickets (file mode has no board)
97
+
98
+ When an initiative is bigger than one run, break it into **tickets** — the plain-file equivalent of
99
+ the hosted product's SDLC board. One file per ticket in `pmos/state/tickets/NNN-slug.md`, numbered
100
+ in **dependency order**, from [templates/ticket-template.md](templates/ticket-template.md):
101
+
102
+ - Frontmatter: `id` (the NNN), `title`, `status` (`todo | doing | done`), and `blocked_by` — an
103
+ explicit list of the ticket ids that must be `done` first. Blocking edges are **text**, not a
104
+ hidden graph.
105
+ - **Work the frontier.** `pmos/scripts/frontier.sh` lists exactly the tickets that are not done and
106
+ whose every blocker is done — the workable set. Start only from there. If it reports the frontier
107
+ is *blocked*, there is a cycle or an undone blocker to resolve before more work is startable.
108
+ - Each ticket names **one deliverable**, behaviorally (interfaces/types/contracts, never file paths
109
+ or line numbers — they stale while the ticket waits). Tickets decompose an initiative; they do not
110
+ replace it — the initiative record, PRD, rubric, and the three gates are unchanged.
111
+
96
112
  ## New agent paths — the lethal-trifecta audit
97
113
 
98
114
  Before creating any new agent path or widening a tool grant, verify it does **not** hold all three
@@ -117,6 +133,15 @@ deployment until this repo has accumulated its own PM-vs-evaluator record (appen
117
133
  `pmos/scripts/metrics.py` computes the north star (human corrections per accepted run), pass rates,
118
134
  and the Goodhart tripwire from `pmos/state/`.
119
135
 
136
+ **Model-awareness (any provider).** PMOS is provider/model-agnostic — any capable coding agent can
137
+ run this loop — but calibration is a property of the **evaluator model**, not of "the Review Gate"
138
+ abstractly. Record the model on every run/eval (the `model` / `evaluator_model` fields above); a
139
+ model swap **forks** the calibration record — you do not inherit a κ another model earned, and
140
+ `metrics.py` warns when labeled evals pool more than one evaluator model. The high-judgment roles
141
+ (the adversarial evaluator, `explain-run`) assume a **frontier-tier reasoner**: a weaker model
142
+ degrades *silently* to leniency and grep-clean while its output still looks well-formed — the
143
+ per-model κ is what makes that degradation visible.
144
+
120
145
  ## PM protocol (weekly)
121
146
 
122
147
  Scan `discoveries.md`; a friction theme seen twice with no resolving decision must be promoted to a
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@1687768
2
- assembled: 2026-07-21
1
+ kernel-source: pmos@917d29e
2
+ assembled: 2026-07-23
3
3
  mode: vendor
package/payload/README.md CHANGED
@@ -13,16 +13,24 @@ personally-owned PMOS backend).
13
13
  - `AGENTS-HUMAN.md` — the same loop for a **human** builder (D58): plain terms + the
14
14
  `scripts/pmos-log.sh` helper that appends validated state records in one command. Mixed
15
15
  human+agent teams share the same state.
16
+ - `SECURITY.md` — the kit's **security posture**: skills are executable instructions, what the kit
17
+ does structurally to be safe to install, and what stays your responsibility. Read it before you
18
+ install skills into your agent.
16
19
  - `skills/` — the PMOS skill kernel (prd, eval-rubric, okr, evaluator, …). Single-sourced from the
17
20
  PMOS repo; where a skill names a hosted tool, apply the AGENTS.md state adapter.
21
+ - `skills-native/` — the same skills in **Agent-Skills standard** form (`<name>/SKILL.md`
22
+ directories), generated from `skills/` so a Claude Code / Codex / skills.sh harness can load them.
23
+ The flat `skills/` files stay the source of truth; this is the portable mirror.
18
24
  - `okf/core/` — kernel concepts (the sdlc loop, the three-tier gate, watermelon flags, …).
19
25
  `okf/product/` — YOUR product's knowledge; drafted by ingest, approved via PR review.
20
26
  - `planning/` — okrs, PRDs, rubrics (`agent-run-eval-rubric.md` is the per-initiative template),
21
27
  `eval-calibration.md` (append every PM override of an evaluator verdict).
22
28
  - `state/` — the operational record, **append-only**: `initiatives/<id>.md`, `runs.jsonl`,
23
- `evals.jsonl`, `acceptances.jsonl`, `discoveries.md`.
29
+ `evals.jsonl`, `acceptances.jsonl`, `discoveries.md`. `tickets/NNN-slug.md` decompose an
30
+ initiative into ordered, dependency-numbered work (file-mode's board — see AGENTS.md).
24
31
  - `scripts/` — `gate-initiative.sh` (the fail-closed CI gate), `metrics.py` (north star +
25
- calibration + Goodhart tripwire), `kit-update.sh` (kernel refresh).
32
+ calibration + Goodhart tripwire), `kit-update.sh` (kernel refresh), `frontier.sh` (lists the
33
+ workable tickets — those not done whose blockers are all done).
26
34
  - `templates/pmos-gate.yml` — CI drop-in; copy to `.github/workflows/`.
27
35
  - `KIT-VERSION` — which PMOS kernel this was assembled from.
28
36
 
@@ -0,0 +1,53 @@
1
+ # PMOS kit — security posture
2
+
3
+ A skill is not a document. It is **instructions that run inside your agent**, with your agent's
4
+ tools and your agent's access. Anything the kit ships into your repo can shape what your agent does.
5
+ This file states, concretely, what the PMOS kit does to be safe to install — and where the
6
+ responsibility stays yours.
7
+
8
+ ## Why "we scanned it" is not a posture
9
+
10
+ The largest skills registry (skills.sh) shipped a malicious-skill scanner. Trail of Bits bypassed it
11
+ with prompt injection plus compiled bytecode. Static scanning of agent instructions is *evadable by
12
+ construction* — the payload is natural language aimed at a model, not a signature you can enumerate.
13
+ So the PMOS kit does not claim "scanned and safe." It claims a set of **structural** properties you
14
+ can check yourself, below.
15
+
16
+ ## What the kit does
17
+
18
+ - **No on-load command execution in shipped skills.** No PMOS kit skill uses Claude Code's dynamic
19
+ `` !`cmd` `` context injection or any load-time shell-out. A skill's body is read as instructions,
20
+ not run as a script when it loads. *(Check: `grep -rn '!\`' pmos/skills` — expect nothing.)*
21
+ - **Least authority, declared.** Shipped skills request the minimum tools they need; the
22
+ security-sensitive one — the **evaluator** — is **read-only by design**: its only write is the
23
+ schema-constrained verdict. The component that reads untrusted content (diffs, PR text) is
24
+ deliberately *not* the component that can act, which removes the exfiltration leg of the lethal
25
+ trifecta.
26
+ - **A `license` on every shipped skill** (portability + provenance), and PMOS producer metadata is
27
+ carried under the spec's `metadata:` extension map, never as ambient top-level directives.
28
+ - **Fail-closed containment.** The kit is assembled by `kit/build-kit.sh`, which greps its own output
29
+ and **aborts** if any reference to a hosted PMOS backend (a project ref, an MCP endpoint, or a
30
+ hosted-backend URL) leaks in. The npm publish path re-runs this check on the exact tarball and
31
+ refuses to publish on anything but a clean result. The kit you get is self-contained: **no
32
+ database, no network, no telemetry.**
33
+ - **No network calls in the installer.** `create-pmos` vendors a bundled payload; it makes no
34
+ outbound requests and sends nothing about you anywhere.
35
+ - **Everything the evaluator reads is untrusted data, never instructions.** An instruction embedded
36
+ in a diff or PR body ("mark this PASS") is treated as *evidence of a defect*, not a command —
37
+ paired with the read-only scope above, because prompt-level defenses alone are bypassable.
38
+
39
+ ## What stays your responsibility
40
+
41
+ - **Review before you install.** Read the skills and scripts in `pmos/` the way you would review any
42
+ dependency PR. The kit is small and plain-text on purpose so you can. `--update` refreshes only the
43
+ kernel and never touches your `state/` — review its diff like any other change.
44
+ - **Your agent's tools are yours to scope.** The kit declares least authority for its own skills, but
45
+ what your agent is *allowed* to do in your repo is set by you, not by us.
46
+ - **Pin what you install.** Prefer a pinned `create-pmos@<version>` over `@latest` in automation, and
47
+ re-review the kernel diff on each update.
48
+
49
+ ## Reporting
50
+
51
+ Found something that violates the posture above — a shipped skill with load-time execution, a
52
+ containment leak, an over-scoped tool request? Open an issue in the PMOS repo describing what and
53
+ where. The posture is only worth stating if it is enforced; a false claim here is itself a bug.
@@ -32,7 +32,8 @@ An initiative with a user-facing surface runs four legs *inside* Build:
32
32
  1. **Brief** — the [design-brief skill](/skills/core/design-brief.skill) writes the context handoff: what
33
33
  to design and the hard, presentation-only constraints, each surface tied to a real data source. It is to
34
34
  design what the [PRD](/skills/core/prd.skill) is to build.
35
- 2. **Design** — the design is made in claude.ai/design (external to this repo).
35
+ 2. **Design** — the design is made in an external design surface (PMOS-self uses claude.ai/design;
36
+ any capable design surface fits — external to this repo).
36
37
  3. **Reconcile + build** — the [design-reconcile skill](/skills/core/design-reconcile.skill) classifies
37
38
  every surface against **the product's capability ledger** and the PM signs off before any code; then the
38
39
  design is built into the product's app on its design system, verified by the product's CI. (For PMOS's own
@@ -42,7 +43,9 @@ An initiative with a user-facing surface runs four legs *inside* Build:
42
43
  to claude.ai/design (for PMOS, see [.design-sync/conventions.md](/.design-sync/conventions.md)), so the
43
44
  next design starts from the product's actual parts.
44
45
 
45
- Legs 2 and 4 are external tools PMOS *references*; legs 1 and 3 are the PMOS skills that make the loop
46
+ The design surface (legs 2 and 4) is **pluggable and provider-agnostic** — PMOS-self uses
47
+ claude.ai/design, but the loop is not tied to it; any capable design surface that can consume a brief
48
+ and export a component library fits (harness-model-agnosticism). Legs 2 and 4 are external tools PMOS *references*; legs 1 and 3 are the PMOS skills that make the loop
46
49
  repeatable.
47
50
 
48
51
  ## Presentation-only is the load-bearing constraint
@@ -119,7 +122,7 @@ PMOS's own instance: the `web/` app, the [D38](/okf/products/pmos/adr/d38-design
119
122
  [eval-driven](/okf/core/concepts/eval-driven-pm.md) style (the fidelity rubric before the build).
120
123
  - Reconciliation is [watermelon-flag](/okf/core/concepts/watermelon-flag.md) applied to UI.
121
124
  - PMOS stays a [control plane](/okf/core/concepts/control-plane.md): it specs and evaluates design; the design
122
- itself is made in claude.ai/design and built by a build agent.
125
+ itself is made in an external design surface (e.g. claude.ai/design) and built by a build agent.
123
126
  - Formalized by [D48](/okf/products/pmos/adr/d48-design-loop.md); made product-scoped by
124
127
  [D50](/okf/products/pmos/adr/d50-design-loop-product-scoped.md); composes with the design *system* (D38,
125
128
  PMOS's own instance).
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Template
3
3
  title: Design Brief Template
4
- description: The scaffold for a design brief instance — the context handoff from Claude Code to claude.ai/design. Copy into /planning/design/ and fill; presentation-only.
4
+ description: The scaffold for a design brief instance — the context handoff from the interactive agent to an external design surface (e.g. claude.ai/design). Copy into /planning/design/ and fill; presentation-only.
5
5
  tags: [template, design, brief, planning]
6
6
  timestamp: 2026-07-01
7
7
  ---
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # frontier.sh — list the workable tickets in a file-mode PMOS kit (E9).
3
+ #
4
+ # "Work the frontier": a ticket is workable when it is NOT done and EVERY ticket it is blocked_by is
5
+ # done. This is the plain-file equivalent of a board's dependency structure — one file per ticket in
6
+ # state/tickets/NNN-slug.md, numbered in dependency order, blocking edges as text.
7
+ #
8
+ # scripts/frontier.sh # from the kit root; lists workable tickets
9
+ # scripts/frontier.sh state/tickets # explicit tickets dir
10
+ #
11
+ # Dependency-free and portable: POSIX-ish bash, no associative arrays (runs on bash 3.2, e.g. stock
12
+ # macOS). No network, no board. Reads only the YAML frontmatter fields id / status / blocked_by.
13
+ set -euo pipefail
14
+
15
+ DIR="${1:-state/tickets}"
16
+ [ -d "$DIR" ] || { echo "frontier: no tickets dir at '$DIR' (nothing to do)"; exit 0; }
17
+
18
+ # --- field readers (frontmatter only: stop at the closing --- fence) ----------------------------
19
+ fm() { awk 'NR>1 && /^---[[:space:]]*$/{exit} {print}' "$1"; } # lines between the opening and closing ---
20
+ field() { printf '%s\n' "$1" | sed -n "s/^$2:[[:space:]]*//p" | head -1 | sed 's/^["'\'']//;s/["'\'']$//;s/[[:space:]]*$//'; }
21
+ # blocked_by is an inline list "[\"002\", \"005\"]"; extract the bare ids (digits) in order.
22
+ blockers() { printf '%s\n' "$1" | sed -n 's/^blocked_by:[[:space:]]*//p' | head -1 | grep -oE '[0-9]+' || true; }
23
+ # derive an id: explicit `id:` field, else the leading NNN of the filename.
24
+ idof() { local i; i="$(field "$1" id)"; [ -n "$i" ] && { printf '%s' "$i"; return; }; basename "$2" | grep -oE '^[0-9]+' || basename "$2" .md; }
25
+
26
+ shopt -s nullglob
27
+ files=("$DIR"/*.md)
28
+ [ ${#files[@]} -eq 0 ] && { echo "frontier: no tickets in '$DIR' (nothing to do)"; exit 0; }
29
+
30
+ # --- pass 1: collect the done ids (newline-delimited; no associative arrays for bash-3.2 portability)
31
+ DONE_IDS=""
32
+ OPEN=0
33
+ for f in "${files[@]}"; do
34
+ meta="$(fm "$f")"; id="$(idof "$meta" "$f")"; st="$(field "$meta" status)"
35
+ if [ "$st" = "done" ]; then DONE_IDS="$DONE_IDS$id
36
+ "; else OPEN=$((OPEN+1)); fi
37
+ done
38
+ is_done() { printf '%s' "$DONE_IDS" | grep -qx "$1"; }
39
+
40
+ # --- pass 2: a ticket is on the frontier when not done and every blocker is done -----------------
41
+ found=0
42
+ for f in "${files[@]}"; do
43
+ meta="$(fm "$f")"; id="$(idof "$meta" "$f")"; st="$(field "$meta" status)"; st="${st:-todo}"
44
+ [ "$st" = "done" ] && continue
45
+ workable=1
46
+ while IFS= read -r b; do
47
+ [ -z "$b" ] && continue
48
+ is_done "$b" || workable=0
49
+ done < <(blockers "$meta")
50
+ if [ "$workable" -eq 1 ]; then
51
+ printf ' ▸ %-5s %-8s %s\n' "$id" "[$st]" "$(field "$meta" title)"
52
+ found=$((found+1))
53
+ fi
54
+ done
55
+
56
+ if [ "$found" -eq 0 ]; then
57
+ # distinguish "all done" from "everything is blocked" — a stuck frontier is a real signal.
58
+ if [ "$OPEN" -eq 0 ]; then echo "frontier: all tickets done ✓"
59
+ else echo "frontier: $OPEN open ticket(s), NONE workable — the frontier is blocked (check for a cycle or an undone blocker)."; fi
60
+ fi
@@ -7,7 +7,9 @@ Reads pmos/state/{runs,evals,acceptances}.jsonl and prints:
7
7
  B2 throughput — accepted runs (guards against winning B1 by doing less)
8
8
  -- eval pass rate — share of evals with verdict PASS / PASS-WITH-FINDINGS
9
9
  B4 calibration — evaluator verdict vs PM label, joined by run_id: raw agreement, TPR, TNR,
10
- Cohen's kappa (the D40 classifier method, computed locally)
10
+ Cohen's kappa (the D40 classifier method, computed locally). κ is broken out
11
+ PER evaluator_model, with a mixed-model warning — a κ is a property of the
12
+ evaluator model and is not inherited across a model swap (harness-model-agnosticism)
11
13
  -- Goodhart tripwire — fires when eval_pass_rate >= 0.8 while avg corrections >= 1.5 (the
12
14
  pass-happy-judge signal; anti-optimism: read the red first)
13
15
 
@@ -47,25 +49,33 @@ n_pass = sum(1 for e in evals if str(e.get("verdict", "")).upper() in passing)
47
49
  pass_rate = (n_pass / len(evals)) if evals else None
48
50
 
49
51
  # --- B4: verdict-vs-PM-label confusion matrix, joined by run_id (D40) ---
52
+ # Calibration is a property of the EVALUATOR MODEL (harness-model-agnosticism): a κ earned by one
53
+ # model is not inherited by another. Compute the matrix OVERALL and PER evaluator_model, and warn
54
+ # when one aggregate κ silently pools verdicts from more than one model.
50
55
  label_by_run = {a.get("run_id"): str(a.get("pm_label", "")).lower() for a in acc if a.get("pm_label")}
51
- tp = tn = fp = fn = 0
52
- for e in evals:
53
- lab = label_by_run.get(e.get("run_id"))
54
- if lab not in ("pass", "fail"):
55
- continue
56
- pred_pass = str(e.get("verdict", "")).upper() in passing
57
- if pred_pass and lab == "pass":
58
- tp += 1
59
- elif pred_pass and lab == "fail":
60
- fp += 1
61
- elif not pred_pass and lab == "fail":
62
- tn += 1
63
- else:
64
- fn += 1
65
- n = tp + tn + fp + fn
66
56
 
67
57
 
68
- def kappa():
58
+ def confusion(rows):
59
+ """(tp, tn, fp, fn) over evals joined to a pass/fail PM label by run_id."""
60
+ tp = tn = fp = fn = 0
61
+ for e in rows:
62
+ lab = label_by_run.get(e.get("run_id"))
63
+ if lab not in ("pass", "fail"):
64
+ continue
65
+ pred_pass = str(e.get("verdict", "")).upper() in passing
66
+ if pred_pass and lab == "pass":
67
+ tp += 1
68
+ elif pred_pass and lab == "fail":
69
+ fp += 1
70
+ elif not pred_pass and lab == "fail":
71
+ tn += 1
72
+ else:
73
+ fn += 1
74
+ return tp, tn, fp, fn
75
+
76
+
77
+ def kappa_of(tp, tn, fp, fn):
78
+ n = tp + tn + fp + fn
69
79
  if n == 0:
70
80
  return None
71
81
  po = (tp + tn) / n
@@ -73,6 +83,14 @@ def kappa():
73
83
  return None if pe == 1 else (po - pe) / (1 - pe)
74
84
 
75
85
 
86
+ tp, tn, fp, fn = confusion(evals)
87
+ n = tp + tn + fp + fn
88
+
89
+
90
+ def kappa():
91
+ return kappa_of(tp, tn, fp, fn)
92
+
93
+
76
94
  avg_corr = (sum(corrections_by_run.values()) / len(corrections_by_run)) if corrections_by_run else None
77
95
  tripwire = pass_rate is not None and avg_corr is not None and pass_rate >= 0.8 and avg_corr >= 1.5
78
96
 
@@ -98,6 +116,28 @@ else:
98
116
  f"TNR {'n/a — no PM fails seen (leniency blind spot)' if tnr is None else f'{tnr:.0%}'}, "
99
117
  f"kappa {'undefined' if k is None else f'{k:.2f}'} (target >= 0.60 — verdict ADVISORY below it, D40)")
100
118
 
119
+ # --- per-evaluator-model calibration (harness-model-agnosticism) -------------
120
+ # κ is a property of (evaluator model × rubric); a model swap does NOT inherit a prior κ. When the
121
+ # labeled evals span >1 evaluator model, the aggregate above is a pool, not a gate — break it out.
122
+ # When no eval records its model, κ silently pools across any swaps: nudge to record provenance.
123
+ labeled_evals = [e for e in evals if label_by_run.get(e.get("run_id")) in ("pass", "fail")]
124
+ by_model = {}
125
+ for e in labeled_evals:
126
+ by_model.setdefault(e.get("evaluator_model") or "unknown", []).append(e)
127
+ if len(by_model) > 1:
128
+ print(f"\n🔴 MIXED-MODEL CALIBRATION — labeled evals span {len(by_model)} evaluator models; the "
129
+ "aggregate κ above POOLS them and is not any single evaluator's calibration (κ is per "
130
+ "evaluator model, D40). Per model:")
131
+ for m in sorted(by_model):
132
+ c = confusion(by_model[m])
133
+ km = kappa_of(*c)
134
+ print(f" - {m}: n={sum(c)}, kappa {'undefined' if km is None else f'{km:.2f}'} "
135
+ f"(tp{c[0]} tn{c[1]} fp{c[2]} fn{c[3]})")
136
+ elif by_model and set(by_model) == {"unknown"}:
137
+ print("\n⚠ evaluator_model unrecorded on every labeled eval — κ cannot be attributed to a model "
138
+ "and silently pools across any model swaps. Pass the evaluator model as the last arg to "
139
+ "`pmos-log.sh eval` (harness-model-agnosticism).")
140
+
101
141
  # --- D57 outcome checks (accepted-but-unvalidated; due prints red) -----------
102
142
  # Declarations live in initiative frontmatter (outcome_metric / outcome_horizon); dispositions are
103
143
  # appended to state/outcome_checks.jsonl ({initiative_id, disposition, evidence, date}). The latest
@@ -6,8 +6,8 @@
6
6
  # script only ever `>>`s; a correction is a NEW record. No dependencies beyond POSIX + python3
7
7
  # (used solely for safe JSON string escaping).
8
8
  #
9
- # pmos-log.sh run <run_id> <initiative_id> [human_corrections]
10
- # pmos-log.sh eval <run_id> <initiative_id> <score 0..1> <PASS|PASS-WITH-FINDINGS|FAIL> <rubric_path>
9
+ # pmos-log.sh run <run_id> <initiative_id> [human_corrections] [model]
10
+ # pmos-log.sh eval <run_id> <initiative_id> <score 0..1> <PASS|PASS-WITH-FINDINGS|FAIL> <rubric_path> [evaluator_model]
11
11
  # pmos-log.sh accept <run_id> <initiative_id> <pass|fail> [note]
12
12
  # pmos-log.sh outcome <initiative_id> <validated|refuted|inconclusive> [evidence]
13
13
  # pmos-log.sh friction <note...>
@@ -33,20 +33,30 @@ append() { # append <file> <json-line>
33
33
  cmd="${1:-}"; shift || true
34
34
  case "$cmd" in
35
35
  run)
36
- [ $# -ge 2 ] || fail "usage: run <run_id> <initiative_id> [human_corrections]"
36
+ [ $# -ge 2 ] || fail "usage: run <run_id> <initiative_id> [human_corrections] [model]"
37
37
  nonempty run_id "$1"; nonempty initiative_id "$2"
38
38
  corr="${3:-0}"
39
39
  printf '%s' "$corr" | grep -qE '^[0-9]+$' || fail "human_corrections must be a non-negative integer, got '$corr'."
40
- append runs.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"human_corrections\":$corr,\"date\":\"$today\"}"
40
+ # optional builder-model provenance (which model produced the run). Omitted when absent — the
41
+ # metric layer treats a missing model as 'unknown'. if-form, not `&&`: an empty $4 under set -e
42
+ # would abort. Keeps calibration honest across model switches (harness-model-agnosticism).
43
+ model_field=""
44
+ if [ -n "${4:-}" ]; then model_field=",\"model\":$(js "$4")"; fi
45
+ append runs.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"human_corrections\":$corr,\"date\":\"$today\"$model_field}"
41
46
  ;;
42
47
  eval)
43
- [ $# -ge 5 ] || fail "usage: eval <run_id> <initiative_id> <score 0..1> <PASS|PASS-WITH-FINDINGS|FAIL> <rubric_path>"
48
+ [ $# -ge 5 ] || fail "usage: eval <run_id> <initiative_id> <score 0..1> <PASS|PASS-WITH-FINDINGS|FAIL> <rubric_path> [evaluator_model]"
44
49
  nonempty run_id "$1"; nonempty initiative_id "$2"
45
50
  printf '%s' "$3" | grep -qE '^(0?\.[0-9]+|0|1(\.0+)?)$' || fail "score must be 0..1 written as 0, 1, N.N or .N — got '$3'."
46
51
  case "$4" in PASS|PASS-WITH-FINDINGS|FAIL) ;; *) fail "verdict must be PASS | PASS-WITH-FINDINGS | FAIL, got '$4'." ;; esac
47
52
  score="$3"; case "$score" in .*) score="0$score" ;; esac # ".5" -> "0.5": bare .5 is not valid JSON
48
53
  [ -f "$KIT/$5" ] || [ -f "$5" ] || fail "rubric_path '$5' not found (relative to the kit or the repo)."
49
- append evals.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"score\":$score,\"verdict\":$(js "$4"),\"rubric_path\":$(js "$5"),\"date\":\"$today\"}"
54
+ # optional evaluator-model provenance (which model rendered the verdict). Calibration (κ) is a
55
+ # property of the evaluator MODEL, so metrics.py segments κ by it; a missing value counts as
56
+ # 'unknown'. if-form, not `&&` (empty $6 under set -e aborts).
57
+ emodel_field=""
58
+ if [ -n "${6:-}" ]; then emodel_field=",\"evaluator_model\":$(js "$6")"; fi
59
+ append evals.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"score\":$score,\"verdict\":$(js "$4"),\"rubric_path\":$(js "$5"),\"date\":\"$today\"$emodel_field}"
50
60
  ;;
51
61
  accept)
52
62
  [ $# -ge 3 ] || fail "usage: accept <run_id> <initiative_id> <pass|fail> [note]"
@@ -36,13 +36,36 @@ Rules:
36
36
  (specified, non-executable) → graduates to active (key removed) by a PM decision in `DECISIONS.md`.
37
37
  - **Optional Phase-B registry metadata (D49)** — a skill may also carry `version`, `owner`, `risk`,
38
38
  `category`, and `scope`. These are **optional**, producer-defined registry fields that
39
- [`gen_skills_seed.sh`](/supabase/seed/gen_skills_seed.sh) sources into the `skills` table so the skill
39
+ the resync generator [`gen_okf_mirror_seed.py`](/supabase/gen_okf_mirror_seed.py) sources into the `skills` table so the skill
40
40
  library can group/version/scope skills. A skill with only `name` + `description` is still fully
41
41
  conformant (e.g. `build-skills/evaluator.skill`); carry the registry keys on durable `skills/core/`
42
42
  entries for consistency with their siblings.
43
+ - `lifecycle` — **optional**, in the same D49 registry slot. Values: `active` (the default when the
44
+ key is absent) | `deprecated`. A deprecated skill **stays on disk and stays in the registry** —
45
+ `get_skill` returns it in full with a `deprecated_notice` so a retrieving agent is *told* not to
46
+ start new work on it. Retirement is a state you can read; deletion is a separate act (see the
47
+ invariant below). Distinct from `maturity_mode`, which is about a skill not yet executable —
48
+ `lifecycle` is about one on its way out.
43
49
  - **No other L1 keys.** Beyond the required `name` + `description`, the optional `maturity_mode`, and the
44
50
  optional Phase-B registry keys above, procedural detail does not belong in L1 — it lives in the body.
45
51
 
52
+ ### The disk ⇄ registry invariant (enforced in CI)
53
+
54
+ **Every `.skill` on disk is seeded, and every seeded skill exists on disk — exactly.** The `skills`
55
+ table is a *mirror*, and `get_skill` serves it to agents in other people's repos, so a mirror that
56
+ has quietly drifted is a wrong answer delivered confidently.
57
+
58
+ The `REGISTRY` check in [`check_okf_drift.py`](/supabase/check_okf_drift.py) fails the build on:
59
+ adding or renaming a `.skill` without regenerating the seed; a seeded skill whose file is gone; and
60
+ any registry-metadata value that disagrees with the file's own frontmatter. That last one is not
61
+ theoretical — a generator whose INSERT omitted the Phase-B columns silently nulled
62
+ `version/owner/risk/category/scope` for **all 21 skills for about three weeks** in 2026-07, and was
63
+ caught only by an incidental version pin in an RLS test. So:
64
+
65
+ > **When you add, rename, delete, or edit the frontmatter of a `.skill`, regenerate the seed in the
66
+ > same PR** ([okf-sync playbook](/okf/core/playbooks/okf-sync.md)) — and sweep `supabase/tests/` for
67
+ > pins on that skill's registry metadata before you do (the prd-grilling F1 rule).
68
+
46
69
  ### Level 2 — SKILL.md body (loaded when relevant)
47
70
 
48
71
  The body below the frontmatter holds the **full procedural detail**: steps, heuristics, required
@@ -143,15 +166,49 @@ cheap:
143
166
  - Level-3 sub-files **≅** the standard's bundled files.
144
167
 
145
168
  (One nuance for the follow-on below: PMOS's *optional* D49 registry keys —
146
- `version`/`owner`/`risk`/`category`/`scope` — are top-level custom keys; the standard would expect
169
+ `version`/`owner`/`risk`/`category`/`scope`/`lifecycle` — are top-level custom keys; the standard would expect
147
170
  producer-defined extensions under a `metadata:` map. The **core** shape is lossless; relocating the
148
171
  registry keys belongs to the native-format packaging follow-on, not this flat form.)
149
172
 
150
- **Named follow-on (not built here):** shipping the **kit** in native Agent-Skills directory format so
151
- a stranger's Claude Code / Codex harness auto-discovers PMOS skills. It touches `build-kit.sh`,
152
- `get_skill`, `gen_skills_seed.sh`, and every skill path, so it is its own initiative — revisit when
153
- native-discovery demand in kit repos is proven. (This mirrors the mattpocock/skills plugin decision:
154
- keep the editable flat-file form, add a native-package path only when the demand is real.)
173
+ ### Kit native-format packaging (E6 — shipped)
174
+
175
+ The **kit** now ships every skill in native Agent-Skills directory form *as well as* the flat file —
176
+ a lossless portability layer, generated **from** the flat `.skill` at build time (never
177
+ hand-authored, so it cannot drift from its source). For each shipped skill, `kit/build-kit.sh` emits
178
+ `skills-native/<name>/SKILL.md` where:
179
+
180
+ - the directory name **equals** the frontmatter `name` (the standard's hard rule), and required
181
+ `name` + `description` are present;
182
+ - PMOS's D49 registry keys (`version`/`owner`/`risk`/`category`/`scope`/`lifecycle`) are relocated
183
+ **under a `metadata:` map with a `pmos-` prefix** — the standard's producer-extension slot — never
184
+ as top-level custom keys, so a conformant runtime ignores them cleanly;
185
+ - a **fail-closed conformance self-check** in `build-kit.sh` aborts the build on any violation
186
+ (name≠dirname, missing required field, or a top-level custom key), the same posture as the
187
+ containment check.
188
+
189
+ The flat `.skill` files remain the file-mode source of truth (the AGENTS.md state adapter references
190
+ `pmos/skills/<name>.skill`); the native tree is the ecosystem-portable mirror. **Still a named
191
+ follow-on (not built here):** wiring `create-pmos` to install the native tree into a runtime's
192
+ auto-discovery locations (`.claude/skills/`, `.agents/skills/`). E6 delivers the conformant
193
+ *packaging*; the *install location* is a separate change, revisited when native-discovery demand in
194
+ kit repos is proven.
195
+
196
+ ### Per-skill eval harness (E8 — convention shipped, benchmark deferred)
197
+
198
+ A skill may carry an **opt-in** eval file at `skills/evals/<name>.evals.json` (mapping to the
199
+ standard's `<name>/evals/evals.json` in native packaging), adopting the skill-creator harness shape:
200
+
201
+ - **`description_triggers`** — `should_fire` / `should_not_fire` prompt lists that test whether the
202
+ L1 description fires on the right requests and stays silent on the wrong ones (trigger-accuracy).
203
+ - **`cases`** — each a `{ name, prompt, expect, with_without }`, where `expect` is an **outcome
204
+ property** a grader checks (never a script) and `with_without` toggles whether the case is meant
205
+ for with-vs-without-the-skill benchmarking.
206
+
207
+ `scripts/check_skill_evals.py` (run in the `okf-drift` job) validates the **shape** of any present
208
+ eval file and that it names a real skill. It **never runs a benchmark** — actually scoring
209
+ with-vs-without needs labeled outcomes + compute and is **deferred** (there is no negative class
210
+ yet); running models in CI would make the gate nondeterministic. The convention and a worked example
211
+ (`skills/evals/prd.evals.json`) ship now; the verdict "this skill measurably helps" does not.
155
212
 
156
213
  ## File naming
157
214
 
@@ -170,8 +227,8 @@ keep the editable flat-file form, add a native-package path only when the demand
170
227
  A skill is well-formed when:
171
228
 
172
229
  1. Level 1 frontmatter has `name` + `description` (both required), plus optionally `maturity_mode` (on
173
- non-active skills) and the Phase-B registry keys `version`/`owner`/`risk`/`category`/`scope` (D49);
174
- no other L1 keys.
230
+ non-active skills) and the Phase-B registry keys `version`/`owner`/`risk`/`category`/`scope`/
231
+ `lifecycle` (D49); no other L1 keys.
175
232
  2. `name` matches the filename stem.
176
233
  3. `description` states both what the skill does and when to use it.
177
234
  4. The body (Level 2) carries the full procedure and is self-sufficient for the common case.