create-pmos 0.187.0 → 0.189.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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-report-setup-test.js +1 -1
  7. package/workspace/c3-scan-fixture.js +247 -5
  8. package/workspace/c3-scan-test.js +453 -106
  9. package/workspace/guide/horizon-scan.md +45 -36
  10. package/workspace/guide/my-work.md +1 -0
  11. package/workspace/lib/day-run.js +4 -3
  12. package/workspace/lib/headless.js +19 -5
  13. package/workspace/lib/my-work.js +2 -1
  14. package/workspace/lib/pmos-verbs.js +2 -2
  15. package/workspace/lib/route-modules.js +1 -1
  16. package/workspace/lib/routes-boards.js +29 -12
  17. package/workspace/lib/routes-scan.js +224 -44
  18. package/workspace/lib/scan-cli.js +143 -45
  19. package/workspace/lib/scan-run.js +242 -87
  20. package/workspace/lib/scan.js +623 -79
  21. package/workspace/lib/work-assigned.js +1 -1
  22. package/workspace/package-lock.json +2 -2
  23. package/workspace/package.json +1 -1
  24. package/workspace/public/pm/c3-doc-sheet.js +7 -1
  25. package/workspace/public/pm/c3-initiatives.js +5 -4
  26. package/workspace/public/pm/c3-scan-api.js +167 -0
  27. package/workspace/public/pm/c3-scan-next.js +248 -0
  28. package/workspace/public/pm/c3-scan-setup.js +317 -0
  29. package/workspace/public/pm/c3-scan-words.js +34 -0
  30. package/workspace/public/pm/c3-scan.css +58 -9
  31. package/workspace/public/pm/c3-scan.js +36 -208
  32. package/workspace/public/pm/c3-toast.js +1 -0
  33. package/workspace/public/pm/console3.css +3 -0
  34. package/workspace/public/pm/pm-work-view.js +11 -5
  35. package/workspace/public/pm/run-card.js +12 -3
  36. package/workspace/public/pm/sessions-panel.js +2 -2
  37. package/workspace/server.js +25 -5
  38. package/workspace/skills/horizon-scan-repo.skill +26 -10
  39. package/workspace/skills/horizon-scan-research.skill +44 -0
  40. package/workspace/skills/horizon-scan-web.skill +12 -5
@@ -1,21 +1,29 @@
1
1
  'use strict';
2
- /* scan-cli.js — `pmos scan …`: the Horizon scan from a terminal (initiative horizon-scan, PRD sections 3 and 5).
2
+ /* scan-cli.js — `pmos scan …`: the Horizon scan from a terminal (initiatives horizon-scan and horizon-scan-portfolio).
3
3
  *
4
4
  * pmos scan file --scan <id> --title "<finding>" --why "<one line>" --source label=ref … [--match <initiative>] [--origin web|repo]
5
5
  * an agent files one finding. It answers `filed` or `duplicate` and nothing else about earlier findings.
6
6
  * pmos scan file --scan <id> --pages <n> the web run says how many public pages it read
7
- * pmos scan list --scan <id> the findings of one scan (the repository run reads the web run's)
7
+ * pmos scan section --report <id> --heading "<h>" --source label=ref … [--body "<text>"] (text on stdin when --body is absent)
8
+ * a run writes one section of a report document. A web section cites only public pages the run read;
9
+ * a repository section cites paths, Jira keys or notes. lib/scan.js assembles the file when the scan ends.
10
+ * pmos scan propose --report <id> --kind create|research|attach --title "<what>" --why "<one line>" --finding "<which finding>" --source label=ref …
11
+ * create: --board <id> --lane <lane> --anchor <KR|health:id> --intent "<one paragraph>"
12
+ * research: --question "<public question>" [--depth quick|standard|deep] attach: --initiative <id>
13
+ * the repository run proposes a next step. The PM approves, changes or dismisses it. It answers `filed` or `duplicate`.
14
+ * pmos scan list --scan <id> [--sections] the findings of one scan (the repository run reads the web run's), and its report sections
8
15
  * pmos scan match <finding> <initiative> --scan <id> the repository run marks a finding that matches an initiative
9
- * pmos scan suggest --board <id> --kind product|domain|approach --name "<what to watch>" --why "<one line>" --from <initiative>
10
- * the suggest run files one watch list suggestion (at most 8 for a board)
11
- * pmos scan status [--board <id>] cadence, watch list and the last scan (read)
12
- * pmos scan cadence <daily|weekly|off> --board <id> PM only
13
- * pmos scan watch keep|remove <item> --board <id> PM only
14
- * pmos scan watch edit <item> --board <id> [--name …] [--kind …] [--why …] PM only
15
- * pmos scan watch add --board <id> --kind … --name … [--why …] PM only
16
+ * pmos scan suggest [--boards <id,id>] --kind product|domain|approach --name "<what to watch>" --why "<one line>" --from <initiative>
17
+ * the suggest run files one watch list suggestion (at most 8 a run)
18
+ * pmos scan status the setting, the watch list, the last scan and the proposals waiting (read)
19
+ * pmos scan setting [--cadence daily|weekly|off] [--repo on|off] [--types benchmark,competitive,insight,productivity] PM only
20
+ * pmos scan cadence <daily|weekly|off> PM only (the first scan's verb; it sets the workspace cadence)
21
+ * pmos scan watch keep|remove <item> PM only
22
+ * pmos scan watch edit <item> [--name …] [--kind …] [--why …] [--boards a,b] PM only
23
+ * pmos scan watch add --kind … --name … [--why …] [--boards a,b] PM only
16
24
  * every command: [--json] [--root <dir>]
17
25
  *
18
- * Who wrote a line is the call path (lib/actor.js), never an argument. The scan runs set two variables the model cannot change: PMOS_SCAN_FILE
26
+ * Who wrote a line is the call path (lib/actor.js), never an argument. The scan runs set variables the model cannot change: PMOS_SCAN_FILE
19
27
  * (the ledger, so the web run, which has no repository, still finds it) and PMOS_SCAN_ORIGIN (web or repo, so a run cannot file as the other).
20
28
  * PMOS_SCAN_RUN names the run, so a finding carries the run that filed it.
21
29
  * `file` answers only `duplicate` for a repeat: it never prints the earlier finding. Nothing here sends anything outward.
@@ -28,25 +36,32 @@ const actorLib = require('./actor.js');
28
36
  const D = require('./driver.js');
29
37
  const S = require('./scan.js');
30
38
 
31
- const COMMANDS = { scan: ['file', 'list', 'match', 'suggest', 'status', 'cadence', 'watch'] };
39
+ const COMMANDS = { scan: ['file', 'section', 'propose', 'list', 'match', 'suggest', 'status', 'setting', 'cadence', 'watch'] };
32
40
  const USAGE = [
33
- 'usage: pmos scan file|list|match|suggest|status|cadence|watch — the Horizon scan',
41
+ 'usage: pmos scan file|section|propose|list|match|suggest|status|setting|cadence|watch — the Horizon scan',
34
42
  ' pmos scan file --scan <id> --title "<finding>" --why "<one line>" --source label=ref … [--match <initiative>]',
35
43
  ' a web finding needs one source that is a public page you read; a repository finding needs a pointer (path, Jira key, note)',
36
44
  ' pmos scan file --scan <id> --pages <n> how many public pages the web run read',
37
- ' pmos scan list --scan <id> the findings of a scan',
45
+ ' pmos scan section --report <id> --heading "<h>" --source label=ref … [--body "<text>"] the text is read from stdin without --body',
46
+ ' a web section cites public pages you read; a repository section cites paths, Jira keys or notes',
47
+ ' pmos scan propose --report <id> --kind create|research|attach --title "<what>" --why "<one line>" --finding "<which finding>" --source label=ref …',
48
+ ' create: --board <id> --lane <lane> --anchor <KR|health:id> --intent "<one paragraph>"',
49
+ ' research: --question "<public question>" [--depth quick|standard|deep] attach: --initiative <id>',
50
+ ' pmos scan list --scan <id> [--sections] the findings of a scan, and its report sections',
38
51
  ' pmos scan match <finding> <initiative> --scan <id> a finding matches an existing initiative',
39
- ' pmos scan suggest --board <id> --kind product|domain|approach --name "<what to watch>" --why "<one line>" --from <initiative>',
40
- ' pmos scan status [--board <id>]',
41
- ' pmos scan cadence <daily|weekly|off> --board <id> PM only',
42
- ' pmos scan watch keep|remove <item> --board <id> PM only',
43
- ' pmos scan watch edit <item> --board <id> [--name …] [--kind …] [--why …] PM only',
44
- ' pmos scan watch add --board <id> --kind … --name … [--why …] PM only',
52
+ ' pmos scan suggest [--boards a,b] --kind product|domain|approach --name "<what to watch>" --why "<one line>" --from <initiative>',
53
+ ' pmos scan status',
54
+ ' pmos scan setting [--cadence daily|weekly|off] [--repo on|off] [--types a,b] PM only',
55
+ ' pmos scan cadence <daily|weekly|off> PM only',
56
+ ' pmos scan watch keep|remove <item> PM only',
57
+ ' pmos scan watch edit <item> [--name …] [--kind …] [--why …] [--boards a,b] PM only',
58
+ ' pmos scan watch add --kind … --name … [--why …] [--boards a,b] PM only',
45
59
  ' every command: [--json] [--root <dir>]',
46
- 'A scan reads and files. It sends nothing and creates no initiative: each finding is an ask the PM accepts or declines.',
60
+ 'A scan reads and files. It sends nothing and creates no initiative: each proposal is a step the PM approves, changes or dismisses.',
47
61
  ];
48
- const BOOL = new Set(['json', 'help']);
49
- const VALUE = new Set(['scan', 'title', 'why', 'source', 'match', 'origin', 'pages', 'board', 'kind', 'name', 'from', 'root']);
62
+ const BOOL = new Set(['json', 'help', 'sections']);
63
+ const VALUE = new Set(['scan', 'title', 'why', 'source', 'match', 'origin', 'pages', 'board', 'boards', 'kind', 'name', 'from', 'root', 'report', 'heading', 'body', 'finding', 'lane', 'anchor',
64
+ 'intent', 'question', 'depth', 'initiative', 'cadence', 'repo', 'types']);
50
65
 
51
66
  function parse(argv) {
52
67
  const pos = []; const flags = {}; const sources = [];
@@ -78,7 +93,22 @@ function sourcesFrom(list) {
78
93
  return { sources: out };
79
94
  }
80
95
 
81
- async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, now } = {}) {
96
+ /** The text a section is made of: the `stdin` option (tests), else standard input. Nothing on standard input for 8 seconds is an empty text, not a hang. */
97
+ function readStdin(given) {
98
+ if (typeof given === 'string') return Promise.resolve(given);
99
+ if (process.stdin.isTTY) return Promise.resolve('');
100
+ return new Promise((resolve) => {
101
+ let data = ''; let got = false;
102
+ const quiet = setTimeout(() => { if (!got) { process.stdin.pause(); resolve(''); } }, 8000);
103
+ process.stdin.setEncoding('utf8');
104
+ process.stdin.on('data', (c) => { got = true; data += c; });
105
+ process.stdin.on('end', () => { clearTimeout(quiet); resolve(data); });
106
+ process.stdin.on('error', () => { clearTimeout(quiet); resolve(data); });
107
+ });
108
+ }
109
+ const listOf = (v) => String(v || '').split(',').map((x) => x.trim()).filter(Boolean);
110
+
111
+ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, now, stdin } = {}) {
82
112
  const args = argv[0] === 'scan' ? argv.slice(1) : argv.slice();
83
113
  const verb = args[0];
84
114
  const { pos, flags, sources } = parse(args.slice(1));
@@ -129,7 +159,7 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
129
159
  const count = sc.findings.filter((id) => state.findings.get(id).origin === origin).length;
130
160
  const limit = origin === 'web' ? S.LIMITS.web : S.LIMITS.repo;
131
161
  // A duplicate is answered before the limit: a repeat costs nothing, and it is counted as skipped.
132
- const dup = S.duplicateOf(state, sc.board, c.finding);
162
+ const dup = S.duplicateOf(state, null, c.finding); // the whole portfolio: the same page or title on any board
133
163
  if (dup) {
134
164
  S.append(file, { ev: 'skip', board: sc.board, scan: sc.scan, origin, of: dup.id }, { now });
135
165
  if (flags.pages !== undefined && Number.isInteger(Number(flags.pages))) S.append(file, { ev: 'pages', board: sc.board, scan: sc.scan, pages: Number(flags.pages) }, { now });
@@ -143,15 +173,69 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
143
173
  return done({ result: 'filed', id }, `filed ${id}`);
144
174
  }
145
175
 
176
+ if (verb === 'section') {
177
+ if (!file) return noState();
178
+ if (!flags.report) { say('usage: pmos scan section --report <id> --heading "<h>" --source label=ref … [--body "<text>"]'); return 2; }
179
+ const state = S.fold(S.readLines(file));
180
+ const rep = state.reports.get(flags.report);
181
+ const sc = rep ? state.scans.get(rep.scan) : null;
182
+ if (!rep) return fail('no such report in this ledger');
183
+ if (!sc || sc.end) return fail('the scan of that report is over; file no more');
184
+ const origin = (E && (E.PMOS_SCAN_ORIGIN === 'web' || E.PMOS_SCAN_ORIGIN === 'repo')) ? E.PMOS_SCAN_ORIGIN : flags.origin;
185
+ if (!S.ORIGINS.includes(origin)) { say('say where the section comes from: --origin web or --origin repo'); return 2; }
186
+ if (rep.type === 'productivity' && origin === 'web') return fail('the productivity report comes from the repository run only');
187
+ const s = sourcesFrom(sources);
188
+ if (s.error) return fail(s.error, 2);
189
+ const body = flags.body !== undefined ? flags.body : await readStdin(stdin);
190
+ const c = S.checkSection({ heading: flags.heading, body, sources: s.sources }, origin, { state, scanId: sc.scan });
191
+ if (!c.ok) return fail(c.error);
192
+ if (rep.sections.some((x) => x.origin === origin && x.heading.toLowerCase() === c.section.heading.toLowerCase())) return fail('that report has a section with that heading already');
193
+ if (rep.sections.filter((x) => x.origin === origin).length >= S.LIMITS.sections) return fail(`this report holds ${S.LIMITS.sections} sections from the ${origin === 'web' ? 'web' : 'repository'} run already; write no more`);
194
+ const run = E && S.RUN_RE.test(String(E.PMOS_SCAN_RUN || '')) ? E.PMOS_SCAN_RUN : null;
195
+ S.append(file, { ev: 'section', board: sc.board, scan: sc.scan, report: rep.id, ...c.section, by: actor.by, ...(run ? { run } : {}) }, { now });
196
+ return done({ result: 'filed', report: rep.id }, `filed section "${c.section.heading}"`);
197
+ }
198
+
199
+ if (verb === 'propose') {
200
+ if (!file) return noState();
201
+ if (!flags.report) { say('usage: pmos scan propose --report <id> --kind create|research|attach --title "<what>" --why "<one line>" --finding "<which finding>" --source label=ref …'); return 2; }
202
+ const origin = (E && (E.PMOS_SCAN_ORIGIN === 'web' || E.PMOS_SCAN_ORIGIN === 'repo')) ? E.PMOS_SCAN_ORIGIN : flags.origin;
203
+ if (origin !== 'repo') return fail('only the repository run proposes next steps; the web run files findings and sections');
204
+ const state = S.fold(S.readLines(file));
205
+ const rep = state.reports.get(flags.report);
206
+ const sc = rep ? state.scans.get(rep.scan) : null;
207
+ if (!rep) return fail('no such report in this ledger');
208
+ if (!sc || sc.end) return fail('the scan of that report is over; file no more');
209
+ const s = sourcesFrom(sources);
210
+ if (s.error) return fail(s.error, 2);
211
+ const fields = flags.kind === 'create' ? { board: flags.board, lane: flags.lane, anchor: flags.anchor, intent: flags.intent }
212
+ : flags.kind === 'research' ? { question: flags.question, ...(flags.depth !== undefined ? { depth: flags.depth } : {}) } : { initiative: flags.initiative };
213
+ const c = S.checkProposal({ kind: flags.kind, title: flags.title, why: flags.why, finding: flags.finding, evidence: s.sources, fields });
214
+ if (!c.ok) return fail(c.error);
215
+ // A repeat is answered before the limit, and says nothing about the earlier proposal.
216
+ if (S.duplicateProposal(state, c.proposal.title)) return done({ result: 'duplicate' }, 'duplicate');
217
+ if (S.proposalsOf(state, rep.id).length >= S.LIMITS.proposals) return fail(`this report holds ${S.LIMITS.proposals} proposals already; file no more`);
218
+ const id = S.newId('p', 5);
219
+ const run = E && S.RUN_RE.test(String(E.PMOS_SCAN_RUN || '')) ? E.PMOS_SCAN_RUN : null;
220
+ S.append(file, { ev: 'proposal', board: sc.board, scan: sc.scan, report: rep.id, id, ...c.proposal, by: actor.by, ...(run ? { run } : {}) }, { now });
221
+ return done({ result: 'filed', id }, `filed ${id}`);
222
+ }
223
+
146
224
  if (verb === 'list') {
147
225
  if (!file) return noState();
148
226
  const state = S.fold(S.readLines(file));
149
227
  const sc = flags.scan ? state.scans.get(flags.scan) : null;
150
228
  if (!sc) return fail('no scan under that id');
151
229
  const rows = sc.findings.map((id) => state.findings.get(id)).map((f) => ({ id: f.id, origin: f.origin, title: f.title, why: f.why, sources: f.sources, match: f.match }));
152
- if (json) { say(JSON.stringify({ ok: true, scan: sc.scan, board: sc.board, findings: rows })); return 0; }
230
+ const reps = flags.sections ? sc.reports.map((id) => state.reports.get(id)).map((r) => ({ id: r.id, type: r.type, title: r.title, origin: r.origin,
231
+ sections: r.sections.map((x) => ({ heading: x.heading, origin: x.origin, body: x.body, sources: x.sources })) })) : null;
232
+ if (json) { say(JSON.stringify({ ok: true, scan: sc.scan, board: sc.board, findings: rows, ...(reps ? { reports: reps } : {}) })); return 0; }
153
233
  for (const f of rows) say(`${f.id} · ${f.origin} · ${f.title} — ${f.why}${f.match ? ` · matches ${f.match}` : ''}${f.sources.length ? ` · ${f.sources.map((x) => x.ref).join(', ')}` : ''}`);
154
234
  if (!rows.length) say('no findings yet');
235
+ for (const r of reps || []) {
236
+ say(`report ${r.id} · ${r.type} · ${r.title}`);
237
+ for (const x of r.sections) say(` [${x.origin}] ${x.heading}: ${x.body.replace(/\s+/g, ' ').slice(0, 300)}${x.sources.length ? ` · ${x.sources.map((y) => y.ref).join(', ')}` : ''}`);
238
+ }
155
239
  return 0;
156
240
  }
157
241
 
@@ -172,53 +256,67 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
172
256
 
173
257
  if (verb === 'suggest') {
174
258
  if (!file) return noState();
175
- if (!flags.board) { say('usage: pmos scan suggest --board <id> --kind product|domain|approach --name "<what to watch>" --why "<one line>" --from <initiative>'); return 2; }
176
259
  if (!flags.from || !S.ID_RE.test(flags.from)) return fail('--from names the initiative this suggestion came from');
177
260
  const c = S.checkWatchItem({ kind: flags.kind, name: flags.name, why: flags.why });
178
261
  if (!c.ok) return fail(c.error);
179
262
  if (!c.item.why) return fail('--why is one line that says why this is worth watching');
263
+ const on = listOf(flags.boards || flags.board);
264
+ if (on.some((b) => !S.ID_RE.test(b)) || on.length > 20) return fail('--boards is a list of board ids');
180
265
  const state = S.fold(S.readLines(file));
181
- const open = S.scansOf(state, flags.board, 'suggest').find((x) => !x.end);
182
- if (!open) return fail('no suggest run is open for that board');
183
- const b = state.boards.get(flags.board);
184
- const mine = b ? [...b.items.values()].length : 0;
185
- if (mine >= S.LIMITS.suggestions) return fail(`a board gets at most ${S.LIMITS.suggestions} suggestions`);
186
- if (b && [...b.items.values()].some((i) => i.name.toLowerCase() === c.item.name.toLowerCase())) return done({ filed: false, result: 'duplicate' }, 'duplicate');
266
+ const open = S.scansOf(state, null, 'suggest').find((x) => !x.end);
267
+ if (!open) return fail('no suggest run is open');
268
+ const mine = [...state.itemById.values()].filter((i) => i.scan === open.scan).length;
269
+ if (mine >= S.LIMITS.suggestions) return fail(`a suggest run files at most ${S.LIMITS.suggestions} suggestions`);
270
+ if ([...state.itemById.values()].some((i) => i.name.toLowerCase() === c.item.name.toLowerCase())) return done({ filed: false, result: 'duplicate' }, 'duplicate');
187
271
  const id = S.newId('w');
188
- S.append(file, { ev: 'watch-suggest', board: flags.board, id, ...c.item, ini: flags.from, by: actor.by }, { now });
272
+ S.append(file, { ev: 'watch-suggest', scan: open.scan, boards: on, id, ...c.item, ini: flags.from, by: actor.by }, { now });
189
273
  return done({ filed: true, id }, `filed ${id}`);
190
274
  }
191
275
 
192
276
  if (verb === 'status') {
193
277
  if (!file) return noState();
194
278
  const state = S.fold(S.readLines(file));
195
- const ids = flags.board ? [flags.board] : [...state.boards.keys()];
196
- const out = ids.map((id) => { const w = S.watchOf(state, id); const last = S.lastScan(state, id); return { board: id, cadence: S.cadenceOf(state, id), suggested: w.suggested.length, kept: w.kept.length,
197
- last: last ? { scan: last.scan, at: last.end, found: last.found, skipped: last.skipped, pages: last.pages, ok: last.ok } : null }; });
198
- if (json) { say(JSON.stringify({ ok: true, boards: out })); return 0; }
199
- for (const o of out) say(`${o.board} · ${o.cadence} · ${o.kept} kept, ${o.suggested} suggested · ${o.last ? `last scan ${o.last.at}: ${o.last.found} found, ${o.last.skipped} skipped` : 'no scan yet'}`);
200
- if (!out.length) say('no scan records yet');
279
+ const w = S.watchOf(state, null);
280
+ const last = S.lastScan(state, null);
281
+ const out = { setting: S.settingOf(state), kept: w.kept.length, suggested: w.suggested.length, proposals_waiting: S.waitingProposals(state).length,
282
+ last: last ? { scan: last.scan, at: last.end, found: last.found, skipped: last.skipped, pages: last.pages, ok: last.ok } : null };
283
+ if (json) { say(JSON.stringify({ ok: true, ...out })); return 0; }
284
+ say(`${out.setting.cadence} · repository ${out.setting.repo ? 'on' : 'off'} · reports ${out.setting.types.join(', ') || 'none'} · ${out.kept} kept, ${out.suggested} suggested · ${out.proposals_waiting} proposals waiting · ${out.last ? `last scan ${out.last.at}: ${out.last.found} found, ${out.last.skipped} skipped` : 'no scan yet'}`);
201
285
  return 0;
202
286
  }
203
287
 
204
288
  // ── the PM's verbs ────────────────────────────────────────────────────────────────────────────────
289
+ if (verb === 'setting') {
290
+ const g = pmOnly('change the scan setting');
291
+ if (!g.ok) return fail(g.error);
292
+ if (!file) return noState();
293
+ const input = {};
294
+ if (flags.cadence !== undefined) input.cadence = flags.cadence;
295
+ if (flags.repo !== undefined) { if (!/^(on|off)$/.test(flags.repo)) return fail('--repo is on or off', 2); input.repo = flags.repo === 'on'; }
296
+ if (flags.types !== undefined) input.types = listOf(flags.types);
297
+ const r = S.setSetting(file, input, actor, { now });
298
+ if (!r.ok) return fail(r.error);
299
+ return done({ setting: r.setting }, `✓ ${r.setting.cadence} · repository ${r.setting.repo ? 'on' : 'off'} · reports ${r.setting.types.join(', ') || 'none'}`);
300
+ }
205
301
  if (verb === 'cadence') {
206
302
  const g = pmOnly('change the scan cadence');
207
303
  if (!g.ok) return fail(g.error);
208
- if (!pos[0] || !flags.board) { say('usage: pmos scan cadence <daily|weekly|off> --board <id>'); return 2; }
304
+ if (!pos[0]) { say('usage: pmos scan cadence <daily|weekly|off>'); return 2; }
209
305
  if (!file) return noState();
210
- const r = S.setCadence(file, flags.board, pos[0], actor, { now });
306
+ const r = S.setCadence(file, flags.board || null, pos[0], actor, { now });
211
307
  if (!r.ok) return fail(r.error);
212
- return done({ board: r.board, cadence: r.cadence }, `✓ board ${r.board}: the scan runs ${r.cadence === 'off' ? 'never' : r.cadence}`);
308
+ return done({ cadence: r.cadence }, `✓ the scan runs ${r.cadence === 'off' ? 'never' : r.cadence}`);
213
309
  }
214
310
  // watch
215
311
  const op = pos[0];
216
- if (!['keep', 'edit', 'remove', 'add'].includes(op) || !flags.board) { say('usage: pmos scan watch keep|edit|remove|add [<item>] --board <id>'); return 2; }
312
+ if (!['keep', 'edit', 'remove', 'add'].includes(op)) { say('usage: pmos scan watch keep|edit|remove|add [<item>]'); return 2; }
217
313
  const g = pmOnly('change the watch list');
218
314
  if (!g.ok) return fail(g.error);
219
315
  if (!file) return noState();
220
- const input = op === 'add' ? { kind: flags.kind, name: flags.name, why: flags.why } : { id: pos[1], ...(flags.name !== undefined ? { name: flags.name } : {}), ...(flags.kind !== undefined ? { kind: flags.kind } : {}), ...(flags.why !== undefined ? { why: flags.why } : {}) };
221
- const r = S.watchOp(file, op, flags.board, input, actor, { now });
316
+ const on = flags.boards !== undefined ? listOf(flags.boards) : undefined;
317
+ const input = op === 'add' ? { kind: flags.kind, name: flags.name, why: flags.why, ...(on ? { boards: on } : {}) }
318
+ : { id: pos[1], ...(flags.name !== undefined ? { name: flags.name } : {}), ...(flags.kind !== undefined ? { kind: flags.kind } : {}), ...(flags.why !== undefined ? { why: flags.why } : {}), ...(on ? { boards: on } : {}) };
319
+ const r = S.watchOp(file, op, flags.board || null, input, actor, { now });
222
320
  if (!r.ok) return fail(r.error);
223
321
  return done({ id: r.id }, `✓ ${op} ${r.id}`);
224
322
  }