create-pmos 0.8.0 → 0.10.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 +89 -0
- package/bin/create-pmos.js +69 -11
- 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/bin/pmos.js +9 -2
- package/workspace/lib/console-state.js +56 -1
- package/workspace/lib/git-info.js +197 -25
- package/workspace/lib/related-docs.js +107 -0
- package/workspace/lib/session-state.js +139 -0
- package/workspace/lib/transcript-read.js +28 -6
- package/workspace/package.json +1 -1
- package/workspace/public/app.js +434 -61
- package/workspace/public/index.html +24 -3
- package/workspace/public/styles.css +909 -572
- 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 +566 -39
package/README.md
CHANGED
|
@@ -333,6 +333,95 @@ 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.10.0
|
|
337
|
+
|
|
338
|
+
**Editing a document now does something for the rest of them.**
|
|
339
|
+
|
|
340
|
+
0.8.0 gave you an editor: change a markdown document in the workspace, see the diff as you type, and
|
|
341
|
+
have the change logged. This release adds the two halves that make that log worth keeping.
|
|
342
|
+
|
|
343
|
+
### After you save, you are offered what your change puts out of step
|
|
344
|
+
|
|
345
|
+
Not a scan of your repository — a list built from **your change**. A document only appears if it has a
|
|
346
|
+
concrete tie to what you edited:
|
|
347
|
+
|
|
348
|
+
- it links to the document you changed, or names it in frontmatter (`prd_path`, `rubric_path`, …)
|
|
349
|
+
- it shares that document's `initiative_id`
|
|
350
|
+
- it **quotes text your change altered** — the strongest signal, because it is now asserting something
|
|
351
|
+
the source no longer says
|
|
352
|
+
|
|
353
|
+
Each candidate says which of those it is, so you can judge the suggestion before spending anything on
|
|
354
|
+
it. **"Seems related" is deliberately not a rule.** It would widen this to your whole repository and
|
|
355
|
+
put you back in front of a queue of an agent's opinions.
|
|
356
|
+
|
|
357
|
+
You tick which documents an agent may read. It drafts the edit that brings each back in line, and each
|
|
358
|
+
draft comes back as a diff to apply or skip. Applying uses the same guard as every other write here:
|
|
359
|
+
the text being replaced must appear exactly once, or the write is refused rather than guessed at.
|
|
360
|
+
|
|
361
|
+
**It never blocks.** The save completed before any of this. Dismiss the offer and nothing is queued,
|
|
362
|
+
nothing badges, nothing waits for you.
|
|
363
|
+
|
|
364
|
+
### Your change log is now something an agent reads
|
|
365
|
+
|
|
366
|
+
Every save regenerates `change-context.md` in your repository's own state directory: the documents you
|
|
367
|
+
change most, the recent changes, and — the part that matters — **your own stated reasons**, in your
|
|
368
|
+
words. An agent reading it before working in your repo knows what you keep rewriting and why, so it
|
|
369
|
+
can stop producing the thing that provoked the rewrite.
|
|
370
|
+
|
|
371
|
+
That is why the editor asks for a reason at all. A reason nobody reads is just friction at save time.
|
|
372
|
+
|
|
373
|
+
Edits made by applying an alignment record **what they were aligned with**, so the log reads as a
|
|
374
|
+
chain rather than as unexplained changes appearing in files you did not open.
|
|
375
|
+
|
|
376
|
+
### Also carried: the 0.9.1 upgrade fix
|
|
377
|
+
|
|
378
|
+
**0.9.1 was never published.** Its fix — `create-pmos workspace` self-upgrading instead of quietly
|
|
379
|
+
launching your old installed app — is in this release. If the workspace has looked unchanged across
|
|
380
|
+
recent versions, that is why; run `npx create-pmos@latest workspace` once and it will self-heal onto
|
|
381
|
+
the current app. Hard-refresh the browser afterwards to drop the cached stylesheet.
|
|
382
|
+
|
|
383
|
+
## What's new in 0.9.1
|
|
384
|
+
|
|
385
|
+
**A fix so workspace upgrades actually reach you.** If you activated the Terminal Workspace in an
|
|
386
|
+
earlier version, `npx create-pmos@latest workspace` was quietly launching your *old* installed app —
|
|
387
|
+
so 0.9.0's restyle and whose-turn view never showed up. That was the bug, not a missing feature.
|
|
388
|
+
|
|
389
|
+
- **`create-pmos workspace` now self-upgrades.** It compares the bundled app's version against what
|
|
390
|
+
you have installed and **reinstalls when the bundle is newer** (then restarts the persistent server
|
|
391
|
+
so the new code actually serves). A same-version install still fast-launches — no needless
|
|
392
|
+
~20-second native rebuild.
|
|
393
|
+
- **`create-pmos workspace --reinstall`** — force it any time.
|
|
394
|
+
- **`pmos upgrade`** — the same, straight from the installed command.
|
|
395
|
+
- If the UI still looks unchanged right after an upgrade, **hard-refresh the browser**
|
|
396
|
+
(Cmd/Ctrl+Shift+R) to drop the cached stylesheet.
|
|
397
|
+
|
|
398
|
+
So if 0.9.0 looked like "not much changed," this is why: the code shipped, but your old install kept
|
|
399
|
+
running. Run `npx create-pmos@latest workspace` once and it will self-heal onto the current app.
|
|
400
|
+
|
|
401
|
+
## What's new in 0.9.0
|
|
402
|
+
|
|
403
|
+
**The Terminal Workspace now tells you whose turn it is.** The optional GUI got this release's real
|
|
404
|
+
work: it reads each session's transcript and shows, per session, whether the agent is still working
|
|
405
|
+
or **waiting on you** — so a wall of terminals becomes a queue you can triage at a glance instead of
|
|
406
|
+
a grid you have to click through.
|
|
407
|
+
|
|
408
|
+
- **Whose-turn, read from the actual transcript.** Each session row and tab carries its state —
|
|
409
|
+
agent-working vs waiting-on-you — derived from the real conversation, not a guess. The sessions
|
|
410
|
+
that need a decision surface; the ones still running recede.
|
|
411
|
+
- **A real design system, not a dev-tool skin.** The workspace was reskinned on a proper palette
|
|
412
|
+
with its own typography, and the fonts ship *inside* the package — nothing is fetched at runtime,
|
|
413
|
+
so it looks the same offline and behind a proxy.
|
|
414
|
+
- **Your git branch is in the frame.** Each session shows the branch it is on and lets you switch,
|
|
415
|
+
so `initiative/<id>` work and its worktree are visible where you are working, not a `git status`
|
|
416
|
+
away.
|
|
417
|
+
- **The router now makes you write the answers down.** `pmos-router` gained a step: the intake
|
|
418
|
+
answers (does this need its own instance? has this been rejected before?) get recorded on the
|
|
419
|
+
initiative, so a routing decision is enforceable later instead of evaporating the moment it's made.
|
|
420
|
+
|
|
421
|
+
All of this rides in `npx create-pmos@latest workspace`. The file-mode kit itself is unchanged and
|
|
422
|
+
still dependency-free; `npx create-pmos@latest --update` brings the router change into an existing
|
|
423
|
+
kit and never touches your `state/`, `planning/`, or `okf/product/` records.
|
|
424
|
+
|
|
336
425
|
## What's new in 0.8.0
|
|
337
426
|
|
|
338
427
|
**The document review is gone. There is a document editor instead.**
|
package/bin/create-pmos.js
CHANGED
|
@@ -140,24 +140,81 @@ function launchWorkspace(targetDir, viaGlobal) {
|
|
|
140
140
|
{ stdio: 'inherit', env: process.env }).status || 0;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
// Compare two dotted numeric versions. Returns -1 if a<b, 0 if equal, 1 if a>b. Missing/non-numeric
|
|
144
|
+
// segments count as 0, so 'unknown' sorts oldest — the safe default (reinstall rather than run stale).
|
|
145
|
+
function cmpVer(a, b) {
|
|
146
|
+
const pa = String(a || '0').split('.').map((n) => parseInt(n, 10) || 0);
|
|
147
|
+
const pb = String(b || '0').split('.').map((n) => parseInt(n, 10) || 0);
|
|
148
|
+
for (let i = 0; i < 3; i++) {
|
|
149
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
150
|
+
if (d) return d < 0 ? -1 : 1;
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
// Version of the workspace app bundled in THIS package. It is kept in lockstep with the create-pmos
|
|
155
|
+
// release version in workspace/package.json, so a newer package means a newer bundled app.
|
|
156
|
+
function bundledWorkspaceVersion() {
|
|
157
|
+
try { return require(path.join(WORKSPACE, 'package.json')).version || null; }
|
|
158
|
+
catch { return null; }
|
|
159
|
+
}
|
|
160
|
+
// Version of the globally-installed `pmos-workspace`, or null if absent/unreadable. Read from the
|
|
161
|
+
// npm global root rather than the app's CLI — no dependency on the old binary supporting --version.
|
|
162
|
+
function installedWorkspaceVersion() {
|
|
163
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
164
|
+
const r = spawnSync(npmCmd, ['root', '-g'], { encoding: 'utf8', shell: process.platform === 'win32' });
|
|
165
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
166
|
+
try { return require(path.join(r.stdout.trim(), 'pmos-workspace', 'package.json')).version || null; }
|
|
167
|
+
catch { return null; }
|
|
168
|
+
}
|
|
169
|
+
function installWorkspaceGlobal() {
|
|
170
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
171
|
+
return spawnSync(npmCmd, ['install', '-g', WORKSPACE, '--no-audit', '--no-fund'],
|
|
172
|
+
{ stdio: 'inherit', shell: process.platform === 'win32' }).status;
|
|
173
|
+
}
|
|
174
|
+
// A persistent server started by the OLD code keeps serving OLD assets after an upgrade — stop it so
|
|
175
|
+
// the next launch brings the server up on the freshly installed code. Best-effort; ignore failures.
|
|
176
|
+
function stopWorkspaceServer() {
|
|
177
|
+
try { spawnSync('pmos', ['stop'], { stdio: 'ignore', shell: process.platform === 'win32' }); }
|
|
178
|
+
catch { /* no server running, or pmos not launchable — nothing to stop */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function workspace(targetArg, opts) {
|
|
144
182
|
workspaceOrDie();
|
|
145
183
|
const targetDir = path.resolve(targetArg || '.');
|
|
184
|
+
const forceReinstall = !!(opts && opts.reinstall);
|
|
146
185
|
|
|
147
|
-
// Already activated?
|
|
186
|
+
// Already activated? Decide launch-vs-reinstall by VERSION, not merely by presence. The old
|
|
187
|
+
// behaviour fast-launched whenever `pmos` was on PATH, so every workspace improvement was invisible
|
|
188
|
+
// to anyone who had activated once — the installed app never advanced. Now: reinstall when forced,
|
|
189
|
+
// when the installed version can't be read, or when the bundled app is newer; otherwise fast-launch
|
|
190
|
+
// (no ~20s native rebuild for an already-current install).
|
|
148
191
|
if (onPath('pmos')) {
|
|
149
|
-
|
|
192
|
+
const bundled = bundledWorkspaceVersion();
|
|
193
|
+
const installed = installedWorkspaceVersion();
|
|
194
|
+
const stale = forceReinstall || installed === null || (bundled != null && cmpVer(installed, bundled) < 0);
|
|
195
|
+
if (!stale) {
|
|
196
|
+
console.log(`✅ workspace already current (pmos-workspace ${installed}) — launching. (Type \`pmos\` directly next time.)`);
|
|
197
|
+
process.exit(launchWorkspace(targetDir, true));
|
|
198
|
+
}
|
|
199
|
+
console.log(forceReinstall
|
|
200
|
+
? 'Reinstalling the Terminal Workspace app (--reinstall)…'
|
|
201
|
+
: `Upgrading the Terminal Workspace app (installed ${installed || 'unknown'} → bundled ${bundled})…`);
|
|
202
|
+
const status = installWorkspaceGlobal();
|
|
203
|
+
if (status === 0) {
|
|
204
|
+
stopWorkspaceServer(); // restart the persistent server on the new code
|
|
205
|
+
console.log(`\n✅ pmos-workspace is now ${bundled}. Launching — if the UI still looks unchanged, hard-refresh the browser (Cmd/Ctrl+Shift+R) to drop the cached stylesheet.`);
|
|
206
|
+
} else {
|
|
207
|
+
console.log('\n⚠ Reinstall did not complete cleanly — launching the previously installed copy.');
|
|
208
|
+
}
|
|
150
209
|
process.exit(launchWorkspace(targetDir, true));
|
|
151
210
|
}
|
|
152
211
|
|
|
212
|
+
// First-time activation (pmos not on PATH).
|
|
153
213
|
console.log('Activating the PMOS Terminal Workspace (one-time). Installing node-pty (native) + putting `pmos` on PATH…');
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
{ stdio: 'inherit', shell: process.platform === 'win32' });
|
|
157
|
-
|
|
158
|
-
if (g.status === 0 && onPath('pmos')) {
|
|
214
|
+
const status = installWorkspaceGlobal();
|
|
215
|
+
if (status === 0 && onPath('pmos')) {
|
|
159
216
|
console.log('\n✅ `pmos` is now on your PATH. From any repo folder, just type: pmos');
|
|
160
|
-
console.log(' pmos status · pmos stop · pmos doctor');
|
|
217
|
+
console.log(' pmos status · pmos stop · pmos doctor · pmos upgrade');
|
|
161
218
|
process.exit(launchWorkspace(targetDir, true));
|
|
162
219
|
}
|
|
163
220
|
|
|
@@ -177,14 +234,15 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
177
234
|
console.log('usage:');
|
|
178
235
|
console.log(' create-pmos [target-dir] vendor the file-mode kit into a repo');
|
|
179
236
|
console.log(' create-pmos --update [kit-or-repo] refresh only the kit kernel');
|
|
180
|
-
console.log(' create-pmos workspace [repo-dir] install + launch the Terminal Workspace GUI');
|
|
237
|
+
console.log(' create-pmos workspace [repo-dir] install/upgrade + launch the Terminal Workspace GUI');
|
|
238
|
+
console.log(' create-pmos workspace --reinstall force-reinstall the workspace app (drop a stale global copy)');
|
|
181
239
|
console.log(' create-pmos --version print version + bundled kernel provenance');
|
|
182
240
|
} else if (args.includes('--version') || args.includes('-v')) {
|
|
183
241
|
console.log(`create-pmos ${PKG.version}`);
|
|
184
242
|
console.log(kitVersion());
|
|
185
243
|
console.log(`workspace bundled: ${fs.existsSync(path.join(WORKSPACE, 'bin', 'pmos.js')) ? 'yes' : 'no'}`);
|
|
186
244
|
} else if (positional[0] === 'workspace') {
|
|
187
|
-
workspace(positional[1]);
|
|
245
|
+
workspace(positional[1], { reinstall: args.includes('--reinstall') });
|
|
188
246
|
} else if (args.includes('--update')) {
|
|
189
247
|
update(positional[0]);
|
|
190
248
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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
|
@@ -68,6 +68,28 @@ router is the bug (fix it via the weekly harness scan).
|
|
|
68
68
|
branch `initiative/<id>`, and the human Acceptance Gate at the end (D12) — the router never bypasses
|
|
69
69
|
[working-in-pmos](/okf/core/playbooks/working-in-pmos.md).
|
|
70
70
|
|
|
71
|
+
## Step 5 — WRITE THE ANSWERS DOWN (this is what makes any of it enforceable)
|
|
72
|
+
|
|
73
|
+
Steps 0.3 and 0.4 used to be asked aloud and then discarded, and the cost was exactly what you would
|
|
74
|
+
predict: nothing could check that a *needs-an-instance* answer was followed by a prototype, or that
|
|
75
|
+
an *existing surface* was explained before it was specified. An unrecorded answer is an
|
|
76
|
+
unenforceable one. So both land on the initiative record, as a **leading token plus your reasoning**:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
specimen_test: "needs-instance — no description settles whether …" # or "description-judgeable — …"
|
|
80
|
+
predecessor: <prototype-initiative-id> # required when the answer is needs-instance
|
|
81
|
+
surface_check: "existing — the Initiative Gate" # or "greenfield — …"
|
|
82
|
+
explainer_path: planning/explainers/<surface>-explainer.md # required when it is existing
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The gate ([`scripts/workflow-state.js`](/scripts/workflow-state.js)) reads the **token** and the
|
|
86
|
+
**ordering of the paths it implies** — the prose is for the PM. It fails a PR whose PRD first
|
|
87
|
+
appeared before its prototype brief (**O3**) or before its surface explainer (**O4**). It does not
|
|
88
|
+
judge whether the answer was *right*: no deterministic check can decide that, and over-firing the
|
|
89
|
+
specimen test twice running stays a D41 tripwire on the test's wording, not a gate condition.
|
|
90
|
+
|
|
91
|
+
Check your position at any time with `node scripts/workflow-state.js <id>`.
|
|
92
|
+
|
|
71
93
|
## Edge rules
|
|
72
94
|
|
|
73
95
|
- **Two shapes at once** (an idea that is also a bet): sharpness first, then the brief-offer, then
|
|
@@ -70,6 +70,28 @@ router is the bug (fix it via the weekly harness scan).
|
|
|
70
70
|
branch `initiative/<id>`, and the human Acceptance Gate at the end (D12) — the router never bypasses
|
|
71
71
|
[working-in-pmos](/okf/core/playbooks/working-in-pmos.md).
|
|
72
72
|
|
|
73
|
+
## Step 5 — WRITE THE ANSWERS DOWN (this is what makes any of it enforceable)
|
|
74
|
+
|
|
75
|
+
Steps 0.3 and 0.4 used to be asked aloud and then discarded, and the cost was exactly what you would
|
|
76
|
+
predict: nothing could check that a *needs-an-instance* answer was followed by a prototype, or that
|
|
77
|
+
an *existing surface* was explained before it was specified. An unrecorded answer is an
|
|
78
|
+
unenforceable one. So both land on the initiative record, as a **leading token plus your reasoning**:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
specimen_test: "needs-instance — no description settles whether …" # or "description-judgeable — …"
|
|
82
|
+
predecessor: <prototype-initiative-id> # required when the answer is needs-instance
|
|
83
|
+
surface_check: "existing — the Initiative Gate" # or "greenfield — …"
|
|
84
|
+
explainer_path: planning/explainers/<surface>-explainer.md # required when it is existing
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The gate ([`scripts/workflow-state.js`](/scripts/workflow-state.js)) reads the **token** and the
|
|
88
|
+
**ordering of the paths it implies** — the prose is for the PM. It fails a PR whose PRD first
|
|
89
|
+
appeared before its prototype brief (**O3**) or before its surface explainer (**O4**). It does not
|
|
90
|
+
judge whether the answer was *right*: no deterministic check can decide that, and over-firing the
|
|
91
|
+
specimen test twice running stays a D41 tripwire on the test's wording, not a gate condition.
|
|
92
|
+
|
|
93
|
+
Check your position at any time with `node scripts/workflow-state.js <id>`.
|
|
94
|
+
|
|
73
95
|
## Edge rules
|
|
74
96
|
|
|
75
97
|
- **Two shapes at once** (an idea that is also a bet): sharpness first, then the brief-offer, then
|
package/workspace/README.md
CHANGED
|
@@ -94,6 +94,34 @@ tokens, JWTs, `Authorization:` values, api keys): scrollback holds whatever the
|
|
|
94
94
|
so D08's never-log-headers discipline applies to what this hands back. Matching runs against
|
|
95
95
|
the *scrubbed* text, so the endpoint cannot be used to grep a secret out.
|
|
96
96
|
|
|
97
|
+
## Branches: a new one doesn't move the others
|
|
98
|
+
|
|
99
|
+
Starting a session, the branch control is **optional** — the default is "stay on current" and it
|
|
100
|
+
performs no git operation at all. When you do pick one:
|
|
101
|
+
|
|
102
|
+
- **`+ new branch…`** can give that branch its **own working tree**, at
|
|
103
|
+
`../<repo>-worktrees/<branch>` (a sibling of the repo — nothing appears inside it, nothing for
|
|
104
|
+
`.gitignore` to chase), registered as its own workspace. Every session already running stays
|
|
105
|
+
exactly where it was. The folder is named in the form before you commit to it, and again in the
|
|
106
|
+
footer once it exists.
|
|
107
|
+
- **That's the default only when there are sessions to protect.** Alone in a workspace a plain
|
|
108
|
+
checkout disturbs nobody, so no folder is made. The checkbox is yours to flip either way.
|
|
109
|
+
- **Unchecking "give it its own folder"** is the shared-tree checkout: this repo moves, and so does
|
|
110
|
+
every session in it. The hint counts them, and only warns when something really would move.
|
|
111
|
+
- A branch can live in **one** working tree at a time. One already open elsewhere is labelled with
|
|
112
|
+
its location in the picker, rather than failing later.
|
|
113
|
+
|
|
114
|
+
**⑂ worktrees** in the sidebar lists every working tree the repo has, and removes the ones that are
|
|
115
|
+
safe to remove. A removal is refused — with the reason — when the tree has sessions running in it,
|
|
116
|
+
uncommitted changes, or commits that no other branch, remote or tag holds. Removing one reclaims
|
|
117
|
+
the folder and **keeps the branch**; nothing is ever removed automatically. Git history is shared
|
|
118
|
+
between worktrees, so what a folder costs is a checkout of tracked files, not a second copy of the
|
|
119
|
+
repo.
|
|
120
|
+
|
|
121
|
+
A workspace anywhere **inside** a repo knows its branch — a subdirectory (`repo/packages/app`)
|
|
122
|
+
counts, which matters for a product repo added by path. A folder with **no git** simply says so and
|
|
123
|
+
starts the session; nothing about git can block work in a repo that doesn't use it.
|
|
124
|
+
|
|
97
125
|
## Boundaries (honest v1 seams)
|
|
98
126
|
- **Server binds 127.0.0.1** — the D62 hosted-PTY-bridge rejection stands; a tailnet/phone client
|
|
99
127
|
attaches to the same local server, never a public endpoint.
|
package/workspace/bin/pmos.js
CHANGED
|
@@ -211,10 +211,17 @@ function doctor() {
|
|
|
211
211
|
else if (cmd === 'ls' || cmd === 'list') code = await ls();
|
|
212
212
|
else if (cmd === 'stop') code = await stop();
|
|
213
213
|
else if (cmd === 'doctor') code = doctor();
|
|
214
|
-
else if (cmd === '
|
|
215
|
-
|
|
214
|
+
else if (cmd === 'upgrade') {
|
|
215
|
+
// Escape hatch: pull the latest bundled app and force-reinstall over this global copy. Delegates
|
|
216
|
+
// to the scaffolder (which owns the install path) rather than reinstalling itself in place.
|
|
217
|
+
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
218
|
+
code = spawnSync(npx, ['-y', 'create-pmos@latest', 'workspace', '--reinstall', process.cwd()],
|
|
219
|
+
{ stdio: 'inherit', shell: process.platform === 'win32' }).status || 0;
|
|
220
|
+
} else if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
221
|
+
say('usage: pmos [<path>|status|ls|stop|doctor|upgrade]');
|
|
216
222
|
say(' pmos open the current repo as a workspace (starts the server if needed)');
|
|
217
223
|
say(' pmos <path> open another repo as a workspace');
|
|
224
|
+
say(' pmos upgrade reinstall the workspace app from the latest published create-pmos');
|
|
218
225
|
code = 0;
|
|
219
226
|
} else if (cmd === 'up' || cmd === 'start' || cmd === 'open') {
|
|
220
227
|
// `pmos open ../other-repo` — the path is the argument AFTER the verb
|
|
@@ -114,4 +114,59 @@ function radar({ stateDir, snapshotDir }) {
|
|
|
114
114
|
} };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
/*
|
|
118
|
+
* ── workflow: where each initiative is, what is next, what is blocking ────────────────────────
|
|
119
|
+
*
|
|
120
|
+
* The one surface that answers "where am I in the loop" without the PM reconstructing it from
|
|
121
|
+
* files. It renders the SAME ladder the CLI and the Initiative Gate print, from the same engine —
|
|
122
|
+
* `scripts/workflow-state.js` — so the panel can never disagree with what CI is about to say.
|
|
123
|
+
*
|
|
124
|
+
* The engine is the one BUNDLED WITH THE WORKSPACE, pointed at the opened repo via its `root`
|
|
125
|
+
* option. It is deliberately not `require`d out of the opened directory: the engine only reads
|
|
126
|
+
* files and runs `git -C <root> log`, so no code from a workspace the PM merely opened is ever
|
|
127
|
+
* executed. An install that does not carry the engine gets an honest error, never a blank panel.
|
|
128
|
+
*/
|
|
129
|
+
function workflow({ root }) {
|
|
130
|
+
if (!root) return { ok: false, error: 'no workspace root resolved for the workflow surface' };
|
|
131
|
+
let engine;
|
|
132
|
+
try { engine = require(path.join(__dirname, '..', '..', 'scripts', 'workflow-state.js')); }
|
|
133
|
+
catch {
|
|
134
|
+
return { ok: false, error: 'the workflow engine (scripts/workflow-state.js) is not present in this install — the ladder cannot be derived here' };
|
|
135
|
+
}
|
|
136
|
+
let ids;
|
|
137
|
+
try { ids = engine.listInitiatives(root); }
|
|
138
|
+
catch (e) { return { ok: false, error: 'could not list initiative records: ' + String((e && e.message) || 'error').slice(0, 120) }; }
|
|
139
|
+
if (!ids.length) return { ok: true, empty: true, source: 'file-mode' };
|
|
140
|
+
|
|
141
|
+
// One commit range for the whole board — resolving it per initiative would re-shell git for each.
|
|
142
|
+
const range = engine.commitRange(root);
|
|
143
|
+
const rows = ids.map((id) => {
|
|
144
|
+
try { return engine.computeWorkflow(id, { root, range }); }
|
|
145
|
+
catch (e) { return { ok: false, id, error: String((e && e.message) || 'derivation failed').slice(0, 160) }; }
|
|
146
|
+
});
|
|
147
|
+
// Same `inFlight` the CLI and the gate use — the panel must not invent its own idea of what is
|
|
148
|
+
// open. Filtering by `legacy` here was a real bug: on main every record is legacy, so the panel
|
|
149
|
+
// read "0 in flight" against 93 initiatives, blank at exactly the moment someone opens it.
|
|
150
|
+
const { shown, hidden, total } = engine.inFlight(rows);
|
|
151
|
+
const shape = (r) => (r.ok ? {
|
|
152
|
+
id: r.id, title: r.title, lane: r.lane, stage: r.stage, legacy: r.legacy,
|
|
153
|
+
in_change: r.in_change, anchor: r.anchor,
|
|
154
|
+
record: r.record, blockers: r.blockers, next: r.next,
|
|
155
|
+
steps: r.steps.map((s) => ({ key: s.key, status: s.status, evidence: s.evidence, next: s.next, blocking: s.blocking, rule: s.rule })),
|
|
156
|
+
} : { id: r.id, error: r.error });
|
|
157
|
+
return {
|
|
158
|
+
ok: true, empty: false, source: 'file-mode',
|
|
159
|
+
data: {
|
|
160
|
+
live: shown.map(shape),
|
|
161
|
+
hidden, // reported on the client — a silent cap reads as "that is all"
|
|
162
|
+
total,
|
|
163
|
+
accepted: rows.filter((r) => r.ok && r.accepted).length,
|
|
164
|
+
unreadable: rows.filter((r) => !r.ok).map(shape),
|
|
165
|
+
blocked: shown.filter((r) => r.ok && r.blockers.length).length,
|
|
166
|
+
degraded: range.ok ? null : range.reason,
|
|
167
|
+
enforced_from: engine.ENFORCED_FROM,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { metrics, radar, workflow, computeMetricsFromRuns, loadJson, loadJsonl };
|