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,184 +1,20 @@
|
|
|
1
|
-
/* pm/c3-scan.js — the Horizon scan on console3
|
|
2
|
-
*
|
|
3
|
-
*
|
|
1
|
+
/* pm/c3-scan.js — the Horizon scan on console3: the Briefing card and the Triage rows (initiative horizon-scan-portfolio; mockup 42-hscan.js).
|
|
2
|
+
* One scan covers the whole portfolio. Its screens are in c3-scan-setup.js (the Portfolio section, the setup, the picker, the running sheet and the
|
|
3
|
+
* Board settings pointer) and c3-scan-next.js (a report's "Proposed next steps"); every call to the server is in c3-scan-api.js. This file draws
|
|
4
|
+
* the two places that stay here:
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* scanAskParts(a, o) the parts of a Triage row for an ask
|
|
8
|
-
* chip and the three buttons. c3-ib-triage.js draws the row with them.
|
|
9
|
-
* scanSection(S) the Briefing's Horizon scan card for the day's scan; hidden when no scan ran today.
|
|
10
|
-
*
|
|
11
|
-
* Reads: GET /api/scan/board, GET /api/scan/today. Presses: POST /api/scan/watch (keep, edit, remove, add) and POST /api/scan/cadence, through the
|
|
12
|
-
* board's api (http.js: the nonce and the page Origin). The page sends no `by`: the server takes the PM from the call path. Plain words for every
|
|
13
|
-
* empty and error state. A removed item has an Undo that brings it back as it was.
|
|
6
|
+
* scanSection(S) the Briefing's Horizon scan card: when the last scan ran, the boards, one row per report with the proposals waiting
|
|
7
|
+
* on it, Open and Scan now. Shown on a day a scan ran, and while proposals still wait.
|
|
8
|
+
* scanAskParts(a, o) the parts of a Triage row for an ask an earlier per-board scan found (c3-ib-triage.js draws the row with them).
|
|
14
9
|
*/
|
|
15
10
|
import { el } from './dom.js';
|
|
16
|
-
import { icon, pbtn, lk, chip,
|
|
17
|
-
import { toast } from './c3-toast.js';
|
|
18
|
-
import { pmGet } from './http.js';
|
|
11
|
+
import { icon, pbtn, lk, chip, plural, sect, put, chgl, ago } from './c3-ini-ui.js';
|
|
19
12
|
import { onBriefing } from './shell-ticker.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
l.rel = 'stylesheet'; l.href = new URL('./c3-scan.css', import.meta.url).href; l.setAttribute('data-c3-scan', '');
|
|
26
|
-
document.head.appendChild(l);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/* ── words ── */
|
|
30
|
-
export const FREQ = [['daily', 'Daily'], ['weekly', 'Weekly'], ['off', 'Off']];
|
|
31
|
-
export const KINDS = ['product', 'domain', 'approach'];
|
|
32
|
-
const KIND_ICON = { product: 'org', domain: 'report', approach: 'bolt' };
|
|
33
|
-
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
34
|
-
const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
35
|
-
const p2 = (n) => String(n).padStart(2, '0');
|
|
36
|
-
const clock = (d) => p2(d.getHours()) + ':' + p2(d.getMinutes());
|
|
37
|
-
const sameDay = (a, b) => a.toDateString() === b.toDateString();
|
|
38
|
-
const errText = (r, what) => what + ': ' + ((r && r.error) || 'the workspace did not answer') + '.';
|
|
39
|
-
const WHEN_SHORT = (d) => WD[d.getDay()] + ' ' + d.getDate() + ' ' + MON[d.getMonth()] + ', ' + clock(d);
|
|
40
|
-
|
|
41
|
-
/** 'today, 09:00', 'yesterday, 09:00' or 'Mon 12 Oct, 09:00', for a time in the past. */
|
|
42
|
-
export function pastLabel(iso, now = new Date()) {
|
|
43
|
-
const d = new Date(iso);
|
|
44
|
-
if (!Number.isFinite(d.getTime())) return '';
|
|
45
|
-
const y = new Date(now); y.setDate(y.getDate() - 1);
|
|
46
|
-
return sameDay(d, now) ? 'today, ' + clock(d) : sameDay(d, y) ? 'yesterday, ' + clock(d) : WHEN_SHORT(d);
|
|
47
|
-
}
|
|
48
|
-
/** 'at the next check', 'today, 09:00', 'tomorrow, 09:00' or 'Mon 19 Oct, 09:00', for the next scan. */
|
|
49
|
-
export function nextLabel(iso, now = new Date()) {
|
|
50
|
-
const d = new Date(iso);
|
|
51
|
-
if (!Number.isFinite(d.getTime())) return '';
|
|
52
|
-
if (d.getTime() - now.getTime() < 90000) return 'within a minute';
|
|
53
|
-
const t = new Date(now); t.setDate(t.getDate() + 1);
|
|
54
|
-
return sameDay(d, now) ? 'today, ' + clock(d) : sameDay(d, t) ? 'tomorrow, ' + clock(d) : WHEN_SHORT(d);
|
|
55
|
-
}
|
|
56
|
-
export const scheduleLine = (c) => (c === 'daily' ? 'Every working day, 09:00' : c === 'weekly' ? 'Every Monday, 09:00' : 'Off. No scan runs on this board');
|
|
57
|
-
|
|
58
|
-
/* ── the Board settings block ── */
|
|
59
|
-
export async function loadScanBoard(api, board) {
|
|
60
|
-
const r = await api.get('/api/scan/board', { board });
|
|
61
|
-
return r && r.ok ? { ok: true, data: r } : { ok: false, error: errText(r, 'The horizon scan could not be read') };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The Horizon scan block. Draws at once with "Reading the horizon scan" and fills itself from the server; every press posts, then reads again.
|
|
66
|
-
* o: { api, board (id), boardName }.
|
|
67
|
-
*/
|
|
68
|
-
export function scanSetting(o) {
|
|
69
|
-
ensureScanCss();
|
|
70
|
-
const wrap = el('div', { class: 'sc-set', 'data-scan-set': '' });
|
|
71
|
-
const S = { data: null, err: '', busy: false, edit: null, editValue: '', addName: '', addKind: 'product', addErr: '' };
|
|
72
|
-
|
|
73
|
-
async function load() {
|
|
74
|
-
const r = await loadScanBoard(o.api, o.board);
|
|
75
|
-
if (r.ok) { S.data = r.data; S.err = ''; } else { S.err = r.error; }
|
|
76
|
-
draw();
|
|
77
|
-
}
|
|
78
|
-
/** One press: post, say so on failure, read again. Returns the answer or null. */
|
|
79
|
-
async function press(route, body, what) {
|
|
80
|
-
if (S.busy) return null;
|
|
81
|
-
S.busy = true; draw();
|
|
82
|
-
const r = await o.api.post('/api/scan/' + route, { board: o.board, ...body });
|
|
83
|
-
S.busy = false;
|
|
84
|
-
if (!r || !r.ok) { toast({ text: errText(r, what + ' did not work'), tone: 'error', timeout: 6000 }); await load(); return null; }
|
|
85
|
-
await load();
|
|
86
|
-
return r;
|
|
87
|
-
}
|
|
88
|
-
const setCadence = async (c) => {
|
|
89
|
-
if (S.data && S.data.cadence === c) return;
|
|
90
|
-
const r = await press('cadence', { cadence: c }, 'The change');
|
|
91
|
-
if (r) toast(c === 'off' ? 'Horizon scan is off for ' + o.boardName : 'Horizon scan: ' + scheduleLine(c).toLowerCase());
|
|
92
|
-
};
|
|
93
|
-
const keep = (it) => press('watch', { op: 'keep', id: it.id }, 'Keep');
|
|
94
|
-
const remove = async (it) => {
|
|
95
|
-
const r = await press('watch', { op: 'remove', id: it.id }, 'Remove');
|
|
96
|
-
if (r) toast({ text: 'Removed ' + it.name, onAction: () => press('watch', { op: 'keep', id: it.id, restore: true }, 'Undo'), timeout: 7000 });
|
|
97
|
-
};
|
|
98
|
-
const save = async (it) => {
|
|
99
|
-
const v = S.editValue.trim();
|
|
100
|
-
if (!v) return;
|
|
101
|
-
S.edit = null;
|
|
102
|
-
if (v !== it.name) await press('watch', { op: 'edit', id: it.id, name: v }, 'Save');
|
|
103
|
-
else if (it.state === 'suggested') await keep(it);
|
|
104
|
-
else draw();
|
|
105
|
-
};
|
|
106
|
-
const add = async () => {
|
|
107
|
-
const v = S.addName.trim();
|
|
108
|
-
if (!v) { S.addErr = 'Write what to watch first.'; draw(); const i = wrap.querySelector('[data-scan-add-in]'); if (i) i.focus(); return; }
|
|
109
|
-
S.addErr = '';
|
|
110
|
-
const r = await press('watch', { op: 'add', name: v, kind: S.addKind }, 'Add');
|
|
111
|
-
if (r) { S.addName = ''; draw(); toast('Added ' + v + ' to the watch list'); }
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const kindTag = (k) => el('span', { class: 'tag' + (k === 'product' ? ' acc' : '') }, k);
|
|
115
|
-
function item(it, first) {
|
|
116
|
-
if (S.edit === it.id) {
|
|
117
|
-
const input = el('input', { type: 'text', 'data-scan-edit-in': '', value: S.editValue, 'aria-label': 'Watch list item name', maxlength: '80', autocomplete: 'off',
|
|
118
|
-
oninput: (e) => { S.editValue = e.target.value; }, onkeydown: (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); save(it); } else if (e.key === 'Escape') { S.edit = null; draw(); } } });
|
|
119
|
-
setTimeout(() => input.focus(), 20);
|
|
120
|
-
return el('li', { class: 'sc-it', 'data-scan-item': it.id, 'data-scan-state': 'edit' }, icon('eye', 'sm'), el('div', { class: 'sc-main' }, el('div', { class: 'form' }, input),
|
|
121
|
-
el('div', { class: 'sc-acts' }, pbtn('primary', 'Save', 'check', { 'data-scan-act': 'save', 'data-id': it.id, onclick: () => save(it) }), pbtn('ghost', 'Cancel', null, { 'data-scan-act': 'cancel', 'data-id': it.id, onclick: () => { S.edit = null; draw(); } }))));
|
|
122
|
-
}
|
|
123
|
-
const kept = it.state === 'kept';
|
|
124
|
-
const acts = [!kept ? pbtn('outline', 'Keep', 'check', { 'data-scan-act': 'keep', 'data-id': it.id, disabled: S.busy, onclick: () => keep(it) }) : null,
|
|
125
|
-
pbtn('ghost', 'Edit', 'edit', { 'data-scan-act': 'edit', 'data-id': it.id, disabled: S.busy, onclick: () => { S.edit = it.id; S.editValue = it.name; draw(); } }),
|
|
126
|
-
pbtn('ghost', 'Remove', 'x', { 'data-scan-act': 'remove', 'data-id': it.id, disabled: S.busy, onclick: () => remove(it) })];
|
|
127
|
-
return el('li', { class: 'sc-it', 'data-scan-item': it.id, 'data-scan-state': it.state }, icon(KIND_ICON[it.kind] || 'eye', 'sm'),
|
|
128
|
-
el('div', { class: 'sc-main' },
|
|
129
|
-
el('div', { class: 'sc-name' }, el('b', null, it.name), kindTag(it.kind), first && kept ? st('ok', 'check', 'Kept') : null),
|
|
130
|
-
first || !kept ? (it.why ? el('span', { class: 'dim sc-why' }, it.why) : null) : null,
|
|
131
|
-
el('div', { class: 'chips' }, chip(icon('init', 'sm'), it.ini ? 'from ' + it.ini : 'added by you')),
|
|
132
|
-
el('div', { class: 'sc-acts' }, ...acts)));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function draw() {
|
|
136
|
-
const d = S.data;
|
|
137
|
-
if (S.err && !d) { put(wrap, lbl('HORIZON SCAN'), el('div', { class: 'fold w', role: 'alert', 'data-scan': 'error' }, icon('alert', 'sm'), S.err, ' ', pbtn('outline', 'Try again', null, { onclick: load }))); return; }
|
|
138
|
-
if (!d) { put(wrap, lbl('HORIZON SCAN'), note('Reading the horizon scan')); return; }
|
|
139
|
-
if (!d.ledger) { put(wrap, lbl('HORIZON SCAN'), el('div', { class: 'fold', 'data-scan': 'no-ledger' }, icon('info', 'sm'), 'This repository has no state folder (pmos/state, state or .pmos), so the scan has nowhere to keep its watch list.')); return; }
|
|
140
|
-
const byOrder = d.items || [];
|
|
141
|
-
const first = !d.last;
|
|
142
|
-
const fromAgent = byOrder.some((i) => i.ini);
|
|
143
|
-
const pending = d.suggested.length;
|
|
144
|
-
const n = byOrder.length;
|
|
145
|
-
const off = d.cadence === 'off';
|
|
146
|
-
const now = new Date();
|
|
147
|
-
const lastLine = d.running ? 'A scan is running now.'
|
|
148
|
-
: d.last ? (d.last.ok === false ? 'Last scan did not finish' + (d.last.reason ? ': ' + d.last.reason : '') + ' · ' + pastLabel(d.last.at, now) + '.' : 'Last scan: ' + pastLabel(d.last.at, now) + '.')
|
|
149
|
-
: 'Last scan: none yet. The first scan runs after you decide on the watch list.';
|
|
150
|
-
const nextLine = off || !d.next ? '' : ' Next scan ' + nextLabel(d.next, now) + '.';
|
|
151
|
-
const noKept = !off && !d.kept.length ? 'No scan runs until you keep an item.' : '';
|
|
152
|
-
const freq = el('div', { class: 'seg ini-seg', role: 'group', 'aria-label': 'How often the agent scans' },
|
|
153
|
-
...FREQ.map(([k, l]) => btn({ 'aria-pressed': String(d.cadence === k), 'data-scan-freq': k, disabled: S.busy, onclick: () => setCadence(k) }, l)));
|
|
154
|
-
const banner = first && fromAgent
|
|
155
|
-
? el('div', { class: 'fold', role: 'status', 'data-scan': 'banner' }, icon('spark', 'sm'), 'The agent suggests ' + plural(n, 'thing') + ' to watch. Keep, edit or remove each before the first scan.' + (pending ? '' : ' You kept them all.'))
|
|
156
|
-
: null;
|
|
157
|
-
const empty = d.suggesting ? el('li', { class: 'dim', 'data-scan': 'suggesting' }, 'The agent is reading the initiatives of this board to suggest what to watch.')
|
|
158
|
-
: el('li', { class: 'dim', 'data-scan': 'empty' }, 'The watch list is empty. The web part of the scan has nothing to search for.' + (d.suggest_failed ? ' The suggest run did not finish. Add what to watch below.' : ''));
|
|
159
|
-
const kindSel = el('select', { 'data-scan-add-kind': '', 'aria-label': 'Kind', onchange: (e) => { S.addKind = e.target.value; } }, ...KINDS.map((k) => el('option', { value: k, selected: k === S.addKind }, k)));
|
|
160
|
-
const addIn = el('input', { type: 'text', 'data-scan-add-in': '', placeholder: 'Add something to watch', 'aria-label': 'Add to the watch list', maxlength: '80', autocomplete: 'off', value: S.addName,
|
|
161
|
-
oninput: (e) => { S.addName = e.target.value; }, onkeydown: (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); add(); } } });
|
|
162
|
-
put(wrap,
|
|
163
|
-
lbl('HORIZON SCAN'),
|
|
164
|
-
el('div', { class: 'chips2' }, el('span', { class: 'dim mono ini-lbl' }, 'HOW OFTEN'), freq),
|
|
165
|
-
el('div', { class: 'sc-meta', 'data-scan': 'meta' },
|
|
166
|
-
el('span', { 'data-scan': 'schedule' }, el('b', null, d.schedule + '.'), off ? '' : ' The scan costs two short runs per board.'),
|
|
167
|
-
el('span', { class: 'dim', 'data-scan': 'last' }, lastLine, nextLine ? el('b', { 'data-scan': 'next' }, nextLine) : null, noKept ? ' ' + noKept : ''),
|
|
168
|
-
off ? null : el('span', { class: 'dim' }, 'The agent skips what it already found, so a ' + d.cadence + ' scan raises only new things.'),
|
|
169
|
-
d.switched_off ? el('span', { class: 'fold w', role: 'alert', 'data-scan': 'switched-off' }, icon('alert', 'sm'), 'Scans are switched off on this machine (PMOS_DRIVER or PMOS_SCAN is off). Nothing runs until that changes.') : null),
|
|
170
|
-
banner,
|
|
171
|
-
lbl('WATCH LIST (' + n + ')'),
|
|
172
|
-
el('ul', { class: 'sc-list', 'data-scan': 'list' }, ...(n ? byOrder.map((i) => item(i, first)) : [empty])),
|
|
173
|
-
el('div', { class: 'sc-add' }, addIn, kindSel, pbtn('outline', 'Add', 'plus', { 'data-scan-act': 'add', disabled: S.busy, onclick: add })),
|
|
174
|
-
S.addErr ? el('div', { class: 'fold w', role: 'alert', 'data-scan': 'add-error' }, icon('alert', 'sm'), S.addErr) : null,
|
|
175
|
-
el('div', { class: 'dim sc-foot' }, 'The web part sees only these topics, never private repository content. It reads public pages and searches. It sends nothing. A scan never creates an initiative: each finding is an ask in Triage.'));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
draw();
|
|
179
|
-
load();
|
|
180
|
-
return wrap;
|
|
181
|
-
}
|
|
13
|
+
import { getPortfolio, getBoards, onScanChange } from './c3-scan-api.js';
|
|
14
|
+
import { openScanDoc } from './c3-scan-next.js';
|
|
15
|
+
import { openScanPicker, latestReports } from './c3-scan-setup.js';
|
|
16
|
+
import { ensureScanCss, pastLabel, sameDayAs } from './c3-scan-words.js';
|
|
17
|
+
export { ensureScanCss, pastLabel };
|
|
182
18
|
|
|
183
19
|
/* ── Triage: an ask the scan found ── */
|
|
184
20
|
export const isScanAsk = (a) => !!(a && a.source === 'scan' && a.found);
|
|
@@ -216,48 +52,40 @@ export function scanAskParts(a, o) {
|
|
|
216
52
|
}
|
|
217
53
|
|
|
218
54
|
/* ── Briefing: the Horizon scan card ── */
|
|
219
|
-
|
|
220
|
-
export function goBoard(board, tab, { settings = false } = {}) {
|
|
221
|
-
try { const u = new URL(location.href); u.searchParams.set('init', 'board:' + encodeURIComponent(board) + ':' + tab); history.replaceState(history.state, '', u); } catch { /* no history */ }
|
|
222
|
-
GO.settings = settings ? board : '';
|
|
223
|
-
return import('./c3-brief.js').then((m) => { if (m.hooks && m.hooks.go) m.hooks.go({ action: 'goinit' }); try { window.dispatchEvent(new CustomEvent('pmos:ini-go', { detail: { board, tab, settings } })); } catch { /* no events */ } });
|
|
224
|
-
}
|
|
225
|
-
/** c3-initiatives.js asks once whether to open a board's settings after it drew the board the card named. */
|
|
226
|
-
export const GO = { settings: '' };
|
|
227
|
-
export function takeSettingsFor(board) { if (GO.settings && GO.settings === board) { GO.settings = ''; return true; } return false; }
|
|
55
|
+
const waitingOn = (d, id) => (d.reports.find((r) => r.id === id) || {}).open || 0;
|
|
228
56
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const first = boards[0] ? boards[0].id : '';
|
|
240
|
-
return sect({ ic: 'spark', title: 'Horizon scan', count: t.found, attrs: { 'data-c3pg': 'scan', 'aria-label': 'Horizon scan' } },
|
|
57
|
+
/** The card for the latest scan, or null when no scan ran today and nothing waits. */
|
|
58
|
+
export function cardOf(d, { ws, boardCount, showReports, now = new Date() } = {}) {
|
|
59
|
+
const mine = latestReports(d);
|
|
60
|
+
const ranToday = !!(d.last && d.last.at && sameDayAs(new Date(d.last.at), now));
|
|
61
|
+
if (!d.last || (!ranToday && !d.waiting) || !mine.length) return null;
|
|
62
|
+
const rows = mine.map((r) => el('li', { 'data-scan-report': r.id }, icon('search', 'sm'),
|
|
63
|
+
el('span', { class: 'sc-find' }, lk({ 'data-scan-act': 'opendoc', 'data-id': r.id, onclick: () => openScanDoc(ws, r, { panel: waitingOn(d, r.id) > 0 }) }, r.title), el('span', { class: 'dim sc-sm' }, plural(waitingOn(d, r.id), 'proposal') + ' waiting')),
|
|
64
|
+
lk({ 'data-scan-act': 'open-report', 'data-id': r.id, 'aria-label': 'Open ' + r.title, onclick: () => openScanDoc(ws, r, { panel: waitingOn(d, r.id) > 0 }) }, 'Open')));
|
|
65
|
+
const foot = 'Nothing was sent.' + (d.last.pages != null ? ' The agent read ' + plural(Number(d.last.pages) || 0, 'public page') + ', by topic only.' : '') + (Number(d.last.skipped) ? ' It skipped ' + plural(Number(d.last.skipped), 'finding') + ' you dismissed before.' : '');
|
|
66
|
+
return sect({ ic: 'search', title: 'Horizon scan', count: mine.length, attrs: { 'data-c3pg': 'scan', 'aria-label': 'Horizon scan' } },
|
|
241
67
|
el('div', { class: 'sc-card' },
|
|
242
|
-
el('div', { class: '
|
|
243
|
-
|
|
244
|
-
(
|
|
245
|
-
|
|
246
|
-
|
|
68
|
+
el('div', { class: 'chips2' }, chip(icon('clock', 'sm'), pastLabel(d.last.at, now)), chip(icon('cols', 'sm'), boardCount ? plural(boardCount, 'board') + ' scanned' : 'Whole portfolio scanned'), el('span', { class: 'tag acc' }, plural(mine.length, 'report') + (ranToday ? ' today' : ''))),
|
|
69
|
+
chgl(rows, { 'data-scan': 'reports' }),
|
|
70
|
+
el('div', { class: 'chips2' }, el('span', { class: 'tag' + (d.waiting ? ' warn' : ''), 'data-scan': 'waiting' }, plural(d.waiting, 'proposal') + ' waiting for you'),
|
|
71
|
+
pbtn('primary', 'Open', 'out', { 'data-scan-act': 'open', onclick: () => (showReports ? showReports() : openScanDoc(ws, mine[0], { panel: d.waiting > 0 })) }),
|
|
72
|
+
pbtn('ghost', 'Scan now', 'bolt', { 'data-scan-act': 'scan', disabled: !d.manual.ok && d.manual.why !== 'running', title: d.manual.left + ' of ' + d.manual.limit + ' manual scans left today', onclick: () => openScanPicker(ws, {}) })),
|
|
73
|
+
el('div', { class: 'fold', 'data-scan': 'foot' }, icon('info', 'sm'), foot)));
|
|
247
74
|
}
|
|
248
75
|
|
|
249
|
-
/** The Briefing's card. Returns a container that holds the card once a scan has run today, and nothing before. */
|
|
76
|
+
/** The Briefing's card. Returns a container that holds the card once a scan has run today (or while proposals wait), and nothing before. */
|
|
250
77
|
export function scanSection(S) {
|
|
251
78
|
ensureScanCss();
|
|
252
79
|
const host = el('div', { 'data-scan-host': '' });
|
|
253
80
|
let sig = '';
|
|
254
|
-
const fill = () =>
|
|
255
|
-
|
|
256
|
-
const g = JSON.stringify(
|
|
81
|
+
const fill = () => Promise.all([getPortfolio(S.ws), getBoards(S.ws)]).then(([d, boards]) => {
|
|
82
|
+
if (!d.ok) return;
|
|
83
|
+
const g = JSON.stringify([d.last, d.reports, d.waiting, boards.length]);
|
|
257
84
|
if (g === sig) return; sig = g;
|
|
258
|
-
put(host,
|
|
85
|
+
put(host, cardOf(d, { ws: S.ws, boardCount: boards.length, showReports: () => import('./c3-brief.js').then((m) => { if (m.hooks && m.hooks.go) m.hooks.go({ action: 'gorep' }); }) }));
|
|
259
86
|
}, () => {});
|
|
260
87
|
onBriefing(() => { fill(); });
|
|
88
|
+
const off = onScanChange(() => { if (!host.isConnected) { off(); return; } fill(); });
|
|
261
89
|
fill();
|
|
262
90
|
return host;
|
|
263
91
|
}
|
|
@@ -11,6 +11,7 @@ export function toast(arg, undo) {
|
|
|
11
11
|
if (!host || !host.isConnected) { host = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'toast' }); document.body.appendChild(host); }
|
|
12
12
|
const t = el('div', { class: 'toast' + (o.tone === 'error' ? ' err' : ''), role: 'status', 'aria-live': 'polite', 'data-c3-toast': '' }, el('span', null, o.text));
|
|
13
13
|
if (o.onAction) t.append(el('button', { type: 'button', 'data-c3-toast-action': '', onclick: () => { clearTimeout(timer); t.remove(); o.onAction(); } }, o.actionLabel || 'Undo'));
|
|
14
|
+
if (host.nextSibling) document.body.appendChild(host); // a sheet opened after the first toast sits above it (same z-index, later in the page): keep the toast last, so its Undo can be pressed
|
|
14
15
|
host.replaceChildren(t);
|
|
15
16
|
clearTimeout(timer); timer = setTimeout(() => t.remove(), o.timeout || 4200);
|
|
16
17
|
}
|
|
@@ -1583,6 +1583,7 @@
|
|
|
1583
1583
|
.c3 .raw{background:var(--muted);border-radius:8px;padding:var(--sp-2) var(--sp-3);font:var(--fs-1)/1.6 var(--mono);color:var(--muted-foreground);overflow-wrap:anywhere;white-space:pre-wrap}
|
|
1584
1584
|
.c3 .c3t-card .draft{background:var(--muted);border-radius:10px;padding:var(--sp-3);font-size:var(--fs-3);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
1585
1585
|
.c3 .c3t-tray{display:flex;gap:var(--sp-2);flex-wrap:wrap;align-items:center}
|
|
1586
|
+
.c3 .c3t-grow{flex:1}
|
|
1586
1587
|
.c3 .c3t-tray input,.c3 .c3t-panel input{flex:1;min-width:120px;border:1px solid var(--border);border-radius:99px;padding:6px 12px;background:var(--card);color:var(--foreground);font:inherit;font-size:var(--fs-2)}
|
|
1587
1588
|
.c3 .c3t-tray input[readonly],.c3 .c3t-panel input[readonly]{opacity:.6;cursor:not-allowed}
|
|
1588
1589
|
.c3 .c3t-card .pill.rec{background:var(--acc);color:var(--on-accent);border-color:var(--acc)}
|
|
@@ -1884,6 +1885,8 @@
|
|
|
1884
1885
|
.c3 .wk-agent-ask input{flex:1;min-width:0;font:inherit;font-size:var(--fs-2);padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:6px}
|
|
1885
1886
|
.c3 .wk-agent-ask[hidden]{display:none}
|
|
1886
1887
|
.c3 .wk-agent-sheet{display:grid;gap:var(--sp-3)}
|
|
1888
|
+
.c3 .wk-agent-st{display:inline-flex;align-items:center;gap:4px;max-width:100%;min-width:0}
|
|
1889
|
+
.c3 .wk-agent-tx{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1887
1890
|
.c3 .wk-agent-will{font-size:var(--fs-2)}
|
|
1888
1891
|
.c3 .wk-agent-sum{margin:0}
|
|
1889
1892
|
.c3 .wk-agent-body{--tc:var(--acc)}
|
|
@@ -707,14 +707,15 @@ const minsAgo = (iso) => { const t = Date.parse(iso); if (!Number.isFinite(t)) r
|
|
|
707
707
|
const whereOf = (item) => (item.origin === 'jira' ? 'Jira' : 'its source');
|
|
708
708
|
const AG = {
|
|
709
709
|
can: (t) => 'Agent can: ' + t, queued: 'Agent: queued', working: (ago) => 'Agent: working' + (ago ? ' · started ' + ago : ''), ready: 'Agent: draft ready',
|
|
710
|
-
sending: 'Agent: posting', sent: (w, at) => 'Posted to ' + w + (at ? ' ' + at : ''), failed: (
|
|
710
|
+
sending: 'Agent: posting', sent: (w, at) => 'Posted to ' + w + (at ? ' ' + at : ''), failed: (asks) => (asks ? 'Agent: needs you' : 'Agent stopped'),
|
|
711
711
|
sitSuggest: (w) => `The agent can draft this for you. You read it here first. Nothing goes to ${w} until you post it.`,
|
|
712
712
|
sitQueued: 'Queued. The agent starts as soon as a session is free. You can close this page.',
|
|
713
713
|
sitWorking: 'The agent is working on it. You can close this page: the item keeps its state.',
|
|
714
714
|
sitReady: (at, w) => `Draft ready${at ? ' at ' + at : ''}. Nothing went to ${w}.`,
|
|
715
715
|
sitSending: (k) => `Posting to ${k}. A session does exactly the step you approved and nothing else.`,
|
|
716
716
|
sitSent: (at, w) => `You posted it to ${w}${at ? ' at ' + at : ''}.`,
|
|
717
|
-
sitFailed: (why) => `The agent stopped${why ? ': ' + why : '.'} Nothing was sent
|
|
717
|
+
sitFailed: (why, asks) => (asks && why ? why.charAt(0).toUpperCase() + why.slice(1) + ' Nothing was sent.' : `The agent stopped${why ? ': ' + why : '.'} Nothing was sent.`),
|
|
718
|
+
openSession: 'Open the session',
|
|
718
719
|
willDo: 'What the agent will do: ', change: 'Change what the agent does', askLabel: 'What the agent will do',
|
|
719
720
|
draftIt: 'Let the agent draft this', tryAgain: 'Try again', post: (k) => 'Post it to ' + k, back: 'Send it back with a note', keepDraft: 'Keep it as a draft',
|
|
720
721
|
noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
|
|
@@ -725,14 +726,14 @@ const AG = {
|
|
|
725
726
|
|
|
726
727
|
function agentChip(item) {
|
|
727
728
|
const a = item.agent; if (!a || item.mock_row) return null;
|
|
728
|
-
const st = (cls, ic, text, spin) => el('span', { class: cls, 'data-pm-agent-state': a.state, 'data-pm-suggest': item.id }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), text);
|
|
729
|
+
const st = (cls, ic, text, spin) => el('span', { class: cls + ' wk-agent-st', 'data-pm-agent-state': a.state, 'data-pm-suggest': item.id, title: text }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), el('span', { class: 'wk-agent-tx' }, text));
|
|
729
730
|
if (a.state === 'suggest') return st('chip2', 'spark', AG.can(a.instruction || a.suggest || ''));
|
|
730
731
|
if (a.state === 'queued') return st('st', 'clock', AG.queued);
|
|
731
732
|
if (a.state === 'working') return st('st c3strip', 'spark', AG.working(minsAgo(a.started_at)), true);
|
|
732
733
|
if (a.state === 'ready') return st('st ok', 'check', AG.ready);
|
|
733
734
|
if (a.state === 'sending') return st('st c3strip', 'out', AG.sending, true);
|
|
734
735
|
if (a.state === 'sent') return st('st ok', 'out', AG.sent(whereOf(item), a.sent && a.sent.at ? hhmm(a.sent.at) : ''));
|
|
735
|
-
if (a.state === 'failed') return st('tag warn', 'alert', AG.failed(a.
|
|
736
|
+
if (a.state === 'failed') return st('tag warn', 'alert', AG.failed(a.asks));
|
|
736
737
|
return null;
|
|
737
738
|
}
|
|
738
739
|
|
|
@@ -755,7 +756,8 @@ function agentCase(inst, item, L) {
|
|
|
755
756
|
const strip = (spin, ic, text) => el('div', { class: 'c3ag', 'data-pm-agent-strip': item.id }, el('div', { class: 'fold a', role: 'status' }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), ' ' + text + ' ',
|
|
756
757
|
c3btn('chip2 lk', { 'data-pm-agent-stop': item.id }, [c3ic('stop', 'sm'), AG.stop], () => agentPress(inst, '/api/my-work/stop', { item: item.id }, AG.stopped))));
|
|
757
758
|
if (a.state === 'suggest' || a.state === 'failed') {
|
|
758
|
-
sit = a.state === 'failed' ? AG.sitFailed(a.error) : AG.sitSuggest(w);
|
|
759
|
+
sit = a.state === 'failed' ? AG.sitFailed(a.error, a.asks) : AG.sitSuggest(w);
|
|
760
|
+
if (a.state === 'failed' && a.session) extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-session': a.session }, [c3ic('term', 'sm'), AG.openSession], () => openAgentSession(a.session, item))));
|
|
759
761
|
const brief = () => agentPress(inst, '/api/my-work/act', { action: 'brief', item: item.id, instruction: ask(), ws: inst.ctx.ws }, AG.handed(w));
|
|
760
762
|
const retry = () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'retry' }, AG.retried);
|
|
761
763
|
opts = [o('agent', a.state === 'failed' ? AG.tryAgain : AG.draftIt, [fx('spark', 'Drafts it here in PMOS', 'ok'), fx('lock', 'Nothing goes to ' + w, 'ok')], a.state === 'failed' ? retry : brief), ...own];
|
|
@@ -805,6 +807,10 @@ function agentCase(inst, item, L) {
|
|
|
805
807
|
return { model, run };
|
|
806
808
|
}
|
|
807
809
|
|
|
810
|
+
function openAgentSession(id, item) {
|
|
811
|
+
try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id, name: 'Job · ' + (item.key || item.title || '') } })); } catch { /* no event */ }
|
|
812
|
+
}
|
|
813
|
+
|
|
808
814
|
function openDraft(item) {
|
|
809
815
|
const a = item.agent || {}; const d = a.draft || {};
|
|
810
816
|
const body = el('div', { class: 'wk-agent-sheet' },
|
|
@@ -103,6 +103,11 @@ function pausedBox(ctx, card) {
|
|
|
103
103
|
}
|
|
104
104
|
// a session that has not started may be at the CLI's trust-the-folder question, where a typed Enter would answer it
|
|
105
105
|
const NOSTART = 'Open the session to see what it waits for: typing into it now could answer a question you have not read.';
|
|
106
|
+
function flipBtn(ui, ctx) {
|
|
107
|
+
const b = button({ variant: 'ghost', onClick: () => { ui.flip = true; ctx.redraw(); }, attrs: { class: 'ib', 'aria-label': 'Flip to the case file', title: 'Case file' } });
|
|
108
|
+
b.replaceChildren(ic('flip', 'sm'));
|
|
109
|
+
return b;
|
|
110
|
+
}
|
|
106
111
|
export function openBtn(ctx) {
|
|
107
112
|
const b = button({ label: 'Open the session', onClick: () => ctx.openSession && ctx.openSession(ctx.id), attrs: { class: 'pill c3t-open', 'data-open-session': '', title: 'Open the session: its real terminal, on this page' } });
|
|
108
113
|
b.prepend(ic('term', 'sm'));
|
|
@@ -210,10 +215,12 @@ function draw(host, ctx) {
|
|
|
210
215
|
else {
|
|
211
216
|
kids.push(el('h3', null, done && card.result.headline ? card.result.headline : (ctx.name || card.title)));
|
|
212
217
|
const strip = stripNode(card, ctx.observed); if (strip) kids.push(strip);
|
|
218
|
+
// every box but the question and the permission carries Open the session; a card with none of them gets its own below
|
|
219
|
+
let opened = false;
|
|
213
220
|
if (perms.length) perms.forEach((p) => kids.push(permissionBox(ctx, p)));
|
|
214
221
|
else if (card.state === 'asking' && card.ask) kids.push(questionBox(ctx, card));
|
|
215
|
-
else if (card.state === 'paused') kids.push(pausedBox(ctx, card));
|
|
216
|
-
else if (ui.hidden !== card.state + ':' + ((card.failure && card.failure.code) || '')) { const f = failBox(ctx, card, ui); if (f) kids.push(f); }
|
|
222
|
+
else if (card.state === 'paused') { kids.push(pausedBox(ctx, card)); opened = true; }
|
|
223
|
+
else if (ui.hidden !== card.state + ':' + ((card.failure && card.failure.code) || '')) { const f = failBox(ctx, card, ui); if (f) { kids.push(f); opened = true; } }
|
|
217
224
|
if (card.state === 'permission' && !perms.length) kids.push(dim('Waiting for your permission. Open the session to see it.'));
|
|
218
225
|
if (done) {
|
|
219
226
|
kids.push(...resultNodes(card.result, { ui, redraw: ctx.redraw, fileBody: ctx.fileBody }).filter(Boolean));
|
|
@@ -225,7 +232,9 @@ function draw(host, ctx) {
|
|
|
225
232
|
if (ui.voice) kids.push(voiceNode(ctx, ui));
|
|
226
233
|
else if (ui.tray) kids.push(trayNode(ctx, card, ui));
|
|
227
234
|
if (!done && !ui.tray && !ui.voice && !perms.length && card.state !== 'asking' && !['working', 'queued'].includes(card.state)) { /* a failure box carries its own moves */ }
|
|
228
|
-
|
|
235
|
+
// a starting or working run shows Open the session on its face: a read-only card (a job PMOS started) has no case file to flip to
|
|
236
|
+
if (!done && (!ctx.readonly || (!opened && ctx.openSession && !ctx.ended))) kids.push(el('div', { class: 'c3t-tray' }, !opened && ctx.openSession && !ctx.ended ? openBtn(ctx) : null, el('span', { class: 'c3t-grow' }),
|
|
237
|
+
ctx.readonly ? null : flipBtn(ui, ctx)));
|
|
229
238
|
}
|
|
230
239
|
const art = el('article', { class: 'c3t-card' + (['working', 'queued'].includes(card.state) ? ' busy' : '') + (ui.flash ? ' flash' : ''), 'data-run-card': card.state || '', 'data-run-id': ctx.id, 'aria-label': ctx.name || ctx.id }, ...kids);
|
|
231
240
|
host.replaceChildren(el('div', { class: 'c3 c3e' }, art));
|
|
@@ -39,7 +39,7 @@ function outcomeOf(card, meta) {
|
|
|
39
39
|
function cardHost(st, s, card, extra) {
|
|
40
40
|
const host = el('div', { 'data-run-host': s.id });
|
|
41
41
|
const ctx = { id: s.id, name: s.name, card, observed: st.data.demo && st.data.demo.observed ? st.data.demo.observed[s.id] : null, origin: s.origin === 'manual' ? (s.cardId ? 'on a card' : 'started by you') : s.origin, src: st.src,
|
|
42
|
-
refresh: st.refresh, openSession: st.openSession, glossary: st.data.demo && st.data.demo.glossary, fileBody: st.data.demo && st.data.demo.fileBody, accepted: false, ...(extra || {}) };
|
|
42
|
+
refresh: st.refresh, openSession: st.openSession, ended: s.endedAt != null, glossary: st.data.demo && st.data.demo.glossary, fileBody: st.data.demo && st.data.demo.fileBody, accepted: false, ...(extra || {}) };
|
|
43
43
|
mountRunCard(host, ctx);
|
|
44
44
|
return host;
|
|
45
45
|
}
|
|
@@ -172,7 +172,7 @@ function draw(host, st) {
|
|
|
172
172
|
const running = auto.filter((s) => s.endedAt == null).length;
|
|
173
173
|
const sw = el('button', { type: 'button', class: 'c3t-sw', role: 'switch', 'aria-checked': String(st.showAuto), 'aria-label': 'Show automatic sessions', 'data-sessions-auto': '', onclick: () => { st.showAuto = !st.showAuto; st.redraw(); } }, el('i'));
|
|
174
174
|
root.append(section('bolt', 'Automatic', auto.length, el('span', { class: 'chips2' }, dim(auto.length + ' today' + (running ? ', ' + running + ' running' : '')), sw), st.showAuto
|
|
175
|
-
? (auto.length ? el('ul', { class: 'list', 'data-c3t': 'auto' }, auto.map((s) => { const c = cards[s.id]; return row(st, s, c, { sub: s.origin + ' · ' + (s.endedAt == null ? 'running' : 'ended') + ' · ' + ago(s.startedAt) + ' · ' + outcomeOf(c), body: c ? [cardHost(st, s, c, { readonly: true })] : [dim('No card yet.')] }); })) : none('None.'))
|
|
175
|
+
? (auto.length ? el('ul', { class: 'list', 'data-c3t': 'auto' }, auto.map((s) => { const c = cards[s.id]; return row(st, s, c, { sub: s.origin + ' · ' + (s.endedAt == null ? 'running' : 'ended') + ' · ' + ago(s.startedAt) + ' · ' + outcomeOf(c), body: c ? [cardHost(st, s, c, { readonly: true })] : [dim('No card yet.'), s.endedAt == null ? el('div', { class: 'c3t-tray' }, openBtn({ id: s.id, openSession: st.openSession })) : null].filter(Boolean) }); })) : none('None.'))
|
|
176
176
|
: el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Hidden. They run, write their result onto a card and clean up. Switch on to see them.')));
|
|
177
177
|
host.append(root);
|
|
178
178
|
}
|
package/workspace/server.js
CHANGED
|
@@ -2551,7 +2551,7 @@ const driver = driverRunLib.create({
|
|
|
2551
2551
|
warn: (what, e) => warnWriteFail(what, e),
|
|
2552
2552
|
});
|
|
2553
2553
|
const driverTick = () => { try { driver.tick({}); } catch (e) { warnWriteFail('driver tick', e); } };
|
|
2554
|
-
// horizon-scan: a scan is two headless runs
|
|
2554
|
+
// horizon-scan: a scan is two headless runs for the whole portfolio (web, then repository), at most one scan at a time, on the workspace cadence inside the working hours.
|
|
2555
2555
|
// Everything it decides is in lib/scan.js and lib/scan-run.js; this only hands it the server's parts. The web run gets an empty scratch folder to run in.
|
|
2556
2556
|
const scanRoutesCtx = () => ({ getWs, stateDirFor, workspaceList: () => [...workspaces.values()], runtimeDir: STATE, selfRoot: EMBEDDED_SELF ? SELF_ROOT : null });
|
|
2557
2557
|
const scanner = scanRunLib.create({
|
|
@@ -2568,7 +2568,8 @@ const scanner = scanRunLib.create({
|
|
|
2568
2568
|
return g.ok ? g.facts.initiatives.filter((i) => i.active && i.board === boardId).map((i) => ({ id: i.id, title: i.title || i.id })) : [];
|
|
2569
2569
|
},
|
|
2570
2570
|
knownInitiative: (w, id) => { try { const r = require('./lib/initiative-rows.js').peek(w.root); return r && r.ok ? r.rows.some((x) => x.id === id) : null; } catch { return null; } },
|
|
2571
|
-
|
|
2571
|
+
// The report documents go in the report-workspace store (lib/report-needs.js reportsDir), where the Documents list reads them.
|
|
2572
|
+
reportsFor: (w) => { const f = require('./lib/scan.js').reportsFolder(w.root); const d = stateDirFor(w); return f || (d ? { abs: path.join(d, 'reports'), rel: path.relative(w.root, path.join(d, 'reports')).split(path.sep).join('/') } : null); },
|
|
2572
2573
|
promptFor: (skillId) => { const h = homeWorkspace(); const t = taskById(skillId, h ? h.root : SELF_ROOT); return t && t.prompt ? t.prompt : null; },
|
|
2573
2574
|
spawn: (spec) => spawnSession({ name: spec.name, agent: 'claude', ws: spec.ws, origin: spec.origin, initialInput: spec.initialInput, scratch: !!spec.scratch, extraEnv: spec.env, autoDelete: undefined, onDone: spec.onDone }),
|
|
2574
2575
|
stop: (s) => { if (s && s.child) stopHeadless(s); },
|
|
@@ -3577,7 +3578,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3577
3578
|
let workdir = cwd && insideRoot(root, path.resolve(root, cwd)) ? path.resolve(root, cwd) : root;
|
|
3578
3579
|
// horizon-scan: the web run reads untrusted pages, so it runs in an empty scratch folder and never in the repository. Only a headless run may ask for it.
|
|
3579
3580
|
if (scratch) {
|
|
3580
|
-
if (!(initialInput && headlessLib.kindFor({ agent, origin: tag })
|
|
3581
|
+
if (!(initialInput && headlessLib.WEB_KINDS.includes(headlessLib.kindFor({ agent, origin: tag })))) return exitedSession({ id, ws: w ? w.id : wsId(SELF_ROOT), root, name: (name && String(name).trim()) || agent, agent, origin: tag, profile: null, profileRefusal: null, plan_item: null, token: sessionToken }, root, 'a scratch folder is only for a background web scan run');
|
|
3581
3582
|
try { workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'pmos-scan-web-')); } catch (e) { return exitedSession({ id, ws: w ? w.id : wsId(SELF_ROOT), root, name: (name && String(name).trim()) || agent, agent, origin: tag, profile: null, profileRefusal: null, plan_item: null, token: sessionToken }, root, 'could not make a scratch folder: ' + String((e && e.code) || e)); }
|
|
3582
3583
|
}
|
|
3583
3584
|
// A repository folder that was moved or deleted after it was registered: tmux would start the agent in its own folder
|
|
@@ -4135,10 +4136,27 @@ function stallQuietTasks(run, now) {
|
|
|
4135
4136
|
if ((t.status !== 'working' && t.status !== 'sending') || !t.session_id) continue;
|
|
4136
4137
|
const s = sessions.get(t.session_id);
|
|
4137
4138
|
if (!s || s.state === 'exited') continue;
|
|
4138
|
-
if (now - Math.max(s.lastOutputAt || 0, Date.parse(t.started_at || 0) || 0)
|
|
4139
|
+
if (now - Math.max(s.lastOutputAt || 0, Date.parse(t.started_at || 0) || 0) < TASK_IDLE_MS) continue;
|
|
4140
|
+
const q = taskStallReason(s);
|
|
4141
|
+
run = dayRunLib.sessionStalled(run, t.session_id, q.why, now, { asks: q.asks }).run;
|
|
4139
4142
|
}
|
|
4140
4143
|
return run;
|
|
4141
4144
|
}
|
|
4145
|
+
// Why a quiet task session is quiet, from its transcript: a tool call that waits on the PM's OK, a turn that ended with
|
|
4146
|
+
// a question for the PM, or (no transcript answer) only that it printed nothing. The session stays open in every case.
|
|
4147
|
+
function taskStallReason(s) {
|
|
4148
|
+
let hx = null; try { hx = transcriptHarness(s); } catch { hx = null; }
|
|
4149
|
+
const shown = sessionState.presented('quiet', hx && hx.turn);
|
|
4150
|
+
const said = String(((harnessOf(s) || {}).lastWords) || '').replace(/\s+/g, ' ').trim().slice(0, 140);
|
|
4151
|
+
if (shown === 'blocked') {
|
|
4152
|
+
const verb = hx && hx.openCall && hx.openCall.verb ? String(hx.openCall.verb).slice(0, 60) : '';
|
|
4153
|
+
return { asks: 'ok', why: `the agent waits for your OK${verb ? ' to ' + verb : ''} in its session. Open the session and answer it: the job carries on from there.` };
|
|
4154
|
+
}
|
|
4155
|
+
if (shown === 'needs') return { asks: 'answer', why: `the agent stopped to ask you something in its session${said ? ': "' + said + '"' : ''}. Open the session and answer it: the job carries on from there.` };
|
|
4156
|
+
// no transcript at all: the agent never began its turn, which is what a CLI held at its trust-the-folder or sign-in screen looks like
|
|
4157
|
+
if (hx && hx.missing && hx.missing.all && s.agent === 'claude') return { asks: 'start', why: 'the session has printed nothing for five minutes and shows no sign that it began the job: Claude Code may be waiting for you to trust the folder or to sign in. Open the session to see it.' };
|
|
4158
|
+
return { asks: null, why: TASK_STALLED_WHY };
|
|
4159
|
+
}
|
|
4142
4160
|
// The skill's own body is the prompt, as fireSchedule hands it over; the line after it names this run's task and mode.
|
|
4143
4161
|
function daySkillPrompt(run, skillId, wsId) {
|
|
4144
4162
|
const w = getWs(wsId || run.ws);
|
|
@@ -9002,7 +9020,9 @@ async function handleRequest(req, res) {
|
|
|
9002
9020
|
// workspace-data-freshness: the source table, the facts rebuild and Refresh now's skill sessions
|
|
9003
9021
|
freshness, factsRefresh: (root) => factsAsync.refresh(root, { force: true }), startSkill: startSkillSession,
|
|
9004
9022
|
// c3-initiative-boards: the PMOS checkout this server is embedded in, whose default board keeps its derived ladder
|
|
9005
|
-
selfRoot: EMBEDDED_SELF ? SELF_ROOT : null
|
|
9023
|
+
selfRoot: EMBEDDED_SELF ? SELF_ROOT : null,
|
|
9024
|
+
// horizon-scan-portfolio: Scan now, Stop and an approved research proposal start and end runs in the scanner; scanClock fixes its clock in the suites
|
|
9025
|
+
scanner, scanClock })) return;
|
|
9006
9026
|
res.writeHead(404); res.end('not found');
|
|
9007
9027
|
}
|
|
9008
9028
|
|