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/README.md +340 -1000
- package/package.json +2 -2
- package/payload/AGENTS.md +51 -152
- package/payload/KIT-VERSION +1 -1
- package/payload/scripts/anchor-resolve.js +25 -1
- package/payload/scripts/md-newlines.js +204 -0
- package/payload/scripts/md-normalize.js +223 -0
- package/workspace/lib/drive-guard.js +60 -18
- package/workspace/lib/engine/anchor-resolve.js +25 -1
- package/workspace/lib/md-normalize.js +223 -0
- package/workspace/lib/sink.js +321 -0
- package/workspace/package.json +3 -3
- package/workspace/public/index.html +4 -0
- package/workspace/public/pm/tab-doc.js +34 -3
- package/workspace/server.js +22 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold the PMOS portable kit
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
-
|
|
141
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
- **Watch the acceptance-friction meta-watermelon** — a run of zero-comment, fast acceptances is
|
|
213
|
-
|
|
214
|
-
- **
|
|
215
|
-
|
|
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.
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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 };
|