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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-report-setup-test.js +1 -1
- package/workspace/c3-scan-fixture.js +247 -5
- package/workspace/c3-scan-test.js +453 -106
- package/workspace/guide/horizon-scan.md +45 -36
- package/workspace/guide/my-work.md +1 -0
- package/workspace/lib/day-run.js +4 -3
- package/workspace/lib/headless.js +19 -5
- package/workspace/lib/my-work.js +2 -1
- package/workspace/lib/pmos-verbs.js +2 -2
- package/workspace/lib/route-modules.js +1 -1
- package/workspace/lib/routes-boards.js +29 -12
- package/workspace/lib/routes-scan.js +224 -44
- package/workspace/lib/scan-cli.js +143 -45
- package/workspace/lib/scan-run.js +242 -87
- package/workspace/lib/scan.js +623 -79
- package/workspace/lib/work-assigned.js +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-doc-sheet.js +7 -1
- package/workspace/public/pm/c3-initiatives.js +5 -4
- package/workspace/public/pm/c3-scan-api.js +167 -0
- package/workspace/public/pm/c3-scan-next.js +248 -0
- package/workspace/public/pm/c3-scan-setup.js +317 -0
- package/workspace/public/pm/c3-scan-words.js +34 -0
- package/workspace/public/pm/c3-scan.css +58 -9
- package/workspace/public/pm/c3-scan.js +36 -208
- package/workspace/public/pm/c3-toast.js +1 -0
- package/workspace/public/pm/console3.css +3 -0
- package/workspace/public/pm/pm-work-view.js +11 -5
- package/workspace/public/pm/run-card.js +12 -3
- package/workspace/public/pm/sessions-panel.js +2 -2
- package/workspace/server.js +25 -5
- package/workspace/skills/horizon-scan-repo.skill +26 -10
- package/workspace/skills/horizon-scan-research.skill +44 -0
- package/workspace/skills/horizon-scan-web.skill +12 -5
|
@@ -1,73 +1,82 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
|
-
title: "The Horizon scan:
|
|
4
|
-
description: "What the Horizon scan does,
|
|
5
|
-
timestamp: 2026-10-
|
|
3
|
+
title: "The Horizon scan: reports and proposals for the whole portfolio"
|
|
4
|
+
description: "What the portfolio Horizon scan does, its settings and schedule, Scan now and its limits, the report documents, the proposals you approve, what it can and cannot do, and the pmos scan commands."
|
|
5
|
+
timestamp: 2026-10-13
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# The Horizon scan:
|
|
8
|
+
# The Horizon scan: reports and proposals for the whole portfolio
|
|
9
9
|
|
|
10
|
-
The Initiative Driver works on initiatives that already exist. The Horizon scan looks the other way. It
|
|
10
|
+
The Initiative Driver works on initiatives that already exist. The Horizon scan looks the other way. It reads the public web for similar products, changes in your domain and approaches other teams use. It reads your repository for what keeps coming up and no initiative covers.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
One scan covers the whole portfolio. It writes **report documents** and then **proposals** for what to do next. A scan never creates an initiative on its own. Nothing happens until you press Approve.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Settings and schedule
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
The settings belong to the workspace, not to a board:
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
18
|
+
- **Cadence.** **Daily** (the default), **Weekly** or **Off**.
|
|
19
|
+
- **Repository.** On or off. When off, a scan reads the web only.
|
|
20
|
+
- **Report types.** Benchmark, competitive, insight and productivity. All are on by default.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Open the board, press **Board settings**, and find **Horizon scan** below Agent. The block shows the last scan and the next one.
|
|
22
|
+
A scheduled scan runs at most once a day, inside your working hours, never inside your quiet hours. Only one scan runs at a time. `PMOS_DRIVER=off` or `PMOS_SCAN=off` starts no scan at all. Board settings only show that the scan runs for the whole portfolio, with a link to the setup.
|
|
25
23
|
|
|
26
24
|
## The watch list
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
One watch list serves all boards. An item has a kind (`product`, `domain` or `approach`), a name, a reason and the boards it relates to. An old per-board list is folded into this list, and its board stays on its items.
|
|
27
|
+
|
|
28
|
+
The suggest run reads the initiatives of all boards and files up to 8 suggestions. For each one, press **Keep**, **Edit** or **Remove**. You can also add your own item. Only kept items are scanned.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
- **domain.** A topic that changes often.
|
|
32
|
-
- **approach.** A way of working that other teams use.
|
|
30
|
+
## Scan now
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
**Scan now** starts a scan by hand. A picker lets you choose the kept items, the repository and the report types.
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
- At most 3 manual scans a day, and one scan at a time.
|
|
35
|
+
- A manual scan may start outside working hours. It never starts inside quiet hours.
|
|
36
|
+
- A running scan shows its steps (web, repository, reports). **Stop** ends the runs and keeps what was already filed.
|
|
37
37
|
|
|
38
38
|
## What a scan does
|
|
39
39
|
|
|
40
40
|
A scan is two short background runs, in this order.
|
|
41
41
|
|
|
42
|
-
1. **The web run.** It gets the
|
|
43
|
-
2. **The repository run.** It reads your repository, your notes and the
|
|
42
|
+
1. **The web run.** It gets the selected items and the report types, and nothing else. It cannot read your repository, your initiatives or your notes. It writes the web sections of the benchmark, competitive and insight reports. Each claim carries the URL the run read.
|
|
43
|
+
2. **The repository run.** It reads your repository, your notes and the initiatives, and the web sections. It has no way to reach the web. It writes the repository sections and the productivity report (which is repository only), and files the proposals.
|
|
44
|
+
|
|
45
|
+
## The report documents
|
|
46
|
+
|
|
47
|
+
Each selected type gives one markdown document at `<state>/reports/scan-<type>/<YYYY-MM-DD>.md`. A second scan on the same day adds `-2`, then `-3`. The frontmatter has `type: Reference`, `title`, `timestamp`, `source: scan:portfolio`, `made_by: agent`, `output: google-doc`, `scan`, `report` and `origin`. The Documents list in Reports reads them with no change.
|
|
48
|
+
|
|
49
|
+
Every claim cites a source: a URL the web run read, a path, a Jira key or a note. Each document ends with a **What next** section that lists its proposals.
|
|
50
|
+
|
|
51
|
+
## Proposals
|
|
52
|
+
|
|
53
|
+
The repository run files up to 5 proposals for each report. Each one names the finding it came from and its evidence.
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
- **create.** A title, a board, a lane, an anchor and an intent. Approve makes the initiative through the initiative writer in the first stage of the board's workflow, and adds the report as evidence.
|
|
56
|
+
- **research.** A question, its sources and a depth. Approve starts a research run: a web run, then a repository run. It writes a follow-up report linked to its parent, with its own proposals.
|
|
57
|
+
- **attach.** Names an existing initiative. Approve adds the finding to it as evidence.
|
|
46
58
|
|
|
47
|
-
|
|
59
|
+
You can **Approve**, **Change** the fields first, or **Dismiss** with a one-line reason. A dismissal counts as a correction, and a later scan does not raise a proposal whose title has the same significant words. **Undo** takes back a dismissal or a change: the proposal waits for you again, and a new line in the scan ledger records it. The correction a dismissal wrote stays. Approve has no Undo, because it made a real initiative or started runs.
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
- **Briefing.** The **Horizon scan** card shows the scan of the day: when it ran, which boards, how many asks were found, how many match an initiative and how many are new. Each finding has **Open in Triage**. The card says how many public pages the agent read and how many findings it skipped.
|
|
61
|
+
If the initiatives are still being read, Approve for attach and research answers that and you press it again.
|
|
51
62
|
|
|
52
63
|
## What it can and cannot do
|
|
53
64
|
|
|
54
65
|
- It can search and read public web pages, and read your repository.
|
|
55
|
-
- It can file
|
|
66
|
+
- It can write report documents and file proposals. Those are the only things it writes.
|
|
56
67
|
- It cannot send anything: no mail, no message, no comment, no ticket.
|
|
57
68
|
- It cannot read a page behind a login.
|
|
58
|
-
- It cannot create an initiative
|
|
69
|
+
- It cannot create an initiative or change a record. Only you can.
|
|
59
70
|
- The run that reads the web never sees your repository. The run that reads your repository never reaches the web.
|
|
60
|
-
- It cannot be started from the page. It runs on its schedule.
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
The agent can be wrong. Check the sources before you approve.
|
|
63
73
|
|
|
64
74
|
## The commands
|
|
65
75
|
|
|
66
|
-
The agent uses `pmos scan file`
|
|
76
|
+
The agent uses `pmos scan file`, `pmos scan section`, `pmos scan match`, `pmos scan propose`, `pmos scan suggest` and `pmos scan list`. `pmos scan status` shows the setting, the watch list and the last scan. The other commands are yours, at an interactive terminal:
|
|
67
77
|
|
|
68
|
-
- `pmos scan cadence <daily|weekly|off
|
|
69
|
-
- `pmos scan
|
|
70
|
-
- `pmos scan watch edit
|
|
71
|
-
- `pmos scan watch add --board <id> --kind product --name "<what to watch>"` adds an item.
|
|
78
|
+
- `pmos scan cadence <daily|weekly|off>` sets how often the portfolio is scanned.
|
|
79
|
+
- `pmos scan setting` changes the repository switch and the report types.
|
|
80
|
+
- `pmos scan watch keep|remove|edit|add ...` manages the watch list. `--boards a,b` sets the boards of an item.
|
|
72
81
|
|
|
73
|
-
The ledger is `scan.jsonl` in the state folder
|
|
82
|
+
The ledger is `scan.jsonl` in the state folder (`pmos/state`, `state` or `.pmos`). Each action adds one line. Nothing is changed or removed.
|
|
@@ -112,6 +112,7 @@ The row chip then shows the agent's state:
|
|
|
112
112
|
| **Agent: draft ready** | The draft waits for you. |
|
|
113
113
|
| **Agent: posting** | You approved the draft. A session posts exactly that text. |
|
|
114
114
|
| **Posted to** the item | The draft went out. Select **Open what was posted** to see it. |
|
|
115
|
+
| **Agent: needs you** | The agent asked you something, or waits for your OK to run a step, in its session. The case file gives the question. Select **Open the session** and answer it there: the job carries on. |
|
|
115
116
|
| **Agent stopped** | The job failed or you stopped it. The case file gives the reason. Select **Try again** to run it again. |
|
|
116
117
|
|
|
117
118
|
The page reads the state again every 15 seconds while an agent is queued, working or posting.
|
package/workspace/lib/day-run.js
CHANGED
|
@@ -243,7 +243,7 @@ function decide(run, taskId, op, now) {
|
|
|
243
243
|
const stalled = !!t.stalled_session;
|
|
244
244
|
// the PM's own text is sent again as it is: a retry never asks an agent to draft over it
|
|
245
245
|
t.status = t.draft && t.draft.by === 'pm' ? 'sending' : 'waiting'; keepSession(t); t.error = null; t.decided = { action: 'retry', at };
|
|
246
|
-
if (stalled) { t.prev_session = null; delete t.stalled_session; delete t.stalled_status; }
|
|
246
|
+
if (stalled) { t.prev_session = null; delete t.stalled_session; delete t.stalled_status; delete t.asks; }
|
|
247
247
|
return null;
|
|
248
248
|
});
|
|
249
249
|
}
|
|
@@ -303,14 +303,14 @@ function sessionEnded(run, sessionId, now) {
|
|
|
303
303
|
function revive(t) {
|
|
304
304
|
if (t.status !== 'failed' || !t.stalled_session || !t.stalled_status) return;
|
|
305
305
|
t.status = t.stalled_status; t.session_id = t.stalled_session; t.error = null;
|
|
306
|
-
delete t.stalled_session; delete t.stalled_status; delete t.failed_at;
|
|
306
|
+
delete t.stalled_session; delete t.stalled_status; delete t.failed_at; delete t.asks;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
/**
|
|
310
310
|
* A task session is open but has printed nothing for a long time and delivered nothing: it waits on a question, or its
|
|
311
311
|
* agent stopped without reporting. The task fails with `why`, which frees its place, and a retry starts a NEW session.
|
|
312
312
|
*/
|
|
313
|
-
function sessionStalled(run, sessionId, why, now) {
|
|
313
|
+
function sessionStalled(run, sessionId, why, now, { asks = null } = {}) {
|
|
314
314
|
if (!run) return { ok: false, error: 'no day run today' };
|
|
315
315
|
const next = clone(run);
|
|
316
316
|
let changed = false;
|
|
@@ -318,6 +318,7 @@ function sessionStalled(run, sessionId, why, now) {
|
|
|
318
318
|
if (!sessionId || t.session_id !== sessionId || (t.status !== 'working' && t.status !== 'sending')) continue;
|
|
319
319
|
t.stalled_status = t.status; t.stalled_session = sessionId; t.session_id = null; t.prev_session = null;
|
|
320
320
|
t.status = 'failed'; t.error = text(why).slice(0, 240) || 'the session stopped without a draft'; t.failed_at = iso(now); changed = true;
|
|
321
|
+
if (asks) t.asks = asks; else delete t.asks;
|
|
321
322
|
}
|
|
322
323
|
return { ok: true, run: next, changed };
|
|
323
324
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (initiative workspace-headless-jobs; PM decision 2026-10-11: keep Jev, no direct Claude API, background
|
|
4
4
|
* jobs that need no human run headless).
|
|
5
5
|
*
|
|
6
|
-
* kindFor({ agent, origin, skill, env }) → 'notes' | 'skill' | 'ask' | 'drive' | 'scan-web' | 'scan-repo' | 'report-setup' | 'report-find' | null (null: open a pane, as before)
|
|
6
|
+
* kindFor({ agent, origin, skill, env }) → 'notes' | 'skill' | 'ask' | 'drive' | 'scan-web' | 'scan-repo' | 'scan-research-web' | 'scan-research-repo' | 'report-setup' | 'report-find' | null (null: open a pane, as before)
|
|
7
7
|
* argsFor(kind, { modelArgs, addDirs }) → the argv after `claude`
|
|
8
8
|
* parseResult(stdout) → { ok, text, isError, turns, durationMs, costUsd, denied[] }
|
|
9
9
|
* logLines(result, exitCode) → the lines the read-only log pane shows at the end
|
|
@@ -51,6 +51,13 @@ const DRIVE_ORIGINS = Object.freeze(['drive:']);
|
|
|
51
51
|
// the repository and fetches nothing. Neither has both untrusted input and a way out (PRD: trust boundary).
|
|
52
52
|
const SCAN_WEB_ORIGINS = Object.freeze(['scan-web:']);
|
|
53
53
|
const SCAN_REPO_ORIGINS = Object.freeze(['scan-repo:', 'scan-suggest:']);
|
|
54
|
+
// horizon-scan-portfolio: an approved research proposal is a second, smaller scan with the same split. Its web run (origin `scan-research-web:<ws>:<scan>`)
|
|
55
|
+
// gets the approved question and the public sources only. Its repository run (origin `scan-research-repo:<ws>:<scan>`) writes the follow-up's repository
|
|
56
|
+
// sections and files its proposals. The kinds are separate so the research web run holds one verb less than the scan web run (no findings) and so a test can
|
|
57
|
+
// read the argv of each.
|
|
58
|
+
const SCAN_RESEARCH_WEB_ORIGINS = Object.freeze(['scan-research-web:']);
|
|
59
|
+
const SCAN_RESEARCH_REPO_ORIGINS = Object.freeze(['scan-research-repo:']);
|
|
60
|
+
const WEB_KINDS = Object.freeze(['scan-web', 'scan-research-web']); // kinds that read untrusted pages: no directory, no Read, Glob or Grep
|
|
54
61
|
// report-workspace: the Ask box run (origin `report-setup:<ask>`) and the weekly finder run (origin `report-find:<run>`).
|
|
55
62
|
const REPORT_SETUP_ORIGINS = Object.freeze(['report-setup:']);
|
|
56
63
|
const REPORT_FIND_ORIGINS = Object.freeze(['report-find:']);
|
|
@@ -63,10 +70,15 @@ const TOOLS = Object.freeze({
|
|
|
63
70
|
drive: Object.freeze(['Read', 'Glob', 'Grep', 'Bash(pmos driver *)', 'Bash(pmos initiative list *)', 'Bash(pmos initiative export *)',
|
|
64
71
|
'Bash(git log *)', 'Bash(ls *)', 'Bash(date *)']),
|
|
65
72
|
// The web run: WebSearch, WebFetch and the one verb that files a finding. No Read, no Glob, no Grep, no repository path in its prompt or its add-dirs.
|
|
66
|
-
|
|
73
|
+
// `pmos scan section` writes a section of a report document; the web run gives each web claim a URL it read.
|
|
74
|
+
'scan-web': Object.freeze(['WebSearch', 'WebFetch', 'Bash(pmos scan file *)', 'Bash(pmos scan section *)']),
|
|
67
75
|
// The repository run: read and search the repository, `pmos scan` (its PM-only verbs refuse an agent session), the initiative list and git log.
|
|
68
76
|
// No WebSearch, no WebFetch, no curl, no git push: it reads the web run's findings, which are untrusted text, so it has nothing that sends or fetches.
|
|
69
77
|
'scan-repo': Object.freeze(['Read', 'Glob', 'Grep', 'Bash(pmos scan *)', 'Bash(pmos initiative list *)', 'Bash(git log *)']),
|
|
78
|
+
// The research web run: the approved question and its public sources. WebSearch, WebFetch and the one verb that writes a section. No Read, Glob or Grep, no directory.
|
|
79
|
+
'scan-research-web': Object.freeze(['WebSearch', 'WebFetch', 'Bash(pmos scan section *)']),
|
|
80
|
+
// The research repository run: the same tools as the scan repository run. It has no web tool and nothing that sends.
|
|
81
|
+
'scan-research-repo': Object.freeze(['Read', 'Glob', 'Grep', 'Bash(pmos scan *)', 'Bash(pmos initiative list *)', 'Bash(git log *)']),
|
|
70
82
|
// The Ask box run: read and search, the one verb that files a proposal, the read-only list of definitions and git log. No Write, no Edit, no web,
|
|
71
83
|
// nothing that sends. The PM's earlier copies are read through a connector only where the PM's own settings allow it (as for `drive`).
|
|
72
84
|
'report-setup': Object.freeze(['Read', 'Glob', 'Grep', 'Bash(pmos report propose *)', 'Bash(pmos report definitions *)', 'Bash(git log *)']),
|
|
@@ -91,6 +103,8 @@ function kindFor({ agent, origin, skill, env } = {}) {
|
|
|
91
103
|
if (DRIVE_ORIGINS.some((p) => o.startsWith(p))) return 'drive';
|
|
92
104
|
if (SCAN_WEB_ORIGINS.some((p) => o.startsWith(p))) return 'scan-web';
|
|
93
105
|
if (SCAN_REPO_ORIGINS.some((p) => o.startsWith(p))) return 'scan-repo';
|
|
106
|
+
if (SCAN_RESEARCH_WEB_ORIGINS.some((p) => o.startsWith(p))) return 'scan-research-web';
|
|
107
|
+
if (SCAN_RESEARCH_REPO_ORIGINS.some((p) => o.startsWith(p))) return 'scan-research-repo';
|
|
94
108
|
if (REPORT_SETUP_ORIGINS.some((p) => o.startsWith(p))) return 'report-setup';
|
|
95
109
|
if (REPORT_FIND_ORIGINS.some((p) => o.startsWith(p))) return 'report-find';
|
|
96
110
|
if (skill && HEADLESS_SKILLS.includes(String(skill))) return 'skill';
|
|
@@ -101,8 +115,8 @@ function argsFor(kind, { modelArgs = [], addDirs = [] } = {}) {
|
|
|
101
115
|
const tools = TOOLS[kind];
|
|
102
116
|
if (!tools) throw new Error('unknown headless kind: ' + kind);
|
|
103
117
|
const argv = ['-p', '--output-format', 'json', '--no-session-persistence', ...modelArgs, '--allowedTools', tools.join(',')];
|
|
104
|
-
//
|
|
105
|
-
if (kind
|
|
118
|
+
// A web run gets no directory at all, whatever the caller passes: it must not be able to read the repository.
|
|
119
|
+
if (!WEB_KINDS.includes(kind)) for (const d of addDirs) if (d) argv.push('--add-dir', String(d));
|
|
106
120
|
return argv;
|
|
107
121
|
}
|
|
108
122
|
|
|
@@ -144,4 +158,4 @@ function logLines(r, exitCode) {
|
|
|
144
158
|
return out;
|
|
145
159
|
}
|
|
146
160
|
|
|
147
|
-
module.exports = { HEADLESS_SKILLS, HEADLESS_ORIGINS, ASK_ORIGINS, DRIVE_ORIGINS, SCAN_WEB_ORIGINS, SCAN_REPO_ORIGINS, REPORT_SETUP_ORIGINS, REPORT_FIND_ORIGINS, TOOLS, DEFAULT_TIMEOUT_MS, kindFor, argsFor, timeoutMs, parseResult, logLines, switchedOff };
|
|
161
|
+
module.exports = { HEADLESS_SKILLS, HEADLESS_ORIGINS, ASK_ORIGINS, DRIVE_ORIGINS, SCAN_WEB_ORIGINS, SCAN_REPO_ORIGINS, SCAN_RESEARCH_WEB_ORIGINS, SCAN_RESEARCH_REPO_ORIGINS, WEB_KINDS, REPORT_SETUP_ORIGINS, REPORT_FIND_ORIGINS, TOOLS, DEFAULT_TIMEOUT_MS, kindFor, argsFor, timeoutMs, parseResult, logLines, switchedOff };
|
package/workspace/lib/my-work.js
CHANGED
|
@@ -195,7 +195,8 @@ function stateOf(item, presses, runs) {
|
|
|
195
195
|
if (last.action === 'handle') return { state: 'yours', sub: null, task: null, instruction: null, decided_at: last.at };
|
|
196
196
|
const t = taskOf(runs, last);
|
|
197
197
|
const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null, body: t.draft ? t.draft.body || null : null,
|
|
198
|
-
sent: t.sent || null, started_at: t.started_at || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0
|
|
198
|
+
sent: t.sent || null, started_at: t.started_at || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
|
|
199
|
+
session: t.stalled_session || null, asks: t.asks || null } : null;
|
|
199
200
|
const base = { task, instruction: last.instruction, decided_at: last.at };
|
|
200
201
|
if (!t) return { state: 'agent', sub: 'waiting', ...base };
|
|
201
202
|
if (t.status === 'review') return { state: 'ready', sub: 'review', ...base };
|
|
@@ -59,9 +59,9 @@ const VERBS = Object.freeze({
|
|
|
59
59
|
// Not a pm-os-absorb verb (run: null): initiative initiative-driver added it through the same table.
|
|
60
60
|
driver: Object.freeze({ module: 'driver-cli.js', run: null, guide: 'initiative-driver.md',
|
|
61
61
|
help: 'pmos driver propose|status|keep|return|drop|pause|resume|mode … — the Initiative Driver: drafts for you to keep, send back or drop (pmos driver help)' }),
|
|
62
|
-
// Not a pm-os-absorb verb (run: null): initiative horizon-scan added it through the same table.
|
|
62
|
+
// Not a pm-os-absorb verb (run: null): initiative horizon-scan added it through the same table; horizon-scan-portfolio added section, propose, setting and --boards.
|
|
63
63
|
scan: Object.freeze({ module: 'scan-cli.js', run: null, guide: 'horizon-scan.md',
|
|
64
|
-
help: 'pmos scan file|list|match|suggest|status|cadence|watch … — the Horizon scan:
|
|
64
|
+
help: 'pmos scan file|section|propose|list [--sections]|match|suggest [--boards]|status|setting|cadence|watch [--boards] … — the Horizon scan: the portfolio watch list, the findings, the report sections and the proposals the PM approves, changes or dismisses (pmos scan help)' }),
|
|
65
65
|
launcher: Object.freeze({ module: 'launcher-cli.js', run: null, guide: 'launcher.md',
|
|
66
66
|
help: 'pmos launcher on|off|status — a PMOS app and a login item, so the workspace starts without a terminal' }),
|
|
67
67
|
});
|
|
@@ -69,7 +69,7 @@ const MODULES = Object.freeze([
|
|
|
69
69
|
'routes-meeting-focus', // console3 Meeting focus: the meeting, its note's talk, the companion's notes (GET); topic corrections and Ask PMOS quietly (page press)
|
|
70
70
|
'routes-stage-tools', // stage-tools-records: the records the PM edits per stage, what PMOS holds per stage, the sprint burndown
|
|
71
71
|
'routes-driver', // initiative-driver: the Agent strip's data, Keep, Send back, Drop, Pause and the board mode (driver.jsonl)
|
|
72
|
-
'routes-scan', // horizon-scan:
|
|
72
|
+
'routes-scan', // horizon-scan-portfolio: one scan for the workspace: the watch list and setting, Scan now and Stop, report documents and their proposals (approve, change, dismiss, undo), plus the first scan's per-board and Briefing reads (scan.jsonl)
|
|
73
73
|
'routes-report-setups', // report-workspace: recurring reports, the Ask box, the finder, the report documents and their export (report-setups.jsonl)
|
|
74
74
|
]);
|
|
75
75
|
const PM_DIR = path.join(__dirname, '..', 'public', 'pm');
|
|
@@ -128,6 +128,30 @@ function appendScanAsk(ctx, w, boardId, f, by) {
|
|
|
128
128
|
return { ok: true, existing: false, id };
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** A new initiative on a board: initiative-writer makes the record (the only writer of one), then `tie` appends the boards-ledger line that puts it in the FIRST stage
|
|
132
|
+
* of the board's workflow, and the initiative's own workflow line when its default template differs from the board's. Triage's accept (asks/accept) and the Horizon
|
|
133
|
+
* scan's approved proposals (lib/routes-scan.js) both come through here, so the placement is one piece of code.
|
|
134
|
+
* `tie({ file, board, initiative, stage })` appends the line that names the initiative on the board (ask-accept, or assign and move).
|
|
135
|
+
* → { ok, initiative: { id, path }, stage, board } | { ok: false, status, error, field } */
|
|
136
|
+
function createAndPlace(ctx, w, boardId, input, actor, tie) {
|
|
137
|
+
const fr = frameFor(ctx, w);
|
|
138
|
+
const board = fr.boards.find((b) => b.id === boardId);
|
|
139
|
+
if (!board) return { ok: false, status: 404, error: `no board ${boardId} in this workspace` };
|
|
140
|
+
const file = ledgerFile(ctx, w);
|
|
141
|
+
if (!file) return { ok: false, status: 409, error: NO_STATE };
|
|
142
|
+
const r = writer.createInitiative(w.root, input, { actor });
|
|
143
|
+
if (!r.ok) return { ok: false, status: r.status || 400, error: r.error, field: r.field || null };
|
|
144
|
+
const tpl = board.defTpl && B.INI_TEMPLATES[board.defTpl] ? board.defTpl : null;
|
|
145
|
+
const ownWf = tpl && tpl !== board.template ? B.INI_TEMPLATES[tpl].stages.map((x) => ({ name: x.name, cat: x.cat, hint: x.hint || '' })) : null;
|
|
146
|
+
const stage = (ownWf || board.workflow)[0].name;
|
|
147
|
+
tie({ file, board, initiative: r.id, stage });
|
|
148
|
+
if (ownWf) append(file, actor, { kind: 'ini-workflow', board: board.id, id: r.id, template: tpl, stages: ownWf, moved: null });
|
|
149
|
+
rowsLib.invalidate(w.root);
|
|
150
|
+
rowCache.delete(w.root);
|
|
151
|
+
if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg(w.id);
|
|
152
|
+
return { ok: true, initiative: { id: r.id, path: r.path }, stage, board: board.id };
|
|
153
|
+
}
|
|
154
|
+
|
|
131
155
|
// ── boards of a workspace ─────────────────────────────────────────────────────────────────────────
|
|
132
156
|
const isSelf = (ctx, w) => !!(ctx.selfRoot && w && path.resolve(w.root) === path.resolve(ctx.selfRoot));
|
|
133
157
|
|
|
@@ -677,17 +701,10 @@ const WRITES = {
|
|
|
677
701
|
if (!p.file) return fail(ctx, res, 409, NO_STATE);
|
|
678
702
|
const input = { title: body.title, lane: body.lane, anchor: body.anchor };
|
|
679
703
|
for (const k of ['intent', 'id']) if (body[k] != null && body[k] !== '') input[k] = body[k];
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const stage = (ownWf || p.board.workflow)[0].name;
|
|
685
|
-
append(p.file, g.actor, { kind: 'ask-accept', board: p.board.id, ask: a.id, initiative: r.id, stage });
|
|
686
|
-
if (ownWf) append(p.file, g.actor, { kind: 'ini-workflow', board: p.board.id, id: r.id, template: tpl, stages: ownWf, moved: null });
|
|
687
|
-
rowsLib.invalidate(p.w.root);
|
|
688
|
-
rowCache.delete(p.w.root);
|
|
689
|
-
if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg(p.w.id);
|
|
690
|
-
ctx.json(res, { ok: true, initiative: { id: r.id, path: r.path }, stage });
|
|
704
|
+
const made = createAndPlace(ctx, p.w, p.board.id, input, g.actor,
|
|
705
|
+
({ file, initiative, stage }) => append(file, g.actor, { kind: 'ask-accept', board: p.board.id, ask: a.id, initiative, stage }));
|
|
706
|
+
if (!made.ok) return fail(ctx, res, made.status || 400, made.error, { field: made.field || null });
|
|
707
|
+
ctx.json(res, { ok: true, initiative: made.initiative, stage: made.stage });
|
|
691
708
|
},
|
|
692
709
|
'/api/boards/asks/more': (ctx, res, body, g) => {
|
|
693
710
|
const p = postFrame(ctx, res, body);
|
|
@@ -992,4 +1009,4 @@ function meetingBoards(ctx) {
|
|
|
992
1009
|
return { list, detail };
|
|
993
1010
|
}
|
|
994
1011
|
|
|
995
|
-
module.exports = { prefixes: ['/api/boards'], handle, move, boardIndex, appendScanAsk, frameFor, boardIdOf, meetingBoards, boardsOf, baseRows, boardRow, portfolio, SELF_REFUSAL, NO_STATE };
|
|
1012
|
+
module.exports = { prefixes: ['/api/boards'], handle, move, boardIndex, appendScanAsk, scanDeclineSignal, createAndPlace, ledgerFile, append, frameFor, boardIdOf, meetingBoards, boardsOf, baseRows, boardRow, portfolio, SELF_REFUSAL, NO_STATE };
|