create-pmos 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/bin/create-pmos.js +15 -3
  2. package/package.json +1 -1
  3. package/payload/KIT-VERSION +2 -2
  4. package/payload/okf/core/concepts/agent-harness.md +2 -2
  5. package/payload/okf/core/concepts/design-loop.md +5 -5
  6. package/payload/okf/core/concepts/eval-driven-pm.md +1 -1
  7. package/payload/okf/core/concepts/okr-anti-optimism.md +1 -1
  8. package/payload/okf/core/concepts/okr-tree.md +2 -2
  9. package/payload/okf/core/concepts/output-eval.md +2 -2
  10. package/payload/okf/core/concepts/sdlc-loop.md +1 -1
  11. package/payload/okf/core/concepts/self-refining-harness.md +3 -3
  12. package/payload/okf/core/templates/content-brief-template.md +1 -1
  13. package/payload/okf/core/templates/design-brief-template.md +1 -1
  14. package/payload/okf/core/templates/eval-rubric-template.md +1 -1
  15. package/payload/okf/core/templates/log.md +1 -1
  16. package/payload/okf/core/templates/prd-template.md +1 -1
  17. package/payload/okf/core/templates/spike-brief-template.md +1 -1
  18. package/payload/scripts/frontier.sh +6 -2
  19. package/payload/scripts/gate-initiative.sh +24 -3
  20. package/payload/scripts/metrics.py +20 -4
  21. package/payload/scripts/read-jsonl.js +53 -0
  22. package/payload/scripts/workflow-state.js +144 -35
  23. package/payload/scripts/workflow-sweep.js +39 -22
  24. package/payload/skills/SKILL-FORMAT.md +1 -1
  25. package/payload/skills/copywriting.skill +1 -1
  26. package/payload/skills/correction-signal-derive.skill +1 -1
  27. package/payload/skills/design-brief.skill +4 -4
  28. package/payload/skills/design-reconcile.skill +1 -1
  29. package/payload/skills/discovery.skill +3 -3
  30. package/payload/skills/eval-rubric.skill +4 -4
  31. package/payload/skills/evaluator.skill +1 -1
  32. package/payload/skills/explain-run.skill +1 -1
  33. package/payload/skills/explain-surface.skill +6 -6
  34. package/payload/skills/harness-refine.skill +1 -1
  35. package/payload/skills/ingest-repo.skill +6 -6
  36. package/payload/skills/okr.skill +2 -2
  37. package/payload/skills/pmos-router.skill +8 -8
  38. package/payload/skills/prd.skill +5 -5
  39. package/payload/skills/prototype-grill.skill +8 -8
  40. package/payload/skills/prototype.skill +7 -7
  41. package/payload/skills/skill-creator.skill +1 -1
  42. package/payload/skills/source-to-concept.skill +1 -1
  43. package/payload/skills/ste-term-map.md +69 -0
  44. package/payload/skills/strategic-brief.skill +4 -4
  45. package/payload/skills/technical-writing-ste.skill +176 -0
  46. package/payload/skills-native/copywriting/SKILL.md +1 -1
  47. package/payload/skills-native/correction-signal-derive/SKILL.md +1 -1
  48. package/payload/skills-native/design-brief/SKILL.md +4 -4
  49. package/payload/skills-native/design-reconcile/SKILL.md +1 -1
  50. package/payload/skills-native/discovery/SKILL.md +3 -3
  51. package/payload/skills-native/eval-rubric/SKILL.md +4 -4
  52. package/payload/skills-native/evaluator/SKILL.md +1 -1
  53. package/payload/skills-native/explain-run/SKILL.md +1 -1
  54. package/payload/skills-native/explain-surface/SKILL.md +6 -6
  55. package/payload/skills-native/harness-refine/SKILL.md +1 -1
  56. package/payload/skills-native/ingest-repo/SKILL.md +6 -6
  57. package/payload/skills-native/okr/SKILL.md +2 -2
  58. package/payload/skills-native/pmos-router/SKILL.md +8 -8
  59. package/payload/skills-native/prd/SKILL.md +5 -5
  60. package/payload/skills-native/prototype/SKILL.md +7 -7
  61. package/payload/skills-native/prototype-grill/SKILL.md +8 -8
  62. package/payload/skills-native/skill-creator/SKILL.md +1 -1
  63. package/payload/skills-native/source-to-concept/SKILL.md +1 -1
  64. package/payload/skills-native/strategic-brief/SKILL.md +4 -4
  65. package/payload/skills-native/technical-writing-ste/SKILL.md +178 -0
  66. package/workspace/bin/pmos.js +9 -3
  67. package/workspace/lib/engine/read-jsonl.js +53 -0
  68. package/workspace/lib/engine/workflow-state.js +144 -35
  69. package/workspace/lib/file-mode-parse.js +19 -4
  70. package/workspace/lib/tree-share.js +69 -1
  71. package/workspace/package-lock.json +2 -2
  72. package/workspace/package.json +1 -1
  73. package/workspace/public/pm/board-read.js +38 -0
  74. package/workspace/public/pm/data.js +102 -1
  75. package/workspace/public/pm/modes.js +50 -28
  76. package/workspace/public/pm/panel-detail.js +2 -17
  77. package/workspace/public/pm/panel-initiatives.js +3 -20
  78. package/workspace/public/pm/panel-newsess.js +14 -1
  79. package/workspace/public/pm/panel-tabs.js +53 -14
  80. package/workspace/public/pm/tab-term.js +30 -2
  81. package/workspace/server.js +95 -70
  82. package/workspace/test-run.js +51 -9
@@ -129,15 +129,27 @@ function workspaceOrDie() {
129
129
  }
130
130
  }
131
131
 
132
+ // A spawn that never RAN is a failure, not a success: spawnSync's .status is null when the child
133
+ // could not be spawned (r.error set — e.g. a broken global `pmos` shim after a partial uninstall;
134
+ // onPath only proves resolvability, not runnability) or died on a signal. The old `.status || 0`
135
+ // coerced both to exit 0, so a launch that launched nothing reported success to whatever invoked it.
136
+ function launchStatus(r) {
137
+ if (r.error) {
138
+ console.error(`create-pmos: ERROR — launch failed: ${r.error.message}`);
139
+ return 1;
140
+ }
141
+ return r.status === null ? 1 : r.status;
142
+ }
143
+
132
144
  function launchWorkspace(targetDir, viaGlobal) {
133
145
  if (viaGlobal && onPath('pmos')) {
134
146
  // `pmos` owns the whole lifecycle: start the server detached, open the browser on this repo.
135
- return spawnSync('pmos', [targetDir], { stdio: 'inherit', shell: process.platform === 'win32' }).status || 0;
147
+ return launchStatus(spawnSync('pmos', [targetDir], { stdio: 'inherit', shell: process.platform === 'win32' }));
136
148
  }
137
149
  // Fallback: run the bundled launcher directly. Its ensureInstalled() pulls runtime deps on first run.
138
150
  console.log('Launching from the bundled copy (the `pmos` command is not on PATH).');
139
- return spawnSync(process.execPath, [path.join(WORKSPACE, 'bin', 'pmos.js'), targetDir],
140
- { stdio: 'inherit', env: process.env }).status || 0;
151
+ return launchStatus(spawnSync(process.execPath, [path.join(WORKSPACE, 'bin', 'pmos.js'), targetDir],
152
+ { stdio: 'inherit', env: process.env }));
141
153
  }
142
154
 
143
155
  // Compare two dotted numeric versions. Returns -1 if a<b, 0 if equal, 1 if a>b. Missing/non-numeric
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. 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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@1313507
2
- assembled: 2026-08-07
1
+ kernel-source: pmos@f04854e
2
+ assembled: 2026-08-08
3
3
  mode: vendor
@@ -24,12 +24,12 @@ The harness includes:
24
24
  - **Knowledge** — the [OKF bundle](/okf/core/concepts/okf-governance.md): concepts, templates,
25
25
  playbooks.
26
26
  - **Skills** — procedural competence loaded on demand
27
- ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)).
27
+ ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)).
28
28
  - **Tools / MCP** — the [ACI surface](/build-skills/MCP-SPEC.md) the agent acts through.
29
29
  - **Sandboxes** — where work runs safely.
30
30
  - **Orchestration** — sequencing, concurrency, routing.
31
31
  - **Guardrails** — the [three-tier eval gate](/okf/core/concepts/output-eval.md), kill conditions.
32
- - **Observability** — `agent_runs`, [discoveries.md](/discoveries.md), transcript review.
32
+ - **Observability** — `agent_runs`, [discoveries.md](/state/discoveries.md), transcript review.
33
33
 
34
34
  **PMOS builds the harness, not the app.** Improving any harness element improves every future run —
35
35
  that is the leverage the [control plane](/okf/core/concepts/control-plane.md) exists to capture.
@@ -29,12 +29,12 @@ An initiative with a user-facing surface runs four legs *inside* Build:
29
29
  build, web-ci) library back)
30
30
  ```
31
31
 
32
- 1. **Brief** — the [design-brief skill](/skills/core/design-brief.skill) writes the context handoff: what
32
+ 1. **Brief** — the [design-brief skill](/skills/design-brief.skill) writes the context handoff: what
33
33
  to design and the hard, presentation-only constraints, each surface tied to a real data source. It is to
34
- design what the [PRD](/skills/core/prd.skill) is to build.
34
+ design what the [PRD](/skills/prd.skill) is to build.
35
35
  2. **Design** — the design is made in an external design surface (PMOS-self uses claude.ai/design;
36
36
  any capable design surface fits — external to this repo).
37
- 3. **Reconcile + build** — the [design-reconcile skill](/skills/core/design-reconcile.skill) classifies
37
+ 3. **Reconcile + build** — the [design-reconcile skill](/skills/design-reconcile.skill) classifies
38
38
  every surface against **the product's capability ledger** and the PM signs off before any code; then the
39
39
  design is built into the product's app on its design system, verified by the product's CI. (For PMOS's own
40
40
  app the ledger is [web-capability-ledger.md](/planning/web-capability-ledger.md), the system is D38, and
@@ -98,8 +98,8 @@ time such that the board hides real "designed-but-not-built" state, the lighter
98
98
 
99
99
  The loop is a **method**; PMOS's own app is just its first instance. What is **shared** vs **per-product** (D50):
100
100
 
101
- - **Shared, authored once, read by every PM** — this concept, the [design-brief](/skills/core/design-brief.skill)
102
- and [design-reconcile](/skills/core/design-reconcile.skill) skills, the
101
+ - **Shared, authored once, read by every PM** — this concept, the [design-brief](/skills/design-brief.skill)
102
+ and [design-reconcile](/skills/design-reconcile.skill) skills, the
103
103
  [design-brief template](/okf/core/templates/design-brief-template.md), and the **generic dimensions** of the
104
104
  [design-fidelity rubric](/planning/evals/design-fidelity-eval-rubric.md). These live in PMOS-self's knowledge
105
105
  and are already reachable by tenants (repo-managed OKF rows are shared-read, D37; the `skills` table is
@@ -52,7 +52,7 @@ this the right thing?* — and on whether the rubric itself was right.
52
52
  ## How it shapes the loop
53
53
 
54
54
  - **Spec stage** authors the rubric alongside the PRD — the
55
- [eval-rubric skill](/skills/core/eval-rubric.skill) and [prd skill](/skills/core/prd.skill) are
55
+ [eval-rubric skill](/skills/eval-rubric.skill) and [prd skill](/skills/prd.skill) are
56
56
  paired for this reason.
57
57
  - **Contract stage** turns the rubric into agreed, testable done-criteria the generator and evaluator
58
58
  both sign off on before work starts.
@@ -36,7 +36,7 @@ report the more pessimistic reading and make the uncertainty explicit.
36
36
  4. **Name the gap to intent.** Where the work has bent away from the parent objective, say so
37
37
  explicitly, even if every individual run passed.
38
38
  5. **Surface friction.** Anything that fought the harness goes to
39
- [discoveries.md](/discoveries.md) tagged `#friction`, not buried in a success summary.
39
+ [discoveries.md](/state/discoveries.md) tagged `#friction`, not buried in a success summary.
40
40
 
41
41
  ## Why this is structural, not cultural
42
42
 
@@ -29,8 +29,8 @@ Objective (qualitative, directional — the "what we want to be true")
29
29
  - **Key Result** — a quantitative, measurable outcome that proves the objective is being met.
30
30
  Each objective has 2–5 KRs. *"Human correction rate per agent run < 0.5 by end of Q3."*
31
31
  - **Initiative** — a concrete body of work expected to move one or more KRs. Each initiative owns a
32
- PRD ([prd skill](/skills/core/prd.skill)) and an eval rubric
33
- ([eval-rubric skill](/skills/core/eval-rubric.skill)).
32
+ PRD ([prd skill](/skills/prd.skill)) and an eval rubric
33
+ ([eval-rubric skill](/skills/eval-rubric.skill)).
34
34
  - **Agent run** — the leaf: one execution against an initiative's pre-run contract, logged to
35
35
  `agent_runs`.
36
36
 
@@ -10,7 +10,7 @@ timestamp: 2026-06-28
10
10
 
11
11
  **Situating context:** Every turn of the [SDLC loop](/okf/core/concepts/sdlc-loop.md) ends by
12
12
  judging what an agent produced. This concept defines the three-tier gate that judgment runs through,
13
- so the [eval-rubric skill](/skills/core/eval-rubric.skill), AGENTS.md, and the PM all use the same
13
+ so the [eval-rubric skill](/skills/eval-rubric.skill), AGENTS.md, and the PM all use the same
14
14
  vocabulary. It informs where automation stops and human judgment begins.
15
15
 
16
16
  ## The three tiers
@@ -72,7 +72,7 @@ green Review Gate auto-accept — removes the only structural defense against dr
72
72
  call sequences.
73
73
  - **Weighted, with partial credit:** the Review Gate scores weighted dimensions and totals against a
74
74
  threshold; it is not all-or-nothing. (See the
75
- [eval-rubric skill](/skills/core/eval-rubric.skill) and
75
+ [eval-rubric skill](/skills/eval-rubric.skill) and
76
76
  [template](/okf/core/templates/eval-rubric-template.md).)
77
77
 
78
78
  ## Relationship to autonomy
@@ -26,7 +26,7 @@ PMOS drives a closed lifecycle. Each turn of the loop moves one initiative throu
26
26
  Gate)
27
27
  ```
28
28
 
29
- 1. **Spec** — the initiative gets a PRD via the [prd skill](/skills/core/prd.skill). The PRD says
29
+ 1. **Spec** — the initiative gets a PRD via the [prd skill](/skills/prd.skill). The PRD says
30
30
  what to build and why, anchored to a KR in the [OKR tree](/okf/core/concepts/okr-tree.md) —
31
31
  or, for maintenance work, a declared [health budget](/okf/core/concepts/health-anchors.md) (D59).
32
32
  2. **Contract** — generator and evaluator agree a **pre-run contract**: testable done-criteria,
@@ -14,7 +14,7 @@ transcripts, `eval-calibration.md`). What it lacked was a named pattern — and
14
14
  that evidence back into harness improvements. This concept names it. It was extracted from the
15
15
  2026-07-01 "Agent = Model + Harness" research (which found the field converging on *harness engineering*
16
16
  as its own discipline, and "agents improving their own tools" as its highest-leverage loop) and it
17
- informs the [harness-refine skill](/build-skills/harness-refine.skill), [D47](/okf/products/pmos/adr/d47-self-refining-harness.md),
17
+ informs the [harness-refine skill](/skills/harness-refine.skill), [D47](/okf/products/pmos/adr/d47-self-refining-harness.md),
18
18
  and the [D23](/okf/products/pmos/adr/d23-quarterly-cadence.md) quarterly harness review.
19
19
 
20
20
  ## The pattern
@@ -36,7 +36,7 @@ one level up — applied to the harness itself:
36
36
  1. **Detect** — a deterministic signal shows a gap: a `v_friction_recurrence` theme with no resolving
37
37
  decision, a recurring `evals` defect class, a persistent `eval-calibration.md` disagreement, a
38
38
  context-budget signal.
39
- 2. **Draft** — the [harness-refine skill](/build-skills/harness-refine.skill) reads that evidence and
39
+ 2. **Draft** — the [harness-refine skill](/skills/harness-refine.skill) reads that evidence and
40
40
  drafts concrete, **evidence-cited** edits, each mapped to one artifact: an MCP tool description
41
41
  ([ACI](/build-skills/MCP-SPEC.md), D19), a skill body, an OKF concept, or an `AGENTS.md` heuristic.
42
42
  It **prunes** as readily as it adds (D23 / [prune the scaffolding](/PRINCIPLES.md)).
@@ -73,5 +73,5 @@ sources.
73
73
  - [control-plane](/okf/core/concepts/control-plane.md) — why PMOS edits the harness, not the app.
74
74
  - [output-eval](/okf/core/concepts/output-eval.md) — the Acceptance Gate this loop submits to.
75
75
  - [watermelon-flag](/okf/core/concepts/watermelon-flag.md) — the L4 failure mode the L3 bounds prevent.
76
- - Skill: [harness-refine](/build-skills/harness-refine.skill). Decision:
76
+ - Skill: [harness-refine](/skills/harness-refine.skill). Decision:
77
77
  [D47](/okf/products/pmos/adr/d47-self-refining-harness.md).
@@ -10,7 +10,7 @@ timestamp: 2026-07-18
10
10
 
11
11
  **Situating context:** Authored with [D58](/okf/products/pmos/adr/d58-builder-profiles.md) as the
12
12
  content lane's spec artifact — the contract copy work is drafted and judged against. The
13
- [copywriting skill](/skills/domains/copywriting.skill) instantiates it. A rubric is recommended,
13
+ [copywriting skill](/skills/copywriting.skill) instantiates it. A rubric is recommended,
14
14
  not required (PM call at D58 ratification); the optional section below is the recommended shape.
15
15
 
16
16
  > Usage: replace every `<…>` placeholder. An empty audience or an unsourced claim list means the
@@ -9,7 +9,7 @@ timestamp: 2026-07-01
9
9
  # Design Brief Template
10
10
 
11
11
  **Situating context:** This template is the canonical scaffold the
12
- [design-brief skill](/skills/core/design-brief.skill) instantiates — leg 1 of the
12
+ [design-brief skill](/skills/design-brief.skill) instantiates — leg 1 of the
13
13
  [design loop](/okf/core/concepts/design-loop.md). It is to design what the
14
14
  [PRD template](/okf/core/templates/prd-template.md) is to build: the shared definition before work starts.
15
15
  Copy the block into `/planning/design/<slug>-design-brief.md` and fill it; the copy is an operational
@@ -9,7 +9,7 @@ timestamp: 2026-06-28
9
9
  # Eval Rubric Template
10
10
 
11
11
  **Situating context:** This template was authored as the canonical scaffold the
12
- [eval-rubric skill](/skills/core/eval-rubric.skill) instantiates, so every rubric uses the same
12
+ [eval-rubric skill](/skills/eval-rubric.skill) instantiates, so every rubric uses the same
13
13
  weighted-scoring math and carries its sprint contract. Copy the block into
14
14
  `/planning/evals/<slug>-eval-rubric.md` and fill it; the copy is operational and does not live in the
15
15
  OKF bundle.
@@ -3,7 +3,7 @@
3
3
  OKF reserved file (no frontmatter). Change history, newest first, ISO 8601 dates.
4
4
 
5
5
  ## 2026-07-01
6
- - Added `design-brief-template` — the design-brief scaffold (leg 1 of the design loop, D48), instantiated by the [design-brief skill](/skills/core/design-brief.skill). Distilled from the first instance, `planning/design-brief.md`.
6
+ - Added `design-brief-template` — the design-brief scaffold (leg 1 of the design loop, D48), instantiated by the [design-brief skill](/skills/design-brief.skill). Distilled from the first instance, `planning/design-brief.md`.
7
7
 
8
8
  ## 2026-06-29
9
9
  - Creation — per-folder index/log added (OKF recursive progressive disclosure; D29 reversal direction).
@@ -9,7 +9,7 @@ timestamp: 2026-06-28
9
9
  # PRD Template
10
10
 
11
11
  **Situating context:** This template was authored as the canonical scaffold the
12
- [prd skill](/skills/core/prd.skill) instantiates, so every PRD in PMOS has the same shape and the
12
+ [prd skill](/skills/prd.skill) instantiates, so every PRD in PMOS has the same shape and the
13
13
  same anchoring discipline. Copy the block below into `/planning/prd/<slug>-prd.md` and fill it; the
14
14
  copy is an operational instance and does not live in the OKF bundle.
15
15
 
@@ -11,7 +11,7 @@ timestamp: 2026-07-17
11
11
  **Situating context:** Authored with [D56](/okf/products/pmos/adr/d56-initiative-lanes.md) as the
12
12
  discovery lane's spec artifact, resolving the eval-first paradox the SDLC stress test named: a
13
13
  prototype exists to *discover* its own done-criteria, so the lane contracts on a **question and a
14
- timebox**, not on requirements. The [discovery skill](/skills/core/discovery.skill) instantiates
14
+ timebox**, not on requirements. The [discovery skill](/skills/discovery.skill) instantiates
15
15
  this template. Copy the block below into `/planning/prd/<id>-spike.md` and fill; the copy is an
16
16
  operational instance, not OKF knowledge.
17
17
 
@@ -18,8 +18,12 @@ DIR="${1:-state/tickets}"
18
18
  # --- field readers (frontmatter only: stop at the closing --- fence) ----------------------------
19
19
  fm() { awk 'NR>1 && /^---[[:space:]]*$/{exit} {print}' "$1"; } # lines between the opening and closing ---
20
20
  field() { printf '%s\n' "$1" | sed -n "s/^$2:[[:space:]]*//p" | head -1 | sed 's/^["'\'']//;s/["'\'']$//;s/[[:space:]]*$//'; }
21
- # blocked_by is an inline list "[\"002\", \"005\"]"; extract the bare ids (digits) in order.
22
- blockers() { printf '%s\n' "$1" | sed -n 's/^blocked_by:[[:space:]]*//p' | head -1 | grep -oE '[0-9]+' || true; }
21
+ # blocked_by is an inline list "[\"002\", \"005\"]"; extract the bare ids in order. Ids are NOT
22
+ # assumed numeric — idof below derives slug ids (an explicit `id:` field, or a digitless filename),
23
+ # and a digits-only extraction silently DROPPED every such blocker, listing a blocked ticket as
24
+ # workable (fail-open). Every non-empty token survives instead: a token that matches no done ticket
25
+ # keeps the ticket off the frontier, so a malformed or unknown blocker fails CLOSED.
26
+ blockers() { printf '%s\n' "$1" | sed -n 's/^blocked_by:[[:space:]]*//p' | head -1 | sed 's/[][]//g' | tr ',' '\n' | sed 's/["'\'']//g' | tr -d ' \t\r' | grep -v '^$' || true; }
23
27
  # derive an id: explicit `id:` field, else the leading NNN of the filename.
24
28
  idof() { local i; i="$(field "$1" id)"; [ -n "$i" ] && { printf '%s' "$i"; return; }; basename "$2" | grep -oE '^[0-9]+' || basename "$2" .md; }
25
29
 
@@ -49,9 +49,30 @@ printf '%s' "$id" | grep -qE '^[a-z0-9][a-z0-9._-]*$' || fail "Initiative id '$i
49
49
  init_file="$KIT_ROOT/state/initiatives/$id.md"
50
50
  [ -f "$init_file" ] || fail "No initiative file at $init_file. Create it first — an initiative must exist before its PR. This is the loop rule the gate enforces."
51
51
 
52
- frontval() { # frontval <key> — first 'key: value' line in the file, value trimmed.
53
- # `|| true`: an absent key must fall through to the explicit fail() below, not abort silently.
54
- { grep -m1 -E "^$1:[[:space:]]*" "$init_file" || true; } | sed -E "s/^$1:[[:space:]]*//" | tr -d '\r' | sed -E 's/[[:space:]]+$//'
52
+ frontval() { # frontval <key> — the key's value from the LEADING frontmatter block, trimmed.
53
+ # The LEADING block only, mirroring workflow-state.js / file-mode-parse.js / metrics.py: the gate
54
+ # used to grep the whole file, so a body line starting `parent_kr: ...` (a pasted example) could
55
+ # fake the declaration the gate exists to check. Same rules as the canonical parsers: an opening
56
+ # --- fence (before any key) is skipped and the next --- ends the block; blank lines are skipped;
57
+ # the first non-key line ends the block (fenceless records, e.g. the kit-ci smoke fixture, keep
58
+ # parsing from line 1); first value wins; ONE pair of surrounding double quotes is stripped. An
59
+ # absent key prints nothing and falls through to the explicit fail() below.
60
+ awk -v want="$1" '
61
+ /^---/ { if (fence || seen) exit; fence = 1; next }
62
+ /^[[:space:]]*$/ { next }
63
+ {
64
+ i = index($0, ":")
65
+ key = (i > 0) ? substr($0, 1, i - 1) : ""
66
+ if (key !~ /^[a-z_]+$/) exit
67
+ seen = 1
68
+ if (key != want) next
69
+ val = substr($0, i + 1)
70
+ gsub(/\r/, "", val)
71
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
72
+ if (length(val) >= 2 && val ~ /^".*"$/) val = substr(val, 2, length(val) - 2)
73
+ print val
74
+ exit
75
+ }' "$init_file"
55
76
  }
56
77
  prd="$(frontval prd_path)"
57
78
  rubric="$(frontval rubric_path)"
@@ -56,14 +56,30 @@ def leading_frontmatter(path):
56
56
  body line, the loop breaks, and every key below it is dropped. Agreement with the JS parser was
57
57
  never sufficient to catch this — both carried the same wrong regex, so the fixture compared two
58
58
  identical mistakes and passed. See kit/fixtures/state/initiatives/epsilon-empty-value.md.
59
+
60
+ The closing --- fence is TERMINAL, never skipped: stepping over it absorbed a key-shaped first
61
+ body line into the block (zeta-fence-fake.md pins this). Fenceless files parse from line 1 and
62
+ stop at the first non-key line. First value wins, and ONE pair of surrounding double quotes is
63
+ stripped, exactly as the JS parsers do (eta-quoted-value.md pins the quoted case).
59
64
  """
60
65
  fm = {}
66
+ fence_open = False
61
67
  with open(path, encoding="utf-8") as fh:
62
- for line in fh:
63
- m = re.match(r"^([a-z_]+):\s*(.*?)\s*$", line.rstrip("\n"))
68
+ for raw in fh:
69
+ line = raw.rstrip("\n")
70
+ if line.startswith("---"):
71
+ # Only the FIRST ---, before any key, opens the block; every later --- ends it.
72
+ if fence_open or fm:
73
+ break
74
+ fence_open = True
75
+ continue
76
+ m = re.match(r"^([a-z_]+):\s*(.*?)\s*$", line)
64
77
  if m:
65
- fm.setdefault(m.group(1), m.group(2))
66
- elif line.strip() and not line.startswith("---"):
78
+ val = m.group(2)
79
+ if len(val) >= 2 and val.startswith('"') and val.endswith('"'):
80
+ val = val[1:-1]
81
+ fm.setdefault(m.group(1), val)
82
+ elif line.strip():
67
83
  break # frontmatter is the LEADING block only
68
84
  return fm
69
85
 
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+ /*
3
+ * scripts/read-jsonl.js — THE JSONL reader for the scripts/ checkers and the workflow engine.
4
+ *
5
+ * Six near-identical readers grew here, with three different torn-line contracts, and the repo's
6
+ * own comments record the cost twice: a silently-dropped torn line re-opened a verified entry
7
+ * (check-verify-queue.js) and made an accepted initiative read as never accepted (workflow-state.js).
8
+ * Each new checker re-decided the torn-line question from scratch and got it wrong at least twice.
9
+ * One reader, one contract — the strictest one found:
10
+ *
11
+ * - a torn line is DROPPED, never guessed at — a truncated row parsed "as far as it goes" would
12
+ * fabricate a record nobody wrote;
13
+ * - and it is REPORTED, never swallowed: every defect comes back with its file, 1-based line,
14
+ * parse reason and a bounded slice of the text, so the caller can say out loud what it could
15
+ * not read. "Assuming someone else counts is how nobody counts."
16
+ * - a MISSING (or unreadable) file is its own state, distinguishable from an empty one — the
17
+ * cadence checker needs the difference and a gate must not confuse "no ledger" with "no rows".
18
+ *
19
+ * Returns { rows, defects, torn, missing }: rows are the parsed objects in file order; defects is
20
+ * [{ file, line, reason, text }]; torn === defects.length, kept as a count because most callers
21
+ * only need the number; missing is true when the file could not be read at all (rows/defects empty).
22
+ *
23
+ * DELIBERATELY FLAT in scripts/, not scripts/lib/: the kit and the workspace bundle both vendor the
24
+ * engine's require closure by BASENAME into a flat directory (kit/build-kit.sh, scripts/
25
+ * vendor-workspace-engine.js), so a `./lib/` require would resolve in the checkout and break in
26
+ * every vendored copy — the exact incident class the scripts/lib/sink.js re-export note records.
27
+ * workspace/lib/file-mode-parse.js does NOT delegate here for the mirrored reason: the workspace
28
+ * bundle ships alone, cannot require across the bundle boundary, and its silent-drop reader is the
29
+ * locked mirror of kit/scripts/metrics.py (kit/fixtures/test_parsers.py holds that agreement).
30
+ *
31
+ * Zero dependencies, stock Node.
32
+ */
33
+ const fs = require('fs');
34
+
35
+ function readJsonl(file) {
36
+ let text;
37
+ try { text = fs.readFileSync(file, 'utf8'); }
38
+ catch { return { rows: [], defects: [], torn: 0, missing: true }; }
39
+ const rows = [];
40
+ const defects = [];
41
+ const lines = text.split('\n');
42
+ for (let i = 0; i < lines.length; i++) {
43
+ const t = lines[i].trim();
44
+ if (!t) continue;
45
+ try { rows.push(JSON.parse(t)); }
46
+ catch (e) {
47
+ defects.push({ file, line: i + 1, reason: String((e && e.message) || 'not valid JSON').slice(0, 120), text: t.slice(0, 120) });
48
+ }
49
+ }
50
+ return { rows, defects, torn: defects.length, missing: false };
51
+ }
52
+
53
+ module.exports = { readJsonl };