create-pmos 0.29.0 → 0.29.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 +45 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/lib/drive-guard.js +51 -6
- package/workspace/package.json +3 -3
- package/workspace/server.js +26 -1
package/README.md
CHANGED
|
@@ -333,6 +333,51 @@ 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.29.1
|
|
337
|
+
|
|
338
|
+
**0.29.0 could not reach an existing install. This is that release, delivered.**
|
|
339
|
+
Run `npx create-pmos@latest workspace`.
|
|
340
|
+
|
|
341
|
+
- **If you are on 0.28.0, 0.29.0 never upgraded you.** The launcher decides whether to reinstall by
|
|
342
|
+
comparing the bundled workspace-app version against the installed one, and 0.29.0 shipped with the
|
|
343
|
+
app version left at `0.28.0` — so the comparison came out equal and the upgrade was silently
|
|
344
|
+
skipped. Everything listed below was in the 0.29.0 tarball and unreachable from an existing
|
|
345
|
+
install. 0.29.1 bumps both versions together, which is what actually delivers it.
|
|
346
|
+
- **The check that catches this already existed and did not run.** An assertion pins the two versions
|
|
347
|
+
in lockstep; CI only ran it when `workspace/**` changed, and a release PR touches
|
|
348
|
+
`npm-pkg/package.json`. The path filter now includes it, so the guard runs on the diff that breaks
|
|
349
|
+
it. This is the second time a version bump has shipped without reaching adopters (0.9.1 was the
|
|
350
|
+
first), and the first time the repo could have known.
|
|
351
|
+
|
|
352
|
+
**What 0.29.0 carried, now actually delivered:**
|
|
353
|
+
|
|
354
|
+
- **A skipped Review Gate no longer shows as a green tick.** `workflow-state.js` rendered a
|
|
355
|
+
`SKIPPED` verdict — a row that says *judging did not happen* — with the same ✓ it uses for a real
|
|
356
|
+
pass, and a `FAIL` likewise. A skip is now `pending` and a fail is `blocked`. If your board looked
|
|
357
|
+
greener than your ledger, this is why.
|
|
358
|
+
- **A chore that carries a rubric now shows its Review Gate.** The rung decided from the
|
|
359
|
+
initiative's **lane**, so any lane that *may* skip a rubric could never surface a verdict it
|
|
360
|
+
actually had — the ladder printed *"chore lane carries no rubric"* while a verdict sat unread. It
|
|
361
|
+
reads the rubric now, in every lane. This is how a real initiative reached `accept` with its gate
|
|
362
|
+
never run.
|
|
363
|
+
- **`evaluator.skill`'s final step is runnable.** It told you to persist the verdict through
|
|
364
|
+
`save_eval` — a hosted-backend tool that **does not exist in file mode**, which is the mode the kit
|
|
365
|
+
runs in. The last instruction of the Review Gate could not be followed. It now names the writer for
|
|
366
|
+
your context: `pmos/scripts/pmos-log.sh eval …`.
|
|
367
|
+
- **`pmos-log.sh` can record a skipped gate.** Its verdict enum was `PASS|PASS-WITH-FINDINGS|FAIL`,
|
|
368
|
+
while `AGENTS.md` tells you *"a gate that was skipped is recorded as skipped — never leave a missing
|
|
369
|
+
eval to be read as a pass."* Following that instruction hit an error, and the only compliant option
|
|
370
|
+
was to edit the ledger by hand. Now: `pmos-log.sh eval <run> <initiative> SKIPPED "<reason>"`,
|
|
371
|
+
which requires the reason and records `score: null`.
|
|
372
|
+
- **The kit build now refuses to ship a skill that names a tool you do not have.** That class had
|
|
373
|
+
already shipped twice in the same sentence of the same skill. The check runs at build time and
|
|
374
|
+
fails the build; its one bound (per-reference, not per-line) is documented in `build-kit.sh`.
|
|
375
|
+
|
|
376
|
+
*Not in this release, stated so you do not go looking:* the same upstream round hardened two modules
|
|
377
|
+
that the kit does not run — `drive-guard.js` ships inside the workspace bundle but nothing requires
|
|
378
|
+
it, and `record-eval.js` is not in the payload at all. Those fixes are real, and they are not
|
|
379
|
+
something your kit was relying on.
|
|
380
|
+
|
|
336
381
|
## What's new in 0.28.0
|
|
337
382
|
|
|
338
383
|
**`search everything…` now searches every file in the folder, and any file it finds opens in a tab.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit \u2014 run the PMOS product-management workflow (initiative \u2192 lane-fit spec \u2192 contract \u2192 build \u2192 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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -202,11 +202,28 @@ const DISPOSITION_INTERNAL_RE = new RegExp(
|
|
|
202
202
|
*/
|
|
203
203
|
const DISPOSITION_VERB = '(?:PROMOTE[SD]?|ITERATE[SD]?|KILL(?:ED|S)?)';
|
|
204
204
|
|
|
205
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* DETERMINERS ONLY, and the narrowing is measured rather than argued.
|
|
207
|
+
*
|
|
208
|
+
* The first version of this set carried 40 words — determiners, auxiliaries and modals — on the
|
|
209
|
+
* reasoning that a passive clause ("was killed off upstream") is prose too. A third independent pass
|
|
210
|
+
* showed what that bought an attacker:
|
|
211
|
+
*
|
|
212
|
+
* status: got PROMOTED by PM wawan on 2026-07-28 prev=refused → PERMITTED
|
|
213
|
+
* status: not KILLED — shipped 2026-07-28 prev=refused → PERMITTED
|
|
214
|
+
*
|
|
215
|
+
* Neither is prose. Both are forged dispositions wearing an auxiliary. Swept against this
|
|
216
|
+
* repository's own records, the ONLY genuine prose that needs an exemption at all is
|
|
217
|
+
* determiner-led — "derived from *the promoted* prototype" — which is exactly the false positive the
|
|
218
|
+
* previous round was contracted to fix. The auxiliaries were paying for a case the corpus does not
|
|
219
|
+
* contain, in forgeable forms.
|
|
220
|
+
*
|
|
221
|
+
* A status line that reads "if a specimen is promoted" is now REFUSED. That is over-refusal, it is
|
|
222
|
+
* the safe direction, and it is the trade this guard exists to make: the cost is a driver rephrasing
|
|
223
|
+
* one status line; the cost of the other direction is a forged PM disposition.
|
|
224
|
+
*/
|
|
206
225
|
const PROSE_LEAD = new Set([
|
|
207
226
|
'the', 'a', 'an', 'this', 'that', 'these', 'those', 'its', 'their', 'our', 'his', 'her',
|
|
208
|
-
'was', 'were', 'been', 'being', 'is', 'are', 'be', 'get', 'gets', 'got', 'gotten',
|
|
209
|
-
'not', 'never', 'unless', 'if', 'when', 'would', 'could', 'should', 'may', 'might',
|
|
210
227
|
]);
|
|
211
228
|
|
|
212
229
|
/**
|
|
@@ -220,8 +237,20 @@ function isDisposition(value) {
|
|
|
220
237
|
let m;
|
|
221
238
|
while ((m = re.exec(v)) !== null) {
|
|
222
239
|
const before = v.slice(0, m.index + m[1].length);
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
/* ADJACENT, separated by whitespace and nothing else.
|
|
241
|
+
*
|
|
242
|
+
* This was `[^A-Za-z']*$` — "the last run of letters, skipping ANY non-letters" — so a colon, a
|
|
243
|
+
* comma, a quote or an em dash between the determiner and the verb kept the carve-out firing
|
|
244
|
+
* while destroying the grammar that justifies it. A third independent pass walked through it:
|
|
245
|
+
*
|
|
246
|
+
* status: CLOSED — verdict is "PROMOTED" (PM wawan) prev=refused → PERMITTED
|
|
247
|
+
* status: CLOSED. Leg A is: PROMOTED 2026-07-28 prev=refused → PERMITTED
|
|
248
|
+
*
|
|
249
|
+
* "is" is not modifying "PROMOTED" across a colon and a quote mark; it is a different clause.
|
|
250
|
+
* An exemption that ignores what sits between its two words is not a grammatical rule, it is a
|
|
251
|
+
* proximity coincidence. */
|
|
252
|
+
const prior = (before.match(/([A-Za-z']+)[ \t]*$/) || [])[1];
|
|
253
|
+
if (prior && PROSE_LEAD.has(prior.toLowerCase())) continue; // adjectival — prose
|
|
225
254
|
return true;
|
|
226
255
|
}
|
|
227
256
|
return false;
|
|
@@ -343,10 +372,26 @@ function carriesDisposition(value, seen) {
|
|
|
343
372
|
const isStatusKey = k.toLowerCase() === 'status' || k.toLowerCase() === 'disposition';
|
|
344
373
|
if (isStatusKey) {
|
|
345
374
|
// A status value may be a string, a Buffer, or a list of candidate values. All are inspected.
|
|
375
|
+
/* Judge what will be WRITTEN, not what the value's JS type happens to be.
|
|
376
|
+
*
|
|
377
|
+
* `typeof c === 'string'` let two shapes through that both serialize to a real disposition:
|
|
378
|
+
*
|
|
379
|
+
* { status: { toJSON() { return 'PROMOTED'; } } } → {"status":"PROMOTED"}
|
|
380
|
+
* { status: new String('PROMOTED') } → {"status":"PROMOTED"}
|
|
381
|
+
*
|
|
382
|
+
* The bytes that land on disk are a forged PM disposition; only the in-memory type differed.
|
|
383
|
+
* A third independent pass found both, and noted the asymmetry that gives it away: the same
|
|
384
|
+
* trick aimed at PM_approved was already caught, because THAT check reads the serialized form.
|
|
385
|
+
* This one now does too. */
|
|
346
386
|
const candidates = Array.isArray(v) ? v : [v];
|
|
347
387
|
for (const c of candidates) {
|
|
348
388
|
if (typeof c === 'string' && isDisposition(c)) return true;
|
|
349
389
|
if (Buffer.isBuffer(c) && isDisposition(c.toString('utf8'))) return true;
|
|
390
|
+
if (c !== null && typeof c === 'object' && !Buffer.isBuffer(c)) {
|
|
391
|
+
let ser;
|
|
392
|
+
try { ser = JSON.stringify(c); } catch { return true; } // uninspectable → refuse
|
|
393
|
+
if (typeof ser === 'string' && isDisposition(ser)) return true;
|
|
394
|
+
}
|
|
350
395
|
}
|
|
351
396
|
}
|
|
352
397
|
if (Buffer.isBuffer(v)) continue; // a Buffer's numeric indices are not an object graph
|
|
@@ -484,6 +529,6 @@ function guardedWriter(write) {
|
|
|
484
529
|
}
|
|
485
530
|
|
|
486
531
|
module.exports = {
|
|
487
|
-
assertWritable, guardedWriter, carriesApproval, carriesDisposition, ForbiddenWrite,
|
|
532
|
+
assertWritable, guardedWriter, carriesApproval, carriesDisposition, isDisposition, PROSE_LEAD, ForbiddenWrite,
|
|
488
533
|
ALLOWED_DIR_PREFIXES, ALLOWED_BASENAMES, HUMAN_DECISION_SINKS, DISPOSITION_RE,
|
|
489
534
|
};
|
package/workspace/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "PMOS Terminal Workspace
|
|
5
|
+
"description": "PMOS Terminal Workspace \u2014 local companion hosting real PTYs beside the library and gates (D62)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
8
|
"pmos": "bin/pmos.js"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"playwright-core": "^1.61.1",
|
|
30
30
|
"ws": "^8.18.0"
|
|
31
31
|
},
|
|
32
|
-
"//devDependencies": "design-parity's pixel axis only. Kept OUT of dependencies deliberately: `create-pmos workspace` installs this package with `npm install -g`, which takes dependencies and not devDependencies, so an adopter installing a local PM tool does not also install a screenshot differ. design-parity.js requires both optionally and degrades to text+style+geometry when they are absent, so the parity check still runs
|
|
32
|
+
"//devDependencies": "design-parity's pixel axis only. Kept OUT of dependencies deliberately: `create-pmos workspace` installs this package with `npm install -g`, which takes dependencies and not devDependencies, so an adopter installing a local PM tool does not also install a screenshot differ. design-parity.js requires both optionally and degrades to text+style+geometry when they are absent, so the parity check still runs \u2014 it just stops measuring pixels, and says so.",
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"pixelmatch": "^7.1.0",
|
|
35
35
|
"pngjs": "^7.0.0"
|
package/workspace/server.js
CHANGED
|
@@ -866,6 +866,25 @@ function searchIndex(root) {
|
|
|
866
866
|
return entry;
|
|
867
867
|
}
|
|
868
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Is the FIRST chunk of this file free of NUL bytes? The bounded form of the reader's own refusal.
|
|
871
|
+
*
|
|
872
|
+
* `readTextDoc` scans the whole file, which is right for serving it and wrong for labelling 200
|
|
873
|
+
* search hits. This reads a fixed head — enough to catch the binary-masquerading-as-text case the
|
|
874
|
+
* reader refuses on, at a cost that does not scale with file size. A file whose head is clean and
|
|
875
|
+
* whose tail hides a NUL will still be labelled openable and still fail to open; that is a strictly
|
|
876
|
+
* smaller lie than labelling every large text file unopenable, and it is stated rather than hidden.
|
|
877
|
+
*/
|
|
878
|
+
function headIsText(full) {
|
|
879
|
+
let fd = null;
|
|
880
|
+
try {
|
|
881
|
+
fd = fs.openSync(full, 'r');
|
|
882
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
883
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
884
|
+
return buf.subarray(0, n).indexOf(0) === -1;
|
|
885
|
+
} catch { return false; } finally { if (fd !== null) { try { fs.closeSync(fd); } catch { /* ignore */ } } }
|
|
886
|
+
}
|
|
887
|
+
|
|
869
888
|
/** One matched line, trimmed and centred on the needle, so a minified file cannot flood the row. */
|
|
870
889
|
function searchSnippet(line, at) {
|
|
871
890
|
const s = String(line || '').replace(/\t/g, ' ');
|
|
@@ -918,7 +937,13 @@ function searchWorkspace(root, query, limit) {
|
|
|
918
937
|
* the honest answer is "we did not look", and the safe rendering of that is a hit the PM
|
|
919
938
|
* cannot click into an unknown. */
|
|
920
939
|
const st = fs.statSync(path.join(root, rel));
|
|
921
|
-
if (!st.isFile()
|
|
940
|
+
if (!st.isFile()) return false;
|
|
941
|
+
/* Past the byte budget we do not READ the file — but "we did not look" is not "the reader
|
|
942
|
+
* refuses it". Returning false there swapped one false label for its opposite: a legitimate
|
|
943
|
+
* 316 KB markdown doc reported openable:false while /api/doc served it 200 with 324,017 bytes.
|
|
944
|
+
* An independent pass measured it. The NUL test only needs the HEAD of the file, so bound the
|
|
945
|
+
* read instead of abandoning the question. */
|
|
946
|
+
if (st.size > SEARCH_CONTENT_BYTES) return headIsText(path.join(root, rel));
|
|
922
947
|
return readTextDoc(root, rel) !== null;
|
|
923
948
|
} catch { return false; }
|
|
924
949
|
};
|