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