create-pmos 0.24.0 → 0.26.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.
@@ -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
 
@@ -662,7 +684,14 @@ async function hydrate(slug) {
662
684
  // and its footer ("read failed · as of …") is entitled to say when it was attempted.
663
685
  const asOf = readStamp();
664
686
  hydrated[slug] = { ...recordFrom(payload), asOf };
665
- 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
+ }));
666
695
  return payload;
667
696
  } finally { inFlight.delete(slug); }
668
697
  }
@@ -780,7 +809,9 @@ export async function loadRepos() {
780
809
  const sel = state.sel || (p ? firstSlugOf(p.name) : null);
781
810
  setState({
782
811
  wsId: p ? p.ws : null,
783
- 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.
784
815
  product: state.product && products.has(state.product) ? state.product : first,
785
816
  sel,
786
817
  });
@@ -926,6 +957,29 @@ export function startSession(slug, sess) {
926
957
  setTimeout(pollSessions, 800);
927
958
  }
928
959
 
960
+ /**
961
+ * Bind a RUNNING session to an initiative — the PM's own word about work already in flight
962
+ * (workspace-session-attribution R3).
963
+ *
964
+ * Every session that existed before the recorded field shipped has no attribution, and so does every
965
+ * session started outside PMOS. Without this the field would be a promise about future sessions and
966
+ * no help at all with the ones on screen.
967
+ *
968
+ * What comes back is the server's answer, not an assumption: `previous` names the initiative this
969
+ * replaced, so a re-bind the PM did not mean to make is visible rather than silent. Pass
970
+ * `initiative: null` to clear a binding — the honest way back from a wrong one.
971
+ *
972
+ * The poll is scheduled rather than the state patched locally: `/api/sessions` is the one reader of
973
+ * this fact, and writing a second opinion into `state.sessions` here is how the board and the server
974
+ * would come to disagree about what a session is for.
975
+ */
976
+ export async function bindSessionInitiative(id, initiative) {
977
+ if (FIXTURE) return { ok: false, error: 'fixture mode has no server' };
978
+ const out = await postJson('/api/session/initiative', { id, initiative: initiative || null });
979
+ if (out && out.ok) setTimeout(pollSessions, 200);
980
+ return out || { ok: false, error: 'the workspace server did not answer' };
981
+ }
982
+
929
983
  /* ── the scratchpad ─────────────────────────────────────────────────────────────────────────────
930
984
  * The pad is PER SESSION, and the server has always stored it that way: `/api/scratchpad` is keyed
931
985
  * by the server-issued session id (workspace-scoped, one file per session, deleted with it). What
@@ -992,7 +1046,7 @@ export async function saveDoc({ ws, path, base, text }) {
992
1046
  * Appends ONE declaration to `artifact-links.jsonl`. It contacts nothing: PMOS holds no credential,
993
1047
  * so this records what the PM says the mapping is and says nothing about the destination.
994
1048
  */
995
- export async function recordArtifactLink({ artifact_path, slug, kind, target }) {
1049
+ export async function recordArtifactLink({ artifact_path, slug, kind, target, origin, state: st }) {
996
1050
  if (FIXTURE) return { ok: true };
997
1051
  const p = products.get(currentProductName());
998
1052
  const out = await postJson('/api/initiative/link', {
@@ -1001,11 +1055,37 @@ export async function recordArtifactLink({ artifact_path, slug, kind, target })
1001
1055
  // The surface labels a Google destination `google doc`; the store's vocabulary is `google`.
1002
1056
  kind: String(kind || '').startsWith('google') ? 'google' : kind,
1003
1057
  target,
1058
+ // Passed through rather than defaulted here. The server owns the rules about which origin may
1059
+ // claim which state — including that a retraction is the PM's alone — and a client that filled
1060
+ // these in itself would be a second opinion about provenance, which is the one thing this store
1061
+ // must have exactly one of.
1062
+ origin, state: st,
1004
1063
  });
1005
1064
  if (out && out.ok === false) console.warn('[pm] the artifact link was NOT recorded: ' + out.error);
1006
1065
  return out;
1007
1066
  }
1008
1067
 
1068
+ /**
1069
+ * panel-detail.js — `recordSync({artifact_path, slug, kind, target, direction, profile})`.
1070
+ *
1071
+ * Records the direction the PM chose and returns the TASK a session will run to carry it out. It
1072
+ * syncs nothing: the row lands as `proposed`, which is the only state PMOS can produce on its own,
1073
+ * and it stays that way until a session that actually performed the write reports back and names
1074
+ * itself. Nothing here contacts Confluence, Jira or Google, and nothing here can.
1075
+ */
1076
+ export async function recordSync({ artifact_path, slug, kind, target, direction, profile }) {
1077
+ if (FIXTURE) return { ok: false, error: 'fixture mode records no sync' };
1078
+ const p = products.get(currentProductName());
1079
+ const out = await postJson('/api/initiative/sync', {
1080
+ ws: state.wsId || (p ? p.ws : ''),
1081
+ initiative_id: slug, artifact_path,
1082
+ kind: String(kind || '').startsWith('google') ? 'google' : kind,
1083
+ target, direction, profile: profile || null,
1084
+ });
1085
+ if (out && out.ok === false) console.warn('[pm] the sync direction was NOT recorded: ' + out.error);
1086
+ return out;
1087
+ }
1088
+
1009
1089
  /**
1010
1090
  * panel-detail.js — `lookupTask({kind, subject, hint, profile})`.
1011
1091
  *
@@ -1018,13 +1098,13 @@ export async function lookupTask({ kind, subject, hint, profile }) {
1018
1098
  return postJson('/api/initiative/lookup-task', { kind, subject, hint, profile });
1019
1099
  }
1020
1100
 
1021
- /** panel-detail.js — `recordInitiativeLink({slug, kind, label, path, st})` → `initiative-links.jsonl`. */
1022
- export async function recordInitiativeLink({ slug, kind, label, path }) {
1101
+ /** panel-detail.js — `recordInitiativeLink({slug, kind, label, path, origin, state})` → `initiative-links.jsonl`. */
1102
+ export async function recordInitiativeLink({ slug, kind, label, path, origin, state: st }) {
1023
1103
  if (FIXTURE) return { ok: true };
1024
1104
  const p = products.get(currentProductName());
1025
1105
  const out = await postJson('/api/initiative/link', {
1026
1106
  store: 'initiative', ws: state.wsId || (p ? p.ws : ''),
1027
- initiative_id: slug, kind, label, target: path,
1107
+ initiative_id: slug, kind, label, target: path, origin, state: st,
1028
1108
  });
1029
1109
  if (out && out.ok === false) console.warn('[pm] the reference link was NOT recorded: ' + out.error);
1030
1110
  return out;