create-pmos 0.168.0 → 0.170.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-driver-fixture.js +61 -0
- package/workspace/c3-driver-test.js +164 -0
- package/workspace/c3-scan-fixture.js +78 -0
- package/workspace/c3-scan-test.js +170 -0
- package/workspace/guide/horizon-scan.md +73 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-driver.md +12 -0
- package/workspace/lib/engine/boards.js +8 -2
- package/workspace/lib/headless.js +21 -3
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-boards.js +30 -1
- package/workspace/lib/routes-driver.js +38 -6
- package/workspace/lib/routes-scan.js +130 -0
- package/workspace/lib/scan-cli.js +226 -0
- package/workspace/lib/scan-run.js +186 -0
- package/workspace/lib/scan.js +396 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-brief-needs.js +19 -3
- package/workspace/public/pm/c3-driver-work.js +110 -0
- package/workspace/public/pm/c3-driver.css +18 -0
- package/workspace/public/pm/c3-driver.js +298 -0
- package/workspace/public/pm/c3-ib-triage.js +6 -2
- package/workspace/public/pm/c3-ii-tabs.js +3 -2
- package/workspace/public/pm/c3-ini-ui.js +4 -2
- package/workspace/public/pm/c3-initiatives.js +21 -0
- package/workspace/public/pm/c3-scan.css +25 -0
- package/workspace/public/pm/c3-scan.js +263 -0
- package/workspace/public/pm/c3-shell.js +3 -2
- package/workspace/public/pm/pm-work-view.js +12 -2
- package/workspace/server.js +40 -4
- package/workspace/skills/horizon-scan-repo.skill +56 -0
- package/workspace/skills/horizon-scan-web.skill +43 -0
- package/workspace/test-files.json +8 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/* pm/c3-driver.js — the Initiative Driver on console3 (initiative initiative-driver, PRD section 5; mockup mockup-c3-driver, 39-driver.js).
|
|
2
|
+
* The agent works on every initiative and Work task on its own and files each result as a PROPOSAL. Nothing it writes changes a record
|
|
3
|
+
* or leaves PMOS: the PM keeps, sends back with a note, or drops. This file draws the three places the PM sees it:
|
|
4
|
+
*
|
|
5
|
+
* driverStrip(ctx, row) the Agent strip on an initiative's Overview: status, last action, blocker, one row per open proposal
|
|
6
|
+
* agentSetting(state, { onPick }) the Agent setting (Off, Shadow, Approve) in the Board settings sheet; loadAgentMode(api, board) reads it
|
|
7
|
+
* workNote(o) · openDraftSheet(o) the one agent line under a Work task and the sheet behind its Review (c3-driver-work.js draws them)
|
|
8
|
+
*
|
|
9
|
+
* Reads: GET /api/driver/strip, /api/driver/mode, /api/driver/open. Presses: POST /api/driver/keep, return, drop, pause, resume, mode,
|
|
10
|
+
* through http.js (the nonce and the page Origin). The page sends no `by`: the server takes the PM from the call path. After each press
|
|
11
|
+
* the strip is read again. Keep, send back and drop have no Undo: each adds a line to the ledger, and nothing is removed.
|
|
12
|
+
* A proposal that was just decided stays in its strip, marked, until the page is left. Plain words for every empty and error state.
|
|
13
|
+
*/
|
|
14
|
+
import { el } from './dom.js';
|
|
15
|
+
import { icon, pbtn, lk, chip, tag, st, note, lbl, chgl, list, nrow, sect, put, btn, plural } from './c3-ini-ui.js';
|
|
16
|
+
import { toast } from './c3-toast.js';
|
|
17
|
+
import { pmGet } from './http.js';
|
|
18
|
+
|
|
19
|
+
/** Link c3-driver.css once, from this module's own folder (the way c3-initiatives.js links its stylesheet). */
|
|
20
|
+
export function ensureDriverCss() {
|
|
21
|
+
if (typeof document === 'undefined' || document.querySelector('link[data-c3-drv]')) return;
|
|
22
|
+
const l = document.createElement('link');
|
|
23
|
+
l.rel = 'stylesheet'; l.href = new URL('./c3-driver.css', import.meta.url).href; l.setAttribute('data-c3-drv', '');
|
|
24
|
+
document.head.appendChild(l);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* ── words ── */
|
|
28
|
+
export const WHAT = {
|
|
29
|
+
problem: ['Problem statement', 'doc'], evidence: ['Evidence', 'doc'], sizing: ['Opportunity sizing', 'target'], assumption: ['Riskiest assumption and spike brief', 'bolt'],
|
|
30
|
+
stakeholders: ['Stakeholders', 'user'], move: ['Move', 'next'], note: ['Note', 'notes'], 'next-step': ['Next step', 'check'], 'follow-up': ['Follow-up', 'mail'],
|
|
31
|
+
};
|
|
32
|
+
export const MODES = [
|
|
33
|
+
['off', 'Off', 'The agent does nothing on this board.'],
|
|
34
|
+
['shadow', 'Shadow', 'The agent drafts but raises nothing. You see its drafts only on the initiative page.'],
|
|
35
|
+
['approve', 'Approve', 'The agent drafts and asks you. Each draft waits for Keep, Send back or Drop.'],
|
|
36
|
+
];
|
|
37
|
+
const errText = (r, what) => what + ': ' + ((r && r.error) || 'the workspace did not answer') + '.';
|
|
38
|
+
const isUrl = (s) => /^https?:\/\/\S+$/i.test(String(s || ''));
|
|
39
|
+
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
40
|
+
const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
41
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
42
|
+
const clock = (d) => p2(d.getHours()) + ':' + p2(d.getMinutes());
|
|
43
|
+
/** '11:05' for today, 'Fri 9 Oct, 16:05' for any other day. */
|
|
44
|
+
export function whenLabel(iso, now = new Date()) {
|
|
45
|
+
const d = new Date(iso);
|
|
46
|
+
if (!Number.isFinite(d.getTime())) return '';
|
|
47
|
+
return d.toDateString() === now.toDateString() ? clock(d) : WD[d.getDay()] + ' ' + d.getDate() + ' ' + MON[d.getMonth()] + ', ' + clock(d);
|
|
48
|
+
}
|
|
49
|
+
/** Whole days a time is old. */
|
|
50
|
+
export const daysOld = (iso, now = Date.now()) => { const t = Date.parse(iso); return Number.isFinite(t) ? Math.floor((now - t) / 864e5) : 0; };
|
|
51
|
+
/** 'Thu 15 Oct' for a YYYY-MM-DD. */
|
|
52
|
+
export function dueLabel(ymd) {
|
|
53
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(ymd || ''));
|
|
54
|
+
if (!m) return '';
|
|
55
|
+
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
56
|
+
return WD[d.getDay()] + ' ' + d.getDate() + ' ' + MON[d.getMonth()];
|
|
57
|
+
}
|
|
58
|
+
const textOf = (p) => (p && p.data && typeof p.data.text === 'string' ? p.data.text : '');
|
|
59
|
+
export const titleOf = (p) => (p.what === 'move' && p.data && p.data.to ? 'Move to ' + p.data.to : (WHAT[p.what] || [p.what])[0]);
|
|
60
|
+
|
|
61
|
+
/** The text a proposal shows when it opens: the draft, then the agent's one-line reason. */
|
|
62
|
+
export function bodyOf(p) {
|
|
63
|
+
const out = [];
|
|
64
|
+
const t = textOf(p);
|
|
65
|
+
if (p.what === 'move') out.push('Move this initiative to ' + ((p.data && p.data.to) || 'the next stage') + '.');
|
|
66
|
+
else if (t) out.push(t);
|
|
67
|
+
else if (p.data && Array.isArray(p.data.items)) out.push(plural(p.data.items.length, 'item') + ' with a pointer each.');
|
|
68
|
+
else if (p.data && typeof p.data === 'object' && Object.keys(p.data).length) out.push(Object.entries(p.data).map(([k, v]) => k + ': ' + (typeof v === 'string' ? v : JSON.stringify(v))).join('\n'));
|
|
69
|
+
if (p.what === 'next-step' && p.data && p.data.due) out.push('Due: ' + dueLabel(p.data.due) + '.');
|
|
70
|
+
if (p.why) out.push('Why: ' + p.why);
|
|
71
|
+
if (p.what === 'move') out.push('Keep moves the stage. The agent only proposes.');
|
|
72
|
+
return out.join('\n');
|
|
73
|
+
}
|
|
74
|
+
/** The sources of a proposal: its own list, then the items of an evidence draft that carry a pointer. */
|
|
75
|
+
export function sourcesOf(p) {
|
|
76
|
+
const out = (p.sources || []).map((s) => ({ label: s.label, ref: s.ref, text: '' }));
|
|
77
|
+
if (p.data && Array.isArray(p.data.items)) for (const i of p.data.items) if (i && i.ref) out.push({ label: 'Evidence', ref: String(i.ref), text: String(i.text || '') });
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
const srcIcon = (label) => (/jira/i.test(label) ? 'jira' : /slack|mail|email|thread/i.test(label) ? 'mail' : 'doc');
|
|
81
|
+
function srcRow(s) {
|
|
82
|
+
const ref = isUrl(s.ref) ? el('a', { class: 'mono drv-ref', href: s.ref, target: '_blank', rel: 'noopener noreferrer' }, s.ref) : el('span', { class: 'mono drv-ref' }, s.ref);
|
|
83
|
+
return el('li', { 'data-drv-src': '' }, icon(srcIcon(s.label), 'sm'), el('span', { class: 'drv-src' }, el('b', null, s.label), ref, s.text ? el('span', null, s.text) : null));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ── what the PM decided this visit: kept and sent-back rows stay in the strip, marked ── */
|
|
87
|
+
const DECIDED = new Map(); // target key -> Map(proposal id -> { st: 'kept' | 'back', note, p })
|
|
88
|
+
const decidedOf = (key) => { if (!DECIDED.has(key)) DECIDED.set(key, new Map()); return DECIDED.get(key); };
|
|
89
|
+
|
|
90
|
+
/** One press on the driver. Returns the server's answer; an error is a toast and null. */
|
|
91
|
+
async function press(ctx, route, body, what) {
|
|
92
|
+
const r = await ctx.api.post('/api/driver/' + route, body);
|
|
93
|
+
if (!r || !r.ok) { toast({ text: errText(r, what + ' did not work'), tone: 'error', timeout: 6000 }); return null; }
|
|
94
|
+
return r;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The sheet that asks for a one-line note and sends a proposal back with it. */
|
|
98
|
+
export function openSendBack(ctx, p, onSent) {
|
|
99
|
+
const input = el('input', { type: 'text', 'data-drv': 'note', placeholder: 'Tell the agent what is wrong or missing', 'aria-label': 'Note to the agent', maxlength: '500', autocomplete: 'off' });
|
|
100
|
+
const err = el('div', { class: 'fold w', role: 'alert', hidden: true, 'data-drv': 'note-error' });
|
|
101
|
+
let busy = false;
|
|
102
|
+
const send = async () => {
|
|
103
|
+
const n = input.value.trim();
|
|
104
|
+
if (!n) { err.hidden = false; put(err, icon('alert', 'sm'), 'Write one line for the agent. A note starts its next run.'); input.focus(); return; }
|
|
105
|
+
if (busy) return; busy = true;
|
|
106
|
+
const r = await press(ctx, 'return', { proposal: p.id, note: n }, 'Send back');
|
|
107
|
+
busy = false;
|
|
108
|
+
if (!r) return;
|
|
109
|
+
sh.close();
|
|
110
|
+
toast('Sent back with your note. The agent redrafts it.');
|
|
111
|
+
onSent(n);
|
|
112
|
+
};
|
|
113
|
+
input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); send(); } });
|
|
114
|
+
const sh = ctx.openSheet({ title: 'Send back: ' + titleOf(p), icon: 'reply', body: el('div', { class: 'drv-sheet' },
|
|
115
|
+
el('p', { class: 'drv-pre' }, bodyOf(p)), el('div', { class: 'form' }, input), err,
|
|
116
|
+
el('div', { class: 'iacts chips2' }, pbtn('primary', 'Send back with note', 'reply', { 'data-drv': 'send-back', onclick: send }), pbtn('ghost', 'Cancel', null, { onclick: () => sh.close() }))) });
|
|
117
|
+
setTimeout(() => input.focus(), 30);
|
|
118
|
+
return sh;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ── the Agent strip ── */
|
|
122
|
+
/** Plain words for the state of the agent on this initiative, from GET /api/driver/strip. */
|
|
123
|
+
export function statusOf(d, now = Date.now()) {
|
|
124
|
+
const open = d.proposals || [];
|
|
125
|
+
const used = (d.budget && d.budget.used) || 0; const limit = (d.budget && d.budget.limit) || 3;
|
|
126
|
+
const oldest = open.map((p) => daysOld(p.at, now)).reduce((a, b) => Math.max(a, b), 0);
|
|
127
|
+
let last = 'No run yet.';
|
|
128
|
+
if (d.last) last = d.last.ok ? 'Last: ' + (d.last.proposals ? 'filed ' + plural(d.last.proposals, 'proposal') : 'looked and found nothing new') + ' · ' + whenLabel(d.last.at, new Date(now))
|
|
129
|
+
: 'Last run did not finish' + (d.last.reason ? ': ' + d.last.reason : '') + ' · ' + whenLabel(d.last.at, new Date(now));
|
|
130
|
+
if (d.paused) return { ic: 'pause', b: 'Agent is paused', sub: 'It files nothing until you resume.', last };
|
|
131
|
+
if (d.running) return { ic: 'spark', b: 'Agent is working · started ' + whenLabel(d.running.since, new Date(now)), sub: 'It works inside the limits you set and files each result as a proposal.', last };
|
|
132
|
+
if (open.length) return { ic: 'wait', b: 'Agent is idle · waiting on you', sub: oldest >= 1 ? 'The oldest proposal is ' + plural(oldest, 'day') + ' old.' : plural(open.length, 'proposal') + ' wait for your decision.', last };
|
|
133
|
+
if (used >= limit) return { ic: 'clock', b: 'Agent is idle · used its runs for today', sub: 'It runs up to ' + limit + ' times a day on each initiative. It starts again tomorrow.', last };
|
|
134
|
+
return { ic: 'spark', b: 'Agent is idle', sub: 'It starts again when something changes: a stage move, a new initiative, or a note from you.', last };
|
|
135
|
+
}
|
|
136
|
+
/** The line that says what stops the agent, or ''. A blocker the PM owns names the PM. */
|
|
137
|
+
export function blockerLine(d) {
|
|
138
|
+
if (!d.blocker || !d.blocker.text) return '';
|
|
139
|
+
if (d.blocker.owner === 'pm') return 'Stopped by: you. ' + d.blocker.text + '. The agent starts no run until you decide.';
|
|
140
|
+
return 'Stopped by: ' + String(d.blocker.text).replace(/\.$/, '') + '.';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The Agent section of an initiative's Overview. Draws at once with "Reading the agent" and fills itself from the server.
|
|
145
|
+
* ctx: api, row (the initiative), board, refresh, openSheet, openBoardSettings.
|
|
146
|
+
*/
|
|
147
|
+
export function driverStrip(ctx, row) {
|
|
148
|
+
ensureDriverCss();
|
|
149
|
+
const wrap = el('div', { class: 'drv', 'data-drv': 'strip' });
|
|
150
|
+
const key = (ctx.ws || '') + ':initiative:' + row.id;
|
|
151
|
+
const S = { data: null, err: '', open: new Set(), busy: false };
|
|
152
|
+
|
|
153
|
+
async function load() {
|
|
154
|
+
const r = await ctx.api.get('/api/driver/strip', { target_kind: 'initiative', id: row.id, board: ctx.board && ctx.board.id });
|
|
155
|
+
if (!r || !r.ok) { S.data = null; S.err = errText(r, 'The agent strip could not be read'); } else { S.data = r; S.err = ''; }
|
|
156
|
+
draw();
|
|
157
|
+
}
|
|
158
|
+
const reread = load;
|
|
159
|
+
|
|
160
|
+
async function decide(route, p, extra) {
|
|
161
|
+
const r = await press(ctx, route, { proposal: p.id, ...(extra || {}) }, route === 'keep' ? 'Keep' : 'Drop');
|
|
162
|
+
return r;
|
|
163
|
+
}
|
|
164
|
+
async function keep(p) {
|
|
165
|
+
const r = await decide('keep', p);
|
|
166
|
+
if (!r) return;
|
|
167
|
+
decidedOf(key).set(p.id, { st: 'kept', p });
|
|
168
|
+
const w = (r.warnings || []).filter(Boolean);
|
|
169
|
+
toast(p.what === 'move' ? 'Kept. The stage moved to ' + ((p.data && p.data.to) || 'the next stage') + '.' + (w.length ? ' ' + w.join(' ') : '') : 'Kept: ' + titleOf(p));
|
|
170
|
+
if (['move', 'problem', 'next-step'].includes(r.wrote)) ctx.refresh(); // the stage chips and the record cards read the new record
|
|
171
|
+
else await reread();
|
|
172
|
+
}
|
|
173
|
+
async function drop(p) {
|
|
174
|
+
const r = await decide('drop', p);
|
|
175
|
+
if (!r) return;
|
|
176
|
+
S.open.delete(p.id);
|
|
177
|
+
toast('Dropped: ' + titleOf(p));
|
|
178
|
+
await reread();
|
|
179
|
+
}
|
|
180
|
+
const sendBack = (p) => openSendBack(ctx, p, (n) => { decidedOf(key).set(p.id, { st: 'back', note: n, p }); reread(); });
|
|
181
|
+
async function pause(on) {
|
|
182
|
+
const r = await press(ctx, on ? 'pause' : 'resume', { target_kind: 'initiative', id: row.id }, on ? 'Pause' : 'Resume');
|
|
183
|
+
if (!r) return;
|
|
184
|
+
toast(on ? 'Agent paused. It files nothing until you resume.' : 'Agent resumed.');
|
|
185
|
+
await reread();
|
|
186
|
+
}
|
|
187
|
+
function openRuns() {
|
|
188
|
+
const d = S.data; const runs = (d && d.today) || [];
|
|
189
|
+
const used = d && d.budget ? d.budget.used : runs.length; const limit = d && d.budget ? d.budget.limit : 3;
|
|
190
|
+
ctx.openSheet({ title: 'Agent runs today', icon: 'clock', body: el('div', { class: 'drv-sheet', 'data-drv': 'runs-sheet' },
|
|
191
|
+
note(row.title + ' · ' + used + ' of ' + limit + ' runs used today'),
|
|
192
|
+
runs.length ? chgl(runs.map((r) => el('li', { 'data-drv-run': r.run }, icon('clock', 'sm'),
|
|
193
|
+
el('span', null, el('b', null, whenLabel(r.start)), ' ', r.end ? (r.ok ? 'Run ended' : 'Run did not finish') : 'Running now'),
|
|
194
|
+
tag(r.end ? (r.ok ? (r.proposals ? 'Filed ' + plural(r.proposals, 'proposal') : 'Nothing new') : (r.reason || 'Failed')) : 'Running now'))))
|
|
195
|
+
: el('div', { class: 'fold' }, icon('info', 'sm'), 'The agent has not run on this initiative today.'),
|
|
196
|
+
el('div', { class: 'fold' }, icon('info', 'sm'), 'The agent runs up to ' + limit + ' times a day on each initiative. It stops at the limit and starts again tomorrow. A run that fails still counts.'),
|
|
197
|
+
el('div', { class: 'iacts chips2' }, pbtn('outline', 'Board settings', 'settings', { onclick: () => { closeAnd(() => ctx.openBoardSettings()); } }))) });
|
|
198
|
+
}
|
|
199
|
+
const closeAnd = (fn) => { import('./c3-sheet.js').then((m) => { m.closeSheet(); fn(); }); };
|
|
200
|
+
|
|
201
|
+
function propRow(p, state) {
|
|
202
|
+
const kept = state && state.st === 'kept'; const back = state && state.st === 'back';
|
|
203
|
+
const old = !state && daysOld(p.at) >= 1;
|
|
204
|
+
const srcs = sourcesOf(p);
|
|
205
|
+
const chips = [
|
|
206
|
+
tag(p.what === 'move' ? 'stage move' : 'draft'),
|
|
207
|
+
p.at ? chip(icon('clock', 'sm'), whenLabel(p.at)) : null,
|
|
208
|
+
srcs.length ? chip(icon('out', 'sm'), plural(srcs.length, 'source')) : null,
|
|
209
|
+
old ? el('span', { class: 'tag warn' }, 'waiting ' + plural(daysOld(p.at), 'day')) : null,
|
|
210
|
+
kept ? st('ok', 'check', 'Kept by you') : null,
|
|
211
|
+
back ? tag('Sent back · agent is redrafting') : null,
|
|
212
|
+
];
|
|
213
|
+
const xp = el('div', { class: 'ip' },
|
|
214
|
+
el('p', { class: 'drv-pre' }, bodyOf(p)),
|
|
215
|
+
back && state.note ? el('div', { class: 'fold' }, icon('reply', 'sm'), ' Your note: ' + state.note) : null,
|
|
216
|
+
srcs.length ? [el('span', { class: 'dim ini-note' }, 'Sources'), chgl(srcs.map(srcRow))] : el('span', { class: 'dim ini-note' }, 'No sources. ' + (p.what === 'move' ? 'This is a stage proposal.' : 'The agent named none.')));
|
|
217
|
+
const acts = state ? [] : S.data.mode === 'shadow' ? [el('span', { class: 'tag', title: 'Shadow mode: the agent drafts and raises nothing.' }, 'Shadow draft')] : [
|
|
218
|
+
pbtn('outline', 'Keep', 'check', { 'data-drv-act': 'keep', onclick: () => keep(p) }),
|
|
219
|
+
pbtn('ghost', 'Send back', null, { 'data-drv-act': 'back', onclick: () => sendBack(p) }),
|
|
220
|
+
pbtn('ghost', 'Drop', null, { 'data-drv-act': 'drop', onclick: () => drop(p) })];
|
|
221
|
+
const r = nrow({ ic: (WHAT[p.what] || ['', 'doc'])[1], hot: !state, title: titleOf(p), titleAttrs: { 'data-drv-title': p.id }, chips, xp, right: acts, attrs: { 'data-drv-row': p.id, 'data-drv-state': state ? state.st : 'open' }, key: 'drv:' + p.id, open: S.open.has(p.id), onToggle: (on) => { if (on) S.open.add(p.id); else S.open.delete(p.id); } });
|
|
222
|
+
return r;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function draw() {
|
|
226
|
+
const d = S.data;
|
|
227
|
+
if (S.err || !d) { put(wrap, sect({ ic: 'spark', title: 'Agent', attrs: { 'aria-label': 'Agent' } },
|
|
228
|
+
S.err ? el('div', { class: 'fold w', role: 'alert', 'data-drv': 'error' }, icon('alert', 'sm'), S.err, ' ', pbtn('outline', 'Try again', null, { onclick: load })) : note('Reading the agent'))); return; }
|
|
229
|
+
if (!d.ledger) { put(wrap, sect({ ic: 'spark', title: 'Agent', attrs: { 'aria-label': 'Agent' } }, el('div', { class: 'fold', 'data-drv': 'no-ledger' }, icon('info', 'sm'), 'This repository has no state folder (pmos/state, state or .pmos), so the agent has nowhere to keep its drafts.'))); return; }
|
|
230
|
+
const used = d.budget.used + ' of ' + d.budget.limit + ' runs used today';
|
|
231
|
+
const right = el('div', { class: 'chips2' },
|
|
232
|
+
d.mode === 'approve' ? (d.paused ? pbtn('outline', 'Resume', 'next', { 'data-drv-act': 'resume', onclick: () => pause(false) }) : pbtn('outline', 'Pause', 'pause', { 'data-drv-act': 'pause', onclick: () => pause(true) })) : null,
|
|
233
|
+
lk({ 'data-drv': 'runs', onclick: openRuns }, icon('clock', 'sm'), used));
|
|
234
|
+
let body;
|
|
235
|
+
if (d.mode === 'off') {
|
|
236
|
+
body = [el('div', { class: 'lh', 'data-drv': 'off' }, el('span', { class: 'ki' }, icon('pause')),
|
|
237
|
+
el('div', { class: 'ini-lhm' }, el('b', null, 'Agent is off for ' + ((ctx.board && ctx.board.name) || 'this board')), el('div', { class: 'dim' }, 'Nothing is drafted on this board. Turn it on in the board settings.')),
|
|
238
|
+
el('div', { class: 'chips2' }, pbtn('outline', 'Board settings', 'settings', { onclick: () => ctx.openBoardSettings() })))];
|
|
239
|
+
} else {
|
|
240
|
+
const s = statusOf(d); const blk = blockerLine(d);
|
|
241
|
+
const decided = decidedOf(key);
|
|
242
|
+
const ids = new Set((d.proposals || []).map((p) => p.id));
|
|
243
|
+
const rows = (d.proposals || []).map((p) => propRow(p, decided.get(p.id) || null));
|
|
244
|
+
for (const [id, x] of decided) if (!ids.has(id) && x.st !== 'dropped') rows.push(propRow(x.p, x));
|
|
245
|
+
const oldest = [...(d.proposals || [])].sort((a, b) => Date.parse(a.at) - Date.parse(b.at))[0];
|
|
246
|
+
body = [
|
|
247
|
+
el('div', { class: 'lh', 'data-drv': 'status' }, el('span', { class: 'ki' }, icon(s.ic)), el('div', { class: 'ini-lhm' }, el('b', null, s.b), el('div', { class: 'dim' }, s.sub), el('div', { class: 'dim', 'data-drv': 'last' }, s.last))),
|
|
248
|
+
d.mode === 'shadow' ? el('div', { class: 'fold', 'data-drv': 'shadow' }, icon('info', 'sm'), ' Shadow mode. The agent drafts but raises nothing: no Needs you rows, no notes in Work. Switch to Approve in the board settings to act on these.') : null,
|
|
249
|
+
blk ? el('div', { class: 'fold w', role: 'alert', 'data-drv': 'blocker' }, icon('alert', 'sm'), el('span', null, blk, d.blocker.owner === 'pm' && oldest && d.mode === 'approve' ? [' ', pbtn('outline', 'Review ' + titleOf(oldest), null, { 'data-drv-act': 'focus', onclick: () => { S.open.add(oldest.id); draw(); const t = wrap.querySelector('[data-drv-row="' + oldest.id + '"]'); if (t && t.scrollIntoView) t.scrollIntoView({ block: 'center' }); } })] : null)) : null,
|
|
250
|
+
rows.length ? list(rows) : el('div', { class: 'fold', 'data-drv': 'empty' }, icon('check', 'sm'), 'No open proposals. The agent files the next one when it has something.')];
|
|
251
|
+
}
|
|
252
|
+
put(wrap, sect({ ic: 'spark', title: 'Agent', count: d.mode === 'off' ? 0 : (d.proposals || []).length, right, attrs: { 'aria-label': 'Agent' } }, ...body));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
draw();
|
|
256
|
+
load();
|
|
257
|
+
return wrap;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* ── the Agent setting in Board settings ── */
|
|
261
|
+
/** Read a board's mode. { ok, mode } or { ok: false, error }. */
|
|
262
|
+
export async function loadAgentMode(api, board) {
|
|
263
|
+
const r = await api.get('/api/driver/mode', { board });
|
|
264
|
+
return r && r.ok ? { ok: true, mode: r.mode } : { ok: false, error: errText(r, 'The agent setting could not be read') };
|
|
265
|
+
}
|
|
266
|
+
/** state: { mode, err, busy }. onPick(mode) saves at once (POST /api/driver/mode); the caller redraws. */
|
|
267
|
+
export function agentSetting(state, { onPick } = {}) {
|
|
268
|
+
ensureDriverCss();
|
|
269
|
+
const cur = MODES.find((m) => m[0] === state.mode);
|
|
270
|
+
return el('div', { class: 'drv-set ini-field', 'data-drv': 'setting' }, lbl('AGENT'),
|
|
271
|
+
state.mode == null && !state.err ? note('Reading the agent setting') : null,
|
|
272
|
+
state.err ? el('div', { class: 'fold w', role: 'alert', 'data-drv': 'setting-error' }, icon('alert', 'sm'), state.err) : null,
|
|
273
|
+
state.mode != null ? [
|
|
274
|
+
el('div', { class: 'seg ini-seg', role: 'group', 'aria-label': 'Agent mode' }, ...MODES.map((m) => btn({ 'aria-pressed': String(state.mode === m[0]), 'data-drv-mode': m[0], disabled: !!state.busy, onclick: () => onPick(m[0]) }, m[1]))),
|
|
275
|
+
el('div', { class: 'dim ini-note', 'data-drv': 'mode-line' }, el('b', null, cur[1] + '.'), ' ' + cur[2]),
|
|
276
|
+
chgl(MODES.filter((m) => m !== cur).map((m) => el('li', null, icon('next', 'sm'), el('span', null, el('b', null, m[1] + ':'), ' ' + m[2]), el('span')))),
|
|
277
|
+
el('div', { class: 'dim ini-note' }, 'The agent files every result as a proposal. It cannot send, merge or change a record by itself. A mode is saved when you press it.')] : null);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* ── Needs you: the agent's rows (Briefing) ── */
|
|
281
|
+
/** The driver rows of GET /api/needs: "Agent has 3 drafts on <title>" and "<title> is waiting on you". [] when the read fails. */
|
|
282
|
+
export async function loadDriverNeeds() {
|
|
283
|
+
const r = await pmGet('/api/needs');
|
|
284
|
+
return r && Array.isArray(r.needs) ? r.needs.filter((n) => n && (n.kind === 'driver-drafts' || n.kind === 'driver-blocked')) : [];
|
|
285
|
+
}
|
|
286
|
+
/** One li.nrow for a driver row (the mockup's two rows). `go(row)` opens the initiative or the Work page. */
|
|
287
|
+
export function needsRow(n, go) {
|
|
288
|
+
ensureDriverCss();
|
|
289
|
+
const blocked = n.kind === 'driver-blocked';
|
|
290
|
+
const work = !n.initiative_id;
|
|
291
|
+
const chips = blocked ? [el('span', { class: 'tag warn' }, 'waiting on you'), chip(icon(work ? 'check' : 'init', 'sm'), work ? 'Work' : n.initiative_id)]
|
|
292
|
+
: [tag('agent'), chip(icon(work ? 'check' : 'init', 'sm'), work ? 'Work' : n.initiative_id)];
|
|
293
|
+
chips[0].className = blocked ? 'tag warn' : 'tag acc';
|
|
294
|
+
chips.push(el('span', { class: 'dim' }, blocked ? 'The agent will not go on until you decide' : 'Keep, send back or drop each one'));
|
|
295
|
+
const li = nrow({ ic: blocked ? 'next' : 'spark', hot: true, title: n.question, onOpen: () => go(n), openLabel: n.question, chips,
|
|
296
|
+
attrs: { 'data-needs-row': n.run_id, 'data-kind': n.kind, 'data-drv-need': n.kind, 'data-ref-row': n.initiative_id || n.run_id } });
|
|
297
|
+
return li;
|
|
298
|
+
}
|
|
@@ -15,6 +15,7 @@ import { el } from './dom.js';
|
|
|
15
15
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
16
16
|
import { icon, pbtn, lk, chip, tag, st, note, noteA, lbl, sect, card, list, empty, nrow, cols, chgl, ago, put } from './c3-ini-ui.js';
|
|
17
17
|
import { TEMPLATES } from './c3-ini-work.js';
|
|
18
|
+
import { isScanAsk, scanAskParts } from './c3-scan.js';
|
|
18
19
|
|
|
19
20
|
const LANES = ['feature', 'discovery', 'chore', 'incident', 'content', 'prototype'];
|
|
20
21
|
const ANCHOR_WHY = 'An initiative needs an anchor: a key result or a health budget. PMOS refuses to create one without it.';
|
|
@@ -52,16 +53,19 @@ registerBoardTab('triage', async (host, ctx) => {
|
|
|
52
53
|
}
|
|
53
54
|
function row(a, state) {
|
|
54
55
|
const open = state === 'open';
|
|
56
|
+
// initiative horizon-scan: an ask the scan found shows its reason, "Found by agent", its sources and its match, with the usual three buttons.
|
|
57
|
+
const scan = isScanAsk(a) ? scanAskParts(a, { today: ctx.today, first: first.name, onAccept: open ? openAccept : null, onMore: openMore, onDecline: openDecline }) : null;
|
|
55
58
|
const ini = a.initiative && (a.initiative.id || a.initiative);
|
|
56
59
|
const sharp = a.draft && typeof a.draft.confidence === 'number' ? (a.draft.confidence >= 0.7 ? 'sharp' : a.draft.confidence < 0.5 ? 'needs detail' : 'readable') : 'no draft';
|
|
57
60
|
return nrow({ ic: state === 'declined' ? 'x' : state === 'accepted' ? 'check' : 'bolt', hot: open, title: a.line, key: 'ask:' + a.id,
|
|
58
|
-
|
|
61
|
+
sub: scan ? scan.sub : null, after: scan ? scan.after : null,
|
|
62
|
+
chips: [...(scan && open ? scan.chips : [a.source ? chip(a.source === 'scan' ? 'Horizon scan' : a.source) : null, a.by ? chip(icon('user', 'sm'), a.by) : null, a.at ? chip(ago(a.at, ctx.today)) : null]),
|
|
59
63
|
open && a.needMore ? st('warn', 'alert', 'asked for more', { 'data-ibt': 'need-more', title: a.moreNote || '' }) : null,
|
|
60
64
|
state === 'accepted' ? st('ok', 'check', 'Accepted') : null, state === 'accepted' && ini ? lk({ 'data-ibt-open': ini, onclick: () => ctx.openInitiative(ini) }, ini) : null,
|
|
61
65
|
state === 'declined' ? tag('declined') : null],
|
|
62
66
|
right: open ? [tag(sharp)] : [],
|
|
63
67
|
xp: open ? [...draftNodes(a), a.needMore && a.moreNote ? note('You asked: ' + a.moreNote) : null,
|
|
64
|
-
el('div', { class: 'chips2' }, pbtn('primary', 'Accept', 'check', { 'data-ibt-accept': a.id, onclick: () => openAccept(a) }),
|
|
68
|
+
scan ? null : el('div', { class: 'chips2' }, pbtn('primary', 'Accept', 'check', { 'data-ibt-accept': a.id, onclick: () => openAccept(a) }),
|
|
65
69
|
pbtn('outline', 'Ask for more', 'reply', { 'data-ibt-more': a.id, onclick: () => openMore(a) }),
|
|
66
70
|
pbtn('ghost', 'Decline', 'x', { 'data-ibt-decline': a.id, onclick: () => openDecline(a) }))].filter(Boolean)
|
|
67
71
|
: state === 'declined' && a.why ? [note('Declined: ' + a.why)] : null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* pm/c3-ii-tabs.js — the six tabs of an initiative page (mockup v5, 34-initiatives.js iniOverview, iniBreakdown, iniDocs, iniTickets,
|
|
2
2
|
* iniTimeline, iniHistory). registerInitiativeTab(id, mount(host, ctx)); ctx.row, ctx.history (newest first), ctx.tools, ctx.toolsByCat
|
|
3
3
|
* and ctx.tool come from the page (c3-initiatives.js).
|
|
4
|
-
* Overview Left: a Summary, then one card per tool of the current stage: the stage records (c3-ini-records.js: problem, RICE, launch
|
|
4
|
+
* Overview On top, the Agent strip (c3-driver.js: GET /api/driver/strip; not in demo mode). Left: a Summary, then one card per tool of the current stage: the stage records (c3-ini-records.js: problem, RICE, launch
|
|
5
5
|
* checklist, burndown, kill conditions, learnings) and the server's stage tools (ideas, PRD, breakdown, tickets, releases,
|
|
6
6
|
* measurements, …) drawn from what PMOS holds. ctx.tool (a tool pressed in the strip or picked in More tools) is drawn
|
|
7
7
|
* first and scrolled to. Right: Details (portfolio, board, workflow with Edit, stage, owner, anchor, horizon, lane), Progress,
|
|
@@ -19,6 +19,7 @@ import { wfOf, catOf, kindOf, tplName, toolLabel, toolIcon, loadItems, dropItems
|
|
|
19
19
|
import { recordCards, recordToolsFor, RECORD_META } from './c3-ini-records.js';
|
|
20
20
|
import { loadDocs, renderDocs } from './c3-ib-docs.js';
|
|
21
21
|
import { timeline, tlInit, tlRange, tlStill } from './c3-ini-timeline.js';
|
|
22
|
+
import { driverStrip } from './c3-driver.js';
|
|
22
23
|
|
|
23
24
|
const errLine = (text) => el('div', { class: 'fold w', role: 'alert', 'data-ini': 'error' }, icon('alert', 'sm'), text);
|
|
24
25
|
const toneOf = (s) => (s === 'late' ? 'bad' : s === 'at risk' ? 'warn' : 'ok');
|
|
@@ -138,7 +139,7 @@ tab('overview', async (ctx, isDead, host) => {
|
|
|
138
139
|
rels.length ? card({ ic: 'out', title: 'Releases', sub: 'live in the Calendar' }, chgl(rels.map((x) => el('li', null, icon('out', 'sm'), el('span', null, (x.name || x.id) + ' · ' + day(String(x.release_at || x.deploy_at || '').slice(0, 10))), tag('Calendar'))))) : null,
|
|
139
140
|
];
|
|
140
141
|
if (picked) requestAnimationFrame(() => { const n = host.querySelector('[data-ini-tool-card="' + CSS.escape(picked) + '"]'); if (n && !isDead()) { if (n.scrollIntoView) n.scrollIntoView({ block: 'nearest' }); n.classList.add('ini-flash'); setTimeout(() => n.classList.remove('ini-flash'), 1400); } });
|
|
141
|
-
return cols(left, right);
|
|
142
|
+
return [ctx.demo ? null : driverStrip(ctx, r), cols(left, right)];
|
|
142
143
|
});
|
|
143
144
|
|
|
144
145
|
/* ── Breakdown ── */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Rules the builders keep (the conformance gate, R28): every control is a <button> from primitives.js button() (btn() below adds the
|
|
8
8
|
* console3 class it is drawn as, and `ini-b`, the reset that takes the primitive's own height and nowrap off); a colour, a width or a
|
|
9
9
|
* position is a class or a `--in-…` custom property set with geo(), never an inline style; icons come from the console3 sprite
|
|
10
|
-
* (c3ic) or, for the
|
|
10
|
+
* (c3ic) or, for the four it lacks (gear, grip, pause, the Jira mark), from svgIcon(). Nothing here reads or writes the server.
|
|
11
11
|
*
|
|
12
12
|
* btn(attrs, ...kids) a button; attrs.class is the console3 class (pill, chip2 lk, ib sm, tt, ...), attrs.onclick its press
|
|
13
13
|
* pbtn(variant, label, icon, attrs) a primitive button drawn as the primitive (primary, outline, ghost), small
|
|
@@ -41,12 +41,14 @@ export function svgIcon(paths, cls) {
|
|
|
41
41
|
}
|
|
42
42
|
export const GEAR = ['M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', 'M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'];
|
|
43
43
|
export const GRIP = ['M9 5h.01M9 12h.01M9 19h.01M15 5h.01M15 12h.01M15 19h.01'];
|
|
44
|
+
export const PAUSE = ['M8 5v14M16 5v14'];
|
|
44
45
|
export const JIRA = ['M12 3l9 9-9 9-9-9z', 'M12 8l4 4-4 4-4-4z'];
|
|
45
46
|
/** An icon by name: the sprite's, or one of the three drawn here. */
|
|
46
47
|
export function icon(name, cls) {
|
|
47
48
|
if (name === 'settings') return svgIcon(GEAR, cls);
|
|
48
49
|
if (name === 'grip') return svgIcon(GRIP, cls);
|
|
49
50
|
if (name === 'jira') return svgIcon(JIRA, cls);
|
|
51
|
+
if (name === 'pause') return svgIcon(PAUSE, cls);
|
|
50
52
|
return c3ic(name, cls);
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -170,7 +172,7 @@ export function nrow(o) {
|
|
|
170
172
|
el('span', { class: 'ki' + (o.hot ? ' hot' : '') + (o.tone ? ' ini-k-' + o.tone : '') }, icon(o.ic || 'doc')),
|
|
171
173
|
el('div', { class: 'nb' }, t, o.sub ? note(o.sub) : null, o.chips && o.chips.length ? el('div', { class: 'chips' }, ...o.chips.filter(Boolean)) : null, o.after || null, xp),
|
|
172
174
|
o.right && o.right.filter(Boolean).length ? el('div', { class: 'acts' }, ...o.right.filter(Boolean)) : el('span'));
|
|
173
|
-
if (xp) attachExpand(li, xp, { key: o.key });
|
|
175
|
+
if (xp) attachExpand(li, xp, { key: o.key, initial: !!o.open, onToggle: o.onToggle });
|
|
174
176
|
return li;
|
|
175
177
|
}
|
|
176
178
|
export function crumbs(parts, right) {
|
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
import { TEMPLATES, BOARD_TEMPLATES, isBuildStage, templateBuilds, offered, wfOf, catOf, kindOf, tplName, TOOL_META, toolLabel, toolIcon, loadItems, dropItems, workTlRows, iniTlRow, rescheduleTask, lockedToast } from './c3-ini-work.js';
|
|
49
49
|
import { timeline, tlInit, tlRange } from './c3-ini-timeline.js';
|
|
50
50
|
import { RECORD_META, recordToolsFor } from './c3-ini-records.js';
|
|
51
|
+
import { agentSetting, loadAgentMode, MODES as AGENT_MODES } from './c3-driver.js';
|
|
52
|
+
import { scanSetting, takeSettingsFor } from './c3-scan.js';
|
|
51
53
|
|
|
52
54
|
export { btn, geo };
|
|
53
55
|
export { TEMPLATES, CATS };
|
|
@@ -159,6 +161,8 @@ export function mountInitiatives(pane, opts) {
|
|
|
159
161
|
}
|
|
160
162
|
const openBoard = (id, tab, filter) => go({ view: 'board', board: id, tab: tab || 'summary', filter: filter || {} });
|
|
161
163
|
const refresh = () => { P.boards = null; P.ini = null; dropItems(); draw(); };
|
|
164
|
+
// The Briefing's Horizon scan card opens a board's Triage or settings (c3-scan.js goBoard). A page that is not built yet reads the address instead.
|
|
165
|
+
if (typeof window !== 'undefined') { if (window.__iniGo) window.removeEventListener('pmos:ini-go', window.__iniGo); window.__iniGo = (e) => { const d = (e && e.detail) || {}; if (d.board && root.isConnected) openBoard(d.board, d.tab || 'summary'); }; window.addEventListener('pmos:ini-go', window.__iniGo); }
|
|
162
166
|
const redraw = () => draw();
|
|
163
167
|
|
|
164
168
|
async function loadBoards() {
|
|
@@ -325,6 +329,7 @@ export function mountInitiatives(pane, opts) {
|
|
|
325
329
|
segTabs(tabs, tab, (k) => openBoard(b.id, k), 'Board views', { 'data-ini': 'board-tabs' }),
|
|
326
330
|
host));
|
|
327
331
|
mountTab(host, tab === 'summary' ? mountSummary : null, (BOARD_TAB_LIST.find((t) => t[0] === tab) || [])[1], makeCtx(b, { tab, filter: P.loc.filter || {} }), token);
|
|
332
|
+
if (!opts.demo && takeSettingsFor(b.id)) openBoardSettings(b); // "Scan settings" on the Briefing's Horizon scan card
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
/* Summary: initiatives at a glance, where they stand, what needs attention */
|
|
@@ -386,6 +391,20 @@ export function mountInitiatives(pane, opts) {
|
|
|
386
391
|
const sh = openSheet({ title: 'Board settings · ' + b.name, icon: 'cols', body: host, wide: true });
|
|
387
392
|
let usage = null;
|
|
388
393
|
loadItems(makeCtx(b, {})).then((r) => { usage = r; render(); });
|
|
394
|
+
// The Agent setting (c3-driver.js): read once, and saved at once when a mode is pressed (POST /api/driver/mode). Not in demo mode.
|
|
395
|
+
const A = { mode: null, err: '', busy: false };
|
|
396
|
+
// The Horizon scan block (c3-scan.js): one node that reads and presses by itself, so a redraw of this form keeps it. Not in demo mode.
|
|
397
|
+
const scanNode = opts.demo ? null : scanSetting({ api: apiFor(b.ws), board: b.id, boardName: b.name });
|
|
398
|
+
if (!opts.demo) loadAgentMode(apiFor(b.ws), b.id).then((r) => { if (r.ok) A.mode = r.mode; else A.err = r.error; render(); });
|
|
399
|
+
async function pickAgent(mode) {
|
|
400
|
+
if (A.busy || mode === A.mode) return;
|
|
401
|
+
A.busy = true; A.err = ''; render();
|
|
402
|
+
const r = await apiFor(b.ws).post('/api/driver/mode', { board: b.id, mode });
|
|
403
|
+
A.busy = false;
|
|
404
|
+
if (!r || !r.ok) A.err = 'The agent setting was not saved: ' + ((r && r.error) || 'no answer') + '.';
|
|
405
|
+
else { A.mode = r.mode; toast(b.name + ': Agent ' + (AGENT_MODES.find((m) => m[0] === r.mode) || [0, r.mode])[1] + '.'); }
|
|
406
|
+
render('[data-drv-mode="' + A.mode + '"]');
|
|
407
|
+
}
|
|
389
408
|
function render(focusSel) {
|
|
390
409
|
const allowed = (k) => B.build || !templateBuilds(k);
|
|
391
410
|
for (const k of [...B.offer]) if (!allowed(k)) B.offer.delete(k);
|
|
@@ -409,6 +428,8 @@ export function mountInitiatives(pane, opts) {
|
|
|
409
428
|
lbl('DEFAULT FOR NEW INITIATIVES AND ACCEPTED ASKS'),
|
|
410
429
|
tplSeg([...B.offer], B.def, (k) => { B.def = k; render('[data-ini-tpl="' + k + '"]'); }, 'Default workflow'),
|
|
411
430
|
tplCase(B.def),
|
|
431
|
+
opts.demo ? null : agentSetting(A, { onPick: pickAgent }),
|
|
432
|
+
scanNode,
|
|
412
433
|
el('div', { class: 'chips2' }, el('span', { class: 'dim ini-note' }, 'Initiatives that do not run their own workflow run the board\'s: ' + workflowLabel(b) + '.'), lk({ 'data-ini': 'edit-board-workflow', onclick: () => openBoardWorkflow(b) }, 'Edit the board\'s workflow')),
|
|
413
434
|
B.err ? errLine(B.err) : null,
|
|
414
435
|
el('div', { class: 'chips2' }, pbtn('primary', B.busy ? 'Saving' : 'Save board settings', null, { 'data-ini': 'settings-save', disabled: B.busy, onclick: save }), pbtn('ghost', 'Cancel', null, { onclick: () => sh.close() })));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* pm/c3-scan.css — the few rules the Horizon scan needs beyond console3.css (c3-scan.js). The mockup's 40-scan.css under the `sc-` prefix: the
|
|
2
|
+
* Board settings block, the scan asks in Triage and the Briefing card. Held to the R28 rules: tokens only, no inline style, corners 0, 6px or 8px,
|
|
3
|
+
* font sizes from the --fs tokens. */
|
|
4
|
+
.c3 .sc-set { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-2); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
|
|
5
|
+
.c3 .sc-set > * { min-width: 0; overflow-wrap: anywhere; }
|
|
6
|
+
.c3 .sc-set .seg { max-width: 100%; overflow-x: auto; }
|
|
7
|
+
.c3 .sc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
|
|
8
|
+
.c3 .sc-it { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 8px; align-items: start; padding: var(--sp-2) 0; border-top: 1px solid var(--border); }
|
|
9
|
+
.c3 .sc-it > .sc-main { min-width: 0; display: grid; gap: 4px; }
|
|
10
|
+
.c3 .sc-it .sc-name { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; overflow-wrap: anywhere; }
|
|
11
|
+
.c3 .sc-it .sc-why { font-size: var(--fs-2); }
|
|
12
|
+
.c3 .sc-it .sc-acts { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
13
|
+
.c3 .sc-it .form { margin: 0; }
|
|
14
|
+
.c3 .sc-add { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
15
|
+
.c3 .sc-add input[type="text"] { flex: 1 1 160px; min-width: 0; }
|
|
16
|
+
.c3 .sc-meta { display: grid; gap: 4px; font-size: var(--fs-2); }
|
|
17
|
+
.c3 .sc-foot { font-size: var(--fs-2); }
|
|
18
|
+
.c3 .nrow .sc-src { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; align-items: center; }
|
|
19
|
+
.c3 .nrow .sc-src .chip2 { max-width: 100%; }
|
|
20
|
+
.c3 .sc-link { text-decoration: none; }
|
|
21
|
+
.c3 .sc-card { display: grid; gap: var(--sp-2); }
|
|
22
|
+
.c3 .sc-card ul.chgl li { flex-wrap: wrap; }
|
|
23
|
+
|
|
24
|
+
.c3 .sc-card .sc-find { display: flex; flex-direction: column; min-width: 0; }
|
|
25
|
+
.c3 .sc-card .sc-find .sc-sub { display: block; }
|