create-pmos 0.147.0 → 0.149.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.
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * What a press does:
9
9
  * - a section row opens IN PLACE (c3-expand): why it is here, the claims with their label and a Traced / Needs your input mark, the sources as
10
- * jump chips (they click the Initiatives, Calendar, Work or Briefing tab), and "Fix with the agent" (disabled, with the reason).
10
+ * jump chips (they click the Initiatives, Calendar, Work or Briefing tab), and "Fix with the agent": it opens the report as a document on Ask the
11
+ * agent with the fix written in, so the agent starts from the page and its draft is accepted only by the PM's slide (page-starts-agent).
11
12
  * - a swipe right approves a section and a swipe left cuts it (c3-swipe). In DEMO the decision is kept on the page, with an Undo toast. In REAL mode
12
13
  * no route stores a section decision, so the swipe is off and the legend and the row say why.
13
14
  * - the Send row opens the case file (c3-case) with the slide. DEMO: the slide marks the report Sent and adds it to the Sent card (nothing leaves).
@@ -27,10 +28,11 @@ import { openSheet } from './c3-sheet.js';
27
28
  import { toast } from './c3-toast.js';
28
29
  import { openDocSheet, splitDoc } from './c3-doc-sheet.js';
29
30
  import { forecastChecksCard } from './c3-misc-panels.js';
31
+ import { c3btn } from './page-agent.js';
30
32
 
31
33
  const insts = new WeakMap();
32
34
  const FAM_ICON = { project: 'tree', product: 'target', communications: 'out', periodic: 'cal' };
33
- const FIX_WHY = 'Not available here: the agent starts from a terminal session, not from this page.';
35
+ const fixPrompt = (s) => (s.st === 'stale' ? 'Bring the section “' + s.h + '” up to date with its sources.' : 'Fix the section “' + s.h + '”: give each claim a label (fact, cited or reported) and its source or ticket key.');
34
36
  const NO_STORE = 'Approving or cutting a section is not recorded here yet: no record stores a section decision.';
35
37
  const dim = (t) => el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, t);
36
38
  const rx = () => (typeof window !== 'undefined' && window.__c3demo && window.__c3repix) || null;
@@ -156,7 +158,7 @@ function claimRow(c) {
156
158
  return el('li', { class: bad ? 'bad' : L[1], 'data-claim': '' }, c3ic(bad ? 'alert' : L[0], 'sm'), el('span', null, c.text + ' ', el('b', { class: 'dim', style: 'font-weight:500' }, traced ? 'Traced' : 'Needs your input')),
157
159
  el('span', { class: 'tag' }, (c.label || 'unlabelled') + (c.source ? ' · ' + c.source : '')));
158
160
  }
159
- function detail(s) {
161
+ function detail(s, onFix) {
160
162
  const claims = s.claims && s.claims.length ? s.claims.map(claimRow) : [el('li', { class: s.st === 'ok' ? 'ok' : s.st === 'stale' ? 'warn' : 'warn' }, c3ic(STATE[s.st][2], 'sm'), el('span', null, STATE[s.st][1] + (s.st === 'stale' ? ': a source is out of date. Refresh it before this goes out.' : s.st === 'check' ? ': a person decides this one.' : ': every number has a source.')), el('span', { class: 'tag' }, 'claims check'))];
161
163
  const chips = s.src.length ? s.src.map((q) => (q[2] ? el('button', { type: 'button', class: 'chip2 lk', 'data-a': 'rsrc', 'data-k': q[2], onClick: (e) => { e.stopPropagation(); jump(q[2]); } }, c3ic(q[0], 'sm'), q[1], c3ic('next', 'sm'))
162
164
  : el('span', { class: 'chip2' }, c3ic(q[0], 'sm'), q[1]))) : [el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, s.by === 'You' ? 'Your judgement. No source to trace.' : 'No source is recorded for this section.')];
@@ -164,15 +166,15 @@ function detail(s) {
164
166
  s.why ? el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, c3ic('info', 'sm'), ' Why it is here: ' + s.why) : null,
165
167
  el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Claims check'), el('ul', { class: 'chgl' }, claims),
166
168
  el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Where it comes from'), el('div', { class: 'chips2', style: 'flex-wrap:wrap' }, chips),
167
- s.st !== 'ok' ? el('div', { class: 'chips2', style: 'flex-wrap:wrap' }, el('button', { type: 'button', class: 'chip2', disabled: true, title: FIX_WHY, 'data-rp-act': 'fix' }, c3ic('spark', 'sm'), 'Fix with the agent'), el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, FIX_WHY)) : null);
169
+ s.st !== 'ok' ? el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-rp-act': 'fix' }, [c3ic('spark', 'sm'), 'Fix with the agent'], (e) => { e.stopPropagation(); if (onFix) onFix(s); })) : null);
168
170
  }
169
171
 
170
- function sectionRow(inst, ctx, fam, key, s) {
172
+ function sectionRow(inst, ctx, fam, key, s, onFix) {
171
173
  const DEMO = !!rx();
172
174
  const k = key + '|' + s.id; const dst = decOf(inst, key, s.id);
173
175
  const cc = STATE[s.st]; const o = ORIG[s.by] || ORIG.Agent;
174
176
  const li = el('li', { class: 'nrow' + (dst === 'cut' ? ' cutr' : ''), 'data-row': '', 'data-section': s.id, 'data-rs': k, tabindex: '0', style: dst === 'cut' ? 'opacity:.5' : null });
175
- const xp = el('div', { class: 'xp' }, el('div', { class: 'xi' }, detail(s)));
177
+ const xp = el('div', { class: 'xp' }, el('div', { class: 'xi' }, detail(s, onFix)));
176
178
  li.append(el('span', { class: 'ki', style: 'color:' + (dst === 'ok' ? 'var(--success)' : cc[0]) }, c3ic(dst === 'ok' ? 'check' : dst === 'cut' ? 'x' : cc[2])),
177
179
  el('div', { class: 'nb' }, el('span', { class: 'tt' }, s.h), el('div', { class: 'dim', style: 'font-size:var(--fs-2);margin-top:2px' }, s.t),
178
180
  el('div', { class: 'chips' }, el('span', { class: 'chip2', style: 'color:' + o[1] }, c3ic(o[0], 'sm'), o[2]), el('span', { class: 'chip2', style: 'color:' + cc[0] }, c3ic(cc[2], 'sm'), cc[1]),
@@ -237,7 +239,7 @@ function secExtraOf(secs, getSheet) {
237
239
  }
238
240
 
239
241
  /** Open a report (or a communications message) as a document. path/name name it; secs are its sections; mode is the segment to start on. */
240
- async function openReportDoc(inst, ctx, { fam, path, name, aud, repName, secs, mode }) {
242
+ async function openReportDoc(inst, ctx, { fam, path, name, aud, repName, secs, mode, prompt }) {
241
243
  const ws = (ctx && ctx.ws) || '';
242
244
  let text = null;
243
245
  try { const r = await fetch('/api/doc?ws=' + encodeURIComponent(ws) + '&path=' + encodeURIComponent(path)); if (r.ok) text = await r.text(); } catch { /* no stored document */ }
@@ -246,7 +248,7 @@ async function openReportDoc(inst, ctx, { fam, path, name, aud, repName, secs, m
246
248
  if (stored) inst.docText[path] = text;
247
249
  let sheet = null; const getSheet = () => sheet;
248
250
  const doc = { path, name, ty: 'Report', v: stored ? 'v1' : '', st: 'draft', ini: '', iniName: 'Portfolio' };
249
- sheet = openDocSheet({ ws, doc, mode: mode || 'read', relations: reportRelations(inst, secs, repName || name, getSheet), secExtra: secExtraOf(secs, getSheet),
251
+ sheet = openDocSheet({ ws, doc, mode: mode || 'read', prompt: prompt || '', relations: reportRelations(inst, secs, repName || name, getSheet), secExtra: secExtraOf(secs, getSheet),
250
252
  generated: stored ? null : { text: mdOf(secs), why: NO_STORE_WHY, health: generatedHealth(secs) },
251
253
  onChanged: async () => { try { const r = await fetch('/api/doc?ws=' + encodeURIComponent(ws) + '&path=' + encodeURIComponent(path)); if (r.ok) { inst.docText[path] = await r.text(); draw(inst, ctx); } } catch { /* the rows keep what they show */ } } });
252
254
  void aud; void fam;
@@ -457,7 +459,7 @@ function draw(inst, ctx) {
457
459
  const doc = el('section', { class: 'card sect', 'data-rp-section': 'family', 'aria-label': fam.name },
458
460
  el('div', { class: 'sh2' }, el('h2', null, c3ic('doc', 'sm'), repName + ' ', R ? el('span', { class: 'chip2' }, c3ic('user', 'sm'), audName) : el('span', { class: 'bdg g' }, String(secs.length)), R ? null : dim(vr.name)),
459
461
  el('span', { class: 'chips2' }, el('button', { type: 'button', class: 'ib sm', 'data-a': 'docopen', title: 'Open as a document to edit by hand or ask the agent', 'aria-label': 'Open as a document to edit by hand or ask the agent', onClick: () => openReportDoc(inst, ctx, { fam, path: pathOf(inst, fam), name: repName + ' for ' + audName, aud: audName, repName, secs: secs.filter((s) => decOf(inst, key, s.id) !== 'cut') }) }, c3ic('notes', 'sm')))),
460
- el('ul', { class: 'list' }, secs.length ? [...secs.map((s) => sectionRow(inst, ctx, fam, key, s)), sendRow(inst, ctx, fam, key, secs, q, audName, repName, vr)] : el('li', { class: 'fold' }, c3ic('check', 'sm'), 'This family has no sections.')),
462
+ el('ul', { class: 'list' }, secs.length ? [...secs.map((s) => sectionRow(inst, ctx, fam, key, s, (x) => openReportDoc(inst, ctx, { fam, path: pathOf(inst, fam), name: repName + ' for ' + audName, aud: audName, repName, secs: secs.filter((q) => decOf(inst, key, q.id) !== 'cut'), mode: 'ask', prompt: fixPrompt(x) }))), sendRow(inst, ctx, fam, key, secs, q, audName, repName, vr)] : el('li', { class: 'fold' }, c3ic('check', 'sm'), 'This family has no sections.')),
461
463
  el('div', { class: 'pgl' }, R ? [el('span', null, 'swipe right: approve'), el('span', null, 'swipe left: cut'), el('span', null, 'tap: where it comes from')] : [el('span', null, 'tap: where it comes from'), el('span', null, 'approve and cut: off, no record stores them')]),
462
464
  notes.length ? el('div', { 'data-rp-section': 'check-notes', style: 'display:grid;gap:var(--sp-2)' }, notes) : null);
463
465
  const rf = (ctx && ctx.reportFacts) || {};
@@ -76,6 +76,7 @@ const jevRoute = require('./lib/jev-route.js'); // prompt-model-routing —
76
76
  const jevClaims = require('./lib/jev-claims.js'); // D80 — the bridge between the claims ledger and the one TypeSafe module
77
77
  const jevCategorize = require('./lib/jev-categorize.js'); // D82 — the bridge between the document categorizer and the one TypeSafe module
78
78
  const validateLib = require('./lib/validate.js');
79
+ const pageAgent = require('./lib/page-agent.js'); // page-starts-agent — the fixed prompts a page press may start
79
80
  const headlessLib = require('./lib/headless.js'); // workspace-headless-jobs — which jobs run as `claude -p`, and with which tools
80
81
  const liveNotes = require('./lib/live-notes.js'); // jev-live-notes — reads and appends a repository's live-notes ledger; no network
81
82
  const jevLive = require('./lib/jev-live.js'); // jev-live-notes Run 7 (D85) — the live view's one bridge to Jev, behind three gates
@@ -3374,7 +3375,7 @@ function sendToSession(id, text) {
3374
3375
 
3375
3376
  /* A headless run (workspace-headless-jobs). The row is an ordinary session record, so every caller that checks a
3376
3377
  * session's state keeps working; it has no pty and no tmux, and `child` is the process. The log is read-only. */
3377
- function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDirs) {
3378
+ function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDirs, onDone) {
3378
3379
  const prompt = safeFirstInput(String(initialInput).replace(/\r+$/, ''));
3379
3380
  const args = headlessLib.argsFor(kind, { modelArgs: launchArgs, addDirs: Array.isArray(addDirs) ? addDirs : [] });
3380
3381
  const s = { ...base, cwd: workdir, state: 'working', pty: null, tmux: null, headless: kind, child: null,
@@ -3425,6 +3426,8 @@ function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDi
3425
3426
  : `exited (code ${code})`;
3426
3427
  if (s.note !== 'exited (code 0)') say(`\x1b[38;5;203m[${s.note}]\x1b[0m\n`);
3427
3428
  persist(s); pushSessions();
3429
+ // page-starts-agent: a caller that started this run from the page reads its result here (a proposal, never a write).
3430
+ if (typeof onDone === 'function') { try { onDone({ result: r, code, signal, timedOut, stopped: !!s.stoppedByPm, note: s.note }); } catch (e) { warnWriteFail('headless result', e); } }
3428
3431
  });
3429
3432
  child.stdin.on('error', () => {});
3430
3433
  child.stdin.end(prompt);
@@ -3445,7 +3448,7 @@ function killHeadless(child, sig) {
3445
3448
  try { process.kill(-child.pid, sig); } catch { try { child.kill(sig); } catch {} }
3446
3449
  }
3447
3450
 
3448
- function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags, addDirs }) {
3451
+ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags, addDirs, onDone }) {
3449
3452
  const id = nextSessionId();
3450
3453
  /* pm-os-absorb Run 1 — every AGENT session gets a token, `<id>.<8 hex>`, in its environment as PMOS_SESSION, and
3451
3454
  * PMOS_BIN, this workspace's own bin/pmos.js. `pmos item` run inside it is then attributed `agent:<token>` (the token
@@ -3558,7 +3561,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
3558
3561
  // ── the headless path (workspace-headless-jobs): a background job that needs no human runs as `claude -p`, with
3559
3562
  // no pane, the same environment and model, a fixed tool list and the prompt on stdin (lib/headless.js says which).
3560
3563
  const hkind = initialInput ? headlessLib.kindFor({ agent, origin: tag, skill: modelAsk && modelAsk.skill }) : null;
3561
- if (hkind) return startHeadless(base, workdir, hkind, argv, penv, initialInput, addDirs);
3564
+ if (hkind) return startHeadless(base, workdir, hkind, argv, penv, initialInput, addDirs, onDone);
3562
3565
 
3563
3566
  // ── the tmux path: the process outlives this server, and we re-attach to it live (R7) ──
3564
3567
  if (TMUX) {
@@ -7561,6 +7564,20 @@ async function handleRequest(req, res) {
7561
7564
  kitBusy = 'app';
7562
7565
  return json(res, { ok: true, ws: w.id, from: APP_VERSION, to: body.version, log: r.log });
7563
7566
  }
7567
+ /* ── page-starts-agent: a console3 press starts an agent session (a draft, a spike, an early gate), shown in Sessions ── */
7568
+ if (p === '/api/agent/start' && req.method === 'POST') {
7569
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
7570
+ const g = pmGuard.requirePagePress(req, body);
7571
+ if (!g.ok) return json(res, { ok: false, error: g.error }, g.status);
7572
+ const w = getWs(body.ws);
7573
+ if (!w) return json(res, { ok: false, error: 'no workspace' }, 404);
7574
+ const job = pageAgent.jobFor(body);
7575
+ if (!job.ok) return json(res, job, 400);
7576
+ const started = startWork({ initiative: job.initiative || undefined, ws: w.id, agent: 'claude', name: job.name, origin: 'page-agent:' + job.task, initialInput: job.prompt + '\r' });
7577
+ if (!started.ok) return json(res, { ok: false, error: started.error }, 409);
7578
+ if (started.session.state === 'exited') return json(res, { ok: false, error: started.session.note || 'the agent could not start', session: { id: started.session.id } }, 409);
7579
+ return json(res, { ok: true, session: { id: started.session.id, name: started.session.name }, notice: started.notice || null });
7580
+ }
7564
7581
  /* ── next-step actions: prepare is read-only; go starts exactly what was shown; review records the PM's word ── */
7565
7582
  if (p === '/api/next-step/prepare' && req.method === 'POST') {
7566
7583
  let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Initiative c3-meeting-card: the console3 Calendar's card before a meeting (In short, What matters today, Since the last one, To prepare, What PMOS knows).",
3
+ "panels": ["pm-c3-meeting-card-test.js"],
4
+ "seconds": { "pm-c3-meeting-card-test.js": 40 }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Initiative page-starts-agent: Ask the agent, Fix with the agent, drafts, spikes and early gates start the agent from the console3 page (lib/page-agent.js, the doc-sheet ask route), against a stub claude.",
3
+ "modules": ["page-starts-agent.test.js"],
4
+ "seconds": { "page-starts-agent.test.js": 20 }
5
+ }