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 +39 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/public/app.js +23 -0
- package/workspace/public/index.html +1 -0
- package/workspace/server.js +66 -3
- package/workspace/skills/doc-review.skill +12 -7
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.
|
|
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": {
|
package/payload/KIT-VERSION
CHANGED
package/workspace/public/app.js
CHANGED
|
@@ -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>
|
package/workspace/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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.**
|
|
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>`)
|
|
74
|
-
|
|
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`
|
|
98
|
-
`422`, the contract rejected a finding — fix it; do not
|
|
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
|
|
101
|
-
exactly this object, and write the whole array back — it must stay
|
|
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"}
|