create-pmos 0.23.0 → 0.25.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 +28 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/README.md +10 -0
- package/workspace/lib/four-panel.js +201 -6
- package/workspace/lib/session-state.js +56 -2
- package/workspace/lib/working-set.js +224 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/index.html +7 -0
- package/workspace/public/pm/data.js +100 -16
- package/workspace/public/pm/panel-detail.js +327 -93
- package/workspace/public/pm/panel-initiatives.js +172 -17
- package/workspace/public/pm/state.js +6 -0
- package/workspace/server.js +69 -2
|
@@ -35,6 +35,13 @@
|
|
|
35
35
|
Both read this file, so the sentence in the terminal and the sentence in the chooser cannot
|
|
36
36
|
drift apart. Classic script (a UMD setting window.PMOSTreeShare), like the two above. -->
|
|
37
37
|
<script src="/lib/tree-share.js"></script>
|
|
38
|
+
<!-- The session→initiative join, made once over every session instead of once per initiative
|
|
39
|
+
(workspace-active-visibility). The SERVER makes the same two-hopper per initiative inside
|
|
40
|
+
/api/initiative (lib/four-panel.js sessionsFor); the BOARD needs it for 178 rows at once and
|
|
41
|
+
cannot spend 178 requests on it. Loaded here rather than imported so it stays walkable in node
|
|
42
|
+
— test.js requires it and walks its whole input domain. It reads window.PMOSSessionState for
|
|
43
|
+
the attention order, so it must come AFTER that script. -->
|
|
44
|
+
<script src="/lib/working-set.js"></script>
|
|
38
45
|
<!-- The phone's navigation reducer. DOM-free, loaded as a classic script (it is a UMD that sets
|
|
39
46
|
window.PMOSMobileNav) and required by test.js, so the whole screen domain is walkable in node
|
|
40
47
|
rather than only in a browser. pm/mobile.js reads it from the global; the module graph does not
|
|
@@ -38,11 +38,17 @@
|
|
|
38
38
|
*
|
|
39
39
|
* ── The one thing this file refuses to do ───────────────────────────────────────────────────
|
|
40
40
|
* Linking an artifact records a DECLARATION (`{artifact_path, kind, target}` appended to
|
|
41
|
-
* `artifact-links.jsonl`)
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* `artifact-links.jsonl`) and choosing a sync direction records another (`artifact-sync.jsonl`).
|
|
42
|
+
* Neither makes an outbound request, because PMOS holds no credential: the request is made by an
|
|
43
|
+
* agent session holding the PM's own MCP, and what comes back here is a report (D53).
|
|
44
|
+
*
|
|
45
|
+
* `state.connectors` used to be published as a literal `[]` on every load. That was written when it
|
|
46
|
+
* was true of every machine, and it stayed after the server learned to DERIVE the answer from the
|
|
47
|
+
* PM's agent profiles — so a careful derivation shipped into a client that had already decided. The
|
|
48
|
+
* panel branches on it (`hasConnector`, `resolverFor`), so the visible cost was a control that never
|
|
49
|
+
* rendered and a machine with a Jira MCP that reported having none. It is now published from the
|
|
50
|
+
* payload, and the constant is gone rather than corrected: a constant cannot be wrong, which is
|
|
51
|
+
* exactly why it could not be trusted.
|
|
46
52
|
*/
|
|
47
53
|
|
|
48
54
|
import { state, setState, subscribe, FIXTURE } from './state.js';
|
|
@@ -299,20 +305,36 @@ function linksFor(payload) {
|
|
|
299
305
|
out.push({ kind: l.kind, label: l.declared, path: l.href || l.declared, st: l.state_text || l.state });
|
|
300
306
|
}
|
|
301
307
|
for (const r of (payload.declarations && payload.declarations.links) || []) {
|
|
302
|
-
|
|
308
|
+
// The row's OWN provenance, carried rather than replaced. `declared · not connected` was a
|
|
309
|
+
// literal written before the store had a state vocabulary, and it outlived it: a reference a
|
|
310
|
+
// session had genuinely resolved rendered the same seven words as one the PM had typed a second
|
|
311
|
+
// earlier. The panel derives its state text from these fields (`linkStateText`), so carrying
|
|
312
|
+
// them is the whole fix — there is no second opinion to keep in step.
|
|
313
|
+
out.push({
|
|
314
|
+
kind: r.kind, label: r.label, path: r.target,
|
|
315
|
+
state: r.state, origin: r.origin, profile: r.profile || null,
|
|
316
|
+
reason: r.reason || null, resolved_at: r.resolved_at || null,
|
|
317
|
+
});
|
|
303
318
|
}
|
|
304
319
|
return out;
|
|
305
320
|
}
|
|
306
321
|
|
|
307
|
-
/** Artifacts, with any declared external destination attached. */
|
|
322
|
+
/** Artifacts, with any declared external destination and the latest sync for it attached. */
|
|
308
323
|
function artsFor(payload) {
|
|
309
324
|
const dest = new Map();
|
|
310
325
|
for (const r of (payload.declarations && payload.declarations.artifacts) || []) {
|
|
311
|
-
|
|
326
|
+
// The server has already collapsed the store to one row per pair and dropped what the PM
|
|
327
|
+
// retracted, so what arrives here is the statement of now.
|
|
328
|
+
dest.set(r.artifact_path, { kind: r.kind, target: r.target, state: r.state, origin: r.origin });
|
|
329
|
+
}
|
|
330
|
+
const sync = new Map();
|
|
331
|
+
for (const r of (payload.declarations && payload.declarations.sync) || []) {
|
|
332
|
+
sync.set(r.artifact_path + '|' + r.kind, r);
|
|
312
333
|
}
|
|
313
334
|
return (payload.artifacts || []).map((a) => {
|
|
314
335
|
const d = dest.get(a.path);
|
|
315
|
-
|
|
336
|
+
if (!d) return { role: a.role, path: a.path };
|
|
337
|
+
return { role: a.role, path: a.path, dest: d, sync: sync.get(a.path + '|' + d.kind) || null };
|
|
316
338
|
});
|
|
317
339
|
}
|
|
318
340
|
|
|
@@ -465,6 +487,29 @@ export function branchOfWorkspace(id) {
|
|
|
465
487
|
return (id && wsBranch.get(id)) || null;
|
|
466
488
|
}
|
|
467
489
|
|
|
490
|
+
/**
|
|
491
|
+
* The same map as a plain object, for `setState`.
|
|
492
|
+
*
|
|
493
|
+
* `branchOfWorkspace` answers "what branch is THIS workspace on?", which is all the + session
|
|
494
|
+
* chooser ever needed. The initiative board asks the other direction — "which initiatives does this
|
|
495
|
+
* machine have a checkout for, and which of my live sessions is on one?" — and a lookup by id
|
|
496
|
+
* cannot be enumerated.
|
|
497
|
+
*
|
|
498
|
+
* It is PUBLISHED rather than exported as a getter for the reason `productWs` is: a panel that has
|
|
499
|
+
* to import the data layer to render a row cannot render that row until the import resolves, and
|
|
500
|
+
* panel 2 is the board. Published, the fact arrives in the same patch as everything else the panel
|
|
501
|
+
* reads, and the panel needs no adapter at all.
|
|
502
|
+
*
|
|
503
|
+
* `wsBranch` stays the single source — this is a projection of it taken at publish time, not a
|
|
504
|
+
* second copy anything writes to. Nulls are kept out: a folder with no git has no branch, and a map
|
|
505
|
+
* carrying holes would make every reader re-check the same thing.
|
|
506
|
+
*/
|
|
507
|
+
function branchMap() {
|
|
508
|
+
const out = {};
|
|
509
|
+
for (const [id, b] of wsBranch) if (typeof b === 'string' && b) out[id] = b;
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
|
|
468
513
|
/** Group registered working trees into products: several worktrees of one repo share a `repo_key`. */
|
|
469
514
|
function foldWorkspaces(rows) {
|
|
470
515
|
const byRepo = new Map();
|
|
@@ -595,7 +640,11 @@ function publishBoards() {
|
|
|
595
640
|
board[name] = p.board;
|
|
596
641
|
records = { ...records, ...p.records };
|
|
597
642
|
}
|
|
598
|
-
|
|
643
|
+
// `wsBranches` rides the same patch as `productWs` and for the same reason: it is a fact about
|
|
644
|
+
// every registered working tree, read once from /api/workspaces, that a panel needs in order to
|
|
645
|
+
// draw a row. panel 2 joins it against `state.sessions` to say which initiative each live agent is
|
|
646
|
+
// on — the same `initiative/<id>` join lib/four-panel.js makes per initiative on the server.
|
|
647
|
+
setState({ board, productWs, wsBranches: branchMap(), records: { ...records, ...(hydrated) } });
|
|
599
648
|
}
|
|
600
649
|
|
|
601
650
|
/* Hydrated (fully derived) records live apart from the light ones so that a board reload cannot
|
|
@@ -635,7 +684,14 @@ async function hydrate(slug) {
|
|
|
635
684
|
// and its footer ("read failed · as of …") is entitled to say when it was attempted.
|
|
636
685
|
const asOf = readStamp();
|
|
637
686
|
hydrated[slug] = { ...recordFrom(payload), asOf };
|
|
638
|
-
setState((s) => ({
|
|
687
|
+
setState((s) => ({
|
|
688
|
+
records: { ...s.records, [slug]: hydrated[slug] },
|
|
689
|
+
// The server's `{configured:[{kind, profile, server, declared}], declaredBy, note}`, straight
|
|
690
|
+
// through. Published on every hydration rather than once at boot, because a profile added
|
|
691
|
+
// while the workspace is open should reach the panel on the next selection rather than on the
|
|
692
|
+
// next restart — and because the payload is the only place this fact is ever produced.
|
|
693
|
+
connectors: payload && payload.connectors ? payload.connectors : s.connectors,
|
|
694
|
+
}));
|
|
639
695
|
return payload;
|
|
640
696
|
} finally { inFlight.delete(slug); }
|
|
641
697
|
}
|
|
@@ -753,7 +809,9 @@ export async function loadRepos() {
|
|
|
753
809
|
const sel = state.sel || (p ? firstSlugOf(p.name) : null);
|
|
754
810
|
setState({
|
|
755
811
|
wsId: p ? p.ws : null,
|
|
756
|
-
|
|
812
|
+
// NOT initialised to a value here. What is resolvable is the server's derivation from the PM's
|
|
813
|
+
// agent profiles, it arrives with the first `/api/initiative` payload, and seeding a `[]` in the
|
|
814
|
+
// meantime is not a placeholder — it is an answer, and the panel would render it as one.
|
|
757
815
|
product: state.product && products.has(state.product) ? state.product : first,
|
|
758
816
|
sel,
|
|
759
817
|
});
|
|
@@ -965,7 +1023,7 @@ export async function saveDoc({ ws, path, base, text }) {
|
|
|
965
1023
|
* Appends ONE declaration to `artifact-links.jsonl`. It contacts nothing: PMOS holds no credential,
|
|
966
1024
|
* so this records what the PM says the mapping is and says nothing about the destination.
|
|
967
1025
|
*/
|
|
968
|
-
export async function recordArtifactLink({ artifact_path, slug, kind, target }) {
|
|
1026
|
+
export async function recordArtifactLink({ artifact_path, slug, kind, target, origin, state: st }) {
|
|
969
1027
|
if (FIXTURE) return { ok: true };
|
|
970
1028
|
const p = products.get(currentProductName());
|
|
971
1029
|
const out = await postJson('/api/initiative/link', {
|
|
@@ -974,11 +1032,37 @@ export async function recordArtifactLink({ artifact_path, slug, kind, target })
|
|
|
974
1032
|
// The surface labels a Google destination `google doc`; the store's vocabulary is `google`.
|
|
975
1033
|
kind: String(kind || '').startsWith('google') ? 'google' : kind,
|
|
976
1034
|
target,
|
|
1035
|
+
// Passed through rather than defaulted here. The server owns the rules about which origin may
|
|
1036
|
+
// claim which state — including that a retraction is the PM's alone — and a client that filled
|
|
1037
|
+
// these in itself would be a second opinion about provenance, which is the one thing this store
|
|
1038
|
+
// must have exactly one of.
|
|
1039
|
+
origin, state: st,
|
|
977
1040
|
});
|
|
978
1041
|
if (out && out.ok === false) console.warn('[pm] the artifact link was NOT recorded: ' + out.error);
|
|
979
1042
|
return out;
|
|
980
1043
|
}
|
|
981
1044
|
|
|
1045
|
+
/**
|
|
1046
|
+
* panel-detail.js — `recordSync({artifact_path, slug, kind, target, direction, profile})`.
|
|
1047
|
+
*
|
|
1048
|
+
* Records the direction the PM chose and returns the TASK a session will run to carry it out. It
|
|
1049
|
+
* syncs nothing: the row lands as `proposed`, which is the only state PMOS can produce on its own,
|
|
1050
|
+
* and it stays that way until a session that actually performed the write reports back and names
|
|
1051
|
+
* itself. Nothing here contacts Confluence, Jira or Google, and nothing here can.
|
|
1052
|
+
*/
|
|
1053
|
+
export async function recordSync({ artifact_path, slug, kind, target, direction, profile }) {
|
|
1054
|
+
if (FIXTURE) return { ok: false, error: 'fixture mode records no sync' };
|
|
1055
|
+
const p = products.get(currentProductName());
|
|
1056
|
+
const out = await postJson('/api/initiative/sync', {
|
|
1057
|
+
ws: state.wsId || (p ? p.ws : ''),
|
|
1058
|
+
initiative_id: slug, artifact_path,
|
|
1059
|
+
kind: String(kind || '').startsWith('google') ? 'google' : kind,
|
|
1060
|
+
target, direction, profile: profile || null,
|
|
1061
|
+
});
|
|
1062
|
+
if (out && out.ok === false) console.warn('[pm] the sync direction was NOT recorded: ' + out.error);
|
|
1063
|
+
return out;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
982
1066
|
/**
|
|
983
1067
|
* panel-detail.js — `lookupTask({kind, subject, hint, profile})`.
|
|
984
1068
|
*
|
|
@@ -991,13 +1075,13 @@ export async function lookupTask({ kind, subject, hint, profile }) {
|
|
|
991
1075
|
return postJson('/api/initiative/lookup-task', { kind, subject, hint, profile });
|
|
992
1076
|
}
|
|
993
1077
|
|
|
994
|
-
/** panel-detail.js — `recordInitiativeLink({slug, kind, label, path,
|
|
995
|
-
export async function recordInitiativeLink({ slug, kind, label, path }) {
|
|
1078
|
+
/** panel-detail.js — `recordInitiativeLink({slug, kind, label, path, origin, state})` → `initiative-links.jsonl`. */
|
|
1079
|
+
export async function recordInitiativeLink({ slug, kind, label, path, origin, state: st }) {
|
|
996
1080
|
if (FIXTURE) return { ok: true };
|
|
997
1081
|
const p = products.get(currentProductName());
|
|
998
1082
|
const out = await postJson('/api/initiative/link', {
|
|
999
1083
|
store: 'initiative', ws: state.wsId || (p ? p.ws : ''),
|
|
1000
|
-
initiative_id: slug, kind, label, target: path,
|
|
1084
|
+
initiative_id: slug, kind, label, target: path, origin, state: st,
|
|
1001
1085
|
});
|
|
1002
1086
|
if (out && out.ok === false) console.warn('[pm] the reference link was NOT recorded: ' + out.error);
|
|
1003
1087
|
return out;
|