@harborppm/cli 0.86.0 → 0.87.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.
Files changed (3) hide show
  1. package/dist/app.mjs +138 -23
  2. package/lib/cli.mjs +30 -4
  3. package/package.json +1 -1
package/dist/app.mjs CHANGED
@@ -1898,7 +1898,20 @@ function boardPositions(tasks) {
1898
1898
  return out;
1899
1899
  });
1900
1900
  }
1901
- var ids$1, projectTasks$1, EMPTY, waiting, positions;
1901
+ /** For each task id, the tasks that name it as their parent, in list order (src/lib/subtasks.ts reads it). */
1902
+ function childrenByParent(tasks) {
1903
+ return cached$1(children, tasks, () => {
1904
+ const out = /* @__PURE__ */ new Map();
1905
+ for (const t of tasks) {
1906
+ if (!t.parentTaskId) continue;
1907
+ const list = out.get(t.parentTaskId);
1908
+ if (list) list.push(t);
1909
+ else out.set(t.parentTaskId, [t]);
1910
+ }
1911
+ return out;
1912
+ });
1913
+ }
1914
+ var ids$1, projectTasks$1, EMPTY, waiting, positions, children;
1902
1915
  var init_indexes = __esmMin((() => {
1903
1916
  init_blockers();
1904
1917
  ids$1 = /* @__PURE__ */ new WeakMap();
@@ -1906,6 +1919,7 @@ var init_indexes = __esmMin((() => {
1906
1919
  EMPTY = Object.freeze([]);
1907
1920
  waiting = /* @__PURE__ */ new WeakMap();
1908
1921
  positions = /* @__PURE__ */ new WeakMap();
1922
+ children = /* @__PURE__ */ new WeakMap();
1909
1923
  }));
1910
1924
 
1911
1925
  // src/lib/premortem.ts
@@ -4765,6 +4779,54 @@ var init_task_people = __esmMin((() => {
4765
4779
  init_agents();
4766
4780
  }));
4767
4781
 
4782
+ // src/lib/subtasks.ts
4783
+ /** The parent `task` is a piece of, when that link holds; otherwise null. */
4784
+ function parentOf(task, tasks) {
4785
+ const id = task.parentTaskId;
4786
+ if (!id || id === task.id) return null;
4787
+ const parent = byId(tasks).get(id);
4788
+ if (!parent || parent.projectId !== task.projectId || parent.parentTaskId) return null;
4789
+ return parent;
4790
+ }
4791
+ /** Whether `task` shows inside its parent rather than on its own. */
4792
+ function isSubtask(task, tasks) {
4793
+ return parentOf(task, tasks) !== null;
4794
+ }
4795
+ /** The subtasks of `parent`, in list order. Empty for a task that is itself a subtask. */
4796
+ function subtasksOf(parent, tasks) {
4797
+ if (isSubtask(parent, tasks)) return NONE$2;
4798
+ const list = childrenByParent(tasks).get(parent.id);
4799
+ if (!list) return NONE$2;
4800
+ return list.filter((t) => t.projectId === parent.projectId && t.id !== parent.id);
4801
+ }
4802
+ /** The tasks that stand on their own: what a board or a list shows. */
4803
+ function topLevel(list, tasks) {
4804
+ return list.filter((t) => !isSubtask(t, tasks));
4805
+ }
4806
+ /**
4807
+ * Why `childId` can't become a subtask of `parentId`, or null when it can.
4808
+ * The parent must be another task in the same project that isn't a subtask,
4809
+ * and the child mustn't have subtasks of its own (one level only).
4810
+ */
4811
+ function subtaskProblem(childId, parentId, tasks) {
4812
+ const index = byId(tasks);
4813
+ const child = index.get(childId);
4814
+ const parent = index.get(parentId);
4815
+ if (!parent) return "That task isn't here any more.";
4816
+ if (childId === parentId) return "A task can't be a subtask of itself.";
4817
+ if (child && child.projectId !== parent.projectId) return "A subtask has to be in the same project as its parent.";
4818
+ if (isSubtask(parent, tasks)) return "That task is already a subtask; subtasks go one level deep.";
4819
+ if (child && subtasksOf(child, tasks).length > 0) return "This task has subtasks of its own, so it can't go inside another.";
4820
+ if (subtasksOf(parent, tasks).filter((t) => t.id !== childId).length >= 50) return `A task can hold up to ${50} subtasks.`;
4821
+ return null;
4822
+ }
4823
+ var SUBTASKS_MAX, NONE$2;
4824
+ var init_subtasks = __esmMin((() => {
4825
+ init_indexes();
4826
+ SUBTASKS_MAX = 50;
4827
+ NONE$2 = Object.freeze([]);
4828
+ }));
4829
+
4768
4830
  // src/lib/board.ts
4769
4831
  /** The column's cap after "No cap" (`uncapped`) or a number is picked. */
4770
4832
  function withWipLimit(col, limit) {
@@ -4943,11 +5005,12 @@ function wipFullMessage(overflow) {
4943
5005
  function columnWip(data, projectId, status, extra = 1) {
4944
5006
  const col = boardConfigOf(data, projectId).columns.find((c) => c.status === status);
4945
5007
  if (!col) return null;
4946
- const current = tasksOf(data.tasks, projectId).filter((t) => t.status === status).length;
5008
+ const current = tasksOf(data.tasks, projectId).filter((t) => t.status === status && !isSubtask(t, data.tasks)).length;
4947
5009
  return wipOverflow(col, current, extra);
4948
5010
  }
4949
5011
  var BOARD_PRESETS, ALL_FIELDS, DEFAULT_DOING_WIP, DEFAULT_REVIEW_WIP, WIP_CAP_CHOICES;
4950
5012
  var init_board = __esmMin((() => {
5013
+ init_subtasks();
4951
5014
  init_indexes();
4952
5015
  init_swatches();
4953
5016
  BOARD_PRESETS = [
@@ -5979,6 +6042,7 @@ init_date_fns();
5979
6042
  init_date_text();
5980
6043
  init_first_voyage();
5981
6044
  init_indexes();
6045
+ init_subtasks();
5982
6046
  init_tide();
5983
6047
  init_format();
5984
6048
  init_raid();
@@ -6049,7 +6113,7 @@ function myRaid(data, personId) {
6049
6113
  /** A project's board: its visible columns, in order, with their tasks. */
6050
6114
  function boardOf(data, projectId) {
6051
6115
  const config = boardConfigOf(data, projectId);
6052
- const tasks = tasksOf(data.tasks, projectId);
6116
+ const tasks = topLevel(tasksOf(data.tasks, projectId), data.tasks);
6053
6117
  return visibleColumns(config).map((col) => ({
6054
6118
  status: col.status,
6055
6119
  label: columnLabel(col),
@@ -6065,7 +6129,7 @@ function neighbourStatus(data, projectId, status, step) {
6065
6129
  return cols[at + step] ?? null;
6066
6130
  }
6067
6131
  function projectTasks(data, projectId) {
6068
- return [...tasksOf(data.tasks, projectId)].sort((a, b) => STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status) || (a.number ?? 0) - (b.number ?? 0));
6132
+ return topLevel(tasksOf(data.tasks, projectId), data.tasks).sort((a, b) => STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status) || (a.number ?? 0) - (b.number ?? 0));
6069
6133
  }
6070
6134
  /**
6071
6135
  * The project a new task goes in when none was picked: the only open one,
@@ -19822,11 +19886,7 @@ function fieldValueText(def, value, nameOf) {
19822
19886
  }
19823
19887
 
19824
19888
  // src/lib/exports.ts
19825
- /**
19826
- * The spreadsheets Harbor hands out: a project's tasks, the risk register and
19827
- * the review. The website downloads them, agents and the terminal get the
19828
- * same text (the export_csv tool, `harbor export`).
19829
- */
19889
+ init_subtasks();
19830
19890
  init_derived_health();
19831
19891
  init_priority();
19832
19892
  init_raid();
@@ -19861,6 +19921,7 @@ function tasksCsv(data, projectId) {
19861
19921
  "Done",
19862
19922
  "Blocked by",
19863
19923
  "Description",
19924
+ "Subtask of",
19864
19925
  ...fields.map((f) => f.name)
19865
19926
  ], tasks.map((t) => [
19866
19927
  taskKey(t, project) ?? "",
@@ -19872,6 +19933,7 @@ function tasksCsv(data, projectId) {
19872
19933
  t.status === "done" ? "yes" : "no",
19873
19934
  blockerIds(t).map((id) => here.get(id)).filter((b) => Boolean(b)).map(name).join("; "),
19874
19935
  t.description,
19936
+ ((p) => p ? name(p) : "")(parentOf(t, data.tasks)),
19875
19937
  ...fields.map((f) => f.type === "url" ? safeLink(t.customFieldValues?.[f.id]) ?? "" : fieldValueText(f, t.customFieldValues?.[f.id], nameOf))
19876
19938
  ]));
19877
19939
  }
@@ -20482,16 +20544,7 @@ function approvalsWaitingOn(tasks, personId) {
20482
20544
  }
20483
20545
 
20484
20546
  // src/lib/mcp/tools.ts
20485
- /**
20486
- * What an agent can read about a harbor, as MCP tools. Each tool is a pure
20487
- * function of one harbor's document (health already stamped) and who is
20488
- * asking, so it is tested against the demo without a server. The route in
20489
- * src/routes/api/mcp.ts signs the caller in, picks the harbor and loads it.
20490
- *
20491
- * Read only: nothing here changes a workspace. Answers carry ids so an agent
20492
- * can ask about one thing next, and every list is capped and pageable, since
20493
- * a harbor can hold thousands of tasks.
20494
- */
20547
+ init_subtasks();
20495
20548
  init_thanks();
20496
20549
  init_task_keys();
20497
20550
  init_brief();
@@ -21267,7 +21320,7 @@ const HARBOR_TOOLS = [
21267
21320
  tool({
21268
21321
  name: "get_task",
21269
21322
  title: "Task details",
21270
- description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), time logged on it (timeLogged: the total and the latest entries, when there is any), its values for the project's own fields by name (fields, when it has any; set them with set_field), and its url on the website." + DATA_NOTE,
21323
+ description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), time logged on it (timeLogged: the total and the latest entries, when there is any), its values for the project's own fields by name (fields, when it has any; set them with set_field), its subtasks or the task it's a subtask of (subtaskOf), and its url on the website." + DATA_NOTE,
21271
21324
  input: object({
21272
21325
  harbor: harborArg,
21273
21326
  task: string().min(1).max(100).describe("Task id or key (\"RR-42\"), from search_tasks or my_work.")
@@ -21319,6 +21372,12 @@ const HARBOR_TOOLS = [
21319
21372
  ...t.time?.length ? { timeLogged: timeOut(t, people) } : {},
21320
21373
  waitingOn: blockersOf(t, data.tasks).map(brief),
21321
21374
  waitedOnBy: waiters.map(brief),
21375
+ ...((parent) => parent ? { subtaskOf: brief(parent) } : {})(parentOf(t, data.tasks)),
21376
+ ...((subs) => subs.length ? { subtasks: subs.map((x) => ({
21377
+ ...brief(x),
21378
+ assignee: nameOf$2(people, x.assigneeId),
21379
+ due: dayOf(x.dueDate)
21380
+ })) } : {})(subtasksOf(t, data.tasks)),
21322
21381
  ...t.sounding ? { sounding: soundingOut(data, t, me(ctx)?.id ?? null, ctx.now) } : {},
21323
21382
  ...t.approval ? { approval: approvalOut(data, t.approval) } : {},
21324
21383
  ...((v) => v ? { fields: v } : {})(fieldValuesOut(data, project, t)),
@@ -23888,7 +23947,9 @@ function tasksSlice({ set, get, write }) {
23888
23947
  get().log(`added ${who?.name ?? "someone"} as a second pair of hands on ${taskIds.length + raidIds.length} item${taskIds.length + raidIds.length === 1 ? "" : "s"}`, "/people");
23889
23948
  },
23890
23949
  addTask: (t, opts) => {
23891
- if (!opts?.allowOverCap) {
23950
+ const wanted = t.parentTaskId ? byId(get().tasks).get(t.parentTaskId) : void 0;
23951
+ const parentTaskId = wanted && wanted.projectId === t.projectId && !subtaskProblem("", wanted.id, get().tasks) ? wanted.id : null;
23952
+ if (!opts?.allowOverCap && !parentTaskId) {
23892
23953
  if (columnWip(get(), t.projectId, t.status, 1)) return "";
23893
23954
  }
23894
23955
  const id = uid("t");
@@ -23899,6 +23960,7 @@ function tasksSlice({ set, get, write }) {
23899
23960
  createdById: get().currentUserId || null,
23900
23961
  completedAt: t.status === "done" ? t.completedAt ?? stamp() : null,
23901
23962
  checklist: t.checklist ?? [],
23963
+ ..."parentTaskId" in t ? { parentTaskId } : {},
23902
23964
  ...blockersPatch(blockerIds({
23903
23965
  id,
23904
23966
  blockedById: t.blockedById,
@@ -23925,6 +23987,10 @@ function tasksSlice({ set, get, write }) {
23925
23987
  },
23926
23988
  updateTask: (id, patch, opts) => {
23927
23989
  const task = get().tasks.find((row) => row.id === id);
23990
+ if (task && patch.parentTaskId && subtaskProblem(id, patch.parentTaskId, get().tasks)) {
23991
+ patch = { ...patch };
23992
+ delete patch.parentTaskId;
23993
+ }
23928
23994
  if (task && patch.status && patch.status !== task.status) {
23929
23995
  if (patch.status === "done" && !opts?.allowBlocked && isBlocked$1(task, get().tasks)) {
23930
23996
  set({ blockPending: {
@@ -23936,13 +24002,24 @@ function tasksSlice({ set, get, write }) {
23936
24002
  } });
23937
24003
  return;
23938
24004
  }
23939
- if (!opts?.allowOverCap) {
24005
+ if (!opts?.allowOverCap && !isSubtask(task, get().tasks)) {
23940
24006
  if (columnWip(get(), task.projectId, patch.status, 1)) return;
23941
24007
  }
23942
24008
  }
24009
+ const moving = task && patch.projectId && patch.projectId !== task.projectId;
24010
+ const carried = moving ? new Set(subtasksOf(task, get().tasks).map((t) => t.id)) : null;
24011
+ if (moving && task.parentTaskId && !("parentTaskId" in patch)) patch = {
24012
+ ...patch,
24013
+ parentTaskId: null
24014
+ };
23943
24015
  const project = get().projects.find((p) => p.id === task?.projectId);
23944
24016
  const newComments = [];
23945
24017
  const tasks = get().tasks.map((row) => {
24018
+ if (carried?.has(row.id)) return {
24019
+ ...row,
24020
+ projectId: patch.projectId,
24021
+ touchedAt: stamp()
24022
+ };
23946
24023
  if (row.id !== id) return row;
23947
24024
  const next = {
23948
24025
  ...row,
@@ -23992,7 +24069,7 @@ function tasksSlice({ set, get, write }) {
23992
24069
  } });
23993
24070
  return;
23994
24071
  }
23995
- if (!opts?.allowOverCap) {
24072
+ if (!opts?.allowOverCap && !isSubtask(task, get().tasks)) {
23996
24073
  if (columnWip(get(), task.projectId, status, 1)) return;
23997
24074
  }
23998
24075
  const nextRow = withStatus(task, status);
@@ -24412,6 +24489,7 @@ var init_tasks = __esmMin((() => {
24412
24489
  init_assignment();
24413
24490
  init_bulk();
24414
24491
  init_task_people();
24492
+ init_subtasks();
24415
24493
  }));
24416
24494
 
24417
24495
  // src/lib/raid-history.ts
@@ -30471,6 +30549,7 @@ init_clock();
30471
30549
  init_date_text();
30472
30550
  init_work();
30473
30551
  init_blockers();
30552
+ init_subtasks();
30474
30553
  init_recurrence();
30475
30554
  init_task_people();
30476
30555
  init_search();
@@ -30589,6 +30668,8 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
30589
30668
  const thread = comments.filter((c) => c.taskId === task.id).slice(-4);
30590
30669
  const mine = comments.filter((c) => c.taskId === task.id && c.authorId === me);
30591
30670
  const blockers = blockersOf(task, tasks);
30671
+ const subtaskOf = parentOf(task, tasks);
30672
+ const subs = subtasksOf(task, tasks);
30592
30673
  const done = () => setEditing(null);
30593
30674
  const nameOf = (id) => people.find((p) => p.id === id)?.name;
30594
30675
  const setCustom = (fieldId, value) => store.getState().setTaskCustomFieldValue(task.id, fieldId, value);
@@ -30941,6 +31022,40 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
30941
31022
  ]
30942
31023
  }, item.id);
30943
31024
  }),
31025
+ subtaskOf ? /* @__PURE__ */ jsxs(Text, {
31026
+ color: theme.muted,
31027
+ wrap: "truncate-end",
31028
+ children: [
31029
+ " ",
31030
+ "Subtask of ",
31031
+ keyOf$1({ projects }, subtaskOf),
31032
+ " ",
31033
+ subtaskOf.title
31034
+ ]
31035
+ }) : null,
31036
+ subs.length > 0 ? /* @__PURE__ */ jsxs(Text, {
31037
+ color: theme.muted,
31038
+ children: [
31039
+ " ",
31040
+ "Subtasks ",
31041
+ subs.filter((x) => x.status === "done").length,
31042
+ "/",
31043
+ subs.length
31044
+ ]
31045
+ }) : null,
31046
+ subs.map((x) => /* @__PURE__ */ jsxs(Text, {
31047
+ wrap: "truncate-end",
31048
+ children: [
31049
+ " ",
31050
+ x.status === "done" ? "[x] " : "[ ] ",
31051
+ keyOf$1({ projects }, x),
31052
+ " ",
31053
+ x.status === "done" ? /* @__PURE__ */ jsx(Text, {
31054
+ strikethrough: true,
31055
+ children: x.title
31056
+ }) : x.title
31057
+ ]
31058
+ }, x.id)),
30944
31059
  (task.github ?? []).slice(0, 3).map((ref) => /* @__PURE__ */ jsxs(Text, {
30945
31060
  color: theme.muted,
30946
31061
  wrap: "truncate-end",
package/lib/cli.mjs CHANGED
@@ -168,7 +168,7 @@ export const HELP = `harbor: Harbor from the terminal
168
168
  [--also "NAME, NAME"] [--due DATE|pilot]
169
169
  [--priority low|medium|high|urgent]
170
170
  [--status S] [--comment TEXT] [--over-limit] [--dry-run]
171
- [--literal] [--template NAME]
171
+ [--literal] [--template NAME] [--parent TASK]
172
172
  reads plain words in TITLE as the
173
173
  website does: @name, by friday,
174
174
  !high, #project (--literal: keep
@@ -183,7 +183,8 @@ export const HELP = `harbor: Harbor from the terminal
183
183
  [--also "NAME, NAME"|none] [--due DATE|pilot|none]
184
184
  [--priority P] [--status S] [--notes TEXT]
185
185
  [--repeat weekly|biweekly|monthly|never]
186
- [--waits-on TASK,TASK|none] [--confirm] [--over-limit]
186
+ [--waits-on TASK,TASK|none] [--parent TASK|none]
187
+ [--confirm] [--over-limit]
187
188
  harbor limit PROJECT [COLUMN N|none] how many tasks a board column
188
189
  takes at once; the owner sets it
189
190
  harbor week [--next|--last|--week DATE] [--review]
@@ -704,7 +705,7 @@ export function hint(message, project) {
704
705
  )
705
706
  .replace(
706
707
  /^Nothing to change: pass a field to update\.$/,
707
- "Nothing to change. Say what with --title, --assignee, --also, --due, --priority, --status, --notes, --repeat or --waits-on.",
708
+ "Nothing to change. Say what with --title, --assignee, --also, --due, --priority, --status, --notes, --repeat, --waits-on or --parent.",
708
709
  )
709
710
  .replace(
710
711
  /^This connection was allowed to read only\..*$/s,
@@ -785,6 +786,10 @@ const DETAIL = {
785
786
  " notes, priority, owner, others on it, checklist and due offset fill what",
786
787
  " the flags don't. Leave out TITLE to take the template's.",
787
788
  "",
789
+ " --parent TASK adds it inside that task as a subtask: a whole task with its",
790
+ " own owner and date, shown in the parent's details rather than on the",
791
+ " board. It goes in the parent's project; subtasks go one level deep.",
792
+ "",
788
793
  " --due pilot takes the earliest day the owner's queue suggests: their open",
789
794
  " work due first, their weekly pace, time away, what it waits on and how",
790
795
  " long their similar tasks took. It needs an owner (--assignee none is",
@@ -2288,6 +2293,7 @@ export async function run(argv, io = {}) {
2288
2293
  ),
2289
2294
  ];
2290
2295
  if (t.alsoOn?.length) lines.push(`Also on it: ${t.alsoOn.join(", ")}.`);
2296
+ if (t.subtaskOf) lines.push(`Subtask of ${t.subtaskOf.key ?? t.subtaskOf.id} ${t.subtaskOf.title}.`);
2291
2297
  if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
2292
2298
  for (const [name, v] of Object.entries(t.fields ?? {}))
2293
2299
  lines.push(`${name}: ${Array.isArray(v) ? v.join(", ") : v === true ? "yes" : v}`);
@@ -2296,6 +2302,15 @@ export async function run(argv, io = {}) {
2296
2302
  if (t.description) lines.push("", t.description);
2297
2303
  if (t.checklist.length)
2298
2304
  lines.push("", ...t.checklist.map((c) => ` [${c.done ? "x" : " "}] ${c.step}`));
2305
+ if (t.subtasks?.length)
2306
+ lines.push(
2307
+ "",
2308
+ `Subtasks (${t.subtasks.filter((x) => x.status === "done").length}/${t.subtasks.length} done):`,
2309
+ ...t.subtasks.map(
2310
+ (x) =>
2311
+ ` [${x.status === "done" ? "x" : " "}] ${x.key ?? x.id} ${x.title}${x.assignee ? `, ${x.assignee}` : ""}${x.due ? `, due ${x.due}` : ""}`,
2312
+ ),
2313
+ );
2299
2314
  if (t.waitingOn.length)
2300
2315
  lines.push(
2301
2316
  "",
@@ -2531,9 +2546,12 @@ export async function run(argv, io = {}) {
2531
2546
  const { positionals, values, harbor } = parse(
2532
2547
  spec(
2533
2548
  ["over-limit", "dry-run", "literal"],
2534
- ["project", "assignee", "also", "due", "priority", "status", "comment", "template"],
2549
+ ["project", "assignee", "also", "due", "priority", "status", "comment", "template", "parent"],
2535
2550
  ),
2536
2551
  );
2552
+ // A subtask goes in its parent's project.
2553
+ if (values.parent && !values.project)
2554
+ values.project = (await client.call("get_task", { harbor, task: values.parent })).projectId;
2537
2555
  // --template with no title: the template's own title.
2538
2556
  const fromTemplate = values.template !== undefined && positionals.length === 0;
2539
2557
  // Several at once: one title a line on stdin, with no title (or -) after add.
@@ -2636,6 +2654,7 @@ export async function run(argv, io = {}) {
2636
2654
  suggestedDue: duePilot(values.due),
2637
2655
  priority: values.priority ?? words.priority?.value,
2638
2656
  status: values.status === undefined ? undefined : statusWord(values.status),
2657
+ parent: values.parent,
2639
2658
  overLimit: values["over-limit"] || undefined,
2640
2659
  })
2641
2660
  .catch((err) => {
@@ -2761,6 +2780,7 @@ export async function run(argv, io = {}) {
2761
2780
  "notes",
2762
2781
  "repeat",
2763
2782
  "waits-on",
2783
+ "parent",
2764
2784
  ],
2765
2785
  ),
2766
2786
  );
@@ -2791,12 +2811,18 @@ export async function run(argv, io = {}) {
2791
2811
  .split(",")
2792
2812
  .map((x) => x.trim())
2793
2813
  .filter(Boolean),
2814
+ parent: values.parent,
2794
2815
  confirmed: values.confirm || undefined,
2795
2816
  overLimit: values["over-limit"] || undefined,
2796
2817
  },
2797
2818
  (r) =>
2798
2819
  [
2799
2820
  `Updated: ${taskLine(r.updated, opts)}`,
2821
+ values.parent !== undefined
2822
+ ? r.updated.subtaskOf
2823
+ ? `A subtask of ${r.updated.subtaskOf}.`
2824
+ : "A task of its own now."
2825
+ : null,
2800
2826
  values.also !== undefined
2801
2827
  ? r.updated.alsoOn?.length
2802
2828
  ? `Also on it: ${r.updated.alsoOn.join(", ")}.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.86.0",
3
+ "version": "0.87.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",