create-pmos 0.52.0 → 0.53.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.
Files changed (229) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/README.md +13 -0
  4. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
  6. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/plan-items-stage.js +271 -0
  11. package/payload/scripts/pmos-log.sh +18 -7
  12. package/payload/scripts/workflow-state.js +28 -2
  13. package/payload/state/discoveries.md +1 -1
  14. package/payload/state/log.md +1 -1
  15. package/workspace/README.md +2 -0
  16. package/workspace/REMOTE.md +24 -63
  17. package/workspace/bin/pmos.js +19 -3
  18. package/workspace/cut-stub.js +40 -0
  19. package/workspace/first-paint-audit.js +145 -0
  20. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  21. package/workspace/fixtures/initiatives-api/list.json +176 -0
  22. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  23. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  24. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  25. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  26. package/workspace/fixtures/report-api/draft.json +177 -0
  27. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  28. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  29. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  30. package/workspace/guide/board.md +81 -0
  31. package/workspace/guide/breakdown.md +154 -0
  32. package/workspace/guide/command-palette.md +75 -0
  33. package/workspace/guide/index.md +28 -17
  34. package/workspace/guide/initiatives-roadmap.md +130 -0
  35. package/workspace/guide/initiatives-screen.md +89 -0
  36. package/workspace/guide/initiatives-timeline.md +57 -0
  37. package/workspace/guide/integrated-work.md +94 -0
  38. package/workspace/guide/knowledge-graph.md +3 -68
  39. package/workspace/guide/launcher.md +51 -0
  40. package/workspace/guide/layout.md +102 -32
  41. package/workspace/guide/live-notes.md +3 -3
  42. package/workspace/guide/pm-only-acts.md +89 -0
  43. package/workspace/guide/pm-only-lines.md +61 -0
  44. package/workspace/guide/proactive-engine.md +21 -5
  45. package/workspace/guide/reports-page.md +80 -0
  46. package/workspace/guide/reports.md +73 -0
  47. package/workspace/guide/timeline.md +70 -0
  48. package/workspace/guide/today.md +85 -0
  49. package/workspace/guide/validate-with-typesafe.md +1 -1
  50. package/workspace/lib/actor.js +91 -0
  51. package/workspace/lib/board-cache.js +32 -2
  52. package/workspace/lib/board-cli.js +108 -0
  53. package/workspace/lib/board-fold.js +232 -0
  54. package/workspace/lib/breakdown-export.js +75 -0
  55. package/workspace/lib/brief.js +20 -2
  56. package/workspace/lib/capture.js +98 -0
  57. package/workspace/lib/engine/plan-items-stage.js +271 -0
  58. package/workspace/lib/engine/workflow-state.js +28 -2
  59. package/workspace/lib/factory-join.js +158 -0
  60. package/workspace/lib/goals.js +165 -0
  61. package/workspace/lib/grant-mint.js +113 -0
  62. package/workspace/lib/health-rules-factory.js +92 -0
  63. package/workspace/lib/health-rules-plan.js +164 -0
  64. package/workspace/lib/health.js +79 -0
  65. package/workspace/lib/initiative-cli.js +222 -0
  66. package/workspace/lib/initiative-display.js +183 -0
  67. package/workspace/lib/initiative-needs.js +127 -0
  68. package/workspace/lib/initiative-rows.js +210 -0
  69. package/workspace/lib/initiative-writer.js +141 -0
  70. package/workspace/lib/issues.js +206 -0
  71. package/workspace/lib/item-cli.js +195 -0
  72. package/workspace/lib/item-spans.js +137 -0
  73. package/workspace/lib/launcher-cli.js +207 -0
  74. package/workspace/lib/measure-cli.js +100 -0
  75. package/workspace/lib/measurements.js +189 -0
  76. package/workspace/lib/mobile-nav.js +57 -53
  77. package/workspace/lib/needs-providers.js +120 -0
  78. package/workspace/lib/placements.js +440 -0
  79. package/workspace/lib/plan-items.js +922 -0
  80. package/workspace/lib/plan-ledgers.js +85 -0
  81. package/workspace/lib/plan-rules.js +72 -0
  82. package/workspace/lib/plan-tree.js +380 -0
  83. package/workspace/lib/pm-guard.js +174 -0
  84. package/workspace/lib/pm-lines-needs.js +331 -0
  85. package/workspace/lib/pmos-verbs.js +69 -0
  86. package/workspace/lib/proactive-cli.js +58 -2
  87. package/workspace/lib/proactive.js +9 -6
  88. package/workspace/lib/qr.js +293 -0
  89. package/workspace/lib/remote-access.js +93 -1
  90. package/workspace/lib/report-cli.js +152 -0
  91. package/workspace/lib/report-draft.js +578 -0
  92. package/workspace/lib/report-needs.js +213 -0
  93. package/workspace/lib/route-modules.js +128 -0
  94. package/workspace/lib/routes-board.js +91 -0
  95. package/workspace/lib/routes-capture.js +52 -0
  96. package/workspace/lib/routes-initiatives.js +371 -0
  97. package/workspace/lib/routes-reports.js +163 -0
  98. package/workspace/lib/routes-timeline.js +197 -0
  99. package/workspace/lib/routes-today.js +32 -0
  100. package/workspace/lib/today-cli.js +99 -0
  101. package/workspace/lib/today.js +333 -0
  102. package/workspace/lib/triage.js +163 -0
  103. package/workspace/lib/verify-work-cli.js +81 -0
  104. package/workspace/lib/work-check-needs.js +99 -0
  105. package/workspace/package-lock.json +1 -1
  106. package/workspace/package.json +4 -5
  107. package/workspace/public/icons/apple-touch-icon.png +0 -0
  108. package/workspace/public/icons/icon-192.png +0 -0
  109. package/workspace/public/icons/icon-512.png +0 -0
  110. package/workspace/public/index.html +40 -19
  111. package/workspace/public/pm/app.js +330 -30
  112. package/workspace/public/pm/bd-board.js +113 -0
  113. package/workspace/public/pm/bd-drag.js +59 -0
  114. package/workspace/public/pm/bd-model.js +152 -0
  115. package/workspace/public/pm/bd-timeline.js +233 -0
  116. package/workspace/public/pm/bd-tree.js +244 -0
  117. package/workspace/public/pm/board-copy.js +57 -0
  118. package/workspace/public/pm/breakdown-api.js +58 -0
  119. package/workspace/public/pm/breakdown-copy.js +138 -0
  120. package/workspace/public/pm/components.css +3135 -0
  121. package/workspace/public/pm/cycle-review.js +125 -0
  122. package/workspace/public/pm/data.js +40 -187
  123. package/workspace/public/pm/dest-board.js +438 -0
  124. package/workspace/public/pm/dest-initiatives.js +134 -0
  125. package/workspace/public/pm/dest-reports.js +270 -0
  126. package/workspace/public/pm/dest-settings.js +217 -0
  127. package/workspace/public/pm/dest-today.js +182 -0
  128. package/workspace/public/pm/dialogs.js +88 -0
  129. package/workspace/public/pm/display-word.js +56 -0
  130. package/workspace/public/pm/dock.js +175 -0
  131. package/workspace/public/pm/fixture.js +8 -227
  132. package/workspace/public/pm/frame-copy.js +99 -0
  133. package/workspace/public/pm/handoff-box.js +82 -0
  134. package/workspace/public/pm/http.js +88 -0
  135. package/workspace/public/pm/icons.js +53 -0
  136. package/workspace/public/pm/initiative-list.js +218 -0
  137. package/workspace/public/pm/initiative-sheet.js +261 -0
  138. package/workspace/public/pm/initiatives-api.js +59 -0
  139. package/workspace/public/pm/initiatives-copy.js +215 -0
  140. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  141. package/workspace/public/pm/initiatives-timeline.js +410 -0
  142. package/workspace/public/pm/initiatives-ui.js +123 -0
  143. package/workspace/public/pm/item-sheet.js +251 -0
  144. package/workspace/public/pm/keep-card.js +204 -0
  145. package/workspace/public/pm/lifecycle.js +7 -1
  146. package/workspace/public/pm/mobile.css +49 -20
  147. package/workspace/public/pm/mobile.js +157 -29
  148. package/workspace/public/pm/modes-live.js +7 -883
  149. package/workspace/public/pm/modes.js +32 -1024
  150. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  151. package/workspace/public/pm/next-step-ui.js +3 -2
  152. package/workspace/public/pm/palette-copy.js +51 -0
  153. package/workspace/public/pm/palette.js +312 -0
  154. package/workspace/public/pm/panel-breakdown.js +702 -0
  155. package/workspace/public/pm/panel-cockpit.js +63 -295
  156. package/workspace/public/pm/panel-detail.js +25 -172
  157. package/workspace/public/pm/panel-initiatives.js +1 -19
  158. package/workspace/public/pm/panel-repos.js +31 -81
  159. package/workspace/public/pm/panel-tabs.js +200 -111
  160. package/workspace/public/pm/panel-trees.js +5 -2
  161. package/workspace/public/pm/portfolio-report.js +61 -0
  162. package/workspace/public/pm/primitives.js +656 -0
  163. package/workspace/public/pm/print.css +33 -0
  164. package/workspace/public/pm/provider-cards.js +136 -0
  165. package/workspace/public/pm/registry.js +194 -0
  166. package/workspace/public/pm/release-card.js +110 -0
  167. package/workspace/public/pm/report-view.js +309 -0
  168. package/workspace/public/pm/reports-copy.js +114 -0
  169. package/workspace/public/pm/roadmap.js +271 -0
  170. package/workspace/public/pm/sig-watch.js +41 -0
  171. package/workspace/public/pm/state.js +8 -4
  172. package/workspace/public/pm/status-banner.js +13 -5
  173. package/workspace/public/pm/tab-doc.js +2 -4
  174. package/workspace/public/pm/tab-term.js +12 -2
  175. package/workspace/public/pm/tab-web.js +3 -2
  176. package/workspace/public/pm/timeline-copy.js +48 -0
  177. package/workspace/public/pm/today-cards.js +244 -0
  178. package/workspace/public/pm/today-copy.js +126 -0
  179. package/workspace/public/pm/today-goals.js +172 -0
  180. package/workspace/public/pm/tokens.css +16 -18
  181. package/workspace/public/pm/topbar.js +102 -0
  182. package/workspace/public/pm/triage-card.js +60 -0
  183. package/workspace/public/pm/undo-hold.js +103 -0
  184. package/workspace/public/pm/view-addrepo.js +2 -1
  185. package/workspace/public/pm/view-livenotes.js +3 -1
  186. package/workspace/public/pm/view-today.js +7 -162
  187. package/workspace/public/vendor/Sortable.min.js +2 -0
  188. package/workspace/public/vendor/lucide-sprite.js +79 -0
  189. package/workspace/replaced-tests.json +2877 -0
  190. package/workspace/run-tests.js +56 -5
  191. package/workspace/server.js +446 -40
  192. package/workspace/sheet-answer.js +61 -0
  193. package/workspace/skills/measure.skill +41 -0
  194. package/workspace/skills/morning-brief.skill +15 -9
  195. package/workspace/skills/report-biweekly.skill +23 -25
  196. package/workspace/skills/tracker-refresh.skill +50 -0
  197. package/workspace/test-files.json +21 -3
  198. package/workspace/test-lists/base.json +6 -0
  199. package/workspace/test-lists/run2-a.json +6 -0
  200. package/workspace/test-lists/run2-b.json +13 -0
  201. package/workspace/test-lists/run2-c.json +19 -0
  202. package/workspace/test-lists/run2-d.json +22 -0
  203. package/workspace/test-lists/run3-0.json +5 -0
  204. package/workspace/test-lists/run3-a.json +5 -0
  205. package/workspace/test-lists/run3-b.json +6 -0
  206. package/workspace/test-lists/run3-c.json +15 -0
  207. package/workspace/test-lists/run3-d.json +6 -0
  208. package/workspace/test-lists/run4-a.json +8 -0
  209. package/workspace/test-lists/run4-b.json +6 -0
  210. package/workspace/test-lists/run4-c.json +13 -0
  211. package/workspace/test-lists/run4-d.json +9 -0
  212. package/workspace/test-lists/run5-a.json +6 -0
  213. package/workspace/test-lists/run5-b.json +6 -0
  214. package/workspace/test-lists/run5-c.json +15 -0
  215. package/workspace/test-lists/workspace-launcher.json +5 -0
  216. package/workspace/test-lists/workspace-phone-app.json +5 -0
  217. package/workspace/test-run.js +27 -1
  218. package/workspace/third-party/README.md +44 -0
  219. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  220. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  221. package/workspace/design-parity.js +0 -673
  222. package/workspace/public/pm/tab-task.js +0 -86
  223. package/workspace/public/pm/view-guide.js +0 -141
  224. package/workspace/public/pm/view-knowledge.js +0 -225
  225. package/workspace/public/pm/view-memory.js +0 -122
  226. package/workspace/public/pm/view-portfolio.js +0 -153
  227. package/workspace/public/pm/view-reports.js +0 -190
  228. package/workspace/public/pm/view-scheduled.js +0 -161
  229. package/workspace/public/pm/view-skills.js +0 -216
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+ /* item-cli.js — `pmos item …`: an initiative's breakdown items from a terminal (pm-os-absorb Run 1, PRD R24).
3
+ *
4
+ * pmos item add <initiative> "<title>" --stage <id|label> [--due YYYY-MM-DD] [--start YYYY-MM-DD]
5
+ * [--owner you|agent|person:<name>|tracker:<ref>] [--after <id>[,<id>]] [--milestone] [--note "<text>"] [--json]
6
+ * pmos item set <item> [--title ..] [--stage ..] [--due ..|none] [--start ..|none] [--owner ..] [--after ids|none]
7
+ * [--milestone|--no-milestone] [--note ..|none] [--blocked "<reason>"|none] [--json]
8
+ * pmos item move <item> <todo|doing|review|done> [--evidence <commit|pr|file|session|note>:<ref>] [--json]
9
+ * pmos item withdraw <item> [--json]
10
+ * pmos item list <initiative> [--json]
11
+ * common: [--root <dir>]
12
+ *
13
+ * The SAME writer and the SAME fold the workspace page uses (lib/plan-items.js) — no server is needed, and
14
+ * the terminal and the page cannot disagree about an item. Who wrote a line is decided by the call path,
15
+ * never by an argument: `pm:wawan` at an interactive terminal, the bound session from PMOS_SESSION, and
16
+ * `agent:unknown` otherwise. There is no `keep` verb: Keep is the PM's press on the workspace page.
17
+ */
18
+ const path = require('path');
19
+ const { execFileSync } = require('child_process');
20
+ const planItems = require('./plan-items.js');
21
+ const planTree = require('./plan-tree.js');
22
+ const { parseArgs } = require('./schedule-cli');
23
+
24
+ const COMMANDS = { item: ['add', 'set', 'move', 'withdraw', 'list'] };
25
+ const USAGE = [
26
+ 'usage: pmos item add|set|move|withdraw|list — an initiative\'s breakdown items',
27
+ ' pmos item add <initiative> "<title>" --stage <stage> [--due YYYY-MM-DD] [--start YYYY-MM-DD]',
28
+ ' [--owner you|agent|person:<name>|tracker:<ref>] [--after <item>[,<item>]] [--milestone] [--note "<text>"]',
29
+ ' pmos item set <item> [--title "<title>"] [--stage <stage>] [--due <date>|none] [--start <date>|none] [--owner <owner>]',
30
+ ' [--after <items>|none] [--milestone|--no-milestone] [--note "<text>"|none] [--blocked "<reason>"|none]',
31
+ ' pmos item move <item> todo|doing|review|done [--evidence commit|pr|file|session|note:<ref>]',
32
+ ' pmos item withdraw <item>',
33
+ ' pmos item list <initiative>',
34
+ ' every command: [--json] [--root <dir>]',
35
+ 'An item id is <initiative>.<10 characters>. A stage is an id or a label of the initiative\'s stages.',
36
+ 'Who wrote each line comes from where the command runs: you at a terminal, the agent session otherwise.',
37
+ 'Keep is pressed on the workspace page (Breakdown or Today). This command does not write it.',
38
+ ];
39
+ const BOOL = new Set(['json', 'milestone', 'no-milestone', 'help']);
40
+ const VALUE = new Set(['title', 'stage', 'due', 'start', 'owner', 'after', 'note', 'blocked', 'evidence', 'root']);
41
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
42
+ const STATE_LABEL = { todo: 'Todo', doing: 'Doing', review: 'Review', done: 'Done', withdrawn: 'Withdrawn' };
43
+
44
+ function parse(argv) {
45
+ const rest = []; const bools = {};
46
+ for (const a of argv) {
47
+ if (a.startsWith('--') && BOOL.has(a.slice(2))) bools[a.slice(2)] = true;
48
+ else if (a === '-h') bools.help = true;
49
+ else rest.push(a);
50
+ }
51
+ const { pos, flags } = parseArgs(rest);
52
+ return { pos, flags: { ...flags, ...bools } };
53
+ }
54
+
55
+ function repoRoot(cwd) {
56
+ try { return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; }
57
+ catch { return cwd; }
58
+ }
59
+ const shortDate = (d) => { if (!d) return null; const t = new Date(d + 'T00:00:00Z'); return `${t.getUTCDate()} ${MONTHS[t.getUTCMonth()]}`; };
60
+ const csv = (v) => String(v).split(',').map((s) => s.trim()).filter(Boolean);
61
+
62
+ /** One line about an item, for a person. */
63
+ function describe(it, stageLabel) {
64
+ const bits = [`${it.id} "${it.title}"`, stageLabel || it.stage, STATE_LABEL[it.state] || it.state];
65
+ if (it.milestone) bits.push('milestone');
66
+ if (it.due) bits.push(`due ${shortDate(it.due)}`);
67
+ if (it.blocked) bits.push(`blocked: ${it.blocked.reason}`);
68
+ if (it.proposed) bits.push('proposed');
69
+ return bits.join(' · ');
70
+ }
71
+
72
+ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, now } = {}) {
73
+ const args = argv[0] === 'item' ? argv.slice(1) : argv.slice();
74
+ const verb = args[0];
75
+ const { pos, flags } = parse(args.slice(1));
76
+ const json = !!flags.json;
77
+ if (!verb || verb === 'help' || verb === '--help' || verb === '-h' || flags.help) {
78
+ for (const l of USAGE) say(l);
79
+ return verb ? 0 : 1; // asked for help → 0; no command at all → 1, as `pmos kg` does
80
+ }
81
+ if (verb === 'keep') { say('Keep is written only from the workspace page (Breakdown or Today).'); return 2; }
82
+ if (!COMMANDS.item.includes(verb)) { say(`unknown item command "${verb}" — one of ${COMMANDS.item.join(', ')}`); return 2; }
83
+ if ('by' in flags || 'via' in flags) { say(`attribution is set from the call path; --${'by' in flags ? 'by' : 'via'} is not accepted`); return 2; }
84
+ for (const k of Object.keys(flags)) {
85
+ if (!BOOL.has(k) && !VALUE.has(k)) { say(`unknown option --${k}`); return 2; }
86
+ if (VALUE.has(k) && flags[k] === '') { say(`--${k} needs a value`); return 2; }
87
+ }
88
+
89
+ const root = flags.root ? path.resolve(cwd, flags.root) : repoRoot(cwd);
90
+ const cliOpts = {};
91
+ if (isTTY !== undefined) cliOpts.isTTY = isTTY;
92
+ if (env !== undefined) cliOpts.env = env;
93
+ if (now !== undefined) cliOpts.now = now;
94
+ const fail = (error, code = 1) => { if (json) say(JSON.stringify({ ok: false, error })); else say(`✗ ${error}`); return code; };
95
+ const stageLabelOf = (initiative, id) => { try { const s = planTree.stagesFor(root, initiative).find((x) => x.id === id); return s ? s.label : id; } catch { return id; } };
96
+ const fresh = (id) => planItems.readFold(root, { now }).items[id] || null;
97
+
98
+ if (verb === 'list') {
99
+ const initiative = pos[0];
100
+ if (!initiative) { say('usage: pmos item list <initiative> [--json]'); return 2; }
101
+ if (!planItems.INIT_RE.test(initiative) || !planTree.recordPath(root, initiative)) return fail(`no initiative ${initiative} in this repository`);
102
+ const r = planItems.readFold(root, { now });
103
+ const stages = planTree.stagesFor(root, initiative);
104
+ const idx = (id) => { const i = stages.findIndex((s) => s.id === id); return i === -1 ? stages.length : i; };
105
+ const items = r.order.map((id) => r.items[id]).filter((it) => it.initiative === initiative)
106
+ .sort((a, b) => (idx(a.stage) - idx(b.stage)) || (a.order - b.order) || String(a.createdAt).localeCompare(String(b.createdAt)) || a.id.localeCompare(b.id));
107
+ const progress = planItems.progressOf(r, initiative);
108
+ if (json) {
109
+ const { profile } = planTree.profileAndLane(root, initiative);
110
+ say(JSON.stringify({ ok: true, initiative, profile, progress, items }));
111
+ return 0;
112
+ }
113
+ say(`${initiative} · ${progress.label}`);
114
+ for (const s of [...stages, { id: null, label: 'Other' }]) {
115
+ const mine = items.filter((it) => (s.id === null ? idx(it.stage) === stages.length : it.stage === s.id));
116
+ if (!mine.length) continue;
117
+ const real = mine.filter((it) => !it.proposed);
118
+ say(` ${s.label}${real.length ? ` · ${real.filter((it) => it.state === 'done').length} of ${real.length}` : ''}`);
119
+ for (const it of mine) say(` ${describe(it, s.id ? s.label : it.stage)} · ${it.owner}`);
120
+ }
121
+ return 0;
122
+ }
123
+
124
+ let events; let id = null; let before = null;
125
+ if (verb === 'add') {
126
+ const [initiative, title] = pos;
127
+ if (!initiative || title === undefined) { say('usage: pmos item add <initiative> "<title>" --stage <stage> [options]'); return 2; }
128
+ if (!planItems.INIT_RE.test(initiative) || !planTree.recordPath(root, initiative)) return fail(`no initiative ${initiative} in this repository`);
129
+ if (!flags.stage) {
130
+ const stages = planTree.stagesFor(root, initiative);
131
+ say(`--stage is required — the stages of ${initiative} are: ${stages.map((s) => `${s.id} (${s.label})`).join(', ')}`);
132
+ return 2;
133
+ }
134
+ const ev = { ev: 'planned', initiative, title, stage: flags.stage };
135
+ if (flags.due) ev.due = flags.due;
136
+ if (flags.start) ev.start = flags.start;
137
+ if (flags.owner) ev.owner = flags.owner;
138
+ if (flags.after) ev.after = csv(flags.after);
139
+ if (flags.milestone) ev.milestone = true;
140
+ if (flags.note) ev.note = flags.note;
141
+ for (const k of ['title', 'blocked', 'evidence', 'no-milestone']) if (k in flags) return fail(`--${k} is not an option of add`, 2);
142
+ events = [ev];
143
+ } else {
144
+ id = pos[0];
145
+ if (!id) { say(`usage: pmos item ${verb} <item> …`); return 2; }
146
+ before = (() => { try { return fresh(id); } catch { return null; } })();
147
+ if (verb === 'set') {
148
+ const fields = {};
149
+ if (flags.title) fields.title = flags.title;
150
+ if (flags.stage) fields.stage = flags.stage;
151
+ for (const k of ['due', 'start', 'note']) if (flags[k]) fields[k] = flags[k] === 'none' ? null : flags[k];
152
+ if (flags.owner) fields.owner = flags.owner;
153
+ if (flags.after) fields.after = flags.after === 'none' ? [] : csv(flags.after);
154
+ if (flags.milestone) fields.milestone = true;
155
+ if (flags['no-milestone']) fields.milestone = false;
156
+ if (flags.milestone && flags['no-milestone']) return fail('--milestone and --no-milestone contradict each other', 2);
157
+ if (flags.evidence) return fail('--evidence belongs to move', 2);
158
+ events = [];
159
+ if (Object.keys(fields).length) events.push({ ev: 'set', item: id, fields });
160
+ if (flags.blocked) events.push(flags.blocked === 'none' ? { ev: 'flagged', item: id, blocked: false } : { ev: 'flagged', item: id, blocked: true, reason: flags.blocked });
161
+ if (!events.length) { say('nothing to set — name at least one option (pmos item help)'); return 2; }
162
+ } else if (verb === 'move') {
163
+ const to = String(pos[1] || '').toLowerCase();
164
+ if (!planItems.STATES.includes(to)) { say(`usage: pmos item move <item> ${planItems.STATES.join('|')} [--evidence <kind>:<ref>]`); return 2; }
165
+ for (const k of Object.keys(flags)) if (!['json', 'root', 'evidence'].includes(k)) return fail(`--${k} is not an option of move`, 2);
166
+ const ev = { ev: 'moved', item: id, to };
167
+ if (flags.evidence) ev.evidence = flags.evidence;
168
+ events = [ev];
169
+ } else if (verb === 'withdraw') {
170
+ for (const k of Object.keys(flags)) if (!['json', 'root'].includes(k)) return fail(`--${k} is not an option of withdraw`, 2);
171
+ events = [{ ev: 'withdrawn', item: id }];
172
+ }
173
+ }
174
+
175
+ let r;
176
+ try { r = planItems.appendFromCli(root, events, cliOpts); }
177
+ catch (e) { return fail('could not record — ' + String((e && e.message) || e).slice(0, 200)); }
178
+ if (!r.ok) return fail(r.error);
179
+ const itemId = id || (r.written[0] && r.written[0].item);
180
+ const after = (() => { try { return fresh(itemId); } catch { return null; } })();
181
+ const item = after || (before ? { ...before, state: 'withdrawn' } : null);
182
+ const actor = planItems.actorFromCli({ ...(isTTY !== undefined ? { isTTY } : {}), ...(env !== undefined ? { env } : {}) });
183
+ if (json) { say(JSON.stringify({ ok: true, item })); return 0; }
184
+ const by = planTree.byLabel(actor.by);
185
+ const label = item ? stageLabelOf(item.initiative, item.stage) : '';
186
+ if (verb === 'add') say(`added ${item.id} "${item.title}" · ${label}${item.due ? ` · due ${shortDate(item.due)}` : ''} · by ${by}`);
187
+ else if (verb === 'withdraw') say(`withdrew ${itemId}${before ? ` "${before.title}"` : ''} · by ${by}`);
188
+ else if (verb === 'move') {
189
+ say(`moved ${item.id} "${item.title}" to ${STATE_LABEL[item.state]}${item.evidence ? ` with evidence ${item.evidence.kind}:${item.evidence.ref}` : ''} · by ${by}`);
190
+ if (item.noEvidence) say(' no evidence — an agent Done with no evidence goes to the PM. Add --evidence <kind>:<ref>.');
191
+ } else say(`updated ${describe(item, label)} · by ${by}`);
192
+ return 0;
193
+ }
194
+
195
+ module.exports = { run, COMMANDS, USAGE };
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+ // item-spans.js — the dated bars of a breakdown: one span per item from its start and due, and the initiative's span
3
+ // over them (pm-os-absorb, PRD R20, R8 predecessor flag).
4
+ //
5
+ // spansOf(items, { stages }) → [{ id, stage, start, due, milestone, proposed, overlapsPredecessor, overlap }]
6
+ // initiativeSpan(spans, { proposed }) → { start, due } | null
7
+ // stageSpans(spans, { proposed }) → [{ stage, start, due }] one per stage, in the order of the spans
8
+ // axisOf(spans, { today }) → { start, end, months: [{ month, start, end }] } whole months, at least MIN_MONTHS
9
+ // dayNum(date) · addDays(date, n) · daysBetween(a, b) calendar-day arithmetic on YYYY-MM-DD, never shifted by a zone
10
+ //
11
+ // `items` is the `items` of a plan-items fold or layout (lib/plan-items.js, lib/plan-tree.js): an object keyed by item
12
+ // id, or a list. Dates are YYYY-MM-DD strings. One engine for Breakdown › Timeline and Initiatives › Timeline (Run 4)
13
+ // and the report's Timeline strip (Run 5). Classic script (a UMD setting window.PMOSItemSpans), served from /lib/, so the
14
+ // bars on the screen and the numbers in a report cannot disagree.
15
+ //
16
+ // The rules (Run 4, slice 4B):
17
+ // - A withdrawn item has no span. An item with neither date has no span. An item with one date spans that one day.
18
+ // - Spans come in stage order (`stages`: stage ids or { id } objects, as the layout lists them), then by the item's
19
+ // `order`, then by start. A stage the list does not name sorts last.
20
+ // - `milestone` is the item's own flag. A milestone's ◆ sits on its due date.
21
+ // - `proposed` is the item's own flag: an agent's proposal that the PM has not kept (lib/plan-items.js). It has a span,
22
+ // but initiativeSpan and stageSpans leave it out unless `{ proposed: true }` is passed, so a batch of proposals
23
+ // that nobody kept never stretches a stage or an initiative bar. axisOf includes it, so its bar has room.
24
+ // - `overlapsPredecessor` (R8, at risk): the item starts on or before the day a predecessor ends. A predecessor is an
25
+ // id in the item's `after` list that is among `items`, live and not Done; it ends on its due date (or its start when
26
+ // it has no due). An item that is Done is never flagged. `overlap` names the predecessor that ends last, as
27
+ // { id, due }, so the screen can give the reason; it is null when nothing overlaps.
28
+ // - A predecessor in another initiative counts only when the caller passes its item too. No date is ever moved here:
29
+ // the flag shows a clash, and the PM decides (no cascade, PRD R20).
30
+ (function (root, factory) {
31
+ const api = factory();
32
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
33
+ else root.PMOSItemSpans = api;
34
+ }(typeof self !== 'undefined' ? self : this, function () {
35
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
36
+ const DAY = 86400000;
37
+ const MIN_MONTHS = 3;
38
+ const date = (v) => (typeof v === 'string' && DATE.test(v) ? v : null);
39
+
40
+ /** Days since 1970-01-01 for a YYYY-MM-DD date (NaN for anything else). */
41
+ function dayNum(d) {
42
+ if (!date(d)) return NaN;
43
+ return Math.round(Date.UTC(Number(d.slice(0, 4)), Number(d.slice(5, 7)) - 1, Number(d.slice(8, 10))) / DAY);
44
+ }
45
+ const fromNum = (n) => new Date(n * DAY).toISOString().slice(0, 10);
46
+ /** The date `n` calendar days after `d` (before it when `n` is negative); null for a bad date. */
47
+ function addDays(d, n) { const x = dayNum(d); return Number.isNaN(x) ? null : fromNum(x + Math.round(Number(n) || 0)); }
48
+ /** Whole days from `a` to `b` (b − a). */
49
+ function daysBetween(a, b) { return dayNum(b) - dayNum(a); }
50
+
51
+ const listOf = (items) => (Array.isArray(items) ? items : items && typeof items === 'object' ? Object.values(items) : []);
52
+
53
+ function stageRank(stages) {
54
+ const rank = new Map();
55
+ (Array.isArray(stages) ? stages : []).forEach((s, i) => {
56
+ const id = typeof s === 'string' ? s : s && typeof s.id === 'string' ? s.id : null;
57
+ if (id && !rank.has(id)) rank.set(id, i);
58
+ });
59
+ return (id) => (rank.has(id) ? rank.get(id) : rank.size);
60
+ }
61
+
62
+ function spansOf(items, opts) {
63
+ const rank = stageRank(opts && opts.stages);
64
+ const live = listOf(items).filter((it) => it && typeof it.id === 'string' && it.state !== 'withdrawn');
65
+ const byId = new Map(live.map((it) => [it.id, it]));
66
+ const out = [];
67
+ for (const it of live) {
68
+ const start = date(it.start);
69
+ const due = date(it.due);
70
+ if (!start && !due) continue;
71
+ const s = start || due;
72
+ let overlap = null;
73
+ if (it.state !== 'done') {
74
+ for (const pid of Array.isArray(it.after) ? it.after : []) {
75
+ const p = byId.get(pid);
76
+ if (!p || p.state === 'done') continue;
77
+ const end = date(p.due) || date(p.start);
78
+ if (end && s <= end && (!overlap || end > overlap.due)) overlap = { id: pid, due: end };
79
+ }
80
+ }
81
+ out.push({
82
+ id: it.id, stage: it.stage || null, start: s, due: due || start, milestone: !!it.milestone, proposed: !!it.proposed,
83
+ overlapsPredecessor: !!overlap, overlap,
84
+ _rank: rank(it.stage), _order: Number.isFinite(it.order) ? it.order : Infinity,
85
+ });
86
+ }
87
+ out.sort((a, b) => a._rank - b._rank || a._order - b._order || (a.start < b.start ? -1 : a.start > b.start ? 1 : 0) || (a.id < b.id ? -1 : 1));
88
+ for (const s of out) { delete s._rank; delete s._order; }
89
+ return out;
90
+ }
91
+
92
+ const counted = (spans, opts) => listOf(spans).filter((s) => s && date(s.start) && date(s.due) && (!s.proposed || !!(opts && opts.proposed)));
93
+
94
+ function initiativeSpan(spans, opts) {
95
+ const list = counted(spans, opts);
96
+ if (!list.length) return null;
97
+ let start = list[0].start;
98
+ let due = list[0].due;
99
+ for (const s of list) { if (s.start < start) start = s.start; if (s.due > due) due = s.due; }
100
+ return { start, due };
101
+ }
102
+
103
+ function stageSpans(spans, opts) {
104
+ const by = new Map();
105
+ for (const s of counted(spans, opts)) {
106
+ const k = s.stage || null;
107
+ const g = by.get(k);
108
+ if (!g) by.set(k, { stage: k, start: s.start, due: s.due });
109
+ else { if (s.start < g.start) g.start = s.start; if (s.due > g.due) g.due = s.due; }
110
+ }
111
+ return [...by.values()];
112
+ }
113
+
114
+ /** The time axis for `spans`: whole months from the earliest start to the latest due, with `today` inside it. */
115
+ function axisOf(spans, opts) {
116
+ const today = date(opts && opts.today);
117
+ const whole = initiativeSpan(spans, { proposed: true });
118
+ let lo = whole ? whole.start : today;
119
+ let hi = whole ? whole.due : today;
120
+ if (today) { if (today < lo) lo = today; if (today > hi) hi = today; }
121
+ if (!lo) return null;
122
+ const months = [];
123
+ let y = Number(lo.slice(0, 4));
124
+ let m = Number(lo.slice(5, 7));
125
+ const pad = (n) => String(n).padStart(2, '0');
126
+ for (;;) {
127
+ const first = `${y}-${pad(m)}-01`;
128
+ const next = m === 12 ? `${y + 1}-01-01` : `${y}-${pad(m + 1)}-01`;
129
+ months.push({ month: `${y}-${pad(m)}`, start: first, end: addDays(next, -1) });
130
+ if (next > hi && months.length >= MIN_MONTHS) break;
131
+ if (m === 12) { y++; m = 1; } else m++;
132
+ }
133
+ return { start: months[0].start, end: months[months.length - 1].end, months };
134
+ }
135
+
136
+ return { spansOf, initiativeSpan, stageSpans, axisOf, dayNum, addDays, daysBetween, MIN_MONTHS };
137
+ }));
@@ -0,0 +1,207 @@
1
+ 'use strict';
2
+ /* launcher-cli.js — `pmos launcher on|off|status`: start the workspace without a terminal (initiative workspace-launcher).
3
+ *
4
+ * pmos launcher on [<path>] [--no-login] write a PMOS app (and, unless --no-login, a login item) for one repository
5
+ * pmos launcher off remove what `on` wrote, and nothing else
6
+ * pmos launcher status say what is installed and whether it still points at a real node and repo
7
+ *
8
+ * The app and the login item are two more callers of `pmos up <repo>`; nothing here starts a server itself. The app
9
+ * opens the browser. The login item passes --no-open, so a login starts the server and opens nothing.
10
+ *
11
+ * macOS ~/Applications/PMOS.app Spotlight, Launchpad, drag it to the Dock
12
+ * ~/Library/LaunchAgents/com.pmos.workspace.plist runs at login (RunAtLoad), once
13
+ * Linux $XDG_DATA_HOME/applications/pmos.desktop the app menu
14
+ * $XDG_CONFIG_HOME/autostart/pmos-workspace.desktop
15
+ *
16
+ * A file started by the desktop has no shell profile, so the PATH of the terminal that ran `on` is written into the
17
+ * script; that is how `claude`, `git` and `tmux` resolve. It never starts remote (tailnet) mode and writes no token.
18
+ * `off` deletes a file only when it carries MARK, so a file of the same name that the PM wrote is left alone.
19
+ * Native Windows is refused: the workspace needs WSL there for a real PTY (doctor.js says the same).
20
+ *
21
+ * plan() is pure: every input is injected, so each platform's files are assertable from any machine.
22
+ */
23
+ const fs = require('fs');
24
+ const os = require('os');
25
+ const path = require('path');
26
+ const { spawnSync } = require('child_process');
27
+ const stateRoot = require('./state-root');
28
+
29
+ const COMMANDS = { launcher: ['on', 'off', 'status'] };
30
+ const MARK = 'pmos-launcher: written by `pmos launcher on`';
31
+ const LABEL = 'com.pmos.workspace';
32
+ const USAGE = [
33
+ 'usage: pmos launcher on|off|status — start the workspace without a terminal',
34
+ ' pmos launcher on [<path>] [--no-login] a PMOS app for this repository, and a login item that starts the server',
35
+ ' pmos launcher off remove the app and the login item',
36
+ ' pmos launcher status what is installed, and for which repository',
37
+ ];
38
+
39
+ // POSIX shell single quotes: nothing inside is special except the quote itself.
40
+ const shq = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
41
+ // XML text and attribute values in a plist.
42
+ const xml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
43
+ // A .desktop Exec argument: double-quoted, with the four characters the spec reserves escaped.
44
+ const deq = (s) => `"${String(s).replace(/[\\"`$]/g, (c) => '\\' + c)}"`;
45
+
46
+ function script({ node, bin, repo, pathEnv, log }) {
47
+ return [
48
+ '#!/bin/sh',
49
+ `# ${MARK} (workspace/lib/launcher-cli.js). \`pmos launcher off\` removes it.`,
50
+ '# It runs `pmos up` on one repository with the PATH the terminal had at setup. Arguments pass through (--no-open).',
51
+ `PATH=${shq(pathEnv)}; export PATH`,
52
+ `exec ${shq(node)} ${shq(bin)} up ${shq(repo)} "$@" >>${shq(log)} 2>&1`,
53
+ '',
54
+ ].join('\n');
55
+ }
56
+
57
+ function plist(dict) {
58
+ const v = (x) => Array.isArray(x) ? `<array>${x.map((s) => `<string>${xml(s)}</string>`).join('')}</array>`
59
+ : typeof x === 'boolean' ? (x ? '<true/>' : '<false/>') : `<string>${xml(x)}</string>`;
60
+ return [
61
+ '<?xml version="1.0" encoding="UTF-8"?>',
62
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
63
+ `<!-- ${MARK} (workspace/lib/launcher-cli.js). \`pmos launcher off\` removes it. -->`,
64
+ '<plist version="1.0">',
65
+ '<dict>',
66
+ ...Object.entries(dict).map(([k, x]) => ` <key>${xml(k)}</key>${v(x)}`),
67
+ '</dict>',
68
+ '</plist>',
69
+ '',
70
+ ].join('\n');
71
+ }
72
+
73
+ function desktop(entries) {
74
+ return [`# ${MARK} (workspace/lib/launcher-cli.js). \`pmos launcher off\` removes it.`, '[Desktop Entry]',
75
+ ...Object.entries(entries).map(([k, x]) => `${k}=${x}`), ''].join('\n');
76
+ }
77
+
78
+ /**
79
+ * The files `on` writes, for one platform. Pure.
80
+ * → { files: [{ role: 'app'|'login', path, content, mode }], app, login } or { refused: '<reason>' }
81
+ * `login: false` leaves the login item out.
82
+ */
83
+ function plan({ platform, home, env = {}, node, bin, repo, login = true }) {
84
+ if (platform === 'win32') return { refused: 'native Windows needs WSL for a real PTY. Run `pmos launcher on` inside WSL.' };
85
+ if (platform !== 'darwin' && platform !== 'linux') return { refused: `no launcher is made for ${platform}. Start the workspace with \`pmos\`.` };
86
+ const log = path.join(stateRoot.defaultRoot({ env, platform, home }), 'launcher.log');
87
+ const body = script({ node, bin, repo, pathEnv: env.PATH || '/usr/bin:/bin', log });
88
+ const files = [];
89
+ if (platform === 'darwin') {
90
+ const appDir = path.join(home, 'Applications', 'PMOS.app');
91
+ const exe = path.join(appDir, 'Contents', 'MacOS', 'pmos-launch');
92
+ files.push({ role: 'app', path: exe, content: body, mode: 0o755 });
93
+ files.push({ role: 'app', path: path.join(appDir, 'Contents', 'Info.plist'), mode: 0o644, content: plist({
94
+ CFBundleName: 'PMOS', CFBundleDisplayName: 'PMOS', CFBundleIdentifier: 'com.pmos.launcher',
95
+ CFBundleExecutable: 'pmos-launch', CFBundlePackageType: 'APPL', CFBundleVersion: '1',
96
+ // No Dock icon: the script hands off to the browser and exits within seconds.
97
+ LSUIElement: true,
98
+ }) });
99
+ const agent = path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`);
100
+ if (login) files.push({ role: 'login', path: agent, mode: 0o644, content: plist({
101
+ Label: LABEL, ProgramArguments: ['/bin/sh', exe, '--no-open'], RunAtLoad: true,
102
+ // `pmos up` detaches the server and exits; launchd must not reap the group it leaves behind.
103
+ AbandonProcessGroup: true,
104
+ }) });
105
+ return { files, app: appDir, login: login ? agent : null, removable: [appDir, agent] };
106
+ }
107
+ const data = env.XDG_DATA_HOME && path.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path.join(home, '.local', 'share');
108
+ const conf = env.XDG_CONFIG_HOME && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(home, '.config');
109
+ const exe = path.join(data, 'pmos', 'pmos-launch.sh');
110
+ const app = path.join(data, 'applications', 'pmos.desktop');
111
+ const auto = path.join(conf, 'autostart', 'pmos-workspace.desktop');
112
+ files.push({ role: 'app', path: exe, content: body, mode: 0o755 });
113
+ files.push({ role: 'app', path: app, mode: 0o644, content: desktop({
114
+ Type: 'Application', Name: 'PMOS', Comment: 'Open the PMOS workspace', Exec: `/bin/sh ${deq(exe)}`,
115
+ Terminal: 'false', Categories: 'Development;' }) });
116
+ if (login) files.push({ role: 'login', path: auto, mode: 0o644, content: desktop({
117
+ Type: 'Application', Name: 'PMOS workspace', Comment: 'Start the PMOS workspace server at login',
118
+ Exec: `/bin/sh ${deq(exe)} --no-open`, Terminal: 'false', NoDisplay: 'true', 'X-GNOME-Autostart-enabled': 'true' }) });
119
+ return { files, app, login: login ? auto : null, removable: [exe, app, auto] };
120
+ }
121
+
122
+ const marked = (f) => { try { return fs.readFileSync(f, 'utf8').includes(MARK); } catch { return false; } };
123
+ // The file under a removable path that carries the mark: the bundle's script for a .app, else the path itself.
124
+ const markFile = (p) => p.endsWith('.app') ? path.join(p, 'Contents', 'MacOS', 'pmos-launch') : p;
125
+
126
+ function repoRootOf(dir) {
127
+ const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: dir, encoding: 'utf8' });
128
+ return r.status === 0 ? String(r.stdout).trim() : dir;
129
+ }
130
+
131
+ // What the installed script points at: the repository and whether node and pmos.js still exist.
132
+ function readTarget(exe) {
133
+ let src = '';
134
+ try { src = fs.readFileSync(exe, 'utf8'); } catch { return null; }
135
+ const m = src.match(/^exec '((?:[^']|'\\'')*)' '((?:[^']|'\\'')*)' up '((?:[^']|'\\'')*)'/m);
136
+ if (!m) return null;
137
+ const un = (s) => s.replace(/'\\''/g, "'");
138
+ return { node: un(m[1]), bin: un(m[2]), repo: un(m[3]) };
139
+ }
140
+
141
+ function run(argv, { say = console.log, platform = process.platform, home = os.homedir(), env = process.env,
142
+ node = process.execPath, bin = path.join(__dirname, '..', 'bin', 'pmos.js'), cwd = process.cwd() } = {}) {
143
+ const pos = argv.slice(1).filter((a) => !a.startsWith('--'));
144
+ const flags = new Set(argv.slice(1).filter((a) => a.startsWith('--')).map((a) => a.slice(2)));
145
+ const unknown = [...flags].filter((f) => f !== 'no-login' && f !== 'help');
146
+ if (unknown.length) { say(`✗ unknown option --${unknown[0]}`); for (const l of USAGE) say(l); return 2; }
147
+ const sub = pos[0];
148
+ if (!sub || flags.has('help') || sub === 'help') { for (const l of USAGE) say(l); return sub ? 0 : 2; }
149
+ if (!COMMANDS.launcher.includes(sub)) { say(`✗ pmos launcher ${sub} is not a command`); for (const l of USAGE) say(l); return 2; }
150
+
151
+ if (sub === 'on') {
152
+ const dir = pos[1] ? path.resolve(cwd, pos[1]) : cwd;
153
+ if (!fs.existsSync(dir)) { say(`✗ ${dir} does not exist`); return 1; }
154
+ const repo = repoRootOf(dir);
155
+ const p = plan({ platform, home, env, node, bin, repo, login: !flags.has('no-login') });
156
+ if (p.refused) { say(`✗ ${p.refused}`); return 1; }
157
+ // Never write over a file of the same name that the launcher did not write.
158
+ const foreign = p.files.filter((f) => fs.existsSync(f.path) && !marked(f.path));
159
+ if (foreign.length) { say(`✗ not written: ${foreign.map((f) => f.path).join(', ')} exists and was not written by pmos launcher`); return 1; }
160
+ for (const f of p.files) {
161
+ fs.mkdirSync(path.dirname(f.path), { recursive: true });
162
+ fs.writeFileSync(f.path, f.content, { mode: f.mode });
163
+ fs.chmodSync(f.path, f.mode);
164
+ }
165
+ say(`✓ PMOS app for ${repo}`);
166
+ say(` ${p.app}`);
167
+ say(platform === 'darwin' ? ' Open it from Spotlight or Launchpad, or drag it to the Dock.' : ' Open it from the app menu.');
168
+ if (p.login) {
169
+ say('✓ login item: the workspace server starts when you log in, without a browser window');
170
+ say(` ${p.login}`);
171
+ } else say(' no login item (--no-login): the server starts when you open the app');
172
+ say(' The app opens the browser on the running workspace and starts the server when none runs.');
173
+ say(' If you move or upgrade node, run `pmos launcher on` again.');
174
+ return 0;
175
+ }
176
+
177
+ const p = plan({ platform, home, env, node, bin, repo: cwd });
178
+ if (p.refused) { say(`✗ ${p.refused}`); return 1; }
179
+
180
+ if (sub === 'off') {
181
+ let removed = 0;
182
+ for (const r of p.removable) {
183
+ if (!fs.existsSync(r)) continue;
184
+ if (!marked(markFile(r))) { say(`· left ${r}: it was not written by pmos launcher`); continue; }
185
+ fs.rmSync(r, { recursive: true, force: true });
186
+ say(`✓ removed ${r}`);
187
+ removed++;
188
+ }
189
+ if (!removed) say('○ nothing to remove: no launcher is installed');
190
+ return 0;
191
+ }
192
+
193
+ // status
194
+ const exe = p.files.find((f) => f.role === 'app' && f.mode === 0o755).path;
195
+ const t = marked(exe) ? readTarget(exe) : null;
196
+ const loginPath = p.removable.find((r) => r.endsWith('.plist') || r.endsWith('pmos-workspace.desktop'));
197
+ const login = fs.existsSync(loginPath) && marked(loginPath);
198
+ if (!t) { say('○ no PMOS app is installed (pmos launcher on)'); if (login) say(`· a login item is installed without the app: ${loginPath}`); return 1; }
199
+ say(`✓ PMOS app — ${p.app}`);
200
+ say(` repository: ${t.repo}${fs.existsSync(t.repo) ? '' : ' ✗ not found'}`);
201
+ say(` node: ${t.node}${fs.existsSync(t.node) ? '' : ' ✗ not found — run `pmos launcher on` again'}`);
202
+ say(` pmos: ${t.bin}${fs.existsSync(t.bin) ? '' : ' ✗ not found — run `pmos launcher on` again'}`);
203
+ say(login ? `✓ login item — ${loginPath}` : '○ no login item: the server starts when you open the app');
204
+ return [t.repo, t.node, t.bin].every((x) => fs.existsSync(x)) ? 0 : 1;
205
+ }
206
+
207
+ module.exports = { run, plan, readTarget, COMMANDS, USAGE, MARK };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+ /* measure-cli.js — `pmos measure …`: record a measured value of a KR or a health budget, with its source (pm-os-absorb
3
+ * Run 2, PRD R17, R23, R24).
4
+ *
5
+ * pmos measure add <anchor> <value> --source <path>@<sha> [--target <number>] [--unit <unit>] [--as-of YYYY-MM-DD]
6
+ * pmos measure list [<anchor>]
7
+ * every command: [--json] [--root <dir>]
8
+ *
9
+ * The SAME writer the workspace page uses (lib/measurements.js). A measurement is evidence, not a PM act, so an agent
10
+ * may write one: the line is attributed by lib/actor.js fromCli (the PM at a terminal, the bound agent session from
11
+ * PMOS_SESSION, `agent:unknown` otherwise). The source is required: the workspace never computes a KR value, so a
12
+ * group shows a value only from a line that says where it was read. `--as-of` is today (UTC) when it is not given.
13
+ * Exit codes: 0 done, 1 refused, 2 usage.
14
+ */
15
+ const path = require('path');
16
+ const { execFileSync } = require('child_process');
17
+ const actorLib = require('./actor.js');
18
+ const measurements = require('./measurements.js');
19
+ const { parseArgs } = require('./schedule-cli');
20
+
21
+ const COMMANDS = { measure: ['add', 'list'] };
22
+ const USAGE = [
23
+ 'usage: pmos measure add|list — measured values of KRs and health budgets',
24
+ ' pmos measure add <anchor> <value> --source <path>@<sha> [--target <number>] [--unit <unit>] [--as-of YYYY-MM-DD]',
25
+ ' pmos measure list [<anchor>]',
26
+ ' every command: [--json] [--root <dir>]',
27
+ 'The anchor is the parent_kr an initiative names: a KR such as "KR 2.1", or health:<id>. Quote an anchor with a space.',
28
+ 'The source is the file the value was read from and the commit it was read at, for example planning/okrs/q4.md@1a2b3c4.',
29
+ ];
30
+ const BOOL = new Set(['json', 'help']);
31
+ const VALUE = new Set(['source', 'target', 'unit', 'as-of', 'root']);
32
+
33
+ function parse(argv) {
34
+ const rest = []; const bools = {};
35
+ for (const a of argv) {
36
+ if (a.startsWith('--') && BOOL.has(a.slice(2))) bools[a.slice(2)] = true;
37
+ else if (a === '-h') bools.help = true;
38
+ else rest.push(a);
39
+ }
40
+ const { pos, flags } = parseArgs(rest);
41
+ return { pos, flags: { ...flags, ...bools } };
42
+ }
43
+ function repoRoot(cwd) {
44
+ try { return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; }
45
+ catch { return cwd; }
46
+ }
47
+ const num = (s) => (typeof s === 'string' && s.trim() !== '' && Number.isFinite(Number(s)) ? Number(s) : NaN);
48
+
49
+ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, now } = {}) {
50
+ const args = argv[0] === 'measure' ? argv.slice(1) : argv.slice();
51
+ const verb = args[0];
52
+ const { pos, flags } = parse(args.slice(1));
53
+ const json = !!flags.json;
54
+ if (!verb || verb === 'help' || verb === '--help' || verb === '-h' || flags.help) {
55
+ for (const l of USAGE) say(l);
56
+ return verb ? 0 : 1;
57
+ }
58
+ if (!COMMANDS.measure.includes(verb)) { say(`unknown measure command "${verb}" — one of ${COMMANDS.measure.join(', ')}`); return 2; }
59
+ if ('by' in flags || 'via' in flags) { say(`attribution is set from the call path; --${'by' in flags ? 'by' : 'via'} is not accepted`); return 2; }
60
+ for (const k of Object.keys(flags)) {
61
+ if (!BOOL.has(k) && !VALUE.has(k)) { say(`unknown option --${k}`); return 2; }
62
+ if (VALUE.has(k) && flags[k] === '') { say(`--${k} needs a value`); return 2; }
63
+ }
64
+ const root = flags.root ? path.resolve(cwd, flags.root) : repoRoot(cwd);
65
+ const fail = (error, code = 1) => { if (json) say(JSON.stringify({ ok: false, error })); else say(`✗ ${error}`); return code; };
66
+
67
+ if (verb === 'list') {
68
+ const m = measurements.readMeasurements(root);
69
+ const anchors = pos[0] ? [pos[0]] : Object.keys(m.byAnchor).sort();
70
+ if (json) { say(JSON.stringify({ ok: true, byAnchor: Object.fromEntries(anchors.map((a) => [a, (m.byAnchor[a] || []).map((x) => ({ ...x, by: actorLib.pageName(x.by) }))])), flags: m.flags })); return 0; }
71
+ if (!anchors.length) say('No measurements yet.');
72
+ for (const a of anchors) {
73
+ for (const x of m.byAnchor[a] || []) say(`${a} · ${x.value}${x.unit || ''}${x.target === null ? '' : ` → ${x.target}${x.unit || ''}`} on ${x.as_of} · ${x.source} · ${actorLib.pageName(x.by)}`);
74
+ }
75
+ for (const f of m.flags) say(`! ${f.why}`);
76
+ return 0;
77
+ }
78
+
79
+ const [anchor, valueText] = pos;
80
+ if (!anchor || valueText === undefined) { say(USAGE[1].trim()); return 2; }
81
+ const value = num(valueText);
82
+ if (!Number.isFinite(value)) return fail('the value must be a number', 2);
83
+ const ev = { ev: 'measured', anchor, value };
84
+ if (flags.target !== undefined) {
85
+ const t = num(flags.target);
86
+ if (!Number.isFinite(t)) return fail('--target must be a number', 2);
87
+ ev.target = t;
88
+ }
89
+ if (flags.unit !== undefined) ev.unit = flags.unit;
90
+ ev.as_of = flags['as-of'] || new Date(now == null ? Date.now() : now).toISOString().slice(0, 10);
91
+ if (flags.source !== undefined) ev.source = flags.source;
92
+ const actor = actorLib.fromCli({ isTTY: isTTY === undefined ? !!(process.stdin.isTTY && process.stdout.isTTY) : isTTY, env: env === undefined ? process.env : env });
93
+ const r = measurements.appendFromCli(root, [ev], { actor, now });
94
+ if (!r.ok) return fail(r.error);
95
+ if (json) say(JSON.stringify({ ok: true, written: r.written, eid: r.lines[0].eid }));
96
+ else say(`✓ ${anchor}: ${value}${ev.unit || ''} on ${ev.as_of} recorded (source ${ev.source})`);
97
+ return 0;
98
+ }
99
+
100
+ module.exports = { run, COMMANDS, USAGE };