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.
@@ -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`). It makes no outbound request, because PMOS holds no credential. The
42
- * sync panel therefore reports an unconfigured connector rather than a difference summary it never
43
- * read — `state.connectors` is published empty, which is what makes panel 3's two approval buttons
44
- * inert. A "2 sections differ" line over a destination nobody contacted is the watermelon this
45
- * repo has a word for.
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
- out.push({ kind: r.kind, label: r.label, path: r.target, st: 'declared · not connected' });
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
- dest.set(r.artifact_path, { kind: r.kind, target: r.target }); // a later row supersedes an earlier
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
- return d ? { role: a.role, path: a.path, dest: d } : { role: a.role, path: a.path };
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
- setState({ board, productWs, records: { ...records, ...(hydrated) } });
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) => ({ records: { ...s.records, [slug]: hydrated[slug] } }));
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
- connectors: [], // no connector is configured — panel 3's approvals stay inert
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, st})` → `initiative-links.jsonl`. */
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;