create-pmos 0.55.3 → 0.55.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.55.3",
3
+ "version": "0.55.5",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@d183aab
1
+ kernel-source: pmos@39b0c74
2
2
  assembled: 2026-10-06
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-06T08:46:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-06T10:22:39Z"}
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-06T08:46:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-06T10:22:39Z"}
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-06T08:46:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-06T10:22:39Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -18,9 +18,23 @@ Press **Prepare my day** to start the day. It runs from the brief to the deliver
18
18
 
19
19
  If the morning-brief session stops before it submits a brief, the Read step says so and gives the reason. **Prepare my day** is then ready again, so press it to try once more. If the session says that its folder is not there, open the repository again from its current folder first.
20
20
 
21
- Each agent job runs in its own session, so you can open any job and follow or steer it. The workspace types the job's prompt into the session when the agent is ready to read, and presses Enter for you. When you approve or revise a job and its session is still open, the workspace uses that same session again.
21
+ Each agent job runs in its own session, so you can open any job and follow or steer it. The workspace types the job's prompt into the session when the agent is ready to read, and presses Enter for you. When you approve or revise a job and its session is still open, the workspace uses that same session again. Prepare again works the same way: when the morning-brief session is still open and has finished its read, or has printed nothing for two minutes, the workspace types the read into that session and runs it again.
22
22
 
23
- **Agent results**, at the top of the left column, counts the jobs and lists each finished one with its draft summary or the reason it failed. The other things that wait for you, such as outcome checks, report drafts and sessions, are in **Also waiting for you** at the end of the left column. When the day is folded, they show under it.
23
+ **Agent results**, at the top of the left column, counts the jobs and lists each finished one with its draft summary or the reason it failed. Your goals for today come under it, above the calendar. The other things that wait for you, such as outcome checks, report drafts and sessions, are in **Also waiting for you** at the end of the left column. When the day is folded, they show under it.
24
+
25
+ ### One day for every repository
26
+
27
+ The day is yours, not a repository's. Every repository shows the same calendar, goals, jobs and mentions. To narrow the day, use the **Show** switch at the top of Today:
28
+
29
+ - **All repositories** shows the whole day. A job, mention, goal or watched place that belongs to a repository carries its name.
30
+ - **Repository** shows the items of the repository you opened Today from, and the items that belong to no repository. The calendar always shows.
31
+
32
+ To tie an item to a repository:
33
+
34
+ 1. For a goal, open it and pick a **Repository**. A goal you add while Today shows one repository belongs to that repository.
35
+ 2. For a Slack channel, Jira project or Confluence space, open **Sources**, open the service, and pick a repository next to the place.
36
+
37
+ 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 neither belongs to no repository.
24
38
 
25
39
  Each job has one of three kinds.
26
40
 
@@ -7,9 +7,16 @@
7
7
  *
8
8
  * emptyDay(date) → { date, goals: [] }
9
9
  * edit(day, op, { now }) → { ok:true, day } | { ok:false, error }
10
- * op: { action: 'add', text, initiative_id? } | { action: 'done'|'drop'|'reopen'|'remove', id }
10
+ * op: { action: 'add', text, initiative_id?, repo? } | { action: 'done'|'drop'|'reopen'|'remove', id }
11
+ * | { action: 'tag', id, repo } (repo: a workspace id, or null for none)
11
12
  * readDay(raw, date) → a checked day record, or an empty day when `raw` is not one
12
13
  * planDay({ day, brief, cards, now, endOfDay }) → the plan Today and `pmos proactive goals` show
14
+ * itemRepos({ day, brief, placeRepos }) → { itemId: repo } for the brief items that have a repository
15
+ *
16
+ * ONE DAY, REPOSITORY TAGS. The day is the PM's, not a repository's: every repository shows the same goals, brief and
17
+ * jobs. A goal can carry a repository (`repo`, a workspace id), and so can a channel, project or space (the settings'
18
+ * `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 item with neither is personal and shows under every repository.
13
20
  *
14
21
  * 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
22
  * session sets it). Failing that, an item or Today card belongs to the first open goal tied to the same initiative. There
@@ -25,7 +32,9 @@ const crypto = require('crypto');
25
32
  const MAX_GOALS = 7;
26
33
  const MAX_TEXT = 140;
27
34
  const STATUSES = ['open', 'done', 'dropped'];
28
- const ACTIONS = ['add', 'done', 'drop', 'reopen', 'remove'];
35
+ const ACTIONS = ['add', 'done', 'drop', 'reopen', 'remove', 'tag'];
36
+ const REPO_RE = /^[A-Za-z0-9._-]{1,80}$/;
37
+ const repoOrNull = (r) => (typeof r === 'string' && REPO_RE.test(r) ? r : null);
29
38
  const INI_RE = /^[a-z0-9][a-z0-9._-]*$/i;
30
39
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
31
40
  const GOAL_ID_RE = /^g-[0-9a-f]{8}$/;
@@ -42,6 +51,7 @@ function readDay(raw, date) {
42
51
  && STATUSES.includes(g.status)).slice(0, MAX_GOALS).map((g) => ({
43
52
  id: g.id, text: clip(g.text, MAX_TEXT), initiative_id: typeof g.initiative_id === 'string' && INI_RE.test(g.initiative_id) ? g.initiative_id : null,
44
53
  status: g.status, added_at: typeof g.added_at === 'string' ? g.added_at : null, closed_at: typeof g.closed_at === 'string' ? g.closed_at : null,
54
+ repo: repoOrNull(g.repo),
45
55
  }));
46
56
  return { date, goals };
47
57
  }
@@ -58,16 +68,22 @@ function edit(day, op, { now = Date.now() } = {}) {
58
68
  if (goals.length >= MAX_GOALS) return { ok: false, error: `a day has at most ${MAX_GOALS} goals. Drop or remove one first.` };
59
69
  const ini = op.initiative_id == null || op.initiative_id === '' ? null : op.initiative_id;
60
70
  if (ini !== null && (typeof ini !== 'string' || !INI_RE.test(ini))) return { ok: false, error: 'initiative_id is not an initiative id' };
71
+ if (op.repo != null && op.repo !== '' && !repoOrNull(op.repo)) return { ok: false, error: 'repo is not a workspace id' };
61
72
  const text = clip(op.text, MAX_TEXT);
62
73
  if (goals.some((g) => g.text.toLowerCase() === text.toLowerCase())) return { ok: false, error: 'that goal is already on the list' };
63
74
  const id = 'g-' + crypto.createHash('sha1').update(`${day.date}|${text}|${at}|${goals.length}`).digest('hex').slice(0, 8);
64
- goals.push({ id, text, initiative_id: ini, status: 'open', added_at: at, closed_at: null });
75
+ goals.push({ id, text, initiative_id: ini, status: 'open', added_at: at, closed_at: null, repo: repoOrNull(op.repo) });
65
76
  return { ok: true, day: { date: day.date, goals }, goal: goals[goals.length - 1] };
66
77
  }
67
78
  const i = goals.findIndex((g) => g.id === op.id);
68
79
  if (i < 0) return { ok: false, error: 'no goal with that id today' };
69
80
  const g = goals[i];
70
81
  if (op.action === 'remove') { goals.splice(i, 1); return { ok: true, day: { date: day.date, goals }, goal: g }; }
82
+ if (op.action === 'tag') {
83
+ if (op.repo != null && op.repo !== '' && !repoOrNull(op.repo)) return { ok: false, error: 'repo is not a workspace id' };
84
+ g.repo = repoOrNull(op.repo);
85
+ return { ok: true, day: { date: day.date, goals }, goal: g };
86
+ }
71
87
  const to = { done: 'done', drop: 'dropped', reopen: 'open' }[op.action];
72
88
  if (g.status === to) return { ok: false, error: `that goal is already ${to}` };
73
89
  g.status = to; g.closed_at = to === 'open' ? null : at;
@@ -184,4 +200,28 @@ function planDay({ day, brief = null, cards = [], now = Date.now(), endOfDay = E
184
200
  };
185
201
  }
186
202
 
187
- module.exports = { MAX_GOALS, MAX_TEXT, STATUSES, ACTIONS, GOAL_ID_RE, START_OF_DAY, END_OF_DAY, emptyDay, readDay, edit, freeTime, planDay };
203
+ /** The place an item came from, as a key that a place name can match: lower case, no leading '#', and a Jira issue key
204
+ * (PAY-412) reduced to its project key (pay). A place named "PAY · Payments" matches by its first word. */
205
+ 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
+ const placeNameKeys = (n) => { const t = String(n || '').trim().toLowerCase().replace(/^#/, ''); return [t, t.split(/[\s·:|]+/)[0]].filter(Boolean); };
207
+
208
+ /** { itemId: repo } for today's brief items: the repository of the open goal an item is under, else of its place. */
209
+ function itemRepos({ day, brief, placeRepos = {} } = {}) {
210
+ const out = {};
211
+ if (!brief || !day || brief.date !== day.date) return out;
212
+ const open = (day.goals || []).filter((g) => g.status === 'open');
213
+ const byId = new Map(open.map((g) => [g.id, g]));
214
+ const byIni = (ini) => (ini ? open.find((g) => g.initiative_id === ini) || null : null);
215
+ for (const x of [...(brief.priorities || []), ...(brief.worth_asking || [])]) {
216
+ const g = (x.goal && byId.get(x.goal)) || byIni(x.initiative_id);
217
+ let r = g ? repoOrNull(g.repo) : null;
218
+ if (!r && x.where) {
219
+ const want = placeKey(x.where);
220
+ for (const [name, repo] of Object.entries(placeRepos[x.source] || {})) if (placeNameKeys(name).includes(want) && repoOrNull(repo)) { r = repo; break; }
221
+ }
222
+ if (r) out[x.id] = r;
223
+ }
224
+ return out;
225
+ }
226
+
227
+ module.exports = { MAX_GOALS, MAX_TEXT, STATUSES, ACTIONS, GOAL_ID_RE, START_OF_DAY, END_OF_DAY, emptyDay, readDay, edit, freeTime, planDay, itemRepos };
@@ -44,7 +44,7 @@ const MAX_WEBSITES = 10;
44
44
  const DAY_CHECK_EVERY = [0, 30, 60];
45
45
  const DAY_CHECK_DEFAULT = Object.freeze({ every: 60, start: '09:00', end: '18:00' });
46
46
  const DEFAULTS = Object.freeze({ name: '', briefSources: BRIEF_DEFAULT, briefScoring: false, backgroundPrepare: false, enabled: true, triggers: Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t, true]))), stalledDays: 5, quiet: null, pauseLearning: false,
47
- scopes: Object.freeze({ slack: Object.freeze({}), jira: Object.freeze({}), confluence: Object.freeze({}) }), dayCheck: DAY_CHECK_DEFAULT, websites: Object.freeze([]) });
47
+ scopes: Object.freeze({ slack: Object.freeze({}), jira: Object.freeze({}), confluence: Object.freeze({}) }), placeRepos: Object.freeze({ slack: Object.freeze({}), jira: Object.freeze({}), confluence: Object.freeze({}) }), dayCheck: DAY_CHECK_DEFAULT, websites: Object.freeze([]) });
48
48
 
49
49
  const idOf = (key) => crypto.createHash('sha1').update(String(key)).digest('hex').slice(0, 12);
50
50
  const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
@@ -178,7 +178,7 @@ const HHMM = /^([01]\d|2[0-3]):([0-5]\d)$/;
178
178
  function normalizeSettings(raw) {
179
179
  const r = raw && typeof raw === 'object' ? raw : {};
180
180
  const s = { name: DEFAULTS.name, briefSources: { ...BRIEF_DEFAULT }, briefScoring: DEFAULTS.briefScoring, backgroundPrepare: DEFAULTS.backgroundPrepare, enabled: DEFAULTS.enabled, triggers: { ...DEFAULTS.triggers }, stalledDays: DEFAULTS.stalledDays, quiet: DEFAULTS.quiet, pauseLearning: DEFAULTS.pauseLearning,
181
- scopes: { slack: {}, jira: {}, confluence: {} }, dayCheck: { ...DAY_CHECK_DEFAULT }, websites: [] };
181
+ scopes: { slack: {}, jira: {}, confluence: {} }, placeRepos: { slack: {}, jira: {}, confluence: {} }, dayCheck: { ...DAY_CHECK_DEFAULT }, websites: [] };
182
182
  if (r.enabled !== undefined) { if (typeof r.enabled !== 'boolean') return { ok: false, error: 'enabled must be true or false' }; s.enabled = r.enabled; }
183
183
  if (r.name !== undefined) { if (typeof r.name !== 'string' || r.name.length > 40) return { ok: false, error: 'name must be text of at most 40 characters' }; s.name = r.name.replace(/\s+/g, ' ').trim(); }
184
184
  if (r.briefScoring !== undefined) { if (typeof r.briefScoring !== 'boolean') return { ok: false, error: 'briefScoring must be true or false' }; s.briefScoring = r.briefScoring; }
@@ -228,6 +228,24 @@ function normalizeSettings(raw) {
228
228
  s.scopes[svc] = out;
229
229
  }
230
230
  }
231
+ // placeRepos: the repository a channel, project or space belongs to (a workspace id). Today shows its mentions and jobs
232
+ // under that repository; a place with none is personal. null clears it; the route merges a patch over what is stored.
233
+ if (r.placeRepos !== undefined) {
234
+ if (!r.placeRepos || typeof r.placeRepos !== 'object' || Array.isArray(r.placeRepos)) return { ok: false, error: `placeRepos must be an object of ${SCOPE_SERVICES.join(', ')} → {name: workspace id}` };
235
+ for (const [svc, names] of Object.entries(r.placeRepos)) {
236
+ if (!SCOPE_SERVICES.includes(svc)) return { ok: false, error: `unknown placeRepos service "${svc}" — one of ${SCOPE_SERVICES.join(', ')}` };
237
+ if (!names || typeof names !== 'object' || Array.isArray(names)) return { ok: false, error: `placeRepos.${svc} must be an object of name → workspace id` };
238
+ const out = {};
239
+ for (const [name, repo] of Object.entries(names)) {
240
+ if (repo === null || repo === '') continue;
241
+ if (!name.trim() || name.length > 120) return { ok: false, error: `a ${svc} name is 1 to 120 characters` };
242
+ if (typeof repo !== 'string' || !/^[A-Za-z0-9._-]{1,80}$/.test(repo)) return { ok: false, error: `the repository of ${svc} "${name}" must be a workspace id` };
243
+ out[name] = repo;
244
+ }
245
+ if (Object.keys(out).length > MAX_SCOPE_NAMES) return { ok: false, error: `placeRepos.${svc} has ${Object.keys(out).length} names — at most ${MAX_SCOPE_NAMES}` };
246
+ s.placeRepos[svc] = out;
247
+ }
248
+ }
231
249
  if (r.dayCheck !== undefined) {
232
250
  const d = r.dayCheck;
233
251
  if (!d || typeof d !== 'object' || Array.isArray(d)) return { ok: false, error: 'dayCheck must be {every, start, end}' };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.55.3",
3
+ "version": "0.55.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.55.3",
3
+ "version": "0.55.5",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -2362,6 +2362,9 @@
2362
2362
  .td-day__r[data-goal-tie="true"] { border-left: 3px solid var(--foreground); }
2363
2363
  .td-day__lead { min-width: 44px; font: 12px var(--mono); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
2364
2364
  .td-day__chips { display: inline-flex; gap: 4px; }
2365
+ .td-day__chips { align-items: center; flex-wrap: wrap; justify-content: flex-end; }
2366
+ .td-day__repo { font: inherit; font-size: 12px; color: var(--foreground); background: var(--background); border: 1px solid var(--border); border-radius: 0; padding: 2px 6px; max-width: 160px; }
2367
+ .td-day__repo:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
2365
2368
  .td-day__nowrow { display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 11px var(--mono); color: var(--destructive); }
2366
2369
  .td-day__nowrow::after { content: ""; flex: 1; border-top: 2px solid var(--destructive); }
2367
2370
  .td-day__gh { display: flex; align-items: center; gap: 6px; margin: 4px 8px 2px; font-size: 12px; font-weight: 600; color: var(--muted-foreground); }
@@ -125,7 +125,10 @@ function build(ctx0) {
125
125
  // the open day takes the list into its left column ("Also waiting for you"), so Today shows one layout; a folded day
126
126
  // leaves it below, as before
127
127
  const owed = { list, more, count: n };
128
- const day = daySection({ ws: dayWs, setState: ctx0.setState || setState, openIni: dayRepo ? (id) => openInitiative(dayRepo, id) : null, owed });
128
+ // one day for every repository: the day is the same everywhere; the Show switch narrows it to the repository Today
129
+ // was opened from (`here`) or shows all of it
130
+ const here = sc.repo ? wsOf(sc.repo) : null;
131
+ const day = daySection({ ws: dayWs, setState: ctx0.setState || setState, openIni: dayRepo ? (id) => openInitiative(dayRepo, id) : null, owed, here, all: sc.all });
129
132
  const inDay = !!(day && day.contains(list));
130
133
  return { children: inDay ? [head, day, health] : [head, day, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + daySig(dayWs) + nodes.map((x) => x.outerHTML).join('') };
131
134
  }
@@ -133,6 +133,11 @@ export const COPY = {
133
133
  c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
134
134
  summaryAll: (n) => `All ${n} agent ${n === 1 ? 'job has' : 'jobs have'} a result.`,
135
135
  waitingHint: 'Outcome checks, reports and sessions from your repositories, outside today\'s jobs.',
136
+ // one day for every repository: a goal or a place can carry a repository; the Show switch narrows the day to it
137
+ repo: { label: 'Repository', none: 'No repository',
138
+ goalHint: 'Its jobs show under this repository. With no repository, the goal is yours and shows under every repository.',
139
+ placeHint: 'Repository: its mentions and jobs show under that repository. With none, they show under every repository.',
140
+ only: (name) => `Showing ${name} and your items with no repository. The calendar always shows.` },
136
141
  now: (at) => `now ${at}`,
137
142
  noCalendar: 'No events read for today.',
138
143
  noJobs: 'No jobs yet. Press Prepare my day. Each job shows here; select one to see what it needs.',
@@ -130,7 +130,10 @@ function deriveStage(run, brief) {
130
130
  return 'done';
131
131
  }
132
132
 
133
- function model(d) {
133
+ /* One day for every repository. `view` is { here: the repository Today was opened from (a workspace id), all: the
134
+ * Show switch is on All repositories }. An item, a goal or a place carries a repository or none; with the switch on one
135
+ * repository, the day keeps that repository's items and the ones with none. The calendar is never narrowed. */
136
+ function model(d, view) {
134
137
  const today = d && d.today;
135
138
  const brief = d && d.brief && (!today || d.brief.date === today) ? d.brief : null;
136
139
  const plan = (d && d.plan) || null;
@@ -156,17 +159,31 @@ function model(d) {
156
159
  const level = (svc, name) => (scopes[svc] && scopes[svc][name]) || 'search';
157
160
  const digests = ((brief && Array.isArray(brief.digests)) ? brief.digests : []).map((g) => ({ ...g, id: g.source + '|' + g.name, level: level(g.source, g.name) }));
158
161
  const places = (brief && Array.isArray(brief.places)) ? brief.places : [];
162
+ const placeRepos = (settings && settings.placeRepos) || {};
163
+ const placeRepo = (svc, name) => (placeRepos[svc] && placeRepos[svc][name]) || null;
164
+ const itemRepos = (d && d.item_repos) || {};
165
+ const repos = (d && Array.isArray(d.repos)) ? d.repos : [];
166
+ const repoName = (id) => { const r = repos.find((x) => x.id === id); return r ? r.name : id; };
167
+ for (const j of jobs) j.repo = itemRepos[j.id] || null;
168
+ for (const g of digests) g.repo = placeRepo(g.source, g.name);
169
+ const v = view || {};
170
+ const narrow = !v.all && !!v.here;
171
+ const inView = (r) => !narrow || !r || r === v.here;
172
+ const shownJobs = jobs.filter((j) => inView(j.repo));
173
+ const shownIds = new Set(shownJobs.map((j) => j.task && j.task.id).filter(Boolean));
174
+ const shownTasks = narrow ? tasks.filter((t) => shownIds.has(t.id)) : tasks;
159
175
  return {
160
- d, brief, plan, goals, goalNo, run, tasks, jobs, events, now, settings, scopes, level, digests, places,
176
+ d, brief, plan, goals: goals.filter((g) => inView(g.repo)), allGoals: goals, goalNo, run, tasks: shownTasks, jobs: shownJobs, events, now, settings, scopes, level,
177
+ digests: digests.filter((g) => inView(g.repo)), places, placeRepo, repos, repoName, here: v.here || null, narrow,
161
178
  stage: run ? (d.stage || deriveStage(run, brief)) : null,
162
- mentions: jobs.filter((j) => j.item && j.item.mention),
179
+ mentions: shownJobs.filter((j) => j.item && j.item.mention),
163
180
  sources: (d && Array.isArray(d.sources)) ? d.sources : [],
164
181
  check: (d && d.check) || null,
165
182
  readFailed: (d && d.read_failed) || null,
166
183
  free: plan && plan.time ? plan.time.free_minutes : null,
167
184
  maxGoals: (d && d.max_goals) || 7,
168
- needs: jobs.filter((j) => j.group === 'needs').length,
169
- done: jobs.filter((j) => j.group === 'done').length,
185
+ needs: shownJobs.filter((j) => j.group === 'needs').length,
186
+ done: shownJobs.filter((j) => j.group === 'done').length,
170
187
  };
171
188
  }
172
189
 
@@ -179,6 +196,20 @@ const TONE = { '': 'muted', ok: 'outline', info: 'outline', goal: 'outline', war
179
196
  function pill(text, tone, attrs) {
180
197
  return badge({ text, tone: TONE[tone || ''] || 'muted', attrs });
181
198
  }
199
+ /** The repository a row belongs to, as a small tag. A row with none (yours) carries no tag. */
200
+ function repoTag(m, r) {
201
+ return r ? badge({ text: m.repoName(r), tone: 'outline', attrs: { 'data-day-repo': r, title: COPY.day.repo.label } }) : null;
202
+ }
203
+ /** A picker of the repository: none, or one of the registered repositories. `onPick(id|null)`. */
204
+ export function repoPicker(m, cur, onPick, attrs) {
205
+ const sel = el('select', { class: 'td-day__repo', 'aria-label': COPY.day.repo.label, ...(attrs || {}) },
206
+ el('option', { value: '' }, COPY.day.repo.none), ...m.repos.map((r) => el('option', { value: r.id }, r.name)),
207
+ // a repository that is no longer registered stays shown, so the picker never hides what is stored
208
+ cur && !m.repos.some((r) => r.id === cur) ? el('option', { value: cur }, cur) : null);
209
+ sel.value = cur || '';
210
+ sel.addEventListener('change', () => onPick(sel.value || null));
211
+ return sel;
212
+ }
182
213
  function statusPill(j) {
183
214
  const s = D.status2;
184
215
  const map = {
@@ -365,7 +396,7 @@ function jobRow(ctx, j) {
365
396
  const n = j.goal ? ctx.m.goalNo.get(j.goal) : null;
366
397
  const sub = j.status === 'failed' && j.task && j.task.error ? j.task.error
367
398
  : [j.item && j.item.where, srcName(j.source)].filter(Boolean).join(' · ');
368
- return rowBtn(ctx, 'job', j.id, { lead: n ? D.pill.goal(n) : '·', title: j.title, meta: sub, trail: statusPill(j),
399
+ return rowBtn(ctx, 'job', j.id, { lead: n ? D.pill.goal(n) : '·', title: j.title, meta: sub, trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), statusPill(j)),
369
400
  dot: j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null });
370
401
  }
371
402
 
@@ -412,7 +443,7 @@ function waitingPanel(owed) {
412
443
 
413
444
  function goalsPanel(ctx) {
414
445
  const { m } = ctx;
415
- return panel('goals', D.goalsCount(m.goals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), goalAdd(ctx));
446
+ return panel('goals', D.goalsCount(m.allGoals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), goalAdd(ctx));
416
447
  }
417
448
 
418
449
  function mentionStatus(j) {
@@ -423,7 +454,7 @@ function mentionStatus(j) {
423
454
 
424
455
  function digestRow(ctx, g) {
425
456
  const sub = D.digestLine(D.levelWord[g.level] || g.level, (g.decisions || []).length, (g.open || []).length);
426
- return rowBtn(ctx, 'channel', g.id, { lead: el('span', { class: 'td-day__chip' }, srcName(g.source)), title: g.name, meta: sub });
457
+ return rowBtn(ctx, 'channel', g.id, { lead: el('span', { class: 'td-day__chip' }, srcName(g.source)), title: g.name, meta: sub, trail: repoTag(ctx.m, g.repo) });
427
458
  }
428
459
 
429
460
  function mentionsPanel(ctx) {
@@ -438,7 +469,7 @@ function mentionsPanel(ctx) {
438
469
  const rows = shown.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'mentions' }, ...shown.map((j) => {
439
470
  const it = j.item;
440
471
  return rowBtn(ctx, 'mention', j.id, { lead: el('span', { class: 'td-day__chip' }, srcName(j.source)),
441
- title: it.about && it.ask ? `${it.about}: ${it.ask}` : it.ask || j.title, meta: D.fromWho(it.who, it.where) || it.why_now || '', trail: mentionStatus(j) });
472
+ title: it.about && it.ask ? `${it.about}: ${it.ask}` : it.ask || j.title, meta: D.fromWho(it.who, it.where) || it.why_now || '', trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mentionStatus(j)) });
442
473
  })) : note(D.noMentions);
443
474
  const live = m.digests.filter((g) => g.level !== 'ignore');
444
475
  const ign = m.digests.filter((g) => g.level === 'ignore');
@@ -469,7 +500,7 @@ function stepItem(st) {
469
500
  function eventDetail(ctx, e) {
470
501
  const { m } = ctx;
471
502
  const n = e.goal ? m.goalNo.get(e.goal) : null;
472
- const g = n ? m.goals[n - 1] : null;
503
+ const g = n ? m.allGoals[n - 1] : null;
473
504
  const ini = e.initiative && e.initiative.id ? e.initiative : null;
474
505
  const steps = e.steps || {};
475
506
  const notes = e.notes || {};
@@ -484,7 +515,7 @@ function eventDetail(ctx, e) {
484
515
  list(D.actions, (w.actions || []).map((a) => (typeof a === 'string' ? a : stepItem(a))))] : note(D.noWispr),
485
516
  mailBlock || note(D.noMail)) : null;
486
517
  const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text))
487
- : [note(D.tieHow, { 'data-day': 'tie' }), m.goals.length < m.maxGoals ? el('div', { class: 'td-day__acts' }, button({ label: D.addAsGoal, size: 'sm', attrs: { 'data-day-act': 'add-goal' },
518
+ : [note(D.tieHow, { 'data-day': 'tie' }), m.allGoals.length < m.maxGoals ? el('div', { class: 'td-day__acts' }, button({ label: D.addAsGoal, size: 'sm', attrs: { 'data-day-act': 'add-goal' },
488
519
  onClick: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null)) toast({ text: D.goalAdded }); } })) : null];
489
520
  return [
490
521
  dhead(D.kind.meeting, e.title),
@@ -557,6 +588,7 @@ function jobDetail(ctx, j) {
557
588
  const mention = j.item && j.item.mention;
558
589
  return [
559
590
  dhead(n ? D.kind.jobGoal(n) : D.kind.job, null),
591
+ j.repo ? el('div', { class: 'td-day__acts' }, repoTag(m, j.repo)) : null,
560
592
  jobCard(ctx, j),
561
593
  meets.length || mention ? el('div', { class: 'td-day__rel' }, el('h4', null, D.related), el('ul', { class: 'td-day__rows' },
562
594
  ...meets.map((e) => relRow(ctx, 'event', e.id, e.title, e.time)),
@@ -586,6 +618,10 @@ function setLevel(ctx, svc, name, lvl) {
586
618
  return setSettings(ctx.ws, { scopes });
587
619
  }
588
620
 
621
+ function setPlaceRepo(ctx, svc, name, repo) {
622
+ return setSettings(ctx.ws, { placeRepos: { [svc]: { [name]: repo } } });
623
+ }
624
+
589
625
  function channelDetail(ctx, id) {
590
626
  const g = ctx.m.digests.find((x) => x.id === id);
591
627
  if (!g) return null;
@@ -594,6 +630,7 @@ function channelDetail(ctx, id) {
594
630
  dhead(D.kind.place(srcName(g.source), D.place[g.source] || '', D.levelWord[g.level] || g.level), g.name),
595
631
  sec(D.decisions, g.decisions), sec(D.open, g.open), sec(D.forGoals, g.goals),
596
632
  el('div', { class: 'td-day__ch' }, el('span', null, D.changeLevel), levelSeg(ctx, g.source, g.name)),
633
+ el('div', { class: 'td-day__ch' }, el('span', null, D.repo.label), repoPicker(ctx.m, g.repo, (r) => setPlaceRepo(ctx, g.source, g.name, r), { 'data-day-place-repo': g.source + ':' + g.name })),
597
634
  note(D.levelHelp),
598
635
  el('div', { class: 'td-day__acts' }, extLink(D.openIn(srcName(g.source)), g.link, { 'data-day-link': 'place' }),
599
636
  button({ label: D.manageSources, size: 'sm', variant: 'ghost', onClick: () => select('sources', 'all') })),
@@ -619,7 +656,7 @@ function stepDetail(ctx, i) {
619
656
  if (i === 1) {
620
657
  const live = m.jobs.filter((j) => !j.worth);
621
658
  return [head, el('p', null, D.madeJobs(live.length, live.filter((j) => j.kind === 'you').length, live.filter((j) => j.kind !== 'you').length)),
622
- m.goals.length ? el('ul', { class: 'td-day__rows' }, ...m.goals.map((g, k) => relRow(ctx, 'goal', g.id, g.text, String(k + 1)))) : null];
659
+ m.goals.length ? el('ul', { class: 'td-day__rows' }, ...m.goals.map((g) => relRow(ctx, 'goal', g.id, g.text, String(m.goalNo.get(g.id))))) : null];
623
660
  }
624
661
  if (i === 2) {
625
662
  const ts = m.jobs.filter((j) => j.task);
@@ -641,7 +678,8 @@ function placeControls(ctx, svc) {
641
678
  // every place the last read found you in, then any you set a level for or that has a digest
642
679
  const names = [...new Set([...m.places.filter((x) => x.source === svc).map((x) => x.name), ...Object.keys(m.scopes[svc] || {}),
643
680
  ...m.digests.filter((g) => g.source === svc).map((g) => g.name)])];
644
- const row = (name) => el('div', { class: 'td-day__ch', 'data-day-place': svc + ':' + name }, el('span', null, name), levelSeg(ctx, svc, name));
681
+ const row = (name) => el('div', { class: 'td-day__ch', 'data-day-place': svc + ':' + name }, el('span', null, name),
682
+ el('span', { class: 'td-day__chips' }, repoPicker(m, m.placeRepo(svc, name), (r) => setPlaceRepo(ctx, svc, name, r), { 'data-day-place-repo': svc + ':' + name }), levelSeg(ctx, svc, name)));
645
683
  const live = names.filter((x) => m.level(svc, x) !== 'ignore');
646
684
  const ign = names.filter((x) => m.level(svc, x) === 'ignore');
647
685
  const input = field({ label: D.placeAdd[svc], value: '', placeholder: D.placeHint, hideLabel: true, attrs: { maxlength: '120', 'data-day-input': 'place-' + svc },
@@ -663,6 +701,7 @@ function placeControls(ctx, svc) {
663
701
  ui.showIgn[svc] ? ign.map(row) : null,
664
702
  input,
665
703
  note(D.levelHelp),
704
+ note(D.repo.placeHint),
666
705
  ];
667
706
  }
668
707
 
@@ -753,14 +792,14 @@ function fold(open) {
753
792
  }
754
793
 
755
794
  /** The section. `ws` is the repository the day run belongs to; `openIni(id)` opens an initiative of it. */
756
- export function daySection({ ws, setState, openIni, owed } = {}) {
795
+ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
757
796
  if (FIXTURE) return null;
758
797
  // like Today's own feeds, a read that lands re-renders only while the section is on the page: a hidden Today must not
759
798
  // cause a whole-page render under whatever the PM is doing elsewhere
760
799
  if (setState) rerender = () => { if (document.querySelector('[data-today="day"]')) setState({}); };
761
800
  ensure(ws);
762
801
  const c = cache.get(ws) || {};
763
- const m = model(c.data);
802
+ const m = model(c.data, { here: here || null, all: !!all });
764
803
  const open = ui.open !== null ? ui.open : !!m.run;
765
804
  if (!open) {
766
805
  return el('section', { class: 'td-day', 'data-today': 'day', 'data-open': 'false', 'aria-label': D.title },
@@ -768,7 +807,7 @@ export function daySection({ ws, setState, openIni, owed } = {}) {
768
807
  }
769
808
  const sel = ui.sel;
770
809
  const ctx = {
771
- ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select,
810
+ ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select, repoPicker,
772
811
  cur: (type, id) => { const s = sel || defaultSel(m); return s.type === type && s.id === String(id) ? 'true' : 'false'; },
773
812
  };
774
813
  ctx.sel = sel || defaultSel(m);
@@ -784,10 +823,11 @@ export function daySection({ ws, setState, openIni, owed } = {}) {
784
823
  return el('section', { class: 'td-day', 'data-today': 'day', 'data-open': 'true', 'aria-label': D.title },
785
824
  topBar(ctx),
786
825
  runLine(ctx),
826
+ m.narrow && m.repos.length > 1 ? note(D.repo.only(m.repoName(m.here)), { 'data-day': 'repo-view' }) : null,
787
827
  c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
788
828
  m.d && m.d.legacy ? note(D.oldServer) : null,
789
829
  el('div', { class: 'td-day__cols' },
790
- el('div', { class: 'td-day__left' }, summaryPanel(ctx), calendar(ctx), jobsPanel(ctx), goalsPanel(ctx), mentionsPanel(ctx), waitingPanel(owed)),
830
+ el('div', { class: 'td-day__left' }, summaryPanel(ctx), goalsPanel(ctx), calendar(ctx), jobsPanel(ctx), mentionsPanel(ctx), waitingPanel(owed)),
791
831
  detail(ctx)));
792
832
  }
793
833
 
@@ -5,7 +5,8 @@
5
5
  * goalDetail(ctx, goalId) → the right column's detail of one goal: its meetings, its jobs, the steps found, and the
6
6
  * presses done, drop, reopen and remove; on an agent step that names an initiative, Prepare
7
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
8
+ * addGoal(ctx, text, ini) → POST the add; true when the goal is on the list. While Today shows one repository, the
9
+ * goal belongs to it (repo); the goal's detail can change that.
9
10
  * goalsSig() → a string that changes whenever what this module draws changes on its own
10
11
  *
11
12
  * `ctx` is the day section's (today-day.js): { ws, m (the day model), select(type, id), reload(), rerender(), cur(type, id) }.
@@ -33,6 +34,8 @@ async function press(ctx, body) {
33
34
  export function addGoal(ctx, text, ini) {
34
35
  const body = { action: 'add', text: String(text || '').trim().slice(0, 140) };
35
36
  if (ini) body.initiative_id = ini;
37
+ // one day for every repository: a goal added while Today shows one repository belongs to it; otherwise to none
38
+ if (ctx.m && ctx.m.narrow && ctx.m.here) body.repo = ctx.m.here;
36
39
  return press(ctx, body);
37
40
  }
38
41
 
@@ -42,7 +45,7 @@ const jobsOf = (m, id) => m.jobs.filter((j) => j.goal === id && !j.worth);
42
45
  export function goalRows(ctx) {
43
46
  const { m } = ctx;
44
47
  if (!m.goals.length) return null;
45
- return el('ul', { class: 'td-day__rows', 'data-day-list': 'goals' }, ...m.goals.map((g, i) => {
48
+ return el('ul', { class: 'td-day__rows', 'data-day-list': 'goals' }, ...m.goals.map((g) => {
46
49
  const js = jobsOf(m, g.id);
47
50
  const done = js.filter((j) => j.group === 'done').length;
48
51
  const need = js.filter((j) => j.group === 'needs').length;
@@ -51,8 +54,8 @@ export function goalRows(ctx) {
51
54
  const cur = ctx.cur('goal', g.id);
52
55
  return el('li', null, row({
53
56
  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: '' })],
57
+ lead: [el('span', { class: 'td-day__lead' }, String(m.goalNo.get(g.id)))],
58
+ trail: [el('span', { class: 'td-day__chips' }, g.repo ? badge({ text: m.repoName(g.repo), tone: 'outline', attrs: { 'data-day-repo': g.repo } }) : null), meter({ done: js.length ? done : g.status === 'done' ? 1 : 0, total: js.length || 1, label: '' })],
56
59
  attrs: { class: 'td-day__r', 'data-day-sel': 'goal:' + g.id, 'data-goal': g.id, 'data-goal-coverage': g.coverage, 'data-cur': cur },
57
60
  openAttrs: { 'aria-current': cur },
58
61
  }));
@@ -62,7 +65,7 @@ export function goalRows(ctx) {
62
65
  export function goalAdd(ctx) {
63
66
  const { m } = ctx;
64
67
  const max = m.maxGoals;
65
- if (m.goals.length >= max) return el('p', { class: 'td-day__note' }, G.full(max));
68
+ if (m.allGoals.length >= max) return el('p', { class: 'td-day__note' }, G.full(max));
66
69
  const f = field({ label: G.inputLabel, value: '', placeholder: G.inputHint, hideLabel: true, attrs: { 'data-goals': 'input', 'data-day-input': 'goal', maxlength: '140' },
67
70
  onCommit: async (v) => {
68
71
  if (!String(v).trim()) { draft = v; return; }
@@ -111,9 +114,9 @@ function stepNode(ctx, st) {
111
114
  /** The detail of one goal. `row(type, id, label, lead, trail)` draws a selectable row (today-day.js). */
112
115
  export function goalDetail(ctx, id, row) {
113
116
  const { m } = ctx;
114
- const i = m.goals.findIndex((g) => g.id === id);
117
+ const i = m.allGoals.findIndex((g) => g.id === id);
115
118
  if (i < 0) return null;
116
- const g = m.goals[i];
119
+ const g = m.allGoals[i];
117
120
  const closed = g.status !== 'open';
118
121
  const js = jobsOf(m, g.id);
119
122
  const meets = m.events.filter((e) => e.goal === g.id);
@@ -128,6 +131,9 @@ export function goalDetail(ctx, id, row) {
128
131
  el('div', { class: 'td-day__dhead' }, el('span', { class: 'td-day__kind' }, D.kind.goal(i + 1)), el('h3', null, g.text)),
129
132
  el('div', { class: 'td-day__acts' }, badge({ text: G.coverage[g.coverage] || g.coverage, tone: g.coverage === 'needs-plan' ? 'warning' : 'outline' }),
130
133
  g.initiative_id ? el('span', { class: 'td-day__mono' }, g.initiative_id) : null),
134
+ ctx.repoPicker && m.repos.length ? el('div', { class: 'td-day__ch', 'data-goals': 'repo' }, el('span', null, D.repo.label),
135
+ ctx.repoPicker(m, g.repo, (r) => press(ctx, { action: 'tag', id: g.id, repo: r }), { 'data-goal-repo': g.id })) : null,
136
+ ctx.repoPicker && m.repos.length ? el('p', { class: 'td-day__note' }, D.repo.goalHint) : null,
131
137
  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
138
  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
139
  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,
@@ -3639,6 +3639,18 @@ function briefOfDay(date) {
3639
3639
  try { return JSON.parse(fs.readFileSync(path.join(STATE, 'briefs', date + '.json'), 'utf8')); } catch { return null; }
3640
3640
  }
3641
3641
  const daySessionAlive = (id) => { const s = id && sessions.get(id); return !!s && s.state !== 'exited'; };
3642
+ // An agent CLI does not exit when its work is done: the morning-brief session stays open after it submits the brief. So
3643
+ // "alive" alone does not mean "still reading". The read is over when today's brief arrived after the session started,
3644
+ // or when the pane has printed nothing for BRIEF_IDLE_MS without a brief (it waits on a question, or never got its prompt).
3645
+ const BRIEF_IDLE_MS = 2 * 60 * 1000;
3646
+ function briefSessionDone(run, date, now) {
3647
+ const bs = run && run.brief_session; const s = bs && sessions.get(bs.id);
3648
+ if (!s || s.state === 'exited' || bs.ended_at) return null;
3649
+ let at = 0; try { at = fs.statSync(path.join(STATE, 'briefs', date + '.json')).mtimeMs; } catch { /* no brief yet */ }
3650
+ if (at && at >= Date.parse(bs.started_at || 0)) return 'submitted';
3651
+ if (now - Math.max(s.lastOutputAt || 0, Date.parse(bs.started_at || 0) || 0) >= BRIEF_IDLE_MS) return 'idle';
3652
+ return null;
3653
+ }
3642
3654
  // The skill's own body is the prompt, as fireSchedule hands it over; the line after it names this run's task and mode.
3643
3655
  function daySkillPrompt(run, skillId) {
3644
3656
  const w = getWs(run.ws);
@@ -7178,6 +7190,18 @@ async function handleRequest(req, res) {
7178
7190
  let run = readDayRun(pdate) || dayRunLib.newRun(pdate, pnow);
7179
7191
  run.ws = w.id;
7180
7192
  if (run.brief_session && !run.brief_session.ended_at) {
7193
+ // A session that finished its read (or sits idle) is used again: the skill's prompt is typed into it and run.
7194
+ const done = daySessionAlive(run.brief_session.id) ? briefSessionDone(run, pdate, pnow) : null;
7195
+ if (done) {
7196
+ const sk = daySkillPrompt(run, 'morning-brief');
7197
+ const s = sessions.get(run.brief_session.id);
7198
+ if (!sk.error) {
7199
+ try { firstInput.deliver(s, `${sk.prompt}\n\nThis run: prepare the day again. Read every source again and submit a new brief for today.\r`); } catch { /* typed below as a new session */ }
7200
+ run.brief_session = { id: s.id, started_at: new Date(pnow).toISOString(), ended_at: null };
7201
+ writeDayRun(run);
7202
+ return json(res, { ok: true, started: true, reused: true, session: s.id, run, goals: open.length });
7203
+ }
7204
+ }
7181
7205
  if (daySessionAlive(run.brief_session.id)) { writeDayRun(run); return json(res, { ok: true, running: true, session: run.brief_session.id, run, goals: open.length }); }
7182
7206
  run = dayRunLib.sessionEnded(run, run.brief_session.id, pnow).run;
7183
7207
  }
@@ -7230,9 +7254,13 @@ async function handleRequest(req, res) {
7230
7254
  const bs = run && run.brief_session; const bsess = bs ? sessions.get(bs.id) : null;
7231
7255
  const readFailed = !run || brief ? null
7232
7256
  : !bs ? { session: null, why: 'the morning-brief session did not start.' }
7233
- : (!!bs.ended_at || !bsess || bsess.state === 'exited') ? { session: bs.id, why: (bsess && bsess.note) || 'the session ended without submitting a brief.' } : null;
7257
+ : (!!bs.ended_at || !bsess || bsess.state === 'exited') ? { session: bs.id, why: (bsess && bsess.note) || 'the session ended without submitting a brief.' }
7258
+ : briefSessionDone(run, date, now) === 'idle' ? { session: bs.id, idle: true, why: 'the session has printed nothing for two minutes and has not submitted a brief.' } : null;
7234
7259
  return json(res, { ok: true, ws: w ? w.id : null, today: date, now: new Date(now).toISOString(), run, stage: dayRunLib.stage(run, brief), brief, plan, cards_error: cardsError, read_failed: readFailed,
7235
- settings: { briefSources: st.briefSources, scopes: st.scopes, dayCheck: st.dayCheck, websites: st.websites }, sources,
7260
+ settings: { briefSources: st.briefSources, scopes: st.scopes, placeRepos: st.placeRepos, dayCheck: st.dayCheck, websites: st.websites }, sources,
7261
+ // one day for every repository: the repositories it can be narrowed to, and the repository of each tagged item
7262
+ repos: [...workspaces.values()].map((x) => ({ id: x.id, name: x.name || path.basename(x.root) })),
7263
+ item_repos: goalsLib.itemRepos({ day: readGoalsDay(date), brief, placeRepos: st.placeRepos || {} }),
7236
7264
  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 : [] } });
7237
7265
  }
7238
7266
  if (p.startsWith('/api/proactive/day/') && req.method === 'POST') {
@@ -7371,7 +7399,9 @@ async function handleRequest(req, res) {
7371
7399
  const bs = body.scopes && typeof body.scopes === 'object' && !Array.isArray(body.scopes) ? body.scopes : null;
7372
7400
  const scopes = bs ? { ...cur.scopes, ...Object.fromEntries(Object.entries(bs).map(([k, v]) => [k, v && typeof v === 'object' && !Array.isArray(v) ? { ...(cur.scopes[k] || {}), ...v } : v])) } : cur.scopes;
7373
7401
  const dayCheck = body.dayCheck && typeof body.dayCheck === 'object' && !Array.isArray(body.dayCheck) ? { ...cur.dayCheck, ...body.dayCheck } : (body.dayCheck === undefined ? cur.dayCheck : body.dayCheck);
7374
- const n = proactiveLib.normalizeSettings({ ...cur, ...body, triggers: { ...cur.triggers, ...(body.triggers || {}) }, briefSources: { ...cur.briefSources, ...(body.briefSources || {}) }, scopes, dayCheck });
7402
+ const bpr = body.placeRepos && typeof body.placeRepos === 'object' && !Array.isArray(body.placeRepos) ? body.placeRepos : null;
7403
+ const placeRepos = bpr ? { ...cur.placeRepos, ...Object.fromEntries(Object.entries(bpr).map(([k, v]) => [k, v && typeof v === 'object' && !Array.isArray(v) ? { ...((cur.placeRepos || {})[k] || {}), ...v } : v])) } : cur.placeRepos;
7404
+ const n = proactiveLib.normalizeSettings({ ...cur, ...body, placeRepos, triggers: { ...cur.triggers, ...(body.triggers || {}) }, briefSources: { ...cur.briefSources, ...(body.briefSources || {}) }, scopes, dayCheck });
7375
7405
  if (!n.ok) return json(res, { ok: false, error: n.error }, 400);
7376
7406
  try { fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PRO_SETTINGS(), JSON.stringify(n.settings) + '\n'); }
7377
7407
  catch (e) { warnWriteFail('proactive-settings.json', e); return json(res, { ok: false, error: 'could not save — nothing changed' }, 500); }