@harborppm/cli 0.80.0 → 0.82.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/dist/app.mjs CHANGED
@@ -9975,6 +9975,7 @@ function inboxOf(data, personId, now = clockNow()) {
9975
9975
  detail: `${r.kind === "issue" ? "Issue" : "Risk"}${where ? ` in ${where}` : ""} · its review date has passed`,
9976
9976
  at: r.dueDate,
9977
9977
  href: `/risks/${r.id}`,
9978
+ raidId: r.id,
9978
9979
  personId: null
9979
9980
  });
9980
9981
  }
@@ -10008,6 +10009,39 @@ function inboxOf(data, personId, now = clockNow()) {
10008
10009
  href: `/projects/${task.projectId}`
10009
10010
  });
10010
10011
  }
10012
+ for (const task of data.tasks) {
10013
+ const a = task.approval;
10014
+ if (!a) continue;
10015
+ const project = projectOf(data, task);
10016
+ const where = `“${task.title}”${project ? ` · ${project.name}` : ""}`;
10017
+ if (a.approverId === personId && a.askedById !== personId && a.status === "pending" && task.status !== "done") {
10018
+ const by = data.people.find((p) => p.id === a.askedById);
10019
+ pings.push({
10020
+ id: `task-approve:${task.id}:${shortHash(a.askedAt)}`,
10021
+ kind: "approval",
10022
+ title: `${by ? firstName$3(by.name) : "Someone"} asked for your approval`,
10023
+ detail: `${where} · approve it, or ask for changes`,
10024
+ at: a.askedAt,
10025
+ taskId: task.id,
10026
+ personId: a.askedById,
10027
+ href: `/projects/${task.projectId}`
10028
+ });
10029
+ } else if (a.askedById === personId && a.approverId !== personId && a.status !== "pending" && a.decidedAt && a.decidedAt >= since) {
10030
+ const who = data.people.find((p) => p.id === a.approverId);
10031
+ const name = who ? firstName$3(who.name) : "The approver";
10032
+ const note = (a.note ?? "").trim();
10033
+ pings.push({
10034
+ id: `task-decided:${task.id}:${shortHash(a.decidedAt)}`,
10035
+ kind: "approval",
10036
+ title: a.status === "approved" ? `${name} approved “${task.title}”` : `${name} asked for changes to “${task.title}”`,
10037
+ detail: note ? excerpt(note) : project?.name ?? "",
10038
+ at: a.decidedAt,
10039
+ taskId: task.id,
10040
+ personId: a.approverId,
10041
+ href: `/projects/${task.projectId}`
10042
+ });
10043
+ }
10044
+ }
10011
10045
  for (const comment of mentionsFor(data.comments, personId)) {
10012
10046
  const task = data.tasks.find((t) => t.id === comment.taskId);
10013
10047
  if (!task || task.status === "done") continue;
@@ -10064,6 +10098,7 @@ function InboxView({ active, width, height, go }) {
10064
10098
  const open = (p) => {
10065
10099
  store.getState().readPing(p.id);
10066
10100
  if (p.taskId) go.openTask(p.taskId);
10101
+ else if (p.raidId) go.openPanel("raid", p.raidId);
10067
10102
  else if (p.programId) go.openProgram(p.programId);
10068
10103
  };
10069
10104
  useInput((input, key) => {
@@ -20235,6 +20270,17 @@ var init_fog = __esmMin((() => {
20235
20270
  fileCache = /* @__PURE__ */ new WeakMap();
20236
20271
  }));
20237
20272
 
20273
+ // src/lib/task-approval.ts
20274
+ init_fog();
20275
+ /** Waits on its approver now: asked, not yet answered, and not done. */
20276
+ function awaitingApproval(task) {
20277
+ return Boolean(task?.approval && task.approval.status === "pending" && task.status !== "done");
20278
+ }
20279
+ /** Open tasks waiting on this person's approval, oldest ask first. */
20280
+ function approvalsWaitingOn(tasks, personId) {
20281
+ return tasks.filter((t) => awaitingApproval(t) && t.approval.approverId === personId).sort((a, b) => a.approval.askedAt.localeCompare(b.approval.askedAt));
20282
+ }
20283
+
20238
20284
  // src/lib/mcp/tools.ts
20239
20285
  /**
20240
20286
  * What an agent can read about a harbor, as MCP tools. Each tool is a pure
@@ -20270,7 +20316,6 @@ init_handover();
20270
20316
  init_agents();
20271
20317
  init_work();
20272
20318
  init_task_people();
20273
- init_fog();
20274
20319
  init_passage();
20275
20320
  init_board();
20276
20321
  init_holidays();
@@ -20516,6 +20561,18 @@ function soundingOut(data, t, meId, now) {
20516
20561
  };
20517
20562
  }
20518
20563
  const dayOf = (iso) => iso ? iso.slice(0, 10) : null;
20564
+ /** A task's approval (src/lib/task-approval.ts), as an agent reads it. */
20565
+ function approvalOut(data, a) {
20566
+ const people = byId(data.people);
20567
+ return {
20568
+ approver: nameOf$2(people, a.approverId) ?? "someone no longer here",
20569
+ askedBy: nameOf$2(people, a.askedById) ?? "someone no longer here",
20570
+ asked: dayOf(a.askedAt),
20571
+ status: a.status === "changes_requested" ? "changes requested" : a.status,
20572
+ ...a.note ? { note: a.note } : {},
20573
+ ...a.decidedAt ? { decided: dayOf(a.decidedAt) } : {}
20574
+ };
20575
+ }
20519
20576
  function overdue(t, today) {
20520
20577
  const due = dayOf(t.dueDate);
20521
20578
  return isOpen(t) && due != null && due < today;
@@ -21041,6 +21098,7 @@ const HARBOR_TOOLS = [
21041
21098
  waitingOn: blockersOf(t, data.tasks).map(brief),
21042
21099
  waitedOnBy: waiters.map(brief),
21043
21100
  ...t.sounding ? { sounding: soundingOut(data, t, me(ctx)?.id ?? null, ctx.now) } : {},
21101
+ ...t.approval ? { approval: approvalOut(data, t.approval) } : {},
21044
21102
  ...pilot ? { pilotDue: {
21045
21103
  earliest: pilot.earliest,
21046
21104
  fits: pilot.fits,
@@ -21094,7 +21152,7 @@ const HARBOR_TOOLS = [
21094
21152
  tool({
21095
21153
  name: "my_work",
21096
21154
  title: "My work",
21097
- description: "What's yours in a harbor: your open tasks (late first), open tasks others own that you're also on (alsoOn), what others are waiting on you for, and the risks you own." + DATA_NOTE,
21155
+ description: "What's yours in a harbor: your open tasks (late first), open tasks others own that you're also on (alsoOn), what others are waiting on you for, the risks you own, and tasks waiting on your approval (approvalsToGive; answer with decide_approval)." + DATA_NOTE,
21098
21156
  input: object({
21099
21157
  harbor: harborArg,
21100
21158
  limit: limitArg(50, 25)
@@ -21141,6 +21199,13 @@ const HARBOR_TOOLS = [
21141
21199
  ...taskKey(t, projects.get(t.projectId)) ? { key: taskKey(t, projects.get(t.projectId)) } : {},
21142
21200
  title: t.title,
21143
21201
  askedBy: nameOf$2(people, t.sounding.askedById)
21202
+ })),
21203
+ approvalsToGive: approvalsWaitingOn(data.tasks, mine.id).map((t) => ({
21204
+ id: t.id,
21205
+ ...taskKey(t, projects.get(t.projectId)) ? { key: taskKey(t, projects.get(t.projectId)) } : {},
21206
+ title: t.title,
21207
+ askedBy: nameOf$2(people, t.approval.askedById),
21208
+ asked: dayOf(t.approval.askedAt)
21144
21209
  }))
21145
21210
  };
21146
21211
  }
package/lib/cli.mjs CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  soundingsText,
42
42
  goalsText,
43
43
  templatesText,
44
+ approvalsText,
44
45
  thanksText,
45
46
  ashoreText,
46
47
  voyageText,
@@ -268,6 +269,10 @@ export const HELP = `harbor: Harbor from the terminal
268
269
  start from (harbor add --template)
269
270
  harbor template TASK [--name NAME] save a task as a template in its
270
271
  project
272
+ harbor approve TASK [--ask NAME|--changes NOTE] [--note TEXT]
273
+ approve a task (finishes it), send
274
+ it back, or ask someone to approve
275
+ harbor approvals tasks waiting on your approval
271
276
  harbor ashore [--weeks N] [--country CODES|none] [--person NAME]
272
277
  public holidays and time away
273
278
  coming up, and whose holidays
@@ -1046,6 +1051,22 @@ const DETAIL = {
1046
1051
  " harbor add --project P --template NAME starts a task from one: its title,",
1047
1052
  " notes, priority, owner, others on it, checklist and due offset.",
1048
1053
  ],
1054
+ approve: [
1055
+ " harbor approve TASK writes: approves a task you were asked",
1056
+ " to approve, which finishes it",
1057
+ ' harbor approve TASK --changes "NOTE" writes: sends it back, saying what',
1058
+ " should change",
1059
+ " harbor approve TASK --ask NAME writes: asks someone to approve it",
1060
+ " (again, after changes)",
1061
+ " harbor approvals reads: tasks waiting on your approval",
1062
+ "",
1063
+ " --note TEXT adds a word to an approval. Whoever asked hears the answer.",
1064
+ ],
1065
+ approvals: [
1066
+ " harbor approvals reads: tasks waiting on your approval,",
1067
+ " oldest ask first",
1068
+ " harbor approve TASK [--changes NOTE] writes: approve it or send it back",
1069
+ ],
1049
1070
  goals: [
1050
1071
  " harbor goals [PORTFOLIO] reads: each portfolio's goals (all when",
1051
1072
  " left out), off track first, with how far",
@@ -3216,6 +3237,43 @@ export async function run(argv, io = {}) {
3216
3237
  soundingsText(r),
3217
3238
  );
3218
3239
  }
3240
+ case "approve": {
3241
+ const { positionals, values, harbor } = parse(spec([], ["ask", "changes", "note"]));
3242
+ const task = positionals[0];
3243
+ if (!task) throw new HarborError("Give a task's key, like harbor approve RR-4.");
3244
+ if (positionals.length > 1)
3245
+ throw new HarborError(`Unexpected "${positionals.slice(1).join(" ")}".`);
3246
+ if (values.ask !== undefined && values.changes !== undefined)
3247
+ throw new HarborError("Ask or send back: one at a time.");
3248
+ const said = (r) => {
3249
+ const a = r.approval ?? {};
3250
+ const name = r.key ?? r.title;
3251
+ if (a.status === "pending") return `Asked ${a.approver} to approve ${name}.`;
3252
+ if (a.status === "approved")
3253
+ return `Approved ${name}. It's done${a.askedBy ? `, and ${a.askedBy} will hear` : ""}.`;
3254
+ return `Sent ${name} back to ${a.askedBy}: “${a.note}”.`;
3255
+ };
3256
+ if (values.ask !== undefined)
3257
+ return call("ask_approval", { harbor, task, approver: values.ask }, (r) => said(r.asked));
3258
+ const changes = values.changes !== undefined;
3259
+ if (changes && !values.changes.trim())
3260
+ throw new HarborError('Say what should change: harbor approve TASK --changes "NOTE".');
3261
+ return call(
3262
+ "decide_approval",
3263
+ {
3264
+ harbor,
3265
+ task,
3266
+ decision: changes ? "changes" : "approve",
3267
+ ...(changes ? { note: values.changes } : values.note ? { note: values.note } : {}),
3268
+ },
3269
+ (r) => said(r.decided),
3270
+ );
3271
+ }
3272
+ case "approvals": {
3273
+ const { positionals, harbor } = parse(spec());
3274
+ if (positionals.length) throw new HarborError(`Unexpected "${positionals.join(" ")}".`);
3275
+ return call("my_work", { harbor, limit: 1 }, (r) => approvalsText(r));
3276
+ }
3219
3277
  case "templates": {
3220
3278
  const { positionals, harbor } = parse(spec());
3221
3279
  const project = positionals.join(" ").trim();
package/lib/format.mjs CHANGED
@@ -1255,6 +1255,16 @@ export function templatesText(r) {
1255
1255
  return lines.join("\n");
1256
1256
  }
1257
1257
 
1258
+ export function approvalsText(r) {
1259
+ const list = r.approvalsToGive ?? [];
1260
+ if (!list.length) return "Nothing is waiting on your approval.";
1261
+ const lines = [`Waiting on your approval (${list.length}):`];
1262
+ for (const t of list)
1263
+ lines.push(` ${t.key ?? t.id} ${t.title} (asked by ${t.askedBy ?? "someone"}${t.asked ? `, ${t.asked}` : ""})`);
1264
+ lines.push("", "harbor approve TASK approves one; --changes \"NOTE\" sends it back.");
1265
+ return lines.join("\n");
1266
+ }
1267
+
1258
1268
  export function goalsText(r) {
1259
1269
  const order = ["off-track", "at-risk", "not-started", "on-track", "achieved"];
1260
1270
  const withGoals = (r.portfolios ?? []).filter((p) => p.goals?.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.80.0",
3
+ "version": "0.82.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",