create-pmos 0.54.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/layout.md +1 -0
- package/workspace/guide/proactive-engine.md +72 -9
- package/workspace/guide/today.md +25 -2
- package/workspace/lib/brief.js +244 -8
- package/workspace/lib/day-run.js +297 -0
- package/workspace/lib/goals.js +25 -3
- package/workspace/lib/proactive-cli.js +119 -6
- package/workspace/lib/proactive.js +53 -7
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +13 -1
- package/workspace/public/pm/branches-sheet.js +56 -0
- package/workspace/public/pm/components.css +134 -23
- package/workspace/public/pm/dest-today.js +10 -8
- package/workspace/public/pm/frame-copy.js +8 -0
- package/workspace/public/pm/today-copy.js +182 -0
- package/workspace/public/pm/today-day.js +760 -0
- package/workspace/public/pm/today-goals.js +96 -161
- package/workspace/replaced-tests.json +10 -0
- package/workspace/server.js +190 -9
- package/workspace/skills/day-check.skill +56 -0
- package/workspace/skills/day-task.skill +47 -0
- package/workspace/skills/morning-brief.skill +45 -10
- package/workspace/test-files.json +2 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* day-run.js — the day run behind "Prepare my day" (initiative `today-goals`, PRD G13, G14, G21, G22).
|
|
3
|
+
*
|
|
4
|
+
* The PM's press of "Prepare my day" records a run for the local day and starts the morning-brief session. When the
|
|
5
|
+
* brief arrives, every agent-owned priority the agent may work on becomes a task. The workspace starts one session per
|
|
6
|
+
* task, at most 5 at once. A task session submits ONE draft and sends nothing. The PM then approves the draft, or
|
|
7
|
+
* revises it with a note (at most 3 times). Approving an `approve` task starts one more session that does exactly the
|
|
8
|
+
* approved outward step and reports it sent. An `auto` task is done when its draft arrives. In the working hours a
|
|
9
|
+
* check session reads what changed and merges it into the brief.
|
|
10
|
+
*
|
|
11
|
+
* PURE: no require at all — no file, process, network or clock of its own. The server reads goals/<date>.run.json,
|
|
12
|
+
* passes it in with `now`, starts the sessions this module names, and writes what an edit returns. Every edit returns
|
|
13
|
+
* a NEW record ({ ok:true, run, ... }) or the reason it is refused ({ ok:false, error }); a refusal changes nothing.
|
|
14
|
+
*
|
|
15
|
+
* newRun(date, now) · readRun(raw, date)
|
|
16
|
+
* addTasks(run, brief) agent priorities (kind approve|auto) → waiting tasks
|
|
17
|
+
* toStart(run, limit) the tasks to start now, each with its mode (do | revise | send)
|
|
18
|
+
* started · deliver · decide · sent · fail · sessionEnded
|
|
19
|
+
* stage(run, brief) reading | working | review | done
|
|
20
|
+
* checkDue(run, {settings, now, events}) · checkStarted · checkDone
|
|
21
|
+
*
|
|
22
|
+
* Status of a task: waiting → working → review (approve) | done (auto)
|
|
23
|
+
* review --approve--> sending → sent review|done --revise--> waiting (with the note)
|
|
24
|
+
* working|sending --fail or session ended--> failed --retry--> waiting
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const MAX_PARALLEL = 5;
|
|
28
|
+
const MAX_REVISIONS = 3;
|
|
29
|
+
const MAX_CHECK_LOG = 20;
|
|
30
|
+
const MAX_INTERVAL_MIN = 120;
|
|
31
|
+
const BEFORE_MEETING_MIN = 15;
|
|
32
|
+
const STATUSES = ['waiting', 'working', 'review', 'sending', 'sent', 'done', 'failed'];
|
|
33
|
+
const KINDS = ['approve', 'auto'];
|
|
34
|
+
const ACTIONS = ['approve', 'revise', 'retry'];
|
|
35
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
36
|
+
const TASK_RE = /^t-[0-9a-f]{12}$/;
|
|
37
|
+
const MIN = 60000;
|
|
38
|
+
|
|
39
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
40
|
+
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
41
|
+
const isUrl = (v) => typeof v === 'string' && v.length <= 500 && /^https?:\/\/\S+$/.test(v);
|
|
42
|
+
const text = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
43
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
44
|
+
const localDate = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
|
|
45
|
+
/** ms of HH:MM on the local day of `ms`. */
|
|
46
|
+
const atClock = (ms, hhmm) => { const d = new Date(ms); const [h, m] = String(hhmm).split(':').map(Number); return new Date(d.getFullYear(), d.getMonth(), d.getDate(), h, m).getTime(); };
|
|
47
|
+
|
|
48
|
+
function emptyChecks() { return { session_id: null, last: null, next: null, empty_streak: 0, log: [] }; }
|
|
49
|
+
|
|
50
|
+
function newRun(date, now) {
|
|
51
|
+
return { date, started_at: iso(now), brief_session: null, tasks: [], checks: emptyChecks() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A run record read from disk, checked. Anything that is not a run for `date` reads as null (no run). */
|
|
55
|
+
function readRun(raw, date) {
|
|
56
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw) || raw.date !== date || !DATE_RE.test(String(date))) return null;
|
|
57
|
+
if (typeof raw.started_at !== 'string' || Number.isNaN(Date.parse(raw.started_at)) || !Array.isArray(raw.tasks)) return null;
|
|
58
|
+
const run = clone(raw);
|
|
59
|
+
const bs = run.brief_session;
|
|
60
|
+
run.brief_session = bs && typeof bs === 'object' && typeof bs.id === 'string' ? { id: bs.id, started_at: bs.started_at || null, ended_at: bs.ended_at || null } : null;
|
|
61
|
+
run.tasks = run.tasks.filter((t) => t && typeof t === 'object' && TASK_RE.test(t.id) && STATUSES.includes(t.status) && KINDS.includes(t.kind))
|
|
62
|
+
.map((t) => ({ ...t, attempts: Number.isInteger(t.attempts) ? t.attempts : 0, revisions: Array.isArray(t.revisions) ? t.revisions : [],
|
|
63
|
+
draft: t.draft || null, decided: t.decided || null, error: t.error || null, session_id: t.session_id || null, started_at: t.started_at || null }));
|
|
64
|
+
const c = run.checks && typeof run.checks === 'object' ? run.checks : {};
|
|
65
|
+
run.checks = { session_id: c.session_id || null, last: c.last || null, next: c.next || null,
|
|
66
|
+
empty_streak: Number.isInteger(c.empty_streak) && c.empty_streak >= 0 ? c.empty_streak : 0, log: Array.isArray(c.log) ? c.log.slice(-MAX_CHECK_LOG) : [] };
|
|
67
|
+
return run;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const findTask = (run, id) => (run && Array.isArray(run.tasks) ? run.tasks.find((t) => t.id === id) || null : null);
|
|
71
|
+
|
|
72
|
+
/** The mode a task's session runs in: `send` for an approved draft, `revise` when the PM sent it back with a note. */
|
|
73
|
+
function modeOf(t) {
|
|
74
|
+
if (t.status === 'sending') return 'send';
|
|
75
|
+
return t.revisions.length && t.draft ? 'revise' : 'do';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Edit one task of a copy of the run; `fn` returns an error string to refuse, or nothing. */
|
|
79
|
+
function withTask(run, taskId, fn) {
|
|
80
|
+
if (!run) return { ok: false, error: 'no day run today — press Prepare my day first' };
|
|
81
|
+
const next = clone(run);
|
|
82
|
+
const t = findTask(next, taskId);
|
|
83
|
+
if (!t) return { ok: false, error: `no task ${taskId} in today's run` };
|
|
84
|
+
const err = fn(t, next);
|
|
85
|
+
if (err) return { ok: false, error: err };
|
|
86
|
+
return { ok: true, run: next, task: t };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Every priority the agent owns and may work on (kind approve or auto) becomes a waiting task, once. A brief for
|
|
91
|
+
* another day adds nothing. An item the PM owns, a `you` item and a worth-asking item never become tasks.
|
|
92
|
+
*/
|
|
93
|
+
function addTasks(run, brief) {
|
|
94
|
+
if (!run) return { ok: false, error: 'no day run today' };
|
|
95
|
+
const next = clone(run);
|
|
96
|
+
const added = [];
|
|
97
|
+
if (!brief || brief.date !== run.date) return { ok: true, run: next, added };
|
|
98
|
+
for (const x of brief.priorities || []) {
|
|
99
|
+
const kind = x.kind || (x.owner === 'agent' ? 'approve' : 'you');
|
|
100
|
+
if (x.owner !== 'agent' || !KINDS.includes(kind)) continue;
|
|
101
|
+
const id = 't-' + x.id;
|
|
102
|
+
if (findTask(next, id)) continue;
|
|
103
|
+
next.tasks.push({ id, item_id: x.id, title: x.title, kind, source: x.source, link: x.link || null, goal: x.goal || null,
|
|
104
|
+
status: 'waiting', session_id: null, started_at: null, attempts: 0, revisions: [], draft: null, decided: null, error: null });
|
|
105
|
+
added.push(id);
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, run: next, added };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The tasks to start now, oldest first, so that the sessions running (working, and sending with a session) stay at
|
|
112
|
+
* most `limit`. An approved task waiting for its send session comes first: the PM is waiting on it.
|
|
113
|
+
* @returns {Array<object>} copies of the tasks, each with its `mode`
|
|
114
|
+
*/
|
|
115
|
+
function toStart(run, limit = MAX_PARALLEL) {
|
|
116
|
+
if (!run) return [];
|
|
117
|
+
const busy = run.tasks.filter((t) => t.status === 'working' || (t.status === 'sending' && t.session_id)).length;
|
|
118
|
+
const free = Math.max(0, limit - busy);
|
|
119
|
+
const ready = [...run.tasks.filter((t) => t.status === 'sending' && !t.session_id), ...run.tasks.filter((t) => t.status === 'waiting')];
|
|
120
|
+
return ready.slice(0, free).map((t) => ({ ...clone(t), mode: modeOf(t) }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A session was started for the task. `sessionId` is null when the start failed (the caller then calls fail). */
|
|
124
|
+
function started(run, taskId, sessionId, now) {
|
|
125
|
+
return withTask(run, taskId, (t) => {
|
|
126
|
+
if (t.status === 'waiting') { t.status = 'working'; t.attempts += 1; }
|
|
127
|
+
else if (t.status === 'sending' && !t.session_id) { /* the approved outward step */ }
|
|
128
|
+
else return `task ${taskId} is ${t.status} — only a waiting or an approved task can start`;
|
|
129
|
+
t.session_id = sessionId || null; t.started_at = iso(now); t.error = null;
|
|
130
|
+
return null;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The task session's ONE draft. An `auto` task is then done; an `approve` task waits for the PM. */
|
|
135
|
+
function deliver(run, taskId, d, now) {
|
|
136
|
+
return withTask(run, taskId, (t) => {
|
|
137
|
+
if (t.status !== 'working') return `task ${taskId} is ${t.status} — only a working task can deliver a draft`;
|
|
138
|
+
const b = d && typeof d === 'object' ? d : {};
|
|
139
|
+
const summary = text(b.summary); const body = typeof b.body === 'string' ? b.body.trim() : '';
|
|
140
|
+
if (!summary) return 'a draft needs a summary';
|
|
141
|
+
if (summary.length > 240) return 'the summary is over 240 characters';
|
|
142
|
+
if (!body) return 'a draft needs a body';
|
|
143
|
+
if (body.length > 8000) return 'the body is over 8000 characters';
|
|
144
|
+
if (b.link !== undefined && b.link !== null && b.link !== '' && !isUrl(b.link)) return 'the link must be an http(s) address of at most 500 characters';
|
|
145
|
+
t.draft = { summary, body, link: b.link || null, at: iso(now) };
|
|
146
|
+
t.status = t.kind === 'auto' ? 'done' : 'review';
|
|
147
|
+
t.error = null;
|
|
148
|
+
return null;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The PM's decision on a draft.
|
|
154
|
+
* approve review (approve task) → sending: a session will do exactly the approved step. done (auto task) → recorded.
|
|
155
|
+
* revise review or done, with a note of 1 to 500 characters, at most 3 times → waiting (the next session revises).
|
|
156
|
+
* retry failed → waiting.
|
|
157
|
+
*/
|
|
158
|
+
function decide(run, taskId, op, now) {
|
|
159
|
+
const o = op && typeof op === 'object' ? op : {};
|
|
160
|
+
if (!ACTIONS.includes(o.action)) return { ok: false, error: `action must be one of ${ACTIONS.join(', ')}` };
|
|
161
|
+
const note = text(o.note);
|
|
162
|
+
return withTask(run, taskId, (t) => {
|
|
163
|
+
const at = iso(now);
|
|
164
|
+
if (o.action === 'approve') {
|
|
165
|
+
if (t.status === 'review' && t.kind === 'approve') { t.status = 'sending'; t.session_id = null; t.started_at = null; }
|
|
166
|
+
else if (!(t.status === 'done' && t.kind === 'auto')) return `task ${taskId} is ${t.status} — only a draft waiting for you can be approved`;
|
|
167
|
+
t.decided = { action: 'approve', at, ...(note ? { note: note.slice(0, 500) } : {}) };
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (o.action === 'revise') {
|
|
171
|
+
if (t.status !== 'review' && t.status !== 'done') return `task ${taskId} is ${t.status} — only a delivered draft can be revised`;
|
|
172
|
+
if (!note) return 'say what to change: a revise needs a note';
|
|
173
|
+
if (note.length > 500) return 'the note is over 500 characters';
|
|
174
|
+
if (t.revisions.length >= MAX_REVISIONS) return `task ${taskId} has had ${MAX_REVISIONS} revisions — at most ${MAX_REVISIONS}. Do the rest yourself.`;
|
|
175
|
+
t.revisions.push({ note, at });
|
|
176
|
+
t.status = 'waiting'; t.session_id = null; t.decided = { action: 'revise', at, note };
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (t.status !== 'failed') return `task ${taskId} is ${t.status} — only a failed task can be run again`;
|
|
180
|
+
t.status = 'waiting'; t.session_id = null; t.error = null; t.decided = { action: 'retry', at };
|
|
181
|
+
return null;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The send session reports the approved step done, with the link to what it posted. */
|
|
186
|
+
function sent(run, taskId, s, now) {
|
|
187
|
+
return withTask(run, taskId, (t) => {
|
|
188
|
+
if (t.status !== 'sending') return `task ${taskId} is ${t.status} — only an approved task being sent can be reported sent`;
|
|
189
|
+
const link = s && s.link;
|
|
190
|
+
if (link !== undefined && link !== null && link !== '' && !isUrl(link)) return 'the link must be an http(s) address of at most 500 characters';
|
|
191
|
+
t.status = 'sent'; t.sent = { link: link || null, at: iso(now) };
|
|
192
|
+
return null;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The session could not finish. The PM sees the reason and can run the task again. */
|
|
197
|
+
function fail(run, taskId, reason, now) {
|
|
198
|
+
return withTask(run, taskId, (t) => {
|
|
199
|
+
if (t.status !== 'working' && t.status !== 'sending') return `task ${taskId} is ${t.status} — only a working or sending task can fail`;
|
|
200
|
+
const why = text(reason);
|
|
201
|
+
if (!why) return 'a failure needs a reason';
|
|
202
|
+
if (why.length > 240) return 'the reason is over 240 characters';
|
|
203
|
+
t.status = 'failed'; t.error = why; t.failed_at = iso(now);
|
|
204
|
+
return null;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A session ended. A task it was working on without delivering, or sending without reporting, has failed. */
|
|
209
|
+
function sessionEnded(run, sessionId, now) {
|
|
210
|
+
if (!run) return { ok: false, error: 'no day run today' };
|
|
211
|
+
const next = clone(run);
|
|
212
|
+
let changed = false;
|
|
213
|
+
for (const t of next.tasks) {
|
|
214
|
+
if (!sessionId || t.session_id !== sessionId) continue;
|
|
215
|
+
if (t.status === 'working') { t.status = 'failed'; t.error = 'the session ended without a draft'; t.failed_at = iso(now); changed = true; }
|
|
216
|
+
else if (t.status === 'sending') { t.status = 'failed'; t.error = 'the session ended before it reported sent'; t.failed_at = iso(now); changed = true; }
|
|
217
|
+
}
|
|
218
|
+
if (next.brief_session && next.brief_session.id === sessionId && !next.brief_session.ended_at) { next.brief_session.ended_at = iso(now); changed = true; }
|
|
219
|
+
if (next.checks.session_id === sessionId) { next.checks.session_id = null; changed = true; }
|
|
220
|
+
return { ok: true, run: next, changed };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Where the run is: reading (no brief for the day yet), working, review (drafts wait for the PM) or done. */
|
|
224
|
+
function stage(run, brief) {
|
|
225
|
+
if (!run) return null;
|
|
226
|
+
if (!brief || brief.date !== run.date) return 'reading';
|
|
227
|
+
if (run.tasks.some((t) => t.status === 'waiting' || t.status === 'working' || t.status === 'sending')) return 'working';
|
|
228
|
+
if (run.tasks.some((t) => t.status === 'review')) return 'review';
|
|
229
|
+
return 'done';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ── the daytime check (G22) ──────────────────────────────────────────────────────────────────────────────────── */
|
|
233
|
+
|
|
234
|
+
/** The wait between two checks: the setting, doubled for each empty check after the first, at most two hours. */
|
|
235
|
+
function intervalMin(every, emptyStreak) {
|
|
236
|
+
return Math.min(MAX_INTERVAL_MIN, every * Math.pow(2, Math.max(0, emptyStreak - 1)));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Whether a check is due now, and why. Not due with no run, while a check runs, with the check off, on another day,
|
|
241
|
+
* or outside the working hours. Due 15 minutes before a meeting (once), after a meeting ends (once), and at the
|
|
242
|
+
* interval from the last check (or the press).
|
|
243
|
+
* @returns {{due:boolean, reason:'interval'|'before-meeting'|'after-meeting'|null, next:string|null}}
|
|
244
|
+
*/
|
|
245
|
+
function checkDue(run, { settings = {}, now, events = [] } = {}) {
|
|
246
|
+
const no = (next = null) => ({ due: false, reason: null, next });
|
|
247
|
+
if (!run) return no();
|
|
248
|
+
const dc = settings.dayCheck || { every: 60, start: '09:00', end: '18:00' };
|
|
249
|
+
if (!dc.every) return no();
|
|
250
|
+
const winStart = atClock(now, dc.start); const winEnd = atClock(now, dc.end);
|
|
251
|
+
const base = Date.parse(run.checks.last || run.started_at);
|
|
252
|
+
const nextMs = Math.max(base + intervalMin(dc.every, run.checks.empty_streak) * MIN, winStart);
|
|
253
|
+
const next = localDate(now) === run.date && nextMs < winEnd ? iso(nextMs) : null;
|
|
254
|
+
if (run.checks.session_id || localDate(now) !== run.date || now < winStart || now >= winEnd) return no(next);
|
|
255
|
+
const last = run.checks.last ? Date.parse(run.checks.last) : null;
|
|
256
|
+
for (const e of events || []) {
|
|
257
|
+
const s = Date.parse(e && e.start);
|
|
258
|
+
if (!Number.isFinite(s)) continue;
|
|
259
|
+
const before = s - BEFORE_MEETING_MIN * MIN;
|
|
260
|
+
if (now >= before && now < s && (last === null || last < before)) return { due: true, reason: 'before-meeting', next };
|
|
261
|
+
}
|
|
262
|
+
for (const e of events || []) {
|
|
263
|
+
const s = Date.parse(e && e.start);
|
|
264
|
+
const f = e && e.end ? Date.parse(e.end) : s + 30 * MIN;
|
|
265
|
+
if (Number.isFinite(f) && f <= now && f > base) return { due: true, reason: 'after-meeting', next };
|
|
266
|
+
}
|
|
267
|
+
if (now >= nextMs) return { due: true, reason: 'interval', next };
|
|
268
|
+
return no(next);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** A check session started. The log keeps the last 20 checks. */
|
|
272
|
+
function checkStarted(run, sessionId, reason, now) {
|
|
273
|
+
if (!run) return { ok: false, error: 'no day run today — press Prepare my day first' };
|
|
274
|
+
if (run.checks.session_id) return { ok: false, error: 'a check is already running' };
|
|
275
|
+
const next = clone(run);
|
|
276
|
+
next.checks.session_id = sessionId || null;
|
|
277
|
+
next.checks.last = iso(now);
|
|
278
|
+
next.checks.log = [...next.checks.log, { at: iso(now), reason: String(reason || 'manual'), found: null, resolved: null }].slice(-MAX_CHECK_LOG);
|
|
279
|
+
return { ok: true, run: next };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The check submitted its merge. Two empty checks in a row make the next wait twice as long. */
|
|
283
|
+
function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
|
|
284
|
+
if (!run) return { ok: false, error: 'no day run today' };
|
|
285
|
+
const next = clone(run);
|
|
286
|
+
const f = Math.max(0, Number(found) || 0); const r = Math.max(0, Number(resolved) || 0);
|
|
287
|
+
next.checks.empty_streak = f === 0 && r === 0 ? next.checks.empty_streak + 1 : 0;
|
|
288
|
+
next.checks.session_id = null;
|
|
289
|
+
const open = [...next.checks.log].reverse().find((x) => x.found === null);
|
|
290
|
+
if (open) { open.found = f; open.resolved = r; open.done_at = iso(now); }
|
|
291
|
+
else next.checks.log = [...next.checks.log, { at: iso(now), reason: 'merge', found: f, resolved: r }].slice(-MAX_CHECK_LOG);
|
|
292
|
+
return { ok: true, run: next };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = { MAX_PARALLEL, MAX_REVISIONS, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
|
|
296
|
+
newRun, readRun, findTask, modeOf, addTasks, toStart, started, deliver, decide, sent, fail, sessionEnded, stage, intervalMin,
|
|
297
|
+
checkDue, checkStarted, checkDone };
|
package/workspace/lib/goals.js
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
* THE JOIN IS CHECKABLE. A brief item or calendar event belongs to a goal when it names the goal's id (the morning-brief
|
|
15
15
|
* session sets it). Failing that, an item or Today card belongs to the first open goal tied to the same initiative. There
|
|
16
16
|
* is no word matching: a link is an id, so the PM can see why a step is under a goal. One step is under one goal only.
|
|
17
|
+
* Only an OPEN goal takes steps (PRD G9): an item that names a done or dropped goal is placed as if it named none.
|
|
18
|
+
*
|
|
19
|
+
* The day view (PRD G10, G12) reads three more lists from the same plan: `mentions_by_source` ({source: [step]}), and
|
|
20
|
+
* `you` and `agent`, the priorities split by who acts. A step carries the item's context (link, where, who, about, ask,
|
|
21
|
+
* reply_needed, how, kind, new) as the brief gave it.
|
|
17
22
|
*/
|
|
18
23
|
const crypto = require('crypto');
|
|
19
24
|
|
|
@@ -98,10 +103,15 @@ function freeTime(events, at, endOfDay = END_OF_DAY) {
|
|
|
98
103
|
|
|
99
104
|
const stepOrder = (a, b) => (a.owner === b.owner ? 0 : a.owner === 'pm' ? -1 : 1) || (b.confidence - a.confidence) || a.title.localeCompare(b.title);
|
|
100
105
|
|
|
106
|
+
// The context a job carries (G18) rides on its step unchanged, so Today and the CLI show what the brief said.
|
|
107
|
+
const ITEM_CONTEXT = ['link', 'where', 'who', 'about', 'ask', 'reply_needed', 'how', 'kind', 'new'];
|
|
101
108
|
function itemStep(x) {
|
|
102
|
-
|
|
103
|
-
effort: x.effort, confidence: x.confidence, initiative_id: x.initiative_id || null, mention: !!x.mention, worth_asking: !!x.worth_asking,
|
|
109
|
+
const step = { ref: 'brief:' + x.id, item_id: x.id, from: 'brief', source: x.source, title: x.title, why: x.why_now, evidence: x.evidence || '', owner: x.owner,
|
|
110
|
+
effort: x.effort, confidence: x.confidence, initiative_id: x.initiative_id || null, goal: x.goal || null, mention: !!x.mention, worth_asking: !!x.worth_asking,
|
|
104
111
|
prepare: x.owner === 'agent' && !!x.initiative_id };
|
|
112
|
+
for (const k of ITEM_CONTEXT) if (x[k] !== undefined) step[k] = x[k];
|
|
113
|
+
if (!step.kind) step.kind = x.owner === 'pm' ? 'you' : 'approve';
|
|
114
|
+
return step;
|
|
105
115
|
}
|
|
106
116
|
function cardStep(c) {
|
|
107
117
|
return { ref: 'today:' + c.id, from: 'today', source: c.source || 'today', title: c.cause, why: c.cause, evidence: c.evidence || '', owner: 'pm',
|
|
@@ -125,11 +135,14 @@ function planDay({ day, brief = null, cards = [], now = Date.now(), endOfDay = E
|
|
|
125
135
|
const goals = (day && day.goals ? day.goals : []).map((g) => ({ ...g, steps: [], context: [] }));
|
|
126
136
|
const open = goals.filter((g) => g.status === 'open');
|
|
127
137
|
const byId = new Map(goals.map((g) => [g.id, g]));
|
|
138
|
+
const openById = new Map(open.map((g) => [g.id, g]));
|
|
128
139
|
const byIni = (ini) => (ini ? open.find((g) => g.initiative_id === ini) || null : null);
|
|
129
140
|
const unplaced = { items: [], cards: [] };
|
|
130
141
|
|
|
142
|
+
// G9: a step goes only under an OPEN goal. A done or dropped goal draws no steps, so an item that names one is
|
|
143
|
+
// placed as if it named no goal (by initiative, else "also" or "worth asking"); it is never lost under a closed goal.
|
|
131
144
|
for (const x of items) {
|
|
132
|
-
const g = (x.goal &&
|
|
145
|
+
const g = (x.goal && openById.get(x.goal)) || byIni(x.initiative_id);
|
|
133
146
|
if (g) g.steps.push(itemStep(x)); else unplaced.items.push(x);
|
|
134
147
|
}
|
|
135
148
|
for (const c of cards || []) {
|
|
@@ -150,6 +163,14 @@ function planDay({ day, brief = null, cards = [], now = Date.now(), endOfDay = E
|
|
|
150
163
|
...unplaced.cards.map(cardStep),
|
|
151
164
|
].sort(stepOrder);
|
|
152
165
|
const worthAsking = unplaced.items.filter((x) => x.worth_asking).map(itemStep).sort(stepOrder);
|
|
166
|
+
// G10: every mention, grouped by the service it came from. G12: the priorities split by who acts — "You" also holds the
|
|
167
|
+
// Today cards that no goal took; a mention the agent owns is in both the mentions and "Agent".
|
|
168
|
+
const mentionsBySource = {};
|
|
169
|
+
for (const x of items) if (x.mention) (mentionsBySource[x.source] = mentionsBySource[x.source] || []).push(itemStep(x));
|
|
170
|
+
for (const k of Object.keys(mentionsBySource)) mentionsBySource[k].sort(stepOrder);
|
|
171
|
+
const prio = items.filter((x) => !x.worth_asking);
|
|
172
|
+
const you = [...prio.filter((x) => x.owner === 'pm').map(itemStep), ...unplaced.cards.map(cardStep)].sort(stepOrder);
|
|
173
|
+
const agent = prio.filter((x) => x.owner === 'agent').map(itemStep).sort(stepOrder);
|
|
153
174
|
const notRead = fresh ? Object.entries(brief.sources || {}).filter(([, v]) => v !== 'read').map(([k, v]) => ({ source: k, state: v })) : [];
|
|
154
175
|
const counts = { open: open.length, done: goals.filter((g) => g.status === 'done').length, planned: goals.filter((g) => g.coverage === 'planned').length,
|
|
155
176
|
needs_plan: goals.filter((g) => g.coverage === 'needs-plan').length };
|
|
@@ -159,6 +180,7 @@ function planDay({ day, brief = null, cards = [], now = Date.now(), endOfDay = E
|
|
|
159
180
|
not_read: notRead,
|
|
160
181
|
time: fresh ? freeTime(events, now, endOfDay) : null,
|
|
161
182
|
mentions: items.filter((x) => x.mention).length,
|
|
183
|
+
mentions_by_source: mentionsBySource, you, agent,
|
|
162
184
|
};
|
|
163
185
|
}
|
|
164
186
|
|
|
@@ -11,9 +11,20 @@
|
|
|
11
11
|
* pmos proactive feedback <id> useful|not-useful|wrong-priority|opened|dismissed [--note TEXT]
|
|
12
12
|
* pmos proactive feedback --missed TEXT
|
|
13
13
|
* pmos proactive brief [show]
|
|
14
|
-
* pmos proactive brief
|
|
14
|
+
* pmos proactive settings [--brief-source NAME=on|off] [--scope SERVICE:NAME=watch|search|ignore|none] [--day-check 30|60|off]
|
|
15
|
+
* [--website URL] [--no-website URL]
|
|
16
|
+
* pmos proactive brief submit --file PATH [--merge]
|
|
15
17
|
* pmos proactive brief schedule [--repo PATH]
|
|
16
18
|
* pmos proactive goals [list] | add TEXT [--initiative ID] | done N | drop N | reopen N | remove N | plan [--repo PATH]
|
|
19
|
+
* pmos proactive day [--repo PATH] the day run: its stage, its tasks and the daytime check
|
|
20
|
+
* pmos proactive day show TASK one task with its item, draft and revision notes (what day-task reads)
|
|
21
|
+
* pmos proactive day deliver TASK --summary S --body-file F [--link URL]
|
|
22
|
+
* pmos proactive day sent TASK [--link URL] | day fail TASK --reason R
|
|
23
|
+
* pmos proactive day approve|revise|retry TASK [--note N] | day check
|
|
24
|
+
*
|
|
25
|
+
* The day verbs are the one exception to "starts nothing": `day approve`, `day revise`, `day retry` and `day check`
|
|
26
|
+
* are the PM's presses, and the server starts the session each implies (today-goals G14, G21, G22). `deliver`, `sent`
|
|
27
|
+
* and `fail` are how a task session reports; they start nothing themselves.
|
|
17
28
|
* pmos proactive prepare plan --initiative ID [--repo PATH]
|
|
18
29
|
* pmos proactive prepare queue --initiative ID --item ITEM --sha HASH [--repo PATH]
|
|
19
30
|
* pmos proactive prepare status | cancel ID | run
|
|
@@ -24,8 +35,8 @@ const path = require('path');
|
|
|
24
35
|
const { execFileSync } = require('child_process');
|
|
25
36
|
const { parseArgs } = require('./schedule-cli');
|
|
26
37
|
|
|
27
|
-
const COMMANDS = { proactive: ['today', 'scan', 'settings', 'feedback', 'brief', 'goals', 'prepare', 'learn'] };
|
|
28
|
-
const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'slack', 'jira', 'confluence'];
|
|
38
|
+
const COMMANDS = { proactive: ['today', 'scan', 'settings', 'feedback', 'brief', 'goals', 'prepare', 'learn', 'day'] };
|
|
39
|
+
const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'slack', 'jira', 'confluence', 'github', 'drive', 'figma', 'website'];
|
|
29
40
|
const onOff = (v, what) => (v === 'on' || v === 'true' ? true : v === 'off' || v === 'false' ? false : { err: `${what} takes on or off` });
|
|
30
41
|
|
|
31
42
|
async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
@@ -75,6 +86,22 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
75
86
|
if (!m) return fail(`--brief-source takes NAME=on or NAME=off, where NAME is one of ${BRIEF_SOURCES.join(', ')}`);
|
|
76
87
|
patch.briefSources = { [m[1]]: m[2] === 'on' };
|
|
77
88
|
}
|
|
89
|
+
if ('scope' in flags) {
|
|
90
|
+
const m = /^(slack|jira|confluence):(.+)=(watch|search|ignore|none)$/.exec(flags.scope);
|
|
91
|
+
if (!m) return fail('--scope takes SERVICE:NAME=LEVEL, where SERVICE is slack, jira or confluence and LEVEL is watch, search, ignore or none — for example slack:#pm-team=watch');
|
|
92
|
+
patch.scopes = { [m[1]]: { [m[2]]: m[3] === 'none' ? null : m[3] } };
|
|
93
|
+
}
|
|
94
|
+
if ('day-check' in flags) {
|
|
95
|
+
const v = flags['day-check'];
|
|
96
|
+
if (!['30', '60', 'off'].includes(v)) return fail('--day-check takes 30, 60 or off');
|
|
97
|
+
patch.dayCheck = { every: v === 'off' ? 0 : Number(v) };
|
|
98
|
+
}
|
|
99
|
+
if ('website' in flags || 'no-website' in flags) {
|
|
100
|
+
let list = [...((probe.settings && probe.settings.websites) || [])];
|
|
101
|
+
if ('website' in flags) list.push(flags.website);
|
|
102
|
+
if ('no-website' in flags) list = list.filter((u) => u !== flags['no-website']);
|
|
103
|
+
patch.websites = [...new Set(list)];
|
|
104
|
+
}
|
|
78
105
|
if ('stalled-days' in flags) patch.stalledDays = Number(flags['stalled-days']);
|
|
79
106
|
if ('quiet' in flags) {
|
|
80
107
|
if (flags.quiet === 'none') patch.quiet = null;
|
|
@@ -89,6 +116,12 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
89
116
|
say(`engine ${s.enabled ? 'on' : 'off'} · stalled after ${s.stalledDays} days · quiet ${s.quiet ? s.quiet.start + '-' + s.quiet.end : 'none'} · learning ${s.pauseLearning ? 'paused' : 'on'}`);
|
|
90
117
|
say('brief sources: ' + BRIEF_SOURCES.map((k) => `${k} ${s.briefSources && s.briefSources[k] ? 'on' : 'off'}`).join(', ') + ' · scoring ' + (s.briefScoring ? 'on' : 'off') + (s.name ? ' · name ' + s.name : ''));
|
|
91
118
|
say('background prepare ' + (s.backgroundPrepare ? 'on' : 'off'));
|
|
119
|
+
const dc = s.dayCheck || { every: 60, start: '09:00', end: '18:00' };
|
|
120
|
+
say('day check ' + (dc.every ? `every ${dc.every} min, ${dc.start}-${dc.end}` : 'off') + ' · websites: ' + ((s.websites || []).join(', ') || 'none'));
|
|
121
|
+
for (const svc of ['slack', 'jira', 'confluence']) {
|
|
122
|
+
const lv = Object.entries((s.scopes || {})[svc] || {});
|
|
123
|
+
if (lv.length) say(`${svc} levels: ` + lv.map(([n, l]) => `${n} ${l}`).join(', ') + ' (any other place: search)');
|
|
124
|
+
}
|
|
92
125
|
say('watching: ' + Object.entries(s.triggers).map(([k, v]) => `${k} ${v ? 'on' : 'off'}`).join(', '));
|
|
93
126
|
return 0;
|
|
94
127
|
}
|
|
@@ -190,6 +223,82 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
190
223
|
for (const l of r.lessons.filter((x) => x.status === 'proposed' || x.status === 'approved')) say(`${l.id} ${l.status} ${l.text}`);
|
|
191
224
|
return 0;
|
|
192
225
|
}
|
|
226
|
+
if (verb === 'day') {
|
|
227
|
+
const sub = pos[0] || 'status';
|
|
228
|
+
const usage = 'usage: pmos proactive day [status] | show TASK | deliver TASK --summary S --body-file F [--link URL] | sent TASK [--link URL] | fail TASK --reason R | approve|revise|retry TASK [--note N] | check';
|
|
229
|
+
const wsOfRepo = async () => {
|
|
230
|
+
let root; try { root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: flags.repo ? path.resolve(cwd, flags.repo) : cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; }
|
|
231
|
+
const wl = await call('GET', '/api/workspaces');
|
|
232
|
+
const hit = ((wl && wl.workspaces) || []).find((w) => path.resolve(w.root) === path.resolve(root));
|
|
233
|
+
return hit ? hit.id : null;
|
|
234
|
+
};
|
|
235
|
+
const get = async () => { const ws = await wsOfRepo(); return call('GET', '/api/proactive/day' + (ws ? '?ws=' + encodeURIComponent(ws) : '')); };
|
|
236
|
+
// A task is named by its id (t-…) or by its position in `pmos proactive day`.
|
|
237
|
+
const taskOf = (d, ref) => {
|
|
238
|
+
const tasks = (d.run && d.run.tasks) || [];
|
|
239
|
+
return /^\d+$/.test(String(ref)) ? tasks[Number(ref) - 1] || null : tasks.find((t) => t.id === ref) || null;
|
|
240
|
+
};
|
|
241
|
+
const post = async (what, body, done) => {
|
|
242
|
+
const r = await call('POST', '/api/proactive/day/' + what, body);
|
|
243
|
+
if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the server did not answer'}. Nothing changed.`);
|
|
244
|
+
say(done(r)); return 0;
|
|
245
|
+
};
|
|
246
|
+
if (sub === 'check') {
|
|
247
|
+
const ws = await wsOfRepo();
|
|
248
|
+
return post('check', ws ? { ws } : {}, (r) => `check started (session ${r.session}) — it merges what changed into today's brief`);
|
|
249
|
+
}
|
|
250
|
+
const d = await get();
|
|
251
|
+
if (!d || d.ok === false) return fail((d && d.error) || 'the server did not answer');
|
|
252
|
+
if (sub === 'status') {
|
|
253
|
+
if (!d.run) { say(`No day run for ${d.today}. Start one with "Prepare my day" on Today, or: pmos proactive goals plan`); return 0; }
|
|
254
|
+
say(`day run ${d.run.date} · stage: ${d.stage} · started ${d.run.started_at}${d.run.brief_session ? ` · brief session ${d.run.brief_session.id}${d.run.brief_session.ended_at ? ' (ended)' : ' (running)'}` : ''}`);
|
|
255
|
+
if (!d.run.tasks.length) say('No agent tasks yet.');
|
|
256
|
+
d.run.tasks.forEach((t, i) => say(`${i + 1}. ${t.id} ${t.status} ${t.kind} ${t.title}${t.revisions.length ? ` (${t.revisions.length} revision${t.revisions.length > 1 ? 's' : ''})` : ''}${t.error ? ' — ' + t.error : ''}`));
|
|
257
|
+
const c = d.check;
|
|
258
|
+
say(`daytime check: ${c.every ? `every ${c.every} min` : 'off'}${c.running ? ' · running now' : ''} · last ${c.last || 'none'} · next ${c.next || 'none'}`);
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
const ref = pos[1];
|
|
262
|
+
const t = d.run ? taskOf(d, ref) : null;
|
|
263
|
+
if (!d.run) return fail('no day run today — press Prepare my day first');
|
|
264
|
+
if (!t) return fail(`no task ${ref || ''} in today's run — ${usage}`);
|
|
265
|
+
if (sub === 'show') {
|
|
266
|
+
// What the day-task skill reads: the job as the brief gave it, the draft so far, and the PM's notes.
|
|
267
|
+
const it = d.brief ? [...(d.brief.priorities || []), ...(d.brief.worth_asking || [])].find((x) => x.id === t.item_id) : null;
|
|
268
|
+
const mode = t.status === 'sending' ? 'send' : (t.revisions.length && t.draft ? 'revise' : 'do');
|
|
269
|
+
say(`${t.id} ${t.status} ${t.kind} mode ${mode} ${t.title}`);
|
|
270
|
+
say(`source: ${t.source}${t.link ? ' · link ' + t.link : ''}${t.goal ? ' · goal ' + t.goal : ''} · attempts ${t.attempts}`);
|
|
271
|
+
if (it) {
|
|
272
|
+
say(`why now: ${it.why_now}`);
|
|
273
|
+
if (it.evidence) say(`evidence: ${it.evidence}`);
|
|
274
|
+
for (const k of ['where', 'who', 'about', 'ask', 'how']) if (it[k]) say(`${k}: ${it[k]}`);
|
|
275
|
+
if (it.reply_needed !== undefined) say(`reply needed: ${it.reply_needed ? 'yes' : 'no'}`);
|
|
276
|
+
if (it.initiative_id) say(`initiative: ${it.initiative_id}`);
|
|
277
|
+
} else say('(the item is no longer in today\'s brief)');
|
|
278
|
+
if (t.kind === 'approve') say('kind approve: deliver ONE draft; nothing goes out until the PM approves it.');
|
|
279
|
+
else say('kind auto: deliver ONE draft; nothing leaves this machine.');
|
|
280
|
+
if (t.draft) { say(`draft (${t.draft.at}): ${t.draft.summary}${t.draft.link ? ' · ' + t.draft.link : ''}`); say(t.draft.body); }
|
|
281
|
+
t.revisions.forEach((rv, i) => say(`revision note ${i + 1} (${rv.at}): ${rv.note}`));
|
|
282
|
+
if (t.decided) say(`decided: ${t.decided.action} at ${t.decided.at}${t.decided.note ? ' — ' + t.decided.note : ''}`);
|
|
283
|
+
if (t.sent) say(`sent ${t.sent.at}${t.sent.link ? ' · ' + t.sent.link : ''}`);
|
|
284
|
+
if (t.error) say(`error: ${t.error}`);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
if (sub === 'deliver') {
|
|
288
|
+
if (!flags.summary || !flags['body-file']) return fail('usage: pmos proactive day deliver TASK --summary S --body-file F [--link URL]');
|
|
289
|
+
let body; try { body = fs.readFileSync(path.resolve(cwd, flags['body-file']), 'utf8'); } catch (e) { return fail(`could not read ${flags['body-file']} — ${e.message}`); }
|
|
290
|
+
return post('deliver', { task: t.id, summary: flags.summary, body, ...(flags.link ? { link: flags.link } : {}) }, (r) => `delivered — ${t.id} is now ${r.task.status}`);
|
|
291
|
+
}
|
|
292
|
+
if (sub === 'sent') return post('sent', { task: t.id, ...(flags.link ? { link: flags.link } : {}) }, () => `sent — ${t.id}`);
|
|
293
|
+
if (sub === 'fail') {
|
|
294
|
+
if (!flags.reason) return fail('usage: pmos proactive day fail TASK --reason R');
|
|
295
|
+
return post('fail', { task: t.id, reason: flags.reason }, () => `failed — ${t.id}: ${flags.reason}`);
|
|
296
|
+
}
|
|
297
|
+
if (['approve', 'revise', 'retry'].includes(sub)) {
|
|
298
|
+
return post('decide', { task: t.id, action: sub, ...(flags.note ? { note: flags.note } : {}) }, (r) => `${sub === 'approve' ? 'approved' : sub === 'revise' ? 'sent back with your note' : 'running again'} — ${t.id} is now ${r.task.status}`);
|
|
299
|
+
}
|
|
300
|
+
return fail(usage);
|
|
301
|
+
}
|
|
193
302
|
if (verb === 'goals') {
|
|
194
303
|
const sub = pos[0] || 'list';
|
|
195
304
|
const usage = 'usage: pmos proactive goals [list] | add TEXT [--initiative ID] | done N | drop N | reopen N | remove N | plan [--repo PATH]';
|
|
@@ -224,7 +333,8 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
224
333
|
if (!ws) return fail('this is not inside a registered workspace — open it with pmos first, or use --repo PATH');
|
|
225
334
|
const r = await call('POST', '/api/proactive/goals/plan', { ws });
|
|
226
335
|
if (!r || r.ok === false) return fail(`not started — ${(r && r.error) || 'the server did not answer'}`);
|
|
227
|
-
|
|
336
|
+
if (r.running) { say(`the morning-brief session ${r.session} is still running — nothing new was started. See the run with: pmos proactive day`); return 0; }
|
|
337
|
+
say(`${r.seeded ? `scheduled the morning brief daily at ${r.schedule.time}, and ` : ''}started a morning-brief session to plan ${r.goals} goal(s). The plan shows when it submits the brief; then the agent's tasks start. See them with: pmos proactive day`);
|
|
228
338
|
return 0;
|
|
229
339
|
}
|
|
230
340
|
if (sub !== 'list') return fail(usage);
|
|
@@ -248,11 +358,14 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
248
358
|
if (verb === 'brief') {
|
|
249
359
|
const sub = pos[0] || 'show';
|
|
250
360
|
if (sub === 'submit') {
|
|
251
|
-
if (!flags.file) return fail('usage: pmos proactive brief submit --file PATH');
|
|
361
|
+
if (!flags.file) return fail('usage: pmos proactive brief submit --file PATH [--merge]');
|
|
252
362
|
let raw; try { raw = JSON.parse(fs.readFileSync(path.resolve(cwd, flags.file), 'utf8')); } catch (e) { return fail(`could not read ${flags.file} as JSON — ${e.message}`); }
|
|
363
|
+
const merge = 'merge' in flags;
|
|
364
|
+
if (merge && raw && typeof raw === 'object' && !Array.isArray(raw)) raw.merge = true;
|
|
253
365
|
const r = await call('POST', '/api/proactive/brief', raw);
|
|
254
366
|
if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the server did not answer'}. Nothing was stored.`);
|
|
255
|
-
say(`
|
|
367
|
+
if (merge) say(`merged into the brief for ${r.brief.date} — ${r.merged ? r.merged.found : 0} new, ${r.merged ? r.merged.resolved : 0} resolved; it now holds ${r.brief.priorities.length} priorities, ${r.brief.worth_asking.length} worth asking`);
|
|
368
|
+
else say(`stored the brief for ${r.brief.date} — ${r.brief.priorities.length} priorities, ${r.brief.worth_asking.length} worth asking`);
|
|
256
369
|
return 0;
|
|
257
370
|
}
|
|
258
371
|
if (sub === 'schedule') {
|