@harborppm/cli 0.51.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/lib/cli.mjs CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  soundingText,
38
38
  soundingsText,
39
39
  voyageText,
40
+ whyText,
40
41
  handoverText,
41
42
  fogText,
42
43
  agentWorkText,
@@ -89,6 +90,8 @@ export const HELP = `harbor: Harbor from the terminal
89
90
  dry dock: the forecast with plan
90
91
  changes tried on a copy; nothing
91
92
  changes without --apply
93
+ harbor why PROJECT|TASK|MILESTONE what's holding it up: the line of
94
+ task waits that sets its date
92
95
  harbor hill [--program P] where each project is on its hill
93
96
  harbor hill PROJECT --at 0-100|top|done move it: up to 50 is figuring it
94
97
  out, past 50 is getting it done
@@ -158,6 +161,10 @@ export const HELP = `harbor: Harbor from the terminal
158
161
  the review: programs worst first,
159
162
  each with its next move (reads
160
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
161
168
  harbor next-move PROGRAM "TEXT" write a program's next move
162
169
  harbor health --program P|--project P|--portfolio P STATE [--note WHY]
163
170
  set its health by hand, or auto
@@ -721,6 +728,20 @@ const DETAIL = {
721
728
  " never marks its own work Done; a person does, on the website or in the",
722
729
  " terminal app.",
723
730
  ],
731
+ why: [
732
+ " harbor why PROJECT the line of task waits that sets a",
733
+ " project's date, against its end",
734
+ " harbor why TASK the waits behind one task (a key",
735
+ " like RR-42), against its due date",
736
+ " harbor why MILESTONE the project a milestone waits on",
737
+ " that finishes last, and its line",
738
+ "",
739
+ " Tasks have due dates, not lengths, so a task can finish on its due date",
740
+ " at the soonest, never before what it waits on, and never in the past.",
741
+ " Each link's slack is the days it can slip before it moves the next one",
742
+ " (or the end); a negative one already does. PROJECT is a name, a key",
743
+ " like RR, or an id.",
744
+ ],
724
745
  voyage: [
725
746
  " harbor voyage PROJECT reads a Done project's voyage log",
726
747
  ' harbor voyage PROJECT "YOUR LINE" writes: adds or changes your line in it',
@@ -750,6 +771,20 @@ const DETAIL = {
750
771
  " enough of it to pick one out; harbor risks lists them. Changes several",
751
772
  " at once, as one change.",
752
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
+ ],
753
788
  review: [
754
789
  " harbor review reads the review: every program,",
755
790
  " worst health first, then soonest end,",
@@ -973,7 +1008,7 @@ export async function run(argv, io = {}) {
973
1008
  }
974
1009
 
975
1010
  // On its own in a terminal: the full-screen app. Piped or scripted: the help.
976
- if (!command || command === "app") {
1011
+ if (!command || command === "app" || command === "bridge") {
977
1012
  const terminal = io.terminal ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
978
1013
  if (!command && !terminal) {
979
1014
  out(HELP);
@@ -982,7 +1017,7 @@ export async function run(argv, io = {}) {
982
1017
  if (!terminal) throw new HarborError("The full-screen app needs a terminal.");
983
1018
  const { values } = parseArgs(rest, { flags: [], options: ["harbor", "server"] });
984
1019
  // In a folder with a local plan (harbor init --local), the app opens on it.
985
- if (!values.harbor && !values.server) {
1020
+ if (!values.harbor && !values.server && command !== "bridge") {
986
1021
  const bundled = await loadApp();
987
1022
  const dir = await bundled.localPlanAt(io.cwd ?? process.cwd());
988
1023
  if (dir) {
@@ -1039,6 +1074,7 @@ export async function run(argv, io = {}) {
1039
1074
  },
1040
1075
  theme: (await readSettings(env)).theme,
1041
1076
  browse: io.browse ?? openBrowser,
1077
+ ...(command === "bridge" ? { startIn: "bridge" } : {}),
1042
1078
  });
1043
1079
  return 0;
1044
1080
  }
@@ -2173,6 +2209,12 @@ export async function run(argv, io = {}) {
2173
2209
  soundingsText(r),
2174
2210
  );
2175
2211
  }
2212
+ case "why": {
2213
+ const { positionals, harbor } = parse(spec());
2214
+ const of = positionals.join(" ").trim();
2215
+ if (!of) throw new HarborError("Say what: a project, a task key like RR-42, or a milestone.");
2216
+ return call("long_pole", { harbor, of }, whyText);
2217
+ }
2176
2218
  case "voyage": {
2177
2219
  const { positionals, harbor } = parse(spec());
2178
2220
  const [project, ...words] = positionals;
package/lib/format.mjs CHANGED
@@ -487,6 +487,35 @@ export function voyageText(r) {
487
487
  );
488
488
  }
489
489
 
490
+ /** What's holding something up, as long_pole answers it: the headline, then the line of waits, first blocker at the top. */
491
+ export function whyText(r) {
492
+ const what = r.of.kind === "task" ? r.of.name : `${r.of.name}${r.of.kind === "milestone" ? " (milestone)" : ""}`;
493
+ if (r.none) return `What's holding up ${what}: ${r.none.replace(/^\w/, (c) => c.toLowerCase())}`;
494
+ const head = [`What's holding up ${what}`];
495
+ if (r.headline) head.push(r.headline);
496
+ const chain = r.chain ?? [];
497
+ const width = Math.min(12, Math.max(0, ...chain.map((l) => (l.key ?? "").length)));
498
+ const lines = chain.flatMap((l, i) => {
499
+ const due = l.due ? (l.late ? `was due ${l.due}` : `due ${l.due}`) : "no due date";
500
+ const finish = l.canFinish !== l.due ? ` · can finish ${l.canFinish}` : "";
501
+ const row = [
502
+ ` ${(l.key ?? "").padEnd(width)}${width ? " " : ""}${l.title}`,
503
+ ` ${" ".repeat(width)}${width ? " " : ""}${l.assignee ?? "nobody yet"} · ${due}${finish} · ${l.slackText}`,
504
+ ];
505
+ return i < chain.length - 1 ? [...row, ` ${" ".repeat(width ? Math.floor(width / 2) : 0)}│`] : row;
506
+ });
507
+ const via = r.via ? [`It waits longest on ${r.via.project}; inside it:`] : [];
508
+ const others = (r.others ?? []).length
509
+ ? [
510
+ "",
511
+ `Also waits on ${r.others
512
+ .map((o) => `${o.project} (finishes ${o.finish}, ${o.slack >= 0 ? `${o.slack} day${o.slack === 1 ? "" : "s"} spare` : `${-o.slack} day${o.slack === -1 ? "" : "s"} over`})`)
513
+ .join(", ")}.`,
514
+ ]
515
+ : [];
516
+ return [...head, "", ...via, ...lines, ...others].join("\n");
517
+ }
518
+
490
519
  /** A position on a 21-wide track, the top of the hill marked in the middle. */
491
520
  function hillTrack(at) {
492
521
  const cells = Array.from({ length: 21 }, (_, i) => (i === 10 ? "┼" : "─"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",