create-pmos 0.31.0 → 0.32.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.31.0",
4
- "description": "Scaffold the PMOS portable kit \u2014 run the PMOS product-management workflow (initiative \u2192 lane-fit spec \u2192 contract \u2192 build \u2192 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. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
3
+ "version": "0.32.0",
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. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "create-pmos": "bin/create-pmos.js"
package/payload/AGENTS.md CHANGED
@@ -6,23 +6,28 @@ timestamp: 2026-07-22
6
6
 
7
7
  # AGENTS.md — PMOS Portable Kit (file mode)
8
8
 
9
- > This file must be read at optimal altitude — concrete heuristics that guide behavior, not brittle
10
- > if-else rules engines, and not vague mandates that assume shared context.
9
+ > This file must be read at optimal altitude — concrete heuristics that guide behavior, not brittle if-else rules engines, and not vague mandates that assume shared context.
11
10
 
12
- You are working in a product repo that vendors the **PMOS portable kit** (D54). PMOS here is a
13
- **control plane** for how work is specced, evaluated, and accepted: you build the product, PMOS
14
- disciplines the loop.
11
+ You are working in a product repo that vendors the **PMOS portable kit** (D54). PMOS here is a **control plane** for how work is specced, evaluated, and accepted: you build the product, PMOS disciplines the loop.
15
12
 
16
- (PMOS itself is a control plane **and a factory** since D65 — but the factory half is not in this
17
- kit. What you have vendored is the governing half. If a future kit ships execution, it will arrive
18
- propose-only: opening pull requests, never holding push or merge rights.)
13
+ (PMOS itself is a control plane **and a factory** since D65 — but the factory half is not in this kit. What you have vendored is the governing half. If a future kit ships execution, it will arrive propose-only: opening pull requests, never holding push or merge rights.)
14
+
15
+ ## Writing markdown — one paragraph, one line
16
+
17
+ **Do not hard-wrap prose.** A paragraph is one line, a list item is one line, however long. Leave a blank line above a list, a table, a fence and a heading.
18
+
19
+ A newline inside a paragraph is invisible in markdown — every renderer joins the lines — and it is a **line break** in Jira wiki markup, in a Google Doc, and in the markdown→ADF converters an agent uses to publish. A wrapped spec arrives at your tracker as ragged half-lines, and nothing on the far side can undo it: an accidental wrap and a deliberate break are the same byte in the source.
20
+
21
+ ```bash
22
+ node pmos/scripts/md-newlines.js <path> # what is wrong, and where
23
+ node pmos/scripts/md-newlines.js --fix <path> # repair it — paragraphs only, never code or tables
24
+ ```
25
+
26
+ It refuses frontmatter, fenced and indented code, table rows, headings, deliberate two-space hard breaks and setext underlines, and it is idempotent: running it twice changes nothing the second time.
19
27
 
20
28
  ## The containment rule (non-negotiable)
21
29
 
22
- **This deployment is file-mode. Make NO network call to any PMOS backend — no MCP connection, no
23
- `get_skill`, no telemetry, nothing.** Every piece of PMOS state lives in this repo, under `pmos/`.
24
- This boundary exists because this product's data must not leave its own remote. If you find
25
- yourself wanting an MCP tool, use the state adapter below instead.
30
+ **This deployment is file-mode. Make NO network call to any PMOS backend — no MCP connection, no `get_skill`, no telemetry, nothing.** Every piece of PMOS state lives in this repo, under `pmos/`. This boundary exists because this product's data must not leave its own remote. If you find yourself wanting an MCP tool, use the state adapter below instead.
26
31
 
27
32
  ## The state adapter — every PMOS tool is a file convention
28
33
 
@@ -43,17 +48,11 @@ yourself wanting an MCP tool, use the state adapter below instead.
43
48
  | `log_friction` / `log_discovery` | append to `pmos/state/discoveries.md` (ISO date, `#friction` tag where applicable) |
44
49
  | `get_agent_runs` / `get_friction` | read those same files (your episodic memory — consult before contracting) |
45
50
 
46
- State files are **append-only**: never rewrite or delete an existing line or initiative record;
47
- correct by appending. When a vendored skill names a hosted tool, apply this table — the skill
48
- bodies are single-sourced from the PMOS kernel and are not forked for file mode.
51
+ State files are **append-only**: never rewrite or delete an existing line or initiative record; correct by appending. When a vendored skill names a hosted tool, apply this table — the skill bodies are single-sourced from the PMOS kernel and are not forked for file mode.
49
52
 
50
53
  ## Work-type lanes (D56)
51
54
 
52
- Every initiative carries a `type` — the lane picks what the gate requires **before** work; the
53
- kernel (anchor — a KR **or** `health:<id>` from `planning/okrs/health-budgets.md` (D59) —
54
- eval-before-work in lane-appropriate form, human Acceptance, append-only state)
55
- holds in every lane. Unknown/missing type = `feature`, the strictest lane. The enum is closed —
56
- never invent a lane; a work shape with no lane escalates to the PM.
55
+ Every initiative carries a `type` — the lane picks what the gate requires **before** work; the kernel (anchor — a KR **or** `health:<id>` from `planning/okrs/health-budgets.md` (D59) — eval-before-work in lane-appropriate form, human Acceptance, append-only state) holds in every lane. Unknown/missing type = `feature`, the strictest lane. The enum is closed — never invent a lane; a work shape with no lane escalates to the PM.
57
56
 
58
57
  | type | spec artifact the gate requires | eval form |
59
58
  |---|---|---|
@@ -64,165 +63,65 @@ never invent a lane; a work shape with no lane escalates to the PM.
64
63
  | `content` | content brief at `prd_path` (audience, message, claims + sources — `skills/copywriting.skill`, template in `okf/core/templates/`) | copy rubric recommended, not required (D58); PM judges at the gate otherwise |
65
64
  | `prototype` | quick prototype brief at `prd_path` (the judgement exercised + the specimen format — `skills/prototype.skill`, template in `okf/core/templates/`), **before** the PRD | **NO rubric, BY DESIGN** — grilled as dialogue, never scored (D60(d)); closes on a PM disposition: promote / iterate / kill |
66
65
 
67
- > **Known gap in this kit build:** `scripts/gate-initiative.sh` does not yet parameterise the
68
- > `prototype` lane — a prototype initiative falls through to the `feature` branch and is asked for a
69
- > `rubric_path` it must not have. Until that is fixed, set the lane's `type: prototype` in the
70
- > initiative file as normal and expect to carry the gate failure knowingly, or run the prototype
71
- > before opening the record. Tracked upstream; not a licence to invent a rubric for leg A.
72
-
73
- **Routing rule (D60, as amended 2026-07-24) — an idea the PM cannot judge from a DESCRIPTION goes to
74
- the `prototype` lane BEFORE anyone writes a PRD.** The entry rule is the **specimen test**: *can the
75
- PM judge this from a description, or do they need an instance of it in front of them?* It is **not
76
- gated on the work having a user interface** — *does this skill's output actually teach?* and *does
77
- this API feel right to call?* qualify exactly as an unseen screen does. The PM briefs; an agent
78
- produces **2–3 divergent specimens in whatever format fits** (lookable UI, a worked sample output,
79
- call/response transcripts, a schema sketch); a craft-grade grilling sharpens them as **dialogue,
80
- never a score**; a promote-disposition derives the feature-lane PRD + rubric from what was validated.
81
- The test must discriminate **both ways** — most work (a tool, a migration, a CI gate) *is*
82
- description-judgeable and routes straight to its PRD untaxed. Contrast `discovery`, which holds a
83
- **question** and delivers a **learning**; this lane holds a **candidate** and delivers a **validated
84
- direction**. Prototypes live in `prototype/` and never ship.
85
-
86
- `no-initiative` label = anchor-free trivia ONLY (a typo, a broken link). A chore, spike,
87
- incident, or piece of content is real work and takes its lane — that is what keeps the record
88
- honest.
66
+ > **Known gap in this kit build:** `scripts/gate-initiative.sh` does not yet parameterise the `prototype` lane — a prototype initiative falls through to the `feature` branch and is asked for a `rubric_path` it must not have. Until that is fixed, set the lane's `type: prototype` in the initiative file as normal and expect to carry the gate failure knowingly, or run the prototype before opening the record. Tracked upstream; not a licence to invent a rubric for leg A.
67
+
68
+ **Routing rule (D60, as amended 2026-07-24) — an idea the PM cannot judge from a DESCRIPTION goes to the `prototype` lane BEFORE anyone writes a PRD.** The entry rule is the **specimen test**: *can the PM judge this from a description, or do they need an instance of it in front of them?* It is **not gated on the work having a user interface** — *does this skill's output actually teach?* and *does this API feel right to call?* qualify exactly as an unseen screen does. The PM briefs; an agent produces **2–3 divergent specimens in whatever format fits** (lookable UI, a worked sample output, call/response transcripts, a schema sketch); a craft-grade grilling sharpens them as **dialogue, never a score**; a promote-disposition derives the feature-lane PRD + rubric from what was validated. The test must discriminate **both ways** — most work (a tool, a migration, a CI gate) *is* description-judgeable and routes straight to its PRD untaxed. Contrast `discovery`, which holds a **question** and delivers a **learning**; this lane holds a **candidate** and delivers a **validated direction**. Prototypes live in `prototype/` and never ship.
69
+
70
+ `no-initiative` label = anchor-free trivia ONLY (a typo, a broken link). A chore, spike, incident, or piece of content is real work and takes its lane — that is what keeps the record honest.
89
71
 
90
72
  ## Agent run protocol (every run, in order)
91
73
 
92
74
  1. Read this file.
93
- 2. Read the relevant `pmos/skills/*.skill` for the task type (L1 index first; body when relevant).
94
- No skill fits → escalate to the PM; do not improvise a procedure.
95
- 3. Retrieve context: `pmos/okf/` for concepts, `pmos/okf/product/` for this product's knowledge,
96
- `pmos/state/runs.jsonl` + `discoveries.md` for what prior runs tried (episodic memory).
97
- - **Existing surface → the PM needs the understanding too.** That retrieval warms *you*. When the
98
- work touches something that already exists — the test is *would specifying this honestly require
99
- reading code that already exists?* — run `skills/explain-surface.skill` on that region **before**
100
- the brief/PRD is authored, in a fresh context and **not** by the agent that will write it.
101
- Standard for existing surfaces, skipped for greenfield. Without it, the agent acquires the
102
- understanding and the **PM approves a spec built on facts they never saw**.
103
- 4. Anchor: the initiative file must exist at stage `intake`/`spec` with its **lane's** spec/eval
104
- artifacts authored **before** build (eval-first, D13, generalized per lane — see the lane table
105
- above; `feature` = PRD + rubric; the incident lane alone records post-hoc). Contract per lane:
106
- `feature` = the rubric's sprint-contract section with the PM's explicit `PM_approved: true`;
107
- `discovery` = the PM-approved spike brief (question + timebox); `chore` = the stated intent;
108
- `content` = the PM-approved content brief; `prototype` = the PM-approved quick brief (the
109
- judgement exercised + the specimen format) — and **no rubric**, which is the decision, not an
110
- omission. No work before the lane's approval. Feature-lane
111
- `test_method`s SHOULD be executable checks written before build (D58) — wire
112
- `templates/product-ci-starter.yml` so tier 1 is never silently empty.
75
+ 2. Read the relevant `pmos/skills/*.skill` for the task type (L1 index first; body when relevant). No skill fits → escalate to the PM; do not improvise a procedure.
76
+ 3. Retrieve context: `pmos/okf/` for concepts, `pmos/okf/product/` for this product's knowledge, `pmos/state/runs.jsonl` + `discoveries.md` for what prior runs tried (episodic memory).
77
+ - **Existing surface → the PM needs the understanding too.** That retrieval warms *you*. When the work touches something that already exists — the test is *would specifying this honestly require reading code that already exists?* — run `skills/explain-surface.skill` on that region **before** the brief/PRD is authored, in a fresh context and **not** by the agent that will write it. Standard for existing surfaces, skipped for greenfield. Without it, the agent acquires the understanding and the **PM approves a spec built on facts they never saw**.
78
+ 4. Anchor: the initiative file must exist at stage `intake`/`spec` with its **lane's** spec/eval artifacts authored **before** build (eval-first, D13, generalized per lane — see the lane table above; `feature` = PRD + rubric; the incident lane alone records post-hoc). Contract per lane: `feature` = the rubric's sprint-contract section with the PM's explicit `PM_approved: true`; `discovery` = the PM-approved spike brief (question + timebox); `chore` = the stated intent; `content` = the PM-approved content brief; `prototype` = the PM-approved quick brief (the judgement exercised + the specimen format) — and **no rubric**, which is the decision, not an omission. No work before the lane's approval. Feature-lane `test_method`s SHOULD be executable checks written before build (D58) — wire `templates/product-ci-starter.yml` so tier 1 is never silently empty.
113
79
  5. Branch `initiative/<id>`; execute at control altitude; one commit per artifact.
114
- 6. Self-evaluate against the contract — self-eval is unreliable; it is an input to the gates, never
115
- a verdict.
80
+ 6. Self-evaluate against the contract — self-eval is unreliable; it is an input to the gates, never a verdict.
116
81
  7. Close the gates: **Quality** = this repo's CI plus the kit gate (`pmos/scripts/gate-initiative.sh`
117
- + `pmos/planning/evals/check_rubrics.py`); **Review** = score against the rubric with the evaluator
118
- skill (adversarial, outcome-only, default-to-refute) and append to `evals.jsonl`; **Acceptance**
119
- = the human PM merges and disposes — final and non-delegable. Never merge your own PR.
82
+ + `pmos/planning/evals/check_rubrics.py`); **Review** = score against the rubric with the evaluator skill (adversarial, outcome-only, default-to-refute) and append to `evals.jsonl`; **Acceptance** = the human PM merges and disposes — final and non-delegable. Never merge your own PR.
120
83
  8. Log the run (`runs.jsonl`) and any friction (`discoveries.md`, `#friction`).
121
84
 
122
85
  ## Human builders & multi-repo initiatives (D58)
123
86
 
124
- - **A human building instead of an agent?** Same loop, plain terms: [AGENTS-HUMAN.md](AGENTS-HUMAN.md).
125
- State is written with `scripts/pmos-log.sh` (validated, append-only) — never hand-edited. Mixed
126
- human+agent teams write the same state through the same conventions.
127
- - **An initiative spanning multiple repos?** ONE repo is its **home** — the initiative record,
128
- brief/PRD, rubric, and state live there. Every sibling repo's PR anchors by declaring
129
- `Initiative: <id>` in the PR body (the gate's body-key path); records land in the home state
130
- layer. There is no cross-repo registry — the body key IS the join. **Sibling repos either do
131
- not install the pmos gate, or mirror the initiative file locally** — a vendored sibling gate
132
- fails closed on a missing local record by design (S5 Review Gate finding).
87
+ - **A human building instead of an agent?** Same loop, plain terms: [AGENTS-HUMAN.md](AGENTS-HUMAN.md). State is written with `scripts/pmos-log.sh` (validated, append-only) — never hand-edited. Mixed human+agent teams write the same state through the same conventions.
88
+ - **An initiative spanning multiple repos?** ONE repo is its **home** — the initiative record, brief/PRD, rubric, and state live there. Every sibling repo's PR anchors by declaring `Initiative: <id>` in the PR body (the gate's body-key path); records land in the home state layer. There is no cross-repo registry — the body key IS the join. **Sibling repos either do not install the pmos gate, or mirror the initiative file locally** — a vendored sibling gate fails closed on a missing local record by design (S5 Review Gate finding).
133
89
 
134
90
  ## Decomposing an initiative into tickets (file mode has no board)
135
91
 
136
- When an initiative is bigger than one run, break it into **tickets** — the plain-file equivalent of
137
- the hosted product's SDLC board. One file per ticket in `pmos/state/tickets/NNN-slug.md`, numbered
138
- in **dependency order**, from [templates/ticket-template.md](templates/ticket-template.md):
139
-
140
- - Frontmatter: `id` (the NNN), `title`, `status` (`todo | doing | done`), and `blocked_by` — an
141
- explicit list of the ticket ids that must be `done` first. Blocking edges are **text**, not a
142
- hidden graph.
143
- - **Sweep the state you derive from.** `node pmos/scripts/workflow-sweep.js` audits the records and
144
- the three ledgers everything else reads. Run it before a review and after any bulk edit. It reports
145
- **defects** (state that contradicts reality — a torn ledger line, a row naming no initiative, a
146
- duplicate `run_id`, a record pointing at a file nobody wrote) separately from **owed** work (a
147
- merged initiative with no run logged, an open Acceptance Gate). `--fix` repairs only the mechanical
148
- defects and writes no `.jsonl` and no judgement — closing your own loops is your job and the
149
- Acceptance Gate is yours alone (D12).
150
- - **Work the frontier.** `pmos/scripts/frontier.sh` lists exactly the tickets that are not done and
151
- whose every blocker is done — the workable set. Start only from there. If it reports the frontier
152
- is *blocked*, there is a cycle or an undone blocker to resolve before more work is startable.
153
- - Each ticket names **one deliverable**, behaviorally (interfaces/types/contracts, never file paths
154
- or line numbers — they stale while the ticket waits). Tickets decompose an initiative; they do not
155
- replace it — the initiative record, PRD, rubric, and the three gates are unchanged.
92
+ When an initiative is bigger than one run, break it into **tickets** — the plain-file equivalent of the hosted product's SDLC board. One file per ticket in `pmos/state/tickets/NNN-slug.md`, numbered in **dependency order**, from [templates/ticket-template.md](templates/ticket-template.md):
93
+
94
+ - Frontmatter: `id` (the NNN), `title`, `status` (`todo | doing | done`), and `blocked_by` — an explicit list of the ticket ids that must be `done` first. Blocking edges are **text**, not a hidden graph.
95
+ - **Sweep the state you derive from.** `node pmos/scripts/workflow-sweep.js` audits the records and the three ledgers everything else reads. Run it before a review and after any bulk edit. It reports **defects** (state that contradicts reality — a torn ledger line, a row naming no initiative, a duplicate `run_id`, a record pointing at a file nobody wrote) separately from **owed** work (a merged initiative with no run logged, an open Acceptance Gate). `--fix` repairs only the mechanical defects and writes no `.jsonl` and no judgement — closing your own loops is your job and the Acceptance Gate is yours alone (D12).
96
+ - **Work the frontier.** `pmos/scripts/frontier.sh` lists exactly the tickets that are not done and whose every blocker is done — the workable set. Start only from there. If it reports the frontier is *blocked*, there is a cycle or an undone blocker to resolve before more work is startable.
97
+ - Each ticket names **one deliverable**, behaviorally (interfaces/types/contracts, never file paths or line numbers — they stale while the ticket waits). Tickets decompose an initiative; they do not replace it — the initiative record, PRD, rubric, and the three gates are unchanged.
156
98
 
157
99
  ## New agent paths — the lethal-trifecta audit
158
100
 
159
- Before creating any new agent path or widening a tool grant, verify it does **not** hold all three
160
- of: **private data access + untrusted-content exposure + external communication**. An agent holding
161
- all three can be tricked by content it reads into exfiltrating what it knows — remove a leg
162
- (preferably external communication) structurally; prompt-level defenses alone are bypassed by
163
- adaptive attacks. If a path genuinely needs all three, its PRD/brief must name the compensating
164
- gate; silence is not a pass. The evaluator is the standing example: it reads adversarial diffs, so
165
- it stays read-only with its verdict append as the only write.
101
+ Before creating any new agent path or widening a tool grant, verify it does **not** hold all three of: **private data access + untrusted-content exposure + external communication**. An agent holding all three can be tricked by content it reads into exfiltrating what it knows — remove a leg (preferably external communication) structurally; prompt-level defenses alone are bypassed by adaptive attacks. If a path genuinely needs all three, its PRD/brief must name the compensating gate; silence is not a pass. The evaluator is the standing example: it reads adversarial diffs, so it stays read-only with its verdict append as the only write.
166
102
 
167
103
  ## Escalation (stop and ask the PM)
168
104
 
169
- An architectural choice with no recorded decision; a wrong or unsatisfiable contract; scope growth;
170
- any destructive/irreversible action; anything that would send product data outside this repo's
171
- remote. Confirm-first regardless of framing.
105
+ An architectural choice with no recorded decision; a wrong or unsatisfiable contract; scope growth; any destructive/irreversible action; anything that would send product data outside this repo's remote. Confirm-first regardless of framing.
172
106
 
173
107
  ## Evaluator leniency warning
174
108
 
175
- The evaluator defaults to generous scoring. Treat the Review Gate as **uncalibrated** in this
176
- deployment until this repo has accumulated its own PM-vs-evaluator record (append overrides to
177
- `pmos/planning/evals/eval-calibration.md`; the D40 classifier method applies locally).
178
- `pmos/scripts/metrics.py` computes the north star (human corrections per accepted run), pass rates,
179
- and the Goodhart tripwire from `pmos/state/`.
180
-
181
- **Model-awareness (any provider).** PMOS is provider/model-agnostic — any capable coding agent can
182
- run this loop — but calibration is a property of the **evaluator model**, not of "the Review Gate"
183
- abstractly. Record the model on every run/eval (the `model` / `evaluator_model` fields above); a
184
- model swap **forks** the calibration record — you do not inherit a κ another model earned, and
185
- `metrics.py` warns when labeled evals pool more than one evaluator model. The high-judgment roles
186
- (the adversarial evaluator, `explain-run`) assume a **frontier-tier reasoner**: a weaker model
187
- degrades *silently* to leniency and grep-clean while its output still looks well-formed — the
188
- per-model κ is what makes that degradation visible.
109
+ The evaluator defaults to generous scoring. Treat the Review Gate as **uncalibrated** in this deployment until this repo has accumulated its own PM-vs-evaluator record (append overrides to `pmos/planning/evals/eval-calibration.md`; the D40 classifier method applies locally). `pmos/scripts/metrics.py` computes the north star (human corrections per accepted run), pass rates, and the Goodhart tripwire from `pmos/state/`.
110
+
111
+ **Model-awareness (any provider).** PMOS is provider/model-agnostic — any capable coding agent can run this loop — but calibration is a property of the **evaluator model**, not of "the Review Gate" abstractly. Record the model on every run/eval (the `model` / `evaluator_model` fields above); a model swap **forks** the calibration record — you do not inherit a κ another model earned, and `metrics.py` warns when labeled evals pool more than one evaluator model. The high-judgment roles (the adversarial evaluator, `explain-run`) assume a **frontier-tier reasoner**: a weaker model degrades *silently* to leniency and grep-clean while its output still looks well-formed — the per-model κ is what makes that degradation visible.
189
112
 
190
113
  ## PM protocol (weekly)
191
114
 
192
- Scan `discoveries.md`; a friction theme seen twice with no resolving decision must be promoted to a
193
- decision. Read run records since last review; log overrides to `eval-calibration.md`. Run
194
- `pmos/scripts/metrics.py` and read the red first. **Scan the lane mix (D56):** count initiatives
195
- per `type` — a chore share climbing review over review, discovery spikes that never close their
196
- disposition, or `no-initiative` PRs that should have been chores are drift signals; a lane misused
197
- twice is promoted to a decision like any recurring friction. **Scan the anchor mix (D59):** feature-vs-health share of the period's initiatives; a budget over
198
- its cap or health-shaped work with a KR anchor is a drift signal; quarterly, review the caps.
199
- **Scan outcome checks (D57):**
200
- metrics.py lists due outcomes (accepted, past horizon, undisposed) — dispose each
201
- (`validated | refuted | inconclusive`, evidence for the first two) or consciously defer; a check
202
- deferred twice, or a horizon moved twice, is promoted like recurring friction. An accepted
203
- initiative with a declared outcome is NOT finished until disposed. **Read the run's explainer where
204
- one exists** — the `explain-run` skill writes an independently-authored literate explainer + quiz to
205
- `pmos/planning/evals/explainers/<run_id>-explainer.md`; start there instead of the raw transcript,
206
- and for a high-risk run pass its quiz as a comprehension self-check before you accept. Recommended
207
- discipline, not an enforced gate — accepting runs you cannot explain is the PM's own watermelon.
115
+ Scan `discoveries.md`; a friction theme seen twice with no resolving decision must be promoted to a decision. Read run records since last review; log overrides to `eval-calibration.md`. Run `pmos/scripts/metrics.py` and read the red first. **Scan the lane mix (D56):** count initiatives per `type` — a chore share climbing review over review, discovery spikes that never close their disposition, or `no-initiative` PRs that should have been chores are drift signals; a lane misused twice is promoted to a decision like any recurring friction. **Scan the anchor mix (D59):** feature-vs-health share of the period's initiatives; a budget over its cap or health-shaped work with a KR anchor is a drift signal; quarterly, review the caps. **Scan outcome checks (D57):** metrics.py lists due outcomes (accepted, past horizon, undisposed) — dispose each (`validated | refuted | inconclusive`, evidence for the first two) or consciously defer; a check deferred twice, or a horizon moved twice, is promoted like recurring friction. An accepted initiative with a declared outcome is NOT finished until disposed. **Read the run's explainer where one exists** — the `explain-run` skill writes an independently-authored literate explainer + quiz to `pmos/planning/evals/explainers/<run_id>-explainer.md`; start there instead of the raw transcript, and for a high-risk run pass its quiz as a comprehension self-check before you accept. Recommended discipline, not an enforced gate — accepting runs you cannot explain is the PM's own watermelon.
208
116
 
209
117
  **Anti-atrophy forcing functions (complacency is structural, not a willpower problem):**
210
- - **Name one hand-verified fact at every Acceptance** — the `record_acceptance` note states something
211
- *you* checked directly (ran it, read the hunk, reproduced it), not the evaluator's verdict restated.
212
- - **Watch the acceptance-friction meta-watermelon** — a run of zero-comment, fast acceptances is
213
- itself a flag; mandated oversight decays into rubber-stamping that legitimizes the system.
214
- - **Guard the four satisficing heuristics**: plan-as-proxy, tests-as-guarantee (CI-green ≠ correct —
215
- the Quality Gate is not the Acceptance Gate), eyeballing-suffices, deference-in-unfamiliar-domains.
216
- - **Adaptive autonomy** — drop an initiative to every-diff review on a failed eval, a reverted merge,
217
- a gate-touching change, or a subsystem you haven't personally touched in ~3 months.
218
- - **Monthly**, do one hands-on rep — personally diagnose one defect or re-derive one diff from its
219
- spec before reading the agent's version (diagnosis-shaped; comprehension decays faster than typing).
220
- **Quarterly**, ask *"what would I no longer know how to fix by hand?"* — supervision cost rises as
221
- runs improve.
118
+
119
+ - **Name one hand-verified fact at every Acceptance** — the `record_acceptance` note states something *you* checked directly (ran it, read the hunk, reproduced it), not the evaluator's verdict restated.
120
+ - **Watch the acceptance-friction meta-watermelon** — a run of zero-comment, fast acceptances is itself a flag; mandated oversight decays into rubber-stamping that legitimizes the system.
121
+ - **Guard the four satisficing heuristics**: plan-as-proxy, tests-as-guarantee (CI-green ≠ correct — the Quality Gate is not the Acceptance Gate), eyeballing-suffices, deference-in-unfamiliar-domains.
122
+ - **Adaptive autonomy** — drop an initiative to every-diff review on a failed eval, a reverted merge, a gate-touching change, or a subsystem you haven't personally touched in ~3 months.
123
+ - **Monthly**, do one hands-on rep — personally diagnose one defect or re-derive one diff from its spec before reading the agent's version (diagnosis-shaped; comprehension decays faster than typing). **Quarterly**, ask *"what would I no longer know how to fix by hand?"* — supervision cost rises as runs improve.
222
124
 
223
125
  ## Kit maintenance
224
126
 
225
- `pmos/KIT-VERSION` records the kernel source. Update the kernel with
226
- `pmos/scripts/kit-update.sh <path-to-pmos-checkout>` — it re-vendors ONLY `skills/`, `okf/core/`,
227
- `scripts/`, `AGENTS.md`, `README.md` and never touches `state/`, `planning/`, or `okf/product/`.
228
- Review the update as a normal PR.
127
+ `pmos/KIT-VERSION` records the kernel source. Update the kernel with `pmos/scripts/kit-update.sh <path-to-pmos-checkout>` — it re-vendors ONLY `skills/`, `okf/core/`, `scripts/`, `AGENTS.md`, `README.md` and never touches `state/`, `planning/`, or `okf/product/`. Review the update as a normal PR.
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@fd42042
1
+ kernel-source: pmos@6d5951a
2
2
  assembled: 2026-08-05
3
3
  mode: vendor
@@ -196,8 +196,31 @@ function readBudgets(root) {
196
196
  * `index` comes from parseOkrIndex; `budgets` from readBudgets (optional — omit to skip the
197
197
  * registry check, which callers that only need the FORM can do).
198
198
  */
199
+ /** Strip ONE layer of matching surrounding quotes.
200
+ *
201
+ * `parent_kr` is a YAML scalar, and a value whose prose contains a colon-space MUST be quoted or
202
+ * the document does not parse. The flat frontmatter reader hands the value through with its quotes
203
+ * intact, so a legitimately-quoted anchor arrived here as `"health:security …` and matched no form.
204
+ *
205
+ * This defect occurred TWICE — `sink-discipline-backport` on 2026-08-04 and
206
+ * `separation-invariant-enforcement` on 2026-08-05 — and the first time it was fixed by unquoting
207
+ * the one file. That is the wave's own lesson landing on the person who wrote it down: the unit of
208
+ * attention was the file, the unit of the defect was the rule. "Do not quote it" is a rule that
209
+ * lives in prose and depends on an author remembering; tolerating the quoting is a rule that lives
210
+ * in code.
211
+ *
212
+ * Only a MATCHING outer pair, and only one layer: a stray leading quote is still malformed, and
213
+ * reporting it as such is the point. */
214
+ function unquote(s) {
215
+ const t = s.trim();
216
+ if (t.length >= 2 && ((t[0] === '"' && t[t.length - 1] === '"') || (t[0] === "'" && t[t.length - 1] === "'"))) {
217
+ return t.slice(1, -1).trim();
218
+ }
219
+ return t;
220
+ }
221
+
199
222
  function resolveAnchor(raw, index, budgets) {
200
- const original = String(raw == null ? '' : raw).trim();
223
+ const original = unquote(String(raw == null ? '' : raw).trim());
201
224
  const base = { raw: original, kind: 'empty', status: 'empty', objective: null, kr: null, budget: null, period: null, form: null, reason: null };
202
225
  if (!original) return Object.assign(base, { reason: 'parent_kr is empty — unanchored work is rejected (D59)' });
203
226
 
@@ -269,6 +292,7 @@ function canonical(resolved, fallbackPeriod) {
269
292
  }
270
293
 
271
294
  module.exports = {
295
+ unquote,
272
296
  parseOkrIndex, readBudgets, resolveAnchor, canonical,
273
297
  objectiveId, krId, stripCommentary, listOkrFiles,
274
298
  ACCEPTED_FORMS,
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /*
4
+ * md-newlines — check, and repair, the newlines in PMOS markdown.
5
+ *
6
+ * THE DEFECT. This repository hard-wraps prose at ~100 columns: 30,345 continuation lines across
7
+ * 1,013 of 1,022 tracked files, a ruler written down nowhere and transmitted by imitation. A newline
8
+ * inside a paragraph is not a line break in markdown, so this looks right in the repo, on GitHub and
9
+ * in the workspace — and stops looking right the moment a file is published. Jira wiki markup treats
10
+ * a newline as a line break, a Google Doc keeps every newline, several markdown→ADF converters emit
11
+ * a hard break per `\n`. The paragraph arrives as a stack of ragged short lines, and nothing on the
12
+ * far side can undo it: an accidental wrap and a deliberate break are the same byte in the source.
13
+ *
14
+ * THE RULES ARE NOT HERE. They are in workspace/lib/md-normalize.js — one implementation shared with
15
+ * the workspace's `normalize` button (the browser loads the same file), so the command, the gate and
16
+ * the button cannot disagree about what a normalized document is. This file is argument handling,
17
+ * file walking and reporting.
18
+ *
19
+ * node scripts/md-newlines.js check every tracked .md
20
+ * node scripts/md-newlines.js <paths…> check just these
21
+ * node scripts/md-newlines.js --fix <paths…> repair them in place
22
+ * node scripts/md-newlines.js --fix --root <dir> repair another repository (an adopter's, a kit)
23
+ * node scripts/md-newlines.js --diff <paths…> show what --fix would change, change nothing
24
+ * node scripts/md-newlines.js --changed <ref> only the .md a diff touches — what CI runs
25
+ *
26
+ * WHY --changed EXISTS, AND WHY THERE IS NO REPO-WIDE GATE. A gate that failed on the existing
27
+ * corpus would fail every PR until a thousand files were rewritten, and would therefore be turned
28
+ * off — so CI ratchets on what the diff touches. New work is normalized from here; old work is
29
+ * repaired when someone touches it, or in one deliberate pass the PM chooses.
30
+ *
31
+ * Exit 0 = nothing to do (or --fix repaired it). Exit 1 = files need normalizing (check/--diff), or
32
+ * a write failed. Exit 2 = the invocation itself was wrong. Stdlib only, no network, no secrets.
33
+ */
34
+ const fs = require('fs');
35
+ const path = require('path');
36
+ const { execFileSync } = require('child_process');
37
+
38
+ /* The normalizer, from the checkout or from a flattened kit payload — the same two-path resolution
39
+ * console-state.js uses for the workflow engine. A kit ships scripts/ with the module beside it. */
40
+ function loadNormalizer() {
41
+ const tried = [];
42
+ for (const rel of ['../workspace/lib/md-normalize.js', './md-normalize.js']) {
43
+ const p = path.join(__dirname, rel);
44
+ tried.push(p);
45
+ if (fs.existsSync(p)) return require(p);
46
+ }
47
+ console.error('md-newlines: could not find md-normalize.js. Looked in:\n ' + tried.join('\n '));
48
+ process.exit(2);
49
+ return null;
50
+ }
51
+
52
+ function parseArgs(argv) {
53
+ const opts = { fix: false, diff: false, root: null, changed: null, paths: [] };
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const a = argv[i];
56
+ if (a === '--fix') opts.fix = true;
57
+ else if (a === '--diff') opts.diff = true;
58
+ else if (a === '--root') { opts.root = argv[++i]; if (!opts.root) return { error: '--root needs a directory' }; }
59
+ else if (a === '--changed') { opts.changed = argv[++i]; if (!opts.changed) return { error: '--changed needs a git ref' }; }
60
+ else if (a === '--help' || a === '-h') return { help: true };
61
+ else if (a.startsWith('-')) return { error: `unknown option '${a}'` };
62
+ else opts.paths.push(a);
63
+ }
64
+ return opts;
65
+ }
66
+
67
+ const USAGE = `md-newlines — check and repair the newlines in markdown documents
68
+
69
+ node scripts/md-newlines.js check every tracked .md
70
+ node scripts/md-newlines.js <paths…> check just these
71
+ node scripts/md-newlines.js --fix <paths…> repair them in place
72
+ node scripts/md-newlines.js --fix --root <dir> repair another repository
73
+ node scripts/md-newlines.js --diff <paths…> show what --fix would change, change nothing
74
+ node scripts/md-newlines.js --changed <ref> only the .md a diff touches (the CI ratchet)
75
+
76
+ A paragraph becomes one line, a list item becomes one line, and a list, table, fence or heading that
77
+ follows prose gets the blank line it needs. Frontmatter, fenced and indented code, table rows,
78
+ headings, deliberate hard breaks (two trailing spaces) and setext underlines are never touched.`;
79
+
80
+ /** Every tracked markdown file under `root`, from git — never a filesystem walk, so nothing untracked or vendored is touched. */
81
+ function trackedMarkdown(root) {
82
+ try {
83
+ return execFileSync('git', ['-C', root, 'ls-files', '*.md', '*.markdown', '*.mdown', '*.mkd'],
84
+ { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
85
+ .split('\n').filter(Boolean);
86
+ } catch (e) {
87
+ console.error(`md-newlines: could not list tracked files in ${root} (${(e && e.message) || e}).`);
88
+ console.error(' Pass explicit paths, or run inside a git repository.');
89
+ process.exit(2);
90
+ return [];
91
+ }
92
+ }
93
+
94
+ /** The markdown files a diff touches — added or modified, never deleted. */
95
+ function changedMarkdown(root, ref) {
96
+ let out;
97
+ try {
98
+ out = execFileSync('git', ['-C', root, 'diff', '--name-only', '--diff-filter=AM', `${ref}...HEAD`],
99
+ { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
100
+ } catch {
101
+ // A shallow clone has no merge base. Fall back to a two-dot diff rather than reporting nothing:
102
+ // an empty list here would print a green tick over a check that looked at no files at all.
103
+ try {
104
+ out = execFileSync('git', ['-C', root, 'diff', '--name-only', '--diff-filter=AM', ref],
105
+ { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
106
+ } catch (e) {
107
+ console.error(`md-newlines: could not diff against '${ref}' (${(e && e.message) || e}).`);
108
+ process.exit(2);
109
+ return [];
110
+ }
111
+ }
112
+ return out.split('\n').filter((f) => /\.(md|markdown|mdown|mkd)$/i.test(f));
113
+ }
114
+
115
+ /** A minimal unified-ish diff: the lines that differ, with their numbers. Enough to read, no deps. */
116
+ function showDiff(rel, before, after) {
117
+ const a = before.split('\n');
118
+ const b = after.split('\n');
119
+ const max = Math.max(a.length, b.length);
120
+ const rows = [];
121
+ for (let i = 0, j = 0; i < max || j < max;) {
122
+ if (a[i] === b[j]) { i++; j++; continue; }
123
+ // A join: one output line equals several input lines glued with spaces.
124
+ if (b[j] !== undefined && a[i] !== undefined && b[j].startsWith(a[i])) {
125
+ const from = i;
126
+ let glued = a[i];
127
+ while (glued !== b[j] && i + 1 < a.length && b[j].startsWith(glued)) { i++; glued += ' ' + a[i].trim(); }
128
+ rows.push(` ${from + 1}-${i + 1} joined → ${b[j].slice(0, 96)}${b[j].length > 96 ? '…' : ''}`);
129
+ i++; j++; continue;
130
+ }
131
+ if (b[j] === '') { rows.push(` ${j + 1} blank line inserted above: ${(b[j + 1] || '').slice(0, 72)}`); j++; continue; }
132
+ rows.push(` ${i + 1} - ${String(a[i]).slice(0, 96)}`);
133
+ rows.push(` ${j + 1} + ${String(b[j]).slice(0, 96)}`);
134
+ i++; j++;
135
+ }
136
+ if (rows.length) console.log(`\n${rel}`), console.log(rows.slice(0, 40).join('\n'));
137
+ if (rows.length > 40) console.log(` … ${rows.length - 40} more`);
138
+ }
139
+
140
+ function main() {
141
+ const opts = parseArgs(process.argv.slice(2));
142
+ if (opts.error) { console.error(`md-newlines: ${opts.error}\n\n${USAGE}`); process.exit(2); }
143
+ if (opts.help) { console.log(USAGE); process.exit(0); }
144
+
145
+ const { normalize } = loadNormalizer();
146
+ const root = path.resolve(opts.root || path.join(__dirname, '..'));
147
+ if (!fs.existsSync(root)) { console.error(`md-newlines: no such directory: ${root}`); process.exit(2); }
148
+
149
+ let files;
150
+ if (opts.paths.length) files = opts.paths.map((p) => path.relative(root, path.resolve(p)));
151
+ else if (opts.changed) files = changedMarkdown(root, opts.changed);
152
+ else files = trackedMarkdown(root);
153
+
154
+ const need = [];
155
+ let repaired = 0, joins = 0, blanks = 0, unreadable = 0;
156
+
157
+ for (const rel of files) {
158
+ const full = path.join(root, rel);
159
+ let src;
160
+ try { src = fs.readFileSync(full, 'utf8'); }
161
+ catch (e) {
162
+ // A file named on the command line that cannot be read is a FAILURE, not a silent skip: a
163
+ // repair tool that quietly passes over what it could not open reports success over nothing.
164
+ console.error(`✗ ${rel} — could not read: ${(e && e.message) || e}`);
165
+ unreadable++; continue;
166
+ }
167
+ const r = normalize(src);
168
+ if (!r.changed) continue;
169
+ need.push(rel);
170
+ joins += r.joins; blanks += r.blanks;
171
+ if (opts.diff) showDiff(rel, src, r.text);
172
+ if (opts.fix) {
173
+ try { fs.writeFileSync(full, r.text); repaired++; }
174
+ catch (e) { console.error(`✗ ${rel} — could not write: ${(e && e.message) || e}`); unreadable++; continue; }
175
+ console.log(`✓ ${rel} — ${r.joins} line(s) joined, ${r.blanks} blank line(s) inserted`);
176
+ } else if (!opts.diff) {
177
+ console.log(`✗ ${rel} — ${r.joins} wrapped line(s), ${r.blanks} missing blank line(s)`);
178
+ }
179
+ }
180
+
181
+ const scope = opts.changed ? `changed since ${opts.changed}` : (opts.paths.length ? 'named' : 'tracked');
182
+ if (unreadable) {
183
+ console.error(`\nmd-newlines: ${unreadable} file(s) could not be read or written.`);
184
+ process.exit(1);
185
+ }
186
+ if (!need.length) {
187
+ console.log(`✅ md-newlines: ${files.length} ${scope} markdown file(s) checked — every paragraph is already one line.`);
188
+ process.exit(0);
189
+ }
190
+ if (opts.fix) {
191
+ console.log(`\n✅ md-newlines: repaired ${repaired} of ${files.length} ${scope} file(s) — ${joins} line(s) joined, ${blanks} blank line(s) inserted.`);
192
+ process.exit(0);
193
+ }
194
+ console.log(`\n❌ md-newlines: ${need.length} of ${files.length} ${scope} markdown file(s) carry hard-wrapped prose`
195
+ + ` (${joins} wrapped line(s), ${blanks} missing blank line(s)).`);
196
+ console.log(' A newline inside a paragraph is invisible here and a line break in Jira, a Google Doc or any');
197
+ console.log(' plain-text destination. Repair them with:');
198
+ console.log(` node scripts/md-newlines.js --fix ${need.slice(0, 3).join(' ')}${need.length > 3 ? ' …' : ''}`);
199
+ console.log(' or inspect first with --diff. See planning/prd/markdown-newlines-prd.md.');
200
+ process.exit(1);
201
+ }
202
+
203
+ if (require.main === module) main();
204
+ module.exports = { parseArgs, trackedMarkdown, changedMarkdown };