create-pmos 0.4.0 → 0.6.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.
Files changed (67) hide show
  1. package/README.md +149 -4
  2. package/bin/create-pmos.js +97 -8
  3. package/package.json +7 -3
  4. package/payload/AGENTS-HUMAN.md +6 -0
  5. package/payload/AGENTS.md +62 -3
  6. package/payload/KIT-VERSION +2 -2
  7. package/payload/README.md +16 -2
  8. package/payload/SECURITY.md +59 -0
  9. package/payload/okf/core/concepts/design-loop.md +6 -3
  10. package/payload/okf/core/concepts/frontmatter-format.md +75 -0
  11. package/payload/okf/core/concepts/index.md +3 -0
  12. package/payload/okf/core/concepts/log.md +13 -0
  13. package/payload/okf/core/templates/design-brief-template.md +1 -1
  14. package/payload/okf/core/templates/eval-rubric-template.md +12 -10
  15. package/payload/okf/core/templates/prd-template.md +6 -0
  16. package/payload/okf/core/templates/prototype-brief-template.md +45 -21
  17. package/payload/planning/evals/agent-run-eval-rubric.md +10 -7
  18. package/payload/scripts/frontier.sh +60 -0
  19. package/payload/scripts/metrics.py +163 -102
  20. package/payload/scripts/pmos-log.sh +16 -6
  21. package/payload/skills/SKILL-FORMAT.md +72 -9
  22. package/payload/skills/discovery.skill +4 -0
  23. package/payload/skills/eval-rubric.skill +1 -1
  24. package/payload/skills/evaluator.skill +65 -20
  25. package/payload/skills/explain-run.skill +9 -6
  26. package/payload/skills/explain-surface.skill +156 -0
  27. package/payload/skills/ingest-repo.skill +4 -3
  28. package/payload/skills/pmos-router.skill +25 -3
  29. package/payload/skills/prd.skill +8 -0
  30. package/payload/skills/prototype.skill +106 -41
  31. package/payload/skills-native/copywriting/SKILL.md +57 -0
  32. package/payload/skills-native/discovery/SKILL.md +75 -0
  33. package/payload/skills-native/eval-rubric/SKILL.md +159 -0
  34. package/payload/skills-native/evaluator/SKILL.md +182 -0
  35. package/payload/skills-native/explain-run/SKILL.md +121 -0
  36. package/payload/skills-native/explain-surface/SKILL.md +158 -0
  37. package/payload/skills-native/ingest-repo/SKILL.md +120 -0
  38. package/payload/skills-native/okr/SKILL.md +70 -0
  39. package/payload/skills-native/pmos-router/SKILL.md +81 -0
  40. package/payload/skills-native/prd/SKILL.md +117 -0
  41. package/payload/skills-native/prototype/SKILL.md +147 -0
  42. package/payload/state/tickets/.gitkeep +0 -0
  43. package/payload/templates/agent-run-eval-rubric.md +10 -7
  44. package/payload/templates/ticket-template.md +20 -0
  45. package/workspace/README.md +112 -0
  46. package/workspace/REMOTE.md +131 -0
  47. package/workspace/bin/pmos.js +206 -0
  48. package/workspace/doctor.js +99 -0
  49. package/workspace/lib/console-state.js +117 -0
  50. package/workspace/lib/file-mode-parse.js +58 -0
  51. package/workspace/lib/harness-parse.js +222 -0
  52. package/workspace/lib/preview-kind.js +98 -0
  53. package/workspace/lib/rate-limit.js +26 -0
  54. package/workspace/lib/remote-access.js +235 -0
  55. package/workspace/lib/review-gate.js +39 -0
  56. package/workspace/lib/schedule-logic.js +132 -0
  57. package/workspace/lib/skill-store.js +143 -0
  58. package/workspace/lib/surface-contract.js +79 -0
  59. package/workspace/lib/transcript-read.js +247 -0
  60. package/workspace/package-lock.json +94 -0
  61. package/workspace/package.json +23 -0
  62. package/workspace/public/app.js +1710 -0
  63. package/workspace/public/index.html +175 -0
  64. package/workspace/public/styles.css +766 -0
  65. package/workspace/server.js +1753 -0
  66. package/workspace/skills/backlog-triage.skill +13 -0
  67. package/workspace/skills/weekly-report.skill +19 -0
@@ -5,19 +5,61 @@ Reads pmos/state/{runs,evals,acceptances}.jsonl and prints:
5
5
  B1 north star — human corrections per ACCEPTED run (lower is better; D10, counted over
6
6
  accepted runs only so leniency can't deflate it)
7
7
  B2 throughput — accepted runs (guards against winning B1 by doing less)
8
- -- eval pass rate — share of evals with verdict PASS / PASS-WITH-FINDINGS
8
+ -- eval pass rate — share of evals passing the Review-Gate rule (see review_gate_pass)
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
 
14
16
  No network, no secrets. Exit 0 always (a reporting tool, not a gate). Anti-optimism: reds print first.
17
+
18
+ Import-safe (D64 Track D1): every helper is module-level and the report runs only under __main__, so
19
+ kit/fixtures/test_parsers.py can import review_gate_pass + leading_frontmatter and prove the Python
20
+ parsers agree with the JavaScript ones in workspace/lib/.
15
21
  """
16
22
  import datetime, glob, json, os, re, sys
17
23
 
18
24
  KIT = os.environ.get("KIT_ROOT") or os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
25
  STATE = os.path.join(KIT, "state")
20
26
 
27
+ # ── CANONICAL Review-Gate pass rule — the MIRROR of workspace/lib/review-gate.js ──────────────
28
+ # Rule: verdict ILIKE 'pass%' OR score >= 0.85. This is the single source of truth's Python half;
29
+ # the JS half is workspace/lib/review-gate.js. kit/fixtures/test_parsers.py runs BOTH against
30
+ # kit/fixtures/review-gate-pass-rule.json and fails if they ever disagree — the mirror can't drift.
31
+ PASS_VERDICT_PREFIX = "pass"
32
+ SCORE_THRESHOLD = 0.85
33
+
34
+
35
+ def review_gate_pass(verdict, score):
36
+ """A recorded verdict is AUTHORITATIVE (pass iff it starts with "pass"); an explicit FAIL is never
37
+ overridden by a high score. Only when NO verdict was recorded does the score decide.
38
+ AMENDED 2026-07-26: was `verdict ILIKE 'pass%' OR score >= 0.85`, which let a FAIL at score 0.93
39
+ through the gate (blind-plant round 2, specimen be58). Mirror of workspace/lib/review-gate.js
40
+ isReviewPass; agreement enforced by kit/fixtures/test_parsers.py."""
41
+ v = str("" if verdict is None else verdict).strip().lower()
42
+ if v:
43
+ return v.startswith(PASS_VERDICT_PREFIX)
44
+ try:
45
+ return float(score) >= SCORE_THRESHOLD
46
+ except (TypeError, ValueError):
47
+ return False
48
+
49
+
50
+ def leading_frontmatter(path):
51
+ """The LEADING frontmatter block only — body text cannot fake a declaration. Mirror of
52
+ workspace/lib/file-mode-parse.js parseLeadingFrontmatter (agreement enforced by the fixture test)."""
53
+ fm = {}
54
+ with open(path, encoding="utf-8") as fh:
55
+ for line in fh:
56
+ m = re.match(r"^([a-z_]+):\s*(.+?)\s*$", line.rstrip("\n"))
57
+ if m:
58
+ fm.setdefault(m.group(1), m.group(2))
59
+ elif line.strip() and not line.startswith("---"):
60
+ break # frontmatter is the LEADING block only
61
+ return fm
62
+
21
63
 
22
64
  def load(name):
23
65
  path = os.path.join(STATE, name)
@@ -34,38 +76,27 @@ def load(name):
34
76
  return rows
35
77
 
36
78
 
37
- runs = load("runs.jsonl")
38
- evals = load("evals.jsonl")
39
- acc = load("acceptances.jsonl")
40
-
41
- accepted = [a for a in acc if a.get("accepted") is True]
42
- corrections_by_run = {r.get("run_id"): int(r.get("human_corrections") or 0) for r in runs}
43
- acc_corr = [corrections_by_run.get(a.get("run_id"), 0) for a in accepted]
44
-
45
- passing = ("PASS", "PASS-WITH-FINDINGS")
46
- n_pass = sum(1 for e in evals if str(e.get("verdict", "")).upper() in passing)
47
- pass_rate = (n_pass / len(evals)) if evals else None
48
-
49
- # --- B4: verdict-vs-PM-label confusion matrix, joined by run_id (D40) ---
50
- 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
-
67
-
68
- def kappa():
79
+ def confusion(rows, label_by_run):
80
+ """(tp, tn, fp, fn) over evals joined to a pass/fail PM label by run_id, using the canonical rule."""
81
+ tp = tn = fp = fn = 0
82
+ for e in rows:
83
+ lab = label_by_run.get(e.get("run_id"))
84
+ if lab not in ("pass", "fail"):
85
+ continue
86
+ pred_pass = review_gate_pass(e.get("verdict"), e.get("score"))
87
+ if pred_pass and lab == "pass":
88
+ tp += 1
89
+ elif pred_pass and lab == "fail":
90
+ fp += 1
91
+ elif not pred_pass and lab == "fail":
92
+ tn += 1
93
+ else:
94
+ fn += 1
95
+ return tp, tn, fp, fn
96
+
97
+
98
+ def kappa_of(tp, tn, fp, fn):
99
+ n = tp + tn + fp + fn
69
100
  if n == 0:
70
101
  return None
71
102
  po = (tp + tn) / n
@@ -73,72 +104,102 @@ def kappa():
73
104
  return None if pe == 1 else (po - pe) / (1 - pe)
74
105
 
75
106
 
76
- avg_corr = (sum(corrections_by_run.values()) / len(corrections_by_run)) if corrections_by_run else None
77
- tripwire = pass_rate is not None and avg_corr is not None and pass_rate >= 0.8 and avg_corr >= 1.5
78
-
79
- print(f"PMOS kit metrics — {len(runs)} run(s), {len(evals)} eval(s), {len(acc)} acceptance record(s)\n")
80
- if tripwire:
81
- print("🔴 GOODHART TRIPWIRE FIRED — evaluator passes >=80% while humans average >=1.5 corrections/run.")
82
- print(" The north star is being gamed; treat the Review Gate as NOT calibrated (D40).\n")
83
- if not accepted:
84
- print("🔴 B1 north star: UNMEASURABLE — no accepted runs yet (record acceptances to pmos/state/acceptances.jsonl).")
85
- else:
86
- ns = sum(acc_corr) / len(accepted)
87
- print(f"B1 north star: {ns:.2f} human corrections per accepted run (over {len(accepted)} accepted run(s); lower is better)")
88
- print(f"B2 throughput: {len(accepted)} accepted run(s)")
89
- print(f" eval pass rate: {'n/a — no evals yet' if pass_rate is None else f'{pass_rate:.0%} ({n_pass}/{len(evals)})'}")
90
- k = kappa()
91
- if n == 0:
92
- print("🔴 B4 calibration: UNMEASURABLE — no eval joined to a PM-labelled acceptance yet; the Review Gate is uncalibrated.")
93
- else:
94
- tpr = tp / (tp + fn) if (tp + fn) else None
95
- tnr = tn / (tn + fp) if (tn + fp) else None
96
- print(f"B4 calibration (n={n}): raw agreement {(tp + tn) / n:.0%}, "
97
- f"TPR {'n/a' if tpr is None else f'{tpr:.0%}'}, "
98
- f"TNR {'n/a — no PM fails seen (leniency blind spot)' if tnr is None else f'{tnr:.0%}'}, "
99
- f"kappa {'undefined' if k is None else f'{k:.2f}'} (target >= 0.60 — verdict ADVISORY below it, D40)")
100
-
101
- # --- D57 outcome checks (accepted-but-unvalidated; due prints red) -----------
102
- # Declarations live in initiative frontmatter (outcome_metric / outcome_horizon); dispositions are
103
- # appended to state/outcome_checks.jsonl ({initiative_id, disposition, evidence, date}). The latest
104
- # line per initiative wins; history is never rewritten. Auto-validation is prohibited (D57) — this
105
- # section only REPORTS; recording a disposition is always the PM's word.
106
- checks = load("outcome_checks.jsonl")
107
- latest_disp = {}
108
- for c in checks:
109
- if c.get("initiative_id"):
110
- latest_disp[c["initiative_id"]] = str(c.get("disposition", "")).lower()
111
-
112
- declared = []
113
- for path in sorted(glob.glob(os.path.join(STATE, "initiatives", "*.md"))):
114
- fm = {}
115
- for line in open(path, encoding="utf-8"):
116
- m = re.match(r"^([a-z_]+):\s*(.+?)\s*$", line)
117
- if m:
118
- fm.setdefault(m.group(1), m.group(2))
119
- elif line.strip() and not line.startswith("---"):
120
- break # frontmatter is the LEADING block only — body text cannot fake a declaration
121
- if not fm.get("id"):
107
+ def main():
108
+ runs = load("runs.jsonl")
109
+ evals = load("evals.jsonl")
110
+ acc = load("acceptances.jsonl")
111
+
112
+ accepted = [a for a in acc if a.get("accepted") is True]
113
+ corrections_by_run = {r.get("run_id"): int(r.get("human_corrections") or 0) for r in runs}
114
+ acc_corr = [corrections_by_run.get(a.get("run_id"), 0) for a in accepted]
115
+
116
+ n_pass = sum(1 for e in evals if review_gate_pass(e.get("verdict"), e.get("score")))
117
+ pass_rate = (n_pass / len(evals)) if evals else None
118
+
119
+ # --- B4: verdict-vs-PM-label confusion matrix, joined by run_id (D40) ---
120
+ label_by_run = {a.get("run_id"): str(a.get("pm_label", "")).lower() for a in acc if a.get("pm_label")}
121
+ tp, tn, fp, fn = confusion(evals, label_by_run)
122
+ n = tp + tn + fp + fn
123
+ k = kappa_of(tp, tn, fp, fn)
124
+
125
+ avg_corr = (sum(corrections_by_run.values()) / len(corrections_by_run)) if corrections_by_run else None
126
+ tripwire = pass_rate is not None and avg_corr is not None and pass_rate >= 0.8 and avg_corr >= 1.5
127
+
128
+ print(f"PMOS kit metrics — {len(runs)} run(s), {len(evals)} eval(s), {len(acc)} acceptance record(s)\n")
129
+ if tripwire:
130
+ print("🔴 GOODHART TRIPWIRE FIRED — evaluator passes >=80% while humans average >=1.5 corrections/run.")
131
+ print(" The north star is being gamed; treat the Review Gate as NOT calibrated (D40).\n")
132
+ if not accepted:
133
+ print("🔴 B1 north star: UNMEASURABLE — no accepted runs yet (record acceptances to pmos/state/acceptances.jsonl).")
134
+ else:
135
+ ns = sum(acc_corr) / len(accepted)
136
+ print(f"B1 north star: {ns:.2f} human corrections per accepted run (over {len(accepted)} accepted run(s); lower is better)")
137
+ print(f"B2 throughput: {len(accepted)} accepted run(s)")
138
+ print(f" eval pass rate: {'n/a — no evals yet' if pass_rate is None else f'{pass_rate:.0%} ({n_pass}/{len(evals)})'}")
139
+ if n == 0:
140
+ print("🔴 B4 calibration: UNMEASURABLE — no eval joined to a PM-labelled acceptance yet; the Review Gate is uncalibrated.")
141
+ else:
142
+ tpr = tp / (tp + fn) if (tp + fn) else None
143
+ tnr = tn / (tn + fp) if (tn + fp) else None
144
+ print(f"B4 calibration (n={n}): raw agreement {(tp + tn) / n:.0%}, "
145
+ f"TPR {'n/a' if tpr is None else f'{tpr:.0%}'}, "
146
+ f"TNR {'n/a — no PM fails seen (leniency blind spot)' if tnr is None else f'{tnr:.0%}'}, "
147
+ f"kappa {'undefined' if k is None else f'{k:.2f}'} (target >= 0.60 — verdict ADVISORY below it, D40)")
148
+
149
+ # --- per-evaluator-model calibration (harness-model-agnosticism) -------------
150
+ labeled_evals = [e for e in evals if label_by_run.get(e.get("run_id")) in ("pass", "fail")]
151
+ by_model = {}
152
+ for e in labeled_evals:
153
+ by_model.setdefault(e.get("evaluator_model") or "unknown", []).append(e)
154
+ if len(by_model) > 1:
155
+ print(f"\n🔴 MIXED-MODEL CALIBRATION — labeled evals span {len(by_model)} evaluator models; the "
156
+ "aggregate κ above POOLS them and is not any single evaluator's calibration (κ is per "
157
+ "evaluator model, D40). Per model:")
158
+ for m in sorted(by_model):
159
+ c = confusion(by_model[m], label_by_run)
160
+ km = kappa_of(*c)
161
+ print(f" - {m}: n={sum(c)}, kappa {'undefined' if km is None else f'{km:.2f}'} "
162
+ f"(tp{c[0]} tn{c[1]} fp{c[2]} fn{c[3]})")
163
+ elif by_model and set(by_model) == {"unknown"}:
164
+ print("\n⚠ evaluator_model unrecorded on every labeled eval — κ cannot be attributed to a model "
165
+ "and silently pools across any model swaps. Pass the evaluator model as the last arg to "
166
+ "`pmos-log.sh eval` (harness-model-agnosticism).")
167
+
168
+ # --- D57 outcome checks (accepted-but-unvalidated; due prints red) -----------
169
+ checks = load("outcome_checks.jsonl")
170
+ latest_disp = {}
171
+ for c in checks:
172
+ if c.get("initiative_id"):
173
+ latest_disp[c["initiative_id"]] = str(c.get("disposition", "")).lower()
174
+
175
+ declared = []
176
+ for path in sorted(glob.glob(os.path.join(STATE, "initiatives", "*.md"))):
177
+ fm = leading_frontmatter(path)
178
+ if not fm.get("id"):
179
+ if fm.get("outcome_metric"):
180
+ print(f"⚠ {os.path.basename(path)} declares an outcome but has no id: — it can never be disposed; fix the frontmatter")
181
+ continue
122
182
  if fm.get("outcome_metric"):
123
- print(f"⚠ {os.path.basename(path)} declares an outcome but has no id: — it can never be disposed; fix the frontmatter")
124
- continue
125
- if fm.get("outcome_metric"):
126
- declared.append(fm)
127
-
128
- if declared:
129
- today = datetime.date.today().isoformat()
130
- undisposed = [f for f in declared if f.get("id") not in latest_disp]
131
- # a missing horizon errs RED (counts as due): a bet with no judgment date can never come due
132
- # by itself — horizon-gaming-by-omission (mirrors the hosted CHECK requiring a horizon).
133
- due = [f for f in undisposed if str(f.get("outcome_horizon", "0000-00-00")) < today]
134
- pending = [f for f in undisposed if f not in due]
135
- print()
136
- if due:
137
- print(f"🔴 D57 outcome checks DUE ({len(due)}) — accepted bets past horizon, never validated:")
138
- for f in due:
139
- print(f" - {f.get('id')}: \"{f.get('outcome_metric')}\" (horizon {f.get('outcome_horizon', '?')})"
140
- " -> dispose validated|refuted|inconclusive to state/outcome_checks.jsonl")
141
- refuted = sum(1 for v in latest_disp.values() if v == "refuted")
142
- print(f"D57 outcomes: {len(declared)} declared — {len(pending)} pending, {len(due)} due, "
143
- f"{len(latest_disp)} disposed"
144
- + (f" ({refuted} refuted — recorded, not hidden)" if refuted else ""))
183
+ declared.append(fm)
184
+
185
+ if declared:
186
+ today = datetime.date.today().isoformat()
187
+ undisposed = [f for f in declared if f.get("id") not in latest_disp]
188
+ # a missing horizon errs RED (counts as due): a bet with no judgment date can never come due
189
+ # by itself — horizon-gaming-by-omission (mirrors the hosted CHECK requiring a horizon).
190
+ due = [f for f in undisposed if str(f.get("outcome_horizon", "0000-00-00")) < today]
191
+ pending = [f for f in undisposed if f not in due]
192
+ print()
193
+ if due:
194
+ print(f"🔴 D57 outcome checks DUE ({len(due)}) — accepted bets past horizon, never validated:")
195
+ for f in due:
196
+ print(f" - {f.get('id')}: \"{f.get('outcome_metric')}\" (horizon {f.get('outcome_horizon', '?')})"
197
+ " -> dispose validated|refuted|inconclusive to state/outcome_checks.jsonl")
198
+ refuted = sum(1 for v in latest_disp.values() if v == "refuted")
199
+ print(f"D57 outcomes: {len(declared)} declared — {len(pending)} pending, {len(due)} due, "
200
+ f"{len(latest_disp)} disposed"
201
+ + (f" ({refuted} refuted — recorded, not hidden)" if refuted else ""))
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()
@@ -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]"
@@ -1,3 +1,9 @@
1
+ ---
2
+ type: Reference
3
+ title: "SKILL-FORMAT.md — Required Format for PMOS Skills"
4
+ timestamp: 2026-07-23
5
+ ---
6
+
1
7
  # SKILL-FORMAT.md — Required Format for PMOS Skills
2
8
 
3
9
  This document defines the **required** structure for every `.skill` file in PMOS. It is the
@@ -36,13 +42,36 @@ Rules:
36
42
  (specified, non-executable) → graduates to active (key removed) by a PM decision in `DECISIONS.md`.
37
43
  - **Optional Phase-B registry metadata (D49)** — a skill may also carry `version`, `owner`, `risk`,
38
44
  `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
45
+ the resync generator [`gen_okf_mirror_seed.py`](/supabase/gen_okf_mirror_seed.py) sources into the `skills` table so the skill
40
46
  library can group/version/scope skills. A skill with only `name` + `description` is still fully
41
47
  conformant (e.g. `build-skills/evaluator.skill`); carry the registry keys on durable `skills/core/`
42
48
  entries for consistency with their siblings.
49
+ - `lifecycle` — **optional**, in the same D49 registry slot. Values: `active` (the default when the
50
+ key is absent) | `deprecated`. A deprecated skill **stays on disk and stays in the registry** —
51
+ `get_skill` returns it in full with a `deprecated_notice` so a retrieving agent is *told* not to
52
+ start new work on it. Retirement is a state you can read; deletion is a separate act (see the
53
+ invariant below). Distinct from `maturity_mode`, which is about a skill not yet executable —
54
+ `lifecycle` is about one on its way out.
43
55
  - **No other L1 keys.** Beyond the required `name` + `description`, the optional `maturity_mode`, and the
44
56
  optional Phase-B registry keys above, procedural detail does not belong in L1 — it lives in the body.
45
57
 
58
+ ### The disk ⇄ registry invariant (enforced in CI)
59
+
60
+ **Every `.skill` on disk is seeded, and every seeded skill exists on disk — exactly.** The `skills`
61
+ table is a *mirror*, and `get_skill` serves it to agents in other people's repos, so a mirror that
62
+ has quietly drifted is a wrong answer delivered confidently.
63
+
64
+ The `REGISTRY` check in [`check_okf_drift.py`](/supabase/check_okf_drift.py) fails the build on:
65
+ adding or renaming a `.skill` without regenerating the seed; a seeded skill whose file is gone; and
66
+ any registry-metadata value that disagrees with the file's own frontmatter. That last one is not
67
+ theoretical — a generator whose INSERT omitted the Phase-B columns silently nulled
68
+ `version/owner/risk/category/scope` for **all 21 skills for about three weeks** in 2026-07, and was
69
+ caught only by an incidental version pin in an RLS test. So:
70
+
71
+ > **When you add, rename, delete, or edit the frontmatter of a `.skill`, regenerate the seed in the
72
+ > same PR** ([okf-sync playbook](/okf/core/playbooks/okf-sync.md)) — and sweep `supabase/tests/` for
73
+ > pins on that skill's registry metadata before you do (the prd-grilling F1 rule).
74
+
46
75
  ### Level 2 — SKILL.md body (loaded when relevant)
47
76
 
48
77
  The body below the frontmatter holds the **full procedural detail**: steps, heuristics, required
@@ -143,15 +172,49 @@ cheap:
143
172
  - Level-3 sub-files **≅** the standard's bundled files.
144
173
 
145
174
  (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
175
+ `version`/`owner`/`risk`/`category`/`scope`/`lifecycle` — are top-level custom keys; the standard would expect
147
176
  producer-defined extensions under a `metadata:` map. The **core** shape is lossless; relocating the
148
177
  registry keys belongs to the native-format packaging follow-on, not this flat form.)
149
178
 
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.)
179
+ ### Kit native-format packaging (E6 — shipped)
180
+
181
+ The **kit** now ships every skill in native Agent-Skills directory form *as well as* the flat file —
182
+ a lossless portability layer, generated **from** the flat `.skill` at build time (never
183
+ hand-authored, so it cannot drift from its source). For each shipped skill, `kit/build-kit.sh` emits
184
+ `skills-native/<name>/SKILL.md` where:
185
+
186
+ - the directory name **equals** the frontmatter `name` (the standard's hard rule), and required
187
+ `name` + `description` are present;
188
+ - PMOS's D49 registry keys (`version`/`owner`/`risk`/`category`/`scope`/`lifecycle`) are relocated
189
+ **under a `metadata:` map with a `pmos-` prefix** — the standard's producer-extension slot — never
190
+ as top-level custom keys, so a conformant runtime ignores them cleanly;
191
+ - a **fail-closed conformance self-check** in `build-kit.sh` aborts the build on any violation
192
+ (name≠dirname, missing required field, or a top-level custom key), the same posture as the
193
+ containment check.
194
+
195
+ The flat `.skill` files remain the file-mode source of truth (the AGENTS.md state adapter references
196
+ `pmos/skills/<name>.skill`); the native tree is the ecosystem-portable mirror. **Still a named
197
+ follow-on (not built here):** wiring `create-pmos` to install the native tree into a runtime's
198
+ auto-discovery locations (`.claude/skills/`, `.agents/skills/`). E6 delivers the conformant
199
+ *packaging*; the *install location* is a separate change, revisited when native-discovery demand in
200
+ kit repos is proven.
201
+
202
+ ### Per-skill eval harness (E8 — convention shipped, benchmark deferred)
203
+
204
+ A skill may carry an **opt-in** eval file at `skills/evals/<name>.evals.json` (mapping to the
205
+ standard's `<name>/evals/evals.json` in native packaging), adopting the skill-creator harness shape:
206
+
207
+ - **`description_triggers`** — `should_fire` / `should_not_fire` prompt lists that test whether the
208
+ L1 description fires on the right requests and stays silent on the wrong ones (trigger-accuracy).
209
+ - **`cases`** — each a `{ name, prompt, expect, with_without }`, where `expect` is an **outcome
210
+ property** a grader checks (never a script) and `with_without` toggles whether the case is meant
211
+ for with-vs-without-the-skill benchmarking.
212
+
213
+ `scripts/check_skill_evals.py` (run in the `okf-drift` job) validates the **shape** of any present
214
+ eval file and that it names a real skill. It **never runs a benchmark** — actually scoring
215
+ with-vs-without needs labeled outcomes + compute and is **deferred** (there is no negative class
216
+ yet); running models in CI would make the gate nondeterministic. The convention and a worked example
217
+ (`skills/evals/prd.evals.json`) ship now; the verdict "this skill measurably helps" does not.
155
218
 
156
219
  ## File naming
157
220
 
@@ -170,8 +233,8 @@ keep the editable flat-file form, add a native-package path only when the demand
170
233
  A skill is well-formed when:
171
234
 
172
235
  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.
236
+ non-active skills) and the Phase-B registry keys `version`/`owner`/`risk`/`category`/`scope`/
237
+ `lifecycle` (D49); no other L1 keys.
175
238
  2. `name` matches the filename stem.
176
239
  3. `description` states both what the skill does and when to use it.
177
240
  4. The body (Level 2) carries the full procedure and is self-sufficient for the common case.
@@ -35,6 +35,10 @@ fake rubric, or escaped the record entirely.
35
35
  1. **Anchor and create the initiative** with `type: discovery`
36
36
  (`create_initiative({id, title, parent_kr, type: 'discovery'})`; file mode: `type: discovery`
37
37
  in the initiative frontmatter).
38
+ 1b. **Existing surface? Understand it first.** If answering the question honestly requires reading
39
+ code that already exists, run an [explain-surface](/build-skills/explain-surface.skill) pass on
40
+ that region — in a fresh context, **before** writing the brief. A spike aimed at a system the PM
41
+ does not understand produces a learning they cannot act on. Greenfield → skip.
38
42
  2. **Write the spike brief** from
39
43
  [spike-brief-template](/okf/core/templates/spike-brief-template.md) into
40
44
  `/planning/prd/<id>-spike.md`; set it as the initiative's `prd_path`. The Initiative Gate
@@ -43,7 +43,7 @@ Acceptance Gate is the PM's and is not graded by the rubric.
43
43
 
44
44
  Every rubric document carries, at minimum:
45
45
 
46
- - `type: Rubric` (or as the bundle/planning convention requires)
46
+ - **OKF frontmatter** — the file opens with `type: Rubric`, `title`, `timestamp` (repo-wide convention, D63; [frontmatter-format](/okf/core/concepts/frontmatter-format.md)).
47
47
  - **`eval_type: capability | regression`** —
48
48
  - `capability` — tests whether the agent can do something new / harder. Used to push the frontier.
49
49
  - `regression` — guards an already-passing capability against backsliding. Saturated capability
@@ -8,7 +8,9 @@ description: Adversarially evaluate an agent run's output against its rubric and
8
8
  This skill is PMOS's **Review Gate** made a first-class, repeatable capability (P4). It formalizes the
9
9
  adversarial independent evaluator run by hand through P1–P3: judge what an agent run produced against a
10
10
  rubric, write a structured verdict to the `evals` table, and surface it on the radar/board. It runs in
11
- **Claude Code** (heavy judgment), not an Edge Function ([D34](/DECISIONS.md)).
11
+ the **interactive agent runtime** — any capable coding agent; heavy judgment belongs in the agent, not
12
+ an Edge Function ([D09](/DECISIONS.md)/[D34](/DECISIONS.md), amended 2026-07-23). PMOS-self runs it in
13
+ Claude Code, but the runtime is not Claude-specific.
12
14
 
13
15
  > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)). Level 1
14
16
  > frontmatter above is the trigger. This Level 2 body is the full procedure.
@@ -23,12 +25,21 @@ rubric, write a structured verdict to the `evals` table, and surface it on the r
23
25
  - **Outcome-only ([D14](/DECISIONS.md)).** Grade what the run *produced* — the artifact, the diff, the
24
26
  live behavior — **not** the tool-call sequence or the path taken. A right answer reached "the wrong
25
27
  way" still passes; a wrong answer reached tidily still fails.
26
- - **Self-preference bias — distrust your own praise.** You are a **Claude evaluator judging
27
- Claude-built work**, so you carry a built-in bias to over-reward outputs in your own family's style
28
- ([D40](/DECISIONS.md)). Counter it: **default to skeptical**, and when a pass feels easy or you are
29
- uncertain, **say so explicitly** and flag the run for the cross-family spot-check / minority-veto jury
30
- ([calibration-method](/planning/evals/calibration-method.md)). Do not let stylistic familiarity read as
31
- quality.
28
+ - **Self-preference bias — distrust your own praise.** An LLM judge over-rewards outputs from **its own
29
+ model family's style** ([D40](/DECISIONS.md)). Read the run's recorded `builder_model` and your own
30
+ `evaluator_model`: when they **share a family** you carry a built-in bias to over-reward (the common
31
+ PMOS-self case — a Claude evaluator on Claude-built work); when they **differ**, the bias flips and you
32
+ may under-reward an unfamiliar style. Either way: **default to skeptical**, and when a pass feels easy
33
+ or you are uncertain, **say so explicitly** and flag the run for the cross-family spot-check /
34
+ minority-veto jury ([calibration-method](/planning/evals/calibration-method.md)). Do not let stylistic
35
+ (un)familiarity read as quality. **Record your `evaluator_model` on the `save_eval` verdict** — κ is
36
+ calibrated per evaluator model, and an unrecorded model silently pools calibration across model swaps.
37
+ - **Capability floor — this role assumes a frontier-tier reasoner.** The adversarial passes below
38
+ (default-to-refute, both-ways pairwise, semantic-pass-not-grep, injection resistance) degrade
39
+ **silently** to leniency and grep-clean on a weaker model — the verdict still looks well-formed while
40
+ the safety property is already gone. If you cannot confidently perform them, **say so in the verdict**
41
+ and score the affected dimensions `unverified` rather than passing on trust; the per-model κ
42
+ ([v_calibration_by_model], file-mode `metrics.py`) is what surfaces a too-weak evaluator over time.
32
43
  - **Position, order, and verbosity bias — judge the artifact, not its packaging.** When comparing
33
44
  options or scoring against exemplars, substance decides, not presentation order or length.
34
45
  **Order must not sway a comparison** — never let "first-seen" or "last-seen" win; for any **pairwise**
@@ -56,6 +67,30 @@ rubric, write a structured verdict to the `evals` table, and surface it on the r
56
67
  PM labels** (TPR/TNR, not a streak) per [D40](/DECISIONS.md), which **supersedes** D20's "3 consecutive
57
68
  agreements" standard. Say in the verdict that the gate is advisory until that kappa threshold is met.
58
69
 
70
+ ## Two-axis verdict (Spec × Standards)
71
+
72
+ Score on **two independent axes**, because "did it do what was asked?" and "is it well-built?" fail
73
+ separately and a single blended number lets one hide the other:
74
+
75
+ - **Spec axis** — does the output satisfy the `pre_run_contract` done-criteria? Outcome-only (D14):
76
+ the right thing was produced, judged against each criterion's `test_method`. This axis is the
77
+ per-criterion binary backbone of step 5.
78
+ - **Standards axis** — is it well-built? Craft, security, tests, maintainability, and the rubric's
79
+ quality dimensions (step 6). A run can meet every done-criterion and still be shoddy here; a
80
+ beautifully-built run can miss the spec.
81
+
82
+ Each axis gets its **own pass/fail and its own one-line rationale**, and the overall verdict is a
83
+ **stated function of the two: a FAIL on *either* axis cannot become an overall PASS.** (Spec-fail →
84
+ FAIL; Standards-fail → at best PASS-WITH-FINDINGS only when every defect is non-blocking, otherwise
85
+ FAIL — never a clean PASS.) The split is **persisted**, not just narrated (step 9).
86
+
87
+ > **Calibration of the axis split is DEFERRED (stated, not done).** Whether separating the axes
88
+ > actually *moves* Cohen's κ against PM labels is the E4 research question — and it is unanswerable
89
+ > until the Review Gate has faced a **negative class** (all labeled pairs are currently `pass`; TNR
90
+ > is undefined). Emit the two axes now for legibility; do **not** claim the split is calibrated or
91
+ > validated. It becomes measurable once `pmos-seeded-known-bad-runs` (or real fails) gives κ a
92
+ > denominator. ([D40](/DECISIONS.md), [calibration-method](/planning/evals/calibration-method.md).)
93
+
59
94
  ## Procedure
60
95
 
61
96
  1. **Read [AGENTS.md](/AGENTS.md)** and the run's `pre_run_contract`. *Done when* you can state the
@@ -111,17 +146,25 @@ rubric, write a structured verdict to the `evals` table, and surface it on the r
111
146
  7. **Enumerate defects** — each with `severity` (critical / must-fix / should-fix / low / nit), the
112
147
  specific file+line or behavior, why it's a problem, and a concrete fix. Lead with the single most
113
148
  important finding.
114
- 8. **Decide a verdict**: `PASS` / `PASS-WITH-FINDINGS` / `FAIL`. PASS-WITH-FINDINGS is correct when the
115
- work is acceptable but carries non-blocking defects. Include a one-line **pass-fraction
116
- disclosure**: the share of pass-side verdicts you (this gate) have recently issued — read it
117
- from `v_calibration` / `v_goodhart_tripwire` where a backend is available, or state
118
- `pass-fraction: unknown (no backend)` in kit mode. A high pass-fraction is a leniency-drift
119
- warning to the PM, not a compliment to the builders
149
+ 8. **Decide the two axis verdicts, then the overall.** State a **Spec** pass/fail (from the step-5
150
+ per-criterion binaries — any FAILed done-criterion fails the axis) and a **Standards** pass/fail
151
+ (from the step-6 dimensions + defect severities — a critical/must-fix defect fails the axis),
152
+ each with a one-line rationale. Then combine into `PASS` / `PASS-WITH-FINDINGS` / `FAIL` under the
153
+ **stated rule: a FAIL on either axis cannot be an overall PASS** — Spec-fail ⇒ FAIL;
154
+ Standards-fail ⇒ FAIL unless every defect is non-blocking, in which case PASS-WITH-FINDINGS; only
155
+ both axes passing yields PASS (clean PASS only with no should-fix-or-worse defects). Include a
156
+ one-line **pass-fraction disclosure**: the share of pass-side verdicts you (this gate) have
157
+ recently issued — read it from `v_calibration` / `v_goodhart_tripwire` where a backend is
158
+ available, or state `pass-fraction: unknown (no backend)` in kit mode. A high pass-fraction is a
159
+ leniency-drift warning to the PM, not a compliment to the builders
120
160
  ([llm-judge-calibration](/planning/research/llm-judge-calibration.md)).
121
161
  9. **Persist it** — `save_eval({ initiative_id, run_id, rubric_path, score, verdict, dimensions })`,
122
- where `dimensions` is `{ <name>: { score, weight, note, evidence } }` — `evidence` carries the
123
- quote/pointer from step 6 (or the string `unverified`). This is what makes the gate self-hosting
124
- — the verdict lands in `evals` and surfaces on `v_eval_gate_status` (radar/board).
162
+ where `dimensions` is `{ <name>: { score, weight, note, evidence, axis } }` — `evidence` carries
163
+ the quote/pointer from step 6 (or the string `unverified`), and **`axis` is `"spec"` or
164
+ `"standards"`** so the two-axis split is persisted, not just narrated. Put the two axis pass/fail
165
+ summaries in the top-level `note`/report and tag every dimension with its axis. This is what makes
166
+ the gate self-hosting — the verdict lands in `evals` and surfaces on `v_eval_gate_status`
167
+ (radar/board).
125
168
  10. **Hand to the PM.** The Acceptance Gate is theirs, and the PM records the ground-truth `pm_label` to
126
169
  `acceptances` — paired against your verdict by `v_calibration` to compute the kappa that calibrates this
127
170
  gate ([D40](/DECISIONS.md), [calibration-method](/planning/evals/calibration-method.md)). If the PM
@@ -130,7 +173,9 @@ rubric, write a structured verdict to the `evals` table, and surface it on the r
130
173
 
131
174
  ## Output shape
132
175
 
133
- A short report: **most important finding first**, then VERDICT (score vs threshold) with the
134
- pass-fraction disclosure, the calibration exemplars used (step 3), the numbered defects, and the
135
- per-dimension scores **with their evidence quotes/pointers** — followed by the `save_eval` call. Be
136
- concrete; cite specifics; if it is genuinely solid, say so — but only after a real refutation attempt.
176
+ A short report: **most important finding first**, then the **two axis verdicts (Spec: pass/fail —
177
+ one line; Standards: pass/fail — one line)** and the **overall VERDICT** (score vs threshold, under
178
+ the combine rule) with the pass-fraction disclosure, the calibration exemplars used (step 3), the
179
+ numbered defects, and the per-dimension scores **with their evidence quotes/pointers and `axis`
180
+ tag** — followed by the `save_eval` call. Be concrete; cite specifics; if it is genuinely solid, say
181
+ so — but only after a real refutation attempt.