create-pmos 0.65.1 → 0.65.3
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/guide/layout.md +1 -1
- package/workspace/guide/today.md +11 -3
- package/workspace/lib/calendar-memory.js +53 -5
- package/workspace/lib/goals.js +25 -6
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/dest-today.js +4 -4
- package/workspace/public/pm/tab-term.js +69 -0
- package/workspace/public/pm/today-agenda.js +29 -5
- package/workspace/public/pm/today-copy.js +11 -3
- package/workspace/public/pm/today-day.js +42 -18
- package/workspace/server.js +60 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.65.
|
|
3
|
+
"version": "0.65.3",
|
|
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-07T01:27:56Z"}
|
|
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-07T01:27:56Z"}
|
|
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-07T01:27:56Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -119,7 +119,7 @@ Every row has a visible ⋯ button. Every control is at least 44 pixels high. No
|
|
|
119
119
|
|
|
120
120
|
You can have the same session open on your phone and your computer. A session has one size, so the screen you type in or tap last sets it. The other screen keeps showing the session and takes the size back as soon as you type in it.
|
|
121
121
|
|
|
122
|
-
On an iPhone or an iPad, the terminal gets smaller when the keyboard opens, so the agent's prompt stays above the keyboard. Tapping the terminal does not zoom the page, and dragging at its top or bottom does not pull or reload the page. A swipe from the left edge of the screen is Safari's Back gesture and leaves the workspace. To avoid that, add the workspace to your Home Screen and open it from there.
|
|
122
|
+
On an iPhone or an iPad, the terminal gets smaller when the keyboard opens, so the agent's prompt stays above the keyboard. Tapping the terminal does not zoom the page, and dragging at its top or bottom does not pull or reload the page. Swipe up or down on the terminal to scroll it, also inside Claude Code or any other program that uses the mouse. A swipe from the left edge of the screen is Safari's Back gesture and leaves the workspace. To avoid that, add the workspace to your Home Screen and open it from there.
|
|
123
123
|
|
|
124
124
|
## The older layout
|
|
125
125
|
|
package/workspace/guide/today.md
CHANGED
|
@@ -30,7 +30,7 @@ An agent job can stop with its session still open, for example when the agent as
|
|
|
30
30
|
|
|
31
31
|
Each job session is named "Job ·" and the first words of the job, so the session list stays short.
|
|
32
32
|
|
|
33
|
-
The Agenda month is at the top of the left column. **Agent results** comes under it, counts the jobs and lists each finished one with its draft summary or the reason it failed. Your goals for today come under Agent results. The day is open when you go to Agenda, also before you press **Prepare my day**. The other things that wait for you, such as outcome checks, report drafts and sessions, are
|
|
33
|
+
The Agenda month is at the top of the left column. **Agent results** comes under it, counts the jobs and lists each finished one with its draft summary or the reason it failed. Your goals for today come under Agent results. The day is open when you go to Agenda, also before you press **Prepare my day**. The other things that wait for you, such as outcome checks, report drafts and sessions, are jobs too. Each one is a row of **Needs you** in the jobs. Press it to see its card, with its own buttons (for example **Draft now**), in the detail column. When the day is folded, they show as cards under it.
|
|
34
34
|
|
|
35
35
|
### The Agenda month
|
|
36
36
|
|
|
@@ -50,7 +50,9 @@ Each day of the month answers three questions:
|
|
|
50
50
|
|
|
51
51
|
The switch above the month shows one answer at a time: **Overview** (all three), **Full** (the time in meetings), **Key** (the meeting that matters), **Prep** (how ready you are) or **Free** (the free blocks of 90 minutes or more).
|
|
52
52
|
|
|
53
|
-
The memory of a meeting says what kind of meeting it is (one-to-one, onboarding or group), how many times it was held, how many are to come, and why it matters.
|
|
53
|
+
The memory of a meeting says what kind of meeting it is (one-to-one, onboarding or group), how many times it was held, how many are to come, and why it matters.
|
|
54
|
+
|
|
55
|
+
To mark a meeting yourself, open it. Press **Mark as key** to make this time a key meeting, or **Key every time** for every time it happens. On a meeting that is key already, press **Not key** or **Never key**. Your mark is used before what the calendar says. Press **Remove my mark** to go back to what the calendar says. The marks are kept in `calendar-marks.json` in the state folder. **Before this meeting** lists the prep. **Last time** is folded: open it to read the notes and what stayed open.
|
|
54
56
|
|
|
55
57
|
The month comes from the calendar memory. The morning-brief session reads your calendar 90 days back and 31 days ahead the first time, and from today to 31 days ahead after that, and sends it with the brief. The workspace keeps 100 days back and 45 days ahead, in `calendar-memory.json` in the state folder. Nothing on the Agenda writes to your calendar. To see what the memory holds, run `pmos proactive calendar`.
|
|
56
58
|
|
|
@@ -66,7 +68,13 @@ To tie an item to a repository:
|
|
|
66
68
|
1. For a goal, open it and pick a **Repository**. A goal you add while Today shows one repository belongs to that repository.
|
|
67
69
|
2. For a Slack channel, Jira project or Confluence space, open **Sources**, open the service, and pick a repository next to the place.
|
|
68
70
|
|
|
69
|
-
A job takes the repository of its goal. A job with no goal takes the repository of the place it came from. A job with
|
|
71
|
+
A job takes the repository of its goal. A job with no goal takes the repository of the initiative it names, then of the place it came from. A goal tied to an initiative takes that initiative's repository when you pick none. A job with none of these belongs to no repository.
|
|
72
|
+
|
|
73
|
+
When **Repository** hides a goal of another repository, the goals say how many it hides. Press **Show all repositories** to see them.
|
|
74
|
+
|
|
75
|
+
A job's agent session runs in the folder of the job's repository, so it can read that repository's files. A job with no repository runs in the repository where you pressed **Prepare my day**.
|
|
76
|
+
|
|
77
|
+
Your goals stay until you mark them done or drop them. A new day starts with the goals that were still open on the last day you had goals.
|
|
70
78
|
|
|
71
79
|
Each job has one of three kinds.
|
|
72
80
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* mergeMemory(stored, memory, { now }) → { ok, memory } the stored file with the submitted window replaced
|
|
6
6
|
* seriesKey(e) · normTitle(title) → the key that joins the occurrences of one meeting
|
|
7
7
|
* kindOf(e) · isImportant(e) → 'one' | 'onboard' | 'group' · true | false
|
|
8
|
-
* agendaMonth({ memory, brief, month, now })
|
|
8
|
+
* agendaMonth({ memory, brief, month, marks, now }) → the month the Agenda draws: weeks, days with their three answers, series
|
|
9
|
+
* readMarks(raw) · setMark(marks, { title, start, scope, on }) · markOf(marks, e) → the PM's key-meeting marks
|
|
9
10
|
*
|
|
10
11
|
* The morning-brief session reads the calendar wide (90 days back, 31 ahead) and submits it with the brief. The workspace
|
|
11
12
|
* checks it here and keeps it in one file. Every answer the month gives is derived here, with no model and no I/O:
|
|
@@ -62,6 +63,51 @@ function isImportant(e) {
|
|
|
62
63
|
return !!e && (e.role === 'present' || e.role === 'decide' || e.external === true || e.exec === true || (typeof e.attendees === 'number' && e.attendees >= 8));
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/* ── the PM's own marks ───────────────────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
* The PM can mark a meeting as key, or as not key, over what isImportant derives: this one time, or every time it
|
|
68
|
+
* happens. A mark is keyed by the title (normTitle), and a time mark also by the start minute, so it holds whether the
|
|
69
|
+
* meeting comes from the calendar memory or from today's brief. */
|
|
70
|
+
const MAX_MARKS = 500;
|
|
71
|
+
const seriesMark = (title) => 't:' + normTitle(title);
|
|
72
|
+
const timeMark = (title, start) => { const t = Date.parse(start); return Number.isFinite(t) ? seriesMark(title) + '@' + new Date(t).toISOString().slice(0, 16) : null; };
|
|
73
|
+
const emptyMarks = () => ({ one: {}, series: {} });
|
|
74
|
+
|
|
75
|
+
/** The marks file, checked: anything else reads as no marks. */
|
|
76
|
+
function readMarks(raw) {
|
|
77
|
+
const out = emptyMarks();
|
|
78
|
+
if (!raw || typeof raw !== 'object') return out;
|
|
79
|
+
for (const k of ['one', 'series']) {
|
|
80
|
+
const m = raw[k] && typeof raw[k] === 'object' && !Array.isArray(raw[k]) ? raw[k] : {};
|
|
81
|
+
for (const [key, v] of Object.entries(m).slice(0, MAX_MARKS)) if (typeof v === 'boolean' && key.length <= 300 && key.startsWith('t:')) out[k][key] = v;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One mark set or cleared. `scope` 'one' (this time) or 'series' (every time); `on` true (key), false (not key) or null
|
|
87
|
+
* (clear, back to what the calendar says). A mark for every time also clears this time's mark. */
|
|
88
|
+
function setMark(marks, { title, start, scope, on } = {}) {
|
|
89
|
+
if (!str(title, 300)) return { ok: false, error: 'a mark needs the meeting title' };
|
|
90
|
+
if (!['one', 'series'].includes(scope)) return { ok: false, error: "scope must be 'one' or 'series'" };
|
|
91
|
+
if (!(on === true || on === false || on === null)) return { ok: false, error: 'on must be true, false or null' };
|
|
92
|
+
const key = scope === 'one' ? timeMark(title, start) : seriesMark(title);
|
|
93
|
+
if (!key || key === 't:') return { ok: false, error: 'a mark needs a title with letters and, for one time, its start' };
|
|
94
|
+
const next = readMarks(marks);
|
|
95
|
+
if (on === null) delete next[scope][key]; else next[scope][key] = on;
|
|
96
|
+
if (scope === 'series') { const t = timeMark(title, start); if (t) delete next.one[t]; }
|
|
97
|
+
if (Object.keys(next[scope]).length > MAX_MARKS) return { ok: false, error: 'too many marks — clear some first' };
|
|
98
|
+
return { ok: true, marks: next };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The PM's mark of a meeting: { on, scope } or null when there is none. A time mark wins over an every-time mark. */
|
|
102
|
+
function markOf(marks, e) {
|
|
103
|
+
if (!marks || !e) return null;
|
|
104
|
+
const t = timeMark(e.title, e.start);
|
|
105
|
+
if (t && marks.one && typeof marks.one[t] === 'boolean') return { on: marks.one[t], scope: 'one' };
|
|
106
|
+
const s = seriesMark(e.title);
|
|
107
|
+
if (marks.series && typeof marks.series[s] === 'boolean') return { on: marks.series[s], scope: 'series' };
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
65
111
|
/* ── the check ─────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
66
112
|
|
|
67
113
|
function validateMemory(raw, { now = Date.now() } = {}) {
|
|
@@ -166,14 +212,16 @@ function fromBrief(e) {
|
|
|
166
212
|
* The month the Agenda draws. `month` is YYYY-MM (default: this month). Weeks run Monday to Sunday. Each day of the month
|
|
167
213
|
* carries its events and the three answers; `series` describes every series that has an event in the month.
|
|
168
214
|
*/
|
|
169
|
-
function agendaMonth({ memory, brief, month, now = Date.now() } = {}) {
|
|
215
|
+
function agendaMonth({ memory, brief, month, marks = null, now = Date.now() } = {}) {
|
|
170
216
|
const today = localDate(now);
|
|
171
217
|
const ym = typeof month === 'string' && /^\d{4}-\d{2}$/.test(month) ? month : today.slice(0, 7);
|
|
172
218
|
const all = ((memory && memory.events) || []).map((e) => ({ ...e, src: 'memory' }));
|
|
173
219
|
// today's events come from the brief of today when there is one; memory events of today are then left out
|
|
174
220
|
const b = brief && brief.date === today && Array.isArray(brief.calendar) ? brief.calendar.map(fromBrief) : null;
|
|
221
|
+
// key: the PM's mark when there is one, else what the calendar says (isImportant)
|
|
222
|
+
const impOf = (e) => { const auto = isImportant(e); const mk = markOf(marks, e); return { imp: mk ? mk.on : auto, auto, mark: mk ? mk.scope : null }; };
|
|
175
223
|
const events = (b ? [...all.filter((e) => localDate(Date.parse(e.start)) !== today), ...b] : all)
|
|
176
|
-
.map((e) => { const s = Date.parse(e.start); const f = Date.parse(e.end); return { ...e, s, f, date: localDate(s), from: minuteOfDay(s), to: f - s >= DAY_MS ? DAY_END : (localDate(f) === localDate(s) ? minuteOfDay(f) : DAY_END), key: seriesKey(e), kind: kindOf(e),
|
|
224
|
+
.map((e) => { const s = Date.parse(e.start); const f = Date.parse(e.end); return { ...e, s, f, date: localDate(s), from: minuteOfDay(s), to: f - s >= DAY_MS ? DAY_END : (localDate(f) === localDate(s) ? minuteOfDay(f) : DAY_END), key: seriesKey(e), kind: kindOf(e), ...impOf(e) }; })
|
|
177
225
|
.sort((x, y) => x.s - y.s);
|
|
178
226
|
|
|
179
227
|
// series over everything remembered, so "held 12 of 13" and "the last time" see beyond the month
|
|
@@ -227,7 +275,7 @@ function agendaMonth({ memory, brief, month, now = Date.now() } = {}) {
|
|
|
227
275
|
if (lv === 'risk' || (lv === 'warn' && level !== 'risk') || (lv === 'ok' && !level)) level = lv;
|
|
228
276
|
const x = series.get(e.key);
|
|
229
277
|
return { id: e.id, src: e.src, title: e.title, start: e.start, end: e.end, time: `${pad(Math.floor(e.from / 60))}:${pad(e.from % 60)}`, minutes: Math.round((e.f - e.s) / 60000),
|
|
230
|
-
kind: e.kind, imp: e.imp, key: e.key, repeats: x.count > 1, routine: routine(e), ended, prep: p ? e.prep : null, prep_done: p ? pd : null, ready: lv,
|
|
278
|
+
kind: e.kind, imp: e.imp, auto: e.auto, mark: e.mark, key: e.key, repeats: x.count > 1, routine: routine(e), ended, prep: p ? e.prep : null, prep_done: p ? pd : null, ready: lv,
|
|
231
279
|
notes: e.notes || null, open: e.open || null, role: e.role || null, external: e.external === true, attendees: typeof e.attendees === 'number' ? e.attendees : null, with: e.with || null };
|
|
232
280
|
});
|
|
233
281
|
if (need && done >= need) level = 'ok';
|
|
@@ -253,6 +301,6 @@ function agendaMonth({ memory, brief, month, now = Date.now() } = {}) {
|
|
|
253
301
|
}
|
|
254
302
|
|
|
255
303
|
module.exports = {
|
|
256
|
-
validateMemory, mergeMemory, agendaMonth, seriesKey, normTitle, kindOf, isImportant, dayLoad,
|
|
304
|
+
validateMemory, mergeMemory, agendaMonth, seriesKey, normTitle, kindOf, isImportant, dayLoad, readMarks, setMark, markOf,
|
|
257
305
|
MAX_EVENTS, MAX_SPAN_DAYS, KEEP_BACK_DAYS, KEEP_AHEAD_DAYS, FREE_BLOCK, PER_STEP,
|
|
258
306
|
};
|
package/workspace/lib/goals.js
CHANGED
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
* | { action: 'tag', id, repo } (repo: a workspace id, or null for none)
|
|
12
12
|
* readDay(raw, date) → a checked day record, or an empty day when `raw` is not one
|
|
13
13
|
* planDay({ day, brief, cards, now, endOfDay }) → the plan Today and `pmos proactive goals` show
|
|
14
|
-
* itemRepos({ day, brief, placeRepos })
|
|
14
|
+
* itemRepos({ day, brief, placeRepos, iniRepos }) → { itemId: repo } for the brief items that have a repository
|
|
15
|
+
* goalRepos({ day, iniRepos }) → { goalId: repo } for the untagged goals whose initiative names one
|
|
16
|
+
* carryOver(prev, date) → a new day holding the goals still open on `prev`
|
|
15
17
|
*
|
|
16
18
|
* ONE DAY, REPOSITORY TAGS. The day is the PM's, not a repository's: every repository shows the same goals, brief and
|
|
17
19
|
* jobs. A goal can carry a repository (`repo`, a workspace id), and so can a channel, project or space (the settings'
|
|
18
20
|
* `placeRepos`). An item takes the repository of the open goal it is under; failing that, of the place it came from
|
|
19
|
-
* (its `where`). An
|
|
21
|
+
* (its `where`). An initiative held by one repository tags the items that name it, and the goals tied to it. An item
|
|
22
|
+
* with none of these is personal and shows under every repository.
|
|
20
23
|
*
|
|
21
24
|
* THE JOIN IS CHECKABLE. A brief item or calendar event belongs to a goal when it names the goal's id (the morning-brief
|
|
22
25
|
* session sets it). Failing that, an item or Today card belongs to the first open goal tied to the same initiative. There
|
|
@@ -56,6 +59,13 @@ function readDay(raw, date) {
|
|
|
56
59
|
return { date, goals };
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
/** A new day starts with the goals still open on the last day that had any: a goal is the PM's until it is done or
|
|
63
|
+
* dropped, not until midnight. `prev` is that day's record (readDay); a day with its own file never takes these. */
|
|
64
|
+
function carryOver(prev, date) {
|
|
65
|
+
if (!prev || !Array.isArray(prev.goals) || !prev.date || prev.date >= date) return emptyDay(date);
|
|
66
|
+
return { date, goals: prev.goals.filter((g) => g.status === 'open').slice(0, MAX_GOALS).map((g) => ({ ...g, closed_at: null })) };
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
/** One edit of the day's goals. A refused edit returns the reason and the caller writes nothing. */
|
|
60
70
|
function edit(day, op, { now = Date.now() } = {}) {
|
|
61
71
|
if (!day || !DATE_RE.test(String(day.date)) || !Array.isArray(day.goals)) return { ok: false, error: 'no day record to edit' };
|
|
@@ -205,8 +215,9 @@ function planDay({ day, brief = null, cards = [], now = Date.now(), endOfDay = E
|
|
|
205
215
|
const placeKey = (s) => { const t = String(s || '').trim().toLowerCase().replace(/^#/, ''); const k = /^([a-z][a-z0-9]*)-\d+$/.exec(t); return k ? k[1] : t; };
|
|
206
216
|
const placeNameKeys = (n) => { const t = String(n || '').trim().toLowerCase().replace(/^#/, ''); return [t, t.split(/[\s·:|]+/)[0]].filter(Boolean); };
|
|
207
217
|
|
|
208
|
-
/** { itemId: repo } for today's brief items: the repository of the open goal an item is under, else of
|
|
209
|
-
|
|
218
|
+
/** { itemId: repo } for today's brief items: the repository of the open goal an item is under, else of the initiative
|
|
219
|
+
* it names (`iniRepos`: { initiativeId: repo }, only the ids one repository holds), else of its place. */
|
|
220
|
+
function itemRepos({ day, brief, placeRepos = {}, iniRepos = {} } = {}) {
|
|
210
221
|
const out = {};
|
|
211
222
|
if (!brief || !day || brief.date !== day.date) return out;
|
|
212
223
|
const open = (day.goals || []).filter((g) => g.status === 'open');
|
|
@@ -214,7 +225,8 @@ function itemRepos({ day, brief, placeRepos = {} } = {}) {
|
|
|
214
225
|
const byIni = (ini) => (ini ? open.find((g) => g.initiative_id === ini) || null : null);
|
|
215
226
|
for (const x of [...(brief.priorities || []), ...(brief.worth_asking || [])]) {
|
|
216
227
|
const g = (x.goal && byId.get(x.goal)) || byIni(x.initiative_id);
|
|
217
|
-
let r = g ? repoOrNull(g.repo) : null;
|
|
228
|
+
let r = g ? repoOrNull(g.repo) || (g.initiative_id ? repoOrNull(iniRepos[g.initiative_id]) : null) : null;
|
|
229
|
+
if (!r && x.initiative_id) r = repoOrNull(iniRepos[x.initiative_id]);
|
|
218
230
|
if (!r && x.where) {
|
|
219
231
|
const want = placeKey(x.where);
|
|
220
232
|
for (const [name, repo] of Object.entries(placeRepos[x.source] || {})) if (placeNameKeys(name).includes(want) && repoOrNull(repo)) { r = repo; break; }
|
|
@@ -224,4 +236,11 @@ function itemRepos({ day, brief, placeRepos = {} } = {}) {
|
|
|
224
236
|
return out;
|
|
225
237
|
}
|
|
226
238
|
|
|
227
|
-
|
|
239
|
+
/** { goalId: repo } for the goals with no repository of their own whose initiative one repository holds. */
|
|
240
|
+
function goalRepos({ day, iniRepos = {} } = {}) {
|
|
241
|
+
const out = {};
|
|
242
|
+
for (const g of (day && day.goals) || []) if (!g.repo && g.initiative_id && repoOrNull(iniRepos[g.initiative_id])) out[g.id] = iniRepos[g.initiative_id];
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { MAX_GOALS, MAX_TEXT, STATUSES, ACTIONS, GOAL_ID_RE, START_OF_DAY, END_OF_DAY, emptyDay, readDay, carryOver, edit, freeTime, planDay, itemRepos, goalRepos };
|
package/workspace/package.json
CHANGED
|
@@ -143,14 +143,14 @@ function build(ctx0) {
|
|
|
143
143
|
const dayWs = dayRepo ? wsOf(dayRepo) : null;
|
|
144
144
|
const prep = onPhone && !(window.pmDeck && window.pmDeck.phoneView('today') === 'day') ? prepCard(dayWs) : null;
|
|
145
145
|
if (prep) { prep.setAttribute('role', 'listitem'); const none = list.querySelector('[data-today="empty"]'); if (none) none.setAttribute('data-today-id', 'empty'); list.prepend(prep); } // the empty note stays, as the deck's last card
|
|
146
|
-
// the open day takes the
|
|
147
|
-
// leaves it below, as before
|
|
148
|
-
const owed = { list, more, count: n };
|
|
146
|
+
// the open day takes the cards into its jobs (rows of Needs you; a pressed row shows its card), so Today shows one layout; a folded day
|
|
147
|
+
// leaves it below, as before. A phone does not draw the day, so its deck keeps every card
|
|
148
|
+
const owed = onPhone ? null : { list, more, count: n };
|
|
149
149
|
// one day for every repository: the day is the same everywhere; the Show switch narrows it to the repository Today
|
|
150
150
|
// was opened from (`here`) or shows all of it
|
|
151
151
|
const here = sc.repo ? wsOf(sc.repo) : null;
|
|
152
152
|
const day = daySection({ ws: dayWs, setState: ctx0.setState || setState, openIni: dayRepo ? (id) => openInitiative(dayRepo, id) : null, owed, here, all: sc.all });
|
|
153
|
-
const inDay = !!(day &&
|
|
153
|
+
const inDay = !!(day && owed && owed.taken);
|
|
154
154
|
return { children: inDay ? [head, day, health] : [head, day, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner, onPhone, prep ? prep.outerHTML : null]) + daySig(dayWs) + nodes.map((x) => x.outerHTML).join('') };
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -915,9 +915,78 @@ function buildReal(tab) {
|
|
|
915
915
|
rec.ro = new ResizeObserver(() => refitVisible());
|
|
916
916
|
try { rec.ro.observe(host); } catch { /* ignore */ }
|
|
917
917
|
}
|
|
918
|
+
touchScroll(rec);
|
|
918
919
|
return rec;
|
|
919
920
|
}
|
|
920
921
|
|
|
922
|
+
/* ── a swipe scrolls ───────────────────────────────────────────────────────────────────────────
|
|
923
|
+
* xterm scrolls on a touch drag only while no program has asked for the mouse. A program that has
|
|
924
|
+
* (Claude Code, tmux, an editor) gets nothing from a swipe, because xterm ignores touch then and a
|
|
925
|
+
* touch is not a wheel. On an iPhone or iPad a swipe was therefore dead in exactly the sessions
|
|
926
|
+
* that matter. A vertical drag on the pane is turned into wheel events instead, and xterm does with
|
|
927
|
+
* them what it does with a mouse wheel: it scrolls its own scrollback, or it reports the wheel to
|
|
928
|
+
* the program that asked for the mouse, or it sends arrow keys in a full-screen program that did
|
|
929
|
+
* not. The finger's speed carries on for a moment after it lifts, as a native scroll does. A tap
|
|
930
|
+
* is left alone, so it still focuses the terminal and raises the keyboard. */
|
|
931
|
+
const TOUCH_SLOP = 6; // px a finger moves before a touch is a swipe rather than a tap
|
|
932
|
+
const MOMENTUM_DECAY = 0.95; // the carried speed kept per frame after the finger lifts
|
|
933
|
+
|
|
934
|
+
function touchScroll(rec) {
|
|
935
|
+
const { host, term } = rec;
|
|
936
|
+
let t = null; // the swipe in progress: { x, y, lastY, lastT, v, swiping }
|
|
937
|
+
let glide = 0; // the momentum frame, if one is queued
|
|
938
|
+
|
|
939
|
+
const wheel = (dy, x, y) => {
|
|
940
|
+
const target = (term.element && term.element.querySelector('.xterm-screen')) || term.element;
|
|
941
|
+
if (!target || !dy) return;
|
|
942
|
+
target.dispatchEvent(new WheelEvent('wheel', {
|
|
943
|
+
deltaY: dy, deltaMode: 0, clientX: x, clientY: y, bubbles: true, cancelable: true,
|
|
944
|
+
}));
|
|
945
|
+
};
|
|
946
|
+
const stopGlide = () => { if (glide) { cancelAnimationFrame(glide); glide = 0; } };
|
|
947
|
+
|
|
948
|
+
host.addEventListener('touchstart', (e) => {
|
|
949
|
+
stopGlide();
|
|
950
|
+
if (e.touches.length !== 1) { t = null; return; }
|
|
951
|
+
const p = e.touches[0];
|
|
952
|
+
t = { x: p.clientX, y: p.clientY, lastY: p.clientY, lastT: e.timeStamp, v: 0, swiping: false };
|
|
953
|
+
}, { capture: true, passive: true });
|
|
954
|
+
|
|
955
|
+
host.addEventListener('touchmove', (e) => {
|
|
956
|
+
if (!t || e.touches.length !== 1) return;
|
|
957
|
+
const p = e.touches[0];
|
|
958
|
+
if (!t.swiping) {
|
|
959
|
+
const dx = p.clientX - t.x, dy = p.clientY - t.y;
|
|
960
|
+
if (Math.abs(dy) < TOUCH_SLOP || Math.abs(dy) < Math.abs(dx)) return;
|
|
961
|
+
t.swiping = true;
|
|
962
|
+
}
|
|
963
|
+
// Ours from here: xterm's own touch handling and the page's scroll do not also see it.
|
|
964
|
+
e.preventDefault();
|
|
965
|
+
e.stopPropagation();
|
|
966
|
+
const dy = t.lastY - p.clientY; // finger up = content up = wheel down
|
|
967
|
+
const dt = Math.max(1, e.timeStamp - t.lastT);
|
|
968
|
+
t.v = 0.8 * (dy / dt) + 0.2 * t.v;
|
|
969
|
+
t.lastY = p.clientY; t.lastT = e.timeStamp;
|
|
970
|
+
wheel(dy, p.clientX, p.clientY);
|
|
971
|
+
}, { capture: true, passive: false });
|
|
972
|
+
|
|
973
|
+
const end = () => {
|
|
974
|
+
if (!t || !t.swiping) { t = null; return; }
|
|
975
|
+
let v = t.v * 16; // px per frame
|
|
976
|
+
const x = t.x, y = t.lastY;
|
|
977
|
+
t = null;
|
|
978
|
+
const step = () => {
|
|
979
|
+
v *= MOMENTUM_DECAY;
|
|
980
|
+
if (Math.abs(v) < 0.5) { glide = 0; return; }
|
|
981
|
+
wheel(v, x, y);
|
|
982
|
+
glide = requestAnimationFrame(step);
|
|
983
|
+
};
|
|
984
|
+
if (Math.abs(v) >= 0.5) glide = requestAnimationFrame(step);
|
|
985
|
+
};
|
|
986
|
+
host.addEventListener('touchend', end, { capture: true, passive: true });
|
|
987
|
+
host.addEventListener('touchcancel', end, { capture: true, passive: true });
|
|
988
|
+
}
|
|
989
|
+
|
|
921
990
|
/* ── the exported surface ──────────────────────────────────────────────────────────────────── */
|
|
922
991
|
|
|
923
992
|
/**
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
* One read feeds it: GET /api/proactive/agenda?month=YYYY-MM. The server derives every answer (how full, the key
|
|
11
11
|
* meeting, readiness, no notes); this file only draws them. A day cell is a button: it picks that day. A week number
|
|
12
12
|
* picks the week. The lens switch says which answer the cells show; the agenda under the month always shows them all.
|
|
13
|
-
*
|
|
13
|
+
* The calendar memory changes only through the morning-brief session's brief. The one write here is the PM's key-meeting
|
|
14
|
+
* mark (POST /api/proactive/agenda/mark): this time or every time, key or not key, over what the calendar says.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { peopleLinks } from './people-link.js';
|
|
17
18
|
import { el } from './dom.js';
|
|
18
|
-
import { button, iconButton, badge, tabs } from './primitives.js';
|
|
19
|
-
import { pmGet } from './http.js';
|
|
19
|
+
import { button, iconButton, badge, tabs, toast } from './primitives.js';
|
|
20
|
+
import { pmGet, pmPost } from './http.js';
|
|
20
21
|
import { COPY } from './today-copy.js';
|
|
21
22
|
|
|
22
23
|
const A = COPY.day.ag;
|
|
@@ -74,10 +75,12 @@ function current() {
|
|
|
74
75
|
return c;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
// a short hash of the days, so a change that keeps the length (a key mark set or cleared) still redraws
|
|
79
|
+
const hash = (t) => { let h = 0; for (let i = 0; i < t.length; i++) h = (h * 31 + t.charCodeAt(i)) | 0; return h; };
|
|
77
80
|
export function agendaSig() {
|
|
78
81
|
const c = ag.month ? cache.get(ag.month) : cache.get('now');
|
|
79
82
|
return JSON.stringify([ag, c ? [c.error, c.data && c.data.now ? String(c.data.now).slice(0, 16) : null, c.data && Object.keys(c.data.days || {}).length, c.data && c.data.memory] : null])
|
|
80
|
-
+ (c && c.data ? JSON.stringify(c.data.days)
|
|
83
|
+
+ (c && c.data ? hash(JSON.stringify(c.data.days)) : 0);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/* ── finding a meeting across the months read ─────────────────────────────────────────────────────────────────── */
|
|
@@ -270,6 +273,26 @@ function whyLines(e) {
|
|
|
270
273
|
return out;
|
|
271
274
|
}
|
|
272
275
|
|
|
276
|
+
async function setMark(e, scope, on) {
|
|
277
|
+
const r = await pmPost('/api/proactive/agenda/mark', { title: e.title, start: e.start, scope, on });
|
|
278
|
+
if (!r || r.ok === false) { toast({ text: A.key.failed((r && r.error) || 'the workspace did not answer'), tone: 'destructive' }); return; }
|
|
279
|
+
for (const k of [...cache.keys()]) loadMonth(k === 'now' ? undefined : k);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The key-meeting line of a meeting: what the PM marked, and the presses that change it. */
|
|
283
|
+
function keyBlock(e) {
|
|
284
|
+
const K = A.key;
|
|
285
|
+
const b = (label, scope, on, id) => button({ label, variant: 'outline', size: 'sm', attrs: { 'data-ag-mark': id }, onClick: () => setMark(e, scope, on) });
|
|
286
|
+
const presses = e.mark ? [b(K.clear, e.mark, null, 'clear')]
|
|
287
|
+
: e.imp ? [b(K.unmark, 'one', false, 'not-one'), e.repeats ? b(K.unmarkAll, 'series', false, 'not-series') : null]
|
|
288
|
+
: [b(K.mark, 'one', true, 'one'), e.repeats ? b(K.markAll, 'series', true, 'series') : null];
|
|
289
|
+
const said = e.mark ? K[(e.imp ? 'marked' : 'not') + e.mark] : null;
|
|
290
|
+
return el('div', { class: 'td-day__phase', 'data-ag': 'key', 'data-ag-key': e.imp ? 'true' : 'false' },
|
|
291
|
+
e.imp ? el('h4', null, A.star + ' ' + K.title) : null,
|
|
292
|
+
said ? el('p', { class: 'td-day__note' }, said) : null,
|
|
293
|
+
el('div', { class: 'td-day__acts' }, ...presses));
|
|
294
|
+
}
|
|
295
|
+
|
|
273
296
|
/** The series of a meeting: how often it met, what is to come, and the last time, folded. Null when unknown. */
|
|
274
297
|
export function memoryBlock(id) {
|
|
275
298
|
const f = findEvent(id);
|
|
@@ -280,7 +303,8 @@ export function memoryBlock(id) {
|
|
|
280
303
|
return el('div', { class: 'td-day__notes', 'data-ag': 'memory' },
|
|
281
304
|
el('h4', null, A.means),
|
|
282
305
|
el('p', null, A.kindWord[f.e.kind] + '. ' + (s.count > 1 ? A.series(s.past, s.coming) : A.once)),
|
|
283
|
-
...whyLines(f.e).map((t) => el('p', null, t)),
|
|
306
|
+
...(f.e.mark ? [] : whyLines(f.e)).map((t) => el('p', null, t)),
|
|
307
|
+
keyBlock(f.e),
|
|
284
308
|
last ? el('details', { class: 'td-ag__last', 'data-ag': 'last' }, el('summary', null, A.lastTime(dayLabel(last.date))),
|
|
285
309
|
last.notes ? el('p', null, last.notes) : el('p', { class: 'td-day__note' }, A.noLast),
|
|
286
310
|
last.open && last.open.length ? el('ul', { class: 'td-day__chk' }, ...last.open.map((x) => el('li', null, x))) : null) : null);
|
|
@@ -136,16 +136,17 @@ export const COPY = {
|
|
|
136
136
|
unreached: 'The workspace did not answer.',
|
|
137
137
|
refused: (why) => `Nothing changed. ${why}`,
|
|
138
138
|
oldServer: 'This workspace server has no day run yet. Only your goals show.',
|
|
139
|
-
panels: { summary: 'Agent results', calendar: 'Calendar', agenda: 'Agenda', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday',
|
|
139
|
+
panels: { summary: 'Agent results', calendar: 'Calendar', agenda: 'Agenda', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', detail: 'Detail' },
|
|
140
140
|
summary: (c) => [c.review ? `${c.review} ${c.review === 1 ? 'draft waits' : 'drafts wait'} for you` : null, c.done ? `${c.done} done` : null,
|
|
141
141
|
c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
|
|
142
142
|
summaryAll: (n) => `All ${n} agent ${n === 1 ? 'job has' : 'jobs have'} a result.`,
|
|
143
|
-
waitingHint: 'Outcome checks, reports and sessions from your repositories, outside today\'s jobs.',
|
|
144
143
|
// one day for every repository: a goal or a place can carry a repository; the Show switch narrows the day to it
|
|
145
144
|
repo: { label: 'Repository', none: 'No repository',
|
|
146
145
|
goalHint: 'Its jobs show under this repository. With no repository, the goal is yours and shows under every repository.',
|
|
147
146
|
placeHint: 'Repository: its mentions and jobs show under that repository. With none, they show under every repository.',
|
|
148
|
-
only: (name) => `Showing ${name} and your items with no repository. The calendar always shows
|
|
147
|
+
only: (name) => `Showing ${name} and your items with no repository. The calendar always shows.`,
|
|
148
|
+
hiddenGoals: (n) => `${n} ${n === 1 ? 'goal belongs' : 'goals belong'} to another repository.`,
|
|
149
|
+
showAll: 'Show all repositories' },
|
|
149
150
|
now: (at) => `now ${at}`,
|
|
150
151
|
noCalendar: 'No events read for today.',
|
|
151
152
|
noJobs: 'No jobs yet. Press Prepare my day. Each job shows here; select one to see what it needs.',
|
|
@@ -329,6 +330,13 @@ export const COPY = {
|
|
|
329
330
|
kindWord: { one: 'One-to-one', onboard: 'Onboarding', group: 'Group meeting' },
|
|
330
331
|
why: { present: 'You present.', decide: 'You decide.', external: 'People from outside the company join.', exec: 'Leadership joins.', many: (n) => `${n} people join.`, routine: 'It repeats, and nothing in it is for you.' },
|
|
331
332
|
means: 'What it means',
|
|
333
|
+
key: {
|
|
334
|
+
title: 'Key meeting',
|
|
335
|
+
markedone: 'You marked this time as key.', markedseries: 'You marked it as key every time.',
|
|
336
|
+
notone: 'You marked this time as not key.', notseries: 'You marked it as not key every time.',
|
|
337
|
+
mark: 'Mark as key', markAll: 'Key every time', unmark: 'Not key', unmarkAll: 'Never key', clear: 'Remove my mark',
|
|
338
|
+
failed: (why) => `The mark was not saved: ${why}`,
|
|
339
|
+
},
|
|
332
340
|
before: 'Before this meeting',
|
|
333
341
|
prepLine: (done, need) => `${done} of ${need} prep steps done.`,
|
|
334
342
|
readyNote: { ok: 'Your prep is done.', warn: 'Some prep is left, and there is time for it.', risk: 'The free time before it is short for the prep left.' },
|
|
@@ -142,7 +142,9 @@ function model(d, view) {
|
|
|
142
142
|
const today = d && d.today;
|
|
143
143
|
const brief = d && d.brief && (!today || d.brief.date === today) ? d.brief : null;
|
|
144
144
|
const plan = (d && d.plan) || null;
|
|
145
|
-
|
|
145
|
+
// a goal with no repository of its own takes the one its initiative is in (goal_repos, from the server)
|
|
146
|
+
const goalRepos = (d && d.goal_repos) || {};
|
|
147
|
+
const goals = (plan ? plan.goals || [] : []).map((g) => (g.repo || !goalRepos[g.id] ? g : { ...g, repo: goalRepos[g.id] }));
|
|
146
148
|
const goalNo = new Map(goals.map((g, i) => [g.id, i + 1]));
|
|
147
149
|
const run = (d && d.run) || null;
|
|
148
150
|
const tasks = run && Array.isArray(run.tasks) ? run.tasks : [];
|
|
@@ -178,7 +180,7 @@ function model(d, view) {
|
|
|
178
180
|
const shownIds = new Set(shownJobs.map((j) => j.task && j.task.id).filter(Boolean));
|
|
179
181
|
const shownTasks = narrow ? tasks.filter((t) => shownIds.has(t.id)) : tasks;
|
|
180
182
|
return {
|
|
181
|
-
d, brief, plan, goals: goals.filter((g) => inView(g.repo)), allGoals: goals, goalNo, run, tasks: shownTasks, jobs: shownJobs, events, now, settings, scopes, level,
|
|
183
|
+
d, brief, plan, goals: goals.filter((g) => inView(g.repo)), allGoals: goals, hiddenGoals: goals.filter((g) => !inView(g.repo)).length, goalNo, run, tasks: shownTasks, jobs: shownJobs, events, now, settings, scopes, level,
|
|
182
184
|
digests: digests.filter((g) => inView(g.repo)), places, placeRepo, repos, repoName, here: v.here || null, narrow,
|
|
183
185
|
stage: run ? (d.stage || deriveStage(run, brief)) : null,
|
|
184
186
|
mentions: shownJobs.filter((j) => j.item && j.item.mention),
|
|
@@ -419,13 +421,31 @@ function jobRow(ctx, j) {
|
|
|
419
421
|
dot: j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null });
|
|
420
422
|
}
|
|
421
423
|
|
|
424
|
+
/* What the repositories owe the PM outside today's jobs (outcome checks, report drafts, sessions) are jobs too: each one is a
|
|
425
|
+
* row of Needs you, and pressing it shows its card, with the card's own buttons, in the detail column. The Agenda has no
|
|
426
|
+
* separate section for them. */
|
|
427
|
+
function owedItems(owed) {
|
|
428
|
+
if (!owed || !owed.list) return [];
|
|
429
|
+
return [...owed.list.children].filter((n) => n.getAttribute && n.getAttribute('data-today-id')).map((n) => {
|
|
430
|
+
const cause = n.querySelector('[data-today-cause]');
|
|
431
|
+
const ev = n.querySelector('.td-card__evidence');
|
|
432
|
+
const text = ((cause || n).textContent || '').trim().replace(/\s+/g, ' ');
|
|
433
|
+
return { id: n.getAttribute('data-today-id'), node: n, title: text, meta: ev ? ev.textContent.trim().replace(/\s+/g, ' ') : '' };
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function owedRow(ctx, o) {
|
|
438
|
+
return rowBtn(ctx, 'owed', o.id, { lead: '·', title: o.title, meta: o.meta, trail: pill(D.status2.you, 'warn') });
|
|
439
|
+
}
|
|
440
|
+
|
|
422
441
|
function jobsPanel(ctx) {
|
|
423
442
|
const { m } = ctx;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
el('
|
|
443
|
+
const owed = ctx.owed;
|
|
444
|
+
if (!m.jobs.length && !owed.length) return panel('jobs', null, note(m.run && m.stage === 'reading' ? D.readingJobs : D.noJobs, { 'data-day': 'no-jobs' }));
|
|
445
|
+
const groups = ['needs', 'working', 'done', 'worth'].map((g) => [g, m.jobs.filter((j) => j.group === g)]).filter(([g, l]) => l.length || (g === 'needs' && owed.length));
|
|
446
|
+
return panel('jobs', D.jobsCount(m.needs + owed.length, m.done), ...groups.map(([g, l]) => el('div', { class: 'td-day__group', 'data-day-group': g },
|
|
447
|
+
el('div', { class: 'td-day__gh' }, D.groups[g], pill(String(l.length + (g === 'needs' ? owed.length : 0)), '')),
|
|
448
|
+
el('ul', { class: 'td-day__rows', 'data-day-list': 'jobs-' + g }, ...l.map((j) => jobRow(ctx, j)), ...(g === 'needs' ? owed.map((o) => owedRow(ctx, o)) : [])))));
|
|
429
449
|
}
|
|
430
450
|
|
|
431
451
|
/* The coordinator's view of the run: what every agent job came back with, in one place. The server runs the jobs as one
|
|
@@ -455,14 +475,12 @@ function summaryPanel(ctx) {
|
|
|
455
475
|
|
|
456
476
|
/* What waits for the PM outside today's jobs (the Today cards: outcome checks, reports, sessions). Today hands its list
|
|
457
477
|
* in, so the cards keep their own controls and the open day shows one layout. */
|
|
458
|
-
function waitingPanel(owed) {
|
|
459
|
-
if (!owed || !owed.list) return null;
|
|
460
|
-
return panel('waiting', owed.count ? String(owed.count) : null, note(D.waitingHint), owed.list, owed.more || null);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
478
|
function goalsPanel(ctx) {
|
|
464
479
|
const { m } = ctx;
|
|
465
|
-
|
|
480
|
+
// a goal of another repository is not lost: the panel says how many the Show switch hides, with the way to them
|
|
481
|
+
const hidden = m.hiddenGoals ? el('p', { class: 'td-day__note', 'data-day': 'goals-hidden' }, D.repo.hiddenGoals(m.hiddenGoals) + ' ',
|
|
482
|
+
button({ label: D.repo.showAll, variant: 'ghost', size: 'sm', attrs: { 'data-day': 'show-all' }, onClick: () => ctx.showAll() })) : null;
|
|
483
|
+
return panel('goals', D.goalsCount(m.allGoals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), hidden, goalAdd(ctx));
|
|
466
484
|
}
|
|
467
485
|
|
|
468
486
|
function mentionStatus(j) {
|
|
@@ -778,9 +796,10 @@ function sourcesDetail(ctx) {
|
|
|
778
796
|
return [dhead(D.kind.settings, D.sources), note(D.sourcesIntro), box, el('div', { class: 'td-day__slist' }, ...rows)];
|
|
779
797
|
}
|
|
780
798
|
|
|
781
|
-
function defaultSel(m) {
|
|
799
|
+
function defaultSel(m, owed) {
|
|
782
800
|
const j = m.jobs.find((x) => x.group === 'needs');
|
|
783
801
|
if (j) return { type: 'job', id: j.id };
|
|
802
|
+
if (owed && owed.length) return { type: 'owed', id: owed[0].id };
|
|
784
803
|
const e = m.events.find((x) => !x.ended);
|
|
785
804
|
if (e) return { type: 'event', id: e.id };
|
|
786
805
|
return { type: 'step', id: String(Math.max(0, Math.min(3, stepIndex(m.stage)))) };
|
|
@@ -799,10 +818,11 @@ function detail(ctx) {
|
|
|
799
818
|
if (s.type === 'step') return stepDetail(ctx, Math.max(0, Math.min(3, Number(s.id) || 0)));
|
|
800
819
|
if (s.type === 'sources') return sourcesDetail(ctx);
|
|
801
820
|
if (s.type === 'meeting') return meetingDetail(ctx, s.id, { dhead });
|
|
821
|
+
if (s.type === 'owed') { const o = ctx.owed.find((x) => x.id === s.id); if (o) o.node.hidden = false; return o ? [dhead(D.groups.needs), el('div', { class: 'td-day__owed', 'data-day': 'owed' }, o.node)] : null; }
|
|
802
822
|
return null;
|
|
803
823
|
};
|
|
804
824
|
kids = find(sel);
|
|
805
|
-
if (!kids) { sel = defaultSel(m); kids = find(sel); }
|
|
825
|
+
if (!kids) { sel = defaultSel(m, ctx.owed); kids = find(sel); }
|
|
806
826
|
return el('aside', { class: 'td-day__detail', 'aria-label': D.panels.detail, 'data-day': 'detail', 'data-day-detail': sel.type, 'data-day-detail-id': sel.id }, kids);
|
|
807
827
|
}
|
|
808
828
|
|
|
@@ -847,9 +867,13 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
847
867
|
const sel = ui.sel;
|
|
848
868
|
const ctx = {
|
|
849
869
|
ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select, repoPicker,
|
|
850
|
-
|
|
870
|
+
showAll: () => { if (setState) setState({ todayScope: 'all' }); },
|
|
871
|
+
cur: (type, id) => { const s = sel || defaultSel(m, ctx.owed); return s.type === type && s.id === String(id) ? 'true' : 'false'; },
|
|
872
|
+
owed: owedItems(owed),
|
|
851
873
|
};
|
|
852
|
-
ctx.sel = sel || defaultSel(m);
|
|
874
|
+
ctx.sel = sel || defaultSel(m, ctx.owed);
|
|
875
|
+
// the open day takes what the repositories owe into its jobs: dest-today draws no second list under it
|
|
876
|
+
if (owed) owed.taken = true;
|
|
853
877
|
if (ui.scroll) {
|
|
854
878
|
ui.scroll = false;
|
|
855
879
|
// on a narrow screen the detail is under the list: bring it into view once it is on the page
|
|
@@ -866,7 +890,7 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
866
890
|
c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
|
|
867
891
|
m.d && m.d.legacy ? note(D.oldServer) : null,
|
|
868
892
|
el('div', { class: 'td-day__cols' },
|
|
869
|
-
el('div', { class: 'td-day__left' }, agenda(ctx), summaryPanel(ctx), goalsPanel(ctx), jobsPanel(ctx), mentionsPanel(ctx)
|
|
893
|
+
el('div', { class: 'td-day__left' }, agenda(ctx), summaryPanel(ctx), goalsPanel(ctx), jobsPanel(ctx), mentionsPanel(ctx)),
|
|
870
894
|
detail(ctx)));
|
|
871
895
|
}
|
|
872
896
|
|
package/workspace/server.js
CHANGED
|
@@ -3772,8 +3772,8 @@ function stallQuietTasks(run, now) {
|
|
|
3772
3772
|
return run;
|
|
3773
3773
|
}
|
|
3774
3774
|
// The skill's own body is the prompt, as fireSchedule hands it over; the line after it names this run's task and mode.
|
|
3775
|
-
function daySkillPrompt(run, skillId) {
|
|
3776
|
-
const w = getWs(run.ws);
|
|
3775
|
+
function daySkillPrompt(run, skillId, wsId) {
|
|
3776
|
+
const w = getWs(wsId || run.ws);
|
|
3777
3777
|
const task = taskById(skillId, w ? w.root : null);
|
|
3778
3778
|
if (!task || task.kind !== 'agent' || !task.prompt) return { error: `the ${skillId} skill is not installed for this repository` };
|
|
3779
3779
|
return { w, task, prompt: skillStore.retargetFiling(task.prompt, filingContract(w)) };
|
|
@@ -3785,9 +3785,22 @@ function jobSessionName(title) {
|
|
|
3785
3785
|
const cut = t.slice(0, 28); const sp = cut.lastIndexOf(' ');
|
|
3786
3786
|
return 'Job · ' + (sp > 12 ? cut.slice(0, sp) : cut).replace(/[\s,.;:·—-]+$/, '') + '…';
|
|
3787
3787
|
}
|
|
3788
|
+
/** The repository a job belongs to (its item's tag: its goal's, its initiative's or its place's repository), when that
|
|
3789
|
+
* repository is registered and has the day-task skill; else null and the job runs where Prepare my day was pressed. */
|
|
3790
|
+
function taskWsId(run, t) {
|
|
3791
|
+
try {
|
|
3792
|
+
const brief = briefOfDay(run.date);
|
|
3793
|
+
if (!brief || !t.item_id) return null;
|
|
3794
|
+
const st = proSettings();
|
|
3795
|
+
const r = goalsLib.itemRepos({ day: readGoalsDay(run.date), brief, placeRepos: st.placeRepos || {}, iniRepos: iniRepos() })[t.item_id];
|
|
3796
|
+
const w = r && r !== run.ws ? getWs(r) : null;
|
|
3797
|
+
return w && !daySkillPrompt(run, 'day-task', w.id).error ? w.id : null;
|
|
3798
|
+
} catch { return null; }
|
|
3799
|
+
}
|
|
3788
3800
|
/** Start one task session; a start that fails marks the task failed with the reason. Returns the new run. */
|
|
3789
3801
|
function startDayTask(run, t, now) {
|
|
3790
|
-
|
|
3802
|
+
// a job runs in its own repository's folder, so it reads that repository's files and records (one day, many repositories)
|
|
3803
|
+
const sk = daySkillPrompt(run, 'day-task', taskWsId(run, t));
|
|
3791
3804
|
let s = null;
|
|
3792
3805
|
const line = `This run: /day-task ${t.id} ${t.mode} — read the task first with: pmos proactive day show ${t.id}`;
|
|
3793
3806
|
// The session that did the task's last step is used again while it is open: it has the skill and the task in its
|
|
@@ -3862,6 +3875,15 @@ function dayTick() {
|
|
|
3862
3875
|
// ── end of the today day run helpers ─────────────────────────────────────────────────────────────────────────────
|
|
3863
3876
|
/* The calendar the Agenda remembers (G23): one file beside the briefs, written only by the brief route. */
|
|
3864
3877
|
const CAL_MEMORY = () => path.join(STATE, 'calendar-memory.json');
|
|
3878
|
+
// the PM's key-meeting marks (lib/calendar-memory.js setMark): a small file beside the calendar memory, written only by
|
|
3879
|
+
// POST /api/proactive/agenda/mark
|
|
3880
|
+
const CAL_MARKS = () => path.join(path.dirname(CAL_MEMORY()), 'calendar-marks.json');
|
|
3881
|
+
function readCalMarks() {
|
|
3882
|
+
try { return calMemLib.readMarks(JSON.parse(fs.readFileSync(CAL_MARKS(), 'utf8'))); } catch { return calMemLib.readMarks(null); }
|
|
3883
|
+
}
|
|
3884
|
+
function writeCalMarks(m) {
|
|
3885
|
+
try { const f = CAL_MARKS(); fs.mkdirSync(path.dirname(f), { recursive: true }); const tmp = `${f}.${process.pid}.tmp`; fs.writeFileSync(tmp, JSON.stringify(m) + '\n'); fs.renameSync(tmp, f); return true; } catch { return false; }
|
|
3886
|
+
}
|
|
3865
3887
|
function readCalMemory() {
|
|
3866
3888
|
try { return JSON.parse(fs.readFileSync(CAL_MEMORY(), 'utf8')); } catch { return null; }
|
|
3867
3889
|
}
|
|
@@ -3935,7 +3957,30 @@ const goalsCardsMemo = new Map(); // ws -> { at, cards, error }: GET /api/proa
|
|
|
3935
3957
|
function readGoalsDay(date) {
|
|
3936
3958
|
let raw = null;
|
|
3937
3959
|
try { raw = JSON.parse(fs.readFileSync(path.join(GOALS_DIR(), date + '.json'), 'utf8')); } catch { /* none yet */ }
|
|
3938
|
-
return goalsLib.readDay(raw, date);
|
|
3960
|
+
if (raw) return goalsLib.readDay(raw, date);
|
|
3961
|
+
// a day with no file of its own yet starts with the goals still open on the last day that had a file (lib/goals.js
|
|
3962
|
+
// carryOver); the first edit of the day writes them into the day's own file
|
|
3963
|
+
let prev = null;
|
|
3964
|
+
try {
|
|
3965
|
+
const last = fs.readdirSync(GOALS_DIR()).map((f) => /^(\d{4}-\d{2}-\d{2})\.json$/.exec(f)).filter((m) => m && m[1] < date).map((m) => m[1]).sort().pop();
|
|
3966
|
+
if (last) prev = goalsLib.readDay(JSON.parse(fs.readFileSync(path.join(GOALS_DIR(), last + '.json'), 'utf8')), last);
|
|
3967
|
+
} catch { /* no earlier day */ }
|
|
3968
|
+
return goalsLib.carryOver(prev, date);
|
|
3969
|
+
}
|
|
3970
|
+
/** { initiativeId: workspace id } for the initiatives exactly one registered repository holds, read at most every 30 s.
|
|
3971
|
+
* An item or goal naming one of them belongs to that repository (lib/goals.js itemRepos, goalRepos). */
|
|
3972
|
+
let iniReposMemo = null;
|
|
3973
|
+
function iniRepos(now = Date.now()) {
|
|
3974
|
+
if (iniReposMemo && now - iniReposMemo.at < 30000) return iniReposMemo.map;
|
|
3975
|
+
const map = {}; const twice = new Set();
|
|
3976
|
+
try {
|
|
3977
|
+
for (const r of portfolio().rows) {
|
|
3978
|
+
if (twice.has(r.id)) continue;
|
|
3979
|
+
if (map[r.id] && map[r.id] !== r.ws) { delete map[r.id]; twice.add(r.id); } else map[r.id] = r.ws;
|
|
3980
|
+
}
|
|
3981
|
+
} catch { /* no tags this read */ }
|
|
3982
|
+
iniReposMemo = { at: now, map };
|
|
3983
|
+
return map;
|
|
3939
3984
|
}
|
|
3940
3985
|
function latestBrief() {
|
|
3941
3986
|
try {
|
|
@@ -7676,7 +7721,8 @@ async function handleRequest(req, res) {
|
|
|
7676
7721
|
settings: { briefSources: st.briefSources, scopes: st.scopes, placeRepos: st.placeRepos, dayCheck: st.dayCheck, websites: st.websites }, sources,
|
|
7677
7722
|
// one day for every repository: the repositories it can be narrowed to, and the repository of each tagged item
|
|
7678
7723
|
repos: [...workspaces.values()].map((x) => ({ id: x.id, name: x.name || path.basename(x.root) })),
|
|
7679
|
-
item_repos: goalsLib.itemRepos({ day: readGoalsDay(date), brief, placeRepos: st.placeRepos || {} }),
|
|
7724
|
+
item_repos: goalsLib.itemRepos({ day: readGoalsDay(date), brief, placeRepos: st.placeRepos || {}, iniRepos: iniRepos(now) }),
|
|
7725
|
+
goal_repos: goalsLib.goalRepos({ day: readGoalsDay(date), iniRepos: iniRepos(now) }),
|
|
7680
7726
|
check: { every: st.dayCheck.every, last: run ? run.checks.last : null, next: due.next, running: !!(run && run.checks.session_id), log: run ? run.checks.log : [] } });
|
|
7681
7727
|
}
|
|
7682
7728
|
/* The Agenda month (G25 to G27): read-only. The calendar memory and today's brief, joined by lib/calendar-memory.js. */
|
|
@@ -7684,7 +7730,15 @@ async function handleRequest(req, res) {
|
|
|
7684
7730
|
const now = Date.now();
|
|
7685
7731
|
const month = url.searchParams.get('month') || undefined;
|
|
7686
7732
|
if (month !== undefined && !/^\d{4}-\d{2}$/.test(month)) return json(res, { ok: false, error: 'month must be YYYY-MM' }, 400);
|
|
7687
|
-
return json(res, calMemLib.agendaMonth({ memory: readCalMemory(), brief: briefOfDay(briefLib.localDate(now)), month, now }));
|
|
7733
|
+
return json(res, calMemLib.agendaMonth({ memory: readCalMemory(), brief: briefOfDay(briefLib.localDate(now)), month, marks: readCalMarks(), now }));
|
|
7734
|
+
}
|
|
7735
|
+
/* A key-meeting mark: the PM's press in a meeting's detail. { title, start, scope: 'one'|'series', on: true|false|null }. */
|
|
7736
|
+
if (p === '/api/proactive/agenda/mark' && req.method === 'POST') {
|
|
7737
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
7738
|
+
const r = calMemLib.setMark(readCalMarks(), { title: body.title, start: body.start, scope: body.scope, on: body.on === undefined ? null : body.on });
|
|
7739
|
+
if (!r.ok) return json(res, { ok: false, error: r.error }, 400);
|
|
7740
|
+
if (!writeCalMarks(r.marks)) return json(res, { ok: false, error: 'could not save — nothing changed' }, 500);
|
|
7741
|
+
return json(res, { ok: true });
|
|
7688
7742
|
}
|
|
7689
7743
|
if (p.startsWith('/api/proactive/day/') && req.method === 'POST') {
|
|
7690
7744
|
const verb = p.slice('/api/proactive/day/'.length);
|