create-pmos 0.59.3 → 0.60.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/scripts/delivery-index.js +85 -7
- package/payload/scripts/workflow-state.js +3 -3
- package/payload/state/discoveries.md +1 -1
- package/workspace/fixtures/initiatives-api/sheet.json +29 -7
- package/workspace/guide/index.md +3 -3
- package/workspace/guide/initiatives-screen.md +25 -1
- package/workspace/guide/layout.md +6 -6
- package/workspace/guide/proactive-engine.md +1 -0
- package/workspace/guide/today.md +23 -1
- package/workspace/lib/calendar-memory.js +257 -0
- package/workspace/lib/console-state.js +1 -1
- package/workspace/lib/doc-category.js +3 -1
- package/workspace/lib/engine/delivery-index.js +85 -7
- package/workspace/lib/engine/workflow-state.js +3 -3
- package/workspace/lib/initiative-files.js +59 -1
- package/workspace/lib/initiative-rows.js +14 -2
- package/workspace/lib/mobile-nav.js +1 -1
- package/workspace/lib/plan-tree.js +44 -5
- package/workspace/lib/proactive-cli.js +16 -1
- package/workspace/lib/routes-initiatives.js +165 -9
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/index.html +1 -1
- package/workspace/public/pm/components.css +70 -0
- package/workspace/public/pm/dest-initiatives.js +7 -1
- package/workspace/public/pm/frame-copy.js +2 -2
- package/workspace/public/pm/initiative-sheet.js +42 -10
- package/workspace/public/pm/initiatives-api.js +15 -0
- package/workspace/public/pm/initiatives-copy.js +13 -0
- package/workspace/public/pm/initiatives-ui.js +14 -0
- package/workspace/public/pm/registry.js +1 -1
- package/workspace/public/pm/today-agenda.js +305 -0
- package/workspace/public/pm/today-copy.js +54 -2
- package/workspace/public/pm/today-day.js +23 -3
- package/workspace/public/pm/today-goals.js +7 -1
- package/workspace/server.js +53 -2
- package/workspace/skills/morning-brief.skill +6 -1
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/scan-consistency.json +5 -0
|
@@ -175,6 +175,42 @@ function owedFor(group, steps, def) {
|
|
|
175
175
|
return null;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The index of the furthest stage with evidence (scan-consistency R2): the last stage that holds a step the ladder
|
|
180
|
+
* reads `done`. -1 when no stage has one.
|
|
181
|
+
*
|
|
182
|
+
* WHY THE STEPPER NEEDS IT. The current stage used to be the FIRST stage with an open step. A step skipped long ago — a
|
|
183
|
+
* `priors` citation nobody wrote, a contract never logged before the build — then pinned the initiative at Spec or
|
|
184
|
+
* Contract for ever, while the build had merged and the PM had accepted it: 90 built initiatives read Spec on this
|
|
185
|
+
* repository, and 51 accepted ones read Intake, Spec, Contract or Build. Work moves forward past a gap; the gap stays a
|
|
186
|
+
* fact. So the current stage is the first stage with an open step AT OR AFTER the frontier, and an earlier stage that
|
|
187
|
+
* still has an open step reads `skipped` (its owed row is kept), never `done`.
|
|
188
|
+
*/
|
|
189
|
+
function frontierOf(stages, steps) {
|
|
190
|
+
const done = new Set((steps || []).filter((s) => s && s.status === 'done').map((s) => s.key));
|
|
191
|
+
// Accepted work is finished whatever its leftover steps (initiative-display.js rule 2): the frontier is past the end,
|
|
192
|
+
// so no stage is current and every stage with an open step reads `skipped`. A document lane is finished once it is
|
|
193
|
+
// approved and no handoff is still open.
|
|
194
|
+
const by = new Map((steps || []).filter(Boolean).map((s) => [s.key, s.status]));
|
|
195
|
+
if (by.get('accept') === 'done' || (by.get('approved') === 'done' && !OPEN(by.get('handoff') || 'na'))) return stages.length;
|
|
196
|
+
let at = -1;
|
|
197
|
+
stages.forEach((s, i) => { if (s.steps.some((k) => done.has(k))) at = i; });
|
|
198
|
+
return at;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The stage an initiative is in now, from its ladder steps, by the same rule the Breakdown and the sheet's stepper use:
|
|
203
|
+
* { id, label } of the first stage at or after the frontier with an open step, or null when no stage has one.
|
|
204
|
+
*/
|
|
205
|
+
function currentStage(root, initiative, steps, def) {
|
|
206
|
+
const stages = stagesFor(root, initiative);
|
|
207
|
+
const front = frontierOf(stages, steps);
|
|
208
|
+
for (let i = Math.max(front, 0); i < stages.length; i++) {
|
|
209
|
+
if (owedFor(stages[i], steps, def) !== null) return { id: stages[i].id, label: stages[i].label };
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
178
214
|
/**
|
|
179
215
|
* The progress label (R6), shared by the layout, the fold's progressOf and the CLI. Proposed and withdrawn items never
|
|
180
216
|
* count; when nothing is kept yet but an agent proposed items, it says so instead of a bare "no breakdown".
|
|
@@ -309,7 +345,8 @@ function layout({ root, initiative, fold, ladder, now }) {
|
|
|
309
345
|
const counted = (list) => list.filter((it) => !it.proposed);
|
|
310
346
|
|
|
311
347
|
let firstOpenSeen = false;
|
|
312
|
-
const
|
|
348
|
+
const frontier = steps ? frontierOf(stages, steps) : -1;
|
|
349
|
+
const stageRows = stages.map((s, i) => {
|
|
313
350
|
const inStage = visibleItems.filter((it) => it.stage === s.id).sort(treeOrder);
|
|
314
351
|
const real = counted(inStage);
|
|
315
352
|
const total = real.length;
|
|
@@ -324,10 +361,12 @@ function layout({ root, initiative, fold, ladder, now }) {
|
|
|
324
361
|
// `state` and `owed` are not derived, so they are left out: the client keeps them from its last full
|
|
325
362
|
// response. A ladder that was asked for but failed gives `current:false`, `state:null`, `owed:null`.
|
|
326
363
|
if (steps) {
|
|
327
|
-
|
|
364
|
+
// A stage behind the frontier with a step still open was passed over, not finished (see frontierOf).
|
|
365
|
+
const skipped = owed !== null && i < frontier;
|
|
366
|
+
const current = owed !== null && !skipped && !firstOpenSeen;
|
|
328
367
|
row.current = current;
|
|
329
|
-
row.state = current ? 'current' : (firstOpenSeen ? 'later' : 'done');
|
|
330
|
-
if (owed !== null) firstOpenSeen = true;
|
|
368
|
+
row.state = current ? 'current' : skipped ? 'skipped' : (firstOpenSeen ? 'later' : 'done');
|
|
369
|
+
if (owed !== null && !skipped) firstOpenSeen = true;
|
|
331
370
|
} else if (ladderIncluded) {
|
|
332
371
|
row.current = false;
|
|
333
372
|
row.state = null;
|
|
@@ -376,5 +415,5 @@ function layout({ root, initiative, fold, ladder, now }) {
|
|
|
376
415
|
|
|
377
416
|
module.exports = {
|
|
378
417
|
STAGE_GROUPS, STEP_NOUN, OWED_ACTOR, OPEN, DONE_WINDOW_MS, INIT_RE, NOT_MERGED_LABEL, withFactory,
|
|
379
|
-
mainTree, recordPath, profileAndLane, viewGroup, stagesFor, stageIdsFor, resolveStage, owedFor, nounFor, byLabel, progressLabel, layout, historyText,
|
|
418
|
+
mainTree, recordPath, profileAndLane, viewGroup, stagesFor, stageIdsFor, resolveStage, owedFor, frontierOf, currentStage, nounFor, byLabel, progressLabel, layout, historyText,
|
|
380
419
|
};
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* [--website URL] [--no-website URL]
|
|
16
16
|
* pmos proactive brief submit --file PATH [--merge]
|
|
17
17
|
* pmos proactive brief schedule [--repo PATH]
|
|
18
|
+
* pmos proactive calendar what the calendar memory holds, and the window the next read covers
|
|
18
19
|
* pmos proactive goals [list] | add TEXT [--initiative ID] | done N | drop N | reopen N | remove N | plan [--repo PATH]
|
|
19
20
|
* pmos proactive day [--repo PATH] the day run: its stage, its tasks and the daytime check
|
|
20
21
|
* pmos proactive day show TASK one task with its item, draft and revision notes (what day-task reads)
|
|
@@ -35,7 +36,7 @@ const path = require('path');
|
|
|
35
36
|
const { execFileSync } = require('child_process');
|
|
36
37
|
const { parseArgs } = require('./schedule-cli');
|
|
37
38
|
|
|
38
|
-
const COMMANDS = { proactive: ['today', 'scan', 'settings', 'feedback', 'brief', 'goals', 'prepare', 'learn', 'day'] };
|
|
39
|
+
const COMMANDS = { proactive: ['today', 'scan', 'settings', 'feedback', 'brief', 'goals', 'prepare', 'learn', 'day', 'calendar'] };
|
|
39
40
|
const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'slack', 'jira', 'confluence', 'github', 'drive', 'figma', 'website'];
|
|
40
41
|
const onOff = (v, what) => (v === 'on' || v === 'true' ? true : v === 'off' || v === 'false' ? false : { err: `${what} takes on or off` });
|
|
41
42
|
|
|
@@ -355,6 +356,18 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
355
356
|
if (t.cards_error) say('! Today could not be read: ' + t.cards_error);
|
|
356
357
|
return 0;
|
|
357
358
|
}
|
|
359
|
+
if (verb === 'calendar') {
|
|
360
|
+
// Read-only: what the Agenda remembers, and which window the morning brief reads next (G23).
|
|
361
|
+
const r = await call('GET', '/api/proactive/agenda');
|
|
362
|
+
if (!r || r.ok === false) return fail((r && r.error) || 'the server did not answer');
|
|
363
|
+
const day = (n) => { const d = new Date(Date.now() + n * 86400000); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; };
|
|
364
|
+
const m = r.memory;
|
|
365
|
+
const stale = !m || !m.updated_at || Date.now() - Date.parse(m.updated_at) > 7 * 86400000 || !m.from || m.from > day(-80);
|
|
366
|
+
if (m && m.events) say(`calendar memory: ${m.events} events from ${m.from} to ${m.to}, updated ${String(m.updated_at || '').slice(0, 16).replace('T', ' ')}`);
|
|
367
|
+
else say('calendar memory: empty');
|
|
368
|
+
say(stale ? `read next: the wide window, from ${day(-90)} to ${day(31)}` : `read next: from ${day(0)} to ${day(31)} (the earlier days are remembered)`);
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
358
371
|
if (verb === 'brief') {
|
|
359
372
|
const sub = pos[0] || 'show';
|
|
360
373
|
if (sub === 'submit') {
|
|
@@ -366,6 +379,8 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
366
379
|
if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the server did not answer'}. Nothing was stored.`);
|
|
367
380
|
if (merge) say(`merged into the brief for ${r.brief.date} — ${r.merged ? r.merged.found : 0} new, ${r.merged ? r.merged.resolved : 0} resolved; it now holds ${r.brief.priorities.length} priorities, ${r.brief.worth_asking.length} worth asking`);
|
|
368
381
|
else say(`stored the brief for ${r.brief.date} — ${r.brief.priorities.length} priorities, ${r.brief.worth_asking.length} worth asking`);
|
|
382
|
+
if (r.memory_error) say('! ' + r.memory_error);
|
|
383
|
+
if (r.remembered) say(`remembered ${r.remembered.events} calendar events from ${r.remembered.from} to ${r.remembered.to}`);
|
|
369
384
|
return 0;
|
|
370
385
|
}
|
|
371
386
|
if (sub === 'schedule') {
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* GET /api/initiatives/<id>/export?ws= → { ok, id, markdown } (lib/breakdown-export.js; nothing is written)
|
|
8
8
|
* POST /api/initiatives { ws, title, lane, anchor, intent?, id?, from?, specimen_test?, surface_check? }
|
|
9
9
|
* → { ok, id, path, written, sig } New initiative (requirePagePress)
|
|
10
|
+
* POST /api/initiatives/rescan { ws } → { ok, started, running } read the repository again now (requirePagePress);
|
|
11
|
+
* nothing is written: the cached rows go stale and one derivation starts
|
|
10
12
|
* POST /api/placements { ws, events: [placed | created | adopted | dropped] }
|
|
11
13
|
* → { ok, written, sig } fixtures/initiatives-api/post-ok.json
|
|
12
14
|
* POST /api/measurements { ws, events: [measured] } → { ok, written, sig }
|
|
@@ -42,10 +44,14 @@ const rowsLib = require('./initiative-rows.js');
|
|
|
42
44
|
const writer = require('./initiative-writer.js');
|
|
43
45
|
const breakdownExport = require('./breakdown-export.js');
|
|
44
46
|
const consoleState = require('./console-state');
|
|
47
|
+
const initiativeFiles = require('./initiative-files');
|
|
48
|
+
const jiraTickets = require('./jira-tickets.js');
|
|
45
49
|
|
|
46
50
|
const HORIZON_RANK = { now: 0, next: 1, later: 2 };
|
|
47
51
|
const FILE_KEYS = [['brief_path', 'brief', 'Brief'], ['prd_path', 'prd', 'PRD'], ['rubric_path', 'rubric', 'Rubric'], ['doc_path', 'doc', 'Document']];
|
|
48
52
|
const cap = (s) => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
|
53
|
+
const GIT_GROUP_LABEL = { code: 'Code', tests: 'Test', documents: 'Document', other: 'File' };
|
|
54
|
+
const GIT_FILES_MAX = 80;
|
|
49
55
|
|
|
50
56
|
/** The signature of everything a read answers from, taken before the read. */
|
|
51
57
|
function sigOf(root) {
|
|
@@ -96,9 +102,14 @@ function frameOf(w, rows, { now, sessions = [] } = {}) {
|
|
|
96
102
|
return { root, rules, placed, fold, meas, join, anchors, accepted, byId, now, sessions };
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
/** The stage of a workflow row as the R4 stage group it falls in: { id, label }.
|
|
105
|
+
/** The stage of a workflow row as the R4 stage group it falls in: { id, label }. The stage the work is IN is read from the
|
|
106
|
+
* ladder steps by plan-tree.js currentStage, the rule the sheet's stepper draws (scan-consistency R2), so a list row and
|
|
107
|
+
* its sheet never name two different stages. Accepted work, and a row without steps, keep the engine's stage. */
|
|
100
108
|
function stageOf(root, rec) {
|
|
101
109
|
const raw = rec.stage || null;
|
|
110
|
+
if (raw !== 'accepted' && Array.isArray(rec.steps)) {
|
|
111
|
+
try { const cur = planTree.currentStage(root, rec.id, rec.steps, null); if (cur && cur.label) return cur; } catch { /* fall through */ }
|
|
112
|
+
}
|
|
102
113
|
if (!raw) return { id: null, label: 'Unknown' };
|
|
103
114
|
try {
|
|
104
115
|
const st = planTree.stagesFor(root, rec.id).find((s) => s.id === raw || s.steps.includes(raw));
|
|
@@ -139,10 +150,14 @@ function rowOf(F, rec) {
|
|
|
139
150
|
const p = F.placed.byInit[id];
|
|
140
151
|
const a = F.anchors.resolve(rec.anchor);
|
|
141
152
|
const ctx = displayCtx(F, rec, id);
|
|
142
|
-
|
|
153
|
+
// Without the steps: display's owed step is the FIRST open one, which names a step skipped long ago; who the sheet
|
|
154
|
+
// waits on comes from the layout's current stage (sheetOf), so the row's word and the stepper agree.
|
|
155
|
+
const d = display.displayOf({ ...rec, steps: undefined, last_activity: rec.last_activity }, ctx);
|
|
143
156
|
const measurement = measurements.latest(F.meas, a.token || '') || (rec.anchor ? measurements.latest(F.meas, String(rec.anchor).trim()) : null);
|
|
144
157
|
const h = health.healthOf({ id, row: rec, fold: ctx.fold, placement: ctx.placement, measurement, factory: factoryJoin.factoryOf(F.join, id), rules: F.rules, now: F.now });
|
|
145
|
-
|
|
158
|
+
let pr = planItems.progressOf(F.fold, id);
|
|
159
|
+
// No planned item: the breakdown the repository's own spec lists, labelled with its source (derivedBreakdown).
|
|
160
|
+
if (!pr.total && !pr.proposed) { const d = derivedBreakdown(F.root, rec, { progress: pr }); if (d) pr = d.progress; }
|
|
146
161
|
return {
|
|
147
162
|
id, title: rec.title || id, lane: rec.lane, anchor: a.anchor, anchor_kind: a.kind,
|
|
148
163
|
horizon: p.horizon, rank: p.rank, confirmed: p.confirmed, dormant: p.dormant, check_by: p.check_by,
|
|
@@ -240,13 +255,13 @@ function listOf(w, F) {
|
|
|
240
255
|
};
|
|
241
256
|
}
|
|
242
257
|
|
|
243
|
-
function sheetOf(w, F, rec) {
|
|
258
|
+
function sheetOf(w, F, rec, opts = {}) {
|
|
244
259
|
const root = F.root;
|
|
245
260
|
const row = rowOf(F, rec);
|
|
246
261
|
let lad = null;
|
|
247
262
|
try { lad = consoleState.ladder({ root, id: rec.id }); } catch (e) { lad = { ok: false, error: String((e && e.message) || e) }; }
|
|
248
263
|
const lay = planTree.layout({ root, initiative: rec.id, fold: F.fold, ladder: lad, now: F.now });
|
|
249
|
-
const stepper = lay.stages.map((s) => ({ id: s.id, label: s.label, state: s.state === 'current' ? 'current' : s.state === 'done' ? 'done' : 'todo' }));
|
|
264
|
+
const stepper = lay.stages.map((s) => ({ id: s.id, label: s.label, state: s.state === 'current' ? 'current' : s.state === 'done' ? 'done' : s.state === 'skipped' ? 'skipped' : 'todo', owed: s.state === 'skipped' && s.owed ? s.owed.noun : null }));
|
|
250
265
|
let waiting = null;
|
|
251
266
|
if (row.display.waitingOn) {
|
|
252
267
|
waiting = { on: row.display.waitingOn, act: row.display.act, action: row.display.waitingOn === 'pm' ? { label: 'Open gate', kind: 'gate', target: null } : { label: 'Hand to agent', kind: 'handoff', target: rec.id } };
|
|
@@ -263,12 +278,144 @@ function sheetOf(w, F, rec) {
|
|
|
263
278
|
const file = planTree.recordPath(root, rec.id);
|
|
264
279
|
if (loaded.ok && file) fm = loaded.engine.parseLeadingFrontmatter(fs.readFileSync(file, 'utf8')) || {};
|
|
265
280
|
} catch { fm = {}; }
|
|
266
|
-
const files =
|
|
281
|
+
const files = filesOf(root, rec, fm, opts);
|
|
282
|
+
const derived = derivedBreakdown(root, rec, lay);
|
|
267
283
|
const verb = { now: 'Moved to Now', next: 'Moved to Next', later: 'Moved to Later' };
|
|
268
284
|
const history = F.placed.moves.filter((m) => m.init === rec.id)
|
|
269
285
|
.map((m) => ({ at: m.at, by: actorLib.pageName(m.by), text: m.from === null && m.to === 'later' ? 'Added to Later' : verb[m.to] }))
|
|
270
286
|
.reverse();
|
|
271
|
-
return { initiative: row, stepper, waiting, files, history, cli: { now: `pmos initiative now ${rec.id}`, export: `pmos initiative export ${rec.id}` } };
|
|
287
|
+
return { initiative: row, stepper, waiting, files, derived, history, cli: { now: `pmos initiative now ${rec.id}`, export: `pmos initiative export ${rec.id}` } };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Every file that belongs to one initiative (scan-consistency R3), in three sources, each row saying which:
|
|
292
|
+
* record the record itself and every `*_path` its frontmatter names (prd, rubric, brief, explainer, design, …)
|
|
293
|
+
* knowledge-graph the documents the library's initiative index joins to it (server.js initiativeIndex: a declared
|
|
294
|
+
* initiative_id, the record's own paths, the file-name convention, the declared-id prefix)
|
|
295
|
+
* git the files the commits for it changed (lib/initiative-files.js: its branch, and trunk commits naming it)
|
|
296
|
+
* A path is listed once, under its first source. A git read that fails is a `{ kind: 'note' }` row, never an empty list.
|
|
297
|
+
*/
|
|
298
|
+
function filesOf(root, rec, fm, opts) {
|
|
299
|
+
const out = [];
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
const exists = (rel) => { try { return fs.statSync(path.join(root, rel)).isFile(); } catch { return false; } };
|
|
302
|
+
// A path the record names is listed even when it is absent, marked `missing` (a dangling pointer is a fact to see);
|
|
303
|
+
// the other two sources list only files that exist.
|
|
304
|
+
const push = (row) => {
|
|
305
|
+
const p = String(row.path || '').trim().replace(/^\.?\//, '');
|
|
306
|
+
if (!p || seen.has(p) || p.split('/').includes('..')) return;
|
|
307
|
+
const there = exists(p);
|
|
308
|
+
if (!there && row.source !== 'record') return;
|
|
309
|
+
seen.add(p);
|
|
310
|
+
out.push(there ? { ...row, path: p } : { ...row, path: p, missing: true });
|
|
311
|
+
};
|
|
312
|
+
for (const [k, kind, label] of FILE_KEYS) if (typeof fm[k] === 'string' && fm[k].trim()) push({ kind, label, path: String(fm[k]).trim(), source: 'record' });
|
|
313
|
+
for (const [k, v] of Object.entries(fm || {})) {
|
|
314
|
+
if (!/_path$/.test(k) || typeof v !== 'string' || !v.trim() || FILE_KEYS.some(([fk]) => fk === k)) continue;
|
|
315
|
+
const kind = k.replace(/_path$/, '');
|
|
316
|
+
push({ kind, label: cap(kind.replace(/_/g, ' ')), path: v.trim(), source: 'record' });
|
|
317
|
+
}
|
|
318
|
+
if (rec.record) push({ kind: 'record', label: 'Record', path: String(rec.record), source: 'record' });
|
|
319
|
+
let groups = [];
|
|
320
|
+
try { groups = opts && opts.initiativeIndex ? (opts.initiativeIndex(root).initiatives || []) : []; } catch { groups = []; }
|
|
321
|
+
const g = groups.find((x) => x && x.id === rec.id);
|
|
322
|
+
for (const a of (g && g.artifacts) || []) {
|
|
323
|
+
const role = String(a.role || 'doc');
|
|
324
|
+
push({ kind: role.replace(/\s+/g, '-').toLowerCase(), label: role === 'doc' ? 'Document' : cap(role), path: String(a.path || '').split(path.sep).join('/'), source: 'knowledge-graph', shared: !!a.shared });
|
|
325
|
+
}
|
|
326
|
+
let git = null;
|
|
327
|
+
try { git = initiativeFiles.filesFor(root, rec.id, { exclude: seen }); } catch (e) { git = { ok: false, reason: String((e && e.message) || e).slice(0, 120) }; }
|
|
328
|
+
if (git && git.ok) {
|
|
329
|
+
const items = git.items.filter((x) => x.where === 'head');
|
|
330
|
+
for (const x of items.slice(0, GIT_FILES_MAX)) push({ kind: x.group, label: GIT_GROUP_LABEL[x.group] || 'File', path: x.path, source: 'git', commits: x.commits });
|
|
331
|
+
if (items.length > GIT_FILES_MAX) out.push({ kind: 'note', label: `${items.length - GIT_FILES_MAX} more files changed for it are not listed`, path: '', source: 'git' });
|
|
332
|
+
} else if (git) {
|
|
333
|
+
out.push({ kind: 'note', label: 'The files git changed for it could not be read', path: '', source: 'git', reason: git.reason || null });
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The work breakdown read from the repository when nobody has planned one (scan-consistency R4).
|
|
340
|
+
*
|
|
341
|
+
* The Breakdown is the plan-items ledger, and on this repository not one initiative has a ledger line, so every
|
|
342
|
+
* Breakdown read empty although the PRDs list the work. When the ledger holds no item for the initiative, this reads the
|
|
343
|
+
* items of the spec the record names (lib/jira-tickets.js items: the user stories and story files) and, when it has none,
|
|
344
|
+
* the top-level entries of its Requirements section. Each item's state is the initiative's, from the ladder: `done` once
|
|
345
|
+
* the PM accepted it, `review` once its build is evidenced, `todo` before that. That is a stated inference about the
|
|
346
|
+
* whole initiative, not a claim about each item, so every answer carries its basis. Nothing is written: Plan and Keep
|
|
347
|
+
* stay the way a breakdown enters the ledger.
|
|
348
|
+
*/
|
|
349
|
+
const REQ_SECTION = /^(?:functional\s+)?requirements\b|^deliverables\b|^work\s+items?\b/i;
|
|
350
|
+
// an item's name: up to its first sentence end, dash or colon, so a requirement written as a paragraph reads as a title
|
|
351
|
+
const firstSentence = (t) => String(t || '').split(/(?<=[a-z0-9)`'"])[.:]\s|\s[—–]\s/i)[0].replace(/[.:;]$/, '').trim();
|
|
352
|
+
const NOT_AN_ITEM = /^(?:in|out|in scope|out of scope|non[- ]goals?|notes?|open questions?|risks?|constraints?)$/i;
|
|
353
|
+
function requirementItems(text) {
|
|
354
|
+
const lines = String(text || '').replace(/^---\n[\s\S]*?\n---\n?/, '').split('\n');
|
|
355
|
+
const out = [];
|
|
356
|
+
for (let i = 0; i < lines.length; i++) {
|
|
357
|
+
const m = /^(#{2,4})\s+(?:\d+[.)]?\s+)?(.+?)\s*$/.exec(lines[i]);
|
|
358
|
+
if (!m || !REQ_SECTION.test(m[2].replace(/[*_`]/g, '').trim())) continue;
|
|
359
|
+
const level = m[1].length;
|
|
360
|
+
const body = [];
|
|
361
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
362
|
+
const h = /^(#{1,6})\s/.exec(lines[j]);
|
|
363
|
+
if (h && h[1].length <= level) break;
|
|
364
|
+
body.push(lines[j]);
|
|
365
|
+
}
|
|
366
|
+
// The section's own top-level entries, before its first subheading; else its subheadings, minus the ones that
|
|
367
|
+
// name a part of the section rather than a piece of work (Non-goals, In, Out).
|
|
368
|
+
const sub = '#'.repeat(level + 1) + ' ';
|
|
369
|
+
const firstSub = body.findIndex((l) => /^#{2,6}\s/.test(l));
|
|
370
|
+
for (const l of firstSub < 0 ? body : body.slice(0, firstSub)) {
|
|
371
|
+
const b = /^(?:[-*]|\d+[.)])\s+(.+)$/.exec(l) || /^(\*\*[A-Z]{1,3}-?\d{1,3}[a-z]?\b.+)$/.exec(l); // a list entry, or a paragraph led by **R-1 …**
|
|
372
|
+
if (b) out.push(b[1]);
|
|
373
|
+
}
|
|
374
|
+
if (!out.length) for (const l of body) if (l.startsWith(sub) && !NOT_AN_ITEM.test(l.slice(sub.length).replace(/[*_`]/g, '').trim())) out.push(l.slice(sub.length));
|
|
375
|
+
if (out.length) break;
|
|
376
|
+
}
|
|
377
|
+
return out.map((t) => {
|
|
378
|
+
const clean = t.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/[*`_]/g, '').trim();
|
|
379
|
+
const idm = /^([A-Z]{1,3}-?\d{1,3}[a-z]?)\b\s*[·:.\-—–)]?\s*(.*)$/.exec(clean);
|
|
380
|
+
const title = firstSentence((idm ? idm[2] : clean).replace(/^[—–-]\s*/, ''));
|
|
381
|
+
return { ref: idm ? idm[1] : null, title: (title || clean).slice(0, 140) };
|
|
382
|
+
}).filter((x) => x.title);
|
|
383
|
+
}
|
|
384
|
+
function derivedBreakdown(root, rec, lay) {
|
|
385
|
+
const p = lay && lay.progress ? lay.progress : null;
|
|
386
|
+
if (p && (p.total || p.proposed)) return null;
|
|
387
|
+
let rfm = {};
|
|
388
|
+
try {
|
|
389
|
+
const loaded = consoleState.loadEngine();
|
|
390
|
+
const file = planTree.recordPath(root, rec.id);
|
|
391
|
+
if (loaded.ok && file) rfm = loaded.engine.parseLeadingFrontmatter(fs.readFileSync(file, 'utf8')) || {};
|
|
392
|
+
} catch { rfm = {}; }
|
|
393
|
+
let items = [];
|
|
394
|
+
let source = null;
|
|
395
|
+
try {
|
|
396
|
+
const st = jiraTickets.items(root, rec.id);
|
|
397
|
+
if (st && st.ok && st.items.length) { items = st.items.map((x) => ({ ref: x.id, title: firstSentence(x.title) })); source = st.sources.join(', '); }
|
|
398
|
+
} catch { items = []; }
|
|
399
|
+
if (!items.length) {
|
|
400
|
+
for (const k of ['prd_path', 'doc_path', 'brief_path']) {
|
|
401
|
+
const rel = typeof rfm[k] === 'string' ? rfm[k].trim() : '';
|
|
402
|
+
if (!rel) continue;
|
|
403
|
+
let text = null;
|
|
404
|
+
try { text = fs.readFileSync(path.join(root, rel), 'utf8'); } catch { text = null; }
|
|
405
|
+
if (text == null) continue;
|
|
406
|
+
items = requirementItems(text);
|
|
407
|
+
if (items.length) { source = rel; break; }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!items.length) return null;
|
|
411
|
+
const steps = Array.isArray(rec.steps) ? rec.steps : [];
|
|
412
|
+
const done = (k) => steps.some((x) => x.key === k && x.status === 'done');
|
|
413
|
+
const state = rec.accepted || done('accept') || done('approved') ? 'done' : done('build') || done('draft') ? 'review' : 'todo';
|
|
414
|
+
const basis = state === 'done' ? 'the PM accepted this initiative'
|
|
415
|
+
: state === 'review' ? 'its build is on the trunk and acceptance is open' : 'no build is evidenced on the trunk yet';
|
|
416
|
+
const list = items.slice(0, 60).map((x, i) => ({ id: x.ref || `item-${i + 1}`, ref: x.ref || null, title: x.title, state }));
|
|
417
|
+
const n = list.length;
|
|
418
|
+
return { source, basis, state, items: list, progress: { done: state === 'done' ? n : 0, total: n, proposed: 0, label: `${state === 'done' ? n : 0} of ${n} · from ${source.split(',')[0].split('/').pop()}` } };
|
|
272
419
|
}
|
|
273
420
|
|
|
274
421
|
// ── HTTP ─────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -356,7 +503,7 @@ async function handle(req, res, url, ctx) {
|
|
|
356
503
|
if (id) {
|
|
357
504
|
const rec = F.byId.get(id);
|
|
358
505
|
if (!rec || !F.placed.byInit[id]) { ctx.json(res, { ok: false, error: `no work-lane initiative ${id} in this repository` }, 404); return true; }
|
|
359
|
-
ctx.json(res, { ...head, sig, now: new Date(now).toISOString(), ...sheetOf(w, F, rec) });
|
|
506
|
+
ctx.json(res, { ...head, sig, now: new Date(now).toISOString(), ...sheetOf(w, F, rec, { initiativeIndex: ctx.initiativeIndex }) });
|
|
360
507
|
return true;
|
|
361
508
|
}
|
|
362
509
|
const body = view === 'list' ? listOf(w, F) : roadmapOf(w, F);
|
|
@@ -375,6 +522,15 @@ async function handle(req, res, url, ctx) {
|
|
|
375
522
|
const w = ctx.getWs(wsParam);
|
|
376
523
|
if (!w || (wsParam && w.id !== wsParam)) { ctx.json(res, { ok: false, status: 404, error: 'no such workspace' }, 404); return true; }
|
|
377
524
|
|
|
525
|
+
if (p === '/api/initiatives/rescan') {
|
|
526
|
+
const g = guard.requirePagePress(req, body);
|
|
527
|
+
if (!g.ok) return answer(ctx, res, g), true;
|
|
528
|
+
initiativeFiles.forget(w.root);
|
|
529
|
+
const r = rowsLib.rescan(w.root);
|
|
530
|
+
if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg(w.id);
|
|
531
|
+
ctx.json(res, { ok: true, ws: w.id, started: r.started, running: r.running });
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
378
534
|
if (p === '/api/initiatives') {
|
|
379
535
|
const g = guard.requirePagePress(req, body);
|
|
380
536
|
if (!g.ok) return answer(ctx, res, g), true;
|
|
@@ -401,4 +557,4 @@ async function handle(req, res, url, ctx) {
|
|
|
401
557
|
return true;
|
|
402
558
|
}
|
|
403
559
|
|
|
404
|
-
module.exports = { prefixes: ['/api/initiatives', '/api/placements', '/api/measurements'], handle, sigOf, rowOf, frameOf, roadmapOf, listOf, sheetOf, unrecordedOf };
|
|
560
|
+
module.exports = { prefixes: ['/api/initiatives', '/api/placements', '/api/measurements'], handle, sigOf, rowOf, frameOf, roadmapOf, listOf, sheetOf, unrecordedOf, filesOf, requirementItems, derivedBreakdown };
|
package/workspace/package.json
CHANGED
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
<button type="button" data-seq="right" aria-label="Right arrow"><svg class="pmc-icon" width="16" height="16" aria-hidden="true" focusable="false" data-icon="arrow-right"><use href="#lucide-arrow-right"></use></svg></button>
|
|
130
130
|
</div>
|
|
131
131
|
<nav class="mobnav" id="mobNav" aria-label="Destinations">
|
|
132
|
-
<button type="button" class="mobnav-b" data-dest="today" aria-current="page"><span class="l">
|
|
132
|
+
<button type="button" class="mobnav-b" data-dest="today" aria-current="page"><span class="l">Agenda</span><span class="mobnav-n" id="mobTodayCount" hidden></span></button>
|
|
133
133
|
<button type="button" class="mobnav-b" data-dest="initiatives" aria-current="false"><span class="l">Initiatives</span></button>
|
|
134
134
|
<button type="button" class="mobnav-b" data-dest="board" aria-current="false"><span class="l">Board</span></button>
|
|
135
135
|
<button type="button" class="mobnav-b" data-dest="reports" aria-current="false"><span class="l">Reports</span></button>
|
|
@@ -1761,6 +1761,29 @@
|
|
|
1761
1761
|
.in-stepper__step[data-state="done"] {
|
|
1762
1762
|
color: var(--foreground);
|
|
1763
1763
|
}
|
|
1764
|
+
.in-stepper__step[data-state="skipped"] {
|
|
1765
|
+
color: var(--warning);
|
|
1766
|
+
}
|
|
1767
|
+
.in-files,
|
|
1768
|
+
.in-derived {
|
|
1769
|
+
display: flex;
|
|
1770
|
+
flex-direction: column;
|
|
1771
|
+
gap: 8px;
|
|
1772
|
+
}
|
|
1773
|
+
.in-files {
|
|
1774
|
+
gap: 16px;
|
|
1775
|
+
}
|
|
1776
|
+
.in-derived {
|
|
1777
|
+
margin-bottom: 16px;
|
|
1778
|
+
}
|
|
1779
|
+
.in-files__title,
|
|
1780
|
+
.in-derived__title {
|
|
1781
|
+
margin: 0;
|
|
1782
|
+
font-size: 12px;
|
|
1783
|
+
line-height: 16px;
|
|
1784
|
+
font-weight: 600;
|
|
1785
|
+
color: var(--muted-foreground);
|
|
1786
|
+
}
|
|
1764
1787
|
.in-waiting {
|
|
1765
1788
|
display: flex;
|
|
1766
1789
|
align-items: center;
|
|
@@ -2651,6 +2674,53 @@
|
|
|
2651
2674
|
.td-day__sites { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
|
|
2652
2675
|
.td-day__sites li { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
|
|
2653
2676
|
.td-day__sites li span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2677
|
+
/* the Agenda month (today-agenda.js, G25 to G27): weeks of 8 columns, the week number and seven day cells */
|
|
2678
|
+
.td-ag__mhead { display: flex; align-items: center; gap: 6px; }
|
|
2679
|
+
.td-ag__mlabel { min-width: 120px; font-size: 14px; text-align: center; color: var(--foreground); }
|
|
2680
|
+
.td-ag__lens { flex-wrap: wrap; max-width: 100%; }
|
|
2681
|
+
.td-ag__grid { display: grid; gap: 2px; }
|
|
2682
|
+
.td-ag__wk { display: grid; grid-template-columns: 36px repeat(7, minmax(0, 1fr)); gap: 2px; }
|
|
2683
|
+
.td-ag__wk--head { font: 11px var(--mono); color: var(--muted-foreground); text-transform: uppercase; }
|
|
2684
|
+
.td-ag__wd { padding: 0 4px; }
|
|
2685
|
+
.td-day .td-ag__wno { height: auto; min-height: 64px; padding: 0; font: 11px var(--mono); letter-spacing: 0; color: var(--muted-foreground); }
|
|
2686
|
+
.td-day .td-ag__wno[aria-pressed="true"] { background: var(--accent); color: var(--foreground); border-color: var(--ring); }
|
|
2687
|
+
.td-day .td-ag__cell {
|
|
2688
|
+
display: grid; align-items: start; justify-content: stretch; height: auto; min-height: 64px; padding: 4px;
|
|
2689
|
+
border: 1px solid var(--border); background: var(--background); text-align: left; text-transform: none; letter-spacing: 0;
|
|
2690
|
+
font-weight: 400; white-space: normal;
|
|
2691
|
+
}
|
|
2692
|
+
.td-day .td-ag__cell[data-out="true"] { opacity: 0.45; }
|
|
2693
|
+
.td-day .td-ag__cell[data-weekend="true"] { background: var(--muted); }
|
|
2694
|
+
.td-day .td-ag__cell[data-today="true"] { border: 2px solid var(--foreground); }
|
|
2695
|
+
.td-day .td-ag__cell[aria-pressed="true"] { background: var(--accent); border-color: var(--ring); }
|
|
2696
|
+
.td-day .td-ag__cell[data-lit="true"] { box-shadow: inset 0 0 0 2px var(--warning); }
|
|
2697
|
+
.td-ag__in { display: grid; gap: 3px; min-width: 0; }
|
|
2698
|
+
.td-ag__num { font: 12px var(--mono); color: var(--foreground); font-variant-numeric: tabular-nums; }
|
|
2699
|
+
.td-ag__cell[data-past="true"] .td-ag__num { color: var(--muted-foreground); }
|
|
2700
|
+
.td-ag__strip { position: relative; height: 6px; background: var(--muted); }
|
|
2701
|
+
.td-ag__blk { position: absolute; top: 0; bottom: 0; left: var(--td-ag-l, 0); width: var(--td-ag-w, 0); background: var(--muted-foreground); }
|
|
2702
|
+
.td-ag__blk--imp { background: var(--foreground); }
|
|
2703
|
+
.td-ag__blk--free { background: var(--ring); }
|
|
2704
|
+
.td-ag__key, .td-ag__meta, .td-ag__nonotes { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; line-height: 14px; }
|
|
2705
|
+
.td-ag__key { font-weight: 600; color: var(--foreground); }
|
|
2706
|
+
.td-ag__meta { color: var(--muted-foreground); }
|
|
2707
|
+
.td-ag__nonotes { color: var(--warning); }
|
|
2708
|
+
.td-ag__ready { justify-self: start; padding: 0 4px; font: 11px/16px var(--mono); border: 1px solid var(--border); color: var(--foreground); }
|
|
2709
|
+
.td-ag__ready[data-ag-ready="warn"] { border-color: var(--warning); color: var(--warning); }
|
|
2710
|
+
.td-ag__ready[data-ag-ready="risk"] { border-color: var(--destructive); background: var(--destructive); color: var(--background); }
|
|
2711
|
+
.td-ag__agenda { display: grid; gap: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
|
|
2712
|
+
.td-ag__ah { margin: 0; font-size: 14px; color: var(--foreground); }
|
|
2713
|
+
.td-ag__tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
|
|
2714
|
+
.td-ag__tile { display: grid; gap: 2px; min-width: 0; padding: 6px 8px; background: var(--muted); }
|
|
2715
|
+
.td-ag__tile b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; color: var(--foreground); }
|
|
2716
|
+
.td-ag__tile[data-ag-ready="risk"] b { color: var(--destructive); }
|
|
2717
|
+
.td-ag__tile[data-ag-ready="warn"] b { color: var(--warning); }
|
|
2718
|
+
.td-ag__tk { font: 11px var(--mono); text-transform: uppercase; color: var(--muted-foreground); }
|
|
2719
|
+
.td-ag__wday { display: grid; gap: 2px; }
|
|
2720
|
+
.td-ag__wday h4 { display: flex; align-items: center; gap: 6px; margin: 4px 0 0; font-size: 12px; color: var(--foreground); }
|
|
2721
|
+
.td-ag__last summary { cursor: pointer; font-size: 12px; color: var(--muted-foreground); }
|
|
2722
|
+
.td-ag__last summary:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
2723
|
+
@media (pointer: coarse) { .td-ag__last summary { min-height: 44px; } }
|
|
2654
2724
|
/* the goal detail keeps the steps the goals section showed (G6): Today cards and the agent steps that can be prepared */
|
|
2655
2725
|
.td-goal__steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
2656
2726
|
.td-goal__step { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
|
|
@@ -18,6 +18,7 @@ import { wsOf } from './data.js';
|
|
|
18
18
|
import { setInitiativeOpener, registerCommand, initiativesViews, initiativesViewCtx, openDestination } from './registry.js';
|
|
19
19
|
import { COPY } from './initiatives-copy.js';
|
|
20
20
|
import { getRoadmap, grantScopes } from './initiatives-api.js';
|
|
21
|
+
import { rescanRepo } from './initiatives-ui.js';
|
|
21
22
|
import { mountRoadmap } from './roadmap.js';
|
|
22
23
|
import { mountList } from './initiative-list.js';
|
|
23
24
|
import { openSheet } from './initiative-sheet.js';
|
|
@@ -32,6 +33,10 @@ registerCommand({
|
|
|
32
33
|
id: 'initiatives-new', label: COPY.newInitiative, hint: 'Add one to Later', cli: 'pmos initiative new "<title>" --anchor <kr>',
|
|
33
34
|
run: () => { openDestination('initiatives'); try { window.dispatchEvent(new CustomEvent('pmos:initiatives-new')); } catch { /* no window */ } },
|
|
34
35
|
});
|
|
36
|
+
registerCommand({
|
|
37
|
+
id: 'initiatives-rescan', label: COPY.rescan, hint: COPY.rescanHint,
|
|
38
|
+
run: () => { openDestination('initiatives'); try { window.dispatchEvent(new CustomEvent('pmos:initiatives-rescan')); } catch { /* no window */ } },
|
|
39
|
+
});
|
|
35
40
|
registerCommand({ id: 'initiatives-list', label: 'Initiatives as a list', hint: 'By objective', cli: 'pmos initiative list', run: () => { try { window.localStorage.setItem('pmos.initiatives.view', 'list'); } catch { /* no storage */ } openDestination('initiatives'); try { window.dispatchEvent(new CustomEvent('pmos:initiatives-view', { detail: 'list' })); } catch { /* no window */ } } });
|
|
36
41
|
|
|
37
42
|
const BUILT_IN = ['roadmap', 'list'];
|
|
@@ -57,10 +62,11 @@ export function mountDestination(host, ctx) {
|
|
|
57
62
|
host.replaceChildren(r.root);
|
|
58
63
|
const reloadView = () => { if (r.handle && r.handle.reload) r.handle.reload(); refreshRows(r, ws); };
|
|
59
64
|
r.onChanged = reloadView;
|
|
60
|
-
// The
|
|
65
|
+
// The listeners are kept on r so disposeDestination can take them off: a closed tab must not keep its host alive.
|
|
61
66
|
r.listeners = [
|
|
62
67
|
['pmos:initiatives-changed', () => { if (roots.get(host) === r) r.onChanged(); }],
|
|
63
68
|
['pmos:initiatives-new', () => { if (roots.get(host) === r) r.openNew(); }],
|
|
69
|
+
['pmos:initiatives-rescan', () => { if (roots.get(host) === r && ws) rescanRepo(ws); }],
|
|
64
70
|
['pmos:initiatives-view', (e) => { if (roots.get(host) === r && e.detail) { r.view = e.detail; r.draw(r.ctx); } }],
|
|
65
71
|
];
|
|
66
72
|
for (const [name, fn] of r.listeners) window.addEventListener(name, fn);
|
|
@@ -22,7 +22,7 @@ export const COPY = {
|
|
|
22
22
|
},
|
|
23
23
|
nav: {
|
|
24
24
|
label: 'Destinations',
|
|
25
|
-
today: '
|
|
25
|
+
today: 'Agenda',
|
|
26
26
|
initiatives: 'Initiatives',
|
|
27
27
|
board: 'Board',
|
|
28
28
|
reports: 'Reports',
|
|
@@ -115,7 +115,7 @@ export const COPY = {
|
|
|
115
115
|
},
|
|
116
116
|
page: {
|
|
117
117
|
title: 'Settings',
|
|
118
|
-
intro: 'Settings for this workspace. Your work is in
|
|
118
|
+
intro: 'Settings for this workspace. Your work is in Agenda, Initiatives, Board and Reports.',
|
|
119
119
|
repos: 'Repositories',
|
|
120
120
|
reposEmpty: 'No repository is registered. Add one to start.',
|
|
121
121
|
add: 'Add repository',
|