create-pmos 0.57.1 → 0.58.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.
@@ -54,6 +54,7 @@ const PM_ONLY_ACTS = Object.freeze([
54
54
  'bind', // bind a session to an initiative (/api/session/initiative)
55
55
  'issue', // Issue a report (issues.jsonl)
56
56
  'release', // the D65 Release of a factory batch (one grant per batch)
57
+ 'push', // select Jira tickets for a push (D88: /api/jira/push, pmos jira approve)
57
58
  ]);
58
59
  /** What each act is, as the refusal says it: "only the PM can <phrase>". */
59
60
  const ACT_PHRASE = Object.freeze({
@@ -66,6 +67,7 @@ const ACT_PHRASE = Object.freeze({
66
67
  bind: 'bind a session to an initiative',
67
68
  issue: 'issue a report',
68
69
  release: 'release a batch',
70
+ push: 'select tickets to push to Jira',
69
71
  });
70
72
  /** Fields only a writer sets. A body that carries one is refused, even with the value the writer would choose. */
71
73
  const WRITER_FIELDS = Object.freeze(['v', 'eid', 'by', 'via', 'at', 'override', 'pm', 'actor', 'nonce', 'grant']);
@@ -34,6 +34,9 @@ const VERBS = Object.freeze({
34
34
  help: 'pmos verify-work — mark work items integrated from the default branch (local refs only)' }),
35
35
  report: Object.freeze({ module: 'report-cli.js', run: 5, guide: 'reports.md',
36
36
  help: 'pmos report draft|issue|list … — draft and issue reports (pmos report help)' }),
37
+ // Not a pm-os-absorb verb (run: null): initiative jira-wbs added it through the same table.
38
+ jira: Object.freeze({ module: 'jira-cli.js', run: null, guide: 'jira.md',
39
+ help: 'pmos jira draft|show|epic|approve|pending|record|query|sync|which … — Jira tickets drafted from the PRD, and their status (pmos jira help)' }),
37
40
  // Not a pm-os-absorb verb (run: null): the workspace-launcher initiative added it through the same table.
38
41
  // Not a pm-os-absorb verb (run: null): docs-three-way-sync added it through the same table.
39
42
  docs: Object.freeze({ module: 'docs-cli.js', run: null, guide: 'docs.md',
@@ -431,12 +431,31 @@ function draftOf(root, { cadence, slot, now, health } = {}) {
431
431
  const pmosWritten = (ref) => /\.jsonl$/.test(ref) || /(^|\/)(runs|reports)\//.test(ref);
432
432
  const isEvidence = (h) => h.ev === 'moved' && h.evidence && h.evidence.kind === 'file' && isStr(h.evidence.ref) && !pmosWritten(h.evidence.ref);
433
433
  const trackerItems = fold.order.map((x) => fold.items[x]).filter((it) => it && work.has(it.initiative) && isStr(it.owner) && it.owner.startsWith('tracker:'));
434
+ /* The Jira status check (initiative jira-wbs, J14) writes a snapshot per initiative each time an agent read Jira. Its
435
+ * timestamp is a read of the tracker on its own, with no plan item: the check writes none (D88). */
436
+ const jiraReads = (() => {
437
+ const dir = path.join(path.dirname(L.initiatives), 'tracker', 'jira');
438
+ let names = [];
439
+ try { names = fs.readdirSync(dir).filter((n) => n.endsWith('.md') && !n.endsWith('-tickets.md') && work.has(n.slice(0, -3))); } catch { return []; }
440
+ const out = [];
441
+ for (const n of names) {
442
+ let fm = null;
443
+ try { fm = E.parseLeadingFrontmatter(fs.readFileSync(path.join(dir, n), 'utf8')); } catch { fm = null; }
444
+ if (fm && isStr(fm.timestamp)) out.push(fm.timestamp);
445
+ }
446
+ return out.sort();
447
+ })();
434
448
  if (trackerItems.length) {
435
449
  const seen = trackerItems.flatMap((it) => it.history.filter(isEvidence).map((h) => String(h.at))).sort();
436
450
  const lastSeen = seen.length ? seen.pop() : null;
437
451
  if (lastSeen && inWin(lastSeen)) reached.push('tracker (file evidence on tracker-owned moves in plan-items)');
438
452
  else notReached.push({ source: 'tracker', why: lastSeen ? `not refreshed since ${short(lastSeen)}` : 'no refresh with file evidence on record' });
439
453
  }
454
+ if (jiraReads.length) {
455
+ const lastRead = jiraReads[jiraReads.length - 1];
456
+ if (inWin(lastRead)) reached.push(`Jira (status check ${short(lastRead)})`);
457
+ else notReached.push({ source: 'Jira', why: `not checked since ${short(lastRead)}` });
458
+ }
440
459
  const fetched = lastFetch(abs);
441
460
  if (fetched) reached.push(`remote refs (last fetch ${short(fetched)}; not fetched for this draft)`);
442
461
  else notReached.push({ source: 'remote refs', why: 'never fetched; this draft reads local refs only' });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.57.1",
3
+ "version": "0.58.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.57.1",
3
+ "version": "0.58.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -1853,6 +1853,212 @@
1853
1853
  }
1854
1854
 
1855
1855
 
1856
+ /* the initiative sheet's Jira tickets tab (initiative jira-wbs, sheet-jira.js) */
1857
+ .in-jira {
1858
+ display: flex;
1859
+ flex-direction: column;
1860
+ gap: 12px;
1861
+ min-width: 0;
1862
+ }
1863
+ .in-jira-head {
1864
+ display: flex;
1865
+ flex-wrap: wrap;
1866
+ align-items: flex-end;
1867
+ justify-content: space-between;
1868
+ gap: 8px 16px;
1869
+ }
1870
+ .in-jira-epic {
1871
+ display: flex;
1872
+ flex-wrap: wrap;
1873
+ align-items: flex-end;
1874
+ gap: 8px;
1875
+ min-width: 0;
1876
+ }
1877
+ .in-jira-epic__text,
1878
+ .in-jira-count {
1879
+ font-size: 14px;
1880
+ font-weight: 600;
1881
+ }
1882
+ .in-jira-status-line {
1883
+ display: flex;
1884
+ flex-wrap: wrap;
1885
+ align-items: center;
1886
+ gap: 4px 12px;
1887
+ }
1888
+ .in-jira-when {
1889
+ font-size: 12px;
1890
+ color: var(--muted-foreground);
1891
+ }
1892
+ .in-jira-note {
1893
+ display: flex;
1894
+ flex-wrap: wrap;
1895
+ align-items: center;
1896
+ gap: 8px 12px;
1897
+ padding: 8px 12px;
1898
+ border: 1px solid var(--border);
1899
+ }
1900
+ .in-jira-note__text {
1901
+ flex: 1 1 200px;
1902
+ font-size: 14px;
1903
+ }
1904
+ .in-jira-list {
1905
+ display: flex;
1906
+ flex-direction: column;
1907
+ border-top: 1px solid var(--border);
1908
+ }
1909
+ .in-jira-pick-gap {
1910
+ display: inline-block;
1911
+ width: 16px;
1912
+ }
1913
+ .in-jira-where,
1914
+ .in-jira-sub {
1915
+ font-size: 12px;
1916
+ color: var(--muted-foreground);
1917
+ white-space: nowrap;
1918
+ }
1919
+ .in-jira-actions {
1920
+ display: flex;
1921
+ flex-wrap: wrap;
1922
+ align-items: center;
1923
+ gap: 8px;
1924
+ }
1925
+ .in-jira-detail-top {
1926
+ display: flex;
1927
+ flex-wrap: wrap;
1928
+ align-items: center;
1929
+ gap: 8px;
1930
+ }
1931
+ .in-jira-detail-top__gap {
1932
+ flex: 1 1 auto;
1933
+ }
1934
+ .in-jira-detail {
1935
+ display: flex;
1936
+ flex-direction: column;
1937
+ gap: 12px;
1938
+ min-width: 0;
1939
+ }
1940
+ .in-jira-detail__title {
1941
+ margin: 0;
1942
+ font-size: 20px;
1943
+ line-height: 28px;
1944
+ font-weight: 600;
1945
+ overflow-wrap: anywhere;
1946
+ }
1947
+ .in-jira-detail__meta {
1948
+ font-size: 12px;
1949
+ color: var(--muted-foreground);
1950
+ }
1951
+ .in-jira-warn {
1952
+ margin: 0;
1953
+ padding: 8px 12px;
1954
+ border: 1px solid var(--warning);
1955
+ font-size: 14px;
1956
+ }
1957
+ .in-jira-fields {
1958
+ display: grid;
1959
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1960
+ gap: 8px 12px;
1961
+ }
1962
+ .in-jira-box {
1963
+ display: flex;
1964
+ flex-wrap: wrap;
1965
+ align-items: center;
1966
+ gap: 4px 16px;
1967
+ padding: 10px 12px;
1968
+ border: 1px solid var(--border);
1969
+ }
1970
+ .in-jira-box__key {
1971
+ font-family: var(--mono);
1972
+ font-size: 14px;
1973
+ font-weight: 600;
1974
+ }
1975
+ .in-jira-box__where {
1976
+ font-size: 14px;
1977
+ }
1978
+ .in-jira-box__sub {
1979
+ font-size: 12px;
1980
+ color: var(--muted-foreground);
1981
+ }
1982
+ .in-jira-box__link {
1983
+ margin-left: auto;
1984
+ font-size: 12px;
1985
+ color: var(--foreground);
1986
+ text-underline-offset: 2px;
1987
+ }
1988
+ .in-jira-diff {
1989
+ display: flex;
1990
+ flex-direction: column;
1991
+ gap: 8px;
1992
+ padding: 10px 12px;
1993
+ border: 1px solid var(--destructive);
1994
+ }
1995
+ .in-jira-diff__title {
1996
+ margin: 0;
1997
+ font-size: 14px;
1998
+ font-weight: 600;
1999
+ }
2000
+ .in-jira-diff__sides {
2001
+ display: grid;
2002
+ grid-template-columns: 1fr 1fr;
2003
+ gap: 12px;
2004
+ }
2005
+ .in-jira-diff__side {
2006
+ display: flex;
2007
+ flex-direction: column;
2008
+ gap: 8px;
2009
+ min-width: 0;
2010
+ font-size: 14px;
2011
+ }
2012
+ .in-jira-diff__label,
2013
+ .in-jira-diff__name,
2014
+ .in-jira-body__name {
2015
+ margin: 0;
2016
+ font-size: 12px;
2017
+ font-weight: 600;
2018
+ letter-spacing: 0.1em;
2019
+ text-transform: uppercase;
2020
+ color: var(--muted-foreground);
2021
+ }
2022
+ .in-jira-diff__part,
2023
+ .in-jira-body__part {
2024
+ display: flex;
2025
+ flex-direction: column;
2026
+ gap: 4px;
2027
+ min-width: 0;
2028
+ overflow-wrap: anywhere;
2029
+ }
2030
+ .in-jira-body,
2031
+ .in-jira-edit {
2032
+ display: flex;
2033
+ flex-direction: column;
2034
+ gap: 16px;
2035
+ font-size: 14px;
2036
+ line-height: 20px;
2037
+ }
2038
+ .in-jira-body__empty {
2039
+ margin: 0;
2040
+ color: var(--muted-foreground);
2041
+ }
2042
+ .in-jira-area {
2043
+ min-height: 96px;
2044
+ }
2045
+ @media (max-width: 640px) {
2046
+ .in-jira-fields {
2047
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2048
+ }
2049
+ .in-jira-diff__sides {
2050
+ grid-template-columns: 1fr;
2051
+ }
2052
+ }
2053
+ @media (pointer: coarse) {
2054
+ .in-jira-box__link {
2055
+ display: inline-flex;
2056
+ align-items: center;
2057
+ min-height: 44px;
2058
+ }
2059
+ }
2060
+
2061
+
1856
2062
  /* -- layouts · .fr- the frame: top bar, dock, Settings (Run 3, slice 3A) -- */
1857
2063
  /* The frame's root: the top bar over the body. app.js drops the cockpit's inline geometry when the frame is on. */
1858
2064
  .fr-root {
@@ -3,7 +3,8 @@
3
3
  * About 70% of the width on a desktop and the whole screen on a phone. Top to bottom:
4
4
  * line 1 the title, the horizon, the health reason (only when not on track), and a meta line: lane · anchor · id
5
5
  * line 2 the stage stepper, then "Waiting on you: approval [Open gate ▸]" or "Waiting on agent: build [Hand to agent ▸]"
6
- * tabs Breakdown (the Run 1 pane, embedded: Tree · Board, and Timeline once Run 4 registers it), Files, History
6
+ * tabs Breakdown (the Run 1 pane, embedded: Tree · Board, and Timeline once Run 4 registers it), Files, History,
7
+ * Jira tickets (sheet-jira.js, initiative jira-wbs: the tickets drafted from the PRD and their status in Jira)
7
8
  * ⋯ horizon, check-by, lifecycle and the anchor (both PM-only: the server's guard answers), Copy CLI, Export breakdown
8
9
  * Nothing on this sheet says "plan". Everything it shows is read from GET /api/initiatives/<id>; every write goes through
9
10
  * initiatives-api.js. The Breakdown tab mounts with a tab id of the form `initiative:<slug>`, so it never meets a panel-4
@@ -17,6 +18,7 @@ import { el } from './dom.js';
17
18
  import { sheet, button, iconButton, tabs, row, badge, menu, toast, empty, field, ensureComponentsCss } from './primitives.js';
18
19
  import { icon, ensureSprite } from './icons.js';
19
20
  import { mountBreakdown, unmountBreakdown } from './panel-breakdown.js';
21
+ import { mountJira, unmountJira } from './sheet-jira.js';
20
22
  import { openDoc } from './panel-initiatives.js';
21
23
  import { openNewSession } from './panel-newsess.js';
22
24
  import { wsOf, readDoc, saveDoc } from './data.js';
@@ -73,7 +75,7 @@ export function openSheet(repo, slug) {
73
75
  const s = sheet({
74
76
  mode: 'auto', title: slug, sub: '', body: [crumb, stepperHost, waitingHost, tabsHost, panel],
75
77
  attrs: { class: 'in-sheet', 'data-in-sheet': 'initiative', 'data-in-id': slug },
76
- onClose: () => { closed = true; unmountBreakdown(panel); if (current === s) current = null; },
78
+ onClose: () => { closed = true; unmountBreakdown(panel); unmountJira(panel); if (current === s) current = null; },
77
79
  });
78
80
  current = s;
79
81
 
@@ -219,10 +221,11 @@ export function openSheet(repo, slug) {
219
221
  function drawTabs() {
220
222
  tabsHost.replaceChildren(tabs({
221
223
  variant: 'segmented', full: true, ariaLabel: COPY.sheet.tabs, value: tab,
222
- items: [{ id: 'breakdown', label: COPY.sheet.breakdown }, { id: 'files', label: COPY.sheet.files }, { id: 'history', label: COPY.sheet.history }],
224
+ items: [{ id: 'breakdown', label: COPY.sheet.breakdown }, { id: 'files', label: COPY.sheet.files }, { id: 'history', label: COPY.sheet.history }, { id: 'jira', label: COPY.sheet.jira }],
223
225
  onChange: (id) => {
224
226
  if (id === tab) return;
225
227
  if (tab === 'breakdown') unmountBreakdown(panel);
228
+ if (tab === 'jira') unmountJira(panel);
226
229
  tab = id;
227
230
  drawTabs(); drawPanel();
228
231
  const t = tabsHost.querySelector('[data-tab="' + id + '"]');
@@ -234,6 +237,7 @@ export function openSheet(repo, slug) {
234
237
  function drawPanel() {
235
238
  if (!data) return;
236
239
  if (tab === 'breakdown') { mountBreakdown(panel, { id: tabId, ws, slug }, { embedded: true }); return; }
240
+ if (tab === 'jira') { mountJira(panel, { ws, slug }); return; }
237
241
  if (tab === 'files') {
238
242
  const files = data.files || [];
239
243
  panel.replaceChildren(files.length ? el('div', { class: 'in-sheet__rows', role: 'list' }, files.map((f) => row({
@@ -120,6 +120,7 @@ export const COPY = {
120
120
  breakdown: 'Breakdown',
121
121
  files: 'Files',
122
122
  history: 'History',
123
+ jira: 'Jira tickets',
123
124
  waitingYou: (act) => `Waiting on you: ${act}`,
124
125
  waitingAgent: (act) => `Waiting on agent: ${act}`,
125
126
  nothingOwed: 'Nothing is owed.',