create-pmos 0.7.3 → 0.9.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 +68 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/skills/pmos-router.skill +22 -0
- package/payload/skills-native/pmos-router/SKILL.md +22 -0
- package/workspace/README.md +28 -0
- package/workspace/lib/apply-change.js +51 -1
- package/workspace/lib/console-state.js +56 -1
- package/workspace/lib/git-info.js +197 -25
- package/workspace/lib/session-state.js +139 -0
- package/workspace/lib/transcript-read.js +28 -6
- package/workspace/public/app.js +429 -274
- package/workspace/public/index.html +30 -7
- package/workspace/public/styles.css +898 -624
- package/workspace/public/vendor/ibm-plex-mono-400.woff2 +0 -0
- package/workspace/public/vendor/ibm-plex-mono-700.woff2 +0 -0
- package/workspace/public/vendor/space-grotesk-var.woff2 +0 -0
- package/workspace/server.js +401 -234
- package/workspace/lib/correction-record.js +0 -127
- package/workspace/lib/finding-contract.js +0 -162
- package/workspace/skills/doc-review.skill +0 -119
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/* correction-record — what a PM disposition leaves behind. (workspace-review-and-edit · R8/R9)
|
|
2
|
-
*
|
|
3
|
-
* The north star is human correction rate per agent run, and its numerator is currently a
|
|
4
|
-
* self-reported integer: all four runs in planning/runs/agent-runs.jsonl carry
|
|
5
|
-
* "human_corrections": 0 with no artifact behind the number. correction-signal-derive.skill
|
|
6
|
-
* explains why the obvious fix does not reach it — "pre-commit conversational steering is
|
|
7
|
-
* git-invisible and stays the self-report's lane."
|
|
8
|
-
*
|
|
9
|
-
* This module builds the artifact for that lane, under one rule that is not negotiable:
|
|
10
|
-
*
|
|
11
|
-
* ATTRIBUTION IS NEVER INFERRED.
|
|
12
|
-
*
|
|
13
|
-
* A record exists only for text an agent PROPOSED in this review and the PM then disposed of.
|
|
14
|
-
* That is the single case where authorship is known rather than guessed — no git blame, no
|
|
15
|
-
* "the file changed while session X was live" timing heuristic. correction-signal-derive.skill
|
|
16
|
-
* states the rule this upholds: "Never guess an attribution. A commit you cannot confidently
|
|
17
|
-
* classify is ambiguous: flagged to the PM, held OUT of the score." A record that would need
|
|
18
|
-
* inference is not written at all (sprint criterion 7), because a manufactured attribution
|
|
19
|
-
* poisons the exact metric this exists to feed.
|
|
20
|
-
*
|
|
21
|
-
* It stores WHY and BY WHOM. It does not store the diff (R9) — git already holds what changed.
|
|
22
|
-
*/
|
|
23
|
-
'use strict';
|
|
24
|
-
(function (root, factory) {
|
|
25
|
-
const api = factory();
|
|
26
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
27
|
-
else root.CorrectionRecord = api;
|
|
28
|
-
})(typeof self !== 'undefined' ? self : this, function () {
|
|
29
|
-
|
|
30
|
-
const DISPOSITIONS = new Set(['apply', 'edit', 'dismiss']);
|
|
31
|
-
|
|
32
|
-
/* Reasons the PM taps. Deliberately short — a taxonomy nobody can hold in their head gets
|
|
33
|
-
* skipped, and a skipped reason is a record that teaches nothing. 'other' carries free text. */
|
|
34
|
-
const REASONS = new Set(['factually-wrong', 'incomplete', 'wrong-scope', 'style', 'not-now', 'other']);
|
|
35
|
-
|
|
36
|
-
/* Required on `edit` (the agent was wrong in a way worth knowing) and on `dismiss` only when
|
|
37
|
-
* the PM chooses to give one (PRD open question 2: optional on dismiss, required on edit —
|
|
38
|
-
* the most common action must not carry the most friction). Never required on `apply`:
|
|
39
|
-
* accepting a proposal unchanged is not a correction and must not inflate the numerator. */
|
|
40
|
-
function reasonRequired(disposition) {
|
|
41
|
-
return disposition === 'edit';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Is this a correction at all? Applying an agent's proposal unchanged is agreement. */
|
|
45
|
-
function isCorrection(disposition) {
|
|
46
|
-
return disposition === 'edit' || disposition === 'dismiss';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const isStr = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Build a record from a disposition, or refuse.
|
|
53
|
-
* input = { run_id, initiative_id, finding, disposition, pm_text?, reason?, reason_note?, pm, at }
|
|
54
|
-
* Returns { ok: true, record } or { ok: false, errors[] }. Never throws, never guesses.
|
|
55
|
-
*/
|
|
56
|
-
function buildRecord(input) {
|
|
57
|
-
const e = [];
|
|
58
|
-
const i = input || {};
|
|
59
|
-
|
|
60
|
-
if (!DISPOSITIONS.has(i.disposition)) e.push(`disposition must be one of ${[...DISPOSITIONS].join(' | ')}`);
|
|
61
|
-
if (!i.finding || typeof i.finding !== 'object') e.push('finding is required — the record is anchored to what was proposed');
|
|
62
|
-
if (!isStr(i.pm)) e.push('pm is required');
|
|
63
|
-
if (!isStr(i.at)) e.push('at (ISO 8601) is required');
|
|
64
|
-
|
|
65
|
-
// ── the attribution rule (R8) ───────────────────────────────────────────────
|
|
66
|
-
// The agent's text must be present and must have come from THIS review's proposal. Anything
|
|
67
|
-
// else would mean deriving authorship from git or from timing, which is exactly forbidden.
|
|
68
|
-
const f = i.finding || {};
|
|
69
|
-
const agentText = f.proposal && f.proposal.after;
|
|
70
|
-
if (i.disposition !== 'dismiss' && !isStr(agentText)) {
|
|
71
|
-
e.push('no agent-proposed text on this finding — attribution would have to be inferred, so no record is written (R8)');
|
|
72
|
-
}
|
|
73
|
-
if (f.withheld === true) {
|
|
74
|
-
// A withheld finding was never applyable, so a disposition other than dismiss is incoherent
|
|
75
|
-
// and would imply the containment guard leaked.
|
|
76
|
-
if (i.disposition !== 'dismiss') e.push('a withheld finding can only be dismissed — it never offered a change (R7)');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (i.disposition === 'edit' && !isStr(i.pm_text)) {
|
|
80
|
-
e.push('pm_text is required on an edit — the record exists to hold what the PM wrote instead');
|
|
81
|
-
}
|
|
82
|
-
if (reasonRequired(i.disposition) && !REASONS.has(i.reason)) {
|
|
83
|
-
e.push(`reason is required on an edit and must be one of ${[...REASONS].join(' | ')}`);
|
|
84
|
-
}
|
|
85
|
-
if (i.reason != null && !REASONS.has(i.reason)) e.push(`unknown reason '${i.reason}'`);
|
|
86
|
-
if (i.reason === 'other' && !isStr(i.reason_note)) e.push("reason 'other' requires reason_note");
|
|
87
|
-
|
|
88
|
-
if (e.length) return { ok: false, errors: e };
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
ok: true,
|
|
92
|
-
record: {
|
|
93
|
-
run_id: isStr(i.run_id) ? i.run_id : null,
|
|
94
|
-
initiative_id: isStr(i.initiative_id) ? i.initiative_id : null,
|
|
95
|
-
file: f.file || null,
|
|
96
|
-
finding_title: f.title || null,
|
|
97
|
-
disposition: i.disposition,
|
|
98
|
-
is_correction: isCorrection(i.disposition),
|
|
99
|
-
// attribution: known, because the agent proposed it here and the PM answered it here.
|
|
100
|
-
attribution: 'proposed-in-review',
|
|
101
|
-
agent_text: isStr(agentText) ? agentText : null,
|
|
102
|
-
pm_text: isStr(i.pm_text) ? i.pm_text : null,
|
|
103
|
-
reason: i.reason || null,
|
|
104
|
-
reason_note: isStr(i.reason_note) ? i.reason_note : null,
|
|
105
|
-
pm: i.pm,
|
|
106
|
-
at: i.at,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Roll records up into the numbers the PRD's success signal names. No manual counting
|
|
112
|
-
* (sprint criterion 8): both come straight off the records. */
|
|
113
|
-
function summarise(records) {
|
|
114
|
-
const r = Array.isArray(records) ? records : [];
|
|
115
|
-
const total = r.length;
|
|
116
|
-
const corrections = r.filter((x) => x && x.is_correction).length;
|
|
117
|
-
const acted = r.filter((x) => x && (x.disposition === 'apply' || x.disposition === 'edit')).length;
|
|
118
|
-
return {
|
|
119
|
-
dispositions: total,
|
|
120
|
-
corrections,
|
|
121
|
-
action_rate: total === 0 ? null : acted / total,
|
|
122
|
-
inferred_attributions: r.filter((x) => x && x.attribution !== 'proposed-in-review').length,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return { DISPOSITIONS, REASONS, reasonRequired, isCorrection, buildRecord, summarise };
|
|
127
|
-
});
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/* finding-contract — what a review finding must be, and what the PM may do with it.
|
|
2
|
-
* (workspace-review-and-edit · R3/R7)
|
|
3
|
-
*
|
|
4
|
-
* Used on BOTH sides, like preview-kind:
|
|
5
|
-
* - server.js requires it to validate a reviewer skill's output before it reaches the client
|
|
6
|
-
* - the browser loads it as /lib/finding-contract.js so the renderer asks the SAME question
|
|
7
|
-
* so the contract, the endpoint and the UI can never disagree about what is applyable.
|
|
8
|
-
*
|
|
9
|
-
* The one security-critical function is isApplyable(). It decides whether an Apply control
|
|
10
|
-
* exists at all. R7's rule is that a proposal which would close a recorded open question or
|
|
11
|
-
* overwrite a PM decision is surfaced as CONTEXT with no Apply affordance — the absence of the
|
|
12
|
-
* control is the mechanism, and a warning label is explicitly not a substitute. The rubric makes
|
|
13
|
-
* this a hard floor (dim 2 must = 1: zero Apply controls ever rendered for a withheld finding),
|
|
14
|
-
* so this boolean is the whole containment story and it is unit-tested directly.
|
|
15
|
-
*
|
|
16
|
-
* The second load-bearing rule is self_undetectable (R3). A review that emits lint findings
|
|
17
|
-
* becomes a linter the PM stops reading, and "judgement-grade" is not enforceable by inspecting
|
|
18
|
-
* the finding's text. So the contract inverts it: every finding must NAME what the artifact
|
|
19
|
-
* cannot see about itself. A reviewer that cannot fill that line has not found this class of
|
|
20
|
-
* defect, and the finding is rejected structurally rather than by taste.
|
|
21
|
-
*/
|
|
22
|
-
'use strict';
|
|
23
|
-
(function (root, factory) {
|
|
24
|
-
const api = factory();
|
|
25
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
26
|
-
else root.FindingContract = api;
|
|
27
|
-
})(typeof self !== 'undefined' ? self : this, function () {
|
|
28
|
-
|
|
29
|
-
/* Finding kinds the review must NOT emit (R3). These are owned by the Frontmatter Gate and the
|
|
30
|
-
* linters; re-reporting them here is how counsel decays into noise. Declared by the reviewer
|
|
31
|
-
* and rejected on sight — a blocklist catches the honest mistake, and self_undetectable below
|
|
32
|
-
* catches the dishonest one. */
|
|
33
|
-
const MECHANICAL_KINDS = new Set([
|
|
34
|
-
'frontmatter', 'schema', 'link', 'broken-link', 'missing-path', 'path',
|
|
35
|
-
'string-match', 'regex', 'count', 'occurrence', 'spelling', 'typo', 'formatting', 'lint',
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
const isStr = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
39
|
-
|
|
40
|
-
/** Reject a finding that a linter should have owned (R3). Kind is declared, not sniffed. */
|
|
41
|
-
function isMechanical(f) {
|
|
42
|
-
return !!f && isStr(f.kind) && MECHANICAL_KINDS.has(f.kind.trim().toLowerCase());
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* THE guardrail (R7). An Apply control may exist only when the finding is not withheld AND
|
|
46
|
-
* actually carries a change to make. Both halves matter: `withheld` is the PM-decision guard,
|
|
47
|
-
* and a null proposal is the context class (nothing to apply). Anything unexpected — a missing
|
|
48
|
-
* field, a malformed proposal, a non-boolean withheld — resolves to NOT applyable. This
|
|
49
|
-
* function fails closed on purpose; an unparseable finding must never grow a button. */
|
|
50
|
-
function isApplyable(f) {
|
|
51
|
-
if (!f || typeof f !== 'object') return false;
|
|
52
|
-
if (f.withheld === true) return false;
|
|
53
|
-
if (f.withheld !== false) return false; // must be explicitly declared, never inferred
|
|
54
|
-
const p = f.proposal;
|
|
55
|
-
if (!p || typeof p !== 'object') return false;
|
|
56
|
-
return isStr(p.before) || isStr(p.after); // a pure insertion has no `before`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Validate one finding against the contract. Returns { ok, errors[] } — never throws. */
|
|
60
|
-
function validateFinding(f) {
|
|
61
|
-
const e = [];
|
|
62
|
-
if (!f || typeof f !== 'object') return { ok: false, errors: ['finding is not an object'] };
|
|
63
|
-
|
|
64
|
-
if (!isStr(f.title)) e.push('title is required');
|
|
65
|
-
if (!isStr(f.file)) e.push('file is required');
|
|
66
|
-
if (!isStr(f.argument)) e.push('argument is required — a finding without reasoning is an assertion');
|
|
67
|
-
|
|
68
|
-
// R3: name what the artifact cannot see about itself, or this is not the class of defect
|
|
69
|
-
// this review exists to surface.
|
|
70
|
-
if (!isStr(f.self_undetectable)) {
|
|
71
|
-
e.push('self_undetectable is required — name what this artifact cannot detect about itself (R3)');
|
|
72
|
-
}
|
|
73
|
-
if (isMechanical(f)) {
|
|
74
|
-
e.push(`kind '${f.kind}' is mechanically findable and belongs to lint, not to this review (R3)`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Evidence is file:line, plural, and cited — an un-sourced finding is a guess.
|
|
78
|
-
if (!Array.isArray(f.evidence) || f.evidence.length === 0) {
|
|
79
|
-
e.push('evidence[] is required — at least one file:line citation');
|
|
80
|
-
} else {
|
|
81
|
-
f.evidence.forEach((ev, i) => {
|
|
82
|
-
if (!ev || typeof ev !== 'object') { e.push(`evidence[${i}] is not an object`); return; }
|
|
83
|
-
if (!isStr(ev.file)) e.push(`evidence[${i}].file is required`);
|
|
84
|
-
if (!Number.isInteger(ev.line) || ev.line < 1) e.push(`evidence[${i}].line must be a positive integer`);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// withheld is declared, never inferred — the containment guard must be explicit (R7).
|
|
89
|
-
if (typeof f.withheld !== 'boolean') {
|
|
90
|
-
e.push('withheld must be an explicit boolean (R7) — it is never inferred');
|
|
91
|
-
}
|
|
92
|
-
// A withheld finding carries no proposal at all. Carrying one invites a later refactor to
|
|
93
|
-
// "just render it disabled", which is exactly the failure dim 2 forbids.
|
|
94
|
-
if (f.withheld === true && f.proposal != null) {
|
|
95
|
-
e.push('a withheld finding must carry proposal: null — it has no change to offer (R7)');
|
|
96
|
-
}
|
|
97
|
-
if (f.withheld === true && !isStr(f.withheld_reason)) {
|
|
98
|
-
e.push('withheld_reason is required — say why this is yours to settle (R7)');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (f.proposal != null) {
|
|
102
|
-
const p = f.proposal;
|
|
103
|
-
if (typeof p !== 'object') e.push('proposal must be an object or null');
|
|
104
|
-
else {
|
|
105
|
-
if (p.before != null && typeof p.before !== 'string') e.push('proposal.before must be a string or absent');
|
|
106
|
-
if (!isStr(p.after) && !isStr(p.before)) e.push('proposal must change something (before and/or after)');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { ok: e.length === 0, errors: e };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Validate a whole review payload. One bad finding fails the review — partial trust is worse
|
|
114
|
-
* than none on a surface whose only job is to be believed. */
|
|
115
|
-
function validateReview(r) {
|
|
116
|
-
const e = [];
|
|
117
|
-
if (!r || typeof r !== 'object') return { ok: false, errors: ['review is not an object'] };
|
|
118
|
-
if (!Array.isArray(r.read) || r.read.length === 0) {
|
|
119
|
-
e.push('read[] is required — the review states what it actually read (R2)');
|
|
120
|
-
}
|
|
121
|
-
if (!Array.isArray(r.findings)) {
|
|
122
|
-
e.push('findings[] is required (an empty array is a valid, and good, review — R10)');
|
|
123
|
-
} else {
|
|
124
|
-
r.findings.forEach((f, i) => {
|
|
125
|
-
const v = validateFinding(f);
|
|
126
|
-
if (!v.ok) v.errors.forEach((msg) => e.push(`findings[${i}]: ${msg}`));
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return { ok: e.length === 0, errors: e };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Quiet-rate over a scored corpus (R10, rubric dim 1). Documents read with zero findings
|
|
133
|
-
* against them, divided by documents believed clean. The rubric's binding number. */
|
|
134
|
-
function quietRate(reviewed, cleanPaths) {
|
|
135
|
-
const clean = new Set(cleanPaths || []);
|
|
136
|
-
if (clean.size === 0) return null; // no clean class ⇒ no valid precision number
|
|
137
|
-
let quiet = 0;
|
|
138
|
-
for (const p of clean) {
|
|
139
|
-
const noisy = (reviewed || []).some((f) => f && f.file === p);
|
|
140
|
-
if (!noisy) quiet++;
|
|
141
|
-
}
|
|
142
|
-
return quiet / clean.size;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Action rate (PRD success signal): Apply+Edit ÷ all dispositions. Dismiss-heavy means the
|
|
146
|
-
* review is noise the PM is politely declining, whatever it scored. */
|
|
147
|
-
function actionRate(dispositions) {
|
|
148
|
-
const d = (dispositions || []).filter((x) => x === 'apply' || x === 'edit' || x === 'dismiss');
|
|
149
|
-
if (d.length === 0) return null;
|
|
150
|
-
return d.filter((x) => x !== 'dismiss').length / d.length;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
MECHANICAL_KINDS,
|
|
155
|
-
isMechanical,
|
|
156
|
-
isApplyable,
|
|
157
|
-
validateFinding,
|
|
158
|
-
validateReview,
|
|
159
|
-
quietRate,
|
|
160
|
-
actionRate,
|
|
161
|
-
};
|
|
162
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: doc-review
|
|
3
|
-
label: "Review documents"
|
|
4
|
-
description: "Read the planning documents you name and report only defects the artifact cannot detect about itself — never lint. Each finding carries its evidence and, where one exists, a proposed change you dispose of."
|
|
5
|
-
run: agent
|
|
6
|
-
agent: claude
|
|
7
|
-
output:
|
|
8
|
-
- Read
|
|
9
|
-
- Findings
|
|
10
|
-
- Withheld
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
You are running the "Review documents" skill in this PMOS workspace (local, file mode). Work only
|
|
14
|
-
from this repository's own files, git, and CLIs — no network (D54).
|
|
15
|
-
|
|
16
|
-
Goal: surface defects in the named documents that **the artifact cannot detect about itself**, and
|
|
17
|
-
file them as a contract-checked review the PM disposes of one at a time.
|
|
18
|
-
|
|
19
|
-
## What counts as a finding — and what does not
|
|
20
|
-
|
|
21
|
-
The whole value of this review is that the PM keeps reading it. A review that reports what a linter
|
|
22
|
-
already reports becomes a linter with manners, and gets ignored by Thursday.
|
|
23
|
-
|
|
24
|
-
**Report only defects the document cannot detect about itself.** The four kinds that recur:
|
|
25
|
-
|
|
26
|
-
- **A metric that does not measure its own claim** — a PRD bets on an outcome, then defines success as
|
|
27
|
-
a capability demo, so it can score full marks and move the outcome not at all.
|
|
28
|
-
- **An anchor that cannot score wrong** — a rubric dimension whose anchors award credit for descoping,
|
|
29
|
-
or that cannot distinguish a correct implementation from an absent one.
|
|
30
|
-
- **A gate that scripts away its own question** — a rubric prescribing the Acceptance Gate as the same
|
|
31
|
-
checklist it already scored, turning D12's strategic judgement into a second review pass.
|
|
32
|
-
- **A constraint contradicted elsewhere** — a non-goal or design rule in one shipped document that
|
|
33
|
-
silently rules out what another document proposes.
|
|
34
|
-
|
|
35
|
-
**NEVER report** — these belong to lint and the Frontmatter Gate, and emitting them fails the
|
|
36
|
-
contract outright: frontmatter or schema validity · broken links or missing paths · string/regex
|
|
37
|
-
matches · counting occurrences · spelling, typos, formatting.
|
|
38
|
-
|
|
39
|
-
**Say nothing when there is nothing to say.** An empty findings list is a correct and valued outcome.
|
|
40
|
-
Do not pad a review to look thorough — a false finding costs the PM more than a missed one, because
|
|
41
|
-
they pay for it every time.
|
|
42
|
-
|
|
43
|
-
## The withheld class — where you must NOT propose a fix
|
|
44
|
-
|
|
45
|
-
Some defects are not defects. Before proposing any change, ask: **would applying this close a
|
|
46
|
-
question the PM has parked, or overwrite a decision they recorded?** Check `pm-decisions.md`,
|
|
47
|
-
`DECISIONS.md`, and the document's own history.
|
|
48
|
-
|
|
49
|
-
If yes, the finding is **withheld**: `"withheld": true`, `"proposal": null`, and a
|
|
50
|
-
`"withheld_reason"` saying why it is the PM's to settle. The client renders no Apply control for it —
|
|
51
|
-
the absence of the button is the guard, so a withheld finding carrying a proposal is rejected by the
|
|
52
|
-
contract, not merely discouraged.
|
|
53
|
-
|
|
54
|
-
The recorded instance: `CLAUDE.md` points file-mode state at `pmos/state/*.jsonl`, which looks stale
|
|
55
|
-
and is not — it is a measured open question (`pm-decisions.md` 491–503).
|
|
56
|
-
|
|
57
|
-
## Procedure
|
|
58
|
-
|
|
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.
|
|
61
|
-
*Done when:* every named path has been read in full, and you can list them.
|
|
62
|
-
|
|
63
|
-
2. **Find, then cut.** For each document, ask the four questions above. Then remove every candidate
|
|
64
|
-
that a regex could have found, and every one you cannot evidence to a specific line.
|
|
65
|
-
*Done when:* each surviving finding names, in one sentence, what the artifact cannot see about
|
|
66
|
-
itself — the `self_undetectable` field. If you cannot write that sentence, drop the finding.
|
|
67
|
-
|
|
68
|
-
3. **Propose, or withhold.** Where a change would genuinely fix it, write `proposal.before` as text
|
|
69
|
-
copied **verbatim** from the file and `proposal.after` as its replacement. `before` must appear in
|
|
70
|
-
the file **exactly once** — the apply path refuses a stale or ambiguous anchor rather than
|
|
71
|
-
guessing. Where the withheld test fires, propose nothing.
|
|
72
|
-
*Done when:* every finding either carries an exact-match proposal or is explicitly withheld.
|
|
73
|
-
|
|
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:
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
{
|
|
81
|
-
"at": "<ISO 8601>",
|
|
82
|
-
"read": ["planning/prd/example-prd.md"],
|
|
83
|
-
"findings": [
|
|
84
|
-
{
|
|
85
|
-
"title": "One sentence, the defect stated plainly.",
|
|
86
|
-
"file": "planning/prd/example-prd.md",
|
|
87
|
-
"kind": "metric-claim-mismatch",
|
|
88
|
-
"argument": "Why this is a defect — the reasoning, not a restatement.",
|
|
89
|
-
"self_undetectable": "What this artifact cannot see about itself.",
|
|
90
|
-
"evidence": [{ "file": "planning/prd/example-prd.md", "line": 17 }],
|
|
91
|
-
"withheld": false,
|
|
92
|
-
"proposal": { "before": "<verbatim text from the file>", "after": "<replacement>" }
|
|
93
|
-
}
|
|
94
|
-
]
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
`withheld` is a required boolean on every finding and is never inferred. A withheld finding sets
|
|
99
|
-
`"proposal": null` and adds `"withheld_reason"`.
|
|
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.
|
|
103
|
-
|
|
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:
|
|
107
|
-
|
|
108
|
-
```json
|
|
109
|
-
{"run_id":"<run-id>","kind":"review","initiative_id":"workspace-review-and-edit","question":"Document review — <n> findings"}
|
|
110
|
-
```
|
|
111
|
-
*Done when:* `needs.json` is a valid array containing the new object.
|
|
112
|
-
|
|
113
|
-
## Output contract — produce EXACTLY these sections
|
|
114
|
-
|
|
115
|
-
- **Read** — the documents actually read, one per line.
|
|
116
|
-
- **Findings** — one line per finding: the title and its `file:line`. `None this review.` if empty,
|
|
117
|
-
which is a good outcome, not a failed run.
|
|
118
|
-
- **Withheld** — findings deliberately left for the PM, with the one-line reason. `None this review.`
|
|
119
|
-
if empty.
|