create-pmos 0.230.0 → 0.232.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.230.0",
3
+ "version": "0.232.0",
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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@19db06e
1
+ kernel-source: pmos@ac35488
2
2
  assembled: 2026-10-14
3
3
  mode: vendor
@@ -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-14T07:57:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T08:45:03Z"}
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-14T07:57:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T08:45:03Z"}
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-14T07:57:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T08:45:03Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -4,7 +4,7 @@
4
4
  * dimensions 1 to 5 and 7). Loads /c3.html?demo=1 at 1440 wide and then at 390 wide. Demo mode answers every /api call in the page,
5
5
  * so nothing reaches the server and the fixture (public/pm/c3-demo-data.js pmItems) is placed by the same rule the server uses.
6
6
  *
7
- * views six views in order (Inbox, Today, Upcoming, Waiting, Someday, Review), each with its count; no Triage, Week, Later
7
+ * views seven views in order (Inbox, Today, Upcoming, Waiting, Someday, Review, Done), each with its count; no Triage, Week, Later
8
8
  * lens; the right column is Focus on every view, with no Flow today (console3-deck-flow-fixes); no Jobs card, Group by job, ring or plan deck (console3-work-parity)
9
9
  * one home none of the decisions and approvals whose home is Briefing · Needs you is a Work row or Work text
10
10
  * Focus Today opens with up to three suggestions, each with the reason; Accept pins, Not today hides, Edit title saves, Unpin
@@ -12,6 +12,8 @@
12
12
  * to a later day; Done takes a one-line outcome
13
13
  * Upcoming the next 7 days by day, then Later; a deferred item says "Not before …"
14
14
  * Review one at a time with progress: Keep, Change (the edit fields), Drop, then "Review done"
15
+ * Done the last 30 days by day (Today, Yesterday, …) with the outcome and how it closed; the badge counts today; Reopen puts an
16
+ * item back in its view, with Undo, also an item that was done before the page opened (c3-work-agent amendment 18)
15
17
  * capture the preview chip names the view the line lands in, by the server's rule, and never Triage
16
18
  * phone at 390 wide every view draws with no horizontal scroll
17
19
  * The page must raise no script error. Every wait polls the page; no fixed sleep stands in for a state.
@@ -40,8 +42,8 @@ const finish = async (code) => {
40
42
  };
41
43
  process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
42
44
 
43
- const VIEWS = ['inbox', 'today', 'upcoming', 'waiting', 'someday', 'review'];
44
- const LABELS = ['Inbox', 'Today', 'Upcoming', 'Waiting', 'Someday', 'Review'];
45
+ const VIEWS = ['inbox', 'today', 'upcoming', 'waiting', 'someday', 'review', 'done'];
46
+ const LABELS = ['Inbox', 'Today', 'Upcoming', 'Waiting', 'Someday', 'Review', 'Done'];
45
47
  /* The decisions and approvals the demo used to list in Work (PRD R9): their home is Briefing · Needs you. */
46
48
  const DECISIONS = ['Approve pricing-page copy before handoff', 'Confirm the growth assumption in the Q4 forecast', 'Say yes to Sam: ship the logging change',
47
49
  'Self-serve onboarding: continue or park?', 'Pick interview slots with 3 customers', 'Assign owners to 2 unowned workshop actions',
@@ -91,9 +93,9 @@ const W = '[data-pm-work]';
91
93
  const readBar = () => st((w) => [...document.querySelectorAll(w + ' [data-pm-lenses] [data-lens]')].filter((b) => b.offsetParent !== null).map((b) => [b.getAttribute('data-lens'), b.textContent.replace(/\d+$/, '').trim()]), W);
92
94
  await until(async () => (await readBar()).length === VIEWS.length, 10000, 100);
93
95
  const bar = await readBar();
94
- ok('the bar has the six views in order: Inbox, Today, Upcoming, Waiting, Someday, Review', JSON.stringify(bar.map((b) => b[0])) === JSON.stringify(VIEWS) && JSON.stringify(bar.map((b) => b[1])) === JSON.stringify(LABELS), JSON.stringify(bar));
96
+ ok('the bar has the seven views in order: Inbox, Today, Upcoming, Waiting, Someday, Review, Done', JSON.stringify(bar.map((b) => b[0])) === JSON.stringify(VIEWS) && JSON.stringify(bar.map((b) => b[1])) === JSON.stringify(LABELS), JSON.stringify(bar));
95
97
  const c0 = await counts();
96
- ok('each view shows its count (3, 5, 6, 2, 2, 3)', JSON.stringify(VIEWS.map((v) => c0[v])) === '[3,5,6,2,2,3]', JSON.stringify(c0));
98
+ ok('each view shows its count (3, 5, 6, 2, 2, 3; Done counts today: 0)', JSON.stringify(VIEWS.map((v) => c0[v])) === '[3,5,6,2,2,3,0]', JSON.stringify(c0));
97
99
  ok('the page opens on Today', await st((w) => (document.querySelector(w + ' [data-pm-lens-head]') || {}).getAttribute && document.querySelector(w + ' [data-pm-lens-head]').getAttribute('data-pm-lens-head') === 'today', W));
98
100
  const text0 = await st((w) => document.querySelector(w).textContent, W);
99
101
  ok('no Triage, Week or Later view', !(await st((w) => !!document.querySelector(w + ' [data-lens="triage"], ' + w + ' [data-lens="week"], ' + w + ' [data-lens="later"]'), W)));
@@ -174,6 +176,28 @@ const W = '[data-pm-work]';
174
176
  if (sam) { await page.fill(W + ' ' + sam + ' [data-pm-outcome]', 'Booked for Tuesday 10:00'); await click(W + ' ' + sam + ' [data-pm-offline-done]'); }
175
177
  ok('Done with a one-line outcome closes an offline item', !!sam && !!await until(async () => !(await titles()).includes('Set up a meeting with Sam about the logging rollout') && (await counts()).upcoming === 6, 5000));
176
178
 
179
+ /* Done (c3-work-agent amendment 18): what the PM finished, by day, with how it closed; Reopen puts it back */
180
+ ok('the Done badge counts the items done today', !!await until(async () => (await counts()).done === 1, 5000), JSON.stringify(await counts()));
181
+ await view('done');
182
+ const doneRows = () => st((w) => [...document.querySelectorAll(w + ' [data-pm-list="done"]')].map((s) => [s.getAttribute('data-pm-sect'), [...s.querySelectorAll('[data-pm-done-item]')].map((r) => ({
183
+ t: (r.querySelector('[data-pm-title]') || {}).textContent, out: (r.querySelector('[data-pm-done-outcome]') || {}).textContent || '', how: (r.querySelector('[data-pm-done-how]') || {}).textContent || '',
184
+ at: (r.querySelector('[data-pm-done-at]') || {}).textContent || '', reopen: !!r.querySelector('[data-pm-reopen]') }))]), W);
185
+ const d0 = await doneRows();
186
+ ok('Done groups the items by day: Today (Sam, done just now) and Yesterday (the PRD)', JSON.stringify(d0.map((g) => [g[0], g[1].map((r) => r.t)])) === JSON.stringify([['today', ['Set up a meeting with Sam about the logging rollout']], ['yday', ['Send revised PRD to Dara']]]), JSON.stringify(d0));
187
+ const samD = d0.length ? d0[0][1][0] : {};
188
+ ok('…a row shows the outcome, how it closed, the time and Reopen', String(samD.out).trim() === 'Booked for Tuesday 10:00' && samD.how === 'You marked it done' && /^Done \d\d:\d\d$/.test(samD.at) && samD.reopen, JSON.stringify(samD));
189
+ ok('…an older row shows the day it was done', d0.length > 1 && /^Done (Tue|Wed) 7 Oct$/.test(d0[1][1][0].at), JSON.stringify(d0[1]));
190
+ ok('…with the summary line and the header', await st((w) => /^2 done in the last 7 days, 2 in 30 days\. Older items stay in their job's sheet\.$/.test((document.querySelector(w + ' [data-pm-done-sum]') || {}).textContent.trim()) && /What you finished/.test(document.querySelector(w + ' [data-pm-lens-head]').textContent), W));
191
+ await click(W + ' [data-pm-done-item] [data-pm-reopen]');
192
+ ok('Reopen says where the item went back to', !!await until(() => st(() => /Reopened\. It is back in Upcoming\./.test(document.body.textContent)), 4000));
193
+ ok('…and it leaves Done and is back in Upcoming', !!await until(async () => { const c = await counts(); return c.done === 0 && c.upcoming === 7; }, 5000), JSON.stringify(await counts()));
194
+ await toastUndo();
195
+ ok('…Undo marks it done again', !!await until(async () => (await counts()).done === 1, 5000), JSON.stringify(await counts()));
196
+ await click(W + ' [data-pm-sect="yday"] [data-pm-reopen]');
197
+ ok('Reopen works on an item done before the page opened', !!await until(async () => !(await doneRows()).some((g) => g[1].some((r) => r.t === 'Send revised PRD to Dara')), 5000), JSON.stringify(await doneRows()));
198
+ await toastUndo();
199
+ ok('…and Undo puts it back in Done', !!await until(async () => (await doneRows()).some((g) => g[1].some((r) => r.t === 'Send revised PRD to Dara')), 5000));
200
+
177
201
  /* the shared actions: Flag from the Inbox (R2) */
178
202
  await view('inbox');
179
203
  const tear = await open('Read the competitor pricing teardown');
@@ -69,7 +69,7 @@ Agenda shows one card, **N items need your brief**, when items wait in To brief.
69
69
 
70
70
  None of these briefs an agent or takes an item. Do that in My work.
71
71
 
72
- ## The Work page: your work in six views
72
+ ## The Work page: your work in seven views
73
73
 
74
74
  The Work page holds all the work that is yours:
75
75
 
@@ -83,7 +83,7 @@ Every action works on these rows. The first action writes the item to your own l
83
83
 
84
84
  The server decides where each item shows, so one item is never in two views.
85
85
 
86
- The six views follow the OmniFocus style. The **Work** tab badge counts Inbox plus Today.
86
+ The seven views follow the OmniFocus style. The **Work** tab badge counts Inbox plus Today.
87
87
 
88
88
  | View | What it holds |
89
89
  |---|---|
@@ -93,6 +93,7 @@ The six views follow the OmniFocus style. The **Work** tab badge counts Inbox pl
93
93
  | Waiting | Items where you wait for someone. The item that has waited longest comes first. |
94
94
  | Someday | Items you parked. They do not appear anywhere else. |
95
95
  | Review | Items in Inbox, Someday or Waiting that nobody touched for 7 days. The most stale item comes first. |
96
+ | Done | Items you marked done in the last 30 days, newest first. Each one shows the outcome and how it closed. |
96
97
 
97
98
  An item goes to the first view that fits these rules, in this order. Parked is Someday. Waiting on someone is Waiting. A defer date after today is Upcoming. Flagged is Today. Due today or earlier is Today. A later due date is Upcoming. An undated item whose defer date has arrived is Today. No date is Inbox.
98
99
 
@@ -166,6 +167,10 @@ An offline item can carry evidence that it is done. PMOS looks at your calendar
166
167
 
167
168
  Review is a weekly pass. It shows one item at a time. Press **Keep** to leave the item as it is and clear it from the queue for 7 days. Press **Change** to edit it. Press **Drop** to remove it. Any change to an item also clears it from the queue.
168
169
 
170
+ ### Done
171
+
172
+ Done lists the items you marked done in the last 30 days, in four cards: **Today**, **Yesterday**, **Earlier this week** and **Earlier, last 30 days**. Each row shows the outcome you wrote, when the item was done, and how it closed: you marked it done, you closed it from the agent draft, the agent posted it, the agent did the step you approved, or you confirmed it. Press **Reopen** to put the item back in its view. The badge on Done counts the items done today. Older items stay in their job's sheet.
173
+
169
174
  ### Link to a team task
170
175
 
171
176
  A Work item can link to the plan item it advances. The link shows on the item with the title of the plan item. Closing the plan item is a separate step, in the initiative.
@@ -176,7 +181,7 @@ A decision or an approval only you can make is not a Work item. It shows in Brie
176
181
 
177
182
  ## Using the Work page
178
183
 
179
- The Work page in the console has six views: **Inbox**, **Today**, **Upcoming**, **Waiting**, **Someday** and **Review**. Each view button shows how many items it holds.
184
+ The Work page in the console has seven views: **Inbox**, **Today**, **Upcoming**, **Waiting**, **Someday**, **Review** and **Done**. Each view button shows how many items it holds. The Done button shows how many items you finished today.
180
185
 
181
186
  Each item is in one view only. The rule, in order: an item in Someday stays in Someday. An item that waits on a person is in Waiting. An item with a Not before date after today is in Upcoming. A flagged item is in Today. An item due today or earlier is in Today. An item due later is in Upcoming. An item with no date is in Inbox.
182
187
 
@@ -17,6 +17,8 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
 
19
19
  const UNDO_MS = 8000;
20
+ /* How an item was closed (Work › Done): the PM marked it, the PM closed it from the agent draft, or the PM confirmed offline work. */
21
+ const DONE_HOW = ['you', 'agent', 'confirmed'];
20
22
  const LISTS = ['waiting', 'today', 'week', 'triage', 'later'];
21
23
  const WORK_LISTS = ['inbox', 'today', 'upcoming', 'waiting', 'someday'];
22
24
  const REVIEW_DAYS = 7;
@@ -136,9 +138,9 @@ function fold(lines) {
136
138
  else if (ev.event === 'edited' && items.has(ev.id)) items.set(ev.id, { ...items.get(ev.id), ...ev.fields, touched_at: ev.at });
137
139
  else if (ev.event === 'done' && items.has(ev.id)) {
138
140
  const outcome = typeof ev.outcome === 'string' && ev.outcome.trim() ? { outcome: ev.outcome.trim() } : {};
139
- items.set(ev.id, { ...items.get(ev.id), done: true, done_at: ev.at, touched_at: ev.at, ...outcome });
141
+ items.set(ev.id, { ...items.get(ev.id), done: true, done_at: ev.at, done_how: DONE_HOW.includes(ev.how) ? ev.how : 'you', touched_at: ev.at, ...outcome });
140
142
  }
141
- else if (ev.event === 'reopened' && items.has(ev.id)) items.set(ev.id, { ...items.get(ev.id), done: false, done_at: null, touched_at: ev.at });
143
+ else if (ev.event === 'reopened' && items.has(ev.id)) items.set(ev.id, { ...items.get(ev.id), done: false, done_at: null, done_how: null, touched_at: ev.at });
142
144
  else if (ev.event === 'kept' && items.has(ev.id)) items.set(ev.id, { ...items.get(ev.id), touched_at: ev.at });
143
145
  else if (ev.event === 'dropped' || ev.event === 'deleted') items.delete(ev.id);
144
146
  }
@@ -177,4 +179,4 @@ function readLines(file) {
177
179
  return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } });
178
180
  }
179
181
 
180
- module.exports = { UNDO_MS, LISTS, WORK_LISTS, REVIEW_DAYS, workPlacement, sortWorkList, reviewQueue, arrivesOn, parseLine, placement, fold, removalEvent, undoEvent, canUndo, appendEvent, readLines };
182
+ module.exports = { UNDO_MS, DONE_HOW, LISTS, WORK_LISTS, REVIEW_DAYS, workPlacement, sortWorkList, reviewQueue, arrivesOn, parseLine, placement, fold, removalEvent, undoEvent, canUndo, appendEvent, readLines };
@@ -13,7 +13,7 @@
13
13
  * defer, flagged, resolves, kind, person, evidence, outcome
14
14
  * POST /api/pm-items/remove { id } → { ok, undo_ms } deleted (created here) or dropped (imported)
15
15
  * POST /api/pm-items/undo { id } → { ok, item } | 409 restores the last removal inside the 8 s window
16
- * POST /api/pm-items/done { id, outcome? } → { ok } folds to item.done + done_at (+ outcome, one line); /reopen undoes it
16
+ * POST /api/pm-items/done { id, outcome?, how? } → { ok } folds to item.done + done_at (+ outcome, one line; + done_how: you|agent|confirmed); /reopen undoes it
17
17
  * POST /api/pm-items/park { id } → { ok } parked: true, placed in Later; /unpark clears it
18
18
  * POST /api/pm-items/flag { id, flagged } → { ok } flagged: boolean; a flagged item is in Today
19
19
  * POST /api/pm-items/defer { id, until|null } → { ok } defer: not before that date (null clears); clears parked
@@ -193,7 +193,8 @@ function flagItem(body, itemLines, events) {
193
193
  function doneItem(b, l) {
194
194
  if (b.outcome !== undefined && b.outcome !== null && typeof b.outcome !== 'string') return { status: 400, body: { ok: false, error: 'outcome must be a string' } };
195
195
  const outcome = typeof b.outcome === 'string' && b.outcome.trim() ? { outcome: b.outcome.trim() } : {};
196
- return flagItem(b, l, [{ event: 'done', id: b.id, ...outcome }]);
196
+ const how = pmItems.DONE_HOW.includes(b.how) ? { how: b.how } : {}; // how it closed, for Work › Done (c3-work-agent amendment 18)
197
+ return flagItem(b, l, [{ event: 'done', id: b.id, ...outcome, ...how }]);
197
198
  }
198
199
  const reopenItem = (b, l) => flagItem(b, l, [{ event: 'reopened', id: b.id }]);
199
200
  const parkItem = (b, l) => flagItem(b, l, [{ event: 'edited', id: b.id, fields: { parked: true, list_override: null } }]);
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+ /* lib/win-hide.js — on Windows, no console window for the commands the server runs.
3
+ *
4
+ * The server runs in the background with no console of its own (bin/pmos.js starts it detached and
5
+ * hidden). On Windows, a process with no console that starts a console program (git, claude, npm, cmd)
6
+ * gets a NEW console window for it, unless the call passes `windowsHide: true`. The server runs such
7
+ * commands all the time, so without this the PM sees windows open again and again.
8
+ *
9
+ * This makes `windowsHide: true` the default for every child_process call in this process. It must be
10
+ * required before any other module takes a reference to child_process (server.js requires it first),
11
+ * because `const { spawn } = require('child_process')` keeps the function it saw at that moment.
12
+ * A call that passes `windowsHide` itself keeps its own value. Elsewhere this does nothing.
13
+ */
14
+ const util = require('util');
15
+
16
+ function withHide(args) {
17
+ const a = [...args];
18
+ // The options object comes after the command and its optional argument list, before any callback.
19
+ const i = Array.isArray(a[1]) || (a[1] == null && a.length > 2) ? 2 : 1;
20
+ const o = a[i];
21
+ if (o && typeof o === 'object' && !Array.isArray(o)) {
22
+ if (o.windowsHide === undefined) a[i] = { ...o, windowsHide: true };
23
+ } else if (o == null && i < a.length) {
24
+ a[i] = { windowsHide: true };
25
+ } else {
26
+ a.splice(i, 0, { windowsHide: true });
27
+ }
28
+ return a;
29
+ }
30
+
31
+ function install(cp = require('child_process')) {
32
+ if (cp.__pmosWindowsHide) return cp;
33
+ for (const name of ['spawn', 'spawnSync', 'execFile', 'execFileSync', 'exec', 'execSync', 'fork']) {
34
+ const orig = cp[name];
35
+ if (typeof orig !== 'function') continue;
36
+ const wrapped = function (...args) { return orig.apply(this, withHide(args)); };
37
+ // util.promisify(execFile) resolves { stdout, stderr } through this hook; keep it, with the default.
38
+ const custom = orig[util.promisify.custom];
39
+ if (custom) wrapped[util.promisify.custom] = (...args) => custom(...withHide(args));
40
+ cp[name] = wrapped;
41
+ }
42
+ Object.defineProperty(cp, '__pmosWindowsHide', { value: true });
43
+ return cp;
44
+ }
45
+
46
+ if (process.platform === 'win32') install();
47
+
48
+ module.exports = { withHide, install };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.230.0",
3
+ "version": "0.232.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.230.0",
9
+ "version": "0.232.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.230.0",
3
+ "version": "0.232.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -387,7 +387,7 @@ const W_KEYS = it('Payments sandbox keys', { waiting_on: 'Dara', due: '2026-10-0
387
387
  const W_LABEL = it('Annual-plan label', { waiting_on: 'Lee', due: '2026-10-07', job: 'j1', priority: 'P3', touched_at: at('2026-09-29') });
388
388
  const W_JIRA = it('Clean up the Checkout Jira labels', { parked: true, created_at: '2026-09-10T09:00:00.000Z', touched_at: at('2026-09-18') });
389
389
  const W_NOMEET = it('Try a no-meeting Wednesday', { parked: true, touched_at: at('2026-10-02') });
390
- /* done yesterday: counts in its job, never drawn */
390
+ /* done yesterday: counts in its job, and Work › Done lists it under Yesterday */
391
391
  const W_PRD = it('Send revised PRD to Dara', { due: '2026-10-07', job: 'j1', done: true, done_at: '2026-10-07T17:10:00.000Z' });
392
392
  const ITEMS = [W_TIMELINE, W_QA, W_CALL, W_RISK, W_PAGER, W_SAM, W_SSO, W_NOTES, W_SALES, W_TAGS, W_Q1, W_TEAR, W_BOOK, W_IDEA, W_KEYS, W_LABEL, W_JIRA, W_NOMEET, W_PRD];
393
393
  /* Focus: up to three jobs for today, each with the reason (rules first: an overdue promise, an offline item due, a flag) */
@@ -2039,6 +2039,10 @@
2039
2039
  }
2040
2040
 
2041
2041
  .c3 .wk-sm{font-size:var(--fs-2)}
2042
+ /* Work › Done (c3-work-agent amendment 18): the outcome line under the title, and the summary above the groups */
2043
+ .c3 .wk-done-out{display:flex;align-items:center;gap:4px;color:var(--foreground);font-size:var(--fs-2)}
2044
+ .c3 .wk-done-out .ic{color:var(--muted-foreground)}
2045
+ .c3 .wk-done-sum{margin-bottom:12px}
2042
2046
  .c3 .wk-bar{appearance:none;-webkit-appearance:none;width:100%;height:8px;border:0;border-radius:6px;overflow:hidden;background:var(--muted)}
2043
2047
  .c3 .wk-bar::-webkit-progress-bar{background:var(--muted)}
2044
2048
  .c3 .wk-bar::-webkit-progress-value{background:var(--acc);border-radius:6px}
@@ -110,9 +110,11 @@ export function c3ib(label, icon, attrs, cls) {
110
110
 
111
111
  const API = '/api/pm-items';
112
112
  const JOBS = '/api/pm-jobs';
113
- /* The six views (initiative console3-work-page, PRD R1): five places an open item can be, and the weekly Review pass. */
114
- const LENSES = [['inbox', 'Inbox'], ['today', 'Today'], ['upcoming', 'Upcoming'], ['waiting', 'Waiting'], ['someday', 'Someday'], ['review', 'Review']];
115
- const WLI = { inbox: 'inbox', today: 'sun', upcoming: 'cal', waiting: 'wait', someday: 'moon', review: 'check' };
113
+ /* The seven views (initiative console3-work-page, PRD R1): five places an open item can be, the weekly Review pass, and Done:
114
+ * what the PM finished in the last 30 days (c3-work-agent amendment 18). */
115
+ const LENSES = [['inbox', 'Inbox'], ['today', 'Today'], ['upcoming', 'Upcoming'], ['waiting', 'Waiting'], ['someday', 'Someday'], ['review', 'Review'], ['done', 'Done']];
116
+ const WLI = { inbox: 'inbox', today: 'sun', upcoming: 'cal', waiting: 'wait', someday: 'moon', review: 'check', done: 'todo' };
117
+ const DONE_DAYS = 30;
116
118
  const WHO = ['you', 'agent-drafts', 'agent-does'];
117
119
  const OUTWARD = /\b(send|sends|sending|email|emails|message|messages|tell|post|reply to)\b/i; // mirrors lib/item-steps.js
118
120
  const RESOLVES_TIP = 'Your work. The team task is separate: close it in its initiative.';
@@ -135,6 +137,7 @@ export const COPY = Object.freeze({
135
137
  lens: {
136
138
  inbox: 'Decide when or whether', today: 'Overdue, due today and flagged', upcoming: 'The next 7 days, then anything later',
137
139
  waiting: 'Things you handed to someone', someday: 'Parked on purpose', review: 'A weekly pass over items without a date',
140
+ done: 'What you finished',
138
141
  },
139
142
  lensSub: {
140
143
  inbox: 'Nothing here has a date. Tap Decide to give each item a day, park it or drop it.',
@@ -143,6 +146,7 @@ export const COPY = Object.freeze({
143
146
  waiting: 'You expect these back. Nudge when one goes quiet.',
144
147
  someday: 'Nothing here asks for attention until the weekly Review.',
145
148
  review: 'Items in Inbox, Waiting or Someday that nobody touched for 7 days, most stale first. One at a time: keep it, change it, or drop it.',
149
+ done: 'The last 30 days, newest first, with the outcome you wrote. Reopen puts an item back where it was.',
146
150
  },
147
151
  sect: { inbox: 'Inbox', waiting: 'Waiting', someday: 'Someday', overdue: 'Overdue', dueToday: 'Due today', flagged: 'Flagged', next7: 'Next 7 days', later: 'Later' },
148
152
  decisions: (n) => ` ${n} decision${n === 1 ? '' : 's'} wait${n === 1 ? 's' : ''} in Briefing, Needs you.`, openBriefing: 'Open Briefing',
@@ -153,7 +157,13 @@ export const COPY = Object.freeze({
153
157
  emptyView: {
154
158
  inbox: 'Inbox is empty. Capture anything that crosses your mind.', today: 'Nothing due, overdue or flagged. Pick from Upcoming when you are ready.',
155
159
  upcoming: 'Nothing dated in the days ahead.', waiting: 'Nobody owes you anything right now.', someday: 'Nothing parked.',
160
+ done: 'Nothing done in the last 30 days. Items you mark done land here.',
156
161
  },
162
+ /* Done (c3-work-agent amendment 18) */
163
+ doneSum: (w, m) => `${w} done in the last 7 days, ${m} in 30 days. Older items stay in their job's sheet.`,
164
+ doneSect: { today: 'Today', yday: 'Yesterday', week: 'Earlier this week', older: 'Earlier, last 30 days' }, doneLegend: 'Reopen puts it back',
165
+ doneAt: (t) => 'Done ' + t, reopen: 'Reopen', backTo: (v) => 'Back to ' + v, reopened: (v) => `Reopened. It is back in ${v}.`,
166
+ how: { you: 'You marked it done', agent: 'Closed from the agent draft', posted: 'Agent posted it', step: 'Agent did the step you approved', confirmed: 'You confirmed it' },
157
167
  done: 'Done', doneToast: 'Marked done.', steps: 'Steps', noSteps: 'No steps yet.', addStep: 'Add a step',
158
168
  who: { you: 'You', 'agent-drafts': 'Agent drafts', 'agent-does': 'Agent does' },
159
169
  whoOutward: 'Sends to a person: the agent drafts, you slide to send.', options: 'Options', plan: 'Plan the steps',
@@ -194,8 +204,9 @@ function applyOv(base, ov) {
194
204
  const jobOfItem = {};
195
205
  const wasDone = new Set((d.items || []).filter((i) => i.done).map((i) => i.id));
196
206
  const items = [...ov.added.slice().reverse(), ...(d.items || [])].filter((i) => !ov.gone.has(i.id)).map((i) => {
197
- const m = { ...i, ...(ov.edits.get(i.id) || {}) };
198
- m.done = !!(i.done || ov.done.has(i.id));
207
+ const e = ov.edits.get(i.id) || {};
208
+ const m = { ...i, ...e };
209
+ m.done = e.done !== undefined ? !!e.done : !!(i.done || ov.done.has(i.id)); // a reopen (done: false) also reopens a fixture's done item
199
210
  m.steps = [...(i.steps || []), ...(ov.steps.get(i.id) || [])].map((s) => (ov.who.has(s.id) ? { ...s, who: ov.who.get(s.id) } : s));
200
211
  jobOfItem[m.id] = m.job;
201
212
  return m;
@@ -229,8 +240,8 @@ function applyLocal(inst, path, body, r) {
229
240
  const now = new Date().toISOString();
230
241
  const edit = (f) => ov.edits.set(id, { ...(ov.edits.get(id) || {}), ...(f || {}), touched_at: now }); // the server stamps touched_at on every event
231
242
  switch (path.replace(API, '').replace(JOBS, '/job')) {
232
- case '/done': ov.done.add(id); edit(body.outcome ? { outcome: body.outcome } : {}); break;
233
- case '/reopen': ov.done.delete(id); edit(); break;
243
+ case '/done': ov.done.add(id); edit({ done: true, done_at: new Date(today + 'T' + new Date().toTimeString().slice(0, 8)).toISOString(), done_how: body.how || 'you', ...(body.outcome ? { outcome: body.outcome } : {}) }); break; // done_at: now, on the fixture's day
244
+ case '/reopen': ov.done.delete(id); edit({ done: false, done_at: null, done_how: null }); break;
234
245
  case '/park': case '/someday': edit({ parked: true }); break;
235
246
  case '/unpark': edit({ parked: false }); break;
236
247
  case '/edit': edit(body.fields); break;
@@ -362,7 +373,8 @@ const undoBy = (inst, route, body) => async () => !!(await press(inst, API + rou
362
373
  /* Does the change move the item out of the view on screen? Then its row leaves; otherwise it stays and redraws. */
363
374
  const leaves = (inst, item, patch) => inst.lens === 'review' || workPlace({ ...item, ...patch }, inst.data.today) !== inst.lens;
364
375
 
365
- const doneAct = (inst, item, row, text, outcome) => act(inst, row, { route: '/done', body: outcome ? { id: item.id, outcome } : { id: item.id }, text: text || COPY.doneToast, undo: undoBy(inst, '/reopen', { id: item.id }) });
376
+ /* `how` says how it closed, for the Done view: 'you' (the default), 'agent' (from the agent draft) or 'confirmed' (offline work the PM confirmed). */
377
+ const doneAct = (inst, item, row, text, outcome, how) => act(inst, row, { route: '/done', body: { id: item.id, ...(outcome ? { outcome } : {}), ...(how ? { how } : {}) }, text: text || COPY.doneToast, undo: undoBy(inst, '/reopen', { id: item.id }) });
366
378
  const dropAct = (inst, item, row) => act(inst, row, { route: '/remove', body: { id: item.id }, text: COPY.removed, undo: undoBy(inst, '/undo', { id: item.id }) });
367
379
  const todayAct = (inst, item, row) => act(inst, row, { route: '/edit', body: { id: item.id, fields: { due: inst.data.today, defer: null } }, text: 'In Today.',
368
380
  undo: undoBy(inst, '/edit', { id: item.id, fields: { due: item.due || null, defer: item.defer || null } }), leave: leaves(inst, item, { due: inst.data.today, defer: null }) });
@@ -490,7 +502,10 @@ function views(inst) {
490
502
  return { inbox: L.triage || [], today: L.today || [], upcoming: [...(L.week || []), ...(L.later || []).filter((i) => !i.parked)], waiting: L.waiting || [], someday: (L.later || []).filter((i) => i.parked) };
491
503
  }
492
504
  const shown = (inst, id) => ((id === 'review' ? (inst.data || {}).review : views(inst)[id]) || []).filter((i) => !inst.hidden.has(i.id) && !i.done);
493
- const countOf = (inst, id) => { const c = (inst.data || {}).counts; return c && typeof c[id] === 'number' ? c[id] : shown(inst, id).length; };
505
+ const countOf = (inst, id) => {
506
+ if (id === 'done') return doneItems(inst).filter((i) => doneAgo(inst, i) <= 0).length; // the Done badge: items done today
507
+ const c = (inst.data || {}).counts; return c && typeof c[id] === 'number' ? c[id] : shown(inst, id).length;
508
+ };
494
509
  const itemById = (inst, id) => ((inst.data && inst.data.items) || []).find((i) => i.id === id)
495
510
  || [...VIEWS.map((v) => views(inst)[v] || []).flat(), ...((inst.data && inst.data.review) || [])].find((i) => i.id === id);
496
511
  /* The title of the team task a Work item advances (PRD R8): the item's own copy, then a map the answer may carry, else the id. */
@@ -589,7 +604,7 @@ function offlineBlock(inst, item) {
589
604
  return el('div', { class: 'wkoff', 'data-pm-offline': item.id },
590
605
  el('p', { class: 'wkoff-n' }, c3ic('user', 'sm'), COPY.offlineNote(item.person)),
591
606
  item.evidence ? el('div', { class: 'wkev', 'data-pm-evidence': item.id }, c3ic('eye', 'sm'), el('span', { class: 'wk-min' }, el('b', null, COPY.looksDone + ' '), item.evidence),
592
- pill(COPY.confirm, 'check', { class: 'pill pri', 'data-pm-confirm': item.id, onclick: () => doneAct(inst, item, rowOf(inst, item), 'Done. You confirmed it.') })) : null,
607
+ pill(COPY.confirm, 'check', { class: 'pill pri', 'data-pm-confirm': item.id, onclick: () => doneAct(inst, item, rowOf(inst, item), 'Done. You confirmed it.', undefined, 'confirmed') })) : null,
593
608
  el('div', { class: 'wkq' }, el('span', { class: 'form wkoff-o' }, out), pill(COPY.done, 'check', { class: 'pill pri', 'data-pm-offline-done': item.id, onclick: doneNow })),
594
609
  el('div', { class: 'wkq' }, el('span', { class: 'dim mono' }, COPY.notYet),
595
610
  ...[['later-today', COPY.laterToday], ['tomorrow', COPY.tomorrow], ['next-week', COPY.nextWeek]].map(([w, l]) => pill(l, null, { 'data-pm-notyet': w, onclick: () => notYetAct(inst, item, rowOf(inst, item), w) })),
@@ -807,10 +822,10 @@ function agentCase(inst, item, L) {
807
822
  // a step in another system replaces the post: the approved send does the step (amendment 17)
808
823
  step ? o('step', AG.doIt(step), [fx('lock', AG.stepFx1, 'warn'), fx('out', AG.stepFx2, 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.doing), { outward: true }) : null,
809
824
  mine || step ? null : o('post', AG.post(k), [fx('lock', 'Outward: exactly this text', 'warn'), fx('out', 'Goes to ' + w + ' as you', 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.posting), { outward: true }),
810
- mine ? o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)) : null,
825
+ mine ? o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast, undefined, 'agent')) : null,
811
826
  o('edit', AG.editDraft, [fx('edit', step ? 'The step then uses your version' : mine ? 'Your version replaces the draft here' : 'Post then sends your version', 'ok')], () => openDraft(item, inst, true)),
812
827
  o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: mine ? AG.keptMine : AG.kept, timeout: 3000 })),
813
- mine ? null : o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
828
+ mine ? null : o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast, undefined, 'agent')),
814
829
  ].filter(Boolean);
815
830
  extras.push(draftCard(inst, item, k, false), conversation(inst, item, false, w));
816
831
  before = el('div', { class: 'wk-conv-h', 'data-pm-agent-decide': item.id }, el('span', { class: 'dim mono' }, c3ic('check', 'sm'), AG.orDecide));
@@ -1376,6 +1391,71 @@ function openNewJob(inst) {
1376
1391
  later(inst, () => input.focus(), 30);
1377
1392
  }
1378
1393
 
1394
+ /* ── Done (c3-work-agent amendment 18): what the PM finished in the last 30 days, by day, with how it closed and Reopen ── */
1395
+
1396
+ const localDay = (iso) => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? '' : d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); };
1397
+ /* Days between the day an item was done and today: 0 = today. */
1398
+ const doneAgo = (inst, i) => daysBetween(localDay(i.done_at), inst.data.today);
1399
+ function doneItems(inst) {
1400
+ if (!inst.data) return [];
1401
+ return (inst.data.items || []).filter((i) => i.done && i.done_at && localDay(i.done_at) && doneAgo(inst, i) <= DONE_DAYS && !inst.hidden.has(i.id))
1402
+ .sort((a, b) => String(b.done_at).localeCompare(String(a.done_at)));
1403
+ }
1404
+ /* How it closed: what the agent did wins (it posted, or did the approved step), else what the done press said. */
1405
+ const DONE_HOW_IC = { you: 'check', agent: 'spark', posted: 'out', step: 'spark', confirmed: 'eye' };
1406
+ function howOf(item) {
1407
+ const a = item.agent;
1408
+ if (a && a.state === 'sent') return a.draft && a.draft.step ? 'step' : 'posted';
1409
+ return DONE_HOW_IC[item.done_how] ? item.done_how : 'you';
1410
+ }
1411
+
1412
+ function doneRow(inst, item) {
1413
+ const today = inst.data.today;
1414
+ const how = howOf(item);
1415
+ const was = workPlace({ ...item, done: false }, today);
1416
+ const chips = [el('span', { class: 'st ok', 'data-pm-done-at': item.id }, c3ic('check', 'sm'), COPY.doneAt(doneAgo(inst, item) <= 0 ? hhmm(item.done_at) : dayLabel(localDay(item.done_at)))),
1417
+ el('span', { class: 'chip2', 'data-pm-done-how': how }, c3ic(DONE_HOW_IC[how], 'sm'), COPY.how[how])];
1418
+ if (item.origin === 'jira' || item.origin === 'wbs') {
1419
+ const label = (item.origin === 'jira' ? 'Jira ' + (item.key || '') : 'WBS' + (item.key ? ' ' + item.key : '')).trim();
1420
+ chips.push(item.url ? el('a', { class: 'tag', href: item.url, target: '_blank', rel: 'noopener' }, label) : el('span', { class: 'tag' }, label));
1421
+ } else if (item.origin === 'brief') chips.push(el('span', { class: 'tag' }, COPY.suggested));
1422
+ if (item.kind === 'offline' && item.person) chips.push(el('span', { class: 'chip2' }, c3ic('user', 'sm'), item.person));
1423
+ if (item.resolves) chips.push(el('span', { class: 'chip2', title: RESOLVES_TIP }, c3ic('init', 'sm'), resolvesTitle(inst, item)));
1424
+ const row = el('li', { class: 'nrow wrow wk-done-row', 'data-pm-done-item': item.id });
1425
+ const reopen = pill(COPY.reopen, 'undo', { 'data-pm-reopen': item.id, title: COPY.backTo(labelOf(was)),
1426
+ onclick: (e) => { e.stopPropagation(); act(inst, row, { route: '/reopen', body: { id: item.id }, text: COPY.reopened(labelOf(was)), undo: undoBy(inst, '/done', { id: item.id, ...(item.done_how ? { how: item.done_how } : {}) }) }); } });
1427
+ row.append(el('span', { class: 'ki' }, c3ic('check')),
1428
+ el('div', { class: 'nb' }, el('span', { class: 'tt', 'data-pm-title': item.id }, item.title),
1429
+ item.outcome ? el('div', { class: 'wk-done-out', 'data-pm-done-outcome': item.id }, c3ic('reply', 'sm'), item.outcome) : null,
1430
+ el('div', { class: 'chips' }, chips)),
1431
+ el('div', { class: 'acts' }, reopen));
1432
+ return row;
1433
+ }
1434
+
1435
+ function doneMain(inst) {
1436
+ const all = doneItems(inst);
1437
+ if (!all.length) {
1438
+ const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic('todo', 'sm'), labelOf('done'), ' ', el('span', { class: 'bdg g' }, '0'))),
1439
+ el('ul', { class: 'list' }, el('li', { class: 'fold', 'data-pm-done-empty': '' }, c3ic('check', 'sm'), COPY.emptyView.done)));
1440
+ sec.setAttribute('data-pm-list', 'done');
1441
+ return [sec];
1442
+ }
1443
+ const g = { today: [], yday: [], week: [], older: [] };
1444
+ all.forEach((i) => { const n = doneAgo(inst, i); (n <= 0 ? g.today : n === 1 ? g.yday : n <= 7 ? g.week : g.older).push(i); });
1445
+ const week = all.filter((i) => doneAgo(inst, i) <= 7).length;
1446
+ const out = [el('div', { class: 'fold wk-done-sum', 'data-pm-done-sum': '' }, c3ic('check', 'sm'), COPY.doneSum(week, all.length))];
1447
+ let first = true;
1448
+ [['today', 'sun'], ['yday', 'clock'], ['week', 'cal'], ['older', 'moon']].filter(([k]) => g[k].length).forEach(([k, ic]) => {
1449
+ const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(ic, 'sm'), COPY.doneSect[k], ' ', el('span', { class: 'bdg g' }, String(g[k].length))),
1450
+ first ? el('span', { class: 'dim wk-sm' }, COPY.doneLegend) : null),
1451
+ el('ul', { class: 'list', role: 'list' }, g[k].map((i) => doneRow(inst, i))));
1452
+ first = false;
1453
+ sec.setAttribute('data-pm-list', 'done'); sec.setAttribute('data-pm-sect', k);
1454
+ out.push(sec);
1455
+ });
1456
+ return out;
1457
+ }
1458
+
1379
1459
  /* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
1380
1460
 
1381
1461
  /* The mockup's workHead() calbar: the .seg view bar with a count on every view (line 3281). */
@@ -1436,6 +1516,7 @@ function draw(inst) {
1436
1516
  if (lens === 'review') main = [reviewMain(inst), inst.draftsHost];
1437
1517
  else if (lens === 'upcoming') main = upcomingMain(inst, list);
1438
1518
  else if (lens === 'today') main = todayMain(inst, list);
1519
+ else if (lens === 'done') main = doneMain(inst);
1439
1520
  else {
1440
1521
  const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(WLI[lens], 'sm'), COPY.sect[lens], ' ', el('span', { class: 'bdg g' }, String(list.length))), legendEl()),
1441
1522
  el('ul', { class: 'list', role: 'list' }, list.length ? list.map((i) => itemRow(inst, i, lens)) : empty));
@@ -20,6 +20,8 @@
20
20
  * under state/ (the reconcile seam). Request headers are never logged (D08).
21
21
  */
22
22
  'use strict';
23
+ // Windows: hide the console window of every command the server runs. First, before anything holds child_process.
24
+ require('./lib/win-hide');
23
25
  // The workspace reads git in the background (repo-watch, git-info, claims), and a plain `git status` takes the index lock to
24
26
  // refresh the index. A press that writes, such as Sync's `git add`, then fails on `index.lock: File exists` when the two
25
27
  // meet. With optional locks off, the server's reads never take that lock. Set only when the PM has not set it, and taken back
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "one-line-setup: on Windows the background server hides the console window of every command it runs (lib/win-hide.js).",
3
+ "modules": ["win-hide.test.js"],
4
+ "seconds": { "win-hide.test.js": 1 }
5
+ }