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.
- package/README.md +149 -4
- package/bin/create-pmos.js +97 -8
- package/package.json +7 -3
- package/payload/AGENTS-HUMAN.md +6 -0
- package/payload/AGENTS.md +62 -3
- package/payload/KIT-VERSION +2 -2
- package/payload/README.md +16 -2
- package/payload/SECURITY.md +59 -0
- package/payload/okf/core/concepts/design-loop.md +6 -3
- package/payload/okf/core/concepts/frontmatter-format.md +75 -0
- package/payload/okf/core/concepts/index.md +3 -0
- package/payload/okf/core/concepts/log.md +13 -0
- package/payload/okf/core/templates/design-brief-template.md +1 -1
- package/payload/okf/core/templates/eval-rubric-template.md +12 -10
- package/payload/okf/core/templates/prd-template.md +6 -0
- package/payload/okf/core/templates/prototype-brief-template.md +45 -21
- package/payload/planning/evals/agent-run-eval-rubric.md +10 -7
- package/payload/scripts/frontier.sh +60 -0
- package/payload/scripts/metrics.py +163 -102
- package/payload/scripts/pmos-log.sh +16 -6
- package/payload/skills/SKILL-FORMAT.md +72 -9
- package/payload/skills/discovery.skill +4 -0
- package/payload/skills/eval-rubric.skill +1 -1
- package/payload/skills/evaluator.skill +65 -20
- package/payload/skills/explain-run.skill +9 -6
- package/payload/skills/explain-surface.skill +156 -0
- package/payload/skills/ingest-repo.skill +4 -3
- package/payload/skills/pmos-router.skill +25 -3
- package/payload/skills/prd.skill +8 -0
- package/payload/skills/prototype.skill +106 -41
- package/payload/skills-native/copywriting/SKILL.md +57 -0
- package/payload/skills-native/discovery/SKILL.md +75 -0
- package/payload/skills-native/eval-rubric/SKILL.md +159 -0
- package/payload/skills-native/evaluator/SKILL.md +182 -0
- package/payload/skills-native/explain-run/SKILL.md +121 -0
- package/payload/skills-native/explain-surface/SKILL.md +158 -0
- package/payload/skills-native/ingest-repo/SKILL.md +120 -0
- package/payload/skills-native/okr/SKILL.md +70 -0
- package/payload/skills-native/pmos-router/SKILL.md +81 -0
- package/payload/skills-native/prd/SKILL.md +117 -0
- package/payload/skills-native/prototype/SKILL.md +147 -0
- package/payload/state/tickets/.gitkeep +0 -0
- package/payload/templates/agent-run-eval-rubric.md +10 -7
- package/payload/templates/ticket-template.md +20 -0
- package/workspace/README.md +112 -0
- package/workspace/REMOTE.md +131 -0
- package/workspace/bin/pmos.js +206 -0
- package/workspace/doctor.js +99 -0
- package/workspace/lib/console-state.js +117 -0
- package/workspace/lib/file-mode-parse.js +58 -0
- package/workspace/lib/harness-parse.js +222 -0
- package/workspace/lib/preview-kind.js +98 -0
- package/workspace/lib/rate-limit.js +26 -0
- package/workspace/lib/remote-access.js +235 -0
- package/workspace/lib/review-gate.js +39 -0
- package/workspace/lib/schedule-logic.js +132 -0
- package/workspace/lib/skill-store.js +143 -0
- package/workspace/lib/surface-contract.js +79 -0
- package/workspace/lib/transcript-read.js +247 -0
- package/workspace/package-lock.json +94 -0
- package/workspace/package.json +23 -0
- package/workspace/public/app.js +1710 -0
- package/workspace/public/index.html +175 -0
- package/workspace/public/styles.css +766 -0
- package/workspace/server.js +1753 -0
- package/workspace/skills/backlog-triage.skill +13 -0
- 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
|
|
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
|
-
|
|
38
|
-
evals
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
for
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[`
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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` (
|
|
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
|
-
**
|
|
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.**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
123
|
-
quote/pointer from step 6 (or the string `unverified`)
|
|
124
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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.
|