create-pmos 0.71.1 → 0.72.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/first-paint-audit.js +4 -2
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/my-work.md +64 -0
- package/workspace/lib/day-run.js +22 -2
- package/workspace/lib/my-work-cli.js +98 -0
- package/workspace/lib/my-work.js +242 -0
- package/workspace/lib/people-store.js +24 -2
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/proactive-cli.js +10 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-my-work.js +122 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +2 -2
- package/workspace/public/pm/components.css +117 -0
- package/workspace/public/pm/dest-mywork.js +242 -0
- package/workspace/public/pm/frame-copy.js +3 -0
- package/workspace/public/pm/my-work-card.js +51 -0
- package/workspace/public/pm/my-work-data.js +36 -0
- package/workspace/public/pm/registry.js +3 -0
- package/workspace/public/pm/topbar.js +16 -2
- package/workspace/server.js +39 -2
- package/workspace/skills/day-task.skill +1 -1
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/skills/my-work-read.skill +43 -0
- package/workspace/test-lists/my-work.json +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.1",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T05:22:25Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T05:22:25Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T05:22:25Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
// The budgets start at specimen A's counts. reports: 14 → 15 in Run 6 for Present, the one control the PM asked to keep
|
|
21
21
|
// with the slides (2026-10-06). Every place +1 for Docs in the top bar (the PM, 2026-10-06, docs-three-way-sync): the
|
|
22
22
|
// budget is for the whole screen and the bar is on every screen. The calibration stays specimen A's own count: the
|
|
23
|
-
// specimen did not change. Every place +1 again for People in the top bar (the PM, 2026-10-06, people-directory)
|
|
23
|
+
// specimen did not change. Every place +1 again for People in the top bar (the PM, 2026-10-06, people-directory), and
|
|
24
|
+
// +1 again for My work beside Agenda (the PM approved that mockup and said build it, 2026-10-07, my-work).
|
|
24
25
|
const SPECIMEN_A = Object.freeze({ today: 17, initiatives: 24, board: 33, reports: 14 });
|
|
25
26
|
const BAR_DOCS = 1;
|
|
26
27
|
const BAR_PEOPLE = 1;
|
|
27
|
-
const
|
|
28
|
+
const BAR_MY_WORK = 1;
|
|
29
|
+
const BAR = BAR_DOCS + BAR_PEOPLE + BAR_MY_WORK;
|
|
28
30
|
// Agenda opens on the month (the PM's Agenda mockup v5, 2026-10-06): the month adds its own controls to Today's budget,
|
|
29
31
|
// at most six week rows of a week button and seven days, the two month arrows and the five lenses.
|
|
30
32
|
const AGENDA_MONTH = 6 * 8 + 2 + 5;
|
package/workspace/guide/index.md
CHANGED
|
@@ -22,6 +22,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
22
22
|
| You want | Go to |
|
|
23
23
|
|---|---|
|
|
24
24
|
| What is waiting for you now | Agenda |
|
|
25
|
+
| Every Jira ticket and WBS item assigned to you, ranked, to brief an agent | My work, or press `g w` |
|
|
25
26
|
| The initiatives of a repository, or of every repository | Initiatives. Choose **All repositories** in Repository ▾ for every repository. |
|
|
26
27
|
| The sessions of a repository, by initiative | The agent dock. ⌘K → **Branches and sessions** lists the branches. |
|
|
27
28
|
| The items of an initiative | Board, or Breakdown in the initiative detail |
|
|
@@ -38,6 +39,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
38
39
|
|
|
39
40
|
- [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
|
|
40
41
|
- [Jira tickets](jira.md): Jira tickets drafted from the PRD, pushed when you select them, and their status.
|
|
42
|
+
- [My work](my-work.md): the work assigned to you, its order, the brief to an agent, and `pmos my-work`.
|
|
41
43
|
- [People](people.md): the people you work with in a repository, how they get there, and `pmos people`.
|
|
42
44
|
- [Docs](docs.md): whether each linked document is the same as, ahead of or behind its Google Docs and Confluence copy.
|
|
43
45
|
- [Layout](layout.md): the frame, the keys and the rule for where a list or a detail opens.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "My work: every Jira ticket and WBS item assigned to you, ranked, briefed to an agent, approved in Agenda"
|
|
4
|
+
timestamp: 2026-10-07
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# My work
|
|
8
|
+
|
|
9
|
+
Open My work from the top bar, from ⌘K (**My work**), or press `g w`.
|
|
10
|
+
|
|
11
|
+
My work lists every Jira ticket and every breakdown (WBS) item assigned to you, in all your registered repositories. It ranks them, shows what each one asks, and lets you give an agent an instruction for it. The agent's draft then waits for you in Agenda.
|
|
12
|
+
|
|
13
|
+
## Where the items come from
|
|
14
|
+
|
|
15
|
+
- **Jira tickets.** The workspace does not read Jira. The **my-work-read** skill reads it in an agent session, with your own Atlassian connection. It reads the open issues assigned to you, with their comments and links, and writes a context card for each: who asked, what they ask, a short summary of the thread, the questions still open, and a suggested instruction for an agent. It does not copy the comments. Press **Read again** in My work to run it.
|
|
16
|
+
- **WBS items.** The plan items whose owner is you, in every registered repository. They come from the same data as the Board's Mine filter. My work changes no plan item.
|
|
17
|
+
|
|
18
|
+
The read is kept in your own runtime folder on this machine. It never goes into a repository.
|
|
19
|
+
|
|
20
|
+
## The order
|
|
21
|
+
|
|
22
|
+
Each item gets a WSJF score: the cost of waiting divided by the size.
|
|
23
|
+
|
|
24
|
+
| Part | How it is counted |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Value | Jira priority: Highest 10, High 8, Medium 5, Low 2. No priority counts as Medium. Add 3 when the item belongs to an initiative, else 1. |
|
|
27
|
+
| Urgency | No due date 1. Overdue 10. Due in 2 days or less 8. Due in 7 days or less 5. Later 3. |
|
|
28
|
+
| Unblocks | 3 for each item it blocks, at most 9. |
|
|
29
|
+
| Size | XS 1, S 2, M 3, L 5, XL 8. No estimate counts as M. |
|
|
30
|
+
|
|
31
|
+
The score is (value + urgency + unblocks) / size. P1 is 12 or more, P2 is 7 or more, P3 is 3 or more, and P4 is the rest. Open **Details** on an item to see each part.
|
|
32
|
+
|
|
33
|
+
## Brief an agent
|
|
34
|
+
|
|
35
|
+
Being assigned a ticket does not mean an agent must resolve it. Every new item starts in **To brief**.
|
|
36
|
+
|
|
37
|
+
1. Open the item. Read **What the agent found**.
|
|
38
|
+
2. Change the instruction. It starts as the agent's suggestion.
|
|
39
|
+
3. Press **Run agent**. Or press **I'll handle it** to keep the item yourself.
|
|
40
|
+
|
|
41
|
+
Run agent adds one job to today's day run, with your instruction. The job runs like every Agenda job: at most 5 at once, one draft, and nothing is sent. A hand icon marks an item that looks like a decision only you can make. An agent can still prepare it.
|
|
42
|
+
|
|
43
|
+
**Back to brief** returns an item to To brief, unless an agent is working on it.
|
|
44
|
+
|
|
45
|
+
## Approve in Agenda
|
|
46
|
+
|
|
47
|
+
When the draft is ready, the item moves to **Ready** in My work, and the job waits in Agenda. Approve, revise or retry it there. My work has no approve button.
|
|
48
|
+
|
|
49
|
+
Agenda shows one card, **N items need your brief**, when items wait in To brief. Its button opens My work. The morning brief lists today's top items from My work and does not brief them for you.
|
|
50
|
+
|
|
51
|
+
## What My work does not do
|
|
52
|
+
|
|
53
|
+
- It does not write to Jira.
|
|
54
|
+
- It does not open a pull request. A job drafts a reply, a comment, a plan or a document.
|
|
55
|
+
|
|
56
|
+
## Commands
|
|
57
|
+
|
|
58
|
+
| Command | What it does |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `pmos my-work list [--top N] [--json]` | Every open item, in the order My work shows it. |
|
|
61
|
+
| `pmos my-work show <ITEM> [--json]` | One item: what the agent found, the score and the state. ITEM is its key, its id or its number in the list. |
|
|
62
|
+
| `pmos my-work submit --file <read.json>` | The my-work-read skill submits its read. |
|
|
63
|
+
|
|
64
|
+
None of these briefs an agent or takes an item. Do that in My work.
|
package/workspace/lib/day-run.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* newRun(date, now) · readRun(raw, date)
|
|
16
16
|
* addTasks(run, brief) agent priorities (kind approve|auto) → waiting tasks
|
|
17
|
+
* addBriefed(run, spec) one task the PM briefed in My work (initiative my-work, PRD R5)
|
|
17
18
|
* toStart(run, limit) the tasks to start now, each with its mode (do | revise | send)
|
|
18
19
|
* started · deliver · decide · sent · fail · sessionEnded · sessionStalled
|
|
19
20
|
* stage(run, brief) reading | working | review | done
|
|
@@ -111,6 +112,24 @@ function addTasks(run, brief) {
|
|
|
111
112
|
return { ok: true, run: next, added };
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* One task the PM briefed in My work: kind approve, with the PM's instruction and the item's context card on it, so the
|
|
117
|
+
* day-task session reads the instruction as the ask (PRD R5, R6). `spec` is lib/my-work.js taskSpec. A run opened only
|
|
118
|
+
* for this (no Prepare my day yet) is marked `opened_by: 'my-work'`, so it does not read as a morning read that failed.
|
|
119
|
+
*/
|
|
120
|
+
function addBriefed(run, spec) {
|
|
121
|
+
if (!run) return { ok: false, error: 'no day run today' };
|
|
122
|
+
const s = spec && typeof spec === 'object' ? spec : {};
|
|
123
|
+
if (!TASK_RE.test(String(s.id || ''))) return { ok: false, error: 'a briefed task needs an id like t-0123456789ab' };
|
|
124
|
+
if (!text(s.instruction)) return { ok: false, error: 'a briefed task needs the PM\'s instruction' };
|
|
125
|
+
if (findTask(run, s.id)) return { ok: false, error: `task ${s.id} is already in today's run` };
|
|
126
|
+
const next = clone(run);
|
|
127
|
+
next.tasks.push({ id: s.id, item_id: null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve', source: s.source || 'my-work', link: isUrl(s.link) ? s.link : null,
|
|
128
|
+
goal: null, my_work: s.my_work || null, brief_at: s.brief_at || null, instruction: text(s.instruction), context: s.context && typeof s.context === 'object' ? clone(s.context) : null,
|
|
129
|
+
status: 'waiting', session_id: null, started_at: null, attempts: 0, revisions: [], draft: null, decided: null, error: null });
|
|
130
|
+
return { ok: true, run: next, added: s.id };
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
/**
|
|
115
134
|
* The tasks to start now, oldest first, so that the sessions running (working, and sending with a session) stay at
|
|
116
135
|
* most `limit`. An approved task waiting for its send session comes first: the PM is waiting on it.
|
|
@@ -260,7 +279,8 @@ function sessionStalled(run, sessionId, why, now) {
|
|
|
260
279
|
/** Where the run is: reading (no brief for the day yet), working, review (drafts wait for the PM) or done. */
|
|
261
280
|
function stage(run, brief) {
|
|
262
281
|
if (!run) return null;
|
|
263
|
-
|
|
282
|
+
// a run My work opened (no Prepare my day pressed) has no morning read to wait for
|
|
283
|
+
if ((!brief || brief.date !== run.date) && !(run.opened_by === 'my-work' && !run.brief_session)) return 'reading';
|
|
264
284
|
if (run.tasks.some((t) => t.status === 'waiting' || t.status === 'working' || t.status === 'sending')) return 'working';
|
|
265
285
|
if (run.tasks.some((t) => t.status === 'review')) return 'review';
|
|
266
286
|
return 'done';
|
|
@@ -330,5 +350,5 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
|
|
|
330
350
|
}
|
|
331
351
|
|
|
332
352
|
module.exports = { MAX_PARALLEL, MAX_REVISIONS, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
|
|
333
|
-
newRun, readRun, findTask, modeOf, addTasks, toStart, started, deliver, decide, sent, fail, sessionEnded, sessionStalled, stage, intervalMin,
|
|
353
|
+
newRun, readRun, findTask, modeOf, addTasks, addBriefed, toStart, started, deliver, decide, sent, fail, sessionEnded, sessionStalled, stage, intervalMin,
|
|
334
354
|
checkDue, checkStarted, checkDone };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* my-work-cli.js — `pmos my-work`: the work assigned to the PM, ranked, from a terminal (initiative my-work, PRD R11).
|
|
3
|
+
*
|
|
4
|
+
* pmos my-work [list] [--top N] [--json] every item, in the order My work shows it, with its tier and state
|
|
5
|
+
* pmos my-work show <ITEM> [--json] one item: the context card, the WSJF parts, the state
|
|
6
|
+
* pmos my-work submit --file <read.json> (the my-work-read skill) submit the read of the PM's assigned Jira issues
|
|
7
|
+
*
|
|
8
|
+
* Every command goes through the running workspace server (GET /api/my-work, POST /api/my-work/read), so a terminal line
|
|
9
|
+
* and the page cannot disagree. Nothing here briefs an agent or takes an item: those are the PM's presses in My work.
|
|
10
|
+
* An ITEM is its id (jira:ATL-491, wbs:<ws>:<item>), its Jira key, or its position in the list.
|
|
11
|
+
*
|
|
12
|
+
* Exit codes: 0 done, 1 refused, 2 usage.
|
|
13
|
+
*/
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { parseArgs } = require('./schedule-cli');
|
|
17
|
+
|
|
18
|
+
const COMMANDS = { 'my-work': ['list', 'show', 'submit'] };
|
|
19
|
+
const USAGE = [
|
|
20
|
+
'usage: pmos my-work [list|show|submit] — the Jira tickets and WBS items assigned to you, ranked',
|
|
21
|
+
' pmos my-work [list] [--top N] [--json] every item, as My work orders it',
|
|
22
|
+
' pmos my-work show <ITEM> [--json] one item: what the agent found, the score, the state',
|
|
23
|
+
' pmos my-work submit --file <read.json> (agent session) submit the read of your assigned Jira issues',
|
|
24
|
+
'Nothing here briefs an agent or takes an item. Do that in My work.',
|
|
25
|
+
];
|
|
26
|
+
const BOOL = new Set(['json', 'help']);
|
|
27
|
+
const STATE_WORD = { brief: 'to brief', yours: 'yours', agent: 'agent', ready: 'ready in Agenda', done: 'done' };
|
|
28
|
+
|
|
29
|
+
function parse(argv) {
|
|
30
|
+
const rest = []; const bools = {};
|
|
31
|
+
for (const a of argv) {
|
|
32
|
+
if (a.startsWith('--') && BOOL.has(a.slice(2))) bools[a.slice(2)] = true;
|
|
33
|
+
else if (a === '-h') bools.help = true;
|
|
34
|
+
else rest.push(a);
|
|
35
|
+
}
|
|
36
|
+
const { pos, flags } = parseArgs(rest);
|
|
37
|
+
return { pos, flags: { ...flags, ...bools } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const line = (x, i) => `${i + 1}. ${x.wsjf.tier} ${String(x.wsjf.score).padStart(4)} ${x.key} ${x.title} · ${STATE_WORD[x.state]}${x.sub && x.state === 'agent' ? ' (' + x.sub + ')' : ''}${x.due ? ' · due ' + x.due : ''}${x.from ? ' · from ' + x.from : ''}`;
|
|
41
|
+
|
|
42
|
+
async function run(argv, { say = console.log, call, cwd = process.cwd() } = {}) {
|
|
43
|
+
const { pos, flags } = parse(argv.slice(1));
|
|
44
|
+
const sub = pos[0] || 'list';
|
|
45
|
+
if (flags.help || sub === 'help') { USAGE.forEach((l) => say(l)); return 0; }
|
|
46
|
+
if (!COMMANDS['my-work'].includes(sub)) { USAGE.forEach((l) => say(l)); return 2; }
|
|
47
|
+
const json = !!flags.json;
|
|
48
|
+
const fail = (error, code = 1) => { if (json) say(JSON.stringify({ ok: false, error })); else say(`✗ ${error}`); return code; };
|
|
49
|
+
if (typeof call !== 'function') return fail('the workspace server is not reachable');
|
|
50
|
+
|
|
51
|
+
if (sub === 'submit') {
|
|
52
|
+
if (!flags.file) return fail('usage: pmos my-work submit --file <read.json>', 2);
|
|
53
|
+
let raw;
|
|
54
|
+
try { raw = JSON.parse(fs.readFileSync(path.resolve(cwd, String(flags.file)), 'utf8')); }
|
|
55
|
+
catch (e) { return fail(`could not read ${flags.file} — ${e.message}`); }
|
|
56
|
+
const r = await call('POST', '/api/my-work/read', raw);
|
|
57
|
+
if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the workspace server did not answer'}. Nothing changed.`);
|
|
58
|
+
if (json) say(JSON.stringify(r)); else say(`read saved — ${r.count} item${r.count === 1 ? '' : 's'} at ${r.read_at}. My work shows them to brief.`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const d = await call('GET', '/api/my-work');
|
|
63
|
+
if (!d || d.ok === false) return fail((d && d.error) || 'the workspace server did not answer');
|
|
64
|
+
if (sub === 'list') {
|
|
65
|
+
const top = flags.top === undefined ? null : Number(flags.top);
|
|
66
|
+
if (top !== null && (!Number.isInteger(top) || top < 1)) return fail('--top takes a whole number of 1 or more', 2);
|
|
67
|
+
const open = d.items.filter((x) => x.state !== 'done');
|
|
68
|
+
const items = top ? open.slice(0, top) : open;
|
|
69
|
+
if (json) { say(JSON.stringify({ ok: true, today: d.today, read_at: d.read_at, counts: d.counts, items })); return 0; }
|
|
70
|
+
say(`My work · ${d.counts.all} open · ${d.counts.brief} to brief · ${d.counts.agents} with agents · ${d.counts.ready} ready in Agenda · read ${d.read_at || 'never'}`);
|
|
71
|
+
for (const b of d.banner || []) say(`! ${b.source}: ${b.error}`);
|
|
72
|
+
if (!items.length) say('Nothing assigned to you. Read again in My work, or run the my-work-read skill.');
|
|
73
|
+
items.forEach((x, i) => say(line(x, i)));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
// show
|
|
77
|
+
const ref = String(pos[1] || '');
|
|
78
|
+
if (!ref) return fail('usage: pmos my-work show <ITEM>', 2);
|
|
79
|
+
const open = d.items.filter((x) => x.state !== 'done');
|
|
80
|
+
const x = /^\d+$/.test(ref) ? open[Number(ref) - 1] : d.items.find((y) => y.id === ref || y.key === ref.toUpperCase() || y.key === ref);
|
|
81
|
+
if (!x) return fail(`no item ${ref} — pmos my-work list shows them`);
|
|
82
|
+
if (json) { say(JSON.stringify({ ok: true, item: x })); return 0; }
|
|
83
|
+
say(`${x.key} ${x.title}`);
|
|
84
|
+
say(`${x.source === 'jira' ? 'Jira' : 'WBS'}${x.repo ? ' · ' + x.repo : ''}${x.status ? ' · ' + x.status : ''}${x.url ? ' · ' + x.url : ''}`);
|
|
85
|
+
say(`state: ${STATE_WORD[x.state]}${x.sub ? ' (' + x.sub + ')' : ''}${x.task ? ' · task ' + x.task.id : ''}`);
|
|
86
|
+
const w = x.wsjf;
|
|
87
|
+
say(`${w.tier} · score ${w.score} = (value ${w.value} + urgency ${w.urgency} + unblocks ${w.unblocks}) / size ${w.size}`);
|
|
88
|
+
if (x.from) say(`from: ${x.from}`);
|
|
89
|
+
if (x.ask) say(`ask: ${x.ask}`);
|
|
90
|
+
if (x.thread) say(`thread: ${x.thread}`);
|
|
91
|
+
(x.open || []).forEach((o) => say(`open: ${o}`));
|
|
92
|
+
if (x.kind === 'decide') say('looks like a decision only you can make');
|
|
93
|
+
if (x.instruction) say(`your instruction: ${x.instruction}`);
|
|
94
|
+
else if (x.suggest) say(`suggested instruction: ${x.suggest}`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { run, COMMANDS, USAGE };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* my-work.js — every Jira ticket and WBS item assigned to the PM, ranked, with one state each (initiative my-work, PRD
|
|
3
|
+
* R1 to R5).
|
|
4
|
+
*
|
|
5
|
+
* PURE: no require at all — no file, process, network or clock of its own. The server reads the stored read
|
|
6
|
+
* (<state>/my-work/read.json), the PM's presses (<state>/my-work/actions.jsonl), the Board fold of each registered
|
|
7
|
+
* repository and the day runs, passes them in with `now`, and writes what an edit returns.
|
|
8
|
+
*
|
|
9
|
+
* checkRead(raw, now) → { ok, read } | { ok:false, error } R1: what the my-work-read skill submits
|
|
10
|
+
* wbsItems(boards) → items R2: owner `you` plan items, every repository
|
|
11
|
+
* score(item, today) → { value, urgency, unblocks, size, score, tier } R3
|
|
12
|
+
* parseActions(text) · checkAct(op) → the press log, one press
|
|
13
|
+
* fold({ read, wbs, actions, runs, today }) → { items, counts } R4: ranked, one state each
|
|
14
|
+
* taskSpec(item, instruction, seq) → what lib/day-run.js addBriefed takes R5
|
|
15
|
+
*
|
|
16
|
+
* An item is { id, source: jira|wbs, key, title, url, priority, status, category: todo|doing|done, due, estimate,
|
|
17
|
+
* blocks, from, ask, thread, open, suggest, kind: agent|decide, initiative, repo, ws }. Its id is `jira:<KEY>` or
|
|
18
|
+
* `wbs:<ws>:<item>`, so the same ticket read twice is the same item.
|
|
19
|
+
*
|
|
20
|
+
* States: brief (nobody decided) · yours (the PM takes it) · agent (a day task works on it; `sub` says how) ·
|
|
21
|
+
* ready (its draft waits in Agenda) · done. Derived on every read; no state is stored.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const PRIORITY_VALUE = Object.freeze({ Highest: 10, High: 8, Medium: 5, Low: 2 });
|
|
25
|
+
const SIZE = Object.freeze({ XS: 1, S: 2, M: 3, L: 5, XL: 8 });
|
|
26
|
+
const TIERS = Object.freeze([[12, 'P1'], [7, 'P2'], [3, 'P3'], [-Infinity, 'P4']]);
|
|
27
|
+
const STATES = Object.freeze(['brief', 'yours', 'agent', 'ready', 'done']);
|
|
28
|
+
const ACTIONS = Object.freeze(['brief', 'handle', 'reopen']);
|
|
29
|
+
const CATEGORIES = Object.freeze(['todo', 'doing', 'done']);
|
|
30
|
+
const KEY_RE = /^[A-Z][A-Z0-9_]*-\d+$/;
|
|
31
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
32
|
+
const ID_RE = /^(jira:[A-Z][A-Z0-9_]*-\d+|wbs:[\w.-]{1,80}:[\w.-]{1,80})$/;
|
|
33
|
+
const MAX_ITEMS = 200;
|
|
34
|
+
const MAX_INSTRUCTION = 2000;
|
|
35
|
+
const DAY = 86400000;
|
|
36
|
+
|
|
37
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
38
|
+
const oneLine = (s) => String(s == null ? '' : s).replace(/[\r\n\t]+/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
39
|
+
const clip = (s, n) => (s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s);
|
|
40
|
+
const str = (v, n) => clip(oneLine(v), n);
|
|
41
|
+
const pick = (map, v) => Object.keys(map).find((k) => k.toLowerCase() === String(v || '').trim().toLowerCase()) || null;
|
|
42
|
+
const httpsUrl = (v) => { const s = String(v || ''); try { return new URL(s).protocol === 'https:' && s.length <= 500 ? s : null; } catch { return null; } };
|
|
43
|
+
const dateOf = (v) => { const s = String(v || '').slice(0, 10); return DATE_RE.test(s) && !Number.isNaN(Date.parse(s + 'T00:00:00Z')) ? s : null; };
|
|
44
|
+
|
|
45
|
+
/** FNV-1a over the string, as 12 hex digits: a task id that is the same for the same item and brief, with no require. */
|
|
46
|
+
function hex12(s) {
|
|
47
|
+
let h1 = 0x811c9dc5; let h2 = 0x01000193;
|
|
48
|
+
for (let i = 0; i < s.length; i++) {
|
|
49
|
+
const c = s.charCodeAt(i);
|
|
50
|
+
h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0;
|
|
51
|
+
h2 = Math.imul(h2 ^ c, 0x0100019d) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
return (h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0')).slice(0, 12);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ── R1 · the read ────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
57
|
+
|
|
58
|
+
function checkItem(x, i) {
|
|
59
|
+
if (!isObj(x)) return { error: `item ${i + 1} is not an object` };
|
|
60
|
+
const key = oneLine(x.key).toUpperCase();
|
|
61
|
+
if (!KEY_RE.test(key)) return { error: `item ${i + 1}: key must be a Jira key like ATL-491` };
|
|
62
|
+
const title = str(x.title || x.summary, 250);
|
|
63
|
+
if (!title) return { error: `item ${key}: a title is required` };
|
|
64
|
+
if (x.priority != null && x.priority !== '' && !pick(PRIORITY_VALUE, x.priority)) return { error: `item ${key}: priority must be Highest, High, Medium or Low` };
|
|
65
|
+
if (x.estimate != null && x.estimate !== '' && !pick(SIZE, x.estimate)) return { error: `item ${key}: estimate must be XS, S, M, L or XL` };
|
|
66
|
+
if (x.due != null && x.due !== '' && !dateOf(x.due)) return { error: `item ${key}: due must be a date YYYY-MM-DD` };
|
|
67
|
+
const blocks = x.blocks == null ? 0 : Number(x.blocks);
|
|
68
|
+
if (!Number.isInteger(blocks) || blocks < 0 || blocks > 99) return { error: `item ${key}: blocks must be a whole number from 0 to 99` };
|
|
69
|
+
const open = Array.isArray(x.open) ? x.open.map((o) => str(o, 200)).filter(Boolean).slice(0, 5) : [];
|
|
70
|
+
const category = CATEGORIES.includes(x.category) ? x.category : 'todo';
|
|
71
|
+
const initiative = /^[a-z0-9][a-z0-9-]{0,79}$/.test(String(x.initiative || '')) ? x.initiative : null;
|
|
72
|
+
return {
|
|
73
|
+
item: {
|
|
74
|
+
id: 'jira:' + key, source: 'jira', key, title, url: httpsUrl(x.url), priority: pick(PRIORITY_VALUE, x.priority) || null,
|
|
75
|
+
status: str(x.status, 60) || null, category, due: dateOf(x.due), estimate: pick(SIZE, x.estimate) || null, blocks,
|
|
76
|
+
from: str(x.from, 80) || null, ask: str(x.ask, 400) || null, thread: str(x.thread, 600) || null, open,
|
|
77
|
+
suggest: str(x.suggest, MAX_INSTRUCTION) || null, kind: x.kind === 'decide' ? 'decide' : 'agent', initiative, repo: null, ws: null,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What the my-work-read skill submits: { items: [...] }. Refused whole on any bad item, so a read is never half-kept. */
|
|
83
|
+
function checkRead(raw, now) {
|
|
84
|
+
if (!isObj(raw) || !Array.isArray(raw.items)) return { ok: false, error: 'the read must be an object with an items list' };
|
|
85
|
+
if (raw.items.length > MAX_ITEMS) return { ok: false, error: `at most ${MAX_ITEMS} items in one read` };
|
|
86
|
+
const items = []; const seen = new Set();
|
|
87
|
+
for (let i = 0; i < raw.items.length; i++) {
|
|
88
|
+
const c = checkItem(raw.items[i], i);
|
|
89
|
+
if (c.error) return { ok: false, error: c.error };
|
|
90
|
+
if (seen.has(c.item.id)) return { ok: false, error: `item ${c.item.key} is listed twice` };
|
|
91
|
+
seen.add(c.item.id);
|
|
92
|
+
items.push(c.item);
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, read: { read_at: new Date(now).toISOString(), items } };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── R2 · WBS items from the Board fold ───────────────────────────────────────────────────────────────────────── */
|
|
98
|
+
|
|
99
|
+
const WBS_CATEGORY = { todo: 'todo', doing: 'doing', review: 'doing', done: 'done' };
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `boards` = [{ ws, repo, board }] where board is lib/board-fold.js boardFor(…, { filter: 'mine' }). Only items with
|
|
103
|
+
* owner `you` are kept. `blocks` counts the items of the same repository whose `after` names the item.
|
|
104
|
+
*/
|
|
105
|
+
function wbsItems(boards) {
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const b of Array.isArray(boards) ? boards : []) {
|
|
108
|
+
if (!b || !isObj(b.board) || !isObj(b.board.items)) continue;
|
|
109
|
+
const all = Object.values(b.board.items);
|
|
110
|
+
const titles = new Map((b.board.initiatives || []).map((i) => [i.id, i.title]));
|
|
111
|
+
for (const it of all) {
|
|
112
|
+
if (!it || it.owner !== 'you' || typeof it.id !== 'string') continue;
|
|
113
|
+
const blocks = all.filter((o) => o && Array.isArray(o.after) && o.after.includes(it.id)).length;
|
|
114
|
+
const id = `wbs:${b.ws}:${it.id}`;
|
|
115
|
+
if (!ID_RE.test(id)) continue;
|
|
116
|
+
out.push({
|
|
117
|
+
id, source: 'wbs', key: it.id, title: str(it.title, 250) || it.id, url: null, priority: null,
|
|
118
|
+
status: str(it.stageLabel || it.stage, 60) || null, category: WBS_CATEGORY[it.state] || 'todo', due: dateOf(it.due), estimate: null,
|
|
119
|
+
blocks, from: titles.get(it.initiative) || it.initiative || null, ask: str(it.note, 400) || null, thread: null, open: [],
|
|
120
|
+
suggest: null, kind: 'agent', initiative: it.initiative || null, repo: b.repo || b.ws, ws: b.ws,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ── R3 · WSJF ─────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
128
|
+
|
|
129
|
+
function urgencyOf(due, today) {
|
|
130
|
+
if (!due || !DATE_RE.test(String(today || ''))) return 1;
|
|
131
|
+
const days = Math.round((Date.parse(due + 'T00:00:00Z') - Date.parse(today + 'T00:00:00Z')) / DAY);
|
|
132
|
+
if (days < 0) return 10;
|
|
133
|
+
if (days <= 2) return 8;
|
|
134
|
+
if (days <= 7) return 5;
|
|
135
|
+
return 3;
|
|
136
|
+
}
|
|
137
|
+
const tierOf = (s) => TIERS.find(([min]) => s >= min)[1];
|
|
138
|
+
|
|
139
|
+
/** The WSJF parts of one item on the local day `today` (YYYY-MM-DD). */
|
|
140
|
+
function score(item, today) {
|
|
141
|
+
const value = PRIORITY_VALUE[item.priority || 'Medium'] + (item.initiative ? 3 : 1);
|
|
142
|
+
const urgency = urgencyOf(item.due, today);
|
|
143
|
+
const unblocks = Math.min(9, 3 * (Number(item.blocks) || 0));
|
|
144
|
+
const size = SIZE[item.estimate || 'M'];
|
|
145
|
+
const s = Math.round(((value + urgency + unblocks) / size) * 10) / 10;
|
|
146
|
+
return { value, urgency, unblocks, size, score: s, tier: tierOf(s) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── the press log ─────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
150
|
+
|
|
151
|
+
/** One press from the page: { action: brief|handle|reopen, item, instruction? }. Returns the record to append. */
|
|
152
|
+
function checkAct(op, now) {
|
|
153
|
+
const o = isObj(op) ? op : {};
|
|
154
|
+
if (!ACTIONS.includes(o.action)) return { ok: false, error: `action must be one of ${ACTIONS.join(', ')}` };
|
|
155
|
+
const item = String(o.item || '');
|
|
156
|
+
if (!ID_RE.test(item)) return { ok: false, error: 'item must be an item id like jira:ATL-491' };
|
|
157
|
+
const rec = { action: o.action, item, at: new Date(now).toISOString() };
|
|
158
|
+
if (o.action === 'brief') {
|
|
159
|
+
const ins = typeof o.instruction === 'string' ? o.instruction.trim() : '';
|
|
160
|
+
if (!ins) return { ok: false, error: 'say what the agent should do: a brief needs an instruction' };
|
|
161
|
+
if (ins.length > MAX_INSTRUCTION) return { ok: false, error: `the instruction is over ${MAX_INSTRUCTION} characters` };
|
|
162
|
+
rec.instruction = ins;
|
|
163
|
+
}
|
|
164
|
+
return { ok: true, record: rec };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The append-only log as text → the presses, oldest first. A torn or unknown line is skipped. */
|
|
168
|
+
function parseActions(text) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const line of String(text || '').split('\n')) {
|
|
171
|
+
if (!line.trim()) continue;
|
|
172
|
+
let r; try { r = JSON.parse(line); } catch { continue; }
|
|
173
|
+
if (isObj(r) && ACTIONS.includes(r.action) && ID_RE.test(String(r.item || '')) && typeof r.at === 'string') out.push(r);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── R4 · the fold ─────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
179
|
+
|
|
180
|
+
const AGENT_SUBS = ['waiting', 'working', 'sending', 'failed'];
|
|
181
|
+
|
|
182
|
+
/** The day task that works on `item` for its last brief, from the run of that brief's day. */
|
|
183
|
+
function taskOf(runs, brief) {
|
|
184
|
+
const day = String(brief.at).slice(0, 10);
|
|
185
|
+
const pool = [];
|
|
186
|
+
for (const [d, run] of Object.entries(isObj(runs) ? runs : {})) if (run && Array.isArray(run.tasks) && (d >= day)) pool.push(...run.tasks);
|
|
187
|
+
return pool.filter((t) => t && t.my_work === brief.item && t.brief_at === brief.at).pop() || null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The state of one item: { state, sub, task, instruction, decided_at }. */
|
|
191
|
+
function stateOf(item, presses, runs) {
|
|
192
|
+
if (item.category === 'done') return { state: 'done', sub: 'source', task: null, instruction: null };
|
|
193
|
+
const last = presses[presses.length - 1] || null;
|
|
194
|
+
if (!last || last.action === 'reopen') return { state: 'brief', sub: null, task: null, instruction: null };
|
|
195
|
+
if (last.action === 'handle') return { state: 'yours', sub: null, task: null, instruction: null, decided_at: last.at };
|
|
196
|
+
const t = taskOf(runs, last);
|
|
197
|
+
const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null } : null;
|
|
198
|
+
const base = { task, instruction: last.instruction, decided_at: last.at };
|
|
199
|
+
if (!t) return { state: 'agent', sub: 'waiting', ...base };
|
|
200
|
+
if (t.status === 'review') return { state: 'ready', sub: 'review', ...base };
|
|
201
|
+
if (t.status === 'sent' || t.status === 'done') return { state: 'done', sub: t.status, ...base };
|
|
202
|
+
return { state: 'agent', sub: AGENT_SUBS.includes(t.status) ? t.status : 'waiting', ...base };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** May the PM press `action` on an item in this state? Null when yes, else why not. */
|
|
206
|
+
function refusal(st, action) {
|
|
207
|
+
if (action === 'brief' || action === 'handle') return st.state === 'brief' ? null : `the item is ${st.state}, not waiting for a brief — press Back to brief first`;
|
|
208
|
+
if (st.state === 'brief') return 'the item is already waiting for a brief';
|
|
209
|
+
if (st.state === 'done' && st.sub === 'source') return 'the item is done in its source';
|
|
210
|
+
if (st.state === 'agent' && (st.sub === 'working' || st.sub === 'sending')) return 'an agent is working on it — wait for its draft in Agenda';
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Every item, ranked, with its state. `read` is the stored read or null; `wbs` is wbsItems(…); `actions` is
|
|
216
|
+
* parseActions(…); `runs` maps a date to that day's run. Counts: brief, agents, ready, yours, all (not done).
|
|
217
|
+
*/
|
|
218
|
+
function fold({ read, wbs, actions, runs, today } = {}) {
|
|
219
|
+
const by = new Map();
|
|
220
|
+
for (const it of (read && Array.isArray(read.items) ? read.items : [])) by.set(it.id, it);
|
|
221
|
+
for (const it of Array.isArray(wbs) ? wbs : []) if (!by.has(it.id)) by.set(it.id, it);
|
|
222
|
+
const presses = new Map();
|
|
223
|
+
for (const a of Array.isArray(actions) ? actions : []) { if (!presses.has(a.item)) presses.set(a.item, []); presses.get(a.item).push(a); }
|
|
224
|
+
const items = [...by.values()].map((it) => ({ ...it, wsjf: score(it, today), ...stateOf(it, presses.get(it.id) || [], runs) }));
|
|
225
|
+
items.sort((a, b) => (b.wsjf.score - a.wsjf.score) || String(a.due || '9999').localeCompare(String(b.due || '9999')) || a.id.localeCompare(b.id));
|
|
226
|
+
const n = (s) => items.filter((x) => x.state === s).length;
|
|
227
|
+
const counts = { brief: n('brief'), agents: n('agent'), ready: n('ready'), yours: n('yours'), done: n('done'), all: items.filter((x) => x.state !== 'done').length };
|
|
228
|
+
return { items, counts };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── R5 · the day task a brief adds ───────────────────────────────────────────────────────────────────────────── */
|
|
232
|
+
|
|
233
|
+
/** What lib/day-run.js addBriefed takes for a brief recorded as `rec` on `item`. */
|
|
234
|
+
function taskSpec(item, rec) {
|
|
235
|
+
return {
|
|
236
|
+
id: 't-' + hex12(item.id + '|' + rec.at), my_work: item.id, brief_at: rec.at, title: item.key + ' · ' + item.title,
|
|
237
|
+
source: item.source, link: item.url, instruction: rec.instruction,
|
|
238
|
+
context: { key: item.key, from: item.from, ask: item.ask, thread: item.thread, open: item.open, repo: item.repo, ws: item.ws || null, initiative: item.initiative, due: item.due },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = { PRIORITY_VALUE, SIZE, STATES, ACTIONS, MAX_INSTRUCTION, ID_RE, checkRead, wbsItems, score, urgencyOf, checkAct, parseActions, stateOf, refusal, fold, taskSpec, hex12 };
|
|
@@ -23,23 +23,45 @@ const crypto = require('crypto');
|
|
|
23
23
|
const P = require('./people');
|
|
24
24
|
const fourPanel = require('./four-panel');
|
|
25
25
|
const consoleState = require('./console-state');
|
|
26
|
+
const planTree = require('./plan-tree');
|
|
26
27
|
|
|
27
28
|
const MAX_PER_RECORD = 500;
|
|
28
29
|
const fs = require('fs');
|
|
29
30
|
const path = require('path');
|
|
30
31
|
|
|
32
|
+
/** One ledger per repository, whichever worktree is open: the main working tree, as plan items read theirs. */
|
|
33
|
+
function repoRoot(root) { return planTree.mainTree(path.resolve(root)); }
|
|
31
34
|
function stateDir(root) {
|
|
32
35
|
const e = consoleState.loadEngine();
|
|
33
36
|
if (!e.ok) return { ok: false, error: e.error };
|
|
34
|
-
const dir = fourPanel.storeDir(e.engine, root);
|
|
37
|
+
const dir = fourPanel.storeDir(e.engine, repoRoot(root));
|
|
35
38
|
return dir ? { ok: true, dir } : { ok: false, error: 'this repository has no file-mode state directory' };
|
|
36
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The other places an earlier version may have written this repository's ledger. The engine puts records in
|
|
42
|
+
* planning/runs/ until the repository has pmos/state/initiatives/, then in pmos/state/, so a repository that gained its
|
|
43
|
+
* first initiative after People was used reads a new, empty file. Before 0.72.1 a worktree also wrote its own ledger.
|
|
44
|
+
* Their rows are read first, so a later press in the ledger now written still wins.
|
|
45
|
+
*/
|
|
46
|
+
function olderLedgers(root, dir) {
|
|
47
|
+
const roots = [...new Set([repoRoot(root), path.resolve(root)])];
|
|
48
|
+
const dirs = roots.flatMap((r) => [path.join(r, 'planning', 'runs'), path.join(r, 'pmos', 'state')]);
|
|
49
|
+
return [...new Set(dirs)].filter((d) => path.resolve(d) !== path.resolve(dir) && fs.existsSync(path.join(d, P.LEDGER)));
|
|
50
|
+
}
|
|
37
51
|
function readRows(root) {
|
|
38
52
|
const s = stateDir(root);
|
|
39
53
|
if (!s.ok) return s;
|
|
40
54
|
const r = fourPanel.readDeclarations(s.dir, P.LEDGER);
|
|
41
55
|
if (!r.ok) return { ok: false, error: r.error };
|
|
42
|
-
|
|
56
|
+
const older = olderLedgers(root, s.dir);
|
|
57
|
+
if (!older.length) return { ok: true, dir: s.dir, file: r.file, rows: r.rows };
|
|
58
|
+
// A line already in a file read before (a worktree's copy of a committed ledger) counts once.
|
|
59
|
+
const rows = [];
|
|
60
|
+
for (const list of [...older.map((d) => fourPanel.readDeclarations(d, P.LEDGER)).filter((o) => o.ok).map((o) => o.rows), r.rows]) {
|
|
61
|
+
const before = new Set(rows.map((x) => JSON.stringify(x)));
|
|
62
|
+
for (const row of list) if (!before.has(JSON.stringify(row))) rows.push(row);
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, dir: s.dir, file: r.file, rows, older: older.map((d) => path.join(d, P.LEDGER)) };
|
|
43
65
|
}
|
|
44
66
|
/** The plan items, for shared work (R10). A repository without a plan gives none. */
|
|
45
67
|
function planItems(root) {
|
|
@@ -44,6 +44,9 @@ const VERBS = Object.freeze({
|
|
|
44
44
|
// Not a pm-os-absorb verb (run: null): people-directory added it through the same table.
|
|
45
45
|
people: Object.freeze({ module: 'people-cli.js', run: null, guide: 'people.md',
|
|
46
46
|
help: 'pmos people list|show|record|settings … — the people you work with in this repository (pmos people help)' }),
|
|
47
|
+
// Not a pm-os-absorb verb (run: null): initiative my-work added it through the same table.
|
|
48
|
+
'my-work': Object.freeze({ module: 'my-work-cli.js', run: null, guide: 'my-work.md',
|
|
49
|
+
help: 'pmos my-work list|show|submit … — the Jira tickets and WBS items assigned to you, ranked (pmos my-work help)' }),
|
|
47
50
|
launcher: Object.freeze({ module: 'launcher-cli.js', run: null, guide: 'launcher.md',
|
|
48
51
|
help: 'pmos launcher on|off|status — a PMOS app and a login item, so the workspace starts without a terminal' }),
|
|
49
52
|
});
|
|
@@ -275,6 +275,16 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
275
275
|
for (const k of ['where', 'who', 'about', 'ask', 'how']) if (it[k]) say(`${k}: ${it[k]}`);
|
|
276
276
|
if (it.reply_needed !== undefined) say(`reply needed: ${it.reply_needed ? 'yes' : 'no'}`);
|
|
277
277
|
if (it.initiative_id) say(`initiative: ${it.initiative_id}`);
|
|
278
|
+
} else if (t.my_work) {
|
|
279
|
+
// a job the PM briefed in My work (initiative my-work, R6): the PM's instruction is the ask
|
|
280
|
+
const c = t.context || {};
|
|
281
|
+
say(`from My work: ${t.my_work}${c.repo ? ' · repository ' + c.repo : ''}${c.due ? ' · due ' + c.due : ''}`);
|
|
282
|
+
if (c.from) say(`who: ${c.from}`);
|
|
283
|
+
if (c.ask) say(`about: ${c.ask}`);
|
|
284
|
+
if (c.thread) say(`thread: ${c.thread}`);
|
|
285
|
+
for (const o of Array.isArray(c.open) ? c.open : []) say(`open question: ${o}`);
|
|
286
|
+
if (c.initiative) say(`initiative: ${c.initiative}`);
|
|
287
|
+
say(`instruction from the PM (this is the ask): ${t.instruction}`);
|
|
278
288
|
} else say('(the item is no longer in today\'s brief)');
|
|
279
289
|
if (t.kind === 'approve') say('kind approve: deliver ONE draft; nothing goes out until the PM approves it.');
|
|
280
290
|
else say('kind auto: deliver ONE draft; nothing leaves this machine.');
|
|
@@ -37,6 +37,7 @@ const MODULES = Object.freeze([
|
|
|
37
37
|
'routes-board', // Run 4 (4D): the cross-initiative Board, Release
|
|
38
38
|
'routes-reports', // Run 5 (5C): drafts, Issue, notes
|
|
39
39
|
'routes-notes', // meeting-notes Run 1: the Notes view (typed notes, dictation, transcripts, summaries)
|
|
40
|
+
'routes-my-work', // initiative my-work: the ranked list of work assigned to the PM, the brief, Read again
|
|
40
41
|
]);
|
|
41
42
|
const PM_DIR = path.join(__dirname, '..', 'public', 'pm');
|
|
42
43
|
const MODULE_NAME = /^[a-z0-9][a-z0-9-]*\.js$/;
|