@harborppm/cli 0.81.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
@@ -10009,6 +10009,39 @@ function inboxOf(data, personId, now = clockNow()) {
10009
10009
  href: `/projects/${task.projectId}`
10010
10010
  });
10011
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
+ }
10012
10045
  for (const comment of mentionsFor(data.comments, personId)) {
10013
10046
  const task = data.tasks.find((t) => t.id === comment.taskId);
10014
10047
  if (!task || task.status === "done") continue;
@@ -20237,6 +20270,17 @@ var init_fog = __esmMin((() => {
20237
20270
  fileCache = /* @__PURE__ */ new WeakMap();
20238
20271
  }));
20239
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
+
20240
20284
  // src/lib/mcp/tools.ts
20241
20285
  /**
20242
20286
  * What an agent can read about a harbor, as MCP tools. Each tool is a pure
@@ -20272,7 +20316,6 @@ init_handover();
20272
20316
  init_agents();
20273
20317
  init_work();
20274
20318
  init_task_people();
20275
- init_fog();
20276
20319
  init_passage();
20277
20320
  init_board();
20278
20321
  init_holidays();
@@ -20518,6 +20561,18 @@ function soundingOut(data, t, meId, now) {
20518
20561
  };
20519
20562
  }
20520
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
+ }
20521
20576
  function overdue(t, today) {
20522
20577
  const due = dayOf(t.dueDate);
20523
20578
  return isOpen(t) && due != null && due < today;
@@ -21043,6 +21098,7 @@ const HARBOR_TOOLS = [
21043
21098
  waitingOn: blockersOf(t, data.tasks).map(brief),
21044
21099
  waitedOnBy: waiters.map(brief),
21045
21100
  ...t.sounding ? { sounding: soundingOut(data, t, me(ctx)?.id ?? null, ctx.now) } : {},
21101
+ ...t.approval ? { approval: approvalOut(data, t.approval) } : {},
21046
21102
  ...pilot ? { pilotDue: {
21047
21103
  earliest: pilot.earliest,
21048
21104
  fits: pilot.fits,
@@ -21096,7 +21152,7 @@ const HARBOR_TOOLS = [
21096
21152
  tool({
21097
21153
  name: "my_work",
21098
21154
  title: "My work",
21099
- 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,
21100
21156
  input: object({
21101
21157
  harbor: harborArg,
21102
21158
  limit: limitArg(50, 25)
@@ -21143,6 +21199,13 @@ const HARBOR_TOOLS = [
21143
21199
  ...taskKey(t, projects.get(t.projectId)) ? { key: taskKey(t, projects.get(t.projectId)) } : {},
21144
21200
  title: t.title,
21145
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)
21146
21209
  }))
21147
21210
  };
21148
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.81.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",