@harborppm/cli 0.42.0 → 0.44.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 +11 -3
- package/dist/app.mjs +2252 -1867
- package/lib/cli.mjs +191 -5
- package/lib/format.mjs +71 -1
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -36,7 +36,9 @@ import {
|
|
|
36
36
|
voyageText,
|
|
37
37
|
handoverText,
|
|
38
38
|
agentWorkText,
|
|
39
|
+
raidDetail,
|
|
39
40
|
raidList,
|
|
41
|
+
reviewList,
|
|
40
42
|
programTree,
|
|
41
43
|
projectList,
|
|
42
44
|
taskLine,
|
|
@@ -130,6 +132,19 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
130
132
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
131
133
|
the register, worst first
|
|
132
134
|
(--closed adds closed ones)
|
|
135
|
+
harbor raid "TITLE" [--status S] [--owner me|none|NAME] [--impact 1-5]
|
|
136
|
+
[--probability 1-5] [--due DATE|none] [--update TEXT]
|
|
137
|
+
[--title T] [--description T] [--mitigation T]
|
|
138
|
+
[--contingency T] [--resolution T] [--rationale T]
|
|
139
|
+
[--notes T]
|
|
140
|
+
one risk, issue or decision in
|
|
141
|
+
full, or change it
|
|
142
|
+
harbor review [--mine|--owner NAME] [--portfolio P] [--done]
|
|
143
|
+
the review: programs worst first,
|
|
144
|
+
each with its next move
|
|
145
|
+
harbor next-move PROGRAM "TEXT" write a program's next move
|
|
146
|
+
harbor health --program P|--project P|--portfolio P STATE [--note WHY]
|
|
147
|
+
set its health by hand, or auto
|
|
133
148
|
harbor brief [--portfolio P] [--all] [--say]
|
|
134
149
|
the weekly brief, ready to send
|
|
135
150
|
(--all lists every late and done
|
|
@@ -320,6 +335,11 @@ const FLAG = {
|
|
|
320
335
|
query: "the words",
|
|
321
336
|
task: "the task",
|
|
322
337
|
note: "the check-in",
|
|
338
|
+
health: "the health",
|
|
339
|
+
decidedBy: "--decided-by",
|
|
340
|
+
decidedOn: "--decided-on",
|
|
341
|
+
item: "the title",
|
|
342
|
+
text: "the next move",
|
|
323
343
|
waitsOn: "--waits-on",
|
|
324
344
|
repeat: "--repeat",
|
|
325
345
|
description: "--notes",
|
|
@@ -506,12 +526,13 @@ const DETAIL = {
|
|
|
506
526
|
" harbor forecast PROJECT reads one project's forecast and what",
|
|
507
527
|
" it rests on",
|
|
508
528
|
"",
|
|
509
|
-
" Worked out from the crew's pace
|
|
510
|
-
"
|
|
529
|
+
" Worked out from the crew's pace: how many tasks they finished each week",
|
|
530
|
+
" over the last six (or the project's life, if shorter), replayed a",
|
|
511
531
|
" thousand times until its open tasks run out. Probably is half of those",
|
|
512
532
|
" runs, very likely 85%, almost certainly 95%. No estimates or points.",
|
|
513
|
-
"
|
|
514
|
-
"
|
|
533
|
+
" No run finishes before the last open task is due, so a task due later",
|
|
534
|
+
" than the pace would finish sets the dates, and the forecast names it. It",
|
|
535
|
+
" warns when under two weeks of pace is all there is.",
|
|
515
536
|
],
|
|
516
537
|
report: [
|
|
517
538
|
" harbor report PROGRAM reads the program's status report",
|
|
@@ -608,6 +629,57 @@ const DETAIL = {
|
|
|
608
629
|
" PROJECT is a name, a key like RR, or an id. When two projects share a",
|
|
609
630
|
" name, give the key.",
|
|
610
631
|
],
|
|
632
|
+
raid: [
|
|
633
|
+
' harbor raid "TITLE" reads one risk, issue or decision in',
|
|
634
|
+
" full: rating, owner, dates, what's",
|
|
635
|
+
" written about it and its updates",
|
|
636
|
+
' harbor raid "TITLE" --status S writes: moves it on. A risk is open,',
|
|
637
|
+
" watching, mitigated or closed; an",
|
|
638
|
+
" issue open, resolved or closed; a",
|
|
639
|
+
" decision proposed, decided or",
|
|
640
|
+
" superseded (decided names you and",
|
|
641
|
+
" today unless --decided-by or",
|
|
642
|
+
" --decided-on say otherwise)",
|
|
643
|
+
' harbor raid "TITLE" --update "TEXT" writes: adds a line to its updates',
|
|
644
|
+
"",
|
|
645
|
+
" Also --owner me|none|NAME, --impact 1-5, --probability 1-5 (risks),",
|
|
646
|
+
" --due DATE|none (review by, or decide by), --title, --description,",
|
|
647
|
+
" --mitigation and --contingency (risks), --resolution (issues and",
|
|
648
|
+
" decisions), --rationale (decisions) and --notes. TITLE is its title or",
|
|
649
|
+
" enough of it to pick one out; harbor risks lists them. Changes several",
|
|
650
|
+
" at once, as one change.",
|
|
651
|
+
],
|
|
652
|
+
review: [
|
|
653
|
+
" harbor review reads the review: every program,",
|
|
654
|
+
" worst health first, then soonest end,",
|
|
655
|
+
" with its lead, late work, priority and",
|
|
656
|
+
" next move (done ones with --done)",
|
|
657
|
+
" harbor review --mine only programs you lead, lead a",
|
|
658
|
+
" project in or hold a task in",
|
|
659
|
+
" harbor review --owner NAME the same for someone else",
|
|
660
|
+
" harbor review --portfolio P one portfolio's",
|
|
661
|
+
" harbor review TASK writes: moves a task to In review",
|
|
662
|
+
"",
|
|
663
|
+
' Write a next move with harbor next-move PROGRAM "TEXT"; set a health by',
|
|
664
|
+
" hand with harbor health. harbor export review prints it as CSV.",
|
|
665
|
+
],
|
|
666
|
+
"next-move": [
|
|
667
|
+
' harbor next-move PROGRAM "TEXT" writes: the line the review and the',
|
|
668
|
+
" brief show for what happens next",
|
|
669
|
+
' harbor next-move PROGRAM "" writes: clears it',
|
|
670
|
+
],
|
|
671
|
+
health: [
|
|
672
|
+
" harbor health --program P STATE --note WHY",
|
|
673
|
+
" writes: sets a program's health by hand",
|
|
674
|
+
" harbor health --project P STATE --note WHY",
|
|
675
|
+
" harbor health --portfolio P STATE --note WHY",
|
|
676
|
+
" harbor health --program P auto writes: health follows late work again",
|
|
677
|
+
"",
|
|
678
|
+
" STATE is on-track, at-risk, off-track, paused or done. Health is worked",
|
|
679
|
+
" out from late work; setting it by hand needs a note saying why (done",
|
|
680
|
+
" doesn't), and the note shows beside it everywhere. Paused or done on a",
|
|
681
|
+
" program carries to its projects.",
|
|
682
|
+
],
|
|
611
683
|
risk: [
|
|
612
684
|
' harbor risk "TITLE" --project P writes: logs a risk on the project',
|
|
613
685
|
' harbor risk "TITLE" --program P writes: logs one on the program',
|
|
@@ -1511,7 +1583,29 @@ export async function run(argv, io = {}) {
|
|
|
1511
1583
|
case "start":
|
|
1512
1584
|
case "review":
|
|
1513
1585
|
case "done": {
|
|
1514
|
-
const { positionals, values, harbor } = parse(
|
|
1586
|
+
const { positionals, values, harbor } = parse(
|
|
1587
|
+
command === "review"
|
|
1588
|
+
? spec(["confirm", "over-limit", "mine", "done"], ["owner", "portfolio"])
|
|
1589
|
+
: spec(["confirm", "over-limit"]),
|
|
1590
|
+
);
|
|
1591
|
+
if (command === "review" && positionals.length === 0) {
|
|
1592
|
+
if (values.mine && values.owner) throw new HarborError("Say --mine or --owner, not both.");
|
|
1593
|
+
return call(
|
|
1594
|
+
"program_review",
|
|
1595
|
+
{
|
|
1596
|
+
harbor,
|
|
1597
|
+
owner: values.mine ? "me" : values.owner,
|
|
1598
|
+
portfolio: values.portfolio,
|
|
1599
|
+
includeDone: Boolean(values.done),
|
|
1600
|
+
},
|
|
1601
|
+
reviewList,
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
if (values.mine || values.done || values.owner || values.portfolio) {
|
|
1605
|
+
throw new HarborError(
|
|
1606
|
+
"--mine, --owner, --portfolio and --done are for the review: harbor review on its own.",
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1515
1609
|
const status = { start: "doing", review: "review", done: "done" }[command];
|
|
1516
1610
|
return call(
|
|
1517
1611
|
"update_task",
|
|
@@ -1865,6 +1959,98 @@ export async function run(argv, io = {}) {
|
|
|
1865
1959
|
: `${r.handedBack.person} is back. Nothing was still waiting to go back.`,
|
|
1866
1960
|
);
|
|
1867
1961
|
}
|
|
1962
|
+
case "raid": {
|
|
1963
|
+
const fields = [
|
|
1964
|
+
"title",
|
|
1965
|
+
"description",
|
|
1966
|
+
"mitigation",
|
|
1967
|
+
"contingency",
|
|
1968
|
+
"resolution",
|
|
1969
|
+
"rationale",
|
|
1970
|
+
"notes",
|
|
1971
|
+
];
|
|
1972
|
+
const { positionals, values, harbor } = parse(
|
|
1973
|
+
spec(
|
|
1974
|
+
[],
|
|
1975
|
+
[
|
|
1976
|
+
"status",
|
|
1977
|
+
"owner",
|
|
1978
|
+
"impact",
|
|
1979
|
+
"probability",
|
|
1980
|
+
"due",
|
|
1981
|
+
"update",
|
|
1982
|
+
"decided-by",
|
|
1983
|
+
"decided-on",
|
|
1984
|
+
...fields,
|
|
1985
|
+
],
|
|
1986
|
+
),
|
|
1987
|
+
);
|
|
1988
|
+
const item = text(positionals, "which one: its title, as harbor risks lists it");
|
|
1989
|
+
const scale = (v, name) => (v === "none" ? null : level(v, name));
|
|
1990
|
+
const args = {
|
|
1991
|
+
harbor,
|
|
1992
|
+
item,
|
|
1993
|
+
status: values.status?.trim().toLowerCase(),
|
|
1994
|
+
owner: values.owner,
|
|
1995
|
+
impact: scale(values.impact, "impact"),
|
|
1996
|
+
probability: scale(values.probability, "probability"),
|
|
1997
|
+
due: values.due === undefined ? undefined : nullable(dueDate(values.due)),
|
|
1998
|
+
decidedBy: values["decided-by"],
|
|
1999
|
+
decidedOn:
|
|
2000
|
+
values["decided-on"] === undefined ? undefined : nullable(dueDate(values["decided-on"])),
|
|
2001
|
+
update: values.update,
|
|
2002
|
+
...Object.fromEntries(fields.map((f) => [f, values[f]])),
|
|
2003
|
+
};
|
|
2004
|
+
const changes = Object.entries(args).filter(
|
|
2005
|
+
([k, v]) => !["harbor", "item"].includes(k) && v !== undefined,
|
|
2006
|
+
);
|
|
2007
|
+
if (!changes.length) {
|
|
2008
|
+
return call("get_raid", { harbor, item }, (r) => raidDetail(r.item));
|
|
2009
|
+
}
|
|
2010
|
+
return call("update_raid", args, (r) => `Updated.\n\n${raidDetail(r.updated)}`);
|
|
2011
|
+
}
|
|
2012
|
+
case "next-move": {
|
|
2013
|
+
const { positionals, harbor } = parse(spec([]));
|
|
2014
|
+
if (positionals.length < 2) {
|
|
2015
|
+
throw new HarborError(
|
|
2016
|
+
'Give the program and its next move: harbor next-move "PROGRAM" "TEXT". harbor review shows them.',
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
const [program, ...words] = positionals;
|
|
2020
|
+
return call("set_next_move", { harbor, program, text: words.join(" ") }, (r) =>
|
|
2021
|
+
r.nextMove
|
|
2022
|
+
? `Next move for ${r.program}: ${r.nextMove}`
|
|
2023
|
+
: `Cleared the next move for ${r.program}.`,
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
case "health": {
|
|
2027
|
+
const { positionals, values, harbor } = parse(
|
|
2028
|
+
spec([], ["program", "project", "portfolio", "note"]),
|
|
2029
|
+
);
|
|
2030
|
+
const state = one(positionals, "health: on-track, at-risk, off-track, paused, done or auto")
|
|
2031
|
+
.trim()
|
|
2032
|
+
.toLowerCase()
|
|
2033
|
+
.replace(/[\s_]+/g, "-");
|
|
2034
|
+
const word = { "on-track": "on track", "at-risk": "at risk", "off-track": "off track" };
|
|
2035
|
+
return call(
|
|
2036
|
+
"set_health",
|
|
2037
|
+
{
|
|
2038
|
+
harbor,
|
|
2039
|
+
program: values.program,
|
|
2040
|
+
project: values.project,
|
|
2041
|
+
portfolio: values.portfolio,
|
|
2042
|
+
health: state,
|
|
2043
|
+
note: values.note,
|
|
2044
|
+
},
|
|
2045
|
+
(r) => {
|
|
2046
|
+
const name = r.program ?? r.project ?? r.portfolio;
|
|
2047
|
+
const h = word[r.health] ?? r.health;
|
|
2048
|
+
return r.setByHand
|
|
2049
|
+
? `${name} is ${h}, set by hand${r.note ? `: ${r.note}` : ""}.`
|
|
2050
|
+
: `${name}'s health follows its work again: ${h}.`;
|
|
2051
|
+
},
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
1868
2054
|
case "risk":
|
|
1869
2055
|
case "issue":
|
|
1870
2056
|
case "decision": {
|
package/lib/format.mjs
CHANGED
|
@@ -270,6 +270,76 @@ export function raidList(r, words = "") {
|
|
|
270
270
|
return lines.join("\n") + more;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* One item of the register in full, as get_raid and update_raid give it:
|
|
275
|
+
* what it is and where, its rating, every written field, then its updates.
|
|
276
|
+
*/
|
|
277
|
+
export function raidDetail(x) {
|
|
278
|
+
const out = [`${x.kind}: ${x.title}`];
|
|
279
|
+
const facts = [
|
|
280
|
+
x.status,
|
|
281
|
+
x.probability != null ? `likelihood ${x.probability}` : null,
|
|
282
|
+
x.impact != null ? `impact ${x.impact}` : null,
|
|
283
|
+
x.score != null ? `score ${x.score}` : null,
|
|
284
|
+
x.owner ? `owner ${x.owner}` : "no owner yet",
|
|
285
|
+
x.project ?? x.program ?? x.portfolio ?? null,
|
|
286
|
+
x.due ? `${(x.dueMeans ?? "due").toLowerCase()} ${x.due}` : null,
|
|
287
|
+
x.decidedBy ? `decided by ${x.decidedBy}${x.decidedOn ? ` on ${x.decidedOn}` : ""}` : null,
|
|
288
|
+
].filter(Boolean);
|
|
289
|
+
out.push(facts.join(" · "));
|
|
290
|
+
const words = {
|
|
291
|
+
risk: {
|
|
292
|
+
description: "What could happen",
|
|
293
|
+
mitigation: "How we reduce it",
|
|
294
|
+
contingency: "If it happens",
|
|
295
|
+
},
|
|
296
|
+
issue: { description: "What's going on", resolution: "How it gets resolved" },
|
|
297
|
+
decision: { description: "Context", resolution: "The decision", rationale: "Why" },
|
|
298
|
+
}[x.kind?.toLowerCase()] ?? { description: "Description" };
|
|
299
|
+
for (const [key, label] of Object.entries({ ...words, notes: "Notes" })) {
|
|
300
|
+
if (x[key])
|
|
301
|
+
out.push(
|
|
302
|
+
"",
|
|
303
|
+
`${label}:`,
|
|
304
|
+
...String(x[key])
|
|
305
|
+
.split("\n")
|
|
306
|
+
.map((l) => ` ${l}`),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (x.updates?.length) {
|
|
310
|
+
out.push("", "Updates:");
|
|
311
|
+
for (const u of x.updates) out.push(` ${u.on ?? ""} ${u.by}: ${u.body.replace(/\s+/g, " ")}`);
|
|
312
|
+
}
|
|
313
|
+
return out.join("\n");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The review, as program_review gives it: programs worst first, each with
|
|
318
|
+
* its lead, end, late work and priority, and the next move under it.
|
|
319
|
+
*/
|
|
320
|
+
export function reviewList(r) {
|
|
321
|
+
const programs = r.programs ?? [];
|
|
322
|
+
const more = hiddenNote(r.done ?? 0, "program");
|
|
323
|
+
if (!programs.length) {
|
|
324
|
+
return r.done ? `Every program here is done.${more.replace("\n", " ")}` : "No programs here.";
|
|
325
|
+
}
|
|
326
|
+
const out = [];
|
|
327
|
+
for (const p of programs) {
|
|
328
|
+
const said = health(p.health) + (p.setByHand ? ` by hand${p.note ? `: ${p.note}` : ""}` : "");
|
|
329
|
+
const bits = [
|
|
330
|
+
said,
|
|
331
|
+
p.lead,
|
|
332
|
+
p.ends ? `ends ${p.ends}` : null,
|
|
333
|
+
p.late ? `${p.late} late` : null,
|
|
334
|
+
p.priority ?? null,
|
|
335
|
+
p.portfolio ?? null,
|
|
336
|
+
].filter(Boolean);
|
|
337
|
+
out.push(`${p.name} (${bits.join(" · ")})`);
|
|
338
|
+
out.push(` ${p.nextMove ? `next: ${p.nextMove}` : "no next move yet"}`);
|
|
339
|
+
}
|
|
340
|
+
return out.join("\n") + more;
|
|
341
|
+
}
|
|
342
|
+
|
|
273
343
|
/** "over 95%", in steps of five, as the website says it (src/lib/tide.ts chanceText). */
|
|
274
344
|
function chance(c) {
|
|
275
345
|
if (c < 0.05) return "under 5%";
|
|
@@ -346,7 +416,7 @@ export function forecastOne(p) {
|
|
|
346
416
|
if (f.lastDue) {
|
|
347
417
|
const which = f.lastDue.key ? `${f.lastDue.key} “${f.lastDue.title}”` : `“${f.lastDue.title}”`;
|
|
348
418
|
lines.push(
|
|
349
|
-
`${which} is due ${f.lastDue.due},
|
|
419
|
+
`${which} is due ${f.lastDue.due}, later than the crew's pace alone would finish, so the dates wait for it.`,
|
|
350
420
|
);
|
|
351
421
|
}
|
|
352
422
|
return lines.join("\n");
|