create-pmos 0.7.0 → 0.7.1
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 +31 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/public/app.js +61 -4
- package/workspace/public/styles.css +19 -0
- package/workspace/server.js +26 -14
package/README.md
CHANGED
|
@@ -333,6 +333,37 @@ 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.1
|
|
337
|
+
|
|
338
|
+
**A correction to 0.7.0 — read this if you used the review surface.**
|
|
339
|
+
|
|
340
|
+
0.7.0 shipped the summoned document review with a defect in the part that records *why* you changed
|
|
341
|
+
an agent's proposal. The client sent a fixed reason — `incomplete` — on **every** edit, whatever the
|
|
342
|
+
actual reason was. So any correction record written by 0.7.0 carries a reason that is very likely
|
|
343
|
+
false, in the one file whose whole purpose is to make the human-correction rate trustworthy.
|
|
344
|
+
|
|
345
|
+
If you ran a review on 0.7.0, treat the `reason` field in `planning/runs/correction-records.jsonl` as
|
|
346
|
+
unreliable. Everything else in those records — the agent's text, your text, the attribution — is
|
|
347
|
+
sound.
|
|
348
|
+
|
|
349
|
+
- **You pick the reason now.** Applying an edit asks *what was wrong with it?* and offers
|
|
350
|
+
`factually wrong · incomplete · wrong scope · style · other…`. It **nudges rather than defaulting**,
|
|
351
|
+
and `other` asks for a few words. Dismiss is still one click — friction belongs on the action that
|
|
352
|
+
teaches something, not the common one.
|
|
353
|
+
- **A half-applied state is now impossible.** The record was validated *after* the document was
|
|
354
|
+
written, so a missing reason could leave your file changed with nothing recorded. Validation runs
|
|
355
|
+
first: every refusal leaves the repository byte-identical.
|
|
356
|
+
- **The counters are on the surface, and honest.** Dispositions, corrections and the action rate are
|
|
357
|
+
derived from the records themselves. Quiet-on-clean reads **`not measured`** rather than a
|
|
358
|
+
flattering zero — measuring it needs a corpus of documents known to be clean, which does not exist
|
|
359
|
+
yet.
|
|
360
|
+
|
|
361
|
+
**The disclosure from 0.7.0 still stands**, and it matters more than this release note: the review
|
|
362
|
+
surface has **not** been through its own review gate. Its false-positive rate is unmeasured. It
|
|
363
|
+
withholds any proposal that would close a recorded open question or overwrite a decision — that guard
|
|
364
|
+
is tested, including in a real browser — but nothing yet tells you how often its findings are simply
|
|
365
|
+
wrong. Use it on documents you are willing to read carefully.
|
|
366
|
+
|
|
336
367
|
## What's new in 0.7.0
|
|
337
368
|
|
|
338
369
|
**The workspace GUI has layouts.** Until now it had exactly one arrangement: a fixed sidebar, the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
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
|
@@ -1905,9 +1905,27 @@ function reviewBody(p) {
|
|
|
1905
1905
|
<div class="rv-lede">${esc(lede)}</div>
|
|
1906
1906
|
<div class="rv-sub">Read ${p.read.map((r) => `<span class="mono">${esc(r)}</span>`).join(', ')}.</div>
|
|
1907
1907
|
${p.findings.map((f, i) => rvFinding(f, i)).join('')}
|
|
1908
|
-
<div class="rv-tail">Read ${p.read.length} document${p.read.length === 1 ? '' : 's'}. <b>Nothing written.</b
|
|
1908
|
+
<div class="rv-tail">Read ${p.read.length} document${p.read.length === 1 ? '' : 's'}. <b>Nothing written.</b>${rvStats(p.stats)}</div>
|
|
1909
1909
|
</div>`;
|
|
1910
1910
|
}
|
|
1911
|
+
// The PRD's success signal, on the face of the surface. Action rate comes straight off the correction
|
|
1912
|
+
// records. Quiet-rate CANNOT — it needs a corpus with a labelled clean class, which does not exist —
|
|
1913
|
+
// so it says "not measured" rather than rendering a flattering zero. An unmeasured number shown as a
|
|
1914
|
+
// number is the watermelon this whole surface exists to catch.
|
|
1915
|
+
function rvStats(s) {
|
|
1916
|
+
if (!s || !s.dispositions) return '';
|
|
1917
|
+
const pct = (v) => (v == null ? null : `${Math.round(v * 100)}%`);
|
|
1918
|
+
const bits = [
|
|
1919
|
+
`${s.dispositions} disposition${s.dispositions === 1 ? '' : 's'}`,
|
|
1920
|
+
`${s.corrections} correction${s.corrections === 1 ? '' : 's'}`,
|
|
1921
|
+
`acted on ${pct(s.action_rate)}`,
|
|
1922
|
+
`quiet on clean <i>not measured</i>`,
|
|
1923
|
+
];
|
|
1924
|
+
const warn = s.inferred_attributions
|
|
1925
|
+
? ` <b class="rv-bad">${s.inferred_attributions} inferred attribution${s.inferred_attributions === 1 ? '' : 's'}</b>`
|
|
1926
|
+
: '';
|
|
1927
|
+
return `<div class="rv-stats">${bits.join(' · ')}${warn}</div>`;
|
|
1928
|
+
}
|
|
1911
1929
|
function rvFinding(f, i) {
|
|
1912
1930
|
const ev = (f.evidence || []).map((e) => `${e.file}:${e.line}`).join(' · ');
|
|
1913
1931
|
// Three states, and only one of them offers a write. `settled` is terminal for this review.
|
|
@@ -1930,6 +1948,13 @@ function rvFinding(f, i) {
|
|
|
1930
1948
|
// The edit surface: opens IN PLACE under the finding — no modal, no mode switch, no navigation.
|
|
1931
1949
|
// Gutter change bars are the VS Code cue; the replaced lines sit struck through above the new ones.
|
|
1932
1950
|
// Cancel needs no write path at all — it just closes, which is the whole of "Cancel writes nothing".
|
|
1951
|
+
// The reason taxonomy, deliberately short. A vocabulary nobody can hold in their head gets skipped,
|
|
1952
|
+
// and a skipped reason is a record that teaches nothing. Values match correction-record.js's REASONS;
|
|
1953
|
+
// the server rejects anything outside it, so this list can never drift into inventing one.
|
|
1954
|
+
const RV_REASONS = [
|
|
1955
|
+
['factually-wrong', 'factually wrong'], ['incomplete', 'incomplete'],
|
|
1956
|
+
['wrong-scope', 'wrong scope'], ['style', 'style'], ['other', 'other…'],
|
|
1957
|
+
];
|
|
1933
1958
|
function rvEditor(f, i) {
|
|
1934
1959
|
const before = (f.proposal && f.proposal.before) || '';
|
|
1935
1960
|
const after = (f.proposal && f.proposal.after) || '';
|
|
@@ -1940,6 +1965,11 @@ function rvEditor(f, i) {
|
|
|
1940
1965
|
${before.split('\n').map((l) => `<div class="rv-l was"><span class="g"></span><span class="t">${esc(l)}</span></div>`).join('')}
|
|
1941
1966
|
</div>
|
|
1942
1967
|
<textarea class="rv-ta" data-rv-ta="${i}" spellcheck="false" rows="${Math.min(12, Math.max(3, nl(after) + 1))}">${esc(after)}</textarea>
|
|
1968
|
+
<div class="rv-reason" data-rv-reason="${i}">
|
|
1969
|
+
<span class="rv-only">What was wrong with it?</span>
|
|
1970
|
+
${RV_REASONS.map((r) => `<button class="rv-rz" data-rv-rz="${i}" data-val="${r[0]}">${esc(r[1])}</button>`).join('')}
|
|
1971
|
+
</div>
|
|
1972
|
+
<input class="rv-note" data-rv-note="${i}" hidden placeholder="in a few words…" aria-label="Why" autocomplete="off" />
|
|
1943
1973
|
<div class="rv-acts">
|
|
1944
1974
|
<button data-rv-save="${i}">Apply</button>
|
|
1945
1975
|
<button class="q" data-rv-cancel="${i}">Cancel</button>
|
|
@@ -1972,13 +2002,40 @@ function wireReview() {
|
|
|
1972
2002
|
});
|
|
1973
2003
|
$('docBody').querySelectorAll('[data-rv-apply]').forEach((b) => b.onclick = () => rvDispose(F(b.dataset.rvApply), 'apply'));
|
|
1974
2004
|
$('docBody').querySelectorAll('[data-rv-dismiss]').forEach((b) => b.onclick = () => rvDispose(F(b.dataset.rvDismiss), 'dismiss'));
|
|
2005
|
+
// The reason tap. Required on an edit (R8 / correction-record.js) — an edit is where the agent was
|
|
2006
|
+
// wrong in a way worth knowing, and a record that cannot say HOW is the self-report this feature
|
|
2007
|
+
// exists to replace. Dismiss stays one click: friction belongs on the informative action, not the
|
|
2008
|
+
// common one (PRD open question 2).
|
|
2009
|
+
$('docBody').querySelectorAll('[data-rv-rz]').forEach((b) => b.onclick = () => {
|
|
2010
|
+
const n = b.dataset.rvRz;
|
|
2011
|
+
const row = $('docBody').querySelector(`[data-rv-reason="${n}"]`);
|
|
2012
|
+
row.querySelectorAll('.rv-rz').forEach((x) => x.classList.toggle('on', x === b));
|
|
2013
|
+
row.dataset.picked = b.dataset.val;
|
|
2014
|
+
const note = $('docBody').querySelector(`[data-rv-note="${n}"]`);
|
|
2015
|
+
note.hidden = b.dataset.val !== 'other'; // 'other' is only useful with words
|
|
2016
|
+
if (b.dataset.val === 'other') note.focus();
|
|
2017
|
+
const nudge = row.querySelector('.rv-nudge'); if (nudge) nudge.remove();
|
|
2018
|
+
});
|
|
1975
2019
|
$('docBody').querySelectorAll('[data-rv-save]').forEach((b) => b.onclick = () => {
|
|
1976
2020
|
const n = b.dataset.rvSave;
|
|
2021
|
+
const row = $('docBody').querySelector(`[data-rv-reason="${n}"]`);
|
|
2022
|
+
const picked = row && row.dataset.picked;
|
|
2023
|
+
if (!picked) {
|
|
2024
|
+
// An inline nudge, not a modal and not a silent default. Defaulting is what produced the bug
|
|
2025
|
+
// this replaces: every edit recorded "incomplete" whether or not it was.
|
|
2026
|
+
if (!row.querySelector('.rv-nudge')) {
|
|
2027
|
+
const s = document.createElement('span');
|
|
2028
|
+
s.className = 'rv-nudge'; s.textContent = 'Pick one — the record is worth nothing without it.';
|
|
2029
|
+
row.appendChild(s);
|
|
2030
|
+
}
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
const note = $('docBody').querySelector(`[data-rv-note="${n}"]`);
|
|
1977
2034
|
const ta = $('docBody').querySelector(`[data-rv-ta="${n}"]`);
|
|
1978
|
-
rvDispose(F(n), 'edit', ta ? ta.value : '');
|
|
2035
|
+
rvDispose(F(n), 'edit', ta ? ta.value : '', picked, note && !note.hidden ? note.value : undefined);
|
|
1979
2036
|
});
|
|
1980
2037
|
}
|
|
1981
|
-
async function rvDispose(f, disposition, pmText) {
|
|
2038
|
+
async function rvDispose(f, disposition, pmText, reason, reasonNote) {
|
|
1982
2039
|
if (!f) return;
|
|
1983
2040
|
const rv = state.review || {};
|
|
1984
2041
|
// The client sends back the exact text it rendered; the server re-derives the diff and refuses if
|
|
@@ -1995,7 +2052,7 @@ async function rvDispose(f, disposition, pmText) {
|
|
|
1995
2052
|
const body = {
|
|
1996
2053
|
ws: state.wsId, disposition, finding: f, rendered_after: renderedAfter,
|
|
1997
2054
|
pm_text: disposition === 'edit' ? pmText : undefined,
|
|
1998
|
-
reason:
|
|
2055
|
+
reason, reason_note: reasonNote, // the PM's own word — never defaulted, never inferred
|
|
1999
2056
|
run_id: rv.id, initiative_id: 'workspace-review-and-edit',
|
|
2000
2057
|
};
|
|
2001
2058
|
let out = {};
|
|
@@ -986,3 +986,22 @@ details.tree>summary:hover{color:var(--text-2)}
|
|
|
986
986
|
padding:8px 11px;background:rgba(72,213,151,.06);color:var(--text-2);
|
|
987
987
|
font-family:var(--mono);font-size:11.5px;line-height:1.8;outline:none}
|
|
988
988
|
.rv-ta:focus{border-color:var(--accent);border-left-color:var(--ok)}
|
|
989
|
+
|
|
990
|
+
/* the reason tap — required on an edit, and never defaulted. The nudge is inline, not a modal:
|
|
991
|
+
a silent default is exactly the bug this replaced (every edit recorded "incomplete"). */
|
|
992
|
+
.rv-reason{display:flex;gap:7px;align-items:center;flex-wrap:wrap;margin-top:10px}
|
|
993
|
+
.rv-rz{padding:0 9px;height:28px;border-radius:999px;border:1px solid var(--border-strong);
|
|
994
|
+
background:var(--chip);font-size:11.5px;color:var(--text-3)}
|
|
995
|
+
.rv-rz:hover{background:var(--card-hover);color:var(--text)}
|
|
996
|
+
/* Selected reads as accent TEXT on a wash, not a filled pill: nothing else on this surface carries
|
|
997
|
+
a solid fill, and no token exists for text-on-accent because no component here needed one. */
|
|
998
|
+
.rv-rz.on{background:var(--accent-wash);border-color:var(--accent);color:var(--accent-text);font-weight:600}
|
|
999
|
+
.rv-nudge{font-size:11.5px;color:var(--warn-text);flex-basis:100%}
|
|
1000
|
+
.rv-note{width:100%;box-sizing:border-box;margin-top:8px;border:1px solid var(--border);
|
|
1001
|
+
border-radius:7px;padding:7px 10px;background:var(--chip);color:var(--text-2);
|
|
1002
|
+
font-family:var(--sans);font-size:12px;outline:none}
|
|
1003
|
+
.rv-note:focus{border-color:var(--accent)}
|
|
1004
|
+
/* the success-signal counters — one quiet line, and an unmeasured number says so */
|
|
1005
|
+
.rv-stats{margin-top:6px;font-family:var(--mono);font-size:10.5px;color:var(--text-dim);line-height:1.7}
|
|
1006
|
+
.rv-stats i{font-style:normal;color:var(--warn-text)}
|
|
1007
|
+
.rv-stats .rv-bad{color:var(--bad);font-weight:600}
|
package/workspace/server.js
CHANGED
|
@@ -1904,19 +1904,30 @@ async function handleRequest(req, res) {
|
|
|
1904
1904
|
// Merge in what the PM has already settled, so a disposed finding renders as settled rather than
|
|
1905
1905
|
// being offered twice. Read-only: dispositions live in the correction records, not in the review.
|
|
1906
1906
|
const settled = {};
|
|
1907
|
+
const records = [];
|
|
1907
1908
|
try {
|
|
1908
1909
|
const txt = fs.readFileSync(path.join(w.root, 'planning', 'runs', 'correction-records.jsonl'), 'utf8');
|
|
1909
1910
|
for (const line of txt.split('\n')) {
|
|
1910
1911
|
if (!line.trim()) continue;
|
|
1911
|
-
try {
|
|
1912
|
+
try {
|
|
1913
|
+
const r = JSON.parse(line);
|
|
1914
|
+
if (!r) continue;
|
|
1915
|
+
records.push(r);
|
|
1916
|
+
if (r.finding_title) settled[r.finding_title] = r.disposition;
|
|
1917
|
+
} catch {}
|
|
1912
1918
|
}
|
|
1913
1919
|
} catch {}
|
|
1920
|
+
// The PRD's success signal is explicit that the counters are part of the build, not a follow-up:
|
|
1921
|
+
// action rate must be derivable off the records with no manual counting. quiet-rate is NOT
|
|
1922
|
+
// derivable here — it needs a corpus with a labelled CLEAN class, which does not exist — so it is
|
|
1923
|
+
// reported as null and rendered as "not measured" rather than as a flattering zero.
|
|
1924
|
+
const stats = { ...correctionRecord.summarise(records), quiet_rate: findingContract.quietRate(null, null) };
|
|
1914
1925
|
const findings = review.findings.map((f) => ({
|
|
1915
1926
|
...f,
|
|
1916
1927
|
applyable: findingContract.isApplyable(f), // the client asks the contract, never re-derives it
|
|
1917
1928
|
settled: settled[f.title] || null,
|
|
1918
1929
|
}));
|
|
1919
|
-
return json(res, { ok: true, id: pick.replace(/\.json$/, ''), read: review.read, at: review.at || null, findings, reviews: files.length });
|
|
1930
|
+
return json(res, { ok: true, id: pick.replace(/\.json$/, ''), read: review.read, at: review.at || null, findings, reviews: files.length, stats, reasons: [...correctionRecord.REASONS] });
|
|
1920
1931
|
}
|
|
1921
1932
|
if (p === '/api/review/dispose' && req.method === 'POST') {
|
|
1922
1933
|
// The ONLY write path this surface has (workspace-review-and-edit R6/R8/R11). The reader was
|
|
@@ -1949,6 +1960,19 @@ async function handleRequest(req, res) {
|
|
|
1949
1960
|
return json(res, { ok: false, error: 'this finding offers nothing to apply — it is context, and yours to settle' }, 400);
|
|
1950
1961
|
}
|
|
1951
1962
|
|
|
1963
|
+
// R8, and it must happen BEFORE the write. buildRecord depends on nothing the write produces, and
|
|
1964
|
+
// validating it after would let a missing reason leave the document changed with no record behind
|
|
1965
|
+
// it — a half-applied state whose whole point was to be impossible. Refuse first, touch the file
|
|
1966
|
+
// second: every rejection below this line leaves the repository byte-identical.
|
|
1967
|
+
const built = correctionRecord.buildRecord({
|
|
1968
|
+
run_id: body.run_id, initiative_id: body.initiative_id, finding: f, disposition,
|
|
1969
|
+
pm_text: body.pm_text, reason: body.reason, reason_note: body.reason_note,
|
|
1970
|
+
pm: body.pm || 'wawan', at: nowISO(),
|
|
1971
|
+
});
|
|
1972
|
+
if (!built.ok) {
|
|
1973
|
+
return json(res, { ok: false, error: 'no correction record could be written, so nothing was changed', errors: built.errors }, 400);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1952
1976
|
let wrote = null;
|
|
1953
1977
|
if (writes) {
|
|
1954
1978
|
// Path guard, identical to every read on this surface (R12): traversal and cross-workspace
|
|
@@ -1983,18 +2007,6 @@ async function handleRequest(req, res) {
|
|
|
1983
2007
|
wrote = f.file;
|
|
1984
2008
|
}
|
|
1985
2009
|
|
|
1986
|
-
// R8: the record is built only where attribution is KNOWN. buildRecord refuses anything that
|
|
1987
|
-
// would need inference, and a refusal here means no record is written at all — never a guess.
|
|
1988
|
-
const built = correctionRecord.buildRecord({
|
|
1989
|
-
run_id: body.run_id, initiative_id: body.initiative_id, finding: f, disposition,
|
|
1990
|
-
pm_text: body.pm_text, reason: body.reason, reason_note: body.reason_note,
|
|
1991
|
-
pm: body.pm || 'wawan', at: nowISO(),
|
|
1992
|
-
});
|
|
1993
|
-
if (!built.ok) {
|
|
1994
|
-
// The document may already have been written — say so rather than imply a clean failure.
|
|
1995
|
-
return json(res, { ok: false, error: 'no correction record written (attribution would have to be inferred)', errors: built.errors, wrote }, 400);
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
2010
|
// Curated record, tracked and append-only — deliberately NOT stateDirFor()'s runtime directory
|
|
1999
2011
|
// (PRD open question 1; pm-decisions.md 491–503 keeps that boundary the PM's to settle).
|
|
2000
2012
|
const sink = path.join(w.root, 'planning', 'runs', 'correction-records.jsonl');
|