create-pmos 0.167.0 → 0.169.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/extension/README.md +33 -0
- package/workspace/extension/address.js +20 -0
- package/workspace/extension/content.js +76 -0
- package/workspace/extension/icons/icon-128.png +0 -0
- package/workspace/extension/icons/icon-16.png +0 -0
- package/workspace/extension/icons/icon-32.png +0 -0
- package/workspace/extension/icons/icon-48.png +0 -0
- package/workspace/extension/manifest.json +15 -0
- package/workspace/extension/options.css +33 -0
- package/workspace/extension/options.html +26 -0
- package/workspace/extension/options.js +24 -0
- package/workspace/extension/panel.css +21 -0
- package/workspace/extension/panel.html +14 -0
- package/workspace/extension/panel.js +67 -0
- package/workspace/guide/console-calendar.md +14 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-driver.md +87 -0
- package/workspace/lib/driver-cli.js +207 -0
- package/workspace/lib/driver-decide.js +118 -0
- package/workspace/lib/driver-facts.js +113 -0
- package/workspace/lib/driver-needs.js +107 -0
- package/workspace/lib/driver-run.js +187 -0
- package/workspace/lib/driver.js +408 -0
- package/workspace/lib/extension-cli.js +39 -0
- package/workspace/lib/headless.js +13 -2
- package/workspace/lib/meeting-focus.js +48 -1
- package/workspace/lib/needs-providers.js +1 -0
- package/workspace/lib/pmos-verbs.js +6 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-boards.js +11 -1
- package/workspace/lib/routes-driver.js +167 -0
- package/workspace/lib/routes-meeting-focus.js +17 -1
- 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-demo-focus.js +12 -2
- package/workspace/public/pm/c3-demo.js +1 -1
- 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-focus.css +31 -0
- package/workspace/public/pm/c3-focus.js +122 -21
- 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 +14 -0
- package/workspace/public/pm/c3-shell.js +2 -2
- package/workspace/public/pm/pm-work-view.js +12 -2
- package/workspace/server.js +21 -1
- package/workspace/skills/initiative-drive.skill +50 -0
- package/workspace/skills/work-unblock.skill +42 -0
- package/workspace/test-files.json +6 -0
- package/workspace/test-lists/c3-meeting-focus.json +13 -4
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* routes-driver.js — the Initiative Driver's routes (initiative initiative-driver, PRD sections 4 and 5).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/driver/strip?ws&target_kind&id[&board] → { ok, ws, target, mode, board, paused, budget, today: [runs of today], running, last, blocker, proposals }
|
|
5
|
+
* POST /api/driver/keep { ws, proposal } → { ok, proposal, wrote, warnings }
|
|
6
|
+
* POST /api/driver/return { ws, proposal, note } → { ok, proposal } the note is required; it wakes a run
|
|
7
|
+
* POST /api/driver/drop { ws, proposal, note? } → { ok, proposal }
|
|
8
|
+
* POST /api/driver/pause { ws, target_kind, id } → { ok, paused: true, target }
|
|
9
|
+
* POST /api/driver/resume { ws, target_kind, id } → { ok, paused: false, target }
|
|
10
|
+
* POST /api/driver/mode { ws, board?, mode } → { ok, board, mode } board omitted: every board
|
|
11
|
+
* GET /api/driver/mode?ws&board → { ok, ws, board, mode } the mode a board runs in (Approve when none is set)
|
|
12
|
+
* GET /api/driver/open?ws&target_kind → { ok, ws, kind, mode, targets: { <id>: [proposal] } } every open proposal of
|
|
13
|
+
* one kind, by target: the Work list draws one agent line per task from this
|
|
14
|
+
*
|
|
15
|
+
* The strip is the data of the Agent strip on an initiative's Overview and of the one-line version on a Work task. Its
|
|
16
|
+
* `proposals` are the open ones; `by` is the page name (`agent:sess-<n>`), never the session token. `running` is a run that has
|
|
17
|
+
* started and not ended; `last` is the most recent run that ended; `blocker` is the sweep's latest finding of today.
|
|
18
|
+
*
|
|
19
|
+
* WHO WRITES. Every POST passes pm-guard requirePagePress, and a request that carries an agent session header is refused
|
|
20
|
+
* first. `by` of every line is the guard's actor. Keep writes through the existing writers (lib/driver-decide.js): a problem
|
|
21
|
+
* into the stage tools, a move through the board move route (lib/routes-boards.js move), a Work next step into the Work items.
|
|
22
|
+
* State: <state folder>/driver.jsonl, one per repository (lib/driver.js). Nothing here sends anything outward.
|
|
23
|
+
*/
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const guard = require('./pm-guard.js');
|
|
26
|
+
const actorLib = require('./actor.js');
|
|
27
|
+
const D = require('./driver.js');
|
|
28
|
+
const decideLib = require('./driver-decide.js');
|
|
29
|
+
const stageTools = require('./stage-tools.js');
|
|
30
|
+
|
|
31
|
+
const text = (v) => (typeof v === 'string' ? v : '');
|
|
32
|
+
const bad = (status, error) => ({ status, body: { ok: false, error } });
|
|
33
|
+
const POSTS = ['keep', 'return', 'drop', 'pause', 'resume', 'mode'];
|
|
34
|
+
const GETS = ['strip', 'mode', 'open'];
|
|
35
|
+
const proposalOut = (p) => ({ id: p.id, what: p.what, data: p.data, why: p.why, sources: p.sources, run: p.run, by: actorLib.pageName(p.by), at: p.at });
|
|
36
|
+
|
|
37
|
+
function wsOf(ctx, id) {
|
|
38
|
+
const w = ctx.getWs(id || '');
|
|
39
|
+
return w && (!id || w.id === id) ? w : null;
|
|
40
|
+
}
|
|
41
|
+
function ledgerOf(ctx, w) {
|
|
42
|
+
const d = ctx.stateDirFor ? ctx.stateDirFor(w) : null;
|
|
43
|
+
return D.ledgerFile(d);
|
|
44
|
+
}
|
|
45
|
+
const runtimeOf = (ctx) => ctx.runtimeDir || require('./state-root.js').defaultRoot();
|
|
46
|
+
|
|
47
|
+
/** The board a target's mode is read from: the board of its latest run, else Work's own, else every board. */
|
|
48
|
+
function boardOfTarget(state, target) {
|
|
49
|
+
if (target.kind === 'work') return 'work';
|
|
50
|
+
const runs = [...state.runs.values()].filter((r) => D.targetKey(r.target) === D.targetKey(target) && r.board);
|
|
51
|
+
return runs.length ? runs[runs.length - 1].board : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stripOf(state, target, { now = Date.now(), board: asked = null } = {}) {
|
|
55
|
+
const key = D.targetKey(target);
|
|
56
|
+
const runs = [...state.runs.values()].filter((r) => D.targetKey(r.target) === key);
|
|
57
|
+
const running = runs.filter((r) => !r.end).pop() || null;
|
|
58
|
+
const ended = runs.filter((r) => r.end).pop() || null;
|
|
59
|
+
const board = boardOfTarget(state, target) || asked || null;
|
|
60
|
+
const day = D.localDay(now);
|
|
61
|
+
const blocker = state.blockers.filter((b) => b.target && D.targetKey(b.target) === key && b.day === day).pop() || null;
|
|
62
|
+
const open = D.openProposals(state, target);
|
|
63
|
+
return {
|
|
64
|
+
target, board, mode: D.modeOf(state, board), paused: D.isPaused(state, target),
|
|
65
|
+
budget: { used: D.runsOnDay(state, target, day), limit: target.kind === 'work' ? D.BUDGET.workPerDay : D.BUDGET.initiativePerDay, concurrent: D.BUDGET.concurrent },
|
|
66
|
+
today: runs.filter((r) => r.start && D.localDay(Date.parse(r.start)) === day).map((r) => ({ run: r.run, start: r.start, end: r.end, ok: r.ok, reason: r.reason, proposals: [...state.proposals.values()].filter((p) => p.run === r.run).length })),
|
|
67
|
+
running: running ? { run: running.run, since: running.start } : null,
|
|
68
|
+
last: ended ? { run: ended.run, at: ended.end, ok: ended.ok, reason: ended.reason, proposals: [...state.proposals.values()].filter((p) => p.run === ended.run).length } : null,
|
|
69
|
+
blocker: blocker ? { code: blocker.code, text: blocker.text, owner: blocker.owner, day: blocker.day } : null,
|
|
70
|
+
proposals: open.map(proposalOut),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function handle(req, res, url, ctx) {
|
|
75
|
+
const p = url.pathname;
|
|
76
|
+
const send = (r) => { ctx.json(res, r.body, r.status); return true; };
|
|
77
|
+
const sub = p.replace(/^\/api\/driver\/?/, '');
|
|
78
|
+
if (!GETS.includes(sub) && !POSTS.includes(sub)) return send(bad(404, 'no such driver route'));
|
|
79
|
+
|
|
80
|
+
if (sub === 'mode' && req.method === 'GET') {
|
|
81
|
+
const w = wsOf(ctx, url.searchParams.get('ws'));
|
|
82
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
83
|
+
const board = text(url.searchParams.get('board'));
|
|
84
|
+
if (!board || !D.ID_RE.test(board)) return send(bad(400, 'board must be a board id'));
|
|
85
|
+
const file = ledgerOf(ctx, w);
|
|
86
|
+
const state = D.fold(file ? D.readLines(file) : []);
|
|
87
|
+
return send({ status: 200, body: { ok: true, ws: w.id, ledger: !!file, board, mode: D.modeOf(state, board) } });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (sub === 'open') {
|
|
91
|
+
if (req.method !== 'GET') return send(bad(405, 'GET only'));
|
|
92
|
+
const w = wsOf(ctx, url.searchParams.get('ws'));
|
|
93
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
94
|
+
const kind = text(url.searchParams.get('target_kind'));
|
|
95
|
+
if (!D.KINDS.includes(kind)) return send(bad(400, `target_kind must be one of ${D.KINDS.join(', ')}`));
|
|
96
|
+
const file = ledgerOf(ctx, w);
|
|
97
|
+
const state = D.fold(file ? D.readLines(file) : []);
|
|
98
|
+
const targets = {};
|
|
99
|
+
for (const pr of D.openProposals(state)) {
|
|
100
|
+
if (pr.target.kind !== kind || pr.target.ws !== w.id) continue;
|
|
101
|
+
(targets[pr.target.id] = targets[pr.target.id] || []).push(proposalOut(pr));
|
|
102
|
+
}
|
|
103
|
+
return send({ status: 200, body: { ok: true, ws: w.id, ledger: !!file, kind, mode: D.modeOf(state, kind === 'work' ? 'work' : null), targets } });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (sub === 'strip') {
|
|
107
|
+
if (req.method !== 'GET') return send(bad(405, 'GET only'));
|
|
108
|
+
const w = wsOf(ctx, url.searchParams.get('ws'));
|
|
109
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
110
|
+
const t = D.checkTarget({ kind: text(url.searchParams.get('target_kind')), ws: w.id, id: text(url.searchParams.get('id')) });
|
|
111
|
+
if (!t.ok) return send(bad(400, t.error));
|
|
112
|
+
const file = ledgerOf(ctx, w);
|
|
113
|
+
const state = D.fold(file ? D.readLines(file) : []);
|
|
114
|
+
return send({ status: 200, body: { ok: true, ws: w.id, ledger: !!file, ...stripOf(state, t.target, { board: text(url.searchParams.get('board')) || null }) } });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (req.method !== 'POST') return send(bad(405, 'POST only'));
|
|
118
|
+
const read = await ctx.readJson(req);
|
|
119
|
+
if (!read.ok) return send(bad(400, 'the body is not JSON'));
|
|
120
|
+
const body = read.body;
|
|
121
|
+
if (req.headers && req.headers['x-pmos-session']) return send(bad(403, 'an agent session cannot keep, send back, drop, pause or change the mode of the driver'));
|
|
122
|
+
const g = guard.requirePagePress(req, body);
|
|
123
|
+
if (!g.ok) return send(bad(g.status, g.error));
|
|
124
|
+
const w = wsOf(ctx, text(body.ws));
|
|
125
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
126
|
+
const file = ledgerOf(ctx, w);
|
|
127
|
+
if (!file) return send(bad(409, 'this repository has no state folder (pmos/state, state or .pmos), so the driver has nowhere to keep its ledger'));
|
|
128
|
+
const refresh = () => { try { if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg(w.id); } catch { /* the next poll recounts */ } };
|
|
129
|
+
|
|
130
|
+
if (sub === 'keep' || sub === 'return' || sub === 'drop') {
|
|
131
|
+
const proposal = text(body.proposal);
|
|
132
|
+
if (!D.PROPOSAL_RE.test(proposal)) return send(bad(400, 'proposal must be a driver proposal id'));
|
|
133
|
+
const stateDir = ctx.stateDirFor(w);
|
|
134
|
+
const dctx = {
|
|
135
|
+
stageFile: path.join(stateDir, stageTools.LEDGER),
|
|
136
|
+
itemsDir: path.join(runtimeOf(ctx), 'pm-items'),
|
|
137
|
+
correctionsFile: path.join(runtimeOf(ctx), 'corrections.jsonl'),
|
|
138
|
+
// The board move route's own function: its validation and its warnings apply, and `by` is the PM.
|
|
139
|
+
move: async (pr) => {
|
|
140
|
+
const routesBoards = require('./routes-boards.js');
|
|
141
|
+
let board = pr.data && pr.data.board;
|
|
142
|
+
if (!board) { try { board = routesBoards.boardIndex(ctx, w).of(pr.target.id); } catch { board = null; } }
|
|
143
|
+
return routesBoards.move(ctx, { ws: w.id, board: board || w.id, id: pr.target.id, to: pr.data.to, why: (pr.why || '').slice(0, 500) }, g);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const r = await decideLib.decide(sub, { file, proposal, note: body.note == null ? undefined : text(body.note), actor: g.actor, ctx: dctx });
|
|
147
|
+
if (!r.ok) return send(bad(r.status, r.error));
|
|
148
|
+
refresh();
|
|
149
|
+
return send({ status: 200, body: { ok: true, proposal: { ...r.proposal, by: actorLib.pageName(r.proposal.by) }, wrote: r.wrote, warnings: r.warnings } });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (sub === 'pause' || sub === 'resume') {
|
|
153
|
+
const t = D.checkTarget({ kind: text(body.target_kind), ws: w.id, id: text(body.id) });
|
|
154
|
+
if (!t.ok) return send(bad(400, t.error));
|
|
155
|
+
const r = decideLib.setPause(file, sub, t.target, g.actor);
|
|
156
|
+
if (!r.ok) return send(bad(r.status, r.error));
|
|
157
|
+
refresh();
|
|
158
|
+
return send({ status: 200, body: { ok: true, paused: r.paused, target: r.target } });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const r = decideLib.setMode(file, body.board == null || body.board === '' ? '*' : text(body.board), text(body.mode), g.actor);
|
|
162
|
+
if (!r.ok) return send(bad(r.status, r.error));
|
|
163
|
+
refresh();
|
|
164
|
+
return send({ status: 200, body: { ok: true, board: r.board, mode: r.mode } });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { prefixes: ['/api/driver'], handle, stripOf, boardOfTarget };
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* minutes of that note's transcript with who said them, the decisions, actions and questions so far, and the companion's state.
|
|
8
8
|
* An unknown meeting is a 404. The read also starts the companion when lib/meeting-focus.js shouldRun says it may: the note
|
|
9
9
|
* records, new lines arrived, 40 s passed, no run is under way, PMOS_FOCUS_AGENT is not off. The run is fire and forget.
|
|
10
|
+
* GET /api/meeting-focus/now?meet=<abc-defg-hij>&title=<Meet tab title> (c3-meet-extension)
|
|
11
|
+
* → { ok, event: { id, title, at, end }, date, by: 'meet'|'title'|'time' }, or a 404 { ok:false, error, choices:[{ id, title, at, end }] }
|
|
12
|
+
* Which meeting a Google Meet call is: the one whose links carry the Meet code, else the one titled like the tab, else the one under way now
|
|
13
|
+
* (lib/meeting-focus.js pickNow). It reads the same calendar as the read above and writes nothing.
|
|
10
14
|
* POST /api/meeting-focus/topic { event | note, date?, action: 'set'|'add'|'park', topic } page press; the PM's correction, appended to the state
|
|
11
15
|
* POST /api/meeting-focus/ask { event, note?, date?, question } page press; one headless read-only run with the PM's question
|
|
12
16
|
* GET /api/meeting-focus/ask?id= → { ok, id, state: working | answered | failed, error? } the answer itself comes back as a note in GET /api/meeting-focus
|
|
@@ -175,6 +179,17 @@ async function read(q, ctx) {
|
|
|
175
179
|
} };
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
/* ── GET /api/meeting-focus/now ───────────────────────────────────────────────────────────────────────────────────── */
|
|
183
|
+
|
|
184
|
+
function nowRoute(q, ctx) {
|
|
185
|
+
const nowMs = typeof ctx.now === 'function' ? ctx.now() : Date.now();
|
|
186
|
+
let events = [];
|
|
187
|
+
try { events = typeof ctx.readEvents === 'function' ? ctx.readEvents() : require('./calendar-events.js').readAll(ctx.runtimeDir, { now: nowMs }); } catch { events = []; }
|
|
188
|
+
const r = F.pickNow(events, { now: nowMs, meet: F.str(q && q.meet, 40), title: F.str(q && q.title, 300) });
|
|
189
|
+
if (!r.ok) return fail(404, r.error, { choices: r.choices });
|
|
190
|
+
return { status: 200, body: { ok: true, event: r.event, date: r.date, by: r.by } };
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
/* ── POST /api/meeting-focus/topic ────────────────────────────────────────────────────────────────────────────────── */
|
|
179
194
|
|
|
180
195
|
async function topic(req, body, ctx) {
|
|
@@ -272,13 +287,14 @@ const respond = (ctx, res, r) => { ctx.json(res, r.body, r.status); return true;
|
|
|
272
287
|
|
|
273
288
|
module.exports = {
|
|
274
289
|
prefixes: ['/api/meeting-focus'],
|
|
275
|
-
read, topic, ask, askState, settleAsk, settleRun, findEvent, keyOf,
|
|
290
|
+
read, now: nowRoute, topic, ask, askState, settleAsk, settleRun, findEvent, keyOf,
|
|
276
291
|
handle: async (req, res, url, ctx) => {
|
|
277
292
|
const p = url.pathname;
|
|
278
293
|
const wsId = url.searchParams.get('ws');
|
|
279
294
|
const w = ctx.getWs ? ctx.getWs(wsId) : null;
|
|
280
295
|
const base = { runtimeDir: ctx.runtimeDir, spawnSession: ctx.spawnSession, w, getWs: ctx.getWs, json: ctx.json };
|
|
281
296
|
if (req.method === 'GET' && p === '/api/meeting-focus') return respond(ctx, res, await read(Object.fromEntries(url.searchParams), base));
|
|
297
|
+
if (req.method === 'GET' && p === '/api/meeting-focus/now') return respond(ctx, res, nowRoute(Object.fromEntries(url.searchParams), base));
|
|
282
298
|
if (req.method === 'GET' && p === '/api/meeting-focus/ask') return respond(ctx, res, askState(Object.fromEntries(url.searchParams), base));
|
|
283
299
|
const fn = req.method === 'POST' && { '/api/meeting-focus/topic': topic, '/api/meeting-focus/ask': ask }[p];
|
|
284
300
|
if (!fn) return false;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.169.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.169.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -15,11 +15,12 @@ import { el } from './dom.js';
|
|
|
15
15
|
import { c3ic } from './pm-work-view.js';
|
|
16
16
|
import { pmPost } from './http.js';
|
|
17
17
|
import { makeXp, attachExpand } from './c3-expand.js';
|
|
18
|
-
import { needRow, belowLine, BX, toast } from './c3-brief.js';
|
|
18
|
+
import { needRow, belowLine, BX, toast, hooks } from './c3-brief.js';
|
|
19
19
|
import { onBriefing, loadBriefing } from './shell-ticker.js';
|
|
20
20
|
import { cardView, TAG_STYLE } from './briefing-model.js';
|
|
21
21
|
import { onBriefPage } from './c3-brief-data.js';
|
|
22
22
|
import { button, ensureComponentsCss } from './primitives.js';
|
|
23
|
+
import { loadDriverNeeds, needsRow as driverRow } from './c3-driver.js';
|
|
23
24
|
|
|
24
25
|
const reduced = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
25
26
|
const post = (path, body) => pmPost(path, body).catch(() => ({ ok: false }));
|
|
@@ -104,11 +105,12 @@ export function needsSection(S) {
|
|
|
104
105
|
const sumState = { dec: 0, own: 0, late: 0, quick: 0 };
|
|
105
106
|
const own = el('ul', { class: 'list', role: 'list', 'data-needs-own': '' });
|
|
106
107
|
const ownNote = el('div');
|
|
108
|
+
const agent = el('ul', { class: 'list', role: 'list', 'data-needs-agent': '' }); // the Initiative Driver's rows (c3-driver.js)
|
|
107
109
|
const list = el('ul', { class: 'list', role: 'list' });
|
|
108
110
|
const extra = el('div');
|
|
109
111
|
const alertHost = el('div');
|
|
110
112
|
const box = el('section', { class: 'card sect', 'data-c3pg': 'needs', 'aria-label': 'Needs you' },
|
|
111
|
-
el('div', { class: 'sh2' }, el('h2', null, c3ic('inbox', 'sm'), 'Needs you ', count)), alertHost, ownNote, own, list, extra);
|
|
113
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('inbox', 'sm'), 'Needs you ', count)), alertHost, ownNote, own, agent, list, extra);
|
|
112
114
|
let sig = ''; let last = null; let ownSig = ''; let page = null;
|
|
113
115
|
const nothing = () => el('li', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing needs you right now.');
|
|
114
116
|
/* the header line: "3 decisions · 4 of yours · 2 overdue", then how many take under 2 minutes */
|
|
@@ -118,6 +120,8 @@ export function needsSection(S) {
|
|
|
118
120
|
if (!quick.isConnected) box.querySelector('.sh2').append(quick);
|
|
119
121
|
quick.replaceChildren(document.createTextNode(dec + (dec === 1 ? ' decision' : ' decisions') + ' · ' + ownRows.length + ' of yours'));
|
|
120
122
|
if (late) quick.append(' · ', el('span', { class: 'tag bad' }, late + ' overdue'));
|
|
123
|
+
const ag = agent.children.length;
|
|
124
|
+
if (ag) quick.append(' · ' + ag + ' from the agent');
|
|
121
125
|
if (sumState.quick) quick.append(' · ', c3ic('bolt', 'sm'), sumState.quick + ' under 2 min');
|
|
122
126
|
};
|
|
123
127
|
const recount = () => {
|
|
@@ -168,9 +172,21 @@ export function needsSection(S) {
|
|
|
168
172
|
extra.replaceChildren();
|
|
169
173
|
recount();
|
|
170
174
|
};
|
|
175
|
+
/* the agent's rows: drafts waiting on the PM, a target the PM is blocking. A press opens the initiative (its Overview holds the strip) or Work. */
|
|
176
|
+
let agentSig = '';
|
|
177
|
+
const goAgent = (n) => {
|
|
178
|
+
if (n.initiative_id) { try { const u = new URL(location.href); u.searchParams.set('init', 'ini:' + encodeURIComponent(n.ws) + ':' + encodeURIComponent(n.initiative_id) + ':overview'); history.replaceState(history.state, '', u); } catch { /* no history */ } }
|
|
179
|
+
if (hooks.go) hooks.go({ action: n.initiative_id ? 'goinit' : 'gowork' });
|
|
180
|
+
};
|
|
181
|
+
const fillAgent = () => loadDriverNeeds().then((rows) => {
|
|
182
|
+
const g = JSON.stringify(rows.map((r) => [r.run_id, r.question]));
|
|
183
|
+
if (g === agentSig) return; agentSig = g;
|
|
184
|
+
agent.replaceChildren(...rows.map((r) => driverRow(r, goAgent)));
|
|
185
|
+
recount();
|
|
186
|
+
}, () => {});
|
|
171
187
|
S.refreshNeeds = () => { sig = ''; if (last) fill(last); };
|
|
172
188
|
S.needsFill = fill;
|
|
173
|
-
onBriefing((b) => { S.brief = b; fill(b); });
|
|
189
|
+
onBriefing((b) => { S.brief = b; fill(b); fillAgent(); });
|
|
174
190
|
onBriefPage(fillOwn);
|
|
175
191
|
loadBriefing();
|
|
176
192
|
return box;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* c3-demo.js passes GET /api/meeting-focus, GET /api/meeting-focus/ask and the two POSTs (topic, ask) here. The content is the approved
|
|
3
3
|
* mockup's (version 7): the Partner API rate-limit review at 18:24 in, with its six topics (one added from the talk), the gist and its four
|
|
4
4
|
* context chips, the room's last lines, three PMOS notes and two it is holding, the captured items and a Google Slides deck.
|
|
5
|
-
* Whatever meeting the page asks for, the demo answers with this one.
|
|
5
|
+
* Whatever meeting the page asks for, the demo answers with this one. GET /api/meeting-focus/now (the compact page of the Chrome extension) names it too;
|
|
6
|
+
* the Meet code NONE_CODE answers "no meeting fits" with three of the day's meetings as choices, so that path can be walked. A correction (set, add, park) and a question change the answer
|
|
6
7
|
* for the rest of the page's life; nothing is saved and nothing reaches the server. Real mode never loads this file. */
|
|
7
8
|
|
|
8
9
|
const DAY = '2026-10-08';
|
|
@@ -91,8 +92,17 @@ function view() {
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
export const NONE_CODE = 'zzz-zzzz-zzz';
|
|
96
|
+
const CHOICES = [['e1', 'Partner standup', '09:30', '10:00'], ['e2', EVENT.title, '11:00', '12:00'], ['e3', 'Onboarding v2 scope check', '14:00', '14:45']]
|
|
97
|
+
.map(([id, title, from, to]) => ({ id: id + '@' + DAY, title, at: DAY + 'T' + from + ':00', end: DAY + 'T' + to + ':00' }));
|
|
98
|
+
|
|
99
|
+
/** GET /api/meeting-focus, GET /api/meeting-focus/ask?id= and GET /api/meeting-focus/now?meet=&title= */
|
|
95
100
|
export function focusGet(path, url) {
|
|
101
|
+
if (path === '/api/meeting-focus/now') {
|
|
102
|
+
const q = new URL(url, location.origin).searchParams;
|
|
103
|
+
if (q.get('meet') === NONE_CODE) return { ok: false, status: 404, error: 'No meeting on the calendar fits this call.', choices: CHOICES };
|
|
104
|
+
return { ok: true, event: { id: EVENT.id, title: EVENT.title, at: EVENT.at, end: EVENT.end }, date: DAY, by: q.get('meet') ? 'meet' : 'time' };
|
|
105
|
+
}
|
|
96
106
|
if (path === '/api/meeting-focus/ask') {
|
|
97
107
|
const q = new URL(url, location.origin).searchParams;
|
|
98
108
|
const a = S.asks.find((x) => x.id === q.get('id'));
|
|
@@ -52,7 +52,7 @@ function getRoute(path, url) {
|
|
|
52
52
|
switch (path) {
|
|
53
53
|
case '/api/briefing': return D.briefing(url);
|
|
54
54
|
case '/api/calendar-views': return withCalendarPage(D.calendarViews(url));
|
|
55
|
-
case '/api/meeting-focus': case '/api/meeting-focus/ask': return focusGet(path, url);
|
|
55
|
+
case '/api/meeting-focus': case '/api/meeting-focus/ask': case '/api/meeting-focus/now': return focusGet(path, url);
|
|
56
56
|
case '/api/pm-items': return D.pmItems();
|
|
57
57
|
case '/api/runs': return D.runs();
|
|
58
58
|
case '/api/releases': return D.releases();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* pm/c3-driver-work.js — the Initiative Driver on a Work task (PRD section 5; mockup 39-driver.js workNote and openDraft).
|
|
2
|
+
* One agent line under the title of a task that has an open driver proposal, and the sheet behind its Review.
|
|
3
|
+
*
|
|
4
|
+
* loadDriverOpen(ws) GET /api/driver/open?target_kind=work → { ok, ws, mode, targets: { <task id>: [proposal] } }, or null
|
|
5
|
+
* workNote(o) the line: o = { item, props, ws, mode, done } → a node, or null (Shadow and Off draw no line, PRD section 5)
|
|
6
|
+
* openDraftSheet(o) the sheet: the draft text, the line that the agent sent nothing, Keep (copies the text), Send back, Drop
|
|
7
|
+
*
|
|
8
|
+
* A kept follow-up is copied to the clipboard and recorded. When the browser refuses the clipboard, the text is selected and the
|
|
9
|
+
* sheet stays open so Ctrl+C works. Presses: POST /api/driver/keep, return, drop (the server takes the PM from the call path).
|
|
10
|
+
* `done()` runs after a press so the Work list reads again. Nothing here sends anything to a person.
|
|
11
|
+
*/
|
|
12
|
+
import { el } from './dom.js';
|
|
13
|
+
import { SEL } from './primitives.js';
|
|
14
|
+
import { pmGet, pmPost } from './http.js';
|
|
15
|
+
import { c3ic } from './pm-work-view.js';
|
|
16
|
+
import { openSheet } from './c3-sheet.js';
|
|
17
|
+
import { toast } from './c3-toast.js';
|
|
18
|
+
import { pbtn, chip, tag, put } from './c3-ini-ui.js';
|
|
19
|
+
import { whenLabel, dueLabel, titleOf, ensureDriverCss } from './c3-driver.js';
|
|
20
|
+
|
|
21
|
+
export async function loadDriverOpen(ws) {
|
|
22
|
+
const r = await pmGet('/api/driver/open?target_kind=work' + (ws ? '&ws=' + encodeURIComponent(ws) : ''));
|
|
23
|
+
return r && r.ok ? r : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const press = async (ws, route, body, what) => {
|
|
27
|
+
const r = await pmPost('/api/driver/' + route, { ws, ...body });
|
|
28
|
+
if (!r || !r.ok) { toast({ text: what + ' did not work: ' + ((r && r.error) || 'the workspace did not answer') + '.', tone: 'error', timeout: 6000 }); return null; }
|
|
29
|
+
return r;
|
|
30
|
+
};
|
|
31
|
+
const short = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
|
|
32
|
+
const lead = (props) => props.find((p) => p.what === 'follow-up') || props[0];
|
|
33
|
+
const who = (item, p) => (p.data && typeof p.data.to === 'string' && p.data.to) || item.waiting_on || '';
|
|
34
|
+
|
|
35
|
+
/** The words of the line, by what the agent filed. Pure. */
|
|
36
|
+
export function lineOf(item, props) {
|
|
37
|
+
const p = lead(props || []);
|
|
38
|
+
if (!p) return null;
|
|
39
|
+
const more = props.length > 1 ? ' (+' + (props.length - 1) + ' more)' : '';
|
|
40
|
+
const t = p.data && typeof p.data.text === 'string' ? p.data.text : '';
|
|
41
|
+
if (p.what === 'follow-up') return { p, text: 'Agent drafted a follow-up' + (who(item, p) ? ' to ' + who(item, p) : '') + '. Nothing is sent.' + more, act: 'review' };
|
|
42
|
+
if (p.what === 'next-step') return { p, text: 'Agent suggests ' + (p.data && p.data.due && !t ? 'a new due date: ' + dueLabel(p.data.due) : 'a next step: ' + short(t, 70) + (p.data && p.data.due ? ', due ' + dueLabel(p.data.due) : '')) + '.' + more, act: 'keepdrop' };
|
|
43
|
+
return { p, text: 'Agent left a note: ' + short(t, 70) + more, act: 'review' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function workNote({ item, props, ws, mode, done }) {
|
|
47
|
+
if (mode !== 'approve') return null;
|
|
48
|
+
ensureDriverCss();
|
|
49
|
+
const l = lineOf(item, props);
|
|
50
|
+
if (!l) return null;
|
|
51
|
+
const lk = (label, attrs, fn) => pbtn('ghost', label, null, { ...attrs, class: 'lk', onclick: (e) => { e.stopPropagation(); fn(); } });
|
|
52
|
+
const decide = async (route) => {
|
|
53
|
+
const r = await press(ws, route, { proposal: l.p.id }, route === 'keep' ? 'Keep' : 'Drop');
|
|
54
|
+
if (!r) return;
|
|
55
|
+
toast(route === 'keep' ? 'Kept: ' + titleOf(l.p) + '.' : 'Dropped the suggestion.');
|
|
56
|
+
done();
|
|
57
|
+
};
|
|
58
|
+
return el('span', { class: 'drv-note', 'data-drv-wnote': item.id, 'data-drv-what': l.p.what }, c3ic('spark', 'sm'), l.text, ' ',
|
|
59
|
+
l.act === 'review' ? lk('Review', { 'data-drv-wnote-act': 'review' }, () => openDraftSheet({ item, props, ws, done }))
|
|
60
|
+
: [lk('Keep', { 'data-drv-wnote-act': 'keep' }, () => decide('keep')), lk('Drop', { 'data-drv-wnote-act': 'drop' }, () => decide('drop'))]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Copy `text`. The clipboard first; when the browser refuses, select the node's text so Ctrl+C works. Returns 'copied' or 'selected'. */
|
|
64
|
+
export async function copyText(text, node) {
|
|
65
|
+
try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return 'copied'; } } catch { /* fall back to the selection */ }
|
|
66
|
+
try { const r = document.createRange(); r.selectNodeContents(node); const s = window.getSelection(); s.removeAllRanges(); s.addRange(r); } catch { /* the PM selects by hand */ }
|
|
67
|
+
return 'selected';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function openDraftSheet({ item, props, ws, done }) {
|
|
71
|
+
ensureDriverCss();
|
|
72
|
+
const p = lead(props);
|
|
73
|
+
const text = (p.data && typeof p.data.text === 'string' && p.data.text) || '';
|
|
74
|
+
const isDraft = p.what === 'follow-up';
|
|
75
|
+
const to = who(item, p);
|
|
76
|
+
const draft = el('p', { class: 'sit drv-pre', 'data-drv': 'draft' }, text || 'The agent filed no text.');
|
|
77
|
+
const backBox = el('div', { class: 'form', hidden: true, 'data-drv': 'back-box' }, el('input', { type: 'text', placeholder: 'Tell the agent what to change', 'aria-label': 'Note to the agent', 'data-drv': 'note', maxlength: '500', autocomplete: 'off' }));
|
|
78
|
+
const status = el('div', { class: 'fold', 'data-drv': 'sheet-note' }, c3ic('info', 'sm'), isDraft ? 'The agent sent nothing and cannot send. Keep copies this text so you can send it yourself.' : 'The agent sent nothing. Keep records that you rely on this note.');
|
|
79
|
+
const sayStatus = (t, warn) => { status.className = 'fold' + (warn ? ' w' : ''); put(status, c3ic(warn ? 'alert' : 'info', 'sm'), t); };
|
|
80
|
+
let sh = null; let backOpen = false;
|
|
81
|
+
const keepBtn = pbtn('primary', isDraft ? 'Keep (copy to send yourself)' : 'Keep', 'check', { 'data-drv': 'sheet-keep', onclick: async () => {
|
|
82
|
+
const r = await press(ws, 'keep', { proposal: p.id }, 'Keep');
|
|
83
|
+
if (!r) return;
|
|
84
|
+
const how = isDraft && text ? await copyText(text, draft) : 'none';
|
|
85
|
+
done();
|
|
86
|
+
if (how === 'selected') { sayStatus('Kept. The browser did not allow copying, so the text is selected: press Ctrl+C. Nothing was sent.', true); keepBtn.disabled = true; return; }
|
|
87
|
+
sh.close();
|
|
88
|
+
toast(how === 'copied' ? 'Draft copied. Nothing was sent: send it yourself.' : 'Kept.');
|
|
89
|
+
} });
|
|
90
|
+
const backBtn = pbtn('outline', 'Send back', 'reply', { 'data-drv': 'sheet-back', onclick: async () => {
|
|
91
|
+
const inp = backBox.querySelector('input');
|
|
92
|
+
if (!backOpen) { backOpen = true; backBox.hidden = false; inp.focus(); backBtn.querySelector(SEL.btnLabel).textContent = 'Send back with note'; return; }
|
|
93
|
+
const n = inp.value.trim();
|
|
94
|
+
if (!n) { sayStatus('Write one line for the agent. A note starts its next run.', true); inp.focus(); return; }
|
|
95
|
+
const r = await press(ws, 'return', { proposal: p.id, note: n }, 'Send back');
|
|
96
|
+
if (!r) return;
|
|
97
|
+
sh.close(); toast('Sent back with your note. The agent redrafts it.'); done();
|
|
98
|
+
} });
|
|
99
|
+
const dropBtn = pbtn('ghost', 'Drop', null, { 'data-drv': 'sheet-drop', onclick: async () => {
|
|
100
|
+
const r = await press(ws, 'drop', { proposal: p.id }, 'Drop');
|
|
101
|
+
if (!r) return;
|
|
102
|
+
sh.close(); toast('Dropped the draft.'); done();
|
|
103
|
+
} });
|
|
104
|
+
sh = openSheet({ title: isDraft ? 'Draft follow-up' + (to ? ' to ' + to : '') : titleOf(p), icon: 'mail', body: el('div', { class: 'drv-sheet', 'data-drv': 'work-sheet' },
|
|
105
|
+
el('div', { class: 'chips' }, tag(isDraft ? 'draft' : p.what), to ? chip(c3ic('user', 'sm'), to) : null, p.at ? chip(c3ic('clock', 'sm'), whenLabel(p.at)) : null, chip(c3ic('check', 'sm'), short(item.title, 48))),
|
|
106
|
+
el('div', { class: 'case' }, draft),
|
|
107
|
+
p.why ? el('div', { class: 'dim drv-small' }, 'Why: ' + p.why) : null, status, backBox,
|
|
108
|
+
el('div', { class: 'iacts chips2' }, keepBtn, backBtn, dropBtn)) });
|
|
109
|
+
return sh;
|
|
110
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* pm/c3-driver.css — the few rules the Initiative Driver needs beyond console3.css (c3-driver.js, c3-driver-work.js). The mockup's
|
|
2
|
+
* 39-driver.css under the `drv-` prefix: the Agent strip on an initiative's Overview, the Agent board setting, the agent line on a Work
|
|
3
|
+
* row and the sheets. Held to the R28 rules: tokens only, no inline style, corners 0, 6px or 8px, font sizes from the --fs tokens. */
|
|
4
|
+
.c3 .drv .nrow .acts{flex-wrap:wrap;justify-content:flex-end}
|
|
5
|
+
.c3 .drv-src{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}
|
|
6
|
+
.c3 .drv-src .drv-ref{font-size:var(--fs-1);color:var(--muted-foreground);overflow-wrap:anywhere}
|
|
7
|
+
.c3 .drv-src a.drv-ref{color:var(--acc)}
|
|
8
|
+
.c3 .drv-pre{margin:0;white-space:pre-line;overflow-wrap:anywhere}
|
|
9
|
+
.c3 .drv-small{font-size:var(--fs-2);line-height:1.45}
|
|
10
|
+
.c3 .drv-sub{margin-top:2px}
|
|
11
|
+
.c3 .drv-set{display:grid;gap:var(--sp-2)}
|
|
12
|
+
.c3 .drv-set .seg{max-width:100%;overflow-x:auto}
|
|
13
|
+
.c3 .drv-note{display:block}
|
|
14
|
+
.c3 .drv-note .ic{vertical-align:-2px;margin-right:6px}
|
|
15
|
+
.c3 .drv-note .lk{margin-left:4px}
|
|
16
|
+
.c3 .drv-sheet{display:grid;gap:var(--sp-3)}
|
|
17
|
+
.c3 .drv-sheet .sit{white-space:pre-line;overflow-wrap:anywhere}
|
|
18
|
+
.c3 .drv-sheet input[type=text]{width:100%}
|