create-pmos 0.54.1 → 0.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pmos.js +3 -1
- 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/proactive-engine.md +73 -9
- package/workspace/guide/today.md +27 -2
- package/workspace/lib/brief.js +268 -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/components.css +135 -23
- package/workspace/public/pm/dest-today.js +10 -8
- package/workspace/public/pm/today-copy.js +185 -0
- package/workspace/public/pm/today-day.js +766 -0
- package/workspace/public/pm/today-goals.js +96 -161
- package/workspace/replaced-tests.json +10 -0
- package/workspace/server.js +213 -10
- package/workspace/skills/day-check.skill +56 -0
- package/workspace/skills/day-task.skill +47 -0
- package/workspace/skills/morning-brief.skill +47 -10
- package/workspace/test-files.json +2 -0
|
@@ -1,206 +1,141 @@
|
|
|
1
|
-
/* pm/today-goals.js — the goals for today
|
|
1
|
+
/* pm/today-goals.js — the goals for today inside the day section (initiative today-goals, PRD G5, G6 and G16).
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* goalRows(ctx) → the left column's goal rows: number, text, jobs done, a small bar
|
|
4
|
+
* goalAdd(ctx) → the add input under them ("Add a goal and press Enter")
|
|
5
|
+
* goalDetail(ctx, goalId) → the right column's detail of one goal: its meetings, its jobs, the steps found, and the
|
|
6
|
+
* presses done, drop, reopen and remove; on an agent step that names an initiative, Prepare
|
|
7
|
+
* then Queue it (the shipped background prepare: one approval is one prompt hash)
|
|
8
|
+
* addGoal(ctx, text, ini) → POST the add; true when the goal is on the list
|
|
9
|
+
* goalsSig() → a string that changes whenever what this module draws changes on its own
|
|
5
10
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* morning-brief schedule) and, on an agent step that names an initiative, Prepare then Queue it (the shipped background
|
|
9
|
-
* prepare: one approval is one prompt hash).
|
|
10
|
-
*
|
|
11
|
-
* FIRST PAINT. Folded, the section is one control (its toggle) and a list of the goals as text, so Today keeps inside its
|
|
12
|
-
* control budget (first-paint-today.test.js). The input and the buttons are drawn only when the PM opens it.
|
|
11
|
+
* `ctx` is the day section's (today-day.js): { ws, m (the day model), select(type, id), reload(), rerender(), cur(type, id) }.
|
|
12
|
+
* The server joins the goals to the brief and Today by goal id or initiative (lib/goals.js planDay). Nothing here guesses.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { el } from './dom.js';
|
|
16
|
-
import { button, field, badge, toast } from './primitives.js';
|
|
17
|
-
import {
|
|
18
|
-
import { FIXTURE } from './state.js';
|
|
16
|
+
import { button, field, badge, toast, row, meter } from './primitives.js';
|
|
17
|
+
import { pmPost } from './http.js';
|
|
19
18
|
import { COPY } from './today-copy.js';
|
|
20
19
|
|
|
21
20
|
const G = COPY.goals;
|
|
22
|
-
const
|
|
23
|
-
const cache = new Map(); // ws -> { data, error, at }
|
|
24
|
-
let open = false;
|
|
21
|
+
const D = COPY.day;
|
|
25
22
|
let draft = '';
|
|
26
23
|
const prepared = new Map(); // step ref -> { key, branch, item_id, prompt_sha, initiative_id, ws } (a plan shown, not yet queued)
|
|
27
|
-
let rerender = () => {};
|
|
28
|
-
let timer = null;
|
|
29
|
-
|
|
30
|
-
async function load(ws) {
|
|
31
|
-
const out = await pmGet('/api/proactive/goals' + (ws ? '?ws=' + encodeURIComponent(ws) : ''));
|
|
32
|
-
const prev = cache.get(ws);
|
|
33
|
-
const next = out && out.ok ? { data: out, error: null, at: Date.now() } : { data: prev ? prev.data : null, error: (out && out.error) || G.unreached, at: Date.now() };
|
|
34
|
-
cache.set(ws, next);
|
|
35
|
-
if (!prev || JSON.stringify(prev.data) !== JSON.stringify(next.data) || prev.error !== next.error) rerender();
|
|
36
|
-
}
|
|
37
24
|
|
|
38
|
-
function
|
|
39
|
-
if (!cache.has(ws)) { cache.set(ws, { data: null, error: null, at: 0 }); load(ws); }
|
|
40
|
-
if (!timer && typeof window !== 'undefined') {
|
|
41
|
-
timer = setInterval(() => { for (const w of cache.keys()) load(w); }, POLL_MS);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function press(ws, body) {
|
|
25
|
+
async function press(ctx, body) {
|
|
46
26
|
const r = await pmPost('/api/proactive/goals', body);
|
|
47
27
|
if (!r || r.ok === false) { toast({ text: G.refused((r && r.error) || G.unreached), tone: 'destructive' }); return false; }
|
|
48
|
-
await
|
|
28
|
+
await ctx.reload();
|
|
49
29
|
return true;
|
|
50
30
|
}
|
|
51
31
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
32
|
+
/** Add a goal. `ini` ties it to an initiative, so the brief's items and meetings of that initiative join it. */
|
|
33
|
+
export function addGoal(ctx, text, ini) {
|
|
34
|
+
const body = { action: 'add', text: String(text || '').trim().slice(0, 140) };
|
|
35
|
+
if (ini) body.initiative_id = ini;
|
|
36
|
+
return press(ctx, body);
|
|
37
|
+
}
|
|
55
38
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
39
|
+
/** The jobs of a goal: the day model's jobs that name it. */
|
|
40
|
+
const jobsOf = (m, id) => m.jobs.filter((j) => j.goal === id && !j.worth);
|
|
41
|
+
|
|
42
|
+
export function goalRows(ctx) {
|
|
43
|
+
const { m } = ctx;
|
|
44
|
+
if (!m.goals.length) return null;
|
|
45
|
+
return el('ul', { class: 'td-day__rows', 'data-day-list': 'goals' }, ...m.goals.map((g, i) => {
|
|
46
|
+
const js = jobsOf(m, g.id);
|
|
47
|
+
const done = js.filter((j) => j.group === 'done').length;
|
|
48
|
+
const need = js.filter((j) => j.group === 'needs').length;
|
|
49
|
+
const closed = g.status !== 'open';
|
|
50
|
+
const sub = closed ? (G.coverage[g.coverage] || g.status) : js.length ? D.goalJobs(done, js.length) + (need ? ' · ' + D.goalNeed(need) : '') : (G.coverage[g.coverage] || D.goalNone);
|
|
51
|
+
const cur = ctx.cur('goal', g.id);
|
|
52
|
+
return el('li', null, row({
|
|
53
|
+
title: g.text, meta: [sub], done: closed, onOpen: () => ctx.select('goal', g.id),
|
|
54
|
+
lead: [el('span', { class: 'td-day__lead' }, String(i + 1))],
|
|
55
|
+
trail: [meter({ done: js.length ? done : g.status === 'done' ? 1 : 0, total: js.length || 1, label: '' })],
|
|
56
|
+
attrs: { class: 'td-day__r', 'data-day-sel': 'goal:' + g.id, 'data-goal': g.id, 'data-goal-coverage': g.coverage, 'data-cur': cur },
|
|
57
|
+
openAttrs: { 'aria-current': cur },
|
|
58
|
+
}));
|
|
59
|
+
}));
|
|
75
60
|
}
|
|
76
61
|
|
|
77
|
-
|
|
78
|
-
|
|
62
|
+
export function goalAdd(ctx) {
|
|
63
|
+
const { m } = ctx;
|
|
64
|
+
const max = m.maxGoals;
|
|
65
|
+
if (m.goals.length >= max) return el('p', { class: 'td-day__note' }, G.full(max));
|
|
66
|
+
const f = field({ label: G.inputLabel, value: '', placeholder: G.inputHint, hideLabel: true, attrs: { 'data-goals': 'input', 'data-day-input': 'goal', maxlength: '140' },
|
|
67
|
+
onCommit: async (v) => {
|
|
68
|
+
if (!String(v).trim()) { draft = v; return; }
|
|
69
|
+
// clear before the press: its reload re-renders the section, and a rebuild must not bring the text back
|
|
70
|
+
draft = '';
|
|
71
|
+
if (await addGoal(ctx, v)) { if (f.control.resetCommitted) f.control.resetCommitted(''); f.control.value = ''; ctx.rerender(); }
|
|
72
|
+
else { draft = v; ctx.rerender(); }
|
|
73
|
+
} });
|
|
74
|
+
// a rebuild keeps what is typed. The field is made empty and filled after, so Enter still commits the restored text.
|
|
75
|
+
f.control.value = draft;
|
|
76
|
+
f.control.addEventListener('input', () => { draft = f.control.value; });
|
|
77
|
+
return el('div', { class: 'td-day__goaladd' }, f);
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
async function prepareStep(
|
|
82
|
-
const r = await pmPost('/api/proactive/prepare/plan', { ws, initiative_id: st.initiative_id });
|
|
80
|
+
async function prepareStep(ctx, st) {
|
|
81
|
+
const r = await pmPost('/api/proactive/prepare/plan', { ws: ctx.ws, initiative_id: st.initiative_id });
|
|
83
82
|
if (!r || r.ok === false) { toast({ text: G.refused((r && r.error) || G.unreached), tone: 'destructive' }); return; }
|
|
84
83
|
if (r.refused) { toast({ text: G.refused(r.refused.why) }); return; }
|
|
85
|
-
prepared.set(st.ref, { key: r.item.key, branch: r.plan && r.plan.branch, item_id: r.item.id, prompt_sha: r.prompt_sha, initiative_id: st.initiative_id, ws: r.ws || ws });
|
|
86
|
-
rerender();
|
|
84
|
+
prepared.set(st.ref, { key: r.item.key, branch: r.plan && r.plan.branch, item_id: r.item.id, prompt_sha: r.prompt_sha, initiative_id: st.initiative_id, ws: r.ws || ctx.ws });
|
|
85
|
+
ctx.rerender();
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
async function queueStep(ref) {
|
|
88
|
+
async function queueStep(ctx, ref) {
|
|
90
89
|
const p = prepared.get(ref);
|
|
91
90
|
if (!p) return;
|
|
92
91
|
const r = await pmPost('/api/proactive/prepare/queue', { confirm: true, ws: p.ws, initiative_id: p.initiative_id, item_id: p.item_id, prompt_sha: p.prompt_sha });
|
|
93
92
|
prepared.delete(ref);
|
|
94
93
|
toast({ text: r && r.ok ? G.queued(p.key) : G.refused((r && r.error) || G.unreached), tone: r && r.ok ? undefined : 'destructive' });
|
|
95
|
-
rerender();
|
|
94
|
+
ctx.rerender();
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
function stepNode(ws, st, opened) {
|
|
101
|
-
const who = st.owner === 'pm' ? G.you : G.agent;
|
|
97
|
+
function stepNode(ctx, st) {
|
|
102
98
|
const meta = [st.source, st.mention ? G.mention : null, st.effort].filter(Boolean).join(' · ');
|
|
103
99
|
const p = prepared.get(st.ref);
|
|
104
|
-
const acts = !
|
|
100
|
+
const acts = !st.prepare ? null
|
|
105
101
|
: p ? el('span', { class: 'td-goal__prep' }, el('span', { class: 'td-goal__prep-text' }, G.prepareShows(p.key, p.branch)),
|
|
106
|
-
button({ label: G.queue, size: 'sm', attrs: { 'data-goal-queue': st.ref }, onClick: () => queueStep(st.ref) }))
|
|
107
|
-
: button({ label: G.prepare, size: 'sm', variant: 'ghost', attrs: { 'data-goal-prepare': st.ref }, onClick: () => prepareStep(
|
|
102
|
+
button({ label: G.queue, size: 'sm', attrs: { 'data-goal-queue': st.ref }, onClick: () => queueStep(ctx, st.ref) }))
|
|
103
|
+
: button({ label: G.prepare, size: 'sm', variant: 'ghost', attrs: { 'data-goal-prepare': st.ref }, onClick: () => prepareStep(ctx, st) });
|
|
108
104
|
return el('li', { class: 'td-goal__step', 'data-goal-step': st.ref },
|
|
109
|
-
badge({ text:
|
|
105
|
+
badge({ text: st.owner === 'pm' ? G.you : G.agent, tone: st.owner === 'pm' ? 'outline' : 'muted' }),
|
|
110
106
|
el('span', { class: 'td-goal__step-title' }, st.title),
|
|
111
107
|
meta ? el('span', { class: 'td-goal__meta' }, meta) : null,
|
|
112
108
|
acts);
|
|
113
109
|
}
|
|
114
110
|
|
|
115
|
-
|
|
111
|
+
/** The detail of one goal. `row(type, id, label, lead, trail)` draws a selectable row (today-day.js). */
|
|
112
|
+
export function goalDetail(ctx, id, row) {
|
|
113
|
+
const { m } = ctx;
|
|
114
|
+
const i = m.goals.findIndex((g) => g.id === id);
|
|
115
|
+
if (i < 0) return null;
|
|
116
|
+
const g = m.goals[i];
|
|
116
117
|
const closed = g.status !== 'open';
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
el('div', { class: 'td-
|
|
129
|
-
el('span', { class: 'td-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function stepList(ws, title, steps, attr) {
|
|
137
|
-
if (!steps.length) return null;
|
|
138
|
-
return el('div', { class: 'td-goals__also', 'data-goals': attr },
|
|
139
|
-
el('p', { class: 'td-goals__sub' }, title),
|
|
140
|
-
el('ul', { class: 'td-goal__steps' }, ...steps.map((st) => stepNode(ws, st, true))));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** The section. `ws` is the repository whose Today cards the goals are joined to. */
|
|
144
|
-
export function goalsSection({ ws, setState } = {}) {
|
|
145
|
-
if (FIXTURE) return null;
|
|
146
|
-
// like Today's own feeds, a read that lands re-renders only while the section is on the page: a hidden Today must not
|
|
147
|
-
// cause a whole-page render under whatever the PM is doing elsewhere
|
|
148
|
-
if (setState) rerender = () => { if (document.querySelector('[data-today="goals"]')) setState({}); };
|
|
149
|
-
ensure(ws);
|
|
150
|
-
const c = cache.get(ws) || {};
|
|
151
|
-
const d = c.data;
|
|
152
|
-
const plan = d && d.plan;
|
|
153
|
-
const goals = plan ? plan.goals : [];
|
|
154
|
-
const openN = goals.filter((g) => g.status === 'open').length;
|
|
155
|
-
const toggle = button({ label: G.toggle(openN, goals.length), variant: 'ghost', iconEnd: open ? 'chevron-down' : 'chevron-right',
|
|
156
|
-
attrs: { 'data-goals': 'toggle', 'aria-expanded': open ? 'true' : 'false' }, onClick: () => { open = !open; rerender(); } });
|
|
157
|
-
const list = goals.length ? el('ol', { class: 'td-goals__list', 'data-goals': 'list' }, ...goals.map((g) => goalNode(ws, g, open))) : null;
|
|
158
|
-
const kids = [el('div', { class: 'td-goals__head' }, toggle), list];
|
|
159
|
-
if (c.error) kids.push(el('p', { class: 'td-goals__note', role: 'status' }, G.refused(c.error)));
|
|
160
|
-
if (open) {
|
|
161
|
-
if (!goals.length) kids.push(el('p', { class: 'td-goals__note' }, G.intro));
|
|
162
|
-
const max = (d && d.max_goals) || 7;
|
|
163
|
-
if (goals.length < max) {
|
|
164
|
-
const f = field({ label: G.inputLabel, value: draft, placeholder: G.inputHint, hideLabel: true, attrs: { 'data-goals': 'input', maxlength: '140' },
|
|
165
|
-
onCommit: async (v) => {
|
|
166
|
-
if (!String(v).trim()) { draft = v; return; }
|
|
167
|
-
// clear before the press: its reload re-renders the section, and a rebuild must not bring the text back
|
|
168
|
-
draft = '';
|
|
169
|
-
if (await press(ws, { action: 'add', text: v })) { if (f.control.resetCommitted) f.control.resetCommitted(''); f.control.value = ''; rerender(); }
|
|
170
|
-
else { draft = v; rerender(); }
|
|
171
|
-
} });
|
|
172
|
-
f.control.addEventListener('input', () => { draft = f.control.value; }); // a rebuild keeps what is typed
|
|
173
|
-
kids.push(f);
|
|
174
|
-
} else kids.push(el('p', { class: 'td-goals__note' }, G.full(max)));
|
|
175
|
-
const run = runs.get(ws);
|
|
176
|
-
const busy = !!run && run.state === 'busy';
|
|
177
|
-
kids.push(el('div', { class: 'td-goals__bar' },
|
|
178
|
-
button({ label: busy ? G.starting : G.prepareDay, attrs: { 'data-goals': 'prepare-day', title: G.prepareDayHint, 'aria-busy': busy ? 'true' : 'false' },
|
|
179
|
-
disabled: !openN || busy, onClick: () => prepareDay(ws) }),
|
|
180
|
-
run && run.state === 'started' && run.session
|
|
181
|
-
? button({ label: G.openSession, variant: 'ghost', attrs: { 'data-goals': 'open-session' }, onClick: () => openSession(run.session) }) : null));
|
|
182
|
-
if (run && run.state !== 'busy') {
|
|
183
|
-
kids.push(el('p', { class: 'td-goals__run' + (run.state === 'failed' ? ' td-goals__run--failed' : ''), 'data-goals': 'run', 'data-run': run.state,
|
|
184
|
-
role: run.state === 'failed' ? 'alert' : 'status' }, run.state === 'failed' ? G.runFailed(run.error) : G.runStarted(hhmm(run.at))));
|
|
185
|
-
}
|
|
186
|
-
if (plan) {
|
|
187
|
-
const lines = [];
|
|
188
|
-
if (!plan.brief) lines.push(G.noBrief);
|
|
189
|
-
else if (!plan.brief.fresh) lines.push(G.stale(plan.brief.date));
|
|
190
|
-
if (plan.time) {
|
|
191
|
-
lines.push(G.free(plan.time.free_minutes, plan.time.until));
|
|
192
|
-
if (plan.time.next) lines.push(G.comingUp(plan.time.next.title, hhmm(plan.time.next.start)));
|
|
193
|
-
}
|
|
194
|
-
if (plan.not_read && plan.not_read.length) lines.push(G.notRead(plan.not_read.map((x) => x.source).join(', ')));
|
|
195
|
-
if (lines.length) kids.push(el('p', { class: 'td-goals__note', 'data-goals': 'context' }, lines.join(' ')));
|
|
196
|
-
kids.push(stepList(ws, G.also, plan.also || [], 'also'));
|
|
197
|
-
kids.push(stepList(ws, G.worthAsking, plan.worth_asking || [], 'worth-asking'));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return el('section', { class: 'td-goals', 'data-today': 'goals', 'aria-label': G.title }, ...kids.filter(Boolean));
|
|
118
|
+
const js = jobsOf(m, g.id);
|
|
119
|
+
const meets = m.events.filter((e) => e.goal === g.id);
|
|
120
|
+
const ctrls = closed
|
|
121
|
+
? [button({ label: G.reopen, size: 'sm', attrs: { 'data-goal-act': 'reopen' }, onClick: () => press(ctx, { action: 'reopen', id: g.id }) })]
|
|
122
|
+
: [button({ label: G.done, size: 'sm', attrs: { 'data-goal-act': 'done' }, onClick: () => press(ctx, { action: 'done', id: g.id }) }),
|
|
123
|
+
button({ label: G.drop, size: 'sm', variant: 'ghost', attrs: { 'data-goal-act': 'drop' }, onClick: () => press(ctx, { action: 'drop', id: g.id }) })];
|
|
124
|
+
ctrls.push(button({ label: G.remove, size: 'sm', variant: 'ghost', attrs: { 'data-goal-act': 'remove' },
|
|
125
|
+
onClick: async () => { if (await press(ctx, { action: 'remove', id: g.id })) ctx.select(null, null); } }));
|
|
126
|
+
const steps = (g.steps || []).filter((st) => st.from !== 'brief' || st.prepare); // brief items show as jobs; Today cards and preparable steps here
|
|
127
|
+
return [
|
|
128
|
+
el('div', { class: 'td-day__dhead' }, el('span', { class: 'td-day__kind' }, D.kind.goal(i + 1)), el('h3', null, g.text)),
|
|
129
|
+
el('div', { class: 'td-day__acts' }, badge({ text: G.coverage[g.coverage] || g.coverage, tone: g.coverage === 'needs-plan' ? 'warning' : 'outline' }),
|
|
130
|
+
g.initiative_id ? el('span', { class: 'td-day__mono' }, g.initiative_id) : null),
|
|
131
|
+
js.length ? el('p', null, D.goalJobs(js.filter((j) => j.group === 'done').length, js.length)) : el('p', { class: 'td-day__note' }, G.noStep),
|
|
132
|
+
meets.length ? el('div', { class: 'td-day__rel' }, el('h4', null, D.meetings), el('ul', { class: 'td-day__rows' }, ...meets.map((e) => row('event', e.id, e.title, e.time)))) : null,
|
|
133
|
+
js.length ? el('div', { class: 'td-day__rel' }, el('h4', null, D.jobs), el('ul', { class: 'td-day__rows' }, ...js.map((j) => row('job', j.id, j.title, '', j)))) : null,
|
|
134
|
+
!closed && steps.length ? el('div', { class: 'td-day__rel', 'data-goals': 'steps' }, el('h4', null, D.stepsTitle), el('ul', { class: 'td-goal__steps' }, ...steps.map((st) => stepNode(ctx, st)))) : null,
|
|
135
|
+
el('div', { class: 'td-day__acts' }, ...ctrls),
|
|
136
|
+
];
|
|
201
137
|
}
|
|
202
138
|
|
|
203
|
-
export function goalsSig(
|
|
204
|
-
|
|
205
|
-
return JSON.stringify([open, draft === '', c.data && c.data.plan, c.error, [...prepared.keys()], runs.get(ws) || null]);
|
|
139
|
+
export function goalsSig() {
|
|
140
|
+
return JSON.stringify([draft === '', [...prepared.keys()]]);
|
|
206
141
|
}
|
|
@@ -40,6 +40,16 @@
|
|
|
40
40
|
"I6 an unknown brief source is refused with the allowed names"
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
"id": "STG-MAX30",
|
|
45
|
+
"file": "workspace/proactive.test.js",
|
|
46
|
+
"run": "today-goals",
|
|
47
|
+
"ref": "planning/prd/today-goals-prd.md G9 (a brief holds at most 30 items)",
|
|
48
|
+
"reason": "G8 asserted that 13 items are refused with 'at most 12'. today-goals G9 raises the limit to 30, so it now sends 31 items and expects 'at most 30'; the event limit and the field limits are asserted unchanged.",
|
|
49
|
+
"names": [
|
|
50
|
+
"G8 too many items, too many events and an over-long field are refused"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
43
53
|
{
|
|
44
54
|
"id": "S6-D86",
|
|
45
55
|
"file": "workspace/d86-seams.test.js",
|