create-pmos 0.71.0 → 0.72.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/first-paint-audit.js +4 -2
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/layout.md +1 -1
- package/workspace/guide/my-work.md +64 -0
- package/workspace/lib/day-run.js +22 -2
- package/workspace/lib/my-work-cli.js +98 -0
- package/workspace/lib/my-work.js +242 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/proactive-cli.js +10 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-my-work.js +122 -0
- package/workspace/lib/sync.js +42 -7
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +23 -6
- package/workspace/public/pm/components.css +117 -0
- package/workspace/public/pm/data.js +3 -2
- package/workspace/public/pm/dest-mywork.js +242 -0
- package/workspace/public/pm/frame-copy.js +12 -0
- package/workspace/public/pm/my-work-card.js +51 -0
- package/workspace/public/pm/my-work-data.js +36 -0
- package/workspace/public/pm/registry.js +3 -0
- package/workspace/public/pm/topbar.js +16 -2
- package/workspace/server.js +42 -3
- package/workspace/skills/day-task.skill +1 -1
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/skills/my-work-read.skill +43 -0
- package/workspace/test-lists/my-work.json +13 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* routes-my-work.js — the My work destination's routes (initiative my-work, PRD R1 to R5, R11).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/my-work → { ok, today, read_at, items, counts, banner } every item assigned to the PM,
|
|
5
|
+
* ranked (lib/my-work.js fold), across every registered repository
|
|
6
|
+
* POST /api/my-work/read { items } → { ok, read_at, count } the my-work-read skill's submission (`pmos my-work submit`)
|
|
7
|
+
* POST /api/my-work/act { action: brief|handle|reopen, item, instruction?, ws }
|
|
8
|
+
* → { ok, item, task? } the PM's press; a brief adds one day task (R5)
|
|
9
|
+
* POST /api/my-work/refresh { ws } → { ok, session } Read again: starts the my-work-read skill
|
|
10
|
+
*
|
|
11
|
+
* STATE. The latest read is <runtime>/my-work/read.json and the presses are <runtime>/my-work/actions.jsonl, append-only.
|
|
12
|
+
* Both stay in the per-user runtime state and never land in a repository: they hold paraphrases of Jira comments. The
|
|
13
|
+
* WBS items are folded from each repository's Board (lib/board-fold.js, filter mine) on every GET, and nothing is
|
|
14
|
+
* written to a plan ledger (D88).
|
|
15
|
+
*
|
|
16
|
+
* THE SERVER NEVER READS JIRA (D53, D71). An agent session reads it with the PM's own connection and posts the read.
|
|
17
|
+
* The two presses that start a session (act brief, refresh) pass pm-guard requirePagePress first: an agent session
|
|
18
|
+
* cannot brief itself or another agent.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const guard = require('./pm-guard.js');
|
|
23
|
+
const myWork = require('./my-work.js');
|
|
24
|
+
const boardFold = require('./board-fold.js');
|
|
25
|
+
const { localDate } = require('./brief.js');
|
|
26
|
+
|
|
27
|
+
const dirOf = (ctx) => path.join(ctx.runtimeDir, 'my-work');
|
|
28
|
+
const readFile = (ctx) => path.join(dirOf(ctx), 'read.json');
|
|
29
|
+
const actionsFile = (ctx) => path.join(dirOf(ctx), 'actions.jsonl');
|
|
30
|
+
|
|
31
|
+
function loadRead(ctx) {
|
|
32
|
+
try { const r = JSON.parse(fs.readFileSync(readFile(ctx), 'utf8')); return r && Array.isArray(r.items) ? r : null; } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
function loadActions(ctx) {
|
|
35
|
+
try { return myWork.parseActions(fs.readFileSync(actionsFile(ctx), 'utf8')); } catch { return []; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The whole list, as GET answers it. */
|
|
39
|
+
function listFor(ctx, now) {
|
|
40
|
+
const today = localDate(now);
|
|
41
|
+
const banner = [];
|
|
42
|
+
const boards = [];
|
|
43
|
+
const all = typeof ctx.workspaceList === 'function' ? ctx.workspaceList() : [];
|
|
44
|
+
for (const w of all) {
|
|
45
|
+
try {
|
|
46
|
+
const b = boardFold.boardFor({ id: w.id, root: w.root }, { filter: 'mine', now });
|
|
47
|
+
if (b.ok) boards.push({ ws: w.id, repo: w.name || path.basename(w.root), board: b });
|
|
48
|
+
else banner.push({ source: 'wbs:' + w.id, error: b.error });
|
|
49
|
+
} catch (e) { banner.push({ source: 'wbs:' + w.id, error: String((e && e.message) || e).slice(0, 160) }); }
|
|
50
|
+
}
|
|
51
|
+
const read = loadRead(ctx);
|
|
52
|
+
const actions = loadActions(ctx);
|
|
53
|
+
const runs = {};
|
|
54
|
+
for (const d of new Set(actions.filter((a) => a.action === 'brief').map((a) => String(a.at).slice(0, 10)).concat([today]))) {
|
|
55
|
+
try { const r = ctx.readDayRun ? ctx.readDayRun(d) : null; if (r) runs[d] = r; } catch { /* a run that cannot be read is no run */ }
|
|
56
|
+
}
|
|
57
|
+
// a run of a later day can hold a task too: today's covers every brief made today
|
|
58
|
+
const out = myWork.fold({ read, wbs: myWork.wbsItems(boards), actions, runs, today });
|
|
59
|
+
return { ok: true, today, read_at: read ? read.read_at || null : null, items: out.items, counts: out.counts, banner };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeAtomic(file, text) {
|
|
63
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
64
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
65
|
+
fs.writeFileSync(tmp, text);
|
|
66
|
+
fs.renameSync(tmp, file);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function handle(req, res, url, ctx) {
|
|
70
|
+
const p = url.pathname;
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
if (p === '/api/my-work') {
|
|
73
|
+
if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
|
|
74
|
+
ctx.json(res, listFor(ctx, now));
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (!['/api/my-work/read', '/api/my-work/act', '/api/my-work/refresh'].includes(p)) return false;
|
|
78
|
+
if (req.method !== 'POST') { ctx.json(res, { ok: false, error: 'POST only' }, 405); return true; }
|
|
79
|
+
const got = await ctx.readJson(req);
|
|
80
|
+
if (!got.ok) { ctx.json(res, { ok: false, error: 'invalid JSON body' }, 400); return true; }
|
|
81
|
+
const body = got.body;
|
|
82
|
+
|
|
83
|
+
if (p === '/api/my-work/read') {
|
|
84
|
+
const c = myWork.checkRead(body, now);
|
|
85
|
+
if (!c.ok) { ctx.json(res, { ok: false, error: c.error }, 400); return true; }
|
|
86
|
+
try { writeAtomic(readFile(ctx), JSON.stringify(c.read) + '\n'); }
|
|
87
|
+
catch { ctx.json(res, { ok: false, error: 'could not save the read — nothing changed' }, 500); return true; }
|
|
88
|
+
ctx.json(res, { ok: true, read_at: c.read.read_at, count: c.read.items.length });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const g = guard.requirePagePress(req, body);
|
|
93
|
+
if (!g.ok) { ctx.json(res, { ok: false, error: g.error }, g.status); return true; }
|
|
94
|
+
|
|
95
|
+
if (p === '/api/my-work/refresh') {
|
|
96
|
+
const r = ctx.myWorkRead(String(body.ws || ''));
|
|
97
|
+
ctx.json(res, r.ok ? { ok: true, session: r.session } : { ok: false, error: r.error }, r.ok ? 200 : r.status || 500);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// act
|
|
102
|
+
const c = myWork.checkAct(body, now);
|
|
103
|
+
if (!c.ok) { ctx.json(res, { ok: false, error: c.error }, 400); return true; }
|
|
104
|
+
const list = listFor(ctx, now);
|
|
105
|
+
const item = list.items.find((x) => x.id === c.record.item);
|
|
106
|
+
if (!item) { ctx.json(res, { ok: false, error: `no item ${c.record.item} is assigned to you` }, 404); return true; }
|
|
107
|
+
const why = myWork.refusal(item, c.record.action);
|
|
108
|
+
if (why) { ctx.json(res, { ok: false, error: why }, 409); return true; }
|
|
109
|
+
let task = null;
|
|
110
|
+
if (c.record.action === 'brief') {
|
|
111
|
+
const b = ctx.myWorkBrief(myWork.taskSpec(item, c.record), String(body.ws || ''));
|
|
112
|
+
if (!b.ok) { ctx.json(res, { ok: false, error: b.error }, b.status || 500); return true; }
|
|
113
|
+
task = b.task;
|
|
114
|
+
}
|
|
115
|
+
try { fs.mkdirSync(dirOf(ctx), { recursive: true }); fs.appendFileSync(actionsFile(ctx), JSON.stringify(c.record) + '\n'); }
|
|
116
|
+
catch { ctx.json(res, { ok: false, error: 'could not save your press' }, 500); return true; }
|
|
117
|
+
const after = listFor(ctx, Date.now()).items.find((x) => x.id === item.id) || null;
|
|
118
|
+
ctx.json(res, { ok: true, item: after, task });
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { prefixes: ['/api/my-work'], handle, listFor };
|
package/workspace/lib/sync.js
CHANGED
|
@@ -118,7 +118,36 @@ async function status(root) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// The press. Returns { ok, steps:[{step, ok, note}], stopped_at?, message }.
|
|
121
|
-
|
|
121
|
+
// While a replay stops on a conflict, the PM's choice settles each conflicted file whole. In a rebase, stage 2 (`--ours`)
|
|
122
|
+
// is the remote's side being built on, and stage 3 (`--theirs`) is this machine's commit being replayed. A side that
|
|
123
|
+
// deleted the file is honoured by removing it. A commit left with nothing of its own is skipped. Bounded: each round
|
|
124
|
+
// replays one commit, so the loop ends after at most one round per unpushed commit plus one.
|
|
125
|
+
async function settleReplay(root, prefer, rounds) {
|
|
126
|
+
for (let i = 0; i <= rounds; i++) {
|
|
127
|
+
const rp = await run(root, ['rev-parse', '--git-path', 'rebase-merge']);
|
|
128
|
+
if (!rp.ok || !fs.existsSync(path.resolve(root, rp.out))) return { ok: true };
|
|
129
|
+
const u = await run(root, ['diff', '--name-only', '--diff-filter=U']);
|
|
130
|
+
const files = u.ok && u.out ? u.out.split('\n').filter(Boolean) : [];
|
|
131
|
+
if (!files.length) return { ok: false, err: 'the replay stopped with no conflicted file' };
|
|
132
|
+
const stage = prefer === 'mine' ? '3' : '2';
|
|
133
|
+
for (const f of files) {
|
|
134
|
+
const has = await run(root, ['cat-file', '-e', `:${stage}:${f}`]);
|
|
135
|
+
const r = has.ok ? await run(root, ['checkout', prefer === 'mine' ? '--theirs' : '--ours', '--', f]) : await run(root, ['rm', '-q', '--', f]);
|
|
136
|
+
if (!r.ok) return { ok: false, err: r.err };
|
|
137
|
+
if (has.ok) { const a = await run(root, ['add', '--', f]); if (!a.ok) return { ok: false, err: a.err }; }
|
|
138
|
+
}
|
|
139
|
+
const empty = (await run(root, ['diff', '--cached', '--quiet'])).ok;
|
|
140
|
+
const next = await run(root, ['-c', 'core.editor=true', 'rebase', empty ? '--skip' : '--continue']);
|
|
141
|
+
if (next.ok) return { ok: true };
|
|
142
|
+
}
|
|
143
|
+
return { ok: false, err: 'the replay did not finish' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// `prefer` is the PM's answer after a stop on a conflict, and only the press passes it (never auto-sync): 'mine' keeps
|
|
147
|
+
// this machine's version of each file both machines changed, 'theirs' keeps the other machine's. Every other change
|
|
148
|
+
// from both machines is kept either way. Still a rebase of this machine's own commits: no force, no merge commit.
|
|
149
|
+
async function sync(root, { label, prefer } = {}) {
|
|
150
|
+
if (prefer !== undefined && prefer !== 'mine' && prefer !== 'theirs') prefer = undefined;
|
|
122
151
|
const steps = [];
|
|
123
152
|
const stop = (step, message) => { steps.push({ step, ok: false, note: message }); return { ok: false, steps, stopped_at: step, message }; };
|
|
124
153
|
const done = (step, note) => steps.push({ step, ok: true, note });
|
|
@@ -157,20 +186,26 @@ async function sync(root, { label } = {}) {
|
|
|
157
186
|
if (s2.behind) {
|
|
158
187
|
// Fast-forward when we have nothing of our own; otherwise replay OUR unpushed commits on top.
|
|
159
188
|
const attrs = s2.ahead ? unionAttributesFor(root) : null;
|
|
160
|
-
|
|
189
|
+
// With the PM's choice, a conflicted hunk takes that side (-X); in a rebase `theirs` is this machine's commit.
|
|
190
|
+
const side = prefer && s2.ahead ? ['-X', prefer === 'mine' ? 'theirs' : 'ours'] : [];
|
|
191
|
+
const verb = s2.ahead ? [...(attrs ? ['-c', `core.attributesFile=${attrs}`] : []), 'rebase', '--autostash', ...side, s2.upstream]
|
|
161
192
|
: ['merge', '--ff-only', s2.upstream];
|
|
162
|
-
|
|
193
|
+
let r = await run(root, verb);
|
|
194
|
+
if (!r.ok && prefer && s2.ahead) r = await settleReplay(root, prefer, s2.ahead);
|
|
163
195
|
if (!r.ok) {
|
|
164
196
|
if (!s2.ahead) return stop('integrate', `cannot fast-forward — ${r.err.split('\n')[0] || 'see git'}`);
|
|
165
197
|
// Name the files before the abort clears them: the PM has to open these, and "the same lines" alone says not where.
|
|
166
198
|
const u = await run(root, ['diff', '--name-only', '--diff-filter=U']);
|
|
167
199
|
const files = u.ok && u.out ? u.out.split('\n').filter(Boolean) : [];
|
|
168
200
|
await run(root, ['rebase', '--abort']);
|
|
169
|
-
|
|
170
|
-
? `both machines changed ${files.join(', ')} — rebase aborted, nothing changed;
|
|
171
|
-
: `the two machines changed the same lines — rebase aborted, nothing changed — ${r.err.split('\n')[0] || 'see git'}`);
|
|
201
|
+
const out = stop('integrate', files.length
|
|
202
|
+
? `both machines changed ${files.join(', ')} — rebase aborted, nothing changed; choose which machine's version to keep`
|
|
203
|
+
: `the two machines changed the same lines — rebase aborted, nothing changed — ${(r.err || '').split('\n')[0] || 'see git'}`);
|
|
204
|
+
// The surface offers the choice only for a conflict it can settle, and never again once one was made.
|
|
205
|
+
if (!prefer) out.conflict = { files };
|
|
206
|
+
return out;
|
|
172
207
|
}
|
|
173
|
-
done('integrate', s2.ahead ? `replayed ${s2.ahead} local commit(s) on the remote` : `fast-forwarded ${s2.behind} commit(s)`);
|
|
208
|
+
done('integrate', s2.ahead ? `replayed ${s2.ahead} local commit(s) on the remote${prefer ? `, keeping ${prefer === 'mine' ? 'this' : 'the other'} machine's version where both changed a file` : ''}` : `fast-forwarded ${s2.behind} commit(s)`);
|
|
174
209
|
} else done('integrate', 'nothing to bring in');
|
|
175
210
|
|
|
176
211
|
const s3 = await status(root);
|
package/workspace/package.json
CHANGED
|
@@ -39,7 +39,7 @@ import { isMobile, mountMobile, applyMobile } from './mobile.js';
|
|
|
39
39
|
/* The frame's parts (Run 3, slice 3A). Small modules with no effect at import: a fixture page loads them and calls none. */
|
|
40
40
|
import { createTopbar, unsyncedOf } from './topbar.js';
|
|
41
41
|
import { createDockRail } from './dock.js';
|
|
42
|
-
import { SEL, ensureComponentsCss, iconButton, menu, toast } from './primitives.js';
|
|
42
|
+
import { SEL, ensureComponentsCss, iconButton, menu, toast, sheet, button } from './primitives.js';
|
|
43
43
|
import { ensureSprite } from './icons.js';
|
|
44
44
|
import { COPY as FC } from './frame-copy.js';
|
|
45
45
|
import { registerCommand } from './registry.js';
|
|
@@ -406,7 +406,7 @@ function render() {
|
|
|
406
406
|
|
|
407
407
|
const FRAME = !FIXTURE && !LEGACY_LAYOUT_FLAG;
|
|
408
408
|
const FRAME_KEY = 'pmos.workspace.pm.frame';
|
|
409
|
-
const FRAME_DESTS = Object.freeze(['today', 'initiatives', 'board', 'reports', 'docs', 'people', 'settings']);
|
|
409
|
+
const FRAME_DESTS = Object.freeze(['today', 'mywork', 'initiatives', 'board', 'reports', 'docs', 'people', 'settings']);
|
|
410
410
|
|
|
411
411
|
function readFrame() {
|
|
412
412
|
if (!FRAME) return {};
|
|
@@ -512,17 +512,34 @@ function syncReading(repo) {
|
|
|
512
512
|
}
|
|
513
513
|
return syncSeen.get(repo).st;
|
|
514
514
|
}
|
|
515
|
-
async function runSync() {
|
|
515
|
+
async function runSync(prefer) {
|
|
516
516
|
const repo = fr.scope === 'all' ? null : activeRepo();
|
|
517
517
|
if (!repo || fr.syncing || !mods.data || typeof mods.data.syncRepository !== 'function') return;
|
|
518
518
|
fr.syncing = true;
|
|
519
519
|
setState({});
|
|
520
520
|
let res;
|
|
521
|
-
|
|
521
|
+
const opts = prefer === 'mine' || prefer === 'theirs' ? { prefer } : undefined;
|
|
522
|
+
try { res = await mods.data.syncRepository(repo, opts); } catch (e) { res = { ok: false, message: String((e && e.message) || e) }; }
|
|
522
523
|
fr.syncing = false;
|
|
523
524
|
syncSeen.delete(repo);
|
|
524
|
-
toast({ text: res && res.ok ? FC.sync.done : FC.sync.stopped((res && (res.stopped_at || res.step)) || FC.sync.label, (res && (res.message || res.error)) || '') });
|
|
525
525
|
setState({});
|
|
526
|
+
// Both machines changed the same lines and nothing changed: the PM chooses which version to keep (D70 amended
|
|
527
|
+
// 2026-10-07). The server offers the choice only once per press, so a choice that still stops ends in the toast.
|
|
528
|
+
if (res && !res.ok && res.conflict) return chooseSyncSide(res.conflict.files || []);
|
|
529
|
+
toast({ text: res && res.ok ? FC.sync.done : FC.sync.stopped((res && (res.stopped_at || res.step)) || FC.sync.label, (res && (res.message || res.error)) || '') });
|
|
530
|
+
}
|
|
531
|
+
function chooseSyncSide(files) {
|
|
532
|
+
const C = FC.sync.conflict;
|
|
533
|
+
let s = null;
|
|
534
|
+
const pick = (side) => () => { s.close(); runSync(side); };
|
|
535
|
+
s = sheet({
|
|
536
|
+
title: C.title, sub: C.sub(files), body: [el('p', {}, C.body)], attrs: { 'data-sync-conflict': '' },
|
|
537
|
+
actions: [
|
|
538
|
+
button({ label: C.push, variant: 'primary', onClick: pick('mine'), attrs: { 'data-sync-prefer': 'mine' } }),
|
|
539
|
+
button({ label: C.pull, onClick: pick('theirs'), attrs: { 'data-sync-prefer': 'theirs' } }),
|
|
540
|
+
button({ label: C.cancel, variant: 'ghost', onClick: () => s.close() }),
|
|
541
|
+
],
|
|
542
|
+
});
|
|
526
543
|
}
|
|
527
544
|
|
|
528
545
|
/* Closing the tab with work that is not yet pushed asks first (D70 as amended 2026-10-06): idle auto-sync waits for a
|
|
@@ -637,7 +654,7 @@ function pressIn(r, key) {
|
|
|
637
654
|
if (b && !b.disabled) { b.click(); return true; }
|
|
638
655
|
return false;
|
|
639
656
|
}
|
|
640
|
-
const G_KEYS = { t: 'today', i: 'initiatives', b: 'board', r: 'reports', d: 'docs', p: 'people' };
|
|
657
|
+
const G_KEYS = { t: 'today', i: 'initiatives', b: 'board', r: 'reports', d: 'docs', p: 'people', w: 'mywork' };
|
|
641
658
|
|
|
642
659
|
function frameKey(e) {
|
|
643
660
|
const mod = e.metaKey || e.ctrlKey;
|
|
@@ -5653,6 +5653,123 @@
|
|
|
5653
5653
|
}
|
|
5654
5654
|
}
|
|
5655
5655
|
|
|
5656
|
+
/* My work (my-work): the work assigned to the PM, ranked, beside the chosen item's context card and brief. */
|
|
5657
|
+
.rp-mywork {
|
|
5658
|
+
display: flex;
|
|
5659
|
+
flex-direction: column;
|
|
5660
|
+
gap: 16px;
|
|
5661
|
+
}
|
|
5662
|
+
.rp-mywork-top {
|
|
5663
|
+
display: flex;
|
|
5664
|
+
flex-wrap: wrap;
|
|
5665
|
+
align-items: center;
|
|
5666
|
+
gap: 12px;
|
|
5667
|
+
}
|
|
5668
|
+
.rp-mywork-top > .rp-head__title {
|
|
5669
|
+
margin-right: auto;
|
|
5670
|
+
}
|
|
5671
|
+
.rp-mywork-body {
|
|
5672
|
+
display: flex;
|
|
5673
|
+
flex-direction: column;
|
|
5674
|
+
gap: 16px;
|
|
5675
|
+
}
|
|
5676
|
+
.rp-mywork-grid {
|
|
5677
|
+
display: grid;
|
|
5678
|
+
grid-template-columns: minmax(280px, 3fr) 2fr;
|
|
5679
|
+
gap: 24px;
|
|
5680
|
+
align-items: start;
|
|
5681
|
+
}
|
|
5682
|
+
.rp-mywork-list {
|
|
5683
|
+
display: flex;
|
|
5684
|
+
flex-direction: column;
|
|
5685
|
+
gap: 8px;
|
|
5686
|
+
min-width: 0;
|
|
5687
|
+
}
|
|
5688
|
+
.rp-mywork-views {
|
|
5689
|
+
border-bottom: 1px solid var(--border);
|
|
5690
|
+
}
|
|
5691
|
+
.rp-mywork-row {
|
|
5692
|
+
border-top: 1px solid var(--border);
|
|
5693
|
+
}
|
|
5694
|
+
.rp-mywork-row:last-child {
|
|
5695
|
+
border-bottom: 1px solid var(--border);
|
|
5696
|
+
}
|
|
5697
|
+
.rp-mywork-row--open {
|
|
5698
|
+
box-shadow: inset 2px 0 0 var(--foreground);
|
|
5699
|
+
}
|
|
5700
|
+
.rp-mywork-state {
|
|
5701
|
+
display: inline-flex;
|
|
5702
|
+
align-items: center;
|
|
5703
|
+
color: var(--muted-foreground);
|
|
5704
|
+
}
|
|
5705
|
+
.rp-mywork-page {
|
|
5706
|
+
display: flex;
|
|
5707
|
+
flex-direction: column;
|
|
5708
|
+
gap: 16px;
|
|
5709
|
+
min-width: 0;
|
|
5710
|
+
}
|
|
5711
|
+
.rp-mywork-who {
|
|
5712
|
+
display: flex;
|
|
5713
|
+
flex-direction: column;
|
|
5714
|
+
gap: 4px;
|
|
5715
|
+
}
|
|
5716
|
+
.rp-mywork-who__title {
|
|
5717
|
+
margin: 0;
|
|
5718
|
+
font-size: 16px;
|
|
5719
|
+
font-weight: 500;
|
|
5720
|
+
}
|
|
5721
|
+
.rp-mywork-link {
|
|
5722
|
+
display: inline-flex;
|
|
5723
|
+
align-items: center;
|
|
5724
|
+
gap: 4px;
|
|
5725
|
+
font-size: 12px;
|
|
5726
|
+
color: var(--foreground);
|
|
5727
|
+
}
|
|
5728
|
+
.rp-mywork-sec,
|
|
5729
|
+
.rp-mywork-brief,
|
|
5730
|
+
.rp-mywork-status {
|
|
5731
|
+
display: flex;
|
|
5732
|
+
flex-direction: column;
|
|
5733
|
+
gap: 8px;
|
|
5734
|
+
}
|
|
5735
|
+
.rp-mywork-sec__title,
|
|
5736
|
+
.rp-mywork-sub {
|
|
5737
|
+
margin: 0;
|
|
5738
|
+
font-size: 12px;
|
|
5739
|
+
font-weight: 500;
|
|
5740
|
+
color: var(--muted-foreground);
|
|
5741
|
+
}
|
|
5742
|
+
.rp-mywork-hint {
|
|
5743
|
+
margin: 0;
|
|
5744
|
+
font-size: 12px;
|
|
5745
|
+
color: var(--muted-foreground);
|
|
5746
|
+
}
|
|
5747
|
+
.rp-mywork-text {
|
|
5748
|
+
margin: 0;
|
|
5749
|
+
font-size: 14px;
|
|
5750
|
+
overflow-wrap: anywhere;
|
|
5751
|
+
}
|
|
5752
|
+
.rp-mywork-quote {
|
|
5753
|
+
padding-left: 8px;
|
|
5754
|
+
border-left: 2px solid var(--border);
|
|
5755
|
+
white-space: pre-wrap;
|
|
5756
|
+
}
|
|
5757
|
+
.rp-mywork-open {
|
|
5758
|
+
margin: 0;
|
|
5759
|
+
padding-left: 16px;
|
|
5760
|
+
font-size: 14px;
|
|
5761
|
+
}
|
|
5762
|
+
.rp-mywork-acts {
|
|
5763
|
+
display: flex;
|
|
5764
|
+
flex-wrap: wrap;
|
|
5765
|
+
gap: 8px;
|
|
5766
|
+
}
|
|
5767
|
+
@media (max-width: 760px) {
|
|
5768
|
+
.rp-mywork-grid {
|
|
5769
|
+
grid-template-columns: 1fr;
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5656
5773
|
/* -- layouts · .nt- Notes: one meeting on one screen — notes and what was caught beside a quiet transcript (meeting-notes Runs 1 and 2) -- */
|
|
5657
5774
|
.nt-wrap {
|
|
5658
5775
|
display: flex;
|
|
@@ -1559,11 +1559,12 @@ export async function syncStatusOf(name) {
|
|
|
1559
1559
|
return getJson('/api/sync/status', { ws });
|
|
1560
1560
|
}
|
|
1561
1561
|
|
|
1562
|
-
|
|
1562
|
+
// prefer: 'mine' | 'theirs' — the PM's answer after Sync stopped on a conflict (D70 as amended 2026-10-07).
|
|
1563
|
+
export async function syncRepository(name, { prefer } = {}) {
|
|
1563
1564
|
if (FIXTURE) return { ok: false, message: 'fixture mode — nothing to sync' };
|
|
1564
1565
|
const ws = wsOf(name);
|
|
1565
1566
|
if (!ws) return { ok: false, message: 'unknown repository' };
|
|
1566
|
-
const res = await postJson('/api/sync', { ws });
|
|
1567
|
+
const res = await postJson('/api/sync', prefer ? { ws, prefer } : { ws });
|
|
1567
1568
|
const p = products.get(name);
|
|
1568
1569
|
if (res && res.ok && p) { p.board = null; await loadBoard(p); publishBoards(); }
|
|
1569
1570
|
return res || { ok: false, message: 'the workspace server did not answer' };
|