create-pmos 0.61.1 → 0.63.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/first-paint-audit.js +4 -2
- package/workspace/guide/index.md +6 -3
- package/workspace/guide/layout.md +5 -4
- package/workspace/guide/notes.md +72 -0
- package/workspace/guide/people.md +70 -0
- package/workspace/lib/calendar-memory.js +2 -1
- package/workspace/lib/jev-notes.js +162 -0
- package/workspace/lib/notes-repo.js +133 -0
- package/workspace/lib/notes-whisper.js +200 -0
- package/workspace/lib/notes.js +511 -0
- package/workspace/lib/people-cli.js +135 -0
- package/workspace/lib/people-store.js +123 -0
- package/workspace/lib/people.js +399 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-notes.js +230 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +2 -2
- package/workspace/public/pm/components.css +441 -0
- package/workspace/public/pm/dest-people.js +385 -0
- package/workspace/public/pm/dest-settings.js +44 -3
- package/workspace/public/pm/frame-copy.js +7 -0
- package/workspace/public/pm/initiative-sheet.js +5 -2
- package/workspace/public/pm/initiatives-copy.js +1 -0
- package/workspace/public/pm/item-sheet.js +18 -1
- package/workspace/public/pm/panel-cockpit.js +3 -0
- package/workspace/public/pm/people-link.js +106 -0
- package/workspace/public/pm/registry.js +2 -0
- package/workspace/public/pm/today-agenda.js +3 -1
- package/workspace/public/pm/today-day.js +2 -0
- package/workspace/public/pm/topbar.js +4 -3
- package/workspace/public/pm/view-notes.js +723 -0
- package/workspace/replaced-tests.json +14 -0
- package/workspace/server.js +30 -1
- package/workspace/skills/gather-requirements.skill +5 -3
- package/workspace/skills/jira-sync.skill +5 -4
- package/workspace/skills/live-notes.skill +3 -1
- package/workspace/skills/morning-brief.skill +5 -3
- package/workspace/test-lists/meeting-notes.json +15 -0
- package/workspace/test-lists/people-directory.json +6 -0
|
@@ -41,6 +41,9 @@ const VERBS = Object.freeze({
|
|
|
41
41
|
// Not a pm-os-absorb verb (run: null): docs-three-way-sync added it through the same table.
|
|
42
42
|
docs: Object.freeze({ module: 'docs-cli.js', run: null, guide: 'docs.md',
|
|
43
43
|
help: 'pmos docs [list|link|unlink|find] … — is each linked document the same as, ahead of or behind its Google Docs and Confluence copy, and record a sync (pmos docs help)' }),
|
|
44
|
+
// Not a pm-os-absorb verb (run: null): people-directory added it through the same table.
|
|
45
|
+
people: Object.freeze({ module: 'people-cli.js', run: null, guide: 'people.md',
|
|
46
|
+
help: 'pmos people list|show|record|settings … — the people you work with in this repository (pmos people help)' }),
|
|
44
47
|
launcher: Object.freeze({ module: 'launcher-cli.js', run: null, guide: 'launcher.md',
|
|
45
48
|
help: 'pmos launcher on|off|status — a PMOS app and a login item, so the workspace starts without a terminal' }),
|
|
46
49
|
});
|
|
@@ -36,6 +36,7 @@ const MODULES = Object.freeze([
|
|
|
36
36
|
'routes-timeline', // Run 4 (4C): GET /api/timeline
|
|
37
37
|
'routes-board', // Run 4 (4D): the cross-initiative Board, Release
|
|
38
38
|
'routes-reports', // Run 5 (5C): drafts, Issue, notes
|
|
39
|
+
'routes-notes', // meeting-notes Run 1: the Notes view (typed notes, dictation, transcripts, summaries)
|
|
39
40
|
]);
|
|
40
41
|
const PM_DIR = path.join(__dirname, '..', 'public', 'pm');
|
|
41
42
|
const MODULE_NAME = /^[a-z0-9][a-z0-9-]*\.js$/;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* routes-notes.js — the Notes view's routes (meeting-notes, Runs 1 and 2; lib/route-modules.js).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/notes → { ok, notes: [row] } newest first; a row never carries the body
|
|
5
|
+
* GET /api/notes/one?id=… → { ok, note } imports an agent summary written since the request
|
|
6
|
+
* POST /api/notes/new { title? } → { ok, note }
|
|
7
|
+
* POST /api/notes/save { id, title?, body? } → { ok, updated_at }
|
|
8
|
+
* POST /api/notes/dictate { text } → { ok, ops } one dictated phrase as insert operations (lib/notes.js dictation)
|
|
9
|
+
* POST /api/notes/record { id, ws? } → { ok, recorded_from } `ws` is the repository the note's lines are checked against
|
|
10
|
+
* POST /api/notes/transcript { id, text, source? } → { ok, added: [{ at, t, text, source, cat, index }] }
|
|
11
|
+
* POST /api/notes/audio { id, source, pcm, final? } → { ok, cut } 16 kHz mono 16-bit samples, base64; local Whisper (D91)
|
|
12
|
+
* POST /api/notes/label { id, index, cat } → { ok, segment } the PM's own label for a line
|
|
13
|
+
* POST /api/notes/speaker { id, source, name } → { ok, speakers }
|
|
14
|
+
* GET /api/notes/engine?ws=… → { ok, whisper, jev } is Whisper set up; which Jev gates hold (booleans only)
|
|
15
|
+
* POST /api/notes/switch { ws, jev?, check? } → { ok, jev } the two D91 switches, default off
|
|
16
|
+
* POST /api/notes/whisper-setup { ws? } → { ok, started, session, command } a terminal with the setup typed in, not run
|
|
17
|
+
* POST /api/notes/private { id, private } → { ok, private }
|
|
18
|
+
* POST /api/notes/summarize { id } → { ok, summary } the local summary
|
|
19
|
+
* POST /api/notes/summarize-agent { id, ws? } → { ok, started, session, handoff, out, withheld, reason }
|
|
20
|
+
* POST /api/notes/delete { id } → { ok }
|
|
21
|
+
*
|
|
22
|
+
* Notes are the PM's own and live in the per-user runtime directory (ctx.runtimeDir), never in a repository, so no route here
|
|
23
|
+
* writes into a registered workspace. Every POST goes through pm-guard `requirePagePress` (a body that is not an object or carries
|
|
24
|
+
* a writer field: 400; no Origin, a foreign Origin or a wrong nonce: 403). No route makes an outbound request of its own: the
|
|
25
|
+
* agent summary starts the PM's own agent session in the repository (ctx.spawnSession), as the skill draft does; Whisper runs
|
|
26
|
+
* on this machine (lib/notes-whisper.js); a label or a check goes to Jev only through lib/jev-notes.js, behind the D91 gates.
|
|
27
|
+
*
|
|
28
|
+
* AFTER A LINE IS ADDED (Run 2). Each new line already has its local label. Then, one note at a time and in order: Jev's label
|
|
29
|
+
* when the label gates hold; for a decision, action or statement, the closest repository passage (local, lib/notes-repo.js),
|
|
30
|
+
* and Jev's comparison when the check gates hold too. The page reads the results with GET /api/notes/one.
|
|
31
|
+
*/
|
|
32
|
+
const guard = require('./pm-guard.js');
|
|
33
|
+
const notes = require('./notes.js');
|
|
34
|
+
const whisper = require('./notes-whisper.js');
|
|
35
|
+
const notesRepo = require('./notes-repo.js');
|
|
36
|
+
const jevNotes = require('./jev-notes.js');
|
|
37
|
+
const tsSettings = require('./typesafe-settings.js');
|
|
38
|
+
|
|
39
|
+
const MAX_PCM_BYTES = 16000 * 2 * 15; // 15 s of 16 kHz 16-bit audio in one request
|
|
40
|
+
const CHECKED = new Set(['decision', 'action', 'statement']);
|
|
41
|
+
|
|
42
|
+
const prefixes = ['/api/notes'];
|
|
43
|
+
const POSTS = new Set(['/api/notes/new', '/api/notes/save', '/api/notes/dictate', '/api/notes/record', '/api/notes/transcript',
|
|
44
|
+
'/api/notes/private', '/api/notes/summarize', '/api/notes/summarize-agent', '/api/notes/delete', '/api/notes/audio', '/api/notes/label',
|
|
45
|
+
'/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup']);
|
|
46
|
+
|
|
47
|
+
/* ── labels and checks, one note at a time ───────────────────────────────────────────────────── */
|
|
48
|
+
const chains = new Map(); // note id → the promise of its enrichment so far
|
|
49
|
+
function enrich(ctx, id, added) {
|
|
50
|
+
if (!added || !added.length) return Promise.resolve();
|
|
51
|
+
const next = (chains.get(id) || Promise.resolve()).then(() => enrichNow(ctx, id, added)).catch(() => {});
|
|
52
|
+
chains.set(id, next);
|
|
53
|
+
next.then(() => { if (chains.get(id) === next) chains.delete(id); });
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
function gatesFor(ctx, root) {
|
|
57
|
+
const dir = ctx.runtimeDir;
|
|
58
|
+
if (!root) return { root: null, exclude: [], ask: null, check: false, key: null, askOpts: {} };
|
|
59
|
+
const repo = tsSettings.getRepo(dir, root);
|
|
60
|
+
const sw = jevNotes.getSwitch(dir, root);
|
|
61
|
+
const key = tsSettings.getKey(dir);
|
|
62
|
+
const snd = jevNotes.senderFor(key, { consent: repo.consent, jev: sw.jev, check: sw.check }, typeof ctx.jevModelOpts === 'function' ? ctx.jevModelOpts(root) : {});
|
|
63
|
+
return { root, exclude: repo.exclude, ask: snd.ask, check: snd.check, key: snd.ask ? key : null, askOpts: snd.askOpts };
|
|
64
|
+
}
|
|
65
|
+
const stillOpen = (dir, id) => { const r = notes.read(dir, id); return r.ok && !r.note.private ? r.note : null; };
|
|
66
|
+
async function enrichNow(ctx, id, added) {
|
|
67
|
+
const dir = ctx.runtimeDir;
|
|
68
|
+
const note = stillOpen(dir, id);
|
|
69
|
+
if (!note) return;
|
|
70
|
+
const w = note.ws ? ctx.getWs(note.ws) : null;
|
|
71
|
+
const g = gatesFor(ctx, w ? w.root : null);
|
|
72
|
+
for (const seg of added) {
|
|
73
|
+
let cat = seg.cat;
|
|
74
|
+
if (g.ask && stillOpen(dir, id)) {
|
|
75
|
+
const l = await jevNotes.label({ ask: g.ask, key: g.key, askOpts: g.askOpts, text: seg.text, root: g.root });
|
|
76
|
+
if (l.cat) { const u = notes.updateSegment(dir, { id, index: seg.index, text: seg.text, patch: { cat: l.cat, cat_by: 'jev' } }); if (u.ok) cat = u.segment.cat; }
|
|
77
|
+
else if (l.reason === 'failed' || l.reason === 'paused') notes.updateSegment(dir, { id, index: seg.index, text: seg.text, patch: { jev: l.reason } });
|
|
78
|
+
}
|
|
79
|
+
if (!g.root || !CHECKED.has(cat)) continue;
|
|
80
|
+
const pass = notesRepo.closest(g.root, seg.text, { exclude: g.exclude });
|
|
81
|
+
if (!pass) continue;
|
|
82
|
+
let check = { doc: pass.path, line: pass.line, verdict: 'related', by: 'local' };
|
|
83
|
+
if (g.ask && g.check && stillOpen(dir, id)) {
|
|
84
|
+
const c = await jevNotes.compare({ ask: g.ask, key: g.key, askOpts: g.askOpts, text: seg.text, passage: pass, root: g.root });
|
|
85
|
+
if (c.verdict) check = { doc: pass.path, line: pass.line, verdict: c.verdict, words: c.words || null, by: 'jev' };
|
|
86
|
+
}
|
|
87
|
+
notes.updateSegment(dir, { id, index: seg.index, text: seg.text, patch: { check } });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ── local Whisper: one transcriber for the process ───────────────────────────────────────────── */
|
|
92
|
+
let transcriber = null; let transcriberDir = null;
|
|
93
|
+
const audioErrors = new Map(); // note id → the last thing that went wrong, for the page
|
|
94
|
+
function transcriberFor(ctx) {
|
|
95
|
+
if (transcriber && transcriberDir === ctx.runtimeDir) return transcriber;
|
|
96
|
+
transcriberDir = ctx.runtimeDir;
|
|
97
|
+
transcriber = whisper.createTranscriber({
|
|
98
|
+
runtimeDir: ctx.runtimeDir,
|
|
99
|
+
onText: async (c) => {
|
|
100
|
+
const r = notes.appendTranscript(ctx.runtimeDir, { id: c.id, text: c.text, source: c.source, t: c.t });
|
|
101
|
+
if (r.ok) { audioErrors.delete(c.id); await enrich(ctx, c.id, r.added).catch(() => {}); }
|
|
102
|
+
},
|
|
103
|
+
onError: (c) => { audioErrors.set(c.id, { at: new Date().toISOString(), error: c.error }); },
|
|
104
|
+
});
|
|
105
|
+
return transcriber;
|
|
106
|
+
}
|
|
107
|
+
function engineInfo(ctx, w) {
|
|
108
|
+
const d = whisper.detect(ctx.runtimeDir);
|
|
109
|
+
const out = { ready: d.ready, missing: d.missing, setup: d.setup, model_dir: d.model_dir, model_size: d.model_size };
|
|
110
|
+
if (!w) return { whisper: out, jev: null };
|
|
111
|
+
const repo = tsSettings.getRepo(ctx.runtimeDir, w.root);
|
|
112
|
+
const sw = jevNotes.getSwitch(ctx.runtimeDir, w.root);
|
|
113
|
+
const info = jevNotes.infoFor(tsSettings.status(ctx.runtimeDir).present, { consent: repo.consent, jev: sw.jev, check: sw.check });
|
|
114
|
+
return { whisper: out, jev: { ...info, paused: jevNotes.pauseStatus(w.root) } };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const send = (ctx, res, r) => ctx.json(res, r, r.ok ? 200 : (r.status || 400));
|
|
118
|
+
|
|
119
|
+
async function handle(req, res, url, ctx) {
|
|
120
|
+
const p = url.pathname;
|
|
121
|
+
const dir = ctx.runtimeDir;
|
|
122
|
+
if (!dir) { ctx.json(res, { ok: false, error: 'the notes store is not available' }, 500); return true; }
|
|
123
|
+
if (p === '/api/notes' || p === '/api/notes/one') {
|
|
124
|
+
if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
|
|
125
|
+
if (p === '/api/notes') { send(ctx, res, notes.list(dir)); return true; }
|
|
126
|
+
const nid = url.searchParams.get('id');
|
|
127
|
+
const r = notes.read(dir, nid);
|
|
128
|
+
if (r.ok) r.audio = { pending: transcriber ? transcriber.pending() : 0, error: audioErrors.get(nid) || null };
|
|
129
|
+
send(ctx, res, r);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (p === '/api/notes/engine') {
|
|
133
|
+
if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
|
|
134
|
+
const wsId = url.searchParams.get('ws');
|
|
135
|
+
const w = wsId ? ctx.getWs(wsId) : null;
|
|
136
|
+
ctx.json(res, { ok: true, ...engineInfo(ctx, w && w.id === wsId ? w : null) });
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (!POSTS.has(p)) { ctx.json(res, { ok: false, error: 'no such notes route' }, 404); return true; }
|
|
140
|
+
if (req.method !== 'POST') { ctx.json(res, { ok: false, error: 'POST only' }, 405); return true; }
|
|
141
|
+
const parsed = await ctx.readJson(req);
|
|
142
|
+
if (!parsed.ok) { ctx.json(res, { ok: false, error: 'The body is not valid JSON.' }, 400); return true; }
|
|
143
|
+
const body = parsed.body;
|
|
144
|
+
const g = guard.requirePagePress(req, body);
|
|
145
|
+
if (!g.ok) { ctx.json(res, { ok: false, error: g.error }, g.status); return true; }
|
|
146
|
+
const id = body && typeof body.id === 'string' ? body.id : '';
|
|
147
|
+
const done = (r) => { send(ctx, res, r); return true; };
|
|
148
|
+
if (p === '/api/notes/new') return done(notes.create(dir, { title: body.title }));
|
|
149
|
+
if (p === '/api/notes/dictate') return done(notes.dictation(body.text));
|
|
150
|
+
if (p === '/api/notes/save') return done(notes.save(dir, { id, title: body.title, body: body.body }));
|
|
151
|
+
const wsOf = () => {
|
|
152
|
+
if (body.ws == null) return { ok: true, w: null };
|
|
153
|
+
const w = typeof body.ws === 'string' ? ctx.getWs(body.ws) : null;
|
|
154
|
+
return w && w.id === body.ws ? { ok: true, w } : { ok: false };
|
|
155
|
+
};
|
|
156
|
+
if (p === '/api/notes/record') {
|
|
157
|
+
const o = wsOf();
|
|
158
|
+
if (!o.ok) { ctx.json(res, { ok: false, error: 'This repository is not registered.' }, 404); return true; }
|
|
159
|
+
return done(notes.record(dir, { id, ws: body.ws == null ? undefined : o.w.id }));
|
|
160
|
+
}
|
|
161
|
+
if (p === '/api/notes/transcript') {
|
|
162
|
+
const r = notes.appendTranscript(dir, { id, text: body.text, source: 'speech' });
|
|
163
|
+
if (r.ok) enrich(ctx, id, r.added);
|
|
164
|
+
return done(r);
|
|
165
|
+
}
|
|
166
|
+
if (p === '/api/notes/audio') {
|
|
167
|
+
const source = body.source === 'meeting' ? 'meeting' : body.source === 'mic' ? 'mic' : null;
|
|
168
|
+
if (!source) return done({ ok: false, status: 400, error: 'the source must be mic or meeting' });
|
|
169
|
+
const note = notes.read(dir, id);
|
|
170
|
+
if (!note.ok) return done(note);
|
|
171
|
+
if (note.note.private) { transcriberFor(ctx).drop(id); return done({ ok: false, status: 409, error: 'this note is marked private, so nothing more is recorded in it' }); }
|
|
172
|
+
if (!note.note.recorded_from) return done({ ok: false, status: 409, error: 'press Listen first' });
|
|
173
|
+
const b64 = typeof body.pcm === 'string' ? body.pcm : '';
|
|
174
|
+
if (b64.length > Math.ceil(MAX_PCM_BYTES / 3) * 4) return done({ ok: false, status: 413, error: 'that audio chunk is too long' });
|
|
175
|
+
const pcm = Buffer.from(b64, 'base64');
|
|
176
|
+
if (pcm.length % 2) return done({ ok: false, status: 400, error: 'the audio must be 16-bit samples' });
|
|
177
|
+
if (!whisper.detect(dir).ready) return done({ ok: false, status: 409, error: 'local transcription is not set up' });
|
|
178
|
+
const ms = (pcm.length / 2 / whisper.RATE) * 1000;
|
|
179
|
+
const t = Math.max(0, Date.now() - Date.parse(note.note.recorded_from) - ms);
|
|
180
|
+
return done(transcriberFor(ctx).push({ id, source, pcm, t, final: body.final === true }));
|
|
181
|
+
}
|
|
182
|
+
if (p === '/api/notes/label') return done(notes.setLabel(dir, { id, index: Number(body.index), cat: body.cat, drop: body.drop === true }));
|
|
183
|
+
if (p === '/api/notes/speaker') return done(notes.renameSpeaker(dir, { id, source: body.source, name: body.name }));
|
|
184
|
+
if (p === '/api/notes/switch') {
|
|
185
|
+
const o = wsOf();
|
|
186
|
+
if (!o.ok || !o.w) { ctx.json(res, { ok: false, error: 'This repository is not registered.' }, 404); return true; }
|
|
187
|
+
jevNotes.setSwitch(dir, o.w.root, { jev: typeof body.jev === 'boolean' ? body.jev : undefined, check: typeof body.check === 'boolean' ? body.check : undefined });
|
|
188
|
+
ctx.json(res, { ok: true, ...engineInfo(ctx, o.w) });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
if (p === '/api/notes/whisper-setup') {
|
|
192
|
+
const o = wsOf();
|
|
193
|
+
if (!o.ok) { ctx.json(res, { ok: false, error: 'This repository is not registered.' }, 404); return true; }
|
|
194
|
+
const command = whisper.setupCommand(dir);
|
|
195
|
+
if (typeof ctx.spawnSession !== 'function') { ctx.json(res, { ok: true, started: false, command, reason: 'this workspace cannot start sessions' }); return true; }
|
|
196
|
+
// typed, not run: the PM reads the commands in their own terminal and presses Enter (D91)
|
|
197
|
+
const s = ctx.spawnSession({ name: 'notes: set up Whisper', agent: 'shell', ws: o.w ? o.w.id : undefined, origin: 'notes-whisper-setup', initialInput: command });
|
|
198
|
+
const started = !!s && s.state !== 'exited';
|
|
199
|
+
ctx.json(res, { ok: true, started, session: s ? s.id : null, command, reason: started ? '' : ((s && s.note) || 'could not start a terminal') });
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (p === '/api/notes/private') {
|
|
203
|
+
const r = notes.setPrivate(dir, { id, private: body.private });
|
|
204
|
+
if (r.ok && r.private && transcriber) transcriber.drop(id);
|
|
205
|
+
return done(r);
|
|
206
|
+
}
|
|
207
|
+
if (p === '/api/notes/summarize') return done(notes.localSummary(dir, { id }));
|
|
208
|
+
if (p === '/api/notes/delete') { if (transcriber) transcriber.drop(id); return done(notes.remove(dir, id)); }
|
|
209
|
+
// summarize-agent: the handoff file first (refused for a private note), then the PM's own agent session
|
|
210
|
+
let w = null;
|
|
211
|
+
if (body.ws != null) {
|
|
212
|
+
w = typeof body.ws === 'string' ? ctx.getWs(body.ws) : null;
|
|
213
|
+
if (!w || w.id !== body.ws) { ctx.json(res, { ok: false, error: 'This repository is not registered.' }, 404); return true; }
|
|
214
|
+
}
|
|
215
|
+
const h = notes.agentHandoff(dir, { id });
|
|
216
|
+
if (!h.ok) return done(h);
|
|
217
|
+
if (typeof ctx.spawnSession !== 'function') {
|
|
218
|
+
notes.agentStarted(dir, { id, started: false, reason: 'this workspace cannot start sessions' });
|
|
219
|
+
ctx.json(res, { ok: true, started: false, reason: 'this workspace cannot start sessions', handoff: h.handoff, out: h.out, withheld: h.withheld });
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
const s = ctx.spawnSession({ name: 'notes: summary', agent: 'claude', ws: w ? w.id : undefined, origin: 'notes-summary:' + id, initialInput: h.prompt + '\r' });
|
|
223
|
+
const started = !!s && s.state !== 'exited';
|
|
224
|
+
const reason = started ? '' : ((s && s.note) || 'could not start a claude session');
|
|
225
|
+
notes.agentStarted(dir, { id, session: s ? s.id : null, started, reason });
|
|
226
|
+
ctx.json(res, { ok: true, started, session: s ? s.id : null, reason, handoff: h.handoff, out: h.out, withheld: h.withheld });
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = { prefixes, handle, enrich, __idle: () => Promise.all([transcriber ? transcriber.idle() : null, ...chains.values()]) };
|
package/workspace/package.json
CHANGED
|
@@ -406,7 +406,7 @@ function render() {
|
|
|
406
406
|
|
|
407
407
|
const FRAME = !FIXTURE && !LEGACY_LAYOUT_FLAG;
|
|
408
408
|
const FRAME_KEY = 'pmos.workspace.pm.frame';
|
|
409
|
-
const FRAME_DESTS = Object.freeze(['today', 'initiatives', 'board', 'reports', 'docs', 'settings']);
|
|
409
|
+
const FRAME_DESTS = Object.freeze(['today', 'initiatives', 'board', 'reports', 'docs', 'people', 'settings']);
|
|
410
410
|
|
|
411
411
|
function readFrame() {
|
|
412
412
|
if (!FRAME) return {};
|
|
@@ -637,7 +637,7 @@ function pressIn(r, key) {
|
|
|
637
637
|
if (b && !b.disabled) { b.click(); return true; }
|
|
638
638
|
return false;
|
|
639
639
|
}
|
|
640
|
-
const G_KEYS = { t: 'today', i: 'initiatives', b: 'board', r: 'reports', d: 'docs' };
|
|
640
|
+
const G_KEYS = { t: 'today', i: 'initiatives', b: 'board', r: 'reports', d: 'docs', p: 'people' };
|
|
641
641
|
|
|
642
642
|
function frameKey(e) {
|
|
643
643
|
const mod = e.metaKey || e.ctrlKey;
|