create-pmos 0.7.2 → 0.7.3

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 CHANGED
@@ -333,6 +333,45 @@ Refreshes the skills, concepts, and scripts only. Your records — `state/`, `pl
333
333
  `okf/product/` — are yours, and an update never touches them. `pmos/KIT-VERSION` always says
334
334
  exactly what you're running.
335
335
 
336
+ ## What's new in 0.7.3
337
+
338
+ **A fix — read this if you ran a document review on 0.7.1 or 0.7.2, and especially if you have more
339
+ than one repository open in the workspace.**
340
+
341
+ Reviews were filed into the **workspace app's own install directory** rather than into the repository
342
+ being reviewed. Two consequences, and the first is the one that matters:
343
+
344
+ - **Reviews from different repositories were pooled together.** Open two repos and the review you
345
+ filed against one appeared when you opened the review surface in the other. A review names the
346
+ documents it read and quotes their contents, so this put one project's material in front of
347
+ another — including in front of an agent session working in that second repo.
348
+ - **In a product repo using this kit, reviews were written nowhere useful.** The review skill was
349
+ written against PMOS's own layout and told the agent to write to `workspace/state/reviews/` and
350
+ read `workspace/state/needs.json`. **A product repo has no `workspace/` folder**, so the agent
351
+ either created a stray one or filed the review where nothing would ever read it.
352
+
353
+ **Both are fixed.** Reviews now resolve through the same per-workspace state directory that
354
+ `needs.json` and `acceptances.jsonl` already used — your repo's own `pmos/state/reviews/`. The skill
355
+ no longer assumes any layout: the paths it needs are passed to it as absolute facts.
356
+
357
+ **If you reviewed anything on 0.7.1 or 0.7.2**, look in the workspace app's install directory under
358
+ `workspace/state/reviews/` for reviews that belong to your repositories, and delete any you would
359
+ rather not have sitting there. Nothing was sent anywhere — this is local files in the wrong folder —
360
+ but if you had two repos open, they saw each other.
361
+
362
+ **And the reason you probably never ran one: there was no way to say what to review.** The skill could
363
+ only be launched from the scheduled lens, and a schedule carries no scope — so its own instruction to
364
+ read exactly the documents you named had nothing behind it.
365
+
366
+ - **A `review` control now sits on the open document.** You are reading a document; you click
367
+ `review`; an agent reviews *that* document. Nothing to type, no schedule to create. The scope is
368
+ the thing in front of you.
369
+
370
+ **The standing disclosure is unchanged**, and it still matters more than any of the above: the review
371
+ surface has **not** been through its own review gate, and its false-positive rate is unmeasured. The
372
+ guard that withholds any proposal which would close a recorded open question or overwrite a decision
373
+ is tested, including in a real browser. How often its findings are simply *wrong* is not yet known.
374
+
336
375
  ## What's new in 0.7.2
337
376
 
338
377
  **A fix — read this if you use the Terminal Workspace on a machine with `tmux` installed.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
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. `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@bd8056d
1
+ kernel-source: pmos@56ab797
2
2
  assembled: 2026-07-28
3
3
  mode: vendor
@@ -883,6 +883,8 @@ async function openDoc(pth, kind) {
883
883
  body.querySelectorAll('[data-doc]').forEach((a) => a.onclick = (e) => { e.preventDefault(); openDoc(a.dataset.doc); });
884
884
  wireImgFallback(body);
885
885
  markActiveDoc(pth);
886
+ // Only a markdown document can be reviewed, and only a document — never a summoned surface.
887
+ $('docReview').hidden = !(state.docPath && /\.(md|markdown|mdown|mkd|mdx|skill)$/i.test(state.docPath));
886
888
  refit();
887
889
  }
888
890
  function markActiveDoc(pth) {
@@ -929,6 +931,7 @@ function renderHtmlDoc(body, text) {
929
931
  }
930
932
  function closeDoc() {
931
933
  state.docPath = null; state.surface = null;
934
+ $('docReview').hidden = true;
932
935
  $('paneDoc').hidden = true;
933
936
  $('railSplit').hidden = true;
934
937
  $('docBody').innerHTML = '';
@@ -1807,6 +1810,25 @@ $('themeBtn').onclick = () => {
1807
1810
  };
1808
1811
  $('closeDoc').onclick = closeDoc;
1809
1812
 
1813
+ // "review" on an open document. The whole point of putting it HERE is that the scope needs no
1814
+ // naming — the thing to review is the thing you are reading. The server assembles the prompt
1815
+ // (skill body + resolved paths) because a tenant repo has no workspace/ folder to point at.
1816
+ $('docReview').onclick = async () => {
1817
+ const pth = state.docPath;
1818
+ if (!pth) return;
1819
+ const btn = $('docReview');
1820
+ btn.disabled = true; btn.textContent = 'starting…';
1821
+ let out = {};
1822
+ try {
1823
+ out = await (await fetch('/api/review/start', { method: 'POST', headers: { 'Content-Type': 'application/json' },
1824
+ body: JSON.stringify({ ws: state.wsId, path: pth }) })).json();
1825
+ } catch { out = { ok: false, error: 'could not reach the workspace server' }; }
1826
+ btn.disabled = false; btn.textContent = 'review';
1827
+ if (!out.ok) { flash(out.error || 'could not start the review'); return; }
1828
+ flash('reviewing ' + pth.split('/').pop() + ' — watch the session');
1829
+ if (out.sessionId) selectSession(out.sessionId);
1830
+ };
1831
+
1810
1832
  // ── ⌘K / Ctrl-K jump palette (client-side: workspaces + sessions + initiatives + docs) ──
1811
1833
  // ── on-demand surfaces (Plan · Deliver · Measure · Report) ─────────────────────────────────────
1812
1834
  // Summoned from ⌘K, rendered in the reader, derived through the server's local state adapter and
@@ -1834,6 +1856,7 @@ async function openSurface(name) {
1834
1856
  showRail();
1835
1857
  $('paneDoc').hidden = false; $('railSplit').hidden = false; foldPane('doc', true);
1836
1858
  state.surface = name; state.docPath = null;
1859
+ $('docReview').hidden = true;
1837
1860
  document.querySelectorAll('.doc').forEach((b) => b.classList.remove('on'));
1838
1861
  $('crumb').innerHTML = `<bdi>surface · ${esc(name)}</bdi>`;
1839
1862
  $('docBody').scrollTop = 0;
@@ -187,6 +187,7 @@
187
187
  <div class="cap">
188
188
  <button class="fold" id="docFold" aria-expanded="true" title="Hide the document"><span class="chev" aria-hidden="true">▾</span>document</button>
189
189
  <span class="crumb" id="crumb"></span>
190
+ <button class="mini docrev" id="docReview" hidden title="Have an agent review this document">review</button>
190
191
  <button class="x" id="closeDoc" aria-label="Close document">✕</button>
191
192
  </div>
192
193
  <div class="pane-body doc-body" id="docBody" tabindex="0"></div>
@@ -603,7 +603,15 @@ const DELIV_DIR = path.join(STATE, 'deliverables');
603
603
  // Reviews the doc-review skill files (workspace-review-and-edit). One JSON per run, written by the
604
604
  // skill session, read back by /api/review. Runtime, not the record: the durable artifact of a review
605
605
  // is the correction records its dispositions produce, which live in planning/runs/ and are tracked.
606
- const REVIEW_DIR = path.join(STATE, 'reviews');
606
+ // PER WORKSPACE, like every other piece of state here. An earlier build put this in the SERVER's own
607
+ // state/ — which meant two open repos shared one review pile, and a tenant repo (which has no
608
+ // workspace/ folder at all) had the reviews written into the installed app instead of into the repo
609
+ // being reviewed. Reviews name documents and quote their contents, so that was a cross-workspace leak,
610
+ // not just untidy filing. stateDirFor() is the same resolver needs.json and acceptances.jsonl use.
611
+ function reviewDirFor(w, opts) {
612
+ const d = stateDirFor(w, opts);
613
+ return d ? path.join(d, 'reviews') : null;
614
+ }
607
615
  // Skills are file-driven (workspace-skill-authoring): built-ins ship committed under workspace/skills/,
608
616
  // user-authored ones are scaffolded into state/skills/ by the New Skill flow. There is ONE user-facing
609
617
  // concept — a skill — so the picker never exposes the internal `run`/`kind`; how a skill runs (server
@@ -1910,13 +1918,14 @@ async function handleRequest(req, res) {
1910
1918
  if (!w) return json(res, { ok: false, error: 'no workspace' }, 404);
1911
1919
  const want = url.searchParams.get('id');
1912
1920
  let files = [];
1913
- try { files = fs.readdirSync(REVIEW_DIR).filter((f) => f.endsWith('.json')).sort(); } catch {}
1921
+ const rdir = reviewDirFor(w);
1922
+ try { files = rdir ? fs.readdirSync(rdir).filter((f) => f.endsWith('.json')).sort() : []; } catch {}
1914
1923
  if (want && !/^[A-Za-z0-9._-]+$/.test(want)) return json(res, { ok: false, error: 'bad review id' }, 400);
1915
1924
  const pick = want ? `${want}.json` : files[files.length - 1];
1916
1925
  if (!pick || !files.includes(pick)) return json(res, { ok: true, empty: true, reviews: files.length });
1917
1926
 
1918
1927
  let review;
1919
- try { review = JSON.parse(fs.readFileSync(path.join(REVIEW_DIR, pick), 'utf8')); }
1928
+ try { review = JSON.parse(fs.readFileSync(path.join(rdir, pick), 'utf8')); }
1920
1929
  catch { return json(res, { ok: false, error: `review '${pick}' is unreadable or not valid JSON` }, 500); }
1921
1930
 
1922
1931
  // The SAME contract the write path enforces (R3/R7). A review that fails it is surfaced as an
@@ -1953,6 +1962,60 @@ async function handleRequest(req, res) {
1953
1962
  }));
1954
1963
  return json(res, { ok: true, id: pick.replace(/\.json$/, ''), read: review.read, at: review.at || null, findings, reviews: files.length, stats, reasons: [...correctionRecord.REASONS] });
1955
1964
  }
1965
+ if (p === '/api/review/start' && req.method === 'POST') {
1966
+ // Review THIS document — the trigger that makes the scope implicit instead of typed. The PM is
1967
+ // reading a document; the thing they want reviewed is the thing in front of them.
1968
+ //
1969
+ // The prompt is assembled HERE, not in the browser, for one reason that matters beyond tidiness:
1970
+ // a tenant product repo has no workspace/ folder. Pointing the agent at
1971
+ // `workspace/skills/doc-review.skill` works in PMOS-self and breaks everywhere else, and so does
1972
+ // telling it to write to `workspace/state/reviews/`. The server knows the skill body and the
1973
+ // resolved state directory for THIS workspace, so both travel in the prompt as absolute facts.
1974
+ let body = {};
1975
+ try { body = JSON.parse((await readBody(req)) || '{}'); }
1976
+ catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
1977
+
1978
+ const w = getWs(body.ws);
1979
+ if (!w) return json(res, { ok: false, error: 'no workspace' }, 404);
1980
+ const rel = String(body.path || '').trim();
1981
+ if (!rel) return json(res, { ok: false, error: 'no document named' }, 400);
1982
+ const full = path.resolve(w.root, rel);
1983
+ if (!insideRoot(w.root, full)) return json(res, { ok: false, error: 'outside workspace' }, 404);
1984
+ if (preview.previewKind(path.basename(full)) !== 'markdown') {
1985
+ return json(res, { ok: false, error: 'only markdown documents can be reviewed' }, 400);
1986
+ }
1987
+ if (!fs.existsSync(full)) return json(res, { ok: false, error: 'no such document' }, 404);
1988
+
1989
+ const task = loadTasks().find((t) => t.id === 'doc-review');
1990
+ if (!task || !task.prompt) return json(res, { ok: false, error: 'the doc-review skill is not installed' }, 500);
1991
+
1992
+ const rdir = reviewDirFor(w, { create: true });
1993
+ if (!rdir) return json(res, { ok: false, error: 'this workspace has no state directory to file a review into' }, 500);
1994
+ try { fs.mkdirSync(rdir, { recursive: true }); }
1995
+ catch (e) { warnWriteFail('reviews directory', e); return json(res, { ok: false, error: 'could not create the reviews directory' }, 500); }
1996
+
1997
+ // Scope first, so it is the last thing the agent reads before the procedure — and stated as
1998
+ // paths, because "the documents the PM named" is only a rule if something names them.
1999
+ const extra = Array.isArray(body.also) ? body.also.filter((x) => typeof x === 'string').slice(0, 9) : [];
2000
+ const scope = [rel, ...extra];
2001
+ const preamble = [
2002
+ 'Review these documents, and only these:',
2003
+ ...scope.map((s) => ` - ${s}`),
2004
+ '',
2005
+ `File the review as JSON into: ${rdir}`,
2006
+ `The needs-you queue for this workspace is: ${needsFileFor(w) || '(none — skip the needs step)'}`,
2007
+ 'Both paths are absolute and already exist. Use them exactly; do not guess a workspace/ path.',
2008
+ '',
2009
+ '--- the skill ---',
2010
+ '',
2011
+ ].join('\n');
2012
+
2013
+ const s = spawnSession({ name: 'review: ' + path.basename(rel), agent: task.agent || 'claude',
2014
+ ws: w.id, origin: 'review:' + rel, initialInput: preamble + task.prompt + '\r' });
2015
+ const started = !!(s && s.pty);
2016
+ return json(res, { ok: started, sessionId: s && s.id, scope, sink: rdir,
2017
+ error: started ? undefined : ((s && s.note) || 'could not start the session') });
2018
+ }
1956
2019
  if (p === '/api/review/dispose' && req.method === 'POST') {
1957
2020
  // The ONLY write path this surface has (workspace-review-and-edit R6/R8/R11). The reader was
1958
2021
  // designed around there being none, so every guard here is load-bearing rather than ceremony.
@@ -56,7 +56,8 @@ and is not — it is a measured open question (`pm-decisions.md` 491–503).
56
56
 
57
57
  ## Procedure
58
58
 
59
- 1. **Take the scope.** Read exactly the documents the PM named. Do not widen it.
59
+ 1. **Take the scope.** The documents are listed at the top of this prompt. Read exactly those and
60
+ do not widen the set — the PM chose them by opening one, not by describing a filter.
60
61
  *Done when:* every named path has been read in full, and you can list them.
61
62
 
62
63
  2. **Find, then cut.** For each document, ask the four questions above. Then remove every candidate
@@ -70,8 +71,10 @@ and is not — it is a measured open question (`pm-decisions.md` 491–503).
70
71
  guessing. Where the withheld test fires, propose nothing.
71
72
  *Done when:* every finding either carries an exact-match proposal or is explicitly withheld.
72
73
 
73
- 4. **File the review.** Pick a run id (`doc-review-<UTC-timestamp>`), then write
74
- `workspace/state/reviews/<run-id>.json` with exactly this shape:
74
+ 4. **File the review.** Pick a run id (`doc-review-<UTC-timestamp>`). Write it to the reviews
75
+ directory **named at the top of this prompt** — an absolute path that already exists. Do not
76
+ guess a `workspace/…` path: a product repo using the PMOS kit has no `workspace/` folder, and
77
+ inventing one files the review where nothing will read it. The shape:
75
78
 
76
79
  ```json
77
80
  {
@@ -94,11 +97,13 @@ and is not — it is a measured open question (`pm-decisions.md` 491–503).
94
97
 
95
98
  `withheld` is a required boolean on every finding and is never inferred. A withheld finding sets
96
99
  `"proposal": null` and adds `"withheld_reason"`.
97
- *Done when:* the file is valid JSON and `GET /api/review` returns `ok: true` for it. If it returns
98
- `422`, the contract rejected a finding — fix it; do not file it anyway.
100
+ *Done when:* the file is valid JSON in the directory the prompt named, and `GET /api/review`
101
+ returns `ok: true` for it. If it returns `422`, the contract rejected a finding — fix it; do not
102
+ file it anyway.
99
103
 
100
- 5. **Tell the PM it is ready.** Read `workspace/state/needs.json` (a missing file is `[]`), append
101
- exactly this object, and write the whole array back — it must stay a valid JSON array:
104
+ 5. **Tell the PM it is ready.** Read the needs-you queue **named at the top of this prompt** (a
105
+ missing file is `[]`), append exactly this object, and write the whole array back — it must stay
106
+ a valid JSON array. If the prompt said there is none, skip this step:
102
107
 
103
108
  ```json
104
109
  {"run_id":"<run-id>","kind":"review","initiative_id":"workspace-review-and-edit","question":"Document review — <n> findings"}