create-pmos 0.25.0 → 0.27.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 +27 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/workspace/README.md +33 -18
- package/workspace/bin/pmos.js +5 -1
- package/workspace/doctor.js +5 -1
- package/workspace/lib/skill-store.js +180 -6
- package/workspace/lib/state-root.js +587 -0
- package/workspace/lib/working-set.js +124 -28
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +2 -2
- package/workspace/public/pm/data.js +86 -0
- package/workspace/public/pm/panel-detail.js +174 -2
- package/workspace/public/pm/panel-initiatives.js +131 -16
- package/workspace/public/pm/panel-newsess.js +507 -0
- package/workspace/public/pm/panel-repos.js +31 -0
- package/workspace/public/pm/panel-tabs.js +34 -2
- package/workspace/public/pm/state.js +45 -1
- package/workspace/public/pm/tab-term.js +288 -15
- package/workspace/server.js +278 -24
- package/workspace/test-run.js +84 -0
package/README.md
CHANGED
|
@@ -333,6 +333,33 @@ 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.26.0
|
|
337
|
+
|
|
338
|
+
**Your sessions now know which initiative they belong to — whatever branch you are on — and each
|
|
339
|
+
initiative shows the agents running on it.** Run `npx create-pmos@latest workspace`; the version bump
|
|
340
|
+
means an existing install auto-upgrades.
|
|
341
|
+
|
|
342
|
+
- **`NOW · working on` stopped missing sessions.** It could only recognise a session by the *branch*
|
|
343
|
+
its folder was on — `initiative/<id>` — which is true of almost no real checkout. Work on `main`,
|
|
344
|
+
or on a branch your agent harness named, and the board could not see the agent you had running.
|
|
345
|
+
A session now **records** which initiative it is for when you start it from one, so it is
|
|
346
|
+
attributed whatever branch you are on, and that survives restarting the workspace.
|
|
347
|
+
- **Nothing is guessed, and nothing is hidden.** Where no initiative was recorded, the
|
|
348
|
+
`initiative/<id>` branch still answers — and the row tells you when the attribution is only a
|
|
349
|
+
branch reading rather than something PMOS was told. A session it genuinely cannot place is listed
|
|
350
|
+
under **not on an initiative** instead of vanishing: an agent that is running should never be
|
|
351
|
+
invisible on the screen whose job is to tell you what is running.
|
|
352
|
+
- **Bind a session that is already running.** `+ bind` records what a session is for — including
|
|
353
|
+
every session that was running before this shipped, and any you started outside PMOS. It is stored
|
|
354
|
+
as **your declaration**, kept distinct from what PMOS was told first-hand, and re-binding tells you
|
|
355
|
+
what it replaced. Binding never touches the process: same session, same folder, still running.
|
|
356
|
+
- **The initiative panel lists its live sessions.** Select an initiative and you see the agents on
|
|
357
|
+
it, each with the same word the sidebar uses — `needs you` · `working` · `quiet 12m` ·
|
|
358
|
+
`maybe blocked` — and its profile.
|
|
359
|
+
- **Click one to get to it.** If its tab is already open, you go to that tab. If not, a tab opens
|
|
360
|
+
**attached** to the running session, scrollback and all. It never starts a second agent — the row
|
|
361
|
+
says which of the two will happen before you click.
|
|
362
|
+
|
|
336
363
|
## What's new in 0.24.0
|
|
337
364
|
|
|
338
365
|
**The initiative board says which work is yours, and which of it has an agent running.** Run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
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
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
kernel-source: pmos@
|
|
2
|
-
assembled: 2026-
|
|
1
|
+
kernel-source: pmos@86ae610
|
|
2
|
+
assembled: 2026-08-02
|
|
3
3
|
mode: vendor
|
package/workspace/README.md
CHANGED
|
@@ -17,7 +17,13 @@ true terminal emulator on the other end. The CLI runs **untouched** — its own
|
|
|
17
17
|
subscription included), slash commands, keybindings. The workspace is the terminal, not a wrapper.
|
|
18
18
|
|
|
19
19
|
## Everything survives a session
|
|
20
|
-
The server holds PTYs in memory and mirrors each session's scrollback + metadata to
|
|
20
|
+
The server holds PTYs in memory and mirrors each session's scrollback + metadata to the runtime
|
|
21
|
+
state directory — **`~/Library/Application Support/pmos` on macOS**, `$XDG_STATE_HOME/pmos` or
|
|
22
|
+
`~/.local/state/pmos` elsewhere, `PMOS_STATE_DIR` overriding both. It is deliberately OUTSIDE the
|
|
23
|
+
app directory: sessions, the workspace registry, layouts, scratchpads, schedules and your authored
|
|
24
|
+
skills used to live at `workspace/state/`, inside the directory the test suite runs from, and were
|
|
25
|
+
twice destroyed by a test. An existing in-repo `state/` is moved there once, on first run; two
|
|
26
|
+
populated directories are never merged (you get told, and nothing moves).
|
|
21
27
|
Close/reload the client → re-attach replays scrollback and the run continues (detach, not kill).
|
|
22
28
|
Restart the server → prior sessions reload as `exited` with full history and one-click respawn.
|
|
23
29
|
|
|
@@ -68,17 +74,22 @@ bar and a 26px status bar, on the oklch token layer, in both themes:
|
|
|
68
74
|
box that forces the groups open and reports matches elsewhere, and an add form. Unanchored
|
|
69
75
|
initiatives are filed honestly and shown **rejected** under D59, never hidden.
|
|
70
76
|
A **`NOW · working on`** group is pinned above the objectives when — and only when — there is
|
|
71
|
-
something to say: every initiative with a live session
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
something to say: every initiative with a live session, a registered working tree on its
|
|
78
|
+
`initiative/<id>` branch, or a tab open in this window, ordered so whatever is **waiting on you**
|
|
79
|
+
is the first row on the board. The three signals are ranked, never merged, and each row's tooltip
|
|
80
|
+
names which one put it there. On a row with a live session the leading `#` becomes that session's
|
|
81
|
+
own glyph — `◆ needs you`, `● working`, `◇ maybe blocked`, `◼ quiet` — in the vocabulary
|
|
82
|
+
`lib/session-state.js` uses everywhere else, with every session named individually on hover. The
|
|
83
|
+
glyphs differ in **shape**, so the signal survives greyscale.
|
|
84
|
+
**A session records which initiative it is for**, so `+ session` on an initiative is attributed
|
|
85
|
+
to it whatever branch you are on — and that survives a server restart. Where nothing was recorded
|
|
86
|
+
the `initiative/<id>` branch still answers, and the row says when the attribution is only a branch
|
|
87
|
+
reading. A session PMOS cannot place is never guessed at **and never hidden**: it is listed under
|
|
88
|
+
*not on an initiative*, and `+ bind` records what it is for as your own declaration.
|
|
89
|
+
3. **Initiative detail** — the record's header and pills (type / anchor / flow / branch), the
|
|
90
|
+
**sessions** running on it (each with the same word the sidebar uses — click one to go to its tab
|
|
91
|
+
if it is open, or to open a tab **attached** to the running session; nothing here ever starts an
|
|
92
|
+
agent), the derived footer, **task phases** (collapsible; a phase's status is derived from its tasks, and the tasks
|
|
82
93
|
are derived from the workflow ladder unless the record authors them), a **links** section for
|
|
83
94
|
references that are not repo files, and **artifacts** with an external destination and the sync
|
|
84
95
|
panel. A record that fails to read renders **red with the parse error** — never a blank panel.
|
|
@@ -118,12 +129,16 @@ would be the watermelon this repo has a word for.
|
|
|
118
129
|
clear the matching needs entry — only ever on your click.
|
|
119
130
|
|
|
120
131
|
**The record lands in the repo that earned it.** Each workspace resolves its own state dir —
|
|
121
|
-
`pmos/state/` (the kit's layout, D54), then `state/`, then `.pmos/`;
|
|
122
|
-
|
|
123
|
-
write. That path matters: `scripts/metrics.py` reads
|
|
124
|
-
star and κ, so an acceptance recorded anywhere else is
|
|
125
|
-
plain (non-PMOS) repo has no layout to invent — the gate
|
|
126
|
-
the response says `fallback: true` rather than guessing.
|
|
132
|
+
`pmos/state/` (the kit's layout, D54), then `state/`, then `.pmos/`; an embedded PMOS-self checkout
|
|
133
|
+
with none of those yet uses the install's own `workspace/state/`. A PMOS-shaped repo with no state
|
|
134
|
+
dir yet gets the kit layout on first write. That path matters: `scripts/metrics.py` reads
|
|
135
|
+
`pmos/state/acceptances.jsonl` for the north star and κ, so an acceptance recorded anywhere else is
|
|
136
|
+
a green click over an empty record. A plain (non-PMOS) repo has no layout to invent — the gate
|
|
137
|
+
records into `workspace/state/` and the response says `fallback: true` rather than guessing.
|
|
138
|
+
|
|
139
|
+
This is D64's **system of record** and it did **not** follow the runtime state out to the per-user
|
|
140
|
+
directory above. A record belongs to the repository that earned it; only the churny, machine-written
|
|
141
|
+
runtime moved.
|
|
127
142
|
|
|
128
143
|
Records carry the kit's `accepted` field (`accept` → true, `return` → false) so file-mode metrics
|
|
129
144
|
count them. `pm_label` is **never** derived from a click — κ labels are the PM's own word (D40).
|
package/workspace/bin/pmos.js
CHANGED
|
@@ -28,9 +28,13 @@ const fs = require('fs');
|
|
|
28
28
|
const path = require('path');
|
|
29
29
|
const { spawn, spawnSync } = require('child_process');
|
|
30
30
|
const D = require('../lib/server-discovery');
|
|
31
|
+
const stateRoot = require('../lib/state-root');
|
|
31
32
|
|
|
32
33
|
const WS = path.resolve(__dirname, '..'); // workspace/
|
|
33
|
-
|
|
34
|
+
// The launcher and the server must agree on WHERE state is, or the launcher looks for pid files in
|
|
35
|
+
// a directory nothing writes to and starts a second server every time. Same resolver, same answer
|
|
36
|
+
// — including the one-time migration out of the app directory (lib/state-root.js).
|
|
37
|
+
const STATE = stateRoot.resolveStateRoot({ env: process.env, legacy: path.join(WS, 'state') }).root;
|
|
34
38
|
// An explicit PMOS_WS_PORT is a PIN: this launcher then talks to that port and no other. Without
|
|
35
39
|
// one, the port comes from discovery — the default first, then whatever the running servers
|
|
36
40
|
// recorded for themselves.
|
package/workspace/doctor.js
CHANGED
|
@@ -124,7 +124,11 @@ function binaryArch(file) {
|
|
|
124
124
|
// when none is running, whether the port it would use is free and who holds it if not.
|
|
125
125
|
{
|
|
126
126
|
const D = require('./lib/server-discovery');
|
|
127
|
-
const
|
|
127
|
+
const stateRoot = require('./lib/state-root');
|
|
128
|
+
// The same resolver the server and the launcher use — a doctor that looked in the app
|
|
129
|
+
// directory would report "not running" over a live workspace (lib/state-root.js). No migration
|
|
130
|
+
// is triggered from here: a diagnostic must not move the PM's files.
|
|
131
|
+
const STATE = stateRoot.defaultRoot({ env: process.env });
|
|
128
132
|
const pinned = process.env.PMOS_WS_PORT ? Number(process.env.PMOS_WS_PORT) : null;
|
|
129
133
|
const ports = D.candidatePorts({ stateDir: STATE, pinned });
|
|
130
134
|
const probe = (port) => new Promise((res) => {
|
|
@@ -96,10 +96,173 @@ function loadSkills(dirs) {
|
|
|
96
96
|
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/* ── THE FILING CONTRACT OF A SKILL THAT ALREADY EXISTS ────────────────────────────────────────
|
|
100
|
+
*
|
|
101
|
+
* Every skill scaffolded before the runtime state moved out of the application directory says, in
|
|
102
|
+
* its body, "write the deliverable to `workspace/state/deliverables/<run-id>.md` and append to
|
|
103
|
+
* `workspace/state/needs.json`". After the move those are two paths nothing reads. An agent that
|
|
104
|
+
* follows them succeeds, reports success, and the deliverable never appears in the needs-you queue
|
|
105
|
+
* — a REPORT THAT SILENTLY NEVER ARRIVES, which is the exact failure this initiative exists to
|
|
106
|
+
* end. The PM has eleven such skills (draft-me, release-notes and nine idpf-*), all authored
|
|
107
|
+
* against the old contract.
|
|
108
|
+
*
|
|
109
|
+
* Injecting the paths into the SCAFFOLD only fixes skills that do not exist yet. So the retarget
|
|
110
|
+
* also happens at FIRE TIME, to the prompt as it is handed to the session — which covers the
|
|
111
|
+
* eleven, covers a skill hand-copied from an old machine, and covers a file the PM edits back to
|
|
112
|
+
* the old wording. It also has to be per-fire rather than a one-off rewrite of the files, because
|
|
113
|
+
* `needs.json` is resolved PER WORKSPACE (server.js#needsFileFor): one skill fired against two
|
|
114
|
+
* repos files into two different queues, and a path baked into the file could only ever name one.
|
|
115
|
+
*
|
|
116
|
+
* ── WHAT COUNTS AS "THE OLD STATE ROOT" ───────────────────────────────────────────────────────
|
|
117
|
+
*
|
|
118
|
+
* The first cut of this matched on the CHARACTER BEFORE the path — start-of-line, whitespace, a
|
|
119
|
+
* quote, a backtick, a paren or a bracket — and that covered 2 of the PM's 11 skills. The other
|
|
120
|
+
* nine (every idpf-*) do not spell the path relatively at all; they spell it ABSOLUTELY:
|
|
121
|
+
*
|
|
122
|
+
* /Users/…/pmos/workspace/state/deliverables/<run-id>.md
|
|
123
|
+
*
|
|
124
|
+
* The character before `workspace/state/` there is `/`, which was not in the class, so nothing
|
|
125
|
+
* matched and nothing was rewritten. That is not an edge case: every one of the PM's 45 schedules
|
|
126
|
+
* fires an idpf-* skill, and the two skills the old rule DID repair have zero schedules between
|
|
127
|
+
* them. A near-miss on a character class was therefore 0% coverage of the real workload.
|
|
128
|
+
*
|
|
129
|
+
* So the question is no longer "what character precedes this" but "IS THIS PATH'S PARENT THE OLD
|
|
130
|
+
* RUNTIME STATE ROOT". We answer it by recovering the whole path token and asking about its
|
|
131
|
+
* DIRECTORY, which is decidable regardless of how the author chose to spell it:
|
|
132
|
+
*
|
|
133
|
+
* `state/…` `./state/…` — relative to the app directory
|
|
134
|
+
* `workspace/state/…` `./workspace/state/…` — relative to a repository root
|
|
135
|
+
* `/any/abs/path/workspace/state/…` — absolute, any install (the nine idpf-*)
|
|
136
|
+
* `<filing.legacy>/…` — the app directory's OWN state dir, derived by the
|
|
137
|
+
* caller from __dirname, whatever it is named
|
|
138
|
+
*
|
|
139
|
+
* And it must still leave `pmos/state/…` alone — D64's file-mode system of record, which did NOT
|
|
140
|
+
* move. That now falls out STRUCTURALLY rather than from a character class: `pmos/` is a directory
|
|
141
|
+
* segment that is neither `workspace/` nor the derived legacy root, so it is refused by the same
|
|
142
|
+
* test that accepts the others. `sched-state/deliverables` is refused for the same reason — the
|
|
143
|
+
* `state` we rewrite has to be a whole path segment, not the tail of a longer name.
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
/* ── WHY THE WALK-BACK HAS TO KNOW ABOUT SCHEMES AND BOUNDARIES ────────────────────────────────
|
|
147
|
+
*
|
|
148
|
+
* The rule above ("is this path's parent the old runtime state root") is answered by recovering the
|
|
149
|
+
* whole path token and asking about its directory. Recovering it by walking LEFT through path
|
|
150
|
+
* characters is right, but a walk-back that stops at the first non-path character does not know
|
|
151
|
+
* WHAT it stopped at — and three of the things it can stop at mean the text to the right is not a
|
|
152
|
+
* path token at all but the tail of something larger:
|
|
153
|
+
*
|
|
154
|
+
* https://example.com/workspace/state/deliverables/x.md stops at `:` → `https:` + the rewrite
|
|
155
|
+
* C:/Users/x/workspace/state/deliverables/x.md stops at `:` → `C:` + the rewrite
|
|
156
|
+
* C:\Users\x\workspace/state/deliverables/x.md stops at `\`
|
|
157
|
+
*
|
|
158
|
+
* In each of those the URL or the drive path is DESTROYED, not merely over-rewritten — and this is
|
|
159
|
+
* not confined to fire time: POST /api/skills and /api/skills/draft run scaffoldSkill, which
|
|
160
|
+
* retargets the body and PERSISTS it, so a PM pasting a body containing such a URL gets it mangled
|
|
161
|
+
* on disk. So the character that STOPPED the walk-back is itself part of the decision: the token is
|
|
162
|
+
* only a path when what precedes it is a real boundary (start of text, whitespace, a quote, a
|
|
163
|
+
* bracket, a comma). A `:`, a `\`, a `?`, a `&`, an `=` or an `@` means we are inside a larger
|
|
164
|
+
* token whose grammar this module does not know, and the only safe answer is to leave it alone.
|
|
165
|
+
*
|
|
166
|
+
* The same defect in the other direction is the TAIL: `needs.json` carried no trailing boundary
|
|
167
|
+
* where `deliverables` did, so `workspace/state/needs.jsonl` matched `…needs.json` and left a
|
|
168
|
+
* dangling `l` on the rewritten path. Both alternatives now end on the same boundary class.
|
|
169
|
+
*
|
|
170
|
+
* ── AND: A `workspace/` DIRECTORY IS NOT AUTOMATICALLY A PMOS INSTALL ─────────────────────────
|
|
171
|
+
*
|
|
172
|
+
* `…/notes/workspace/state/deliverables/` has the same SHAPE as the old root and is not it. For an
|
|
173
|
+
* ABSOLUTE path we can simply look, and the three answers are all different:
|
|
174
|
+
*
|
|
175
|
+
* absent from this machine → ACCEPT. A path that no longer exists is the case the retarget was
|
|
176
|
+
* written for: a skill authored on an older checkout or another
|
|
177
|
+
* machine, naming a directory that has since moved. Refusing here
|
|
178
|
+
* would restore the silent no-show for exactly the bodies that need
|
|
179
|
+
* the rewrite most.
|
|
180
|
+
* present and a PMOS workspace → ACCEPT. Another checkout of this application; its `state/` is
|
|
181
|
+
* the old root by construction.
|
|
182
|
+
* present and something else → REFUSE. Somebody's `workspace/` folder, not ours.
|
|
183
|
+
*
|
|
184
|
+
* A RELATIVE path cannot be resolved without a working directory, so only the two spellings the
|
|
185
|
+
* scaffold and the PM's skills actually use are accepted (`workspace/state/…`, `./workspace/…`).
|
|
186
|
+
* A deeper relative prefix (`example.com/workspace/`, `../workspace/`) is refused: it is either not
|
|
187
|
+
* a path at all or not one this module can judge.
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
// The tail: `state/deliverables` or `state/needs.json`. Everything to its LEFT is recovered by
|
|
191
|
+
// walking back through path characters, not by a lookbehind — a path token can be arbitrarily long
|
|
192
|
+
// and we need the whole of it to judge the directory. BOTH alternatives end on the boundary class:
|
|
193
|
+
// without it `state/needs.jsonl` rewrote to `<needs>l`.
|
|
194
|
+
const TAIL_END = "(?=$|[/`'\")\\],\\s])";
|
|
195
|
+
const LEGACY_TAIL = new RegExp(`state\\/(?:needs\\.json${TAIL_END}|deliverables${TAIL_END})`, 'g');
|
|
196
|
+
const PATH_CHAR = /[A-Za-z0-9_.~/-]/;
|
|
197
|
+
// What may sit immediately LEFT of a whole path token. Deliberately an allow-list: `:` (a URL
|
|
198
|
+
// scheme, a Windows drive), `\`, `?`, `&`, `=`, `@`, `#` and every other character are refused,
|
|
199
|
+
// because they mean the walk-back stopped inside a larger token rather than at its start.
|
|
200
|
+
const TOKEN_BOUNDARY = /[\s`'"([{<,;|>]/;
|
|
201
|
+
|
|
202
|
+
/** Absent → true (unknowable, and the stale-path case this rewrite exists for); else: is it ours? */
|
|
203
|
+
function isPmosWorkspaceDir(dir) {
|
|
204
|
+
try {
|
|
205
|
+
if (!fs.existsSync(dir)) return true;
|
|
206
|
+
let name = '';
|
|
207
|
+
try { name = String((JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')) || {}).name || ''); }
|
|
208
|
+
catch { /* no package.json, or not JSON — fall through to the shape check */ }
|
|
209
|
+
if (name === 'pmos-workspace') return true;
|
|
210
|
+
return fs.existsSync(path.join(dir, 'server.js')) && fs.existsSync(path.join(dir, 'lib', 'skill-store.js'));
|
|
211
|
+
} catch { return true; } // a directory we cannot even stat is not evidence AGAINST the rewrite
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Is `prefix` (the text immediately left of `state/…`, up to a non-path character) the old root? */
|
|
215
|
+
function isLegacyStateParent(prefix, legacyRoot) {
|
|
216
|
+
// `state` must be a WHOLE segment: either the token starts here, or its parent ends in a slash.
|
|
217
|
+
if (prefix !== '' && !prefix.endsWith('/')) return false;
|
|
218
|
+
if (prefix === '' || prefix === './') return true; // state/… · ./state/…
|
|
219
|
+
if (prefix === 'workspace/' || prefix === './workspace/') return true; // the relative spelling
|
|
220
|
+
// The app directory's own state dir, derived by the caller from __dirname. Covers an install
|
|
221
|
+
// that does not sit under a directory literally named `workspace`. Checked BEFORE the probe, so
|
|
222
|
+
// the install we are running inside is never subject to it.
|
|
223
|
+
if (legacyRoot) {
|
|
224
|
+
const parent = legacyRoot.slice(0, legacyRoot.lastIndexOf('/') + 1);
|
|
225
|
+
const name = legacyRoot.slice(legacyRoot.lastIndexOf('/') + 1);
|
|
226
|
+
if (name === 'state' && parent && (prefix === parent || prefix === '.' + parent)) return true;
|
|
227
|
+
}
|
|
228
|
+
// …/workspace/state/… — absolute only (`//host/…` is an authority, not a path), and only when
|
|
229
|
+
// that `workspace` is, or plausibly was, a PMOS install.
|
|
230
|
+
if (prefix.startsWith('/') && !prefix.startsWith('//') && prefix.endsWith('/workspace/')) {
|
|
231
|
+
return isPmosWorkspaceDir(prefix.slice(0, -1));
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Rewrite every reference to the OLD runtime state root onto the paths this fire actually files to.
|
|
238
|
+
* `filing` is `{ deliverables, needs, legacy? }`; `legacy` is the application directory's own
|
|
239
|
+
* `state/` (server.js#LEGACY_STATE) and is optional — the segment rules above stand without it.
|
|
240
|
+
*/
|
|
241
|
+
function retargetFiling(text, filing) {
|
|
242
|
+
if (!text || !filing) return text;
|
|
243
|
+
const legacyRoot = filing.legacy ? String(filing.legacy).replace(/\/+$/, '') : '';
|
|
244
|
+
const src = String(text);
|
|
245
|
+
let out = ''; let cursor = 0; let m;
|
|
246
|
+
LEGACY_TAIL.lastIndex = 0;
|
|
247
|
+
while ((m = LEGACY_TAIL.exec(src))) {
|
|
248
|
+
let start = m.index;
|
|
249
|
+
while (start > 0 && PATH_CHAR.test(src[start - 1])) start--;
|
|
250
|
+
if (start < cursor) continue; // overlaps a rewrite already emitted
|
|
251
|
+
// The character that STOPPED the walk-back decides whether we recovered a whole path token or
|
|
252
|
+
// the tail of a URL / a drive path / a query parameter. See the block comment above.
|
|
253
|
+
if (start > 0 && !TOKEN_BOUNDARY.test(src[start - 1])) continue;
|
|
254
|
+
const target = m[0].startsWith('state/needs') ? filing.needs : filing.deliverables;
|
|
255
|
+
if (!target || !isLegacyStateParent(src.slice(start, m.index), legacyRoot)) continue;
|
|
256
|
+
out += src.slice(cursor, start) + target;
|
|
257
|
+
cursor = m.index + m[0].length;
|
|
258
|
+
}
|
|
259
|
+
return out + src.slice(cursor);
|
|
260
|
+
}
|
|
261
|
+
|
|
99
262
|
// Scaffold a well-formed user skill file from the New Skill flow's fields. Returns { ok, slug, content }
|
|
100
263
|
// or { ok:false, error }. User skills default to run:agent — a user-authored skill reasons through a
|
|
101
264
|
// claude session (the existing agent path); the deterministic fast-path is reserved for the built-ins.
|
|
102
|
-
function scaffoldSkill({ name, description, output, body } = {}) {
|
|
265
|
+
function scaffoldSkill({ name, description, output, body, filing } = {}) {
|
|
103
266
|
const slug = slugify(name);
|
|
104
267
|
if (!NAME_RE.test(slug)) return { ok: false, error: 'name must be a short slug (letters, digits, hyphens)' };
|
|
105
268
|
const label = titleCase(name) || slug;
|
|
@@ -107,7 +270,9 @@ function scaffoldSkill({ name, description, output, body } = {}) {
|
|
|
107
270
|
let out = Array.isArray(output) ? output : String(output || '').split(/[\n,]/);
|
|
108
271
|
out = out.map((x) => String(x).replace(/\s+/g, ' ').trim()).filter(Boolean).slice(0, MAX_OUTPUT);
|
|
109
272
|
if (!out.length) out = ['Summary', 'Findings', 'Next steps']; // always a valid contract
|
|
110
|
-
|
|
273
|
+
// A hand-written body is retargeted too: a PM pasting the old contract text in gets a skill that
|
|
274
|
+
// files where the console reads, not one that quietly writes into a directory nobody opens.
|
|
275
|
+
const proc = retargetFiling(String(body || '').trim(), filing) || defaultBody(label, desc, out, slug, filing);
|
|
111
276
|
const content = [
|
|
112
277
|
'---',
|
|
113
278
|
`name: ${slug}`,
|
|
@@ -131,8 +296,16 @@ function scaffoldSkill({ name, description, output, body } = {}) {
|
|
|
131
296
|
// write the deliverable and append a STRUCTURED needs.json object, or the console reds the queue
|
|
132
297
|
// (needsResult treats a non-array queue as corrupt). No prose "note to needs-you", ever. This is a
|
|
133
298
|
// starting shape a PM or the AI-draft session then sharpens with the task's real steps.
|
|
134
|
-
|
|
299
|
+
// `filing` names the two paths the run must write to. They are INJECTED, not hard-coded, because
|
|
300
|
+
// the workspace's runtime state no longer lives at a path that can be spelled relative to a
|
|
301
|
+
// repository — it moved out of the application directory (lib/state-root.js) precisely so the test
|
|
302
|
+
// suite could not reach it. A scaffold that still said "workspace/state/deliverables/" would send
|
|
303
|
+
// every scheduled agent's report to a directory the console never reads: a silent, permanent
|
|
304
|
+
// no-show in the needs-you queue. The defaults keep this module usable standalone.
|
|
305
|
+
function defaultBody(label, desc, out, slug, filing) {
|
|
135
306
|
const id = slug || slugify(label) || 'run';
|
|
307
|
+
const deliverables = (filing && filing.deliverables) || 'workspace/state/deliverables';
|
|
308
|
+
const needs = (filing && filing.needs) || 'workspace/state/needs.json';
|
|
136
309
|
return [
|
|
137
310
|
`You are running the "${label}" skill in this PMOS workspace (local, file mode). Work only from`,
|
|
138
311
|
"this repository's own files, git, and CLIs — no network (D54).",
|
|
@@ -148,8 +321,8 @@ function defaultBody(label, desc, out, slug) {
|
|
|
148
321
|
' An empty section is the literal line "None this period." — never a dropped heading, never invented content.',
|
|
149
322
|
' Done when: every contract section is filled or explicitly "None this period.".',
|
|
150
323
|
`3. File the result so the PM sees it. Pick a unique run id (e.g. \`${id}-<UTC-timestamp>\`), then:`,
|
|
151
|
-
|
|
152
|
-
|
|
324
|
+
` a. Write the deliverable Markdown to \`${deliverables}/<run-id>.md\`.`,
|
|
325
|
+
` b. Read \`${needs}\` (treat a missing file as \`[]\`), append EXACTLY this object,`,
|
|
153
326
|
' and write the whole array back — it MUST stay a valid JSON array or the console reds the queue:',
|
|
154
327
|
` {"run_id":"<run-id>","kind":"report","initiative_id":"${id}","question":"${label} — ready","deliverable":"<run-id>"}`,
|
|
155
328
|
' Done when: the deliverable file exists and needs.json is a valid array containing the new object.',
|
|
@@ -159,4 +332,5 @@ function defaultBody(label, desc, out, slug) {
|
|
|
159
332
|
].join('\n');
|
|
160
333
|
}
|
|
161
334
|
|
|
162
|
-
module.exports = { NAME_RE, slugify, titleCase, parseSkill, loadDir, loadSkills, scaffoldSkill
|
|
335
|
+
module.exports = { NAME_RE, slugify, titleCase, parseSkill, loadDir, loadSkills, scaffoldSkill,
|
|
336
|
+
retargetFiling, isLegacyStateParent };
|