@harborppm/cli 0.19.0 → 0.20.1

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 CHANGED
@@ -118,8 +118,11 @@ harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
118
118
  harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
119
119
  harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
120
120
  harbor voyage PROJECT ["YOUR LINE"]
121
+ harbor limit PROJECT [COLUMN N|none]
121
122
  ```
122
123
 
124
+ `harbor limit RR` prints how many tasks each of a board's columns takes at once; `harbor limit RR doing 8` changes one (1 to 20, or `none` for no limit), if you own the harbor. `harbor projects --json` carries each board's `limits` too.
125
+
123
126
  `harbor premortem` runs a pre-mortem with your crew: `--start` asks everyone "it's after the end date and this project failed: what happened?", each person adds answers alone for five minutes, and then the answers show together on the project's Risks & issues tab, where each becomes a risk in one click. On its own it prints the question and the answers you can see. Once a project is Done, `harbor voyage` prints its voyage log (planned against actual dates, the biggest slips, the risks that came true, who closed the most, and the crew's lines) and adds yours.
124
127
 
125
128
  Every command takes `--json` for the raw result (a refusal then comes out as `{"error": "…"}` on standard output, still with exit code 1), and `harbor COMMAND --help` shows just that command. Statuses are backlog, todo, doing, review and done. A `DATE` is YYYY-MM-DD, `today`, `tomorrow`, or `+N` for N days from today.
package/dist/app.mjs CHANGED
@@ -1264,6 +1264,18 @@ const ALL_FIELDS = {
1264
1264
  };
1265
1265
  const DEFAULT_DOING_WIP = 3;
1266
1266
  const DEFAULT_REVIEW_WIP = 2;
1267
+ /** The column's cap after "No cap" (`uncapped`) or a number is picked. */
1268
+ function withWipLimit(col, limit) {
1269
+ const { uncapped: _drop, ...rest } = col;
1270
+ return limit == null ? {
1271
+ ...rest,
1272
+ wipLimit: null,
1273
+ uncapped: true
1274
+ } : {
1275
+ ...rest,
1276
+ wipLimit: limit
1277
+ };
1278
+ }
1267
1279
  function defaultWipFor(status) {
1268
1280
  if (status === "doing") return 3;
1269
1281
  if (status === "review") return 2;
@@ -1337,6 +1349,15 @@ function clampWip(value) {
1337
1349
  if (!Number.isFinite(n) || n <= 0) return null;
1338
1350
  return Math.min(20, Math.round(n));
1339
1351
  }
1352
+ function capOf(c) {
1353
+ const n = clampWip(c.wipLimit);
1354
+ if (n != null) return { wipLimit: n };
1355
+ if (c.uncapped === true) return {
1356
+ wipLimit: null,
1357
+ uncapped: true
1358
+ };
1359
+ return { wipLimit: defaultWipFor(c.status) };
1360
+ }
1340
1361
  function normalizeBoardConfig(raw) {
1341
1362
  const base = defaultBoardConfig();
1342
1363
  if (!raw) return base;
@@ -1350,7 +1371,7 @@ function normalizeBoardConfig(raw) {
1350
1371
  status: c.status,
1351
1372
  label: typeof c.label === "string" && c.label.trim() ? c.label.trim() : TASK_STATUS_LABEL[c.status],
1352
1373
  visible: c.visible !== false,
1353
- wipLimit: clampWip(c.wipLimit) ?? defaultWipFor(c.status)
1374
+ ...capOf(c)
1354
1375
  });
1355
1376
  }
1356
1377
  for (const status of TASK_COLUMNS) if (!order.includes(status)) order.push(status);
@@ -7532,8 +7553,9 @@ function daysUntil(date, now) {
7532
7553
  function incomplete(task) {
7533
7554
  return task.status !== "done";
7534
7555
  }
7535
- function wipLimitOf(status, stored) {
7556
+ function wipLimitOf(status, stored, uncapped) {
7536
7557
  if (stored != null) return stored;
7558
+ if (uncapped === true) return null;
7537
7559
  if (status === "doing") return 3;
7538
7560
  if (status === "review") return 2;
7539
7561
  return null;
@@ -7542,7 +7564,7 @@ function wipOverflowing(data, projectId) {
7542
7564
  const cols = data.boardConfigs?.[projectId]?.columns;
7543
7565
  if (cols?.length) return cols.some((c) => {
7544
7566
  if (c.visible === false) return false;
7545
- const limit = wipLimitOf(c.status, c.wipLimit);
7567
+ const limit = wipLimitOf(c.status, c.wipLimit, c.uncapped);
7546
7568
  if (limit == null) return false;
7547
7569
  return tasksOf(data.tasks, projectId).filter((t) => t.status === c.status).length > limit;
7548
7570
  });
@@ -8704,10 +8726,7 @@ function hierarchySlice({ get, write }) {
8704
8726
  const columns = config.columns.map((c) => {
8705
8727
  if (c.status !== status) return c;
8706
8728
  const floor = Math.max(c.wipLimit ?? 0, current);
8707
- return {
8708
- ...c,
8709
- wipLimit: Math.min(20, floor + 1)
8710
- };
8729
+ return withWipLimit(c, Math.min(20, floor + 1));
8711
8730
  });
8712
8731
  get().setBoardConfig(projectId, {
8713
8732
  ...config,
package/lib/cli.mjs CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  import {
20
20
  STATUS,
21
21
  harbors,
22
+ limitsText,
22
23
  myWork,
23
24
  premortemText,
24
25
  voyageText,
@@ -59,6 +60,7 @@ export const HELP = `harbor: Harbor from the terminal
59
60
  harbor programs [--done] programs and their projects
60
61
  harbor projects [words] [--program P] [--done]
61
62
  projects with health and open work
63
+ (--json adds each board's limits)
62
64
  harbor mine what's on you, late first
63
65
  harbor tasks [words] [--project P] [--assignee me|none|NAME]
64
66
  [--late] [--status S,S]
@@ -83,6 +85,8 @@ export const HELP = `harbor: Harbor from the terminal
83
85
  [--priority P] [--status S] [--notes TEXT]
84
86
  [--repeat weekly|biweekly|monthly|never]
85
87
  [--waits-on TASK,TASK|none] [--confirm] [--over-limit]
88
+ harbor limit PROJECT [COLUMN N|none] how many tasks a board column
89
+ takes at once; the owner sets it
86
90
  harbor comment TASK "TEXT"
87
91
  harbor checkin "WHAT YOU'RE DOING" [--on-blocker]
88
92
  harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
@@ -213,6 +217,8 @@ const FLAG = {
213
217
  repeat: "--repeat",
214
218
  description: "--notes",
215
219
  body: "the comment",
220
+ column: "the column",
221
+ limit: "the limit",
216
222
  };
217
223
 
218
224
  /** Harbor's refusals are worded for agents; say them as the commands and flags that answer them. */
@@ -283,6 +289,17 @@ export function premortemForTerminal(r) {
283
289
 
284
290
  /** Commands whose forms do different things: one line each, saying which read and which write. */
285
291
  const DETAIL = {
292
+ limit: [
293
+ " harbor limit PROJECT reads its board's column limits",
294
+ " harbor limit PROJECT COLUMN N writes: COLUMN takes at most N tasks",
295
+ " at once (1 to 20; the website offers",
296
+ " 1-5, 8, 10 and 15)",
297
+ " harbor limit PROJECT COLUMN none writes: no limit on COLUMN",
298
+ "",
299
+ " COLUMN is a status: doing or review usually, or backlog, todo, done.",
300
+ " Only the harbor's owner can change a limit. A move past a limit is",
301
+ " refused until you run it again with --over-limit.",
302
+ ],
286
303
  premortem: [
287
304
  " harbor premortem PROJECT reads it: the question, the time left,",
288
305
  " and the answers you may see",
@@ -851,13 +868,17 @@ export async function run(argv, io = {}) {
851
868
  lines.push(
852
869
  "",
853
870
  "Waits on:",
854
- ...t.waitingOn.map((w) => ` ${w.key ?? w.id} ${w.title} (${STATUS[w.status] ?? w.status})`),
871
+ ...t.waitingOn.map(
872
+ (w) => ` ${w.key ?? w.id} ${w.title} (${STATUS[w.status] ?? w.status})`,
873
+ ),
855
874
  );
856
875
  if (t.waitedOnBy.length)
857
876
  lines.push(
858
877
  "",
859
878
  "Holds up:",
860
- ...t.waitedOnBy.map((w) => ` ${w.key ?? w.id} ${w.title} (${STATUS[w.status] ?? w.status})`),
879
+ ...t.waitedOnBy.map(
880
+ (w) => ` ${w.key ?? w.id} ${w.title} (${STATUS[w.status] ?? w.status})`,
881
+ ),
861
882
  );
862
883
  if (t.comments.length)
863
884
  lines.push(
@@ -1111,6 +1132,26 @@ export async function run(argv, io = {}) {
1111
1132
  }
1112
1133
  return call("get_premortem", { harbor, project }, premortemText, premortemForTerminal);
1113
1134
  }
1135
+ case "limit": {
1136
+ const { positionals, harbor } = parse(spec());
1137
+ const [project, column, value, ...extra] = positionals;
1138
+ if (!project) throw new HarborError("Give a project, like harbor limit RR doing 5.");
1139
+ if (!column) {
1140
+ return call("get_project", { harbor, project }, (r) =>
1141
+ limitsText(r.name, r.limits, r.tasks),
1142
+ );
1143
+ }
1144
+ if (value === undefined || extra.length) {
1145
+ throw new HarborError(`Give one limit for ${column}: a number like 5, or none.`);
1146
+ }
1147
+ const limit = /^(none|no|off|0)$/i.test(value) ? null : Number(value);
1148
+ if (limit !== null && !(Number.isInteger(limit) && limit >= 1 && limit <= 20)) {
1149
+ throw new HarborError("A limit is a whole number from 1 to 20, or none.");
1150
+ }
1151
+ return call("set_column_limit", { harbor, project, column, limit }, (r) =>
1152
+ limitsText(r.set.project, r.limits, r.tasks),
1153
+ );
1154
+ }
1114
1155
  case "voyage": {
1115
1156
  const { positionals, harbor } = parse(spec());
1116
1157
  const [project, ...words] = positionals;
package/lib/format.mjs CHANGED
@@ -144,6 +144,18 @@ export function projectList(r, { done = false } = {}) {
144
144
  );
145
145
  }
146
146
 
147
+ /** A project's column limits with how full each is: "doing 4 of 5; no limit on backlog, todo, done". */
148
+ export function limitsText(name, limits, tasks = {}) {
149
+ const entries = Object.entries(limits ?? {});
150
+ const capped = entries
151
+ .filter(([, n]) => n != null)
152
+ .map(([status, n]) => `${STATUS[status] ?? status} ${tasks[status] ?? 0} of ${n}`);
153
+ const open = entries.filter(([, n]) => n == null).map(([status]) => STATUS[status] ?? status);
154
+ if (!capped.length) return `${name}'s board has no column limits.`;
155
+ const rest = open.length ? `; no limit on ${open.join(", ")}` : "";
156
+ return `${name}: ${capped.join(", ")}${rest}.`;
157
+ }
158
+
147
159
  export function harbors(r) {
148
160
  const list = r.harbors ?? [];
149
161
  if (!list.length) return "You aren't in any harbor yet. Run harbor to make one.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
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",