create-pmos 0.53.0 → 0.53.1

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.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/README.md +2 -0
  7. package/workspace/REMOTE.md +24 -63
  8. package/workspace/bin/pmos.js +4 -2
  9. package/workspace/guide/command-palette.md +1 -1
  10. package/workspace/guide/index.md +2 -1
  11. package/workspace/guide/launcher.md +51 -0
  12. package/workspace/guide/layout.md +4 -0
  13. package/workspace/guide/proactive-engine.md +21 -5
  14. package/workspace/lib/board-fold.js +2 -2
  15. package/workspace/lib/breakdown-export.js +1 -6
  16. package/workspace/lib/brief.js +20 -2
  17. package/workspace/lib/goals.js +165 -0
  18. package/workspace/lib/initiative-rows.js +7 -3
  19. package/workspace/lib/launcher-cli.js +207 -0
  20. package/workspace/lib/plan-tree.js +15 -2
  21. package/workspace/lib/pmos-verbs.js +3 -0
  22. package/workspace/lib/proactive-cli.js +58 -2
  23. package/workspace/lib/proactive.js +2 -2
  24. package/workspace/lib/qr.js +293 -0
  25. package/workspace/lib/remote-access.js +93 -1
  26. package/workspace/lib/report-draft.js +1 -2
  27. package/workspace/lib/routes-timeline.js +1 -1
  28. package/workspace/package-lock.json +1 -1
  29. package/workspace/package.json +1 -1
  30. package/workspace/public/icons/apple-touch-icon.png +0 -0
  31. package/workspace/public/icons/icon-192.png +0 -0
  32. package/workspace/public/icons/icon-512.png +0 -0
  33. package/workspace/public/index.html +11 -0
  34. package/workspace/public/pm/app.js +39 -8
  35. package/workspace/public/pm/components.css +82 -1
  36. package/workspace/public/pm/dest-today.js +8 -2
  37. package/workspace/public/pm/dock.js +73 -3
  38. package/workspace/public/pm/frame-copy.js +5 -0
  39. package/workspace/public/pm/panel-tabs.js +14 -12
  40. package/workspace/public/pm/today-copy.js +29 -0
  41. package/workspace/public/pm/today-goals.js +172 -0
  42. package/workspace/replaced-tests.json +13 -0
  43. package/workspace/server.js +134 -4
  44. package/workspace/skills/morning-brief.skill +15 -9
  45. package/workspace/test-files.json +2 -0
  46. package/workspace/test-lists/workspace-launcher.json +5 -0
  47. package/workspace/test-lists/workspace-phone-app.json +5 -0
@@ -94,6 +94,8 @@ const judgmentLib = require('./lib/judgment');
94
94
  const proactiveLib = require('./lib/proactive');
95
95
  const learnLib = require('./lib/learn');
96
96
  const briefLib = require('./lib/brief');
97
+ const goalsLib = require('./lib/goals');
98
+ const todayLib = require('./lib/today');
97
99
  const prepareQueue = require('./lib/prepare-queue');
98
100
  const harness = require('./lib/harness-parse');
99
101
  const transcript = require('./lib/transcript-read');
@@ -3621,6 +3623,21 @@ function prepareTick({ manual = false } = {}) {
3621
3623
  const PRO_SIGNALS = () => path.join(STATE, 'signals.jsonl');
3622
3624
  const PRO_FEEDBACK = () => path.join(STATE, 'proactive-feedback.jsonl');
3623
3625
  const PRO_SETTINGS = () => path.join(STATE, 'proactive-settings.json');
3626
+ // today-goals: one goals file per local day, and the newest stored brief (the same read GET /api/proactive/brief makes).
3627
+ const GOALS_DIR = () => path.join(STATE, 'goals');
3628
+ const goalsCardsMemo = new Map(); // ws -> { at, cards, error }: GET /api/proactive/goals reads Today's cards at most every 30 s
3629
+ function readGoalsDay(date) {
3630
+ let raw = null;
3631
+ try { raw = JSON.parse(fs.readFileSync(path.join(GOALS_DIR(), date + '.json'), 'utf8')); } catch { /* none yet */ }
3632
+ return goalsLib.readDay(raw, date);
3633
+ }
3634
+ function latestBrief() {
3635
+ try {
3636
+ const dir = path.join(STATE, 'briefs');
3637
+ const f = fs.readdirSync(dir).filter((n) => /^\d{4}-\d{2}-\d{2}\.json$/.test(n)).sort().pop();
3638
+ return f ? JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) : null;
3639
+ } catch { return null; }
3640
+ }
3624
3641
  let proLastScan = 0;
3625
3642
  const PRO_SCAN_EVERY_MS = 10 * 60 * 1000;
3626
3643
  function proSettings() {
@@ -4977,6 +4994,29 @@ async function handleRequest(req, res) {
4977
4994
  res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'max-age=86400' });
4978
4995
  return res.end(svg);
4979
4996
  }
4997
+ // workspace-phone-app: what Add to Home Screen reads, so the phone opens the workspace full screen
4998
+ // with its own icon. The manifest is built here rather than shipped as a file because its colours
4999
+ // are the dark theme's tokens and its start_url must be the bare page: never a ?token= URL, which
5000
+ // would store the secret in the phone's home-screen entry. No service worker, on purpose: this is
5001
+ // live state, and an offline cache would show a stale board as if it were current.
5002
+ if (p === '/manifest.webmanifest') {
5003
+ res.writeHead(200, { 'Content-Type': 'application/manifest+json', 'Cache-Control': 'no-cache' });
5004
+ return res.end(JSON.stringify({
5005
+ name: 'PMOS workspace', short_name: 'PMOS', id: '/', start_url: '/', scope: '/',
5006
+ display: 'standalone', orientation: 'any', background_color: '#090b0c', theme_color: '#090b0c',
5007
+ icons: [
5008
+ { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
5009
+ { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
5010
+ // The mark sits inside the central 62%, so the same image survives an Android maskable crop.
5011
+ { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
5012
+ ],
5013
+ }));
5014
+ }
5015
+ // The three icons, by name (an allowlist, like /lib/ above). iOS also asks for /apple-touch-icon.png
5016
+ // at the root unprompted, so that path answers too.
5017
+ if (p === '/icons/icon-192.png' || p === '/icons/icon-512.png' || p === '/icons/apple-touch-icon.png' || p === '/apple-touch-icon.png') {
5018
+ return serveFile(res, path.join(PUB, 'icons', path.basename(p)), 'image/png');
5019
+ }
4980
5020
  // xterm assets straight from node_modules (offline; no CDN)
4981
5021
  if (p === '/vendor/xterm.js') return serveFile(res, path.join(XROOT, 'xterm', 'lib', 'xterm.js'));
4982
5022
  if (p === '/vendor/xterm.css') return serveFile(res, path.join(XROOT, 'xterm', 'css', 'xterm.css'));
@@ -6911,6 +6951,66 @@ async function handleRequest(req, res) {
6911
6951
  if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
6912
6952
  return json(res, { ok: true, ws: w.id, name: w.name, ...validateLib.readFindings(validationFindingsFile(w)) });
6913
6953
  }
6954
+ /* ── today-goals (G1, G4, G5): the PM's goals for the day, the plan that joins them to the brief and Today, and "plan my
6955
+ * day". The goals routes write only goals/<date>.json in the state folder. "Plan my day" seeds the morning-brief
6956
+ * schedule when the repository has none and runs it through fireSchedule, the run-now path. Nothing else starts. ── */
6957
+ if (p === '/api/proactive/goals' && req.method === 'GET') {
6958
+ const now = Date.now();
6959
+ const date = briefLib.localDate(now);
6960
+ const w = getWs(url.searchParams.get('ws'));
6961
+ const day = readGoalsDay(date);
6962
+ const brief = latestBrief();
6963
+ let cards = []; let cardsError = null;
6964
+ // Today's cards are read only when an open goal can take them, and at most once per 30 seconds per repository: the
6965
+ // read is synchronous and heavy on a large repository, and the Today feed itself must not wait behind it.
6966
+ if (w && day.goals.some((x) => x.status === 'open')) {
6967
+ const memo = goalsCardsMemo.get(w.id);
6968
+ if (memo && now - memo.at < 30000) ({ cards, error: cardsError } = memo);
6969
+ else {
6970
+ let sess = []; try { sess = sessionList(); } catch { /* no session cards */ }
6971
+ const t = todayLib.todayFor(w, { stateDir: stateDirFor(w), sessions: sess, registered: (root) => { const x = workspaces.get(wsId(root)); return !!x && path.resolve(x.root) === path.resolve(root); } });
6972
+ if (t.ok) cards = t.cards; else cardsError = t.error;
6973
+ goalsCardsMemo.set(w.id, { at: now, cards, error: cardsError });
6974
+ }
6975
+ }
6976
+ const plan = goalsLib.planDay({ day, brief, cards, now });
6977
+ const sched = readSchedules().find((x) => x.skill === 'morning-brief' && (!w || x.ws === w.id)) || null;
6978
+ return json(res, { ok: true, ws: w ? w.id : null, today: date, plan, cards_error: cardsError, max_goals: goalsLib.MAX_GOALS,
6979
+ schedule: sched ? { id: sched.id, time: sched.time, lastRun: sched.lastRun, lastStatus: sched.lastStatus } : null,
6980
+ note: 'Goals are kept in the workspace state folder on this computer. The plan joins them to the morning brief and Today by goal id or initiative, never by guessing.' });
6981
+ }
6982
+ if (p === '/api/proactive/goals' && req.method === 'POST') {
6983
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6984
+ const now = Date.now();
6985
+ const date = briefLib.localDate(now);
6986
+ const r = goalsLib.edit(readGoalsDay(date), body, { now });
6987
+ if (!r.ok) return json(res, { ok: false, error: r.error }, /no goal with that id/.test(r.error) ? 404 : 400);
6988
+ try { fs.mkdirSync(GOALS_DIR(), { recursive: true }); fs.writeFileSync(path.join(GOALS_DIR(), date + '.json'), JSON.stringify(r.day) + '\n'); }
6989
+ catch (e) { warnWriteFail('goals', e); return json(res, { ok: false, error: 'could not save — nothing changed' }, 500); }
6990
+ return json(res, { ok: true, day: r.day, goal: r.goal });
6991
+ }
6992
+ if (p === '/api/proactive/goals/plan' && req.method === 'POST') {
6993
+ // A PM press: make sure the morning brief is scheduled for this repository, then run it now. The session reads the
6994
+ // goals with `pmos proactive goals` and submits a brief whose items name them.
6995
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6996
+ const w = getWs(body.ws);
6997
+ if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
6998
+ const open = readGoalsDay(briefLib.localDate(Date.now())).goals.filter((g) => g.status === 'open');
6999
+ if (!open.length) return json(res, { ok: false, error: 'add a goal for today first: the plan is made for your goals' }, 409);
7000
+ let sched = readSchedules().find((x) => x.ws === w.id && x.skill === 'morning-brief');
7001
+ let seeded = false;
7002
+ if (!sched) {
7003
+ if (!taskById('morning-brief', w.root)) return json(res, { ok: false, error: 'the morning-brief skill is not installed for this repository' }, 409);
7004
+ const norm = scheduleLogic.normalizeSchedule({ ...SEED_SETS.brief[0], task: 'morning-brief' });
7005
+ if (!norm.ok) return json(res, { ok: false, error: norm.error }, 500);
7006
+ sched = { id: 'sc-' + Date.now().toString(36) + 'g', ws: w.id, ...norm.schedule, profile: null, lastRun: null, lastStatus: null };
7007
+ sched.nextRun = scheduleLogic.nextRun(sched);
7008
+ const list = readSchedules(); list.unshift(sched); writeSchedules(list); seeded = true;
7009
+ }
7010
+ const result = fireSchedule(sched);
7011
+ return json(res, { ok: !!result.ok, seeded, schedule: { id: sched.id, time: sched.time }, result, goals: open.length,
7012
+ ...(result.ok ? {} : { error: result.reason || 'the morning-brief session did not start' }) }, result.ok ? 200 : 500);
7013
+ }
6914
7014
  /* ── background prepare (D3): plan is read-only; queue records an approval; the runner starts it later ── */
6915
7015
  if (p === '/api/proactive/prepare/plan' && req.method === 'POST') {
6916
7016
  let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
@@ -6984,7 +7084,8 @@ async function handleRequest(req, res) {
6984
7084
  const dir = path.join(STATE, 'briefs');
6985
7085
  if (req.method === 'POST') {
6986
7086
  let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6987
- const v = briefLib.validateBrief(body, { now: Date.now(), settings: proSettings() });
7087
+ const bday = body && typeof body.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(body.date) ? body.date : briefLib.localDate(Date.now());
7088
+ const v = briefLib.validateBrief(body, { now: Date.now(), settings: proSettings(), goalIds: readGoalsDay(bday).goals.map((g) => g.id) });
6988
7089
  if (!v.ok) return json(res, { ok: false, error: v.error }, 400);
6989
7090
  try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, v.brief.date + '.json'), JSON.stringify(v.brief) + '\n'); }
6990
7091
  catch (e) { warnWriteFail('briefs', e); return json(res, { ok: false, error: 'could not save — nothing changed' }, 500); }
@@ -7460,8 +7561,31 @@ function httpGuard(req, res) {
7460
7561
  // sub-resource loads (CSS/JS) or the WS upgrade — so the documented "open http://…/?token=<t>"
7461
7562
  // phone flow (REMOTE.md + startup banner) must reach THIS page-load guard, and every later request
7462
7563
  // rides the cookie we seed just below. presentedToken() resolves all channels. (local hotfix)
7564
+ // workspace-phone-app: the sign-in form posts here. It is the one route an unauthorized peer may
7565
+ // reach, and all it can do is compare a token (constant-time) and, on a match, seed the same
7566
+ // cookie a valid ?token= navigation seeds. The body is capped; nothing about it is logged (D08).
7567
+ const navigation = remoteAccess.isPageNavigation({ method: req.method, accept: req.headers.accept });
7568
+ if (req.method === 'POST' && (req.url || '').split('?')[0] === remoteAccess.AUTH_PATH) {
7569
+ let body = '';
7570
+ req.on('data', (c) => { body += c; if (body.length > 4096) req.destroy(); });
7571
+ req.on('end', () => {
7572
+ if (remoteAccess.tokenMatches(remoteAccess.parseAuthForm(body), TOKEN)) {
7573
+ res.writeHead(303, { Location: '/', 'Set-Cookie': remoteAccess.sessionCookie(TOKEN), 'Cache-Control': 'no-store' });
7574
+ return res.end();
7575
+ }
7576
+ res.writeHead(401, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
7577
+ res.end(remoteAccess.signInPage({ failed: true }));
7578
+ });
7579
+ return;
7580
+ }
7463
7581
  if (!remoteAccess.authorize({ remoteAddress: req.socket.remoteAddress,
7464
7582
  presentedToken: remoteAccess.presentedToken(req), expectedToken: TOKEN })) {
7583
+ // A phone's browser gets a page it can act on, not JSON. API calls, scripts and anything else
7584
+ // that is not a page load keep the JSON 401 they had.
7585
+ if (navigation) {
7586
+ res.writeHead(401, { 'Content-Type': 'text/html; charset=utf-8', 'WWW-Authenticate': 'Bearer', 'Cache-Control': 'no-store' });
7587
+ return res.end(req.method === 'HEAD' ? undefined : remoteAccess.signInPage());
7588
+ }
7465
7589
  res.writeHead(401, { 'Content-Type': 'application/json', 'WWW-Authenticate': 'Bearer' });
7466
7590
  res.end(JSON.stringify({ ok: false, error: 'unauthorized' }));
7467
7591
  return;
@@ -7474,8 +7598,14 @@ function httpGuard(req, res) {
7474
7598
  let qTok = null;
7475
7599
  try { qTok = new URL(req.url || '', 'http://localhost').searchParams.get('token'); } catch { /* ignore */ }
7476
7600
  if (qTok && remoteAccess.tokenMatches(qTok.trim(), TOKEN)) {
7477
- res.setHeader('Set-Cookie',
7478
- `${remoteAccess.COOKIE_NAME}=${encodeURIComponent(TOKEN)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=604800`);
7601
+ res.setHeader('Set-Cookie', remoteAccess.sessionCookie(TOKEN));
7602
+ // workspace-phone-app: on a page load, redirect to the same URL without the token, so it does
7603
+ // not stay in the address bar, the history, or the start_url of a home-screen install.
7604
+ const clean = navigation ? remoteAccess.stripTokenParam(req.url) : null;
7605
+ if (clean) {
7606
+ res.writeHead(303, { Location: clean, 'Cache-Control': 'no-store' });
7607
+ return res.end();
7608
+ }
7479
7609
  }
7480
7610
  }
7481
7611
  return handleRequest(req, res);
@@ -7667,7 +7797,7 @@ if (require.main === module) {
7667
7797
  // old EADDRINUSE advice told the PM to do, on another port) has to leave the same trace as
7668
7798
  // one started by `pmos` — otherwise the next `pmos` cannot see it and starts a second server.
7669
7799
  try { fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PID_FILE, String(process.pid)); } catch {}
7670
- for (const line of remoteAccess.startupLines(BIND, PORT)) console.log(line);
7800
+ for (const line of remoteAccess.startupLines(BIND, PORT, { ansi: !!process.stdout.isTTY && !process.env.NO_COLOR })) console.log(line);
7671
7801
  });
7672
7802
  });
7673
7803
  }
@@ -1,20 +1,21 @@
1
1
  ---
2
2
  name: morning-brief
3
3
  label: "Morning brief"
4
- description: "Prepare today's brief: the calendar, the priorities and what is worth asking. Read-only on every source; it submits the brief to the workspace and sends nothing."
4
+ description: "Prepare today's brief and plan the PM's goals for the day: the calendar, Slack, Jira and Confluence mentions, the priorities and what is worth asking. Read-only on every source; it submits the brief to the workspace and sends nothing."
5
5
  run: agent
6
6
  agent: claude
7
7
  output:
8
+ - Goals
8
9
  - Calendar
9
10
  - Priorities
10
11
  - Worth asking
11
12
  - Sources reached and not reached
12
13
  ---
13
14
 
14
- /morning-brief — prepare today's brief for the PM.
15
+ /morning-brief — prepare today's brief for the PM, and plan the goals they set for today.
15
16
 
16
17
  Rules that always hold:
17
- 1. READ-ONLY on every source. Never send, reply, forward, post, comment, create, edit, accept, decline, transition, label, archive or delete anything in mail, chat, Jira, Confluence, calendar, Wispr Flow or any other source.
18
+ 1. READ-ONLY on every source. Never send, reply, forward, post, react, comment, create, edit, accept, decline, transition, label, archive, mark read or delete anything in mail, Slack, Jira, Confluence, calendar, Wispr Flow or any other source.
18
19
  2. SETTINGS DECIDE THE SOURCES. Run `pmos proactive settings`. Read a source only if its line under "brief sources" says on. A source that is off is reported as "off". Mail is off unless the PM turned it on.
19
20
  3. LIGHT ON CONTENT. Titles, times, names and ids only. Never copy a message body, an attachment, a token or a credential. Skip anything marked private, personal, confidential or HR. At most one short evidence line per item.
20
21
  4. NOTHING INVENTED. A source you cannot reach in this session is reported as "not-reached". If nothing is waiting, submit a brief with no items. An honest short brief is correct.
@@ -23,21 +24,26 @@ Rules that always hold:
23
24
 
24
25
  Procedure:
25
26
  1. Run `pmos proactive settings` and `pmos proactive today`. The second lists what the workspace derived from the ledgers. Each of its items may become a priority with source "derived".
27
+ Then run `pmos proactive goals`. It lists the PM's goals for today, each with its id (g-xxxxxxxx), its status and, if set, an initiative. The goals are what the PM wants done today: plan for them first.
26
28
  Then run `pmos proactive learn lessons`. Each line is a preference the PM approved, written from their own feedback. Use it only to decide how confident to be and where an item goes. A lesson never asks you to read another source, to switch a setting on, or to write anything.
27
29
  2. Read today's events from the calendar and today's and tomorrow's meeting notes from Wispr Flow, if those sources are on and reachable. Read Jira issues assigned to the PM or due soon, and Confluence pages changed since yesterday, if on and reachable. Read mail only if on: subjects and senders of threads waiting on the PM, no bodies.
28
- 3. For each thing the PM should act on today, write one item: a short title, why it is for today, its source, the initiative id if one applies, who acts (pm or agent), effort (minutes, an-hour, half-day, day), your confidence from 0 to 1 that it belongs on today's list, and one short evidence line. Give a low confidence when you are guessing. The workspace shows an item under 0.5 as "worth asking", not as a priority.
30
+ Read MENTIONS of the PM since yesterday morning, if the source is on and reachable: Slack messages that @-mention the PM or are direct messages to them; Jira issues and comments that mention the PM; Confluence pages and comments that mention the PM. For each, keep only where it is (channel, issue key, page title), who wrote it, and a one-line paraphrase of what is asked. Never copy the message itself.
31
+ 3. PLAN EACH OPEN GOAL. For each open goal, find what moves it today in what you read and in `pmos proactive today`: a meeting where it is decided, a mention that blocks it, an issue or page it needs, a step an agent can prepare. Write each as an item with "goal": "<the goal id>". An agent step names the initiative it advances, so the PM can prepare it from Today. A calendar event that belongs to a goal also gets "goal". If you find nothing for a goal, write nothing for it: the workspace shows that goal as needing a plan. Never invent a step.
32
+ Then ANTICIPATE: for each other thing the PM should act on today (a mention waiting on them, a meeting to prepare for, an issue due), write one item with no goal.
33
+ For each item, write: a short title, why it is for today, its source, the initiative id if one applies, who acts (pm or agent), effort (minutes, an-hour, half-day, day), your confidence from 0 to 1 that it belongs on today's list, and one short evidence line. Set "mention": true on an item that comes from a mention of the PM. Give a low confidence when you are guessing. The workspace shows an item under 0.5 as "worth asking", not as a priority.
29
34
  4. Write the brief as a JSON file in the scratch folder of this session, in this shape, and nothing else:
30
35
  ```
31
- {"date": "YYYY-MM-DD", "sources": {"calendar": "read", "wispr": "not-reached", "mail": "off", "jira": "read", "confluence": "read"},
32
- "calendar": [{"start": "<ISO date and time>", "end": "<ISO>", "title": "…", "with": "…"}],
33
- "items": [{"title": "…", "why_now": "…", "source": "calendar|wispr|mail|jira|confluence|derived", "initiative_id": "<id or omit>", "owner": "pm|agent", "effort": "minutes|an-hour|half-day|day", "confidence": 0.0, "evidence": "…"}],
36
+ {"date": "YYYY-MM-DD", "sources": {"calendar": "read", "wispr": "not-reached", "mail": "off", "slack": "read", "jira": "read", "confluence": "read"},
37
+ "calendar": [{"start": "<ISO date and time>", "end": "<ISO>", "title": "…", "with": "…", "goal": "<goal id or omit>"}],
38
+ "items": [{"title": "…", "why_now": "…", "source": "calendar|wispr|mail|slack|jira|confluence|derived", "initiative_id": "<id or omit>", "goal": "<goal id or omit>", "mention": false, "owner": "pm|agent", "effort": "minutes|an-hour|half-day|day", "confidence": 0.0, "evidence": "…"}],
34
39
  "judgment": {"used": false}}
35
40
  ```
36
- Only report calendar events if calendar is "read". Only give an item a source that is "read" (or "derived").
41
+ Only report calendar events if calendar is "read". Only give an item a source that is "read" (or "derived"). Only use a goal id that `pmos proactive goals` listed today: the workspace refuses any other.
37
42
  5. Run `pmos proactive brief submit --file <that file>`. If it is refused, read the reason, fix the file and submit once more. Do not work around a refusal.
38
- 6. Finish by saying in two lines how many priorities and worth-asking items you submitted and which sources were not reached.
43
+ 6. Finish by saying in three lines how many goals have a step and which have none, how many priorities and worth-asking items you submitted, and which sources were not reached.
39
44
 
40
45
  Output contract — produce EXACTLY these sections in your final message:
46
+ - Goals
41
47
  - Calendar
42
48
  - Priorities
43
49
  - Worth asking
@@ -9,6 +9,7 @@
9
9
  ],
10
10
  "modules": [
11
11
  "proactive.test.js",
12
+ "goals.test.js",
12
13
  "focus.test.js",
13
14
  "handoff.test.js",
14
15
  "repo-scan.test.js",
@@ -104,6 +105,7 @@
104
105
  "seconds": {
105
106
  "test.js": 48,
106
107
  "proactive.test.js": 4,
108
+ "goals.test.js": 6,
107
109
  "focus.test.js": 1,
108
110
  "handoff.test.js": 1,
109
111
  "repo-scan.test.js": 1,
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "workspace-launcher: `pmos launcher on|off|status` (lib/launcher-cli.js).",
3
+ "modules": ["launcher.test.js"],
4
+ "seconds": { "launcher.test.js": 2 }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "workspace-phone-app: the tailnet branches of the access guard (sign-in page, /auth, the token-stripping redirect), driven through httpGuard as a library because test.js runs loopback-only.",
3
+ "modules": ["phone-access.test.js"],
4
+ "seconds": { "phone-access.test.js": 3 }
5
+ }