create-pmos 0.46.0 → 0.48.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/AGENTS-HUMAN.md +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +13 -3
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
- package/payload/mods/pmos-cockpit/hooks/lib/handover-run.mjs +33 -0
- package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +189 -18
- package/payload/mods/pmos-cockpit/types/index.d.ts +16 -1
- package/payload/okf/core/concepts/frontmatter-format.md +6 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/pmos-log.sh +18 -8
- package/payload/scripts/pmos-model.js +233 -2
- package/payload/scripts/status-facts.js +204 -0
- package/payload/scripts/workflow-state.js +52 -0
- package/payload/skills/SKILL-FORMAT.md +1 -0
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +3 -3
- package/workspace/bin/live-notes.js +133 -0
- package/workspace/design-parity.js +328 -21
- package/workspace/doc-category-bench.js +165 -0
- package/workspace/fixtures/doc-category-labelled.json +167 -0
- package/workspace/guide/initiatives-and-next-steps.md +16 -2
- package/workspace/guide/knowledge-graph.md +89 -2
- package/workspace/guide/layout.md +30 -2
- package/workspace/guide/proactive-engine.md +26 -4
- package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
- package/workspace/guide/statuses.md +1 -0
- package/workspace/guide/validate-with-typesafe.md +17 -1
- package/workspace/lib/claims.js +51 -2
- package/workspace/lib/doc-category-corrections.js +113 -0
- package/workspace/lib/doc-category-writeback.js +110 -0
- package/workspace/lib/doc-category.js +225 -0
- package/workspace/lib/engine/pmos-model.js +233 -2
- package/workspace/lib/engine/status-facts.js +204 -0
- package/workspace/lib/engine/workflow-state.js +52 -0
- package/workspace/lib/jev-categorize.js +399 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-route.js +129 -0
- package/workspace/lib/kg-cli.js +5 -2
- package/workspace/lib/kg-dangling-baseline.json +1 -0
- package/workspace/lib/kg-dangling-rules.json +1 -0
- package/workspace/lib/kg.js +26 -9
- package/workspace/lib/learn.js +282 -0
- package/workspace/lib/live-notes.js +890 -0
- package/workspace/lib/proactive-cli.js +49 -1
- package/workspace/lib/proactive.js +8 -7
- package/workspace/lib/typesafe-settings.js +10 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +5 -4
- package/workspace/public/pm/app.js +148 -79
- package/workspace/public/pm/data.js +58 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +267 -21
- package/workspace/public/pm/panel-cockpit.js +636 -0
- package/workspace/public/pm/panel-detail.js +210 -8
- package/workspace/public/pm/panel-initiatives.js +9 -9
- package/workspace/public/pm/panel-newsess.js +145 -86
- package/workspace/public/pm/panel-repos.js +34 -17
- package/workspace/public/pm/panel-tabs.js +145 -27
- package/workspace/public/pm/state.js +45 -4
- package/workspace/public/pm/tab-doc.js +244 -16
- package/workspace/public/pm/tab-term.js +11 -1
- package/workspace/public/pm/tokens.css +4 -3
- package/workspace/public/pm/view-addrepo.js +207 -0
- package/workspace/public/pm/view-files.js +197 -0
- package/workspace/public/pm/view-guide.js +141 -0
- package/workspace/public/pm/view-knowledge.js +225 -0
- package/workspace/public/pm/view-memory.js +122 -0
- package/workspace/public/pm/view-portfolio.js +153 -0
- package/workspace/public/pm/view-reports.js +190 -0
- package/workspace/public/pm/view-scheduled.js +161 -0
- package/workspace/public/pm/view-skills.js +216 -0
- package/workspace/public/pm/view-today.js +168 -0
- package/workspace/public/pm/view-validate.js +171 -0
- package/workspace/server.js +394 -10
- package/workspace/skills/live-notes.skill +58 -0
- package/workspace/skills/morning-brief.skill +1 -0
|
@@ -336,6 +336,55 @@ function answerFor(row) {
|
|
|
336
336
|
};
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* The status-from-artifacts lines, derived by the engine (scripts/status-facts.js) and only worded
|
|
341
|
+
* here. Each is a flag beside the ladder with its basis. None is a step: nothing here changes a
|
|
342
|
+
* phase, a dot or a count, and a check that could not run says so instead of staying silent.
|
|
343
|
+
*/
|
|
344
|
+
function factsFor(row) {
|
|
345
|
+
if (!row) return [];
|
|
346
|
+
const out = [];
|
|
347
|
+
const sa = row.since_approval;
|
|
348
|
+
if (sa) {
|
|
349
|
+
if (sa.status === 'unknown') out.push({ tone: 'info', label: 'Since approval', text: 'Cannot tell. ' + (sa.reason || '') });
|
|
350
|
+
else {
|
|
351
|
+
for (const a of sa.approvals || []) {
|
|
352
|
+
const what = a.kind === 'contract' ? 'the contract was approved' : 'you accepted it';
|
|
353
|
+
if (a.status === 'unknown') out.push({ tone: 'info', label: 'Since approval', text: 'Cannot tell for ' + what + '. ' + (a.reason || '') });
|
|
354
|
+
else if (!(a.changed || []).length) out.push({ tone: 'ok', label: 'Since approval', text: 'No spec file changed after ' + what + '.' });
|
|
355
|
+
else {
|
|
356
|
+
const files = [...new Set(a.changed.map((x) => x.file.split('/').pop()))].join(', ');
|
|
357
|
+
const last = a.changed[a.changed.length - 1];
|
|
358
|
+
out.push({ tone: 'warn', label: 'Since approval', text: files + ' changed after ' + what + ' (' + a.changed_total + ' commit' + (a.changed_total === 1 ? '' : 's') + ', latest ' + last.sha + (last.date ? ' on ' + last.date : '') + ').' });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const cv = row.coverage;
|
|
364
|
+
if (cv) {
|
|
365
|
+
if (cv.status === 'unknown') out.push({ tone: 'info', label: 'Coverage', text: 'Cannot tell. ' + (cv.reason || '') });
|
|
366
|
+
else if (!cv.uncited.length && !cv.orphan_citations.length) out.push({ tone: 'ok', label: 'Coverage', text: 'The rubric cites all ' + cv.requirements + ' requirement ids.' });
|
|
367
|
+
else {
|
|
368
|
+
const bits = [];
|
|
369
|
+
if (cv.uncited.length) bits.push(cv.uncited.join(', ') + ' not cited in the rubric');
|
|
370
|
+
if (cv.orphan_citations.length) bits.push(cv.orphan_citations.join(', ') + ' cited but not in the PRD');
|
|
371
|
+
out.push({ tone: 'warn', label: 'Coverage', text: bits.join('; ') + '. A citation is not a grade.' });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (row.record_says) out.push({ tone: 'warn', label: 'Record says', text: 'The status line ' + row.record_says.reason + '. The ladder is not changed.' });
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* D83 — what Jev suggests about the documents tied to this initiative. A suggestion, in words, with the model that
|
|
380
|
+
* gave it. It is never a step: it is listed after the facts and nothing reads it to colour a phase.
|
|
381
|
+
*/
|
|
382
|
+
function suggestionLines(sg) {
|
|
383
|
+
if (!sg) return [];
|
|
384
|
+
if (sg.ok === false) return [{ tone: 'info', label: 'Documents suggest', text: 'Cannot tell. ' + (sg.reason || '') }];
|
|
385
|
+
return (sg.items || []).map((x) => ({ tone: 'info', label: 'Documents suggest', text: (x.sentence || (x.doc + ': ' + x.kind)) + ' A suggestion, not a check' + (x.model ? ' (' + x.model + ')' : '') + '.' }));
|
|
386
|
+
}
|
|
387
|
+
|
|
339
388
|
/** The Review-Gate line, read off the `review` rung — no verdict is not a pass. */
|
|
340
389
|
function evalFor(ladderSteps) {
|
|
341
390
|
const r = (ladderSteps || []).find((s) => s.key === 'review');
|
|
@@ -405,6 +454,12 @@ function artsFor(payload) {
|
|
|
405
454
|
});
|
|
406
455
|
}
|
|
407
456
|
|
|
457
|
+
/** The rung that blocks the initiative, as the card draws it: the engine's label, evidence and next step. Null when none blocks. */
|
|
458
|
+
function blockerOf(row) {
|
|
459
|
+
const s = row && Array.isArray(row.steps) ? row.steps.find((x) => x && x.blocking === true && x.status === 'blocked') : null;
|
|
460
|
+
return s ? { key: s.key, label: s.label || s.key, evidence: s.evidence || '', next: s.next || '' } : null;
|
|
461
|
+
}
|
|
462
|
+
|
|
408
463
|
/** The full record panel 3 renders, from one `/api/initiative` payload. */
|
|
409
464
|
function recordFrom(payload) {
|
|
410
465
|
if (!payload || payload.ok === false) {
|
|
@@ -454,11 +509,14 @@ function recordFrom(payload) {
|
|
|
454
509
|
arts: artsFor(payload),
|
|
455
510
|
// R4/R5/R6 — what remains to acceptance, the conversations that belong to it, and the files git says
|
|
456
511
|
// were changed for it. Each arrives already derived by the server; this layer only carries them.
|
|
512
|
+
// workspace-cockpit-nav R2: the first rung the ladder says is blocking, or null. Nothing is invented — the words are the engine's.
|
|
513
|
+
blocker: blockerOf(row),
|
|
457
514
|
nextSteps: payload.next_steps || null,
|
|
458
515
|
lcHistory: payload.lifecycle_history || [],
|
|
459
516
|
convs: Array.isArray(payload.conversations) ? payload.conversations : [],
|
|
460
517
|
gitFiles: payload.files || null,
|
|
461
518
|
eval: evalFor(row && row.steps),
|
|
519
|
+
facts: factsFor(row).concat(suggestionLines(payload.suggestions)),
|
|
462
520
|
l2: l2For(row),
|
|
463
521
|
};
|
|
464
522
|
if (!row) {
|
|
@@ -46,7 +46,7 @@ export const TAB_COLORS = {
|
|
|
46
46
|
activeText: '#f9fbfb',
|
|
47
47
|
idleText: 'oklch(0.6 0.02 213)',
|
|
48
48
|
// dot colour by tab kind; anything that is not term/task (i.e. doc) is blue.
|
|
49
|
-
dot: { term: '#47cf79', task: '#b98cff', doc: '#2b7fff' }
|
|
49
|
+
dot: { term: '#47cf79', task: '#b98cff', doc: '#2b7fff', view: '#f2b24a' }
|
|
50
50
|
};
|
|
51
51
|
export const TERM_COLORS = { prompt: '#47cf79', dim: '#6f7d81', text: '#c4ccce' };
|
|
52
52
|
export const SCRATCH_SEND_COLOR = { on: 'oklch(0.85 0.01 214)', off: 'oklch(0.55 0.02 213)' };
|
|
@@ -575,7 +575,7 @@ export const allRecords = (userInits = {}) => ({ ...DATA, ...userInits });
|
|
|
575
575
|
export const recordFor = (slug, userInits = {}) => allRecords(userInits)[slug];
|
|
576
576
|
|
|
577
577
|
/** The default selection (line 1397). */
|
|
578
|
-
export const defaultSlug = product => (product === 'atlas-crm' ? 'crm-import-wizard' : 'workspace-pm-spine');
|
|
578
|
+
export const defaultSlug = product => (COCKPIT_SEED ? (SEED_DEFAULTS[product] || null) : product === 'atlas-crm' ? 'crm-import-wizard' : 'workspace-pm-spine');
|
|
579
579
|
|
|
580
580
|
/** sessionsOf (line 1398) — authored sessions plus any started this session. */
|
|
581
581
|
export function sessionsOf(slug, { userInits = {}, extraSess = {} } = {}) {
|
|
@@ -1000,3 +1000,41 @@ export const FIXTURE = {
|
|
|
1000
1000
|
};
|
|
1001
1001
|
|
|
1002
1002
|
export default FIXTURE;
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
/* ───────────── the cockpit mockup's seed (`?fixture=cockpit`) ─────────────
|
|
1006
|
+
* planning/design/cockpit-mockup/WsA.dc.html draws three repositories and one initiative in detail. The seed is applied by
|
|
1007
|
+
* mutating the exports above in place, once, so every reader (panels, parity script) sees one set of data. It is the
|
|
1008
|
+
* mockup's fake data and nothing else; no value here is read from a repository. */
|
|
1009
|
+
const COCKPIT_SEED = (() => { try { return new URLSearchParams(location.search).get('fixture') === 'cockpit'; } catch { return false; } })();
|
|
1010
|
+
const SEED_DEFAULTS = { microsites: 'ooh-weather-expansion', pmos: 'cockpit-terminal-mod', 'scarlet': 'scarlet-onboarding' };
|
|
1011
|
+
if (COCKPIT_SEED) {
|
|
1012
|
+
const task = (title, status, k) => ({ title, status, instruction: 'Run ' + title + '.', input: 'planning/ + prior phase outputs', expect: 'A reviewable ' + k + ' artifact exists.', evalNote: 'graded against the ' + k + ' rubric', output: status === 'done' ? 'planning/' + k + '/' + k + '.md' : '—' });
|
|
1013
|
+
PRODUCTS.splice(0, PRODUCTS.length, ['microsites', '6', 'github'], ['pmos', '3', 'local'], ['scarlet', '1', 'github']);
|
|
1014
|
+
for (const k of Object.keys(ALLGROUPS)) delete ALLGROUPS[k];
|
|
1015
|
+
ALLGROUPS.microsites = [{ gid: 'O2', idc: C.fg, gname: 'Microsites ship on time', gct: '6', hasKr: true, krid: 'KR1', krname: 'q3-2026', krct: '6',
|
|
1016
|
+
slugs: ['ooh-weather-expansion', 'psle-paywall-revamp', 'smrt-news-feed', 'lightbox-ga-tagging', 'login-metric-tagging', 'shortlink-redirects'] }];
|
|
1017
|
+
ALLGROUPS.pmos = [{ gid: 'health:tech', idc: C.inf, gname: 'health budget', gct: '3', hasKr: false, slugs: ['cockpit-terminal-mod', 'workspace-cockpit-nav', 'kit-publish'] }];
|
|
1018
|
+
ALLGROUPS['scarlet'] = [{ gid: 'O1', idc: C.fg, gname: 'Onboarding', gct: '1', hasKr: false, slugs: ['scarlet-onboarding'] }];
|
|
1019
|
+
for (const k of Object.keys(DATA)) delete DATA[k];
|
|
1020
|
+
const mk = (tag, stage, steps, extra) => ({ tag, stage, flow: 'adlc-feature', pills: [{ t: 'feature', c: C.pur }, { t: 'q3-2026 · O2 · KR1', c: C.mut }], branch: null,
|
|
1021
|
+
answer: { tone: 'info', text: tag }, steps, sessions: [], arts: [], eval: null, l2: [stage, '', C.mut], ...(extra || {}) });
|
|
1022
|
+
const oneStep = (k, s) => [{ k, s, t: '' }];
|
|
1023
|
+
DATA['ooh-weather-expansion'] = mk('OOH weather microsite: add regional forecast units', 'eval', [
|
|
1024
|
+
{ k: 'spec', s: 'done', t: '' }, { k: 'build', s: 'done', t: '' }, { k: 'eval', s: 'blocked', t: '' }, { k: 'accepted', s: 'pending', t: '' }], {
|
|
1025
|
+
pills: [{ t: 'feature', c: C.mut }, { t: 'q3-2026 · O2 · KR1', c: C.mut }, { t: '⇄ Jira OOH-212', c: C.mut }],
|
|
1026
|
+
blocker: { label: 'Merge', evidence: 'needs a grant for PR 41', next: '' },
|
|
1027
|
+
tasks: {
|
|
1028
|
+
spec: [task('Write the PRD', 'done', 'spec'), task('Write the rubric', 'done', 'spec')],
|
|
1029
|
+
build: [task('Build the forecast units', 'done', 'build'), task('Wire the regional feed', 'done', 'build'), task('Add the tests', 'done', 'build')],
|
|
1030
|
+
eval: [task('Run independent review', 'blocked', 'eval'), task('Record outcome check', 'todo', 'eval')],
|
|
1031
|
+
accepted: [task('Accept the run', 'todo', 'accepted')],
|
|
1032
|
+
},
|
|
1033
|
+
sessions: [{ name: 'claude · merge PR 41', st: 'working', label: 'working' }, { name: 'review run', st: 'working', label: 'working' }],
|
|
1034
|
+
arts: [{ role: 'PRD', path: 'planning/prd/ooh-weather-expansion-prd.md', dest: null }] });
|
|
1035
|
+
DATA['psle-paywall-revamp'] = mk('PSLE paywall revamp', 'accepted', oneStep('accepted', 'pending'));
|
|
1036
|
+
DATA['smrt-news-feed'] = mk('SMRT news feed', 'build', oneStep('build', 'current'));
|
|
1037
|
+
for (const id of ['lightbox-ga-tagging', 'login-metric-tagging', 'shortlink-redirects']) DATA[id] = mk(id, 'accepted', oneStep('accepted', 'done'));
|
|
1038
|
+
for (const id of ['cockpit-terminal-mod', 'workspace-cockpit-nav', 'scarlet-onboarding']) DATA[id] = mk(id, 'spec', oneStep('spec', 'blocked'));
|
|
1039
|
+
DATA['kit-publish'] = mk('kit-publish', 'accepted', oneStep('accepted', 'pending'));
|
|
1040
|
+
}
|
|
@@ -23,23 +23,36 @@ const ROW = 'display:block;width:100%;padding:7px 14px;min-height:40px;text-alig
|
|
|
23
23
|
const BTN = 'font-family:var(--mono);font-size:10.5px;padding:3px 9px;border:1px solid var(--border);color:var(--muted-foreground);min-height:26px';
|
|
24
24
|
const NOTE = 'padding:8px 14px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5';
|
|
25
25
|
|
|
26
|
-
/* one cached read per endpoint; `reload(key)` drops it so the next render refetches
|
|
26
|
+
/* one cached read per endpoint; `reload(key)` drops it so the next render refetches.
|
|
27
|
+
*
|
|
28
|
+
* AN ANSWER OLDER THAN A RELOAD IS DROPPED. `reload` follows a write (a saved setting, a dismissed
|
|
29
|
+
* item). A read that was already in flight when the write went out carries the state from BEFORE it,
|
|
30
|
+
* and it used to land after the fresh read and overwrite it — then count as fresh for 15 s. Measured
|
|
31
|
+
* on Today: quiet hours saved, the note shown, and 1.5 s later the note gone while the server held
|
|
32
|
+
* the setting (pm-r22-test.js, about 1 run in 8). `gen` counts reloads per key; a response whose
|
|
33
|
+
* generation is out of date is not written. */
|
|
27
34
|
const cache = new Map();
|
|
35
|
+
const gen = new Map();
|
|
28
36
|
function read(key, url) {
|
|
29
37
|
const hit = cache.get(key);
|
|
30
38
|
if (hit && (hit.state !== 'ok' || Date.now() - hit.at < 15000)) return hit;
|
|
31
39
|
if (!hit || hit.state === 'ok') {
|
|
32
40
|
const next = { state: hit ? hit.state : 'loading', data: hit ? hit.data : null, at: hit ? hit.at : 0 };
|
|
33
41
|
cache.set(key, next);
|
|
42
|
+
const g = gen.get(key) || 0;
|
|
43
|
+
const current = () => (gen.get(key) || 0) === g;
|
|
34
44
|
fetch(url).then(readJson).then(
|
|
35
|
-
(data) => { cache.set(key, { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }); setState({}); },
|
|
36
|
-
(e) => { cache.set(key, { state: 'error', error: (e && e.message) || String(e), at: Date.now() }); setState({}); },
|
|
45
|
+
(data) => { if (!current()) return; cache.set(key, { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }); setState({}); },
|
|
46
|
+
(e) => { if (!current()) return; cache.set(key, { state: 'error', error: (e && e.message) || String(e), at: Date.now() }); setState({}); },
|
|
37
47
|
);
|
|
38
48
|
return next;
|
|
39
49
|
}
|
|
40
50
|
return hit;
|
|
41
51
|
}
|
|
42
|
-
const reload = (key) => cache.delete(key);
|
|
52
|
+
const reload = (key) => { gen.set(key, (gen.get(key) || 0) + 1); cache.delete(key); };
|
|
53
|
+
/* refetch on the next render but keep showing the last answer meanwhile, so a list does not blank while it updates.
|
|
54
|
+
* It follows a write too, so it bumps the generation like `reload`: a read already in flight is older than the write. */
|
|
55
|
+
const refresh = (key) => { gen.set(key, (gen.get(key) || 0) + 1); const h = cache.get(key); if (h && h.state === 'ok') h.at = 0; else cache.delete(key); };
|
|
43
56
|
/* A route this server does not have answers 404 with the plain text `not found`. Parsing that as JSON
|
|
44
57
|
* showed the PM "Unexpected token 'o', \"not found\" is not valid JSON" — true, and no help. The usual
|
|
45
58
|
* cause is a server still running code older than the page, so say that and the one command. */
|
|
@@ -275,7 +288,8 @@ async function memDo(body) {
|
|
|
275
288
|
const out = await post('/api/memory', { ws, ...body });
|
|
276
289
|
memNote = out && out.ok ? (out.staged ? 'held for approval' : null) : ((out && out.error) || 'not saved');
|
|
277
290
|
if (out && !out.ok && out.entries && out.entries.length) memNote += ' — current entries: ' + out.entries.map((e) => '“' + e.text.slice(0, 40) + '”').join(', ');
|
|
278
|
-
reload
|
|
291
|
+
// the list caches its read per workspace, so the reload names that key; a bare 'memory' left the old list on screen for 15s
|
|
292
|
+
reload('memory:' + ws); reload('memory'); setState({});
|
|
279
293
|
}
|
|
280
294
|
const ask = (q, v) => (typeof window !== 'undefined' && window.prompt ? window.prompt(q, v || '') : null);
|
|
281
295
|
let dataMod = null;
|
|
@@ -494,10 +508,147 @@ export function skillDetailLive(det) {
|
|
|
494
508
|
|
|
495
509
|
/* ── knowledge ──────────────────────────────────────────────────────────────────────────────── */
|
|
496
510
|
function flatten(node, out, depth) {
|
|
497
|
-
for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth });
|
|
511
|
+
for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth, category: d.category || null, basis: d.category_basis || null, why: d.category_why || null, kind: d.category_kind || null, disagrees: d.category_disagrees || [] });
|
|
498
512
|
if (depth < 3) for (const sub of node.dirs || []) { out.push({ dir: sub.name || sub.path, depth }); flatten(sub, out, depth + 1); }
|
|
499
513
|
return out;
|
|
500
514
|
}
|
|
515
|
+
/* jev-doc-categorizer Run 1 — each document's category, derived locally beside its own `type`, with the basis that decided it.
|
|
516
|
+
* `undetermined` is its own value: the file has no evidence, and it is shown as that, never as `other`. A press on a category
|
|
517
|
+
* narrows the list; a second press on it shows everything again. Nothing is written. */
|
|
518
|
+
let knCat = '';
|
|
519
|
+
const catTitle = (it) => it.category ? 'category ' + it.category + (it.kind ? ' (' + it.kind + ')' : '') + (it.basis ? ' · decided by ' + it.basis : ' · no evidence') + (it.why ? ' (' + it.why + ')' : '')
|
|
520
|
+
+ (it.disagrees && it.disagrees.length ? ' · ' + it.disagrees.map((d) => d.basis + ' says ' + d.category).join(', ') : '') + ' · advisory' : '';
|
|
521
|
+
const catChip = (it) => it.category ? el('span', { 'data-cat': it.category, 'data-cat-basis': it.basis || '', title: catTitle(it), style: 'margin-left:8px;font-size:9.5px;letter-spacing:.06em;color:var(--muted-foreground)' }, iv('· ' + it.category + (it.basis === 'jev' ? ' (jev)' : ''))) : null;
|
|
522
|
+
function categoryBar(trees) {
|
|
523
|
+
const counts = {};
|
|
524
|
+
for (const t of trees) for (const it of flatten(t, [], 0)) if (it.path && it.category) counts[it.category] = (counts[it.category] || 0) + 1;
|
|
525
|
+
const names = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
|
|
526
|
+
if (!names.length) return null;
|
|
527
|
+
return el('div', { 'data-cat-bar': '1', style: 'display:flex;flex-wrap:wrap;gap:4px;padding:2px 14px 6px' },
|
|
528
|
+
names.map((n) => el('button', { 'data-cat-filter': n, 'aria-pressed': knCat === n ? 'true' : 'false', title: 'show only ' + n + ' documents', onClick: () => { knCat = knCat === n ? '' : n; setState({}); },
|
|
529
|
+
style: 'font-family:var(--mono);font-size:10px;padding:2px 7px;border:1px solid var(--border);' + (knCat === n ? 'background:var(--accent);' : '') }, iv(n + ' ' + counts[n]))));
|
|
530
|
+
}
|
|
531
|
+
/* D82 — Jev on documents the local rules leave undetermined. The switch is off until the PM turns it on; the first run shows
|
|
532
|
+
* its request count and sends only after the PM confirms that number; afterwards opening this list sends only new or changed
|
|
533
|
+
* files. Jev's categories are marked "(jev)" and are advisory. */
|
|
534
|
+
let dPlan = null; // the plan the PM is looking at: { ws, plan }
|
|
535
|
+
let dNote = null;
|
|
536
|
+
const dSay = (text) => { dNote = text; setState({}); };
|
|
537
|
+
async function dSwitch(ws, on) {
|
|
538
|
+
const out = await post('/api/doccat/jev', { ws, categorize: on });
|
|
539
|
+
reload('library:' + ws); dPlan = null;
|
|
540
|
+
dSay(out && out.ok ? (on ? 'categorizing with Jev is on — review the first run’s count, then confirm it; nothing is sent before that' : 'categorizing with Jev is off') : ((out && out.error) || 'not saved'));
|
|
541
|
+
}
|
|
542
|
+
async function dResume(ws) {
|
|
543
|
+
const out = await post('/api/doccat/jev/resume', { ws });
|
|
544
|
+
reload('library:' + ws);
|
|
545
|
+
dSay(out && out.ok ? 'categorizing with Jev resumed' : ((out && out.error) || 'not resumed'));
|
|
546
|
+
}
|
|
547
|
+
async function dPlanFor(ws) {
|
|
548
|
+
const out = await post('/api/doccat/jev/run', { ws });
|
|
549
|
+
if (!out || out.ok === false) { dPlan = null; return dSay((out && out.error) || 'no plan'); }
|
|
550
|
+
dPlan = { ws, plan: out.plan }; dSay('review the count, then confirm to send');
|
|
551
|
+
}
|
|
552
|
+
async function dRun() {
|
|
553
|
+
if (!dPlan) return;
|
|
554
|
+
const { ws, plan } = dPlan;
|
|
555
|
+
dSay('sending ' + plan.requests + ' request(s) to ' + plan.host + ' …');
|
|
556
|
+
const out = await post('/api/doccat/jev/run', { ws, confirm: true, expect_requests: plan.requests, expect_fingerprint: plan.fingerprint });
|
|
557
|
+
dPlan = null; reload('library:' + ws);
|
|
558
|
+
dSay(!out || out.ok === false ? ((out && out.error) || 'the run failed') : 'asked ' + out.sent + ' · ' + out.decided + ' categorized · ' + out.unsure + ' unsure · ' + (out.unreadable || 0) + ' unreadable · ' + out.cached + ' answered from the cache · ' + out.failed + ' failed');
|
|
559
|
+
}
|
|
560
|
+
function doccatBlock(ws, j) {
|
|
561
|
+
if (!ws || !j) return [];
|
|
562
|
+
const kids = [];
|
|
563
|
+
kids.push(el('div', { 'data-doccat': 'explain', style: NOTE }, iv('Files with no local evidence are “undetermined”. Jev can be asked to categorize them from a short snippet: the title, the headings and the opening, never the whole file. Its answers are marked (jev), are advisory, and an unsure answer stays undetermined.')));
|
|
564
|
+
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
565
|
+
el('button', { style: BTN + (j.switch ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-doccat': 'switch', onClick: () => dSwitch(ws, !j.switch) },
|
|
566
|
+
iv(j.switch ? 'categorize with Jev: ON — turn off' : 'categorize with Jev: OFF — turn on')),
|
|
567
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', disabled: j.open ? null : 'true', 'data-doccat': 'plan',
|
|
568
|
+
title: 'Shows how many requests the undetermined files would take, and sends only after you confirm that number.', onClick: () => dPlanFor(ws) }, 'ask Jev about undetermined files'),
|
|
569
|
+
dPlan && dPlan.ws === ws ? el('button', { style: BTN + ';color:var(--foreground);border-color:var(--foreground)', hover: 'background:var(--accent)', 'data-doccat': 'run', onClick: dRun }, 'confirm and send ' + dPlan.plan.requests + ' request(s)') : null));
|
|
570
|
+
kids.push(el('div', { 'data-doccat': 'gate', 'data-open': j.open ? 'yes' : 'no', style: NOTE + (j.open ? '' : ';color:var(--foreground)') },
|
|
571
|
+
iv(j.open ? (j.confirmed ? 'Active: new or changed undetermined files are sent when the workspace reads this repository’s documents (when it starts, and when this list opens), at most once a minute.' : 'Ready: nothing is sent until you confirm the first run’s count.') : 'Not active: ' + j.sentence + ' All three must hold before anything is sent.')));
|
|
572
|
+
if (j.paused) kids.push(el('div', { 'data-doccat': 'paused', style: 'display:flex;gap:8px;align-items:center;padding:2px 14px 6px;font-size:11.5px;color:var(--destructive)' },
|
|
573
|
+
el('span', null, iv(j.paused.reason)),
|
|
574
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-doccat': 'resume', onClick: () => dResume(ws) }, 'resume')));
|
|
575
|
+
const l = j.last;
|
|
576
|
+
if (l && l.waiting) kids.push(el('div', { 'data-doccat': 'waiting', style: NOTE + ';color:var(--foreground)' }, iv(l.waiting + ' new or changed file(s) are waiting: more than an automatic pass sends. Ask Jev and confirm the count to send them.')));
|
|
577
|
+
else if (l) kids.push(el('div', { 'data-doccat': 'last', style: NOTE }, iv('Last Jev pass ' + String(l.at).replace('T', ' ').slice(0, 16) + ': sent ' + l.sent + ' · categorized ' + l.decided + ' · unsure ' + l.unsure + ' · unreadable ' + (l.unreadable || 0) + ' · withheld ' + (l.withheld_count || 0) + ' · excluded ' + l.excluded + ' · failed ' + l.failed + '.')));
|
|
578
|
+
if (dPlan && dPlan.ws === ws) {
|
|
579
|
+
const p = dPlan.plan;
|
|
580
|
+
kids.push(el('div', { 'data-doccat': 'plan-text', style: NOTE }, iv(p.requests + ' request(s), about ' + p.characters.toLocaleString() + ' characters, to ' + p.host + ' · ' + p.candidates + ' undetermined file(s) · ' + p.cached + ' already answered · ' + p.excluded + ' excluded · ' + p.withheld_count + ' withheld by the credential scan.')));
|
|
581
|
+
}
|
|
582
|
+
if (dNote) kids.push(el('div', { 'data-doccat': 'note', style: NOTE }, iv(dNote)));
|
|
583
|
+
return kids;
|
|
584
|
+
}
|
|
585
|
+
/* jev-doc-categorizer Run 3 (C7) — the PM corrects a category. `fix` on a row opens a small control: a category, a ticket kind for
|
|
586
|
+
* a ticket, `save correction` and `clear correction`. A correction is kept per user, outside the repository, and never edits the
|
|
587
|
+
* file. `write it into the file` is a separate press: it shows the one-line diff first, and `apply` writes it only if the file is
|
|
588
|
+
* still the text the diff was made from (the server compares the sha1 and answers 409 when it is not). The server checks every
|
|
589
|
+
* value again; these two lists only fill the menus. */
|
|
590
|
+
const CATS = ['prd', 'meeting-notes', 'report', 'ticket', 'knowledge', 'spec-artifact', 'other'];
|
|
591
|
+
const KINDS = ['design', 'engineering', 'other'];
|
|
592
|
+
let knFix = null; // { path, category, ticket_kind } — the row whose control is open
|
|
593
|
+
let knPlan = null; // { path, category, ticket_kind, diff, sha, unchanged } — the write-back diff on screen
|
|
594
|
+
let knNote = null; // { path, tone, text }
|
|
595
|
+
let knBusy = false;
|
|
596
|
+
const FIX = 'font-family:var(--mono);font-size:9.5px;padding:1px 6px;margin-right:10px;border:1px solid var(--border);color:var(--muted-foreground);flex:none';
|
|
597
|
+
const SEL = 'font-family:var(--mono);font-size:10.5px;padding:2px 4px;border:1px solid var(--border);background:var(--background);color:var(--foreground)';
|
|
598
|
+
const knSay = (path, tone, text) => { knNote = { path, tone, text }; knBusy = false; setState({}); };
|
|
599
|
+
function knOpen(it) {
|
|
600
|
+
if (knFix && knFix.path === it.path) { knFix = null; knPlan = null; knNote = null; setState({}); return; }
|
|
601
|
+
const cat = CATS.includes(it.category) ? it.category : 'other';
|
|
602
|
+
knFix = { path: it.path, category: cat, ticket_kind: cat === 'ticket' ? (it.kind || 'other') : null };
|
|
603
|
+
knPlan = null; knNote = null; setState({});
|
|
604
|
+
}
|
|
605
|
+
async function knSave(ws, clear) {
|
|
606
|
+
if (!knFix || knBusy) return;
|
|
607
|
+
knBusy = true; setState({});
|
|
608
|
+
const body = clear ? { ws, path: knFix.path, clear: true } : { ws, path: knFix.path, category: knFix.category, ticket_kind: knFix.category === 'ticket' ? knFix.ticket_kind : null };
|
|
609
|
+
const res = await post('/api/doccat/correct', body);
|
|
610
|
+
refresh('library:' + ws);
|
|
611
|
+
if (res && res.ok) knSay(knFix.path, 'ok', clear ? (res.cleared ? 'correction cleared — the rules decide this file again' : 'this file had no correction') : 'saved — your correction wins over every rule. The file is not changed.');
|
|
612
|
+
else knSay(knFix.path, 'bad', (res && res.error) || 'not saved');
|
|
613
|
+
}
|
|
614
|
+
async function knWriteback(ws, confirm) {
|
|
615
|
+
if (!knFix || knBusy) return;
|
|
616
|
+
knBusy = true; setState({});
|
|
617
|
+
const want = { category: knFix.category, ticket_kind: knFix.category === 'ticket' ? knFix.ticket_kind : null };
|
|
618
|
+
if (!confirm) {
|
|
619
|
+
const res = await post('/api/doccat/writeback', { ws, path: knFix.path, ...want });
|
|
620
|
+
if (res && res.ok) { knPlan = { path: knFix.path, ...want, diff: res.diff, sha: res.sha, next_sha: res.next_sha, unchanged: !!res.unchanged }; knSay(knFix.path, 'mut', res.unchanged ? 'the file already says this — nothing to write' : 'this is the change. Press apply to write it into the file.'); }
|
|
621
|
+
else { knPlan = null; knSay(knFix.path, 'bad', (res && res.error) || 'no diff'); }
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (!knPlan) { knBusy = false; return; }
|
|
625
|
+
const res = await post('/api/doccat/writeback', { ws, path: knPlan.path, category: knPlan.category, ticket_kind: knPlan.ticket_kind, confirm: true, expect_sha: knPlan.sha, expect_next_sha: knPlan.next_sha });
|
|
626
|
+
knPlan = null;
|
|
627
|
+
refresh('library:' + ws);
|
|
628
|
+
if (res && res.ok) knSay(knFix.path, 'ok', res.written ? 'written into the file\'s frontmatter' : 'the file already says this — nothing was written');
|
|
629
|
+
else knSay(knFix.path, 'bad', (res && res.error) || 'not written');
|
|
630
|
+
}
|
|
631
|
+
function knPanel(ws, it) {
|
|
632
|
+
const f = knFix;
|
|
633
|
+
const pad = 'padding:2px 14px 8px ' + (20 + it.depth * 12) + 'px';
|
|
634
|
+
const btn = (attr, label, fn, disabled) => el('button', { [attr]: '1', disabled: knBusy || disabled, onClick: fn, style: BTN, hover: 'color:var(--foreground)' }, label);
|
|
635
|
+
const kids = [
|
|
636
|
+
el('select', { 'data-cat-select': '1', 'aria-label': 'category', style: SEL, onChange: (e) => { f.category = e.target.value; f.ticket_kind = f.category === 'ticket' ? (f.ticket_kind || 'other') : null; knPlan = null; setState({}); } },
|
|
637
|
+
CATS.map((c) => el('option', { value: c, selected: c === f.category }, c))),
|
|
638
|
+
f.category === 'ticket' ? el('select', { 'data-cat-kind': '1', 'aria-label': 'ticket kind', style: SEL, onChange: (e) => { f.ticket_kind = e.target.value; knPlan = null; setState({}); } },
|
|
639
|
+
KINDS.map((k) => el('option', { value: k, selected: k === f.ticket_kind }, k))) : null,
|
|
640
|
+
btn('data-cat-save', 'save correction', () => knSave(ws, false)),
|
|
641
|
+
it.basis === 'pm correction' ? btn('data-cat-clear', 'clear correction', () => knSave(ws, true)) : null,
|
|
642
|
+
btn('data-cat-writeback', 'write it into the file', () => knWriteback(ws, false)),
|
|
643
|
+
];
|
|
644
|
+
const out = [el('div', { 'data-cat-panel': it.path, style: pad + ';display:flex;flex-wrap:wrap;gap:6px;align-items:center' }, kids)];
|
|
645
|
+
if (knPlan && knPlan.path === it.path && !knPlan.unchanged) {
|
|
646
|
+
out.push(el('pre', { 'data-cat-diff': '1', style: pad.replace('padding:2px', 'margin:0;padding:6px') + ';font-family:var(--mono);font-size:10.5px;white-space:pre-wrap;color:var(--foreground);background:var(--accent)' }, iv(knPlan.diff)));
|
|
647
|
+
out.push(el('div', { style: pad + ';display:flex;gap:6px' }, btn('data-cat-apply', 'apply', () => knWriteback(ws, true)), btn('data-cat-cancel', 'cancel', () => { knPlan = null; knNote = null; setState({}); })));
|
|
648
|
+
}
|
|
649
|
+
if (knNote && knNote.path === it.path) out.push(el('div', { 'data-cat-note': knNote.tone, style: pad + ';font-size:11px;color:' + TONE[knNote.tone] }, iv(knNote.text)));
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
501
652
|
export function knowledgeLive() {
|
|
502
653
|
const ws = state.wsId || '';
|
|
503
654
|
const r = read('library:' + ws, '/api/library?ws=' + encodeURIComponent(ws));
|
|
@@ -505,11 +656,18 @@ export function knowledgeLive() {
|
|
|
505
656
|
if (st) return [st];
|
|
506
657
|
const trees = (r.data && r.data.knowledge) || [];
|
|
507
658
|
const kids = [el('div', { style: NOTE }, iv('This repository\'s knowledge: the OKF bundle, skills and build-skills. Open a document to read it.'))];
|
|
659
|
+
const bar = categoryBar(trees); if (bar) kids.push(bar);
|
|
660
|
+
kids.push(...doccatBlock(ws, r.data && r.data.doccat));
|
|
508
661
|
for (const t of trees) {
|
|
509
662
|
kids.push(el('div', { style: LABEL }, iv(t.path || t.name || 'knowledge')));
|
|
510
663
|
for (const it of flatten(t, [], 0)) {
|
|
664
|
+
if (knCat && (it.dir || it.category !== knCat)) continue;
|
|
511
665
|
if (it.dir) { kids.push(el('div', { style: 'padding:3px 14px 1px ' + (14 + it.depth * 12) + 'px;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(it.dir + '/'))); continue; }
|
|
512
|
-
|
|
666
|
+
const open = !!(knFix && knFix.path === it.path);
|
|
667
|
+
kids.push(el('div', { 'data-cat-row': it.path, style: 'display:flex;align-items:center' },
|
|
668
|
+
el('button', { onClick: () => openDocIn(null, it.path), style: 'display:block;flex:1 1 auto;min-width:0;text-align:left;padding:4px 14px 4px ' + (20 + it.depth * 12) + 'px;font-family:var(--mono);font-size:11.5px', hover: 'background:var(--accent)', 'data-r4': 'know' }, iv(it.name), catChip(it)),
|
|
669
|
+
el('button', { 'data-cat-fix': it.path, 'aria-expanded': open ? 'true' : 'false', title: 'correct the category of this document', onClick: () => knOpen(it), style: FIX + (open ? ';background:var(--accent);color:var(--foreground)' : ''), hover: 'color:var(--foreground)' }, 'fix')));
|
|
670
|
+
if (open) kids.push(...knPanel(ws, it));
|
|
513
671
|
}
|
|
514
672
|
}
|
|
515
673
|
// R11 — the notes PMOS and its agents wrote here (requirement notes, report drafts and definitions, memory).
|
|
@@ -614,6 +772,12 @@ async function jSwitch(on) {
|
|
|
614
772
|
reload('claims'); jPlan = null;
|
|
615
773
|
jSay(out && out.ok ? (on ? 'checking with Jev automatically is on — the first check records what exists and sends nothing; after that only changed pairs are sent' : 'checking with Jev automatically is off') : ((out && out.error) || 'not saved'));
|
|
616
774
|
}
|
|
775
|
+
async function jStatus(on) {
|
|
776
|
+
const ws = vWs(); if (!ws) return;
|
|
777
|
+
const out = await post('/api/claims/jev/status', { ws, status_read: on });
|
|
778
|
+
reload('claims'); jPlan = null;
|
|
779
|
+
jSay(out && out.ok ? (on ? 'reading documents for status is on — the next check records the documents that exist and sends nothing for them; after that only changed documents are sent' : 'reading documents for status is off') : ((out && out.error) || 'not saved'));
|
|
780
|
+
}
|
|
617
781
|
async function jResume() {
|
|
618
782
|
const ws = vWs(); if (!ws) return;
|
|
619
783
|
const out = await post('/api/claims/jev/resume', { ws });
|
|
@@ -648,7 +812,11 @@ function jevBlock(ws, d) {
|
|
|
648
812
|
kids.push(el('div', { 'data-r24': 'explain', style: NOTE }, iv('Where the local checks cannot decide, a pair of short texts (a requirement and the rubric’s dimensions, a story section and the requirement it cites, a sentence and the decision it cites, two sections of one PRD) can be put to Jev. Only pairs that changed are sent. Jev’s answers are suspicions: advisory, shown in words, and never counted as problems.')));
|
|
649
813
|
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
650
814
|
el('button', { style: BTN + (j.switch ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-r24': 'switch', onClick: () => jSwitch(!j.switch) },
|
|
651
|
-
iv(j.switch ? 'check with Jev automatically: ON — turn off' : 'check with Jev automatically: OFF — turn on'))
|
|
815
|
+
iv(j.switch ? 'check with Jev automatically: ON — turn off' : 'check with Jev automatically: OFF — turn on')),
|
|
816
|
+
el('button', { style: BTN + (j.status_read ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-r24': 'status-switch',
|
|
817
|
+
title: 'Also send the documents tied to each initiative (its PRD, rubric and brief, and notes that name it), to ask whether a document is a stub and whether a note contradicts a requirement. The answers are suggestions in the initiative detail. Needs the switch on the left.',
|
|
818
|
+
onClick: () => jStatus(!j.status_read) },
|
|
819
|
+
iv(j.status_read ? 'read documents for status: ON — turn off' : 'read documents for status: OFF — turn on'))));
|
|
652
820
|
kids.push(el('div', { 'data-r24': 'gate', 'data-open': j.open ? 'yes' : 'no', style: NOTE + (j.open ? '' : ';color:var(--foreground)') },
|
|
653
821
|
iv(j.open ? 'Active: a key is saved, validation is on for this repository, automatic checking is on and this switch is on.' : 'Not active: ' + j.sentence + ' All four must hold before anything is sent.')));
|
|
654
822
|
if (j.paused) kids.push(el('div', { 'data-r24': 'paused', style: 'display:flex;gap:8px;align-items:center;padding:2px 14px 6px;font-size:11.5px;color:var(--destructive)' },
|
|
@@ -810,12 +978,42 @@ export function validateList() {
|
|
|
810
978
|
/* ── Today (proactive-engine D1): what is waiting, computed from the ledgers, with feedback ─────────────────────── */
|
|
811
979
|
let tNote = null;
|
|
812
980
|
let tMissed = false;
|
|
981
|
+
/* The text inputs of the Today list are kept across redraws. The list is drawn again on every state change (the 4-second retry of
|
|
982
|
+
* an unread repository is one), and a new input on each draw threw away what the person was typing and the caret with it. Each
|
|
983
|
+
* input is made once; a redraw writes the stored value into it only while the person has not edited it since the last save. */
|
|
984
|
+
const tInputs = new Map();
|
|
985
|
+
function tInput(key, attrs, stored) {
|
|
986
|
+
let n = tInputs.get(key);
|
|
987
|
+
if (!n) {
|
|
988
|
+
n = el('input', { ...attrs, onInput: (e) => { e.target.dataset.dirty = '1'; } });
|
|
989
|
+
tInputs.set(key, n);
|
|
990
|
+
}
|
|
991
|
+
if (!n.dataset.dirty && n.value !== stored) n.value = stored;
|
|
992
|
+
// The redraw moves the input into a new row, and moving a node blurs it: give the focus and the caret back once the draw is done.
|
|
993
|
+
if (typeof document !== 'undefined' && document.activeElement === n) {
|
|
994
|
+
let a = null, b = null;
|
|
995
|
+
try { a = n.selectionStart; b = n.selectionEnd; } catch { /* a number input has no selection */ }
|
|
996
|
+
queueMicrotask(() => {
|
|
997
|
+
if (!n.isConnected || document.activeElement === n) return;
|
|
998
|
+
n.focus();
|
|
999
|
+
try { if (a !== null) n.setSelectionRange(a, b); } catch { /* as above */ }
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
return n;
|
|
1003
|
+
}
|
|
1004
|
+
/** Save a settings patch from kept inputs; on success their text is the stored value again. */
|
|
1005
|
+
async function tSaveFrom(inputs, patch) {
|
|
1006
|
+
const out = await tSettings(patch);
|
|
1007
|
+
if (out && out.ok !== false) for (const n of inputs) delete n.dataset.dirty;
|
|
1008
|
+
setState({});
|
|
1009
|
+
}
|
|
813
1010
|
let tRetry = null;
|
|
814
1011
|
const tSay = (tone, text) => { tNote = { tone, text }; setState({}); };
|
|
815
1012
|
const OWNER_WORD = { pm: 'yours', agent: 'the agent\'s' };
|
|
816
1013
|
async function tFeedback(body, say) {
|
|
817
1014
|
const out = await post('/api/proactive/feedback', body);
|
|
818
1015
|
if (!out || out.ok === false) return tSay('bad', (out && out.error) || 'feedback was not saved');
|
|
1016
|
+
reload('learn');
|
|
819
1017
|
tSay('ok', say);
|
|
820
1018
|
}
|
|
821
1019
|
async function tOpen(c) {
|
|
@@ -831,8 +1029,9 @@ async function tScan() {
|
|
|
831
1029
|
}
|
|
832
1030
|
async function tSettings(patch) {
|
|
833
1031
|
const out = await post('/api/proactive/settings', patch);
|
|
834
|
-
reload('today'); reload('brief'); reload('prepare');
|
|
1032
|
+
reload('today'); reload('brief'); reload('prepare'); reload('learn');
|
|
835
1033
|
tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'not saved') : 'saved');
|
|
1034
|
+
return out;
|
|
836
1035
|
}
|
|
837
1036
|
function tCard(c) {
|
|
838
1037
|
const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
|
|
@@ -841,6 +1040,7 @@ function tCard(c) {
|
|
|
841
1040
|
el('span', { style: 'font-size:12.5px' }, iv(c.title)),
|
|
842
1041
|
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(c.kind + ' · ' + (OWNER_WORD[c.owner] || c.owner)))),
|
|
843
1042
|
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(c.why + (c.evidence ? ' — ' + c.evidence : ''))),
|
|
1043
|
+
c.adjust ? el('span', { style: 'display:block;font-size:10.5px;color:var(--muted-foreground);margin:0 0 4px', 'data-r26': 'adjust' }, iv('Learned from your feedback: ' + c.adjust_why + '.')) : null,
|
|
844
1044
|
el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
|
|
845
1045
|
c.open ? b('open', 'open', () => tOpen(c)) : null,
|
|
846
1046
|
c.initiative_id && c.ws ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r23': 'prepare', onClick: () => bgShowPlan(c) }, 'prepare in the background') : null,
|
|
@@ -967,6 +1167,51 @@ function backgroundSection() {
|
|
|
967
1167
|
if (d.entries.some((e) => e.status === 'queued')) kids.push(el('div', { style: 'padding:4px 14px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'look at the queue now; ignores quiet hours only', 'data-r23': 'run', onClick: bgRun }, 'run the queue now')));
|
|
968
1168
|
return kids;
|
|
969
1169
|
}
|
|
1170
|
+
/* learning (proactive-engine D4): bounded weights, a weekly rollup, lessons that wait for the PM's press, triggers left off */
|
|
1171
|
+
let lNote = null;
|
|
1172
|
+
async function lPost(url, body, say) {
|
|
1173
|
+
const out = await post(url, body);
|
|
1174
|
+
lNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'not done — nothing changed' } : { tone: 'ok', text: typeof say === 'function' ? say(out) : say };
|
|
1175
|
+
reload('learn'); reload('today'); setState({});
|
|
1176
|
+
}
|
|
1177
|
+
const lRate = (x, what) => x.rate === null ? what + ': not enough evidence (' + x.n + ' of ' + x.min + ' needed)' : what + ': ' + Math.round(x.rate * 100) + '% (' + x.n + ' counted)';
|
|
1178
|
+
function learningSection() {
|
|
1179
|
+
const r = read('learn', '/api/proactive/learn');
|
|
1180
|
+
const st = status(r, 'what it learned');
|
|
1181
|
+
const kids = [el('div', { style: LABEL }, iv('learning'))];
|
|
1182
|
+
if (st) return [...kids, st];
|
|
1183
|
+
const d = r.data; const ro = d.rollup;
|
|
1184
|
+
const b = (label, attr, fn, title) => el('button', { style: BTN, hover: 'color:var(--foreground)', title: title || null, 'data-r26': attr, onClick: fn }, label);
|
|
1185
|
+
kids.push(el('div', { style: NOTE, 'data-r26': 'state' }, iv((d.paused ? 'Paused: nothing is weighted, narrowed or proposed, and feedback is still recorded. ' : 'On. ') + d.evidenceCount + ' piece(s) of evidence' + (d.resetAt ? ' since you reset it' : '') + '. Computed from your own feedback and review decisions on this computer; no model is called. It cannot lower or hide what blocks on you.')));
|
|
1186
|
+
kids.push(el('div', { style: NOTE, 'data-r26': 'rollup' }, iv('Last ' + ro.days + ' days — ' + lRate(ro.hit, 'hit rate') + ' · ' + lRate(ro.asIs, 'used as is') + ' · ' + lRate(ro.corrections, 'corrections per prepared run') + ' · ' + ro.missed + ' reported missed')));
|
|
1187
|
+
kids.push(el('div', { style: NOTE + (ro.watermelon.flag ? ';color:var(--destructive)' : ''), 'data-r26': 'watermelon' }, iv('Watermelon check: ' + ro.watermelon.note + (ro.watermelon.reasons.length ? ' ' + ro.watermelon.reasons.join('; ') + '.' : ''))));
|
|
1188
|
+
const cells = d.cells.filter((c) => c.delta);
|
|
1189
|
+
if (!cells.length) kids.push(el('div', { style: NOTE, 'data-r26': 'no-weights' }, iv('No weight has moved yet. A cell moves after ' + r.data.min_cell + ' events, by at most ' + d.caps.kind + ' points per trigger, ' + d.caps.lane + ' per lane and ' + d.caps.time + ' per trigger and time of day, ' + d.caps.total + ' in all.')));
|
|
1190
|
+
for (const c of cells) kids.push(el('div', { style: 'padding:2px 14px;font-size:11.5px', 'data-r26': 'weight' }, iv(c.dim + ' ' + c.key.replace('|', ' in the ') + ': ' + (c.delta > 0 ? '+' : '') + c.delta + ' (' + c.n + ' events, net ' + c.sum + ')')));
|
|
1191
|
+
for (const n of d.narrowed) kids.push(el('div', { 'data-r26': 'narrowed', 'data-kind': n.kind, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
|
|
1192
|
+
el('span', { style: 'display:block;font-size:12px' }, iv('Left off Today: ' + n.label.toLowerCase() + '. Useful ' + Math.round(n.rate * 100) + '% over ' + n.n + ' ratings in ' + n.days + ' days; the floor is ' + Math.round(n.floor * 100) + '%. They are still scanned.')),
|
|
1193
|
+
b('restore', 'restore', () => lPost('/api/proactive/learn/restore', { kind: n.kind, confirm: true }, 'restored — it stays until 10 new ratings of it have arrived'))));
|
|
1194
|
+
const proposed = d.lessons.filter((x) => x.status === 'proposed');
|
|
1195
|
+
kids.push(el('div', { style: LABEL }, iv('lessons to approve')));
|
|
1196
|
+
if (!proposed.length) kids.push(el('div', { style: NOTE, 'data-r26': 'no-lessons' }, iv('None waiting. A weekly review proposes lessons; none enters anything until you approve it.')));
|
|
1197
|
+
for (const l of proposed) kids.push(el('div', { 'data-r26': 'lesson', 'data-id': l.id, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
|
|
1198
|
+
el('span', { style: 'display:block;font-size:12px' }, iv(l.text)),
|
|
1199
|
+
el('span', { style: 'display:block;font-size:10.5px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(l.evidence)),
|
|
1200
|
+
el('span', { style: 'display:flex;gap:4px' },
|
|
1201
|
+
b('approve', 'approve', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'approve', confirm: true }, 'approved — the morning-brief session will read it'), 'the morning-brief session reads it; nothing else does'),
|
|
1202
|
+
b('reject', 'reject', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'reject', confirm: true }, 'rejected — it is not proposed again for 28 days')))));
|
|
1203
|
+
if (d.approved.length) kids.push(el('div', { style: LABEL }, iv('approved lessons (' + d.approved.length + ' of ' + d.max_approved + ')')));
|
|
1204
|
+
for (const l of d.approved) kids.push(el('div', { 'data-r26': 'approved', 'data-id': l.id, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
|
|
1205
|
+
el('span', { style: 'display:block;font-size:12px' }, iv(l.text)),
|
|
1206
|
+
b('retire', 'retire', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'retire', confirm: true }, 'retired'))));
|
|
1207
|
+
if (lNote) kids.push(el('div', { 'data-r26': 'note', style: NOTE + ';color:' + (lNote.tone === 'bad' ? 'var(--destructive)' : 'var(--foreground)') }, iv(lNote.text)));
|
|
1208
|
+
const last = d.snapshots[0];
|
|
1209
|
+
kids.push(el('div', { style: NOTE, 'data-r26': 'last-run' }, iv(last ? 'Last weekly review: ' + String(last.at).replace('T', ' ').slice(0, 16) + ' UTC. It runs about every 7 days, only while the workspace server runs, and does not catch up.' : 'No weekly review has run yet. It runs about every 7 days once there is feedback, only while the workspace server runs, and does not catch up.')));
|
|
1210
|
+
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
1211
|
+
b('run the weekly review now', 'run', () => lPost('/api/proactive/learn/run', {}, (o) => 'review taken — ' + o.proposed.length + ' lesson(s) proposed'), 'refused while learning is paused'),
|
|
1212
|
+
b('forget what it learned', 'reset', () => { if (typeof window !== 'undefined' && window.confirm && !window.confirm('Forget what it learned? Earlier feedback stops counting. Nothing is deleted.')) return; lPost('/api/proactive/learn/reset', { confirm: true }, 'reset — earlier feedback is ignored from now on, not deleted'); }, 'earlier feedback stops counting; nothing is deleted')));
|
|
1213
|
+
return kids;
|
|
1214
|
+
}
|
|
970
1215
|
export function todayList() {
|
|
971
1216
|
const r = read('today', '/api/proactive/today');
|
|
972
1217
|
const kids = [el('div', { style: NOTE, 'data-r22': 'intro' }, iv('What is waiting for you, worked out from the ledgers and git on this computer. It runs only while the workspace server runs, reads no mail or calendar, and changes nothing: your feedback only changes how this list is ordered later.'))];
|
|
@@ -974,12 +1219,12 @@ export function todayList() {
|
|
|
974
1219
|
kids.push(el('div', { style: LABEL }, iv('waiting on records')));
|
|
975
1220
|
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:4px 14px' },
|
|
976
1221
|
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'scan', onClick: tScan }, 'scan now'),
|
|
977
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); setState({}); } }, '↻ refresh'),
|
|
1222
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); reload('learn'); setState({}); } }, '↻ refresh'),
|
|
978
1223
|
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed', onClick: () => { tMissed = !tMissed; setState({}); } }, 'something was missed')));
|
|
979
1224
|
if (tMissed) {
|
|
980
|
-
const ta =
|
|
1225
|
+
const ta = tInput('missed', { type: 'text', placeholder: 'what should it have shown you?', 'data-r22': 'missed-input', style: 'flex:1;min-width:0;font-size:11.5px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);min-height:28px' }, '');
|
|
981
1226
|
kids.push(el('div', { style: 'display:flex;gap:4px;padding:2px 14px 6px' }, ta,
|
|
982
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed-save', onClick: async () => { await tFeedback({ verdict: 'missed', note: ta.value }, 'recorded as missed'); if (tNote && tNote.tone === 'ok') tMissed = false; setState({}); } }, 'save')));
|
|
1227
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed-save', onClick: async () => { await tFeedback({ verdict: 'missed', note: ta.value }, 'recorded as missed'); if (tNote && tNote.tone === 'ok') { tMissed = false; delete ta.dataset.dirty; } setState({}); } }, 'save')));
|
|
983
1228
|
}
|
|
984
1229
|
if (tNote) kids.push(el('div', { 'data-r22': 'note', style: NOTE + ';color:' + (tNote.tone === 'bad' ? 'var(--destructive)' : tNote.tone === 'ok' ? 'var(--foreground)' : 'var(--muted-foreground)') }, iv(tNote.text)));
|
|
985
1230
|
const st = status(r, 'Today');
|
|
@@ -993,6 +1238,7 @@ export function todayList() {
|
|
|
993
1238
|
for (const u of d.unreadable || []) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r22': 'unreadable' }, iv('Not read — ' + (u.name || u.repo || 'a repository') + ': ' + (u.error || 'unknown reason') + '. Nothing is shown for it, which is not the same as nothing waiting.')));
|
|
994
1239
|
if (d.empty) kids.push(el('div', { style: NOTE, 'data-r22': 'empty' }, iv('Nothing is waiting on any repository that was read.')));
|
|
995
1240
|
for (const c of d.candidates || []) { kids.push(tCard(c)); if (bgPlan && bgPlan.cid === c.id) kids.push(bgPlanView()); }
|
|
1241
|
+
for (const n of d.narrowed || []) kids.push(el('div', { style: NOTE, 'data-r26': 'narrowed-note' }, iv(n.hidden + ' ' + n.label.toLowerCase() + ' hidden: you rated them useful only ' + Math.round(n.rate * 100) + '% of the time. Restore them under learning below.')));
|
|
996
1242
|
kids.push(...backgroundSection());
|
|
997
1243
|
if (d.dismissed) kids.push(el('div', { style: NOTE, 'data-r22': 'dismissed' }, iv(d.dismissed + ' dismissed item(s) hidden for 7 days.')));
|
|
998
1244
|
// settings
|
|
@@ -1005,16 +1251,16 @@ export function todayList() {
|
|
|
1005
1251
|
kids.push(el('div', { style: LABEL }, iv('morning brief')));
|
|
1006
1252
|
for (const k of Object.keys(s.briefSources || {})) kids.push(chk('read ' + k + (k === 'mail' ? ' (titles and senders only)' : ''), s.briefSources[k], 'set-src-' + k, (v) => ({ briefSources: { [k]: v } })));
|
|
1007
1253
|
kids.push(chk('typed scoring (sends titles and metadata to the Typesafe service)', s.briefScoring, 'set-scoring', (v) => ({ briefScoring: v })));
|
|
1008
|
-
const nm =
|
|
1009
|
-
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:4px 14px;font-size:12px' }, nm, el('button', { style: BTN, 'data-r22': 'set-name-save', onClick: () =>
|
|
1010
|
-
kids.push(
|
|
1011
|
-
kids.push(chk('pause learning (nothing
|
|
1012
|
-
const days =
|
|
1013
|
-
const qs =
|
|
1014
|
-
const qe =
|
|
1254
|
+
const nm = tInput('name', { type: 'text', maxlength: '40', placeholder: 'your name for the greeting', 'data-r22': 'set-name', style: 'width:180px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.name || '');
|
|
1255
|
+
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:4px 14px;font-size:12px' }, nm, el('button', { style: BTN, 'data-r22': 'set-name-save', onClick: () => tSaveFrom([nm], { name: nm.value }) }, 'save')));
|
|
1256
|
+
kids.push(...learningSection());
|
|
1257
|
+
kids.push(chk('pause learning (no weights, no lessons, nothing narrowed)', s.pauseLearning, 'set-pause', (v) => ({ pauseLearning: v })));
|
|
1258
|
+
const days = tInput('stalled', { type: 'number', min: '1', max: '90', 'data-r22': 'set-stalled-days', style: 'width:60px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, String(s.stalledDays));
|
|
1259
|
+
const qs = tInput('quiet-start', { type: 'text', placeholder: '22:00', 'data-r22': 'set-quiet-start', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.quiet ? s.quiet.start : '');
|
|
1260
|
+
const qe = tInput('quiet-end', { type: 'text', placeholder: '07:00', 'data-r22': 'set-quiet-end', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.quiet ? s.quiet.end : '');
|
|
1015
1261
|
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px;font-size:12px' }, iv('stalled after'), days, iv('days'),
|
|
1016
|
-
el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () =>
|
|
1262
|
+
el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () => tSaveFrom([days], { stalledDays: Number(days.value) }) }, 'save')));
|
|
1017
1263
|
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px 8px;font-size:12px' }, iv('quiet hours'), qs, iv('to'), qe,
|
|
1018
|
-
el('button', { style: BTN, 'data-r22': 'set-quiet-save', onClick: () =>
|
|
1264
|
+
el('button', { style: BTN, 'data-r22': 'set-quiet-save', onClick: () => tSaveFrom([qs, qe], { quiet: qs.value.trim() || qe.value.trim() ? { start: qs.value.trim(), end: qe.value.trim() } : null }) }, 'save')));
|
|
1019
1265
|
return [el('div', { style: 'padding:2px 0 8px', 'data-r22': 'today' }, kids)];
|
|
1020
1266
|
}
|