create-pmos 0.13.0 → 0.14.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 +20 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/lib/drive-guard.js +98 -0
- package/workspace/lib/drive-plan.js +97 -0
- package/workspace/package.json +1 -1
package/README.md
CHANGED
|
@@ -333,6 +333,26 @@ 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.14.0
|
|
337
|
+
|
|
338
|
+
**The foundation of an autonomous workflow driver lands in the workspace.** Two new libraries
|
|
339
|
+
(`workspace/lib/drive-plan.js` + `drive-guard.js`) give the workflow loop a head and a conscience:
|
|
340
|
+
|
|
341
|
+
- **A planner that knows when to stop.** The workflow ladder never returns "nothing left to do" —
|
|
342
|
+
it always names a next honest action, so a naive `while (next)` loop never exits. The planner
|
|
343
|
+
decides `dispatch` vs `stop` on three explicit triggers: **human-gate** (the next step is a PM
|
|
344
|
+
decision — a contract or an acceptance), **no-progress** (the same next action twice running means
|
|
345
|
+
the last dispatch moved nothing — a stop, not a retry), and **budget** (a hard ceiling so an
|
|
346
|
+
unforeseen case still terminates).
|
|
347
|
+
- **A guard that can't sign the PM's name.** A gate in PMOS is "a file with the right words in it,"
|
|
348
|
+
which an autonomous writer could satisfy. The guard is the driver's *only* write path and refuses
|
|
349
|
+
to write acceptances, a `PM_approved` flag (at any nesting depth, or smuggled as raw text), or a
|
|
350
|
+
disposition token — so the loop can advance work but can never self-approve past a human gate.
|
|
351
|
+
|
|
352
|
+
This is the planner + guard (14 tests cover them); the return path, an independent verify pass, and
|
|
353
|
+
a live board run are the next increments under the same contract. Run `npx create-pmos@latest
|
|
354
|
+
workspace` to pick it up — the workspace version bump means an existing install auto-upgrades.
|
|
355
|
+
|
|
336
356
|
## What's new in 0.13.0
|
|
337
357
|
|
|
338
358
|
**Six more skills now travel with the kit** — the allowlist had been shipping dangling references to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* drive-guard — the driver's ONLY write path (pmos-autonomous-drive-build, R9).
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM THIS EXISTS FOR, stated plainly because it is easy to under-rate:
|
|
5
|
+
* nothing in PMOS checks an identity. `/api/gate` sets `pm: true` unconditionally
|
|
6
|
+
* (workspace/server.js). Every gate the corpus calls "the PM's" — D16's contract approval, D12's
|
|
7
|
+
* acceptance, D60's disposition — is, mechanically, A FILE WITH THE RIGHT WORDS IN IT. An
|
|
8
|
+
* autonomous writer can satisfy all of them by writing. If it can, the PM's gate structure is
|
|
9
|
+
* decorative and the whole three-tier eval gate is theatre.
|
|
10
|
+
*
|
|
11
|
+
* The harness already knows how to make a declaration falsifiable and did not apply it here: O7
|
|
12
|
+
* validates `priors` ids against the corpus, so a fabricated citation FAILS — while O5's
|
|
13
|
+
* `PM_approved` is a bare boolean, so a fabricated approval PASSES. This module is the smallest
|
|
14
|
+
* honest answer available to code: the driver cannot write those records because the only writer it
|
|
15
|
+
* has REFUSES them.
|
|
16
|
+
*
|
|
17
|
+
* WHY REFUSAL AND NOT OMISSION. A driver that simply never calls the acceptance writer is one
|
|
18
|
+
* prompt away from calling it. The rubric's veto dimension asks for structural incapability and
|
|
19
|
+
* says so: "a code path that merely omits them fails this dimension". So the guard is a chokepoint
|
|
20
|
+
* that throws, and the tests ATTEMPT each forbidden write and assert the throw. A rule the driver
|
|
21
|
+
* is trusted to follow is exactly what leg A showed cannot be trusted.
|
|
22
|
+
*
|
|
23
|
+
* SCOPE, honestly. This binds the DRIVER, not the repo: a human, another agent, or a careless
|
|
24
|
+
* `fs.writeFileSync` elsewhere is unaffected. It is a seatbelt on one vehicle, not a law of
|
|
25
|
+
* physics — and that limit is the argument for making O5's boolean falsifiable at the engine, which
|
|
26
|
+
* is a separate initiative this one does not pretend to have done.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
class ForbiddenWrite extends Error {
|
|
32
|
+
constructor(what, detail) {
|
|
33
|
+
super(`drive-guard: refused to write ${what} — ${detail}`);
|
|
34
|
+
this.name = 'ForbiddenWrite';
|
|
35
|
+
this.what = what;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Ledgers that record a human decision. The driver may never append to these. */
|
|
40
|
+
const FORBIDDEN_FILES = Object.freeze(['acceptances.jsonl']);
|
|
41
|
+
|
|
42
|
+
/** Disposition tokens (D60). Case-insensitive; a status carrying one is a PM act. */
|
|
43
|
+
const DISPOSITION_RE = /\b(PROMOTED|ITERATE[D]?|KILLED)\b/i;
|
|
44
|
+
|
|
45
|
+
/** True when `value` contains a truthy PM_approved anywhere in its object graph. */
|
|
46
|
+
function carriesApproval(value, depth = 0) {
|
|
47
|
+
if (depth > 8 || value === null || typeof value !== 'object') return false;
|
|
48
|
+
for (const [k, v] of Object.entries(value)) {
|
|
49
|
+
if (k === 'PM_approved' && v === true) return true;
|
|
50
|
+
if (k === 'pre_run_contract' && v && typeof v === 'object' && v.PM_approved === true) return true;
|
|
51
|
+
if (carriesApproval(v, depth + 1)) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The chokepoint. Every driver write goes through here or does not happen.
|
|
58
|
+
* @param {{file:string, payload?:any, text?:string}} req
|
|
59
|
+
* @throws {ForbiddenWrite}
|
|
60
|
+
*/
|
|
61
|
+
function assertWritable(req) {
|
|
62
|
+
const file = (req && req.file) || '';
|
|
63
|
+
const base = path.basename(file);
|
|
64
|
+
|
|
65
|
+
if (FORBIDDEN_FILES.includes(base)) {
|
|
66
|
+
throw new ForbiddenWrite('an acceptance', 'the Acceptance Gate is the PM\'s and non-delegable (D12)');
|
|
67
|
+
}
|
|
68
|
+
if (req && req.payload !== undefined && carriesApproval(req.payload)) {
|
|
69
|
+
throw new ForbiddenWrite('pre_run_contract.PM_approved',
|
|
70
|
+
'the contract is the PM\'s to approve before any build (D16, AGENTS.md §4)');
|
|
71
|
+
}
|
|
72
|
+
if (req && typeof req.text === 'string' && /PM_approved\s*[:=]\s*true/i.test(req.text)) {
|
|
73
|
+
throw new ForbiddenWrite('pre_run_contract.PM_approved',
|
|
74
|
+
'the contract is the PM\'s to approve before any build (D16) — caught in raw text');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A disposition is a PM act (D60) wherever it lands: the record's `status:` is where it is
|
|
78
|
+
// conventionally written, so both the structured and the raw-text shapes are refused.
|
|
79
|
+
const status = req && req.payload && typeof req.payload === 'object' ? req.payload.status : null;
|
|
80
|
+
if (typeof status === 'string' && DISPOSITION_RE.test(status)) {
|
|
81
|
+
throw new ForbiddenWrite('a disposition', 'promote/iterate/kill is the PM\'s (D60 step 7)');
|
|
82
|
+
}
|
|
83
|
+
if (req && typeof req.text === 'string' && /^status:.*$/im.test(req.text)
|
|
84
|
+
&& DISPOSITION_RE.test(req.text.match(/^status:.*$/im)[0])) {
|
|
85
|
+
throw new ForbiddenWrite('a disposition', 'promote/iterate/kill is the PM\'s (D60 step 7)');
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Wrap a writer so it cannot be called with a forbidden payload. The driver is given ONLY this. */
|
|
91
|
+
function guardedWriter(write) {
|
|
92
|
+
return function guarded(req) {
|
|
93
|
+
assertWritable(req);
|
|
94
|
+
return write(req);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { assertWritable, guardedWriter, carriesApproval, ForbiddenWrite, FORBIDDEN_FILES };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* drive-plan — the autonomous drive's decision function (pmos-autonomous-drive-build, R1/R2/R10).
|
|
3
|
+
*
|
|
4
|
+
* PURE. Given a ladder (scripts/workflow-state.js `<id> --json`) and the loop's own memory, decide
|
|
5
|
+
* whether to dispatch the next step or stop. No I/O, no clock, no spawning — so every rule below is
|
|
6
|
+
* testable without a PTY, which is the point: the rules are the risky part, not the plumbing.
|
|
7
|
+
*
|
|
8
|
+
* WHY A TERMINATION RULE EXISTS AT ALL. The engine's `next` is NEVER null — measured across the
|
|
9
|
+
* board, 0 of 25 sampled initiatives return one. It always names something, because there is always
|
|
10
|
+
* a next honest action even on finished work. So `while (next) dispatch()` never exits. The counter
|
|
11
|
+
* cannot tell the driver when to stop, and the driver must decide for itself. That is R2, and it is
|
|
12
|
+
* the difference between a loop and a runaway.
|
|
13
|
+
*
|
|
14
|
+
* THE THREE STOPS (R2), in the order they are checked:
|
|
15
|
+
* 1. human-gate — the next action is a decision, not work. D16 contract, D12 acceptance.
|
|
16
|
+
* 2. no-progress — the same next action twice running. A step that did not move the ladder will
|
|
17
|
+
* not move it by being run again; retrying is how a loop burns a budget looking
|
|
18
|
+
* busy. NO PROGRESS IS A STOP, NOT A RETRY.
|
|
19
|
+
* 3. budget — a hard ceiling, so a case nobody predicted still terminates.
|
|
20
|
+
*
|
|
21
|
+
* The driver never writes anything from here. Deciding and writing are separate on purpose — see
|
|
22
|
+
* drive-guard.js, which is what makes the human-gate stop structural rather than merely intended.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Ladder steps whose next action is a HUMAN decision the driver may never take (R9/R10).
|
|
26
|
+
* `contract` is D16 (no build begins without PM approval); `accept` is D12 (non-delegable).
|
|
27
|
+
* Deliberately NOT here: `review` (an evaluator agent scores it) and `quality` (CI decides). */
|
|
28
|
+
const HUMAN_GATES = Object.freeze(['contract', 'accept']);
|
|
29
|
+
|
|
30
|
+
/** A disposition is the PM's (D60) and is recorded on the initiative record rather than as a
|
|
31
|
+
* ladder step, so it cannot appear as `next.step`. Named for the guard, which does enforce it. */
|
|
32
|
+
const HUMAN_RECORDS = Object.freeze(['disposition', 'acceptance', 'pre_run_contract.PM_approved']);
|
|
33
|
+
|
|
34
|
+
const DEFAULT_BUDGET = 12;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} ladder parsed `workflow-state.js <id> --json`
|
|
38
|
+
* @param {object} memory { steps: number, lastKey: string|null } the loop's own history
|
|
39
|
+
* @param {object} [opts] { budget?: number }
|
|
40
|
+
* @returns {{action:'dispatch'|'stop', reason:string, step?:string, why?:string, detail:string}}
|
|
41
|
+
*/
|
|
42
|
+
function plan(ladder, memory, opts) {
|
|
43
|
+
const budget = (opts && Number.isFinite(opts.budget)) ? opts.budget : DEFAULT_BUDGET;
|
|
44
|
+
const mem = memory || { steps: 0, lastKey: null };
|
|
45
|
+
|
|
46
|
+
// An unreadable ladder is a stop, never a guess. The engine reports `ok:false` when it cannot
|
|
47
|
+
// derive; proceeding on that would be inventing state, which R1 forbids.
|
|
48
|
+
if (!ladder || ladder.ok === false) {
|
|
49
|
+
return { action: 'stop', reason: 'unreadable',
|
|
50
|
+
detail: 'the ladder could not be derived — the driver never invents state (R1)' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Accepted work is finished work. Checked before `next`, which still names an action even here.
|
|
54
|
+
if (ladder.accepted) {
|
|
55
|
+
return { action: 'stop', reason: 'complete',
|
|
56
|
+
detail: `${ladder.id} has a recorded acceptance — nothing further is owed` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const next = ladder.next;
|
|
60
|
+
if (!next || !next.step) {
|
|
61
|
+
return { action: 'stop', reason: 'no-next',
|
|
62
|
+
detail: 'the ladder named no next step (unexpected — `next` is normally never null)' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 1. HUMAN GATE. The decision is the PM's; the driver stops and names what is owed.
|
|
66
|
+
if (HUMAN_GATES.includes(next.step)) {
|
|
67
|
+
return { action: 'stop', reason: 'human-gate', step: next.step, why: next.why,
|
|
68
|
+
detail: `${next.step} is the PM's decision — ${next.why || 'no reason given'}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. NO PROGRESS. Same step + same reason as last iteration means the previous dispatch changed
|
|
72
|
+
// nothing the engine can see. Running it again is not a retry, it is the same failure twice.
|
|
73
|
+
const key = `${next.step}::${next.why || ''}`;
|
|
74
|
+
if (mem.lastKey && mem.lastKey === key) {
|
|
75
|
+
return { action: 'stop', reason: 'no-progress', step: next.step, why: next.why,
|
|
76
|
+
detail: `'${next.step}' is unchanged after a dispatch — stopping rather than retrying` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. BUDGET. Checked last so a genuine human-gate or no-progress stop is reported as itself
|
|
80
|
+
// rather than masked by an exhausted counter.
|
|
81
|
+
if (mem.steps >= budget) {
|
|
82
|
+
return { action: 'stop', reason: 'budget', step: next.step,
|
|
83
|
+
detail: `step budget of ${budget} exhausted — refusing to continue silently (R13)` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { action: 'dispatch', reason: 'next-action', step: next.step, why: next.why,
|
|
87
|
+
detail: next.action || `advance '${next.step}'` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Fold a decision into the loop's memory. Pure; the caller owns the value. */
|
|
91
|
+
function remember(memory, decision) {
|
|
92
|
+
const mem = memory || { steps: 0, lastKey: null };
|
|
93
|
+
if (!decision || decision.action !== 'dispatch') return mem;
|
|
94
|
+
return { steps: mem.steps + 1, lastKey: `${decision.step}::${decision.why || ''}` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { plan, remember, HUMAN_GATES, HUMAN_RECORDS, DEFAULT_BUDGET };
|
package/workspace/package.json
CHANGED