@harborppm/cli 0.60.0 → 0.62.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 +10 -5
- package/dist/app.mjs +864 -309
- package/lib/cli.mjs +166 -22
- package/lib/format.mjs +62 -3
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -39,9 +39,11 @@ import {
|
|
|
39
39
|
lifeboatsText,
|
|
40
40
|
followedText,
|
|
41
41
|
soundingsText,
|
|
42
|
+
thanksText,
|
|
42
43
|
ashoreText,
|
|
43
44
|
voyageText,
|
|
44
45
|
whyText,
|
|
46
|
+
bridgeText,
|
|
45
47
|
barnaclesText,
|
|
46
48
|
handoverText,
|
|
47
49
|
fogText,
|
|
@@ -143,10 +145,11 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
143
145
|
project as that one really ran
|
|
144
146
|
harbor add "TITLE" --project P [--assignee me|none|NAME]
|
|
145
147
|
[--due DATE|pilot] [--priority low|medium|high|urgent]
|
|
146
|
-
[--status S] [--comment TEXT] [--over-limit]
|
|
148
|
+
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
147
149
|
with no TITLE: one title a line
|
|
148
150
|
from stdin; --due pilot takes the
|
|
149
151
|
date the owner's queue suggests
|
|
152
|
+
(harbor help add)
|
|
150
153
|
harbor start TASK | submit TASK | done TASK [--confirm] [--over-limit]
|
|
151
154
|
move a task to In play, In review
|
|
152
155
|
or Done
|
|
@@ -161,9 +164,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
161
164
|
your own week: tasks on days
|
|
162
165
|
against your hours, only for
|
|
163
166
|
you; --review: planned vs done
|
|
164
|
-
harbor week TASK DAY [--hours N]
|
|
165
|
-
harbor week TASK --off [DAY]
|
|
167
|
+
harbor week TASK --day DAY [--hours N] put a task on a day: mon to fri
|
|
168
|
+
harbor week TASK --off [--day DAY] take it off that day, or the week
|
|
166
169
|
harbor week --day-hours N the hours you work a day
|
|
170
|
+
(harbor help week)
|
|
167
171
|
harbor comment TASK "TEXT"
|
|
168
172
|
harbor checkin "WHAT YOU'RE DOING" [--on-blocker]
|
|
169
173
|
harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
|
|
@@ -188,10 +192,17 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
188
192
|
the review: programs worst first,
|
|
189
193
|
each with its next move (reads
|
|
190
194
|
only; harbor submit moves a task)
|
|
195
|
+
harbor thanks [--all] [--given] thanks you were given this month
|
|
196
|
+
harbor thanks TASK "LINE" [--to NAME] a one-line thanks for a task
|
|
197
|
+
harbor thanks --milestone NAME|--decision TITLE "LINE" [--to NAME]
|
|
198
|
+
the same for a milestone or a
|
|
199
|
+
decision
|
|
191
200
|
harbor bridge [--harbor H] run the review as a meeting: one
|
|
192
201
|
project a screen, what changed
|
|
193
202
|
since the last one, decisions,
|
|
194
203
|
actions and risks logged live
|
|
204
|
+
harbor bridge --list [--program P] what changed since the last
|
|
205
|
+
review, as text (or --json)
|
|
195
206
|
harbor next-move PROGRAM "TEXT" write a program's next move
|
|
196
207
|
harbor health --program P|--project P|--portfolio P STATE [--note WHY]
|
|
197
208
|
set its health by hand, or auto
|
|
@@ -239,6 +250,8 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
239
250
|
harbor fog keep TASK it's still needed: out of the fog
|
|
240
251
|
harbor watch [--project P] [--bell] a live ticker of the harbor's
|
|
241
252
|
changes; yours bright, q stops
|
|
253
|
+
harbor watch --once [--since TIME] [--json] the latest changes (or all since
|
|
254
|
+
TIME), printed once, for scripts
|
|
242
255
|
harbor lifeboats [--program P] work only one person knows, each
|
|
243
256
|
with a second pair of hands
|
|
244
257
|
harbor lifeboats N [--person NAME] adds that second pair of hands
|
|
@@ -544,6 +557,34 @@ export function voyageForTerminal(r, project = r.project) {
|
|
|
544
557
|
|
|
545
558
|
/** Commands whose forms do different things: one line each, saying which read and which write. */
|
|
546
559
|
const DETAIL = {
|
|
560
|
+
add: [
|
|
561
|
+
' harbor add "TITLE" --project P writes: one task, yours unless',
|
|
562
|
+
" --assignee says otherwise",
|
|
563
|
+
" harbor add --project P < titles.txt writes: one task a line of stdin",
|
|
564
|
+
" harbor add --project P --dry-run reads: says what it would add, and",
|
|
565
|
+
" adds nothing",
|
|
566
|
+
"",
|
|
567
|
+
" --assignee me|none|NAME, --due DATE|pilot, --priority low|medium|high|urgent,",
|
|
568
|
+
" --status S, --comment TEXT and --over-limit apply to every task added, so",
|
|
569
|
+
" ten lines with --comment add the same comment to all ten. --due pilot",
|
|
570
|
+
" takes the date the owner's queue suggests. More than 100 lines is harbor",
|
|
571
|
+
" import's job.",
|
|
572
|
+
],
|
|
573
|
+
week: [
|
|
574
|
+
" harbor week [--next|--last|--week DATE] reads: your week, the days and",
|
|
575
|
+
" their hours, and what's not on a",
|
|
576
|
+
" day yet",
|
|
577
|
+
" harbor week --review reads: what you planned against",
|
|
578
|
+
" what you finished",
|
|
579
|
+
" harbor week TASK --day DAY [--hours N] writes: puts the task on that day",
|
|
580
|
+
" (mon to fri), 2 hours unless --hours",
|
|
581
|
+
" harbor week TASK --off [--day DAY] writes: takes it off that day, or",
|
|
582
|
+
" off the week",
|
|
583
|
+
" harbor week --day-hours N writes: the hours you work a day",
|
|
584
|
+
"",
|
|
585
|
+
" Only you see your week. Tasks in review are left out of what's not on a",
|
|
586
|
+
" day yet, since they wait on someone else.",
|
|
587
|
+
],
|
|
547
588
|
init: [
|
|
548
589
|
" harbor init [PROJECT] [--hook] writes: .harbor/ at the top of this",
|
|
549
590
|
" repository, one file per task in",
|
|
@@ -817,10 +858,18 @@ const DETAIL = {
|
|
|
817
858
|
" cards moving, comments, health changes",
|
|
818
859
|
" --project P one project's changes only",
|
|
819
860
|
" --bell ring the terminal's bell when something of yours changes",
|
|
861
|
+
" --since TIME start with every change since TIME (14:30 today, or",
|
|
862
|
+
" 2026-10-21, or 2026-10-21T14:30) instead of the last 5",
|
|
863
|
+
" --once print those and stop instead of listening",
|
|
864
|
+
" --json one JSON object a line (at, text, quote, mine, tone;",
|
|
865
|
+
" earlier: true for the ones from before it started)",
|
|
820
866
|
"",
|
|
821
|
-
"
|
|
822
|
-
"
|
|
823
|
-
"
|
|
867
|
+
" It starts with the latest few changes and a \"since HH:MM\" line, then",
|
|
868
|
+
" prints each new one as it lands. Lines about your own work (your tasks,",
|
|
869
|
+
" mentions of you, what you lead) are bright; the rest are dim. It runs",
|
|
870
|
+
" until q or Ctrl+C (or until it's killed, when piped or with --json);",
|
|
871
|
+
" --once returns at once. Made for a narrow split or a tmux pane; it needs",
|
|
872
|
+
" full access (harbor login without --read-only).",
|
|
824
873
|
],
|
|
825
874
|
lifeboats: [
|
|
826
875
|
" harbor lifeboats [--program P] reads: work only one person has",
|
|
@@ -927,19 +976,40 @@ const DETAIL = {
|
|
|
927
976
|
" enough of it to pick one out; harbor risks lists them. Changes several",
|
|
928
977
|
" at once, as one change.",
|
|
929
978
|
],
|
|
979
|
+
thanks: [
|
|
980
|
+
" harbor thanks reads the thanks you were given in",
|
|
981
|
+
" the last 30 days, newest first",
|
|
982
|
+
" harbor thanks --all everyone's; --given: the ones you gave",
|
|
983
|
+
' harbor thanks RR-42 "LINE" writes: thanks whoever has the task',
|
|
984
|
+
' harbor thanks --milestone NAME "LINE" writes: thanks for a milestone (its',
|
|
985
|
+
" sign-off person unless --to says)",
|
|
986
|
+
' harbor thanks --decision TITLE "LINE" writes: thanks whoever made the call',
|
|
987
|
+
" --to NAME someone else instead",
|
|
988
|
+
"",
|
|
989
|
+
" Fair winds: a thanks shows on the work, on the person's card on People,",
|
|
990
|
+
" in the next brief and in the project's voyage log. Nobody can thank",
|
|
991
|
+
" themselves, and nothing counts or ranks them.",
|
|
992
|
+
],
|
|
930
993
|
bridge: [
|
|
931
994
|
" harbor bridge opens the full-screen app on the",
|
|
932
995
|
" review meeting (b on the Programs",
|
|
933
996
|
" screen, or : Run the review meeting)",
|
|
997
|
+
" harbor bridge --list [--program P] prints the agenda instead: what",
|
|
998
|
+
" changed on each project since its",
|
|
999
|
+
" program's last review, every item",
|
|
1000
|
+
" (--json for scripts and agents; the",
|
|
1001
|
+
" same when piped). Reading it doesn't",
|
|
1002
|
+
" count as holding the meeting",
|
|
934
1003
|
"",
|
|
935
1004
|
" One open project a screen, with only what changed since its program was",
|
|
936
1005
|
" last reviewed (a week back the first time): health moves, work finished,",
|
|
937
1006
|
" newly late or added, risks raised, decisions made. ←→ move between",
|
|
938
1007
|
" projects; d logs a decision, a an action (who takes it, due in a week),",
|
|
939
|
-
" r a risk, m the program's next move
|
|
940
|
-
"
|
|
941
|
-
"
|
|
942
|
-
"
|
|
1008
|
+
" r a risk, m the program's next move, v shows every item behind an",
|
|
1009
|
+
" \"and N more\" (↑↓ scroll); e ends it, Esc or q leaves without ending",
|
|
1010
|
+
" it. The summary lists what was logged; p posts it to each program's",
|
|
1011
|
+
" chat channel where one is set (Ship to shore, set on the website), y",
|
|
1012
|
+
" copies it. The next meeting counts from when this one ended.",
|
|
943
1013
|
],
|
|
944
1014
|
review: [
|
|
945
1015
|
" harbor review reads the review: every program,",
|
|
@@ -1164,8 +1234,14 @@ export async function run(argv, io = {}) {
|
|
|
1164
1234
|
}
|
|
1165
1235
|
|
|
1166
1236
|
// On its own in a terminal: the full-screen app. Piped or scripted: the help.
|
|
1167
|
-
|
|
1168
|
-
|
|
1237
|
+
const inTerminal = io.terminal ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
1238
|
+
// harbor bridge --list (or --json, or piped) prints the agenda through the commands below.
|
|
1239
|
+
const bridgeApp =
|
|
1240
|
+
command === "bridge" &&
|
|
1241
|
+
inTerminal &&
|
|
1242
|
+
!rest.some((a) => a === "--list" || a === "--json" || a.startsWith("--program"));
|
|
1243
|
+
if (!command || command === "app" || bridgeApp) {
|
|
1244
|
+
const terminal = inTerminal;
|
|
1169
1245
|
if (!command && !terminal) {
|
|
1170
1246
|
out(HELP);
|
|
1171
1247
|
return 0;
|
|
@@ -2059,7 +2135,10 @@ export async function run(argv, io = {}) {
|
|
|
2059
2135
|
}
|
|
2060
2136
|
case "add": {
|
|
2061
2137
|
const { positionals, values, harbor } = parse(
|
|
2062
|
-
spec(
|
|
2138
|
+
spec(
|
|
2139
|
+
["over-limit", "dry-run"],
|
|
2140
|
+
["project", "assignee", "due", "priority", "status", "comment"],
|
|
2141
|
+
),
|
|
2063
2142
|
);
|
|
2064
2143
|
if (!values.project) throw new HarborError("Say which project: --project NAME.");
|
|
2065
2144
|
// Several at once: one title a line on stdin, with no title (or -) after add.
|
|
@@ -2079,6 +2158,17 @@ export async function run(argv, io = {}) {
|
|
|
2079
2158
|
throw new HarborError(
|
|
2080
2159
|
"That's more than 100 tasks; harbor import FILE takes a spreadsheet.",
|
|
2081
2160
|
);
|
|
2161
|
+
if (values["dry-run"]) {
|
|
2162
|
+
const n = titles.length === 1 ? "1 task" : `${titles.length} tasks`;
|
|
2163
|
+
if (asJson) out(jsonText({ dryRun: true, project: values.project, titles }));
|
|
2164
|
+
else
|
|
2165
|
+
out(
|
|
2166
|
+
plain(
|
|
2167
|
+
[`Would add ${n} to ${values.project}:`, ...titles.map((t) => ` ${t}`)].join("\n"),
|
|
2168
|
+
),
|
|
2169
|
+
);
|
|
2170
|
+
return 0;
|
|
2171
|
+
}
|
|
2082
2172
|
const added = [];
|
|
2083
2173
|
for (const title of titles) {
|
|
2084
2174
|
const r = await client
|
|
@@ -2244,7 +2334,7 @@ export async function run(argv, io = {}) {
|
|
|
2244
2334
|
}
|
|
2245
2335
|
case "week": {
|
|
2246
2336
|
const { positionals, values, harbor } = parse(
|
|
2247
|
-
spec(["next", "last", "review", "off"], ["week", "hours", "day-hours"]),
|
|
2337
|
+
spec(["next", "last", "review", "off"], ["week", "hours", "day-hours", "day"]),
|
|
2248
2338
|
);
|
|
2249
2339
|
if ([values.next, values.last, values.week].filter(Boolean).length > 1)
|
|
2250
2340
|
throw new HarborError("Say one of --next, --last or --week DATE.");
|
|
@@ -2259,21 +2349,24 @@ export async function run(argv, io = {}) {
|
|
|
2259
2349
|
throw new HarborError(`--${name} is a number of hours, like 2 or 1.5.`);
|
|
2260
2350
|
return n;
|
|
2261
2351
|
};
|
|
2262
|
-
|
|
2263
|
-
|
|
2352
|
+
// The day comes as --day; a bare second word still works, as it did first.
|
|
2353
|
+
const [task, bareDay, ...extra] = positionals;
|
|
2354
|
+
if (extra.length || (bareDay && values.day))
|
|
2355
|
+
throw new HarborError("Give a task key and a day: harbor week RR-42 --day wed.");
|
|
2356
|
+
const day = values.day ?? bareDay;
|
|
2264
2357
|
const dayHours = number(values["day-hours"], "day-hours");
|
|
2265
2358
|
const show = (r) =>
|
|
2266
2359
|
[r.done, weekText(r, { ...opts, review: Boolean(values.review) })]
|
|
2267
2360
|
.filter(Boolean)
|
|
2268
2361
|
.join("\n\n");
|
|
2269
2362
|
if (!task && dayHours === undefined) {
|
|
2270
|
-
if (values.off || values.hours)
|
|
2271
|
-
throw new HarborError("Say which task: harbor week RR-42 wed --hours 3.");
|
|
2363
|
+
if (values.off || values.hours || values.day)
|
|
2364
|
+
throw new HarborError("Say which task: harbor week RR-42 --day wed --hours 3.");
|
|
2272
2365
|
return call("get_week", { harbor, week }, show);
|
|
2273
2366
|
}
|
|
2274
2367
|
if (task && !day && !values.off)
|
|
2275
2368
|
throw new HarborError(
|
|
2276
|
-
`Say which day for ${task}: harbor week ${task} wed, or --off to take it off the week.`,
|
|
2369
|
+
`Say which day for ${task}: harbor week ${task} --day wed, or --off to take it off the week.`,
|
|
2277
2370
|
);
|
|
2278
2371
|
return call(
|
|
2279
2372
|
"plan_week",
|
|
@@ -2663,13 +2756,18 @@ export async function run(argv, io = {}) {
|
|
|
2663
2756
|
return call("get_voyage_log", { harbor, project }, voyageText, voyageForTerminal);
|
|
2664
2757
|
}
|
|
2665
2758
|
case "watch": {
|
|
2666
|
-
const { positionals, values, harbor } = parse(spec(["bell"], ["project"]));
|
|
2759
|
+
const { positionals, values, harbor } = parse(spec(["bell", "once"], ["project", "since"]));
|
|
2667
2760
|
none(positionals);
|
|
2668
2761
|
if (!hasFullAccess(creds))
|
|
2669
2762
|
throw new HarborError(
|
|
2670
2763
|
"Watching needs full access, as the full-screen app does. Run harbor login (without --read-only), then try again.",
|
|
2671
2764
|
);
|
|
2672
|
-
const { watchHere } = await loadApp();
|
|
2765
|
+
const { watchHere, watchSince } = await loadApp();
|
|
2766
|
+
const since = values.since ? watchSince(values.since, new Date()) : null;
|
|
2767
|
+
if (values.since && !since)
|
|
2768
|
+
throw new HarborError(
|
|
2769
|
+
`--since takes a time today (14:30) or a date (2026-10-21, or 2026-10-21T14:30), not "${values.since}".`,
|
|
2770
|
+
);
|
|
2673
2771
|
await watchHere({
|
|
2674
2772
|
source: tokenSource({ creds, save: (c) => writeCredentials(c, env), fetcher }),
|
|
2675
2773
|
fetcher,
|
|
@@ -2677,7 +2775,10 @@ export async function run(argv, io = {}) {
|
|
|
2677
2775
|
saved: creds.harbor ?? null,
|
|
2678
2776
|
project: values.project,
|
|
2679
2777
|
bell: Boolean(values.bell),
|
|
2680
|
-
colour: colourDepth(io.stdout ?? process.stdout, env) !== "none",
|
|
2778
|
+
colour: !asJson && colourDepth(io.stdout ?? process.stdout, env) !== "none",
|
|
2779
|
+
since,
|
|
2780
|
+
once: Boolean(values.once),
|
|
2781
|
+
json: asJson,
|
|
2681
2782
|
});
|
|
2682
2783
|
return 0;
|
|
2683
2784
|
}
|
|
@@ -2716,6 +2817,49 @@ export async function run(argv, io = {}) {
|
|
|
2716
2817
|
}
|
|
2717
2818
|
return call("fog", { harbor, project: values.project, limit: 100 }, (r) => fogText(r));
|
|
2718
2819
|
}
|
|
2820
|
+
case "bridge": {
|
|
2821
|
+
const { positionals, values, harbor } = parse(spec(["list"], ["program"]));
|
|
2822
|
+
none(positionals);
|
|
2823
|
+
return call("review_meeting", { harbor, program: values.program }, bridgeText);
|
|
2824
|
+
}
|
|
2825
|
+
case "thanks": {
|
|
2826
|
+
const { positionals, values, harbor } = parse(
|
|
2827
|
+
spec(["all", "given"], ["to", "milestone", "decision"]),
|
|
2828
|
+
);
|
|
2829
|
+
const forWork = values.milestone || values.decision;
|
|
2830
|
+
if (!forWork && positionals.length === 0) {
|
|
2831
|
+
return call(
|
|
2832
|
+
"list_thanks",
|
|
2833
|
+
{
|
|
2834
|
+
harbor,
|
|
2835
|
+
person: values.all ? "all" : "me",
|
|
2836
|
+
given: Boolean(values.given),
|
|
2837
|
+
},
|
|
2838
|
+
thanksText,
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
const [first, ...rest] = positionals;
|
|
2842
|
+
const task = forWork ? undefined : first;
|
|
2843
|
+
const line = (forWork ? positionals : rest).join(" ").trim();
|
|
2844
|
+
if (task && !TASK_KEY.test(task) && !line) {
|
|
2845
|
+
throw new HarborError(
|
|
2846
|
+
'Say what for and the line: harbor thanks RR-42 "Caught it before launch" (or --milestone NAME, --decision TITLE).',
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
if (!line) throw new HarborError('Add the line of thanks in quotes: harbor thanks RR-42 "Thank you".');
|
|
2850
|
+
return call(
|
|
2851
|
+
"give_thanks",
|
|
2852
|
+
{
|
|
2853
|
+
harbor,
|
|
2854
|
+
task,
|
|
2855
|
+
milestone: values.milestone,
|
|
2856
|
+
decision: values.decision,
|
|
2857
|
+
to: values.to,
|
|
2858
|
+
line,
|
|
2859
|
+
},
|
|
2860
|
+
(r) => `Fair winds: thanked ${r.thanked.to ?? "them"} for ${r.thanked.for}.`,
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2719
2863
|
case "handover": {
|
|
2720
2864
|
const { positionals, values, harbor } = parse(
|
|
2721
2865
|
spec(["confirm"], ["to", "until", "note", "keep"]),
|
package/lib/format.mjs
CHANGED
|
@@ -526,15 +526,66 @@ export function premortemText(r, project = r.project) {
|
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
/** A project's voyage log, as get_voyage_log answers it. */
|
|
529
|
+
/** `harbor thanks`: each thanks on two lines, the words first, no counts. */
|
|
530
|
+
export function thanksText(r) {
|
|
531
|
+
if (r.note) return r.note;
|
|
532
|
+
if (!r.thanks.length) {
|
|
533
|
+
return r.person
|
|
534
|
+
? `No thanks in the last ${r.days} days yet. harbor thanks TASK "LINE" gives one.`
|
|
535
|
+
: `Nobody has thanked anyone in the last ${r.days} days. harbor thanks TASK "LINE" gives one.`;
|
|
536
|
+
}
|
|
537
|
+
return r.thanks
|
|
538
|
+
.map((t) => `“${t.line}”\n ${t.from} to ${t.to}, for ${t.for} · ${t.at.slice(0, 10)}`)
|
|
539
|
+
.join("\n");
|
|
540
|
+
}
|
|
541
|
+
|
|
529
542
|
export function voyageText(r) {
|
|
530
543
|
const head = [`Voyage log · ${r.project}${r.done ? ` · Done ${r.finished}` : " · not Done yet"}`];
|
|
531
544
|
if (r.note) head.push(r.note);
|
|
532
545
|
head.push((r.stats ?? []).join(" · "));
|
|
533
546
|
const story = (r.story ?? []).map((s) => ` - ${s}`);
|
|
534
547
|
const crew = (r.crew ?? []).map((c) => ` - ${c.who ?? "Someone"}: ${c.line}`);
|
|
535
|
-
|
|
536
|
-
"
|
|
548
|
+
const thanks = (r.thanks ?? []).map(
|
|
549
|
+
(t) => ` - ${t.from ?? "Someone"} to ${t.to ?? "someone"}: “${t.line}”`,
|
|
537
550
|
);
|
|
551
|
+
return [
|
|
552
|
+
...head,
|
|
553
|
+
"",
|
|
554
|
+
...story,
|
|
555
|
+
...(thanks.length ? ["", "Fair winds along the way:", ...thanks] : []),
|
|
556
|
+
...(crew.length ? ["", "From the crew:", ...crew] : []),
|
|
557
|
+
].join("\n");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** The review meeting's agenda, as review_meeting answers it: each project that moved, with every item. */
|
|
561
|
+
export function bridgeText(r) {
|
|
562
|
+
const programs = r.programs ?? [];
|
|
563
|
+
if (programs.length === 0) return "No open programs to review.";
|
|
564
|
+
const out = programs.map(
|
|
565
|
+
(p) =>
|
|
566
|
+
`${p.name}: changes since ${p.lastReviewed ? "the last review" : "last week"}, ${p.since.slice(0, 10)}`,
|
|
567
|
+
);
|
|
568
|
+
for (const p of r.projects ?? []) {
|
|
569
|
+
const facts = [
|
|
570
|
+
p.health,
|
|
571
|
+
p.lead,
|
|
572
|
+
p.ends ? `ends ${p.ends}` : null,
|
|
573
|
+
`${p.late} late of ${p.open} open`,
|
|
574
|
+
p.highRisks ? `${p.highRisks} high risk${p.highRisks === 1 ? "" : "s"}` : null,
|
|
575
|
+
p.nextMilestone ? `next milestone ${p.nextMilestone.name}, ${p.nextMilestone.due}` : null,
|
|
576
|
+
].filter(Boolean);
|
|
577
|
+
out.push("", `${p.project} (${p.program})`, ` ${facts.join(" · ")}`);
|
|
578
|
+
for (const c of p.changes) {
|
|
579
|
+
if (c.items) {
|
|
580
|
+
out.push(` - ${c.text.split(":")[0]} (${c.items.length}):`);
|
|
581
|
+
for (const item of c.items) out.push(` ${item}`);
|
|
582
|
+
} else out.push(` - ${c.text}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if ((r.projects ?? []).length === 0) out.push("", "Nothing moved since the last review.");
|
|
586
|
+
const quiet = (r.quiet ?? []).map((q) => q.project);
|
|
587
|
+
if (quiet.length) out.push("", `No change on ${quiet.join(", ")}.`);
|
|
588
|
+
return out.join("\n");
|
|
538
589
|
}
|
|
539
590
|
|
|
540
591
|
/** What's holding something up, as long_pole answers it: the headline, then the line of waits, first blocker at the top. */
|
|
@@ -978,8 +1029,16 @@ export function weekText(w, { today, review = false, width } = {}) {
|
|
|
978
1029
|
out.push("", `Not on a day yet: ${w.unplannedCount}. harbor week KEY DAY puts one there.`);
|
|
979
1030
|
for (const t of w.unplanned.slice(0, 5))
|
|
980
1031
|
out.push(` ${taskLine(t, { today, mine: true, width: width ? width - 2 : undefined })}`);
|
|
981
|
-
if (w.unplannedCount > 5)
|
|
1032
|
+
if (w.unplannedCount > 5)
|
|
1033
|
+
out.push(
|
|
1034
|
+
` … and ${w.unplannedCount - 5} more (--json lists ${w.unplannedCount > w.unplanned.length ? w.unplanned.length : "all"})`,
|
|
1035
|
+
);
|
|
982
1036
|
}
|
|
1037
|
+
if (w.inReviewCount)
|
|
1038
|
+
out.push(
|
|
1039
|
+
"",
|
|
1040
|
+
`${w.inReviewCount} of yours ${w.inReviewCount === 1 ? "is" : "are"} in review, waiting on someone else, so ${w.inReviewCount === 1 ? "it's" : "they're"} left out.`,
|
|
1041
|
+
);
|
|
983
1042
|
}
|
|
984
1043
|
if (reviewing) {
|
|
985
1044
|
const r = w.review;
|