@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 +65 -2
- package/lib/cli.mjs +58 -0
- package/lib/format.mjs +10 -0
- package/package.json +1 -1
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,
|
|
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);
|