@harborppm/cli 0.52.0 → 0.53.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/README.md +3 -0
- package/dist/app.mjs +567 -32
- package/lib/cli.mjs +21 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,6 +166,9 @@ harbor raid "TITLE" [--status S] [--owner me|none|NAME] [--impact 1-5] [--probab
|
|
|
166
166
|
harbor review [WORDS] [--mine|--owner NAME] [--portfolio P] [--done]
|
|
167
167
|
the review: programs worst first, with next moves
|
|
168
168
|
(reads only; harbor submit moves a task to In review)
|
|
169
|
+
harbor bridge run the review as a meeting: one project a screen,
|
|
170
|
+
what changed since the last one, log decisions,
|
|
171
|
+
actions and risks live, post the summary to chat
|
|
169
172
|
harbor next-move PROGRAM "TEXT" write a program's next move
|
|
170
173
|
harbor health --program P|--project P|--portfolio P STATE [--note WHY]
|
|
171
174
|
set its health by hand (auto hands it back)
|
package/dist/app.mjs
CHANGED
|
@@ -3760,7 +3760,7 @@ function projectKeyFor(name, taken) {
|
|
|
3760
3760
|
for (let n = 2;; n++) if (!taken.has(`${base}${n}`)) return `${base}${n}`;
|
|
3761
3761
|
}
|
|
3762
3762
|
/** Whether `a` has the better claim to the number both hold. */
|
|
3763
|
-
function first(a, b, stored) {
|
|
3763
|
+
function first$1(a, b, stored) {
|
|
3764
3764
|
const ka = kept(a, stored);
|
|
3765
3765
|
if (ka !== kept(b, stored)) return ka;
|
|
3766
3766
|
return made(a, b) <= 0;
|
|
@@ -3808,7 +3808,7 @@ function settleKeys(data, stored) {
|
|
|
3808
3808
|
}
|
|
3809
3809
|
const other = holder.get(n);
|
|
3810
3810
|
if (other) {
|
|
3811
|
-
const [keep, move] = first(other, t, stored) ? [other, t] : [t, other];
|
|
3811
|
+
const [keep, move] = first$1(other, t, stored) ? [other, t] : [t, other];
|
|
3812
3812
|
holder.set(n, keep);
|
|
3813
3813
|
loose.push(move);
|
|
3814
3814
|
} else holder.set(n, t);
|
|
@@ -6485,6 +6485,493 @@ function BriefView({ active, width, height, go }) {
|
|
|
6485
6485
|
});
|
|
6486
6486
|
}
|
|
6487
6487
|
|
|
6488
|
+
//#endregion
|
|
6489
|
+
//#region src/lib/bridge.ts
|
|
6490
|
+
init_date_fns();
|
|
6491
|
+
init_indexes();
|
|
6492
|
+
init_raid();
|
|
6493
|
+
init_swatches();
|
|
6494
|
+
init_task_keys();
|
|
6495
|
+
init_format();
|
|
6496
|
+
/** How many items of one kind a slide lists before "and N more". */
|
|
6497
|
+
const LIST_CAP = 4;
|
|
6498
|
+
/** When a program's changes are counted from: its last meeting, else a week back. */
|
|
6499
|
+
function bridgeSince(program, now) {
|
|
6500
|
+
if (program.reviewedAt && !Number.isNaN(Date.parse(program.reviewedAt))) return program.reviewedAt;
|
|
6501
|
+
return addDays(now, -7).toISOString();
|
|
6502
|
+
}
|
|
6503
|
+
const after = (at, since) => !!at && !Number.isNaN(Date.parse(at)) && Date.parse(at) > Date.parse(since);
|
|
6504
|
+
const first = (name) => name?.split(" ")[0] ?? null;
|
|
6505
|
+
/** "RR-4 Draft the copy (Sam)", with the key when it has one. */
|
|
6506
|
+
function taskLine$1(t, project, doc) {
|
|
6507
|
+
const key = taskKey(t, project);
|
|
6508
|
+
const who = t.assigneeId ? first(byId(doc.people).get(t.assigneeId)?.name) : null;
|
|
6509
|
+
return `${key ? `${key} ` : ""}${t.title}${who ? ` (${who})` : ""}`;
|
|
6510
|
+
}
|
|
6511
|
+
function listed(prefix, items) {
|
|
6512
|
+
const shown = items.slice(0, 4);
|
|
6513
|
+
const rest = items.length - shown.length;
|
|
6514
|
+
return {
|
|
6515
|
+
text: `${prefix}${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}`,
|
|
6516
|
+
more: rest
|
|
6517
|
+
};
|
|
6518
|
+
}
|
|
6519
|
+
const highRated = (r) => r.kind !== "decision" && isActive(r) && (r.severity === "high" || (r.impact ?? 0) >= 4);
|
|
6520
|
+
/** What changed on one project since `since`: health, work finished, late or added, the register, the hill. */
|
|
6521
|
+
function projectChanges(doc, project, since, now) {
|
|
6522
|
+
const out = [];
|
|
6523
|
+
const today = todayISO(now);
|
|
6524
|
+
const sinceDay = since.slice(0, 10);
|
|
6525
|
+
const history = project.healthHistory ?? [];
|
|
6526
|
+
const moves = history.filter((h) => after(h.at, since));
|
|
6527
|
+
if (moves.length > 0) {
|
|
6528
|
+
const before = history[history.length - moves.length - 1]?.health ?? null;
|
|
6529
|
+
const last = moves[moves.length - 1];
|
|
6530
|
+
const note = last.note?.trim() ? ` “${last.note.trim()}”` : "";
|
|
6531
|
+
const word = (h) => HEALTH_LABEL$1[h] ?? h;
|
|
6532
|
+
out.push({
|
|
6533
|
+
kind: "health",
|
|
6534
|
+
text: before && before !== last.health ? `Went from ${word(before)} to ${word(last.health)}${note}` : `Now ${word(last.health)}${note}`,
|
|
6535
|
+
more: 0
|
|
6536
|
+
});
|
|
6537
|
+
}
|
|
6538
|
+
const tasks = tasksOf(doc.tasks, project.id);
|
|
6539
|
+
const finished = tasks.filter((t) => t.status === "done" && after(t.completedAt, since)).sort((a, b) => (b.completedAt ?? "").localeCompare(a.completedAt ?? ""));
|
|
6540
|
+
if (finished.length) out.push({
|
|
6541
|
+
kind: "finished",
|
|
6542
|
+
...listed("Finished: ", finished.map((t) => taskLine$1(t, project, doc)))
|
|
6543
|
+
});
|
|
6544
|
+
const late = tasks.filter((t) => t.status !== "done" && t.dueDate && t.dueDate >= sinceDay && t.dueDate < today).sort((a, b) => (a.dueDate ?? "").localeCompare(b.dueDate ?? ""));
|
|
6545
|
+
if (late.length) out.push({
|
|
6546
|
+
kind: "late",
|
|
6547
|
+
...listed("Now late: ", late.map((t) => taskLine$1(t, project, doc)))
|
|
6548
|
+
});
|
|
6549
|
+
const added = tasks.filter((t) => after(t.createdAt, since) && !finished.includes(t));
|
|
6550
|
+
if (added.length) out.push({
|
|
6551
|
+
kind: "added",
|
|
6552
|
+
...listed("Added: ", added.map((t) => taskLine$1(t, project, doc)))
|
|
6553
|
+
});
|
|
6554
|
+
const register = doc.raidItems.filter((r) => r.projectId === project.id);
|
|
6555
|
+
const decisions = register.filter((r) => r.kind === "decision" && after(r.updatedAt, since));
|
|
6556
|
+
const decided = decisions.filter((r) => r.status === "decided");
|
|
6557
|
+
if (decided.length) out.push({
|
|
6558
|
+
kind: "decided",
|
|
6559
|
+
...listed("Decided: ", decided.map((r) => r.title))
|
|
6560
|
+
});
|
|
6561
|
+
const proposed = decisions.filter((r) => r.status === "proposed");
|
|
6562
|
+
if (proposed.length) out.push({
|
|
6563
|
+
kind: "decided",
|
|
6564
|
+
...listed("To decide: ", proposed.map((r) => r.title))
|
|
6565
|
+
});
|
|
6566
|
+
const raised = register.filter((r) => r.kind !== "decision" && after(r.createdAt, since));
|
|
6567
|
+
if (raised.length) out.push({
|
|
6568
|
+
kind: "raised",
|
|
6569
|
+
...listed("Raised: ", raised.map((r) => `${r.kind === "issue" ? "issue" : "risk"} ${r.title}`))
|
|
6570
|
+
});
|
|
6571
|
+
const updated = register.filter((r) => r.kind !== "decision" && !raised.includes(r) && after(r.updatedAt, since));
|
|
6572
|
+
if (updated.length) out.push({
|
|
6573
|
+
kind: "updated",
|
|
6574
|
+
...listed("Updated: ", updated.map((r) => r.title))
|
|
6575
|
+
});
|
|
6576
|
+
if (after(project.hillMovedAt, since) && typeof project.hillAt === "number") out.push({
|
|
6577
|
+
kind: "hill",
|
|
6578
|
+
text: project.hillAt < 50 ? "Moved on the hill, still figuring it out" : "Moved on the hill, getting it done",
|
|
6579
|
+
more: 0
|
|
6580
|
+
});
|
|
6581
|
+
return out;
|
|
6582
|
+
}
|
|
6583
|
+
/**
|
|
6584
|
+
* The meeting's slides: the open projects of the given programs (in the
|
|
6585
|
+
* review's order), each with what changed. Projects where nothing moved
|
|
6586
|
+
* come back as `quiet`, named on the summary rather than given a screen.
|
|
6587
|
+
*/
|
|
6588
|
+
function bridgeSlides(doc, programs, now) {
|
|
6589
|
+
const today = todayISO(now);
|
|
6590
|
+
const slides = [];
|
|
6591
|
+
const quiet = [];
|
|
6592
|
+
const rank = {
|
|
6593
|
+
"off-track": 0,
|
|
6594
|
+
"at-risk": 1,
|
|
6595
|
+
"on-track": 2,
|
|
6596
|
+
paused: 3,
|
|
6597
|
+
done: 4
|
|
6598
|
+
};
|
|
6599
|
+
for (const program of programs) {
|
|
6600
|
+
if (program.health === "done") continue;
|
|
6601
|
+
const since = bridgeSince(program, now);
|
|
6602
|
+
const milestone = doc.milestones.filter((m) => m.programId === program.id && !m.done).sort((a, b) => a.dueDate.localeCompare(b.dueDate))[0] ?? null;
|
|
6603
|
+
const projects = doc.projects.filter((p) => p.programId === program.id && p.health !== "done").sort((a, b) => rank[a.health] - rank[b.health] || a.endDate.localeCompare(b.endDate));
|
|
6604
|
+
for (const project of projects) {
|
|
6605
|
+
const changes = projectChanges(doc, project, since, now);
|
|
6606
|
+
if (changes.length === 0) {
|
|
6607
|
+
quiet.push({
|
|
6608
|
+
program,
|
|
6609
|
+
project
|
|
6610
|
+
});
|
|
6611
|
+
continue;
|
|
6612
|
+
}
|
|
6613
|
+
const tasks = tasksOf(doc.tasks, project.id);
|
|
6614
|
+
slides.push({
|
|
6615
|
+
program,
|
|
6616
|
+
project,
|
|
6617
|
+
since,
|
|
6618
|
+
changes,
|
|
6619
|
+
late: tasks.filter((t) => t.status !== "done" && !!t.dueDate && t.dueDate < today).length,
|
|
6620
|
+
open: tasks.filter((t) => t.status !== "done").length,
|
|
6621
|
+
highRisks: doc.raidItems.filter((r) => r.projectId === project.id && highRated(r)).length,
|
|
6622
|
+
milestone
|
|
6623
|
+
});
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
return {
|
|
6627
|
+
slides,
|
|
6628
|
+
quiet
|
|
6629
|
+
};
|
|
6630
|
+
}
|
|
6631
|
+
/**
|
|
6632
|
+
* The end summary, one part per program: projects covered and what was
|
|
6633
|
+
* logged on each, read back from the document (so an edit made since shows,
|
|
6634
|
+
* and an item since deleted drops out).
|
|
6635
|
+
*/
|
|
6636
|
+
function meetingSummary(doc, slides, logged) {
|
|
6637
|
+
const projects = byId(doc.projects);
|
|
6638
|
+
const people = byId(doc.people);
|
|
6639
|
+
const tasks = byId(doc.tasks);
|
|
6640
|
+
const raid = byId(doc.raidItems);
|
|
6641
|
+
const parts = /* @__PURE__ */ new Map();
|
|
6642
|
+
const partOf = (program) => {
|
|
6643
|
+
let part = parts.get(program.id);
|
|
6644
|
+
if (!part) {
|
|
6645
|
+
part = {
|
|
6646
|
+
program,
|
|
6647
|
+
covered: [],
|
|
6648
|
+
lines: []
|
|
6649
|
+
};
|
|
6650
|
+
parts.set(program.id, part);
|
|
6651
|
+
}
|
|
6652
|
+
return part;
|
|
6653
|
+
};
|
|
6654
|
+
for (const s of slides) partOf(s.program).covered.push(s.project);
|
|
6655
|
+
for (const item of logged) {
|
|
6656
|
+
if (item.kind === "action") {
|
|
6657
|
+
const t = tasks.get(item.id);
|
|
6658
|
+
const project = t ? projects.get(t.projectId) : void 0;
|
|
6659
|
+
const program = project ? doc.programs.find((p) => p.id === project.programId) : void 0;
|
|
6660
|
+
if (!t || !project || !program) continue;
|
|
6661
|
+
const who = t.assigneeId ? first(people.get(t.assigneeId)?.name) : null;
|
|
6662
|
+
const key = taskKey(t, project);
|
|
6663
|
+
partOf(program).lines.push({
|
|
6664
|
+
kind: "action",
|
|
6665
|
+
text: `${key ? `${key} ` : ""}${t.title}${who ? `, ${who}` : ""}${t.dueDate ? `, due ${t.dueDate}` : ""}`
|
|
6666
|
+
});
|
|
6667
|
+
continue;
|
|
6668
|
+
}
|
|
6669
|
+
const r = raid.get(item.id);
|
|
6670
|
+
const project = r?.projectId ? projects.get(r.projectId) : void 0;
|
|
6671
|
+
const program = project ? doc.programs.find((p) => p.id === project.programId) : void 0;
|
|
6672
|
+
if (!r || !program) continue;
|
|
6673
|
+
const said = r.kind === "decision" && r.resolution?.trim() ? `: ${r.resolution.trim()}` : "";
|
|
6674
|
+
partOf(program).lines.push({
|
|
6675
|
+
kind: item.kind,
|
|
6676
|
+
text: `${r.title}${said}`
|
|
6677
|
+
});
|
|
6678
|
+
}
|
|
6679
|
+
return [...parts.values()];
|
|
6680
|
+
}
|
|
6681
|
+
const LABEL = {
|
|
6682
|
+
decision: "Decided",
|
|
6683
|
+
action: "Action",
|
|
6684
|
+
risk: "Risk"
|
|
6685
|
+
};
|
|
6686
|
+
/** One program's part as the post for its chat channel: plain lines, capped well under every service's limit. */
|
|
6687
|
+
function summaryText(part, minutes) {
|
|
6688
|
+
const names = part.covered.map((p) => p.name);
|
|
6689
|
+
const head = `Review of ${part.program.name}: ${names.length} project${names.length === 1 ? "" : "s"} covered${minutes > 0 ? ` in ${minutes} min` : ""}${names.length ? ` (${names.join(", ")})` : ""}.`;
|
|
6690
|
+
const lines = part.lines.map((l) => `${LABEL[l.kind]}: ${l.text}`);
|
|
6691
|
+
return `${head}\n${lines.length ? lines.join("\n") : "Nothing new logged."}`.slice(0, 2e3);
|
|
6692
|
+
}
|
|
6693
|
+
/** The ids of programs whose projects the meeting covered, to stamp `reviewedAt` on. */
|
|
6694
|
+
function reviewedPrograms(slides, quiet) {
|
|
6695
|
+
return [.../* @__PURE__ */ new Set([...slides.map((s) => s.program.id), ...quiet.map((q) => q.program.id)])];
|
|
6696
|
+
}
|
|
6697
|
+
|
|
6698
|
+
//#endregion
|
|
6699
|
+
//#region src/tui/views/bridge.tsx
|
|
6700
|
+
init_date_fns();
|
|
6701
|
+
init_date_text();
|
|
6702
|
+
const WORD = {
|
|
6703
|
+
decision: "Decided",
|
|
6704
|
+
action: "Action",
|
|
6705
|
+
risk: "Risk"
|
|
6706
|
+
};
|
|
6707
|
+
/**
|
|
6708
|
+
* Bridge mode, as on the website's review: the meeting one project at a
|
|
6709
|
+
* time with only what changed since the program was last reviewed (←→ or
|
|
6710
|
+
* j k), d a decision, a an action, r a risk, m the program's next move, e
|
|
6711
|
+
* ends it. The summary lists what was logged; p posts it to each program's
|
|
6712
|
+
* chat channel where one is set, y copies it.
|
|
6713
|
+
*/
|
|
6714
|
+
function BridgeView({ active, width, height, go, programIds, onBack }) {
|
|
6715
|
+
const { store, transport, today } = useDeps();
|
|
6716
|
+
const workspaceId = useHarbor((s) => s.workspaceId);
|
|
6717
|
+
const viewOnly = useHarbor((s) => s.myRole === "viewer" || s.keepPaused);
|
|
6718
|
+
const programs = useHarbor((s) => s.programs);
|
|
6719
|
+
const projects = useHarbor((s) => s.projects);
|
|
6720
|
+
const tasks = useHarbor((s) => s.tasks);
|
|
6721
|
+
const raidItems = useHarbor((s) => s.raidItems);
|
|
6722
|
+
const people = useHarbor((s) => s.people);
|
|
6723
|
+
const milestones = useHarbor((s) => s.milestones);
|
|
6724
|
+
const me = useHarbor((s) => s.currentUserId);
|
|
6725
|
+
const { stdout } = useStdout();
|
|
6726
|
+
const data = useMemo(() => ({
|
|
6727
|
+
programs,
|
|
6728
|
+
projects,
|
|
6729
|
+
tasks,
|
|
6730
|
+
raidItems,
|
|
6731
|
+
people,
|
|
6732
|
+
milestones
|
|
6733
|
+
}), [
|
|
6734
|
+
programs,
|
|
6735
|
+
projects,
|
|
6736
|
+
tasks,
|
|
6737
|
+
raidItems,
|
|
6738
|
+
people,
|
|
6739
|
+
milestones
|
|
6740
|
+
]);
|
|
6741
|
+
const [started] = useState(() => /* @__PURE__ */ new Date());
|
|
6742
|
+
const [{ slides, quiet }] = useState(() => {
|
|
6743
|
+
const picked = programIds.map((id) => store.getState().programs.find((p) => p.id === id)).filter((p) => p !== void 0);
|
|
6744
|
+
return bridgeSlides(store.getState(), picked, started);
|
|
6745
|
+
});
|
|
6746
|
+
const [index, setIndex] = useState(0);
|
|
6747
|
+
const [logged, setLogged] = useState([]);
|
|
6748
|
+
const [ended, setEnded] = useState(slides.length === 0 ? started : null);
|
|
6749
|
+
const [channels, setChannels] = useState({});
|
|
6750
|
+
const parts = useMemo(() => meetingSummary(data, slides, logged), [
|
|
6751
|
+
data,
|
|
6752
|
+
slides,
|
|
6753
|
+
logged
|
|
6754
|
+
]);
|
|
6755
|
+
const minutes = ended ? Math.round((ended.getTime() - started.getTime()) / 6e4) : 0;
|
|
6756
|
+
useEffect(() => {
|
|
6757
|
+
if (!ended || !transport.meetingPost || viewOnly || !workspaceId) return;
|
|
6758
|
+
let live = true;
|
|
6759
|
+
for (const part of parts) transport.meetingPost(workspaceId, part.program.id).then((a) => {
|
|
6760
|
+
if (live && a.set && a.service) setChannels((c) => ({
|
|
6761
|
+
...c,
|
|
6762
|
+
[part.program.id]: a.service
|
|
6763
|
+
}));
|
|
6764
|
+
}).catch(() => {});
|
|
6765
|
+
return () => {
|
|
6766
|
+
live = false;
|
|
6767
|
+
};
|
|
6768
|
+
}, [ended]);
|
|
6769
|
+
const end = () => {
|
|
6770
|
+
const at = /* @__PURE__ */ new Date();
|
|
6771
|
+
setEnded(at);
|
|
6772
|
+
if (!viewOnly) store.getState().markReviewed(reviewedPrograms(slides, quiet), at.toISOString());
|
|
6773
|
+
};
|
|
6774
|
+
const slide = slides[index];
|
|
6775
|
+
const program = slide ? programs.find((p) => p.id === slide.program.id) ?? slide.program : null;
|
|
6776
|
+
const add = (kind, id) => setLogged((l) => [...l, {
|
|
6777
|
+
kind,
|
|
6778
|
+
id
|
|
6779
|
+
}]);
|
|
6780
|
+
const logDecision = () => {
|
|
6781
|
+
if (!slide) return;
|
|
6782
|
+
go.ask(`Decision on ${slide.project.name}: what was it about?`, (title) => {
|
|
6783
|
+
if (!title.trim()) return;
|
|
6784
|
+
go.ask("What was decided? (Enter to skip)", (said) => {
|
|
6785
|
+
add("decision", store.getState().addRaidItem({
|
|
6786
|
+
kind: "decision",
|
|
6787
|
+
title: title.trim(),
|
|
6788
|
+
projectId: slide.project.id,
|
|
6789
|
+
ownerId: me,
|
|
6790
|
+
status: "decided",
|
|
6791
|
+
resolution: said.trim(),
|
|
6792
|
+
decidedById: me,
|
|
6793
|
+
decidedAt: today()
|
|
6794
|
+
}));
|
|
6795
|
+
go.notify("Decision logged.");
|
|
6796
|
+
});
|
|
6797
|
+
});
|
|
6798
|
+
};
|
|
6799
|
+
const logAction = () => {
|
|
6800
|
+
if (!slide) return;
|
|
6801
|
+
go.ask(`Action on ${slide.project.name}: what needs doing?`, (title) => {
|
|
6802
|
+
if (!title.trim()) return;
|
|
6803
|
+
const lead = people.find((p) => p.id === slide.project.ownerId);
|
|
6804
|
+
go.pick("Who takes it?", [
|
|
6805
|
+
...lead ? [{
|
|
6806
|
+
label: lead.name,
|
|
6807
|
+
value: lead.id,
|
|
6808
|
+
hint: "leads it"
|
|
6809
|
+
}] : [],
|
|
6810
|
+
...people.filter((p) => p.id !== lead?.id && p.kind !== "agent").map((p) => ({
|
|
6811
|
+
label: p.name,
|
|
6812
|
+
value: p.id,
|
|
6813
|
+
hint: p.role
|
|
6814
|
+
})),
|
|
6815
|
+
{
|
|
6816
|
+
label: "Nobody yet",
|
|
6817
|
+
value: null
|
|
6818
|
+
}
|
|
6819
|
+
], (who) => {
|
|
6820
|
+
const due = addDays(/* @__PURE__ */ new Date(`${today()}T12:00:00`), 7).toISOString().slice(0, 10);
|
|
6821
|
+
add("action", store.getState().addTask({
|
|
6822
|
+
projectId: slide.project.id,
|
|
6823
|
+
title: title.trim(),
|
|
6824
|
+
description: "",
|
|
6825
|
+
status: "todo",
|
|
6826
|
+
priority: "medium",
|
|
6827
|
+
assigneeId: who ?? null,
|
|
6828
|
+
dueDate: due
|
|
6829
|
+
}, { allowOverCap: true }));
|
|
6830
|
+
go.notify(`Action added, due ${shortDay(due, today())}.`);
|
|
6831
|
+
});
|
|
6832
|
+
});
|
|
6833
|
+
};
|
|
6834
|
+
const logRisk = () => {
|
|
6835
|
+
if (!slide) return;
|
|
6836
|
+
go.ask(`Risk on ${slide.project.name}: what could go wrong?`, (title) => {
|
|
6837
|
+
if (!title.trim()) return;
|
|
6838
|
+
add("risk", store.getState().addRaidItem({
|
|
6839
|
+
kind: "risk",
|
|
6840
|
+
title: title.trim(),
|
|
6841
|
+
projectId: slide.project.id,
|
|
6842
|
+
ownerId: slide.project.ownerId || null,
|
|
6843
|
+
status: "open"
|
|
6844
|
+
}));
|
|
6845
|
+
go.notify("Risk raised.");
|
|
6846
|
+
});
|
|
6847
|
+
};
|
|
6848
|
+
const post = async () => {
|
|
6849
|
+
const targets = parts.filter((p) => channels[p.program.id]);
|
|
6850
|
+
if (!transport.meetingPost || !workspaceId || targets.length === 0) return go.notify("None of these programs has a chat channel. Set one on the website's program page.");
|
|
6851
|
+
let ok = 0;
|
|
6852
|
+
for (const part of targets) if ((await transport.meetingPost(workspaceId, part.program.id, summaryText(part, minutes)).catch((e) => {
|
|
6853
|
+
go.notify(`Didn't post: ${e.message}`);
|
|
6854
|
+
return null;
|
|
6855
|
+
}))?.posted) ok++;
|
|
6856
|
+
if (ok) go.notify(`Posted to ${ok} channel${ok === 1 ? "" : "s"}.`);
|
|
6857
|
+
};
|
|
6858
|
+
useInput((input, key) => {
|
|
6859
|
+
if (ended) {
|
|
6860
|
+
if (key.escape) return onBack();
|
|
6861
|
+
if (input === "y") {
|
|
6862
|
+
stdout.write(copySequence(parts.map((p) => summaryText(p, minutes)).join("\n\n")));
|
|
6863
|
+
return go.notify("Copied the summary, if this terminal lets apps copy.");
|
|
6864
|
+
}
|
|
6865
|
+
if (input === "p" && !viewOnly) return void post();
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
6868
|
+
if (key.escape || input === "e") return end();
|
|
6869
|
+
if (key.rightArrow || input === "j" || input === " ") return setIndex((i) => Math.min(slides.length - 1, i + 1));
|
|
6870
|
+
if (key.leftArrow || input === "k") return setIndex((i) => Math.max(0, i - 1));
|
|
6871
|
+
if (viewOnly) return;
|
|
6872
|
+
if (input === "d") return logDecision();
|
|
6873
|
+
if (input === "a") return logAction();
|
|
6874
|
+
if (input === "r") return logRisk();
|
|
6875
|
+
if (input === "m" && program) return go.ask(`Next move for ${program.name}`, (text) => store.getState().setNextMove(program.id, text.trim()), program.nextMove ?? "");
|
|
6876
|
+
}, { isActive: active });
|
|
6877
|
+
const w = width - 2;
|
|
6878
|
+
if (ended) {
|
|
6879
|
+
const text = [
|
|
6880
|
+
parts.length ? `${parts.reduce((n, p) => n + p.covered.length, 0)} projects covered${minutes ? ` in ${minutes} min` : ""}. The next meeting shows what changes from here.` : "Nothing changed since the last review.",
|
|
6881
|
+
...parts.flatMap((part) => [
|
|
6882
|
+
"",
|
|
6883
|
+
`${part.program.name}${channels[part.program.id] ? ` · ${channels[part.program.id]} channel` : ""}`,
|
|
6884
|
+
`Covered: ${part.covered.map((p) => p.name).join(", ") || "none"}`,
|
|
6885
|
+
...part.lines.length ? part.lines.map((l) => `• ${WORD[l.kind]}: ${l.text}`) : ["Nothing new logged."]
|
|
6886
|
+
]),
|
|
6887
|
+
...quiet.length ? ["", `No change on ${quiet.map((q) => q.project.name).join(", ")}.`] : []
|
|
6888
|
+
].join("\n");
|
|
6889
|
+
const lines = wrapLines(text, w).slice(0, Math.max(1, height - 2));
|
|
6890
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
6891
|
+
flexDirection: "column",
|
|
6892
|
+
paddingX: 1,
|
|
6893
|
+
width,
|
|
6894
|
+
children: [/* @__PURE__ */ jsx(Heading, {
|
|
6895
|
+
right: `${Object.keys(channels).length && !viewOnly ? "p post · " : ""}y copy · Esc back`,
|
|
6896
|
+
children: "Meeting summary"
|
|
6897
|
+
}), lines.map((line, i) => /* @__PURE__ */ jsx(Text, {
|
|
6898
|
+
wrap: "truncate-end",
|
|
6899
|
+
children: line || " "
|
|
6900
|
+
}, i))]
|
|
6901
|
+
});
|
|
6902
|
+
}
|
|
6903
|
+
if (!slide || !program) return null;
|
|
6904
|
+
const owner = people.find((p) => p.id === slide.project.ownerId);
|
|
6905
|
+
const here = meetingSummary(data, [], logged.filter((l) => l.kind === "action" ? tasks.some((t) => t.id === l.id && t.projectId === slide.project.id) : raidItems.some((r) => r.id === l.id && r.projectId === slide.project.id))).flatMap((p) => p.lines);
|
|
6906
|
+
const text = [
|
|
6907
|
+
[
|
|
6908
|
+
HEALTH_LABEL[slide.project.health],
|
|
6909
|
+
owner?.name,
|
|
6910
|
+
`ends ${shortDay(slide.project.endDate, today())}`,
|
|
6911
|
+
`${slide.late} late of ${slide.open} open`,
|
|
6912
|
+
slide.highRisks ? `${slide.highRisks} high risk${slide.highRisks === 1 ? "" : "s"}` : null
|
|
6913
|
+
].filter(Boolean).join(" · "),
|
|
6914
|
+
slide.milestone ? `Next milestone: ${slide.milestone.name}, ${shortDay(slide.milestone.dueDate, today())}` : "",
|
|
6915
|
+
"",
|
|
6916
|
+
`Since ${program.reviewedAt ? "the last review" : "last week"}, ${timeAgo(new Date(slide.since), started)}:`,
|
|
6917
|
+
...slide.changes.map((c) => `• ${c.text}`),
|
|
6918
|
+
...here.length ? [
|
|
6919
|
+
"",
|
|
6920
|
+
"Logged in this meeting:",
|
|
6921
|
+
...here.map((l) => `• ${WORD[l.kind]}: ${l.text}`)
|
|
6922
|
+
] : [],
|
|
6923
|
+
"",
|
|
6924
|
+
`Next move: ${program.nextMove?.trim() || "none yet"}`
|
|
6925
|
+
].filter((l, i, all) => l !== "" || all[i - 1] !== "").join("\n");
|
|
6926
|
+
const lines = wrapLines(text, w).slice(0, Math.max(1, height - 3));
|
|
6927
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
6928
|
+
flexDirection: "column",
|
|
6929
|
+
paddingX: 1,
|
|
6930
|
+
width,
|
|
6931
|
+
children: [
|
|
6932
|
+
/* @__PURE__ */ jsxs(Heading, {
|
|
6933
|
+
right: `${index + 1} of ${slides.length} · ${program.name}`,
|
|
6934
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
6935
|
+
color: theme.accent,
|
|
6936
|
+
children: "Meeting ▸ "
|
|
6937
|
+
}), slide.project.name]
|
|
6938
|
+
}),
|
|
6939
|
+
lines.map((line, i) => /* @__PURE__ */ jsx(Text, {
|
|
6940
|
+
wrap: "truncate-end",
|
|
6941
|
+
children: line || " "
|
|
6942
|
+
}, i)),
|
|
6943
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
6944
|
+
color: theme.muted,
|
|
6945
|
+
wrap: "truncate-end",
|
|
6946
|
+
children: [
|
|
6947
|
+
"←→ projects",
|
|
6948
|
+
viewOnly ? "" : " · d decision · a action · r risk · m next move",
|
|
6949
|
+
" · e end"
|
|
6950
|
+
]
|
|
6951
|
+
})
|
|
6952
|
+
]
|
|
6953
|
+
});
|
|
6954
|
+
}
|
|
6955
|
+
|
|
6956
|
+
//#endregion
|
|
6957
|
+
//#region src/lib/review.ts
|
|
6958
|
+
init_indexes();
|
|
6959
|
+
init_derived_health();
|
|
6960
|
+
/** A program is someone's when they lead it, lead one of its projects or hold a task in one. */
|
|
6961
|
+
function programTouches(program, data, personId) {
|
|
6962
|
+
if (program.ownerId === personId) return true;
|
|
6963
|
+
return data.projects.some((p) => p.programId === program.id && (p.ownerId === personId || tasksOf(data.tasks, p.id).some((t) => t.assigneeId === personId)));
|
|
6964
|
+
}
|
|
6965
|
+
/**
|
|
6966
|
+
* The review's programs: worst health first, then soonest end, narrowed to
|
|
6967
|
+
* one person's and one portfolio's when asked. Done ones are left in; the
|
|
6968
|
+
* website, the terminal and the agents' tool each tuck them away themselves.
|
|
6969
|
+
*/
|
|
6970
|
+
function reviewPrograms(data, opts = {}) {
|
|
6971
|
+
const { ownerId, portfolioId } = opts;
|
|
6972
|
+
return data.programs.filter((p) => (!portfolioId || p.portfolioId === portfolioId) && (!ownerId || programTouches(p, data, ownerId))).sort((a, b) => rankHealth(a.health) - rankHealth(b.health) || a.endDate.localeCompare(b.endDate));
|
|
6973
|
+
}
|
|
6974
|
+
|
|
6488
6975
|
//#endregion
|
|
6489
6976
|
//#region src/tui/moves.ts
|
|
6490
6977
|
init_board();
|
|
@@ -7747,26 +8234,6 @@ var init_load = __esmMin((() => {
|
|
|
7747
8234
|
};
|
|
7748
8235
|
}));
|
|
7749
8236
|
|
|
7750
|
-
//#endregion
|
|
7751
|
-
//#region src/lib/review.ts
|
|
7752
|
-
init_load();
|
|
7753
|
-
init_indexes();
|
|
7754
|
-
init_derived_health();
|
|
7755
|
-
/** A program is someone's when they lead it, lead one of its projects or hold a task in one. */
|
|
7756
|
-
function programTouches(program, data, personId) {
|
|
7757
|
-
if (program.ownerId === personId) return true;
|
|
7758
|
-
return data.projects.some((p) => p.programId === program.id && (p.ownerId === personId || tasksOf(data.tasks, p.id).some((t) => t.assigneeId === personId)));
|
|
7759
|
-
}
|
|
7760
|
-
/**
|
|
7761
|
-
* The review's programs: worst health first, then soonest end, narrowed to
|
|
7762
|
-
* one person's and one portfolio's when asked. Done ones are left in; the
|
|
7763
|
-
* website, the terminal and the agents' tool each tuck them away themselves.
|
|
7764
|
-
*/
|
|
7765
|
-
function reviewPrograms(data, opts = {}) {
|
|
7766
|
-
const { ownerId, portfolioId } = opts;
|
|
7767
|
-
return data.programs.filter((p) => (!portfolioId || p.portfolioId === portfolioId) && (!ownerId || programTouches(p, data, ownerId))).sort((a, b) => rankHealth(a.health) - rankHealth(b.health) || a.endDate.localeCompare(b.endDate));
|
|
7768
|
-
}
|
|
7769
|
-
|
|
7770
8237
|
//#endregion
|
|
7771
8238
|
//#region src/lib/priority.ts
|
|
7772
8239
|
/** Rules with anything missing or out of range brought back to the defaults. */
|
|
@@ -7804,6 +8271,7 @@ var init_priority = __esmMin((() => {
|
|
|
7804
8271
|
|
|
7805
8272
|
//#endregion
|
|
7806
8273
|
//#region src/tui/views/heat.tsx
|
|
8274
|
+
init_load();
|
|
7807
8275
|
init_priority();
|
|
7808
8276
|
init_raid();
|
|
7809
8277
|
const CELL = 7;
|
|
@@ -9242,7 +9710,7 @@ function ProgramsView({ active, width, height, go }) {
|
|
|
9242
9710
|
const where = portfolio ? ` in ${portfolios.find((p) => p.id === portfolio)?.name ?? "a portfolio"}` : "";
|
|
9243
9711
|
return /* @__PURE__ */ jsx(RowList, {
|
|
9244
9712
|
title: `${whose}${where}`,
|
|
9245
|
-
right: `${count(listed.length, "program")}${done ? ` · ${done} done ${showDone ? "shown" : "hidden"} (D)` : ""} · m next move · h health · o p narrow`,
|
|
9713
|
+
right: `${count(listed.length, "program")}${done ? ` · ${done} done ${showDone ? "shown" : "hidden"} (D)` : ""} · b run the meeting · m next move · h health · o p narrow`,
|
|
9246
9714
|
rows,
|
|
9247
9715
|
empty: done ? "Every program here is done. D shows them." : owner || portfolio ? "Nothing here. o and p change what's shown." : "No programs yet. n adds one.",
|
|
9248
9716
|
active,
|
|
@@ -9255,6 +9723,7 @@ function ProgramsView({ active, width, height, go }) {
|
|
|
9255
9723
|
go.create("program", portfolioId ? { portfolioId } : {});
|
|
9256
9724
|
},
|
|
9257
9725
|
e: (r) => r && go.openPanel("program", r.id),
|
|
9726
|
+
b: () => split.open.length ? go.openBridge(split.open.map((p) => p.id)) : go.notify("No open programs here to review."),
|
|
9258
9727
|
m: (r) => {
|
|
9259
9728
|
const program = r && programs.find((p) => p.id === r.id);
|
|
9260
9729
|
if (!program) return;
|
|
@@ -12858,6 +13327,8 @@ const NAV = [
|
|
|
12858
13327
|
const navKey = (i) => i < 10 ? String((i + 1) % 10) : "C";
|
|
12859
13328
|
/** The sidebar's width: its longest line ("9 Calendar", ": Anything") with room either side. */
|
|
12860
13329
|
const NAV_WIDTH = 15;
|
|
13330
|
+
/** Every open program, in the review's order: what a meeting started from the palette covers. */
|
|
13331
|
+
const openReviewIds = (data) => reviewPrograms(data).filter((p) => p.health !== "done").map((p) => p.id);
|
|
12861
13332
|
/** What the corner says about saving, and in which of the theme's colours. */
|
|
12862
13333
|
const SAVE_LABEL = {
|
|
12863
13334
|
saved: ["● saved", "good"],
|
|
@@ -12866,7 +13337,7 @@ const SAVE_LABEL = {
|
|
|
12866
13337
|
retrying: ["● not saved, retrying", "bad"],
|
|
12867
13338
|
offline: ["● offline, will save", "bad"]
|
|
12868
13339
|
};
|
|
12869
|
-
function App({ ui }) {
|
|
13340
|
+
function App({ ui, startIn }) {
|
|
12870
13341
|
const { store, sync, account, harbors, today, remember } = useDeps();
|
|
12871
13342
|
const { exit } = useApp();
|
|
12872
13343
|
const { columns, rows } = useWindowSize();
|
|
@@ -12879,7 +13350,11 @@ function App({ ui }) {
|
|
|
12879
13350
|
const save = useStore(ui, (s) => s.save);
|
|
12880
13351
|
const notice = useStore(ui, (s) => s.notice);
|
|
12881
13352
|
const [screen, setScreen] = useState("today");
|
|
12882
|
-
const [stack, setStack] = useState([
|
|
13353
|
+
const [stack, setStack] = useState(() => startIn === "bridge" ? [{
|
|
13354
|
+
kind: "bridge",
|
|
13355
|
+
id: "bridge-start",
|
|
13356
|
+
programIds: openReviewIds(store.getState())
|
|
13357
|
+
}] : []);
|
|
12883
13358
|
const [panel, setPanel] = useState(null);
|
|
12884
13359
|
const [overlay, setOverlay] = useState(null);
|
|
12885
13360
|
const [focus, setFocus] = useState("main");
|
|
@@ -12916,6 +13391,11 @@ function App({ ui }) {
|
|
|
12916
13391
|
kind: "portfolio",
|
|
12917
13392
|
id
|
|
12918
13393
|
}),
|
|
13394
|
+
openBridge: (programIds) => push({
|
|
13395
|
+
kind: "bridge",
|
|
13396
|
+
id: `bridge-${Date.now()}`,
|
|
13397
|
+
programIds
|
|
13398
|
+
}),
|
|
12919
13399
|
openPanel: (kind, id) => {
|
|
12920
13400
|
setPanel({
|
|
12921
13401
|
kind,
|
|
@@ -13009,6 +13489,11 @@ function App({ ui }) {
|
|
|
13009
13489
|
hint: "make",
|
|
13010
13490
|
run: () => go.create("raid")
|
|
13011
13491
|
},
|
|
13492
|
+
{
|
|
13493
|
+
label: "Run the review meeting",
|
|
13494
|
+
hint: "bridge",
|
|
13495
|
+
run: () => go.openBridge(openReviewIds(store.getState()))
|
|
13496
|
+
},
|
|
13012
13497
|
{
|
|
13013
13498
|
label: "Find a task",
|
|
13014
13499
|
hint: "/",
|
|
@@ -13045,7 +13530,7 @@ function App({ ui }) {
|
|
|
13045
13530
|
if (input === "?") return setOverlay({ kind: "help" });
|
|
13046
13531
|
if (input === "H") return setOverlay({ kind: "harbors" });
|
|
13047
13532
|
if (input === "C") return goScreen("crew");
|
|
13048
|
-
if (input === "r") {
|
|
13533
|
+
if (input === "r" && page?.kind !== "bridge") {
|
|
13049
13534
|
sync.retryNow();
|
|
13050
13535
|
return notify("Saving now.");
|
|
13051
13536
|
}
|
|
@@ -13178,7 +13663,7 @@ function App({ ui }) {
|
|
|
13178
13663
|
close();
|
|
13179
13664
|
then(text);
|
|
13180
13665
|
}
|
|
13181
|
-
})
|
|
13666
|
+
}, `${label}\u0000${initial ?? ""}`)
|
|
13182
13667
|
});
|
|
13183
13668
|
} else if (overlay?.kind === "read") overlayNode = /* @__PURE__ */ jsx(Letter, {
|
|
13184
13669
|
title: overlay.title,
|
|
@@ -13248,7 +13733,12 @@ function App({ ui }) {
|
|
|
13248
13733
|
id: panel.id,
|
|
13249
13734
|
...panelProps
|
|
13250
13735
|
});
|
|
13251
|
-
const hints = panel ? [["Esc", "back"], ["u", "undo"]] : page?.kind === "
|
|
13736
|
+
const hints = panel ? [["Esc", "back"], ["u", "undo"]] : page?.kind === "bridge" ? [
|
|
13737
|
+
["←→", "projects"],
|
|
13738
|
+
["d a r", "log"],
|
|
13739
|
+
["e", "end"],
|
|
13740
|
+
["Esc", "end/back"]
|
|
13741
|
+
] : page?.kind === "project" ? [
|
|
13252
13742
|
["←→↑↓", "move"],
|
|
13253
13743
|
["Enter", "open"],
|
|
13254
13744
|
["n", "new"],
|
|
@@ -13279,7 +13769,12 @@ function App({ ui }) {
|
|
|
13279
13769
|
["q", "quit"]
|
|
13280
13770
|
];
|
|
13281
13771
|
let screenNode;
|
|
13282
|
-
if (page?.kind === "
|
|
13772
|
+
if (page?.kind === "bridge") screenNode = /* @__PURE__ */ jsx(BridgeView, {
|
|
13773
|
+
...viewProps,
|
|
13774
|
+
programIds: page.programIds,
|
|
13775
|
+
onBack: back
|
|
13776
|
+
}, page.id);
|
|
13777
|
+
else if (page?.kind === "project") screenNode = /* @__PURE__ */ jsx(ProjectView, {
|
|
13283
13778
|
...viewProps,
|
|
13284
13779
|
projectId: page.id,
|
|
13285
13780
|
onBack: back
|
|
@@ -14405,6 +14900,15 @@ function hierarchySlice({ get, write }) {
|
|
|
14405
14900
|
nextMove: text
|
|
14406
14901
|
} : p) });
|
|
14407
14902
|
},
|
|
14903
|
+
markReviewed: (programIds, at) => {
|
|
14904
|
+
const ids = new Set(programIds);
|
|
14905
|
+
if (ids.size === 0) return;
|
|
14906
|
+
const when = at ?? stamp();
|
|
14907
|
+
write({ programs: get().programs.map((p) => ids.has(p.id) ? {
|
|
14908
|
+
...p,
|
|
14909
|
+
reviewedAt: when
|
|
14910
|
+
} : p) });
|
|
14911
|
+
},
|
|
14408
14912
|
setHealthOverride: (kind, id, health, note) => {
|
|
14409
14913
|
const at = stamp();
|
|
14410
14914
|
const body = {
|
|
@@ -16486,7 +16990,8 @@ function plainTransport(t) {
|
|
|
16486
16990
|
history: async (query) => plainDeep(await t.history(query)),
|
|
16487
16991
|
create: async (name) => plainDeep(await t.create(name)),
|
|
16488
16992
|
crew: async (workspaceId, act) => plainDeep(await t.crew(workspaceId, act)),
|
|
16489
|
-
invite: async (workspaceId, email, role) => plainDeep(await t.invite(workspaceId, email, role))
|
|
16993
|
+
invite: async (workspaceId, email, role) => plainDeep(await t.invite(workspaceId, email, role)),
|
|
16994
|
+
...t.meetingPost ? { meetingPost: async (workspaceId, programId, text) => plainDeep(await t.meetingPost(workspaceId, programId, text)) } : {}
|
|
16490
16995
|
};
|
|
16491
16996
|
}
|
|
16492
16997
|
function httpTransport(source, fetcher = fetch) {
|
|
@@ -16550,6 +17055,12 @@ function httpTransport(source, fetcher = fetch) {
|
|
|
16550
17055
|
harbor: workspaceId,
|
|
16551
17056
|
email,
|
|
16552
17057
|
role
|
|
17058
|
+
}),
|
|
17059
|
+
meetingPost: (workspaceId, programId, text) => post({
|
|
17060
|
+
op: "meeting-post",
|
|
17061
|
+
workspaceId,
|
|
17062
|
+
programId,
|
|
17063
|
+
...text ? { text } : {}
|
|
16553
17064
|
})
|
|
16554
17065
|
};
|
|
16555
17066
|
}
|
|
@@ -17274,6 +17785,7 @@ function memoryHarbor(start, opts) {
|
|
|
17274
17785
|
id,
|
|
17275
17786
|
name
|
|
17276
17787
|
});
|
|
17788
|
+
const posts = [];
|
|
17277
17789
|
return {
|
|
17278
17790
|
transport: {
|
|
17279
17791
|
crew: opts.noCrew ? async () => Promise.reject(new TransportError(opts.noCrew, 404)) : crew.crew,
|
|
@@ -17317,6 +17829,23 @@ function memoryHarbor(start, opts) {
|
|
|
17317
17829
|
};
|
|
17318
17830
|
return save(sent);
|
|
17319
17831
|
},
|
|
17832
|
+
async meetingPost(_id, programId, text) {
|
|
17833
|
+
unreachable();
|
|
17834
|
+
const service = opts.channels?.[programId] ?? null;
|
|
17835
|
+
if (!text) return {
|
|
17836
|
+
set: Boolean(service),
|
|
17837
|
+
service
|
|
17838
|
+
};
|
|
17839
|
+
if (service) posts.push({
|
|
17840
|
+
programId,
|
|
17841
|
+
text
|
|
17842
|
+
});
|
|
17843
|
+
return {
|
|
17844
|
+
set: Boolean(service),
|
|
17845
|
+
service,
|
|
17846
|
+
posted: Boolean(service)
|
|
17847
|
+
};
|
|
17848
|
+
},
|
|
17320
17849
|
async create(made) {
|
|
17321
17850
|
return { harbor: {
|
|
17322
17851
|
id,
|
|
@@ -17360,6 +17889,8 @@ function memoryHarbor(start, opts) {
|
|
|
17360
17889
|
}
|
|
17361
17890
|
},
|
|
17362
17891
|
now,
|
|
17892
|
+
/** What was posted to the pretend chat channels, oldest first. */
|
|
17893
|
+
posts,
|
|
17363
17894
|
/** How many versions this harbor has been through. */
|
|
17364
17895
|
get versions() {
|
|
17365
17896
|
return versions.length;
|
|
@@ -26047,7 +26578,10 @@ async function startApp(deps, options = {}) {
|
|
|
26047
26578
|
remember: deps.remember,
|
|
26048
26579
|
browse: deps.browse
|
|
26049
26580
|
},
|
|
26050
|
-
children: /* @__PURE__ */ jsx(App, {
|
|
26581
|
+
children: /* @__PURE__ */ jsx(App, {
|
|
26582
|
+
ui,
|
|
26583
|
+
startIn: deps.startIn
|
|
26584
|
+
})
|
|
26051
26585
|
}));
|
|
26052
26586
|
return {
|
|
26053
26587
|
instance,
|
|
@@ -26125,7 +26659,8 @@ async function runApp(opts) {
|
|
|
26125
26659
|
theme,
|
|
26126
26660
|
firstRun,
|
|
26127
26661
|
remember: opts.remember,
|
|
26128
|
-
browse: opts.browse
|
|
26662
|
+
browse: opts.browse,
|
|
26663
|
+
startIn: opts.startIn
|
|
26129
26664
|
}, { alternateScreen: true });
|
|
26130
26665
|
await instance.waitUntilExit();
|
|
26131
26666
|
}
|
package/lib/cli.mjs
CHANGED
|
@@ -161,6 +161,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
161
161
|
the review: programs worst first,
|
|
162
162
|
each with its next move (reads
|
|
163
163
|
only; harbor submit moves a task)
|
|
164
|
+
harbor bridge [--harbor H] run the review as a meeting: one
|
|
165
|
+
project a screen, what changed
|
|
166
|
+
since the last one, decisions,
|
|
167
|
+
actions and risks logged live
|
|
164
168
|
harbor next-move PROGRAM "TEXT" write a program's next move
|
|
165
169
|
harbor health --program P|--project P|--portfolio P STATE [--note WHY]
|
|
166
170
|
set its health by hand, or auto
|
|
@@ -767,6 +771,20 @@ const DETAIL = {
|
|
|
767
771
|
" enough of it to pick one out; harbor risks lists them. Changes several",
|
|
768
772
|
" at once, as one change.",
|
|
769
773
|
],
|
|
774
|
+
bridge: [
|
|
775
|
+
" harbor bridge opens the full-screen app on the",
|
|
776
|
+
" review meeting (b on the Programs",
|
|
777
|
+
" screen, or : Run the review meeting)",
|
|
778
|
+
"",
|
|
779
|
+
" One open project a screen, with only what changed since its program was",
|
|
780
|
+
" last reviewed (a week back the first time): health moves, work finished,",
|
|
781
|
+
" newly late or added, risks raised, decisions made. ←→ move between",
|
|
782
|
+
" projects; d logs a decision, a an action (who takes it, due in a week),",
|
|
783
|
+
" r a risk, m the program's next move; e ends it. The summary lists what",
|
|
784
|
+
" was logged; p posts it to each program's chat channel where one is set",
|
|
785
|
+
" (Ship to shore, set on the website), y copies it. The next meeting",
|
|
786
|
+
" counts from when this one ended.",
|
|
787
|
+
],
|
|
770
788
|
review: [
|
|
771
789
|
" harbor review reads the review: every program,",
|
|
772
790
|
" worst health first, then soonest end,",
|
|
@@ -990,7 +1008,7 @@ export async function run(argv, io = {}) {
|
|
|
990
1008
|
}
|
|
991
1009
|
|
|
992
1010
|
// On its own in a terminal: the full-screen app. Piped or scripted: the help.
|
|
993
|
-
if (!command || command === "app") {
|
|
1011
|
+
if (!command || command === "app" || command === "bridge") {
|
|
994
1012
|
const terminal = io.terminal ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
995
1013
|
if (!command && !terminal) {
|
|
996
1014
|
out(HELP);
|
|
@@ -999,7 +1017,7 @@ export async function run(argv, io = {}) {
|
|
|
999
1017
|
if (!terminal) throw new HarborError("The full-screen app needs a terminal.");
|
|
1000
1018
|
const { values } = parseArgs(rest, { flags: [], options: ["harbor", "server"] });
|
|
1001
1019
|
// In a folder with a local plan (harbor init --local), the app opens on it.
|
|
1002
|
-
if (!values.harbor && !values.server) {
|
|
1020
|
+
if (!values.harbor && !values.server && command !== "bridge") {
|
|
1003
1021
|
const bundled = await loadApp();
|
|
1004
1022
|
const dir = await bundled.localPlanAt(io.cwd ?? process.cwd());
|
|
1005
1023
|
if (dir) {
|
|
@@ -1056,6 +1074,7 @@ export async function run(argv, io = {}) {
|
|
|
1056
1074
|
},
|
|
1057
1075
|
theme: (await readSettings(env)).theme,
|
|
1058
1076
|
browse: io.browse ?? openBrowser,
|
|
1077
|
+
...(command === "bridge" ? { startIn: "bridge" } : {}),
|
|
1059
1078
|
});
|
|
1060
1079
|
return 0;
|
|
1061
1080
|
}
|