create-pmos 0.11.1 → 0.12.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/payload/skills/prototype.skill +16 -6
- package/payload/skills-native/prototype/SKILL.md +16 -6
- package/workspace/lib/harness-parse.js +8 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/app.js +106 -13
- package/workspace/public/index.html +6 -1
- package/workspace/public/styles.css +10 -0
- package/workspace/server.js +32 -7
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.12.1
|
|
337
|
+
|
|
338
|
+
A small kernel refresh: the **`prototype` skill's grilling step** (leg A of the D60 prototype loop)
|
|
339
|
+
now dispatches an **independent griller** as a distinct, fresh-context critic — the specimens are
|
|
340
|
+
sharpened by a genuine second party, not the same agent reviewing its own work. Update an existing
|
|
341
|
+
kit with `npx create-pmos@latest --update`. (The Terminal Workspace app is unchanged in this
|
|
342
|
+
release, so an already-current `pmos` install won't reinstall.)
|
|
343
|
+
|
|
344
|
+
## What's new in 0.12.0
|
|
345
|
+
|
|
346
|
+
**The sidebar says what is actually true.** 0.11.x told you things were *unreadable* when they were
|
|
347
|
+
simply *not there* — and used the same red for both.
|
|
348
|
+
|
|
349
|
+
If you pointed the workspace at an ordinary folder or a repository that isn't a PMOS repo, almost
|
|
350
|
+
everything read as an error. None of it was.
|
|
351
|
+
|
|
352
|
+
- **A folder that isn't a git repository** now says so, quietly. It used to say `branch unreadable`,
|
|
353
|
+
which is an alarm about a completely ordinary directory.
|
|
354
|
+
- **A repository with no initiative records** reads **no initiatives yet** — a fact about the
|
|
355
|
+
repository, calmly stated. Pointing the workspace at any repo you like is a supported thing to do.
|
|
356
|
+
- **A shell session** no longer reports its non-existent transcript as a failed identification. A
|
|
357
|
+
shell is a prompt, not an agent narrating itself.
|
|
358
|
+
- **A row keeps its name.** Previously a plain folder rendered as a row titled `branch unreadable` —
|
|
359
|
+
a row with no name at all, unfindable in a tree of them.
|
|
360
|
+
|
|
361
|
+
Red still means red: a corrupt state queue, a genuine git failure, or an engine that is installed but
|
|
362
|
+
throws all still draw the alarm. The point is that they no longer share it with the normal case.
|
|
363
|
+
|
|
364
|
+
Three controls also stopped being ambiguous:
|
|
365
|
+
|
|
366
|
+
- **`+ new session — run an agent here`** now names the repository and branch it will start in.
|
|
367
|
+
"Here" meant the selected tree all along; with every repository on screen at once, nothing said
|
|
368
|
+
which that was.
|
|
369
|
+
- **`+ session here`** starts a session. It used to select the tree and stop.
|
|
370
|
+
- **A row with no sessions no longer expands** just because something needs your attention — that
|
|
371
|
+
count is already shown on the row, and opening an empty container to "reveal" it taught you to
|
|
372
|
+
distrust the tree.
|
|
373
|
+
|
|
374
|
+
And the pane is called **repos & sessions** rather than `work`.
|
|
375
|
+
|
|
376
|
+
> **One disclosure.** The Review Gate for this work was not scored before release. Its rubric makes
|
|
377
|
+
> the honesty checks *runnable* precisely so they cannot be satisfied by reading the code, and they
|
|
378
|
+
> were not run by an independent evaluator. Everything above was verified by the automated suites
|
|
379
|
+
> (412 checks) and by hand; none of it was independently reviewed.
|
|
380
|
+
|
|
336
381
|
## What's new in 0.11.1
|
|
337
382
|
|
|
338
383
|
**Fixes: the ladder never actually worked.** If you installed 0.11.0 and every row in the new
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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
|
@@ -109,12 +109,22 @@ system, a convention set, or a prototype to validate an idea.
|
|
|
109
109
|
applies in-loop; claim-sourcing waits for the content lane at production). The non-visual analogue
|
|
110
110
|
is the same rule: real inputs and real outputs, because fake ones hide exactly the friction the
|
|
111
111
|
specimen exists to surface. Respect the accessibility floors even at low fidelity.
|
|
112
|
-
6. **Grill — dialogue, never a score
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
threshold, no `evals` row** — re-introducing scoring
|
|
112
|
+
6. **Grill — dialogue, never a score. Dispatch
|
|
113
|
+
[`prototype-grill`](/build-skills/prototype-grill.skill).** A craft-grade critic (fresh context,
|
|
114
|
+
default-to-refute stance, the class's craft loaded) interrogates each specimen: *why this
|
|
115
|
+
structure? what job does the focal element serve? what would the best practitioner in this class
|
|
116
|
+
say is wrong? what was NOT tried?* It writes `prototype/<id>/grilling.md`. Sharpen the specimens
|
|
117
|
+
from it. **Produces no score, no verdict, no threshold, no `evals` row** — re-introducing scoring
|
|
118
|
+
here under any name is a D60 violation.
|
|
119
|
+
|
|
120
|
+
**The critic must be a DIFFERENT agent from the one that produced the specimens.** This was prose
|
|
121
|
+
here from the day the lane existed, with nothing to dispatch and nothing to check it — so the
|
|
122
|
+
cheapest path was to grill your own work, and a dialogue with one party is a monologue with extra
|
|
123
|
+
steps. It stopped being hypothetical on 2026-07-29, when
|
|
124
|
+
[`r2-free-zone-baseline.md`](/prototype/pmos-autonomous-drive/r2-free-zone-baseline.md) grilled
|
|
125
|
+
its own specimens and flagged itself for it. If no second agent is available, **report that as a
|
|
126
|
+
blocker** and let the PM knowingly accept a self-grilling — a recorded compromise, never a
|
|
127
|
+
fabricated dialogue.
|
|
118
128
|
7. **PM disposition** (the only gate on leg A, and it is the PM's):
|
|
119
129
|
- **promote** → open a **new feature-lane initiative**; its PRD and rubric are **derived from the
|
|
120
130
|
validated specimen** (what was seen and chosen becomes the spec's substance). Leg B, where the
|
|
@@ -111,12 +111,22 @@ system, a convention set, or a prototype to validate an idea.
|
|
|
111
111
|
applies in-loop; claim-sourcing waits for the content lane at production). The non-visual analogue
|
|
112
112
|
is the same rule: real inputs and real outputs, because fake ones hide exactly the friction the
|
|
113
113
|
specimen exists to surface. Respect the accessibility floors even at low fidelity.
|
|
114
|
-
6. **Grill — dialogue, never a score
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
threshold, no `evals` row** — re-introducing scoring
|
|
114
|
+
6. **Grill — dialogue, never a score. Dispatch
|
|
115
|
+
[`prototype-grill`](/build-skills/prototype-grill.skill).** A craft-grade critic (fresh context,
|
|
116
|
+
default-to-refute stance, the class's craft loaded) interrogates each specimen: *why this
|
|
117
|
+
structure? what job does the focal element serve? what would the best practitioner in this class
|
|
118
|
+
say is wrong? what was NOT tried?* It writes `prototype/<id>/grilling.md`. Sharpen the specimens
|
|
119
|
+
from it. **Produces no score, no verdict, no threshold, no `evals` row** — re-introducing scoring
|
|
120
|
+
here under any name is a D60 violation.
|
|
121
|
+
|
|
122
|
+
**The critic must be a DIFFERENT agent from the one that produced the specimens.** This was prose
|
|
123
|
+
here from the day the lane existed, with nothing to dispatch and nothing to check it — so the
|
|
124
|
+
cheapest path was to grill your own work, and a dialogue with one party is a monologue with extra
|
|
125
|
+
steps. It stopped being hypothetical on 2026-07-29, when
|
|
126
|
+
[`r2-free-zone-baseline.md`](/prototype/pmos-autonomous-drive/r2-free-zone-baseline.md) grilled
|
|
127
|
+
its own specimens and flagged itself for it. If no second agent is available, **report that as a
|
|
128
|
+
blocker** and let the PM knowingly accept a self-grilling — a recorded compromise, never a
|
|
129
|
+
fabricated dialogue.
|
|
120
130
|
7. **PM disposition** (the only gate on leg A, and it is the PM's):
|
|
121
131
|
- **promote** → open a **new feature-lane initiative**; its PRD and rubric are **derived from the
|
|
122
132
|
validated specimen** (what was seen and chosen becomes the spec's substance). Leg B, where the
|
|
@@ -52,6 +52,8 @@ const EFFORT = /\beffort[:\s]+(low|medium|high|xhigh|max)\b/i;
|
|
|
52
52
|
const TAIL_BYTES = 32_768;
|
|
53
53
|
|
|
54
54
|
const NOT_CLAUDE = 'only claude sessions are parsed';
|
|
55
|
+
// A shell is a prompt, not an agent narrating itself. Absence here is the design, not a read failure.
|
|
56
|
+
const SHELL_NO_WORDS = 'a shell — it narrates nothing to read back';
|
|
55
57
|
const UNREADABLE = "can't read its output — no recognisable Claude Code blocks";
|
|
56
58
|
|
|
57
59
|
/** Split the scrubbed tail into lines, dropping a partial first line. */
|
|
@@ -153,7 +155,12 @@ function parseHarness(scrubbed, agent) {
|
|
|
153
155
|
const out = { lastWords: null, tools: null, model: null, context: null, missing: {} };
|
|
154
156
|
|
|
155
157
|
if (agent !== 'claude') {
|
|
156
|
-
|
|
158
|
+
// R4 — a SHELL has no "last words" in the sense this parser means, and never will: it is a
|
|
159
|
+
// prompt, not an agent narrating itself. "can't read its last words" describes a failed attempt
|
|
160
|
+
// at something that was never going to happen, on the most ordinary pane in the app. State what
|
|
161
|
+
// it is instead. `codex` and any future agent keep the parser-scope message, which is the
|
|
162
|
+
// accurate thing to say about them: the transcript exists, this parser does not read it.
|
|
163
|
+
out.missing.lastWords = agent === 'shell' ? SHELL_NO_WORDS : `can't read its last words — ${NOT_CLAUDE}`;
|
|
157
164
|
out.missing.tools = NOT_CLAUDE;
|
|
158
165
|
out.missing.model = NOT_CLAUDE;
|
|
159
166
|
out.missing.context = NOT_CLAUDE;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.12.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
package/workspace/public/app.js
CHANGED
|
@@ -221,6 +221,7 @@ async function selectWorkspace(id, opts = {}) {
|
|
|
221
221
|
if (changed) closeDoc();
|
|
222
222
|
renderWorkspaces();
|
|
223
223
|
renderSessions();
|
|
224
|
+
renderNewTarget(); // R5 — "here" moved; the composer must say so even if it is already open
|
|
224
225
|
loadBoards();
|
|
225
226
|
// focus a session in THIS workspace: the remembered one if it belongs here, else its first live one
|
|
226
227
|
const mine = mySessions();
|
|
@@ -259,12 +260,43 @@ async function selectWorkspace(id, opts = {}) {
|
|
|
259
260
|
*/
|
|
260
261
|
const TREE_GLYPH = { working: '●', quiet: '◼', blocked: '✗', idle: '—', exited: '○', none: '·' };
|
|
261
262
|
|
|
262
|
-
|
|
263
|
+
/* THE THREE-VALUED RULE (R2, and the veto of this initiative's rubric).
|
|
264
|
+
*
|
|
265
|
+
* Every derived cell in this tree is one of three things, and they get three renderings:
|
|
266
|
+
*
|
|
267
|
+
* value — the fact, rendered plainly
|
|
268
|
+
* verified absent— the read SUCCEEDED and found nothing. Quiet, specific, never red. This is the
|
|
269
|
+
* common case for anything that is not a PMOS repo, and it is the one the first
|
|
270
|
+
* version had no rendering for at all.
|
|
271
|
+
* unreadable — the read FAILED. Red, and it keeps the word.
|
|
272
|
+
*
|
|
273
|
+
* The first version had two renderings for three facts, so one was always mislabelled: a plain
|
|
274
|
+
* folder reported `branch unreadable`, which is an alarm about a completely ordinary directory, on
|
|
275
|
+
* the same surface that uses red for a corrupt state queue and a blocked initiative. A surface that
|
|
276
|
+
* cries wolf on the normal case teaches the PM to ignore red where red is real.
|
|
277
|
+
*
|
|
278
|
+
* `absent()` and `broken()` are the only two ways to say it, so a fourth site cannot invent a
|
|
279
|
+
* fifth vocabulary. */
|
|
280
|
+
const absent = (txt, title) => `<span class="gone"${title ? ` title="${esc(title)}"` : ''}>${esc(txt)}</span>`;
|
|
281
|
+
const broken = (txt, title) => `<span class="bl" role="alert"${title ? ` title="${esc(title)}"` : ''}>${esc(txt)}</span>`;
|
|
282
|
+
|
|
283
|
+
// The initiative a branch names, and the record behind it — four cases now, all of them stated. A
|
|
263
284
|
// branch with no record is NOT an initiative with a missing file; it is a branch, and the row says
|
|
264
|
-
// so rather than inventing an id (R2).
|
|
285
|
+
// so rather than inventing an id (R2). A workspace with no branch is either an ordinary folder that
|
|
286
|
+
// is not a repository (quiet) or a git read that failed (a fault) — never the same word for both.
|
|
287
|
+
// A ROW ALWAYS HAS AN IDENTITY. The first version used this label for both the row's name and its
|
|
288
|
+
// condition, so a plain folder rendered as a row literally titled `branch unreadable` — a row with
|
|
289
|
+
// no name at all, which is unfindable in a tree of them. The name is what the row IS; the condition
|
|
290
|
+
// is what is true of it, and it belongs on the second line with the other derived facts.
|
|
265
291
|
function initiativeOf(w) {
|
|
266
292
|
const br = w.branch;
|
|
267
|
-
if (!br)
|
|
293
|
+
if (!br) {
|
|
294
|
+
if (!w.branch_error) return { kind: 'detached', label: w.name, id: null };
|
|
295
|
+
// 'no-git' is not a failure to report a branch; it is a folder that HAS no branch. The message
|
|
296
|
+
// is branchInfo's own ("not a git repository") — this renderer does not compose its own prose.
|
|
297
|
+
if (w.branch_code === 'no-git') return { kind: 'nogit', label: w.name, id: null };
|
|
298
|
+
return { kind: 'branchbroken', label: w.name, id: null, condition: w.branch_error };
|
|
299
|
+
}
|
|
268
300
|
const m = /^initiative\/(.+)$/.exec(br);
|
|
269
301
|
if (!m) return { kind: 'branch', label: br, id: null };
|
|
270
302
|
const id = m[1];
|
|
@@ -283,12 +315,15 @@ function boardRow(w, id) {
|
|
|
283
315
|
// something needs the PM. Rows without a signal collapse to a count (R6). The PM's own toggle always
|
|
284
316
|
// wins — `state.openTrees` holds it, and it is re-applied after every render for the same reason
|
|
285
317
|
// `state.openHarness` is: this pane is rebuilt by innerHTML on every push.
|
|
318
|
+
// R7 — EXPANSION MUST BE EARNED. Signals divide in two, and the first version conflated them:
|
|
319
|
+
// * a signal about the TREE (a needs count, a blocker) belongs ON the row and is already rendered
|
|
320
|
+
// there — opening the container shows the PM nothing they cannot already see;
|
|
321
|
+
// * a signal about a SESSION is only visible once the container is open, so it earns the open.
|
|
322
|
+
// Conflating them is why a repository with one queued need and no sessions expanded to reveal a
|
|
323
|
+
// lone "+ session here" button. A row that opens onto nothing teaches you to distrust its glyphs.
|
|
286
324
|
function treeHasSignal(w) {
|
|
287
325
|
const mine = state.sessions.filter((s) => s.ws === w.id);
|
|
288
|
-
if (
|
|
289
|
-
if (state.needsByWs[w.id] && state.needsByWs[w.id].error) return true;
|
|
290
|
-
const init = initiativeOf(w);
|
|
291
|
-
if (init.row && init.row.blockers && init.row.blockers.length) return true;
|
|
326
|
+
if (!mine.length) return false; // nothing to reveal, so nothing to open for
|
|
292
327
|
return mine.some((s) => {
|
|
293
328
|
const shown = stateLabel(s).shown;
|
|
294
329
|
return shown === 'working' || shown === 'blocked' || shown === 'exited';
|
|
@@ -340,6 +375,18 @@ function renderTree() {
|
|
|
340
375
|
}).join('');
|
|
341
376
|
host.innerHTML = html || `<div class="empty">No repositories — run <b>pmos</b> inside one.</div>`;
|
|
342
377
|
host.querySelectorAll('[data-pick]').forEach((b) => b.onclick = () => selectWorkspace(b.dataset.pick));
|
|
378
|
+
// R6 — select the tree AND open the composer against it. Deliberately NOT awaited: selectWorkspace
|
|
379
|
+
// ends on four network reads (library · needs · branch · worktrees), and making the composer wait
|
|
380
|
+
// on them puts a variable delay between a click and any visible response — the failure mode the
|
|
381
|
+
// board cache was built to remove elsewhere. `state.wsId` is assigned synchronously at the top of
|
|
382
|
+
// selectWorkspace, so the form opens naming the RIGHT tree immediately; loadBranch() refills the
|
|
383
|
+
// branch picker when it lands, which is the same path the + button already relies on.
|
|
384
|
+
host.querySelectorAll('[data-start]').forEach((b) => b.onclick = (e) => {
|
|
385
|
+
e.stopPropagation();
|
|
386
|
+
const p = selectWorkspace(b.dataset.start);
|
|
387
|
+
openNewForm();
|
|
388
|
+
p.then(() => { if (!$('newForm').hidden) renderNewTarget(); });
|
|
389
|
+
});
|
|
343
390
|
host.querySelectorAll('[data-repo-pick]').forEach((b) => b.onclick = () => openRepoDetail(b.dataset.repoPick));
|
|
344
391
|
host.querySelectorAll('[data-forget]').forEach((b) => b.onclick = (e) => { e.stopPropagation(); forgetWorkspace(b.dataset.forget); });
|
|
345
392
|
host.querySelectorAll('[data-toggle]').forEach((b) => b.onclick = (e) => {
|
|
@@ -388,11 +435,30 @@ function treeRow(w, g) {
|
|
|
388
435
|
} else if (board && board.pending) {
|
|
389
436
|
wf = `<span class="wf"><span class="drv">deriving the ladder…</span></span>`;
|
|
390
437
|
} else if (board && board.ok === false) {
|
|
391
|
-
|
|
438
|
+
// The one case that stays RED. The engine's own message says which fault it is — an install
|
|
439
|
+
// that carries no engine reads differently from one whose engine threw — so it is carried
|
|
440
|
+
// through as the tooltip rather than compressed into two words (incident
|
|
441
|
+
// kit-workflow-engine-missing: "ladder unreadable" was every adopter's entire experience of
|
|
442
|
+
// this surface, and it named neither the cause nor anything the PM could act on).
|
|
443
|
+
wf = `<span class="wf">${broken('ladder unreadable', board.error || '')}</span>`;
|
|
444
|
+
} else if (board && board.ok === true && board.empty === true) {
|
|
445
|
+
// VERIFIED ABSENT, and the sentence is about the REPOSITORY, not the branch: the engine read
|
|
446
|
+
// this repo successfully and it has no initiative records. Pointing the workspace at an
|
|
447
|
+
// ordinary repo is a supported thing to do, so this is the calm common case, not a defect.
|
|
448
|
+
wf = `<span class="wf">${absent('no initiatives yet', 'this repository has no planning/initiatives/ records — the workspace read it successfully')}</span>`;
|
|
392
449
|
} else if (init.kind === 'norecord') {
|
|
393
|
-
|
|
450
|
+
// An initiative BRANCH whose record is missing — genuinely a gap in the repo, but not a failed
|
|
451
|
+
// read, so it stays quiet and says precisely which of the two it is.
|
|
452
|
+
wf = `<span class="wf">${absent('no record for this branch', `the branch names initiative "${init.id}" but planning/initiatives/${init.id}.md is not there`)}</span>`;
|
|
394
453
|
} else if (init.kind === 'branch') {
|
|
395
|
-
wf = `<span class="wf"
|
|
454
|
+
wf = `<span class="wf">${absent('not an initiative branch', `"${w.branch}" does not match initiative/<id>, so no ladder is derived for it`)}</span>`;
|
|
455
|
+
} else if (init.kind === 'nogit') {
|
|
456
|
+
wf = `<span class="wf">${absent('not a git repository', 'an ordinary folder — sessions run here, but nothing git-derived applies to it')}</span>`;
|
|
457
|
+
} else if (init.kind === 'branchbroken') {
|
|
458
|
+
// The other half of the split: git IS here and the read FAILED. This one keeps the alarm.
|
|
459
|
+
wf = `<span class="wf">${broken('branch unreadable', init.condition || '')}</span>`;
|
|
460
|
+
} else if (init.kind === 'detached') {
|
|
461
|
+
wf = `<span class="wf">${absent('detached HEAD', 'no branch is checked out, so no initiative is derived from one')}</span>`;
|
|
396
462
|
}
|
|
397
463
|
// the attribution line: which branch this ladder came from, and as of when
|
|
398
464
|
const stamp = board && board.as_of ? new Date(board.as_of).toLocaleTimeString() : null;
|
|
@@ -400,8 +466,11 @@ function treeRow(w, g) {
|
|
|
400
466
|
? `<span class="attrib">derived on <b>${esc(w.branch || '?')}</b>${stamp ? ` · as of ${esc(stamp)}` : ''}${board && board.stale ? ' · refreshing' : ''}</span>`
|
|
401
467
|
: '';
|
|
402
468
|
|
|
469
|
+
// R6 — the verb is performed. This used to call selectWorkspace() and stop, and its own tooltip
|
|
470
|
+
// admitted it ("select this tree, THEN start a session"). `data-start` selects the tree AND opens
|
|
471
|
+
// the composer against it, focused.
|
|
403
472
|
const sessHtml = open ? mine.map((s) => sessionRow(s)).join('')
|
|
404
|
-
+ `<button class="tree-new" data-
|
|
473
|
+
+ `<button class="tree-new" data-start="${esc(w.id)}" title="start a session in ${esc(w.name)}">+ session here</button>`
|
|
405
474
|
: '';
|
|
406
475
|
const collapsed = !open && mine.length
|
|
407
476
|
? `<span class="rollup">${mine.length} session${mine.length === 1 ? '' : 's'}${live ? '' : ' · all exited'}</span>` : '';
|
|
@@ -413,9 +482,9 @@ function treeRow(w, g) {
|
|
|
413
482
|
<button class="tr" data-pick="${esc(w.id)}" aria-current="${cur}" title="${esc(w.root)}">
|
|
414
483
|
<span class="r1"><span class="g" aria-hidden="true">${TREE_GLYPH[gl] || '·'}</span>
|
|
415
484
|
<span class="nm">${esc(init.label)}</span>
|
|
416
|
-
${w.home ?
|
|
485
|
+
${w.home ? `<span class="tag" title="the folder pmos was launched in — it is always hosted and cannot be removed">home</span>` : ''}
|
|
417
486
|
<span class="dir">${esc(w.name === g.name ? 'main tree' : w.name)}</span></span>
|
|
418
|
-
<span class="r2">${wf}${n && n.count ? `<span class="needs-n">${n.count} need you</span>` : ''}${n && n.error ? '
|
|
487
|
+
<span class="r2">${wf}${n && n.count ? `<span class="needs-n">${n.count} need you</span>` : ''}${n && n.error ? broken('needs unreadable', n.error) : ''}${collapsed}</span>
|
|
419
488
|
${attrib}
|
|
420
489
|
</button>
|
|
421
490
|
${w.home ? '' : `<button class="forget" data-forget="${esc(w.id)}" aria-label="Stop hosting ${esc(w.name)}" title="${live ? 'end its sessions first' : 'stop hosting this repo (the repo is untouched)'}">✕</button>`}
|
|
@@ -2094,10 +2163,34 @@ function resolveDocPath(ctx, u) {
|
|
|
2094
2163
|
}
|
|
2095
2164
|
|
|
2096
2165
|
// ── new session: name it, pick its branch, before it starts ──
|
|
2166
|
+
/* R5 — "HERE" MUST HAVE A VISIBLE REFERENT.
|
|
2167
|
+
*
|
|
2168
|
+
* The composer acts on `state.wsId` and always did. That was legible when it lived underneath a
|
|
2169
|
+
* single selected workspace pane; once the sidebar became one tree showing every repository at
|
|
2170
|
+
* once, "run an agent here" named nothing on screen. This does not change WHAT it acts on (that
|
|
2171
|
+
* would be a behaviour change — PRD assumption A3); it makes the target sayable before the click.
|
|
2172
|
+
*
|
|
2173
|
+
* Called from three places, because the target changes without the form being reopened: on open, on
|
|
2174
|
+
* every workspace selection, and on every workspace push (a rename or a checkout moves the branch). */
|
|
2175
|
+
function newTargetText() {
|
|
2176
|
+
const w = state.workspaces.find((x) => x.id === state.wsId);
|
|
2177
|
+
if (!w) return { text: 'no tree selected — choose one below first', none: true };
|
|
2178
|
+
const init = initiativeOf(w);
|
|
2179
|
+
const where = init.kind === 'nogit' ? 'a plain folder' : (w.branch ? `on ${w.branch}` : 'no branch checked out');
|
|
2180
|
+
return { text: `in ${w.name} · ${where}`, none: false };
|
|
2181
|
+
}
|
|
2182
|
+
function renderNewTarget() {
|
|
2183
|
+
const el = $('newWhere');
|
|
2184
|
+
if (!el) return;
|
|
2185
|
+
const t = newTargetText();
|
|
2186
|
+
el.textContent = t.text;
|
|
2187
|
+
el.dataset.none = t.none ? '1' : '0';
|
|
2188
|
+
}
|
|
2097
2189
|
function openNewForm() {
|
|
2098
2190
|
const w = state.workspaces.find((x) => x.id === state.wsId);
|
|
2099
2191
|
$('newForm').hidden = false;
|
|
2100
2192
|
$('newBtn').setAttribute('aria-expanded', 'true');
|
|
2193
|
+
renderNewTarget();
|
|
2101
2194
|
// pre-filled but editable — a default that reads better than "shell" in a list of shells
|
|
2102
2195
|
const n = mySessions().length + 1;
|
|
2103
2196
|
$('newName').value = `${w ? w.name : 'session'} ${n}`;
|
|
@@ -68,12 +68,17 @@
|
|
|
68
68
|
<div class="cap">
|
|
69
69
|
<span class="grip" id="sideGrip" data-pane="side" role="button" tabindex="0"
|
|
70
70
|
aria-label="Move the sidebar" title="Move this column">⠿</span>
|
|
71
|
-
|
|
71
|
+
<!-- R8: the pane states its structure. It was called `work` — the least informative word
|
|
72
|
+
available for a tree of repositories, their working trees, and their sessions. -->
|
|
73
|
+
<button class="fold" id="wsFold" data-fold="ws" aria-expanded="true" title="Hide the repositories, their working trees and their sessions"><span class="chev" aria-hidden="true">▾</span>repos & sessions</button>
|
|
72
74
|
<span class="ct" id="needCount"></span>
|
|
73
75
|
<button class="mini" id="addWsBtn" aria-expanded="false" title="Add a repository">+</button></div>
|
|
74
76
|
<div class="pane-body">
|
|
75
77
|
<button class="newsess" id="newBtn" aria-expanded="false"><i>+</i> new session — run an agent here</button>
|
|
76
78
|
<form class="miniform" id="newForm" hidden>
|
|
79
|
+
<!-- R5: "here" gets a visible referent. The composer has always acted on the selected
|
|
80
|
+
tree; with every repository on screen at once, nothing said which that was. -->
|
|
81
|
+
<p class="where" id="newWhere"></p>
|
|
77
82
|
<label class="lbl" for="newName">name this session</label>
|
|
78
83
|
<input id="newName" type="text" placeholder="e.g. jira-sync build" aria-label="Session name" autocomplete="off" spellcheck="false" />
|
|
79
84
|
<div class="seg" role="radiogroup" aria-label="Agent to run" id="newAgents">
|
|
@@ -1371,7 +1371,12 @@ line-height:1.35;cursor:pointer}
|
|
|
1371
1371
|
.tr .wf .lane{color:var(--muted-foreground)}
|
|
1372
1372
|
.tr .wf .stg{color:var(--brand)}
|
|
1373
1373
|
.tr .wf .nx{color:var(--muted-foreground)}
|
|
1374
|
+
/* THE THREE-VALUED VOCABULARY (R2). Two renderings for three facts means one is always mislabelled
|
|
1375
|
+
— which is how an ordinary folder came to render as an alarm on the same surface that uses
|
|
1376
|
+
--destructive for a corrupt state queue. `.bl` keeps the alarm; `.gone` is the quiet, specific
|
|
1377
|
+
"the read succeeded and there is nothing here"; a value is styled as neither. */
|
|
1374
1378
|
.tr .wf .bl,.tr .r2 .bl{color:var(--destructive)}
|
|
1379
|
+
.tr .wf .gone,.tr .r2 .gone{color:var(--muted-foreground);opacity:.8}
|
|
1375
1380
|
.tr .wf .drv{color:var(--muted-foreground);font-style:italic}
|
|
1376
1381
|
.tr .needs-n{color:var(--warning)}
|
|
1377
1382
|
.tr .rollup{color:var(--muted-foreground)}
|
|
@@ -1384,6 +1389,11 @@ line-height:1.35;cursor:pointer}
|
|
|
1384
1389
|
border:1px dashed var(--border);border-radius:var(--radius-sm);color:var(--muted-foreground);
|
|
1385
1390
|
font:inherit;font-size:var(--text-2xs);cursor:pointer;text-align:left}
|
|
1386
1391
|
.tree-new:hover{color:var(--foreground);border-color:var(--input)}
|
|
1392
|
+
/* R5 — the composer's target. "run an agent here" named nothing once every repository was on
|
|
1393
|
+
screen at once; this is the referent, and it updates on every selection change. */
|
|
1394
|
+
.miniform .where{font-family:var(--mono);font-size:var(--text-2xs);color:var(--muted-foreground);
|
|
1395
|
+
margin:0 0 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1396
|
+
.miniform .where[data-none="1"]{color:var(--warning)}
|
|
1387
1397
|
.cap .ct[data-bad="1"]{color:var(--destructive)}
|
|
1388
1398
|
|
|
1389
1399
|
/* ── The rail's initiative dossier (R7·R8·R9) ────────────────────────────────────────────────── */
|
package/workspace/server.js
CHANGED
|
@@ -229,7 +229,7 @@ function repoIdentity(root) {
|
|
|
229
229
|
const hit = repoKeyCache.get(root);
|
|
230
230
|
const now = Date.now();
|
|
231
231
|
if (hit && now - hit.at < REPO_KEY_TTL_MS) return hit;
|
|
232
|
-
let key = root, name = path.basename(root), branch = null, branchError = null;
|
|
232
|
+
let key = root, name = path.basename(root), branch = null, branchError = null, branchCode = null;
|
|
233
233
|
try {
|
|
234
234
|
const trees = gitInfo.worktrees(root);
|
|
235
235
|
if (trees.length && trees[0].dir) { key = path.resolve(trees[0].dir); name = path.basename(key); }
|
|
@@ -237,9 +237,16 @@ function repoIdentity(root) {
|
|
|
237
237
|
try {
|
|
238
238
|
const bi = gitInfo.branchInfo(root);
|
|
239
239
|
if (bi.ok) branch = bi.detached ? null : bi.current;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
// R1/A2 — carry the CODE and the MESSAGE, not a boolean. branchInfo already separates the two
|
|
241
|
+
// failures and says so in its own comment: `no-git` is an ordinary folder and "deserves no
|
|
242
|
+
// alarm", `git-error` is something the PM may want to look at. Keeping only a truthy string here
|
|
243
|
+
// is what let the client render both as `branch unreadable` — an alarm on the normal case, on
|
|
244
|
+
// the same surface that uses red for a corrupt state queue. The renderer must not re-derive
|
|
245
|
+
// meaning from a boolean; a second place that decides what a failure MEANS is how the first
|
|
246
|
+
// one drifts.
|
|
247
|
+
else { branchError = bi.error || 'branch could not be read'; branchCode = bi.code || 'git-error'; }
|
|
248
|
+
} catch { branchError = 'branch could not be read'; branchCode = 'git-error'; }
|
|
249
|
+
const val = { at: now, key, name, branch, branchError, branchCode };
|
|
243
250
|
repoKeyCache.set(root, val);
|
|
244
251
|
return val;
|
|
245
252
|
}
|
|
@@ -250,8 +257,11 @@ function wsList() {
|
|
|
250
257
|
id: w.id, name: w.name, root: w.root, home: HOME_ENABLED && w.root === SELF_ROOT,
|
|
251
258
|
shape: wsShape(w.root), addedAt: w.addedAt,
|
|
252
259
|
sessions: [...sessions.values()].filter((s) => s.ws === w.id).length,
|
|
253
|
-
// the repository this working tree belongs to, and the branch it is on
|
|
254
|
-
|
|
260
|
+
// the repository this working tree belongs to, and the branch it is on. `branch_code`
|
|
261
|
+
// distinguishes an ordinary non-repo folder ('no-git') from a real git fault ('git-error') —
|
|
262
|
+
// the client renders the first quietly and the second as a fault (R1/R2).
|
|
263
|
+
repo_key: rid.key, repo_name: rid.name, branch: rid.branch,
|
|
264
|
+
branch_error: rid.branchError, branch_code: rid.branchCode,
|
|
255
265
|
};
|
|
256
266
|
});
|
|
257
267
|
}
|
|
@@ -1018,7 +1028,18 @@ function transcriptHarness(s) {
|
|
|
1018
1028
|
s.txFoundAt = now;
|
|
1019
1029
|
}
|
|
1020
1030
|
const found = s.txFound;
|
|
1021
|
-
if (found.missing)
|
|
1031
|
+
if (found.missing) {
|
|
1032
|
+
// R4 — VERIFIED ABSENT IS NOT A FAILED SEARCH. A `shell` pane is walked for a descendant claude
|
|
1033
|
+
// because the PM often starts one there; when none is running, findTranscript's answer ("the CLI
|
|
1034
|
+
// wrote no session record") describes the SEARCH, and reads as a fault on a pane that is doing
|
|
1035
|
+
// exactly what a shell does. Nothing is wrong, nothing is missing, and there is nothing for the
|
|
1036
|
+
// PM to fix — so say what is true and what would change it. Every other reason (a record that is
|
|
1037
|
+
// unreadable, one belonging to another workspace) IS a fault and keeps its own words.
|
|
1038
|
+
if (s.agent === 'shell' && /wrote no session record/.test(found.missing)) {
|
|
1039
|
+
return { missing: { all: 'no agent is running in this shell — start one here and its harness appears', calm: true } };
|
|
1040
|
+
}
|
|
1041
|
+
return { missing: { all: found.missing } };
|
|
1042
|
+
}
|
|
1022
1043
|
let stamp = null;
|
|
1023
1044
|
try { const st = fs.statSync(found.file); stamp = `${st.size}:${st.mtimeMs}`; } catch {}
|
|
1024
1045
|
if (s.tx && s.txStamp === stamp) return s.tx;
|
|
@@ -2879,6 +2900,10 @@ if (require.main === module) {
|
|
|
2879
2900
|
}
|
|
2880
2901
|
module.exports = { server, spawnSession, removeSession, renameSession, sessions, libraryTree, readTextDoc,
|
|
2881
2902
|
registerWorkspace, forgetWorkspace, workspaces, library, initiativeIndex, fsList,
|
|
2903
|
+
// R1 — exported so the suite can assert the CARRY-THROUGH: branchInfo separates an ordinary
|
|
2904
|
+
// non-repo folder from a real git fault, and the whole legibility defect was that distinction
|
|
2905
|
+
// being dropped one layer above it.
|
|
2906
|
+
repoIdentity, wsList,
|
|
2882
2907
|
// exported so the suite can assert the D08 invariant end-to-end: the harness parsers consume
|
|
2883
2908
|
// scrubbed text, so a secret printed into scrollback must not survive into a rendered value.
|
|
2884
2909
|
scrub, harnessOf,
|