@harborppm/cli 0.98.0 → 0.99.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/README.md +12 -4
- package/dist/app.mjs +196 -50
- package/lib/cli.mjs +493 -85
- package/lib/format.mjs +97 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ harbor project "NAME" --program P [--lead me|NAME] [--starts DATE]
|
|
|
166
166
|
they really finished in that project
|
|
167
167
|
harbor add "TITLE" --project P [--assignee me|NAME] [--also "NAME, NAME"] [--due DATE|pilot]
|
|
168
168
|
[--priority low|medium|high|urgent] [--status S] [--comment TEXT]
|
|
169
|
-
[--starts DATE] [--over-limit] [--literal]
|
|
169
|
+
[--starts DATE] [--over-limit] [--literal] [--dry-run] [--unless-exists]
|
|
170
170
|
no TITLE: one title a line from stdin
|
|
171
171
|
--due pilot takes the earliest day the owner's queue
|
|
172
172
|
suggests (their open work due first, weekly pace, time
|
|
@@ -213,11 +213,19 @@ harbor import FILE [--project P] [--portfolio P] [--no-people]
|
|
|
213
213
|
add tasks from a CSV file
|
|
214
214
|
harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
|
|
215
215
|
harbor voyage PROJECT ["YOUR LINE"]
|
|
216
|
-
harbor messages PROJECT ["TEXT"] [--limit N]
|
|
216
|
+
harbor messages PROJECT ["TEXT"] [--limit N] | messages PROJECT --edit "TEXT" [--message ID]
|
|
217
217
|
harbor note PROJECT ["NOTE"] [--opens DATE|--milestone NAME|--off-track]
|
|
218
218
|
harbor sound TASK [--answer DAYS|--ask NAMES|--settle DAYS] [--why TEXT]
|
|
219
219
|
harbor soundings [PROJECT]
|
|
220
220
|
harbor goals [PORTFOLIO] each portfolio's goals, off track first
|
|
221
|
+
harbor goal "GOAL" [--title T] [--owner NAME] [--due DATE|none] [--link WORK] [--unlink WORK]
|
|
222
|
+
[--kr "KR" --at N|--target N [--start N] [--unit U]|--counts WORK]
|
|
223
|
+
harbor goal "TITLE" --new [--portfolio P] [--owner NAME] [--due DATE] [--why T]
|
|
224
|
+
harbor milestones [PROGRAM] [--project P] [--done]
|
|
225
|
+
harbor milestone "NAME" [--program P] [--due DATE] [--rename T] [--reached|--open]
|
|
226
|
+
[--signs-off NAME|none] [--approve|--changes NOTE]
|
|
227
|
+
harbor milestone "NAME" --new --program P --due DATE
|
|
228
|
+
harbor reports [DASHBOARD] the Reports page's dashboards, each chart as numbers
|
|
221
229
|
harbor ashore [--weeks N] [--country CODES|none] [--person NAME]
|
|
222
230
|
harbor limit PROJECT [COLUMN N|none]
|
|
223
231
|
harbor week [--next|--last|--week DATE] [--review]
|
|
@@ -253,13 +261,13 @@ Agents on the crew: put an AI app on People with Add an agent and assign it task
|
|
|
253
261
|
|
|
254
262
|
A task has one owner (`--assignee`), who answers for it and whose load it counts against, and anyone else on it with them (`--also "Sam, Aya"`): they see it under Also on in `harbor mine` and on Today, and count as knowing it. `harbor edit RR-4 --also none` leaves only the owner.
|
|
255
263
|
|
|
256
|
-
`harbor add --project Trail --comment "From Monday's call" < list.txt` adds one task per line of the file, each with the comment (every option applies to every line); `--dry-run`
|
|
264
|
+
`harbor add --project Trail --comment "From Monday's call" < list.txt` adds one task per line of the file, each with the comment (every option applies to every line); `--dry-run` asks Harbor to add them on a copy it throws away, so it shows each task as it would really be (key, column, owner, priority, dates, parent, milestone), an open task that already has the title, and anything it would refuse and why; it exits 1 when something would be refused. `--unless-exists` adds a title only if the project has no open task by that name, so a retried script doesn't double up. `--json` gives `added` and `skipped` lists, each entry with its key, for one title or a hundred.
|
|
257
265
|
|
|
258
266
|
`harbor add "Book the venue @Quinn by Friday !high #Website"` reads the title in plain words, as the website's New task does: it adds "Book the venue" for Quinn, due Friday, high priority, in the Website project, and says what it read. A flag wins over the words (they stay in the title), `--literal` keeps the title exactly as typed, and `--due` takes the same words (`--due "next friday"`, `--due "in two weeks"`). In the full-screen app, `n` reads a new task's title the same way.
|
|
259
267
|
|
|
260
268
|
`harbor limit RR` prints how many tasks each of a board's columns takes at once; `harbor limit RR doing 8` changes one (1 to 20, or `none` for no limit), if you own the harbor. `harbor projects --json` carries each board's `limits` too.
|
|
261
269
|
|
|
262
|
-
`harbor premortem` runs a pre-mortem with your crew: `--start` asks everyone "it's after the end date and this project failed: what happened?", each person adds answers alone for five minutes, and then the answers show together on the project's Risks & issues tab, where each becomes a risk in one click. On its own it prints the question and the answers you can see. Once a project is Done, `harbor voyage` prints its voyage log (planned against actual dates, the biggest slips, the risks that came true, who closed the most, and the crew's lines) and adds yours. `harbor messages PROJECT` reads a project's Messages tab, one conversation for the whole project, and `harbor messages PROJECT "TEXT"` writes to it; `@Name` sends it to that person's Inbox. `harbor note` leaves a note for later on a project: it stays sealed (the crew sees only that it's there and who left it) until its day, until one of the program's milestones is reached, or until the project turns Off track, and then it opens at the top of the project and in the brief. `harbor sound TASK --ask crew` takes a sounding: it asks you, the task's owner and whoever has open work on its project how many working days the task will take (`--ask "Sam,Priya"` picks who). Each person answers alone with `harbor sound TASK --answer 4 --why "…"`, and nobody sees the others' numbers until everyone has; then `harbor sound TASK` shows the spread, an estimate when the guesses are close, or "talk about this first" when they're far apart, and `--settle DAYS` records what you agreed. `harbor soundings` lists them, the ones waiting on your answer first. `harbor goals` lists each portfolio's goals
|
|
270
|
+
`harbor premortem` runs a pre-mortem with your crew: `--start` asks everyone "it's after the end date and this project failed: what happened?", each person adds answers alone for five minutes, and then the answers show together on the project's Risks & issues tab, where each becomes a risk in one click. On its own it prints the question and the answers you can see. Once a project is Done, `harbor voyage` prints its voyage log (planned against actual dates, the biggest slips, the risks that came true, who closed the most, and the crew's lines) and adds yours. `harbor messages PROJECT` reads a project's Messages tab, one conversation for the whole project, and `harbor messages PROJECT "TEXT"` writes to it; `@Name` sends it to that person's Inbox. `harbor note` leaves a note for later on a project: it stays sealed (the crew sees only that it's there and who left it) until its day, until one of the program's milestones is reached, or until the project turns Off track, and then it opens at the top of the project and in the brief. `harbor sound TASK --ask crew` takes a sounding: it asks you, the task's owner and whoever has open work on its project how many working days the task will take (`--ask "Sam,Priya"` picks who). Each person answers alone with `harbor sound TASK --answer 4 --why "…"`, and nobody sees the others' numbers until everyone has; then `harbor sound TASK` shows the spread, an estimate when the guesses are close, or "talk about this first" when they're far apart, and `--settle DAYS` records what you agreed. `harbor soundings` lists them, the ones waiting on your answer first. `harbor goals` lists each portfolio's goals, off track first, with each key result's progress. `harbor goal "TITLE" --new` sets one, and `harbor goal "GOAL"` reads or changes one: `--owner`, `--due`, `--why`, `--link "Ridge Runner"` to count a program, project or task toward it, `--kr "Stores signed" --target 40` to add a number key result and `--kr "Stores signed" --at 12` to move it. `harbor milestones` lists a program's milestones soonest first, with how many of their tasks are done, who signs them off and what they wait on; `harbor milestone "NAME" --new --program P --due DATE` adds one, and `harbor milestone "NAME"` moves (`--due`), renames, ticks (`--reached`), sets who signs it off (`--signs-off`), or, if it's yours to sign, `--approve`s it or asks for `--changes`. `harbor reports` prints the Reports page's dashboards, each chart as its numbers. `harbor messages PROJECT --edit "TEXT"` fixes your latest message there. `harbor ashore` lists the public holidays and time away in the next eight weeks; `--country US,GB` sets whose holidays the harbor takes and `--person NAME --country DE` one person's own, which the forecast and the weekly load then count as days off.
|
|
263
271
|
|
|
264
272
|
`harbor brief --say` turns the brief into the BBC's Shipping Forecast: each program an area, wind for late work, visibility for health, gale warnings for anything off track. It prints the forecast and speaks it with your computer's own voice (`say` on a Mac, Windows' speech, or `espeak-ng` on Linux, which you may need to install). With `--json` it prints the forecast and stays quiet.
|
|
265
273
|
|
package/dist/app.mjs
CHANGED
|
@@ -5327,7 +5327,7 @@ function cascadeDelete(data, kind, id) {
|
|
|
5327
5327
|
taskIds: r.taskIds.filter((t) => !taskIds.has(t))
|
|
5328
5328
|
})),
|
|
5329
5329
|
spendEntries: (data.spendEntries ?? []).filter((e) => !projectIds.has(e.projectId)),
|
|
5330
|
-
reflections: (data.reflections ?? []).filter((r) => !projectIds.has(r.projectId) && !(r.thanksFor?.kind === "milestone" && goneMilestones.has(r.thanksFor.id)))
|
|
5330
|
+
reflections: (data.reflections ?? []).filter((r) => !projectIds.has(r.projectId) && !(r.programId && programIds.has(r.programId)) && !(r.thanksFor?.kind === "milestone" && goneMilestones.has(r.thanksFor.id)))
|
|
5331
5331
|
},
|
|
5332
5332
|
counts: {
|
|
5333
5333
|
programs: programIds.size,
|
|
@@ -10687,9 +10687,23 @@ function landfallJson(project, tasks, today, people) {
|
|
|
10687
10687
|
}
|
|
10688
10688
|
|
|
10689
10689
|
// src/lib/project-messages.ts
|
|
10690
|
-
/**
|
|
10691
|
-
function
|
|
10692
|
-
return (reflections ?? []).filter((r) =>
|
|
10690
|
+
/** One conversation's messages (answers too), oldest first. */
|
|
10691
|
+
function messagesIn(reflections, place) {
|
|
10692
|
+
return (reflections ?? []).filter((r) => inPlace(r, place)).sort(oldestFirst);
|
|
10693
|
+
}
|
|
10694
|
+
/**
|
|
10695
|
+
* The message an answer hangs under: the one asked for, or, for an answer
|
|
10696
|
+
* to an answer, that answer's own message. Null when it isn't a message in
|
|
10697
|
+
* the same conversation.
|
|
10698
|
+
*/
|
|
10699
|
+
function replyRoot(reflections, place, replyToId) {
|
|
10700
|
+
if (!replyToId) return null;
|
|
10701
|
+
const all = reflections ?? [];
|
|
10702
|
+
const target = all.find((r) => r.id === replyToId && inPlace(r, place));
|
|
10703
|
+
if (!target) return null;
|
|
10704
|
+
if (!target.replyToId) return target.id;
|
|
10705
|
+
const root = all.find((r) => r.id === target.replyToId && inPlace(r, place));
|
|
10706
|
+
return root ? root.id : target.id;
|
|
10693
10707
|
}
|
|
10694
10708
|
/** Why a message can't be posted, or null. */
|
|
10695
10709
|
function messageProblem(body) {
|
|
@@ -10708,25 +10722,30 @@ function mayChangeMessage(message, personId, role, action) {
|
|
|
10708
10722
|
return action === "delete" && role === "owner";
|
|
10709
10723
|
}
|
|
10710
10724
|
/**
|
|
10711
|
-
* What a person's Inbox says about
|
|
10712
|
-
*
|
|
10713
|
-
*
|
|
10714
|
-
*
|
|
10715
|
-
*
|
|
10725
|
+
* What a person's Inbox says about messages since `since`: each message that
|
|
10726
|
+
* @mentions them, and for each conversation they've written in, the messages
|
|
10727
|
+
* others posted after their last one (a mention among them is already its
|
|
10728
|
+
* own line). An answer to one of your messages reaches you as well, even in
|
|
10729
|
+
* a conversation where that's all you've written. A finished project's or
|
|
10730
|
+
* program's chatter is left out, but a mention or an answer to you there
|
|
10731
|
+
* still reaches you: a wrap-up "thanks @Dana" is meant to.
|
|
10716
10732
|
*/
|
|
10717
10733
|
function messageLines(data, personId, since) {
|
|
10718
|
-
const done = new Set(data.projects.filter((p) => p.health === "done").map((p) => p.id));
|
|
10719
|
-
const
|
|
10734
|
+
const done = /* @__PURE__ */ new Set([...data.projects.filter((p) => p.health === "done").map((p) => `project:${p.id}`), ...(data.programs ?? []).filter((p) => p.health === "done").map((p) => `program:${p.id}`)]);
|
|
10735
|
+
const byPlace = /* @__PURE__ */ new Map();
|
|
10736
|
+
const mine = /* @__PURE__ */ new Set();
|
|
10720
10737
|
for (const r of data.reflections ?? []) {
|
|
10721
10738
|
if (!isMessage(r)) continue;
|
|
10722
|
-
|
|
10739
|
+
if (r.authorId === personId) mine.add(r.id);
|
|
10740
|
+
const key = placeKey(placeOf(r));
|
|
10741
|
+
const list = byPlace.get(key);
|
|
10723
10742
|
if (list) list.push(r);
|
|
10724
|
-
else
|
|
10743
|
+
else byPlace.set(key, [r]);
|
|
10725
10744
|
}
|
|
10726
10745
|
const out = [];
|
|
10727
|
-
for (const [
|
|
10728
|
-
const finished = done.has(
|
|
10729
|
-
list.sort(
|
|
10746
|
+
for (const [key, list] of byPlace) {
|
|
10747
|
+
const finished = done.has(key);
|
|
10748
|
+
list.sort(oldestFirst);
|
|
10730
10749
|
let joined = false;
|
|
10731
10750
|
let replies = [];
|
|
10732
10751
|
for (const m of list) {
|
|
@@ -10744,7 +10763,8 @@ function messageLines(data, personId, since) {
|
|
|
10744
10763
|
});
|
|
10745
10764
|
continue;
|
|
10746
10765
|
}
|
|
10747
|
-
|
|
10766
|
+
const toMe = m.replyToId != null && mine.has(m.replyToId);
|
|
10767
|
+
if (joined && !finished || toMe) replies.push(m);
|
|
10748
10768
|
}
|
|
10749
10769
|
const latest = replies.at(-1);
|
|
10750
10770
|
if (latest) out.push({
|
|
@@ -10755,11 +10775,26 @@ function messageLines(data, personId, since) {
|
|
|
10755
10775
|
}
|
|
10756
10776
|
return out;
|
|
10757
10777
|
}
|
|
10758
|
-
var MESSAGE_MAX, isMessage, messageText;
|
|
10778
|
+
var placeOf, placeKey, inPlace, placePath, MESSAGE_MAX, isMessage, oldestFirst, messagesOf, messageText;
|
|
10759
10779
|
var init_project_messages = __esmMin((() => {
|
|
10760
10780
|
init_work();
|
|
10781
|
+
placeOf = (r) => r.programId ? {
|
|
10782
|
+
kind: "program",
|
|
10783
|
+
id: r.programId
|
|
10784
|
+
} : {
|
|
10785
|
+
kind: "project",
|
|
10786
|
+
id: r.projectId
|
|
10787
|
+
};
|
|
10788
|
+
placeKey = (p) => `${p.kind}:${p.id}`;
|
|
10789
|
+
inPlace = (r, place) => isMessage(r) && (place.kind === "program" ? r.programId === place.id : !r.programId && r.projectId === place.id);
|
|
10790
|
+
placePath = (p) => p.kind === "program" ? `/programs/${p.id}?view=messages` : `/projects/${p.id}?view=messages`;
|
|
10761
10791
|
MESSAGE_MAX = 4e3;
|
|
10762
10792
|
isMessage = (r) => r.kind === "message";
|
|
10793
|
+
oldestFirst = (a, b) => a.createdAt.localeCompare(b.createdAt);
|
|
10794
|
+
messagesOf = (reflections, projectId) => messagesIn(reflections, {
|
|
10795
|
+
kind: "project",
|
|
10796
|
+
id: projectId
|
|
10797
|
+
});
|
|
10763
10798
|
messageText = (body) => body.trim().slice(0, MESSAGE_MAX);
|
|
10764
10799
|
}));
|
|
10765
10800
|
|
|
@@ -11112,14 +11147,15 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
11112
11147
|
});
|
|
11113
11148
|
}
|
|
11114
11149
|
for (const line of messageLines(data, personId, since)) {
|
|
11115
|
-
const
|
|
11116
|
-
|
|
11150
|
+
const place = placeOf(line.message);
|
|
11151
|
+
const holder = place.kind === "program" ? data.programs.find((p) => p.id === place.id) : data.projects.find((p) => p.id === place.id);
|
|
11152
|
+
if (!holder) continue;
|
|
11117
11153
|
const names = line.authorIds.map((id) => {
|
|
11118
11154
|
const who = data.people.find((p) => p.id === id);
|
|
11119
11155
|
return who ? firstName$3(who.name) : "Someone";
|
|
11120
11156
|
});
|
|
11121
11157
|
const snippet = excerpt(line.message.body);
|
|
11122
|
-
const where = `in ${
|
|
11158
|
+
const where = `in ${holder.name}’s messages`;
|
|
11123
11159
|
pings.push({
|
|
11124
11160
|
id: `message-${line.kind}:${line.message.id}`,
|
|
11125
11161
|
kind: line.kind,
|
|
@@ -11127,7 +11163,7 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
11127
11163
|
detail: snippet ? `${where} — ${snippet}` : where,
|
|
11128
11164
|
at: line.message.createdAt,
|
|
11129
11165
|
personId: line.message.authorId,
|
|
11130
|
-
href:
|
|
11166
|
+
href: placePath(place)
|
|
11131
11167
|
});
|
|
11132
11168
|
}
|
|
11133
11169
|
for (const task of data.tasks) {
|
|
@@ -22339,6 +22375,7 @@ function approvalsWaitingOn(tasks, personId) {
|
|
|
22339
22375
|
// src/lib/mcp/tools.ts
|
|
22340
22376
|
init_project_messages();
|
|
22341
22377
|
init_subtasks();
|
|
22378
|
+
init_blockers();
|
|
22342
22379
|
init_thanks();
|
|
22343
22380
|
init_task_keys();
|
|
22344
22381
|
init_brief();
|
|
@@ -22366,6 +22403,7 @@ init_task_people();
|
|
|
22366
22403
|
init_fog();
|
|
22367
22404
|
init_passage();
|
|
22368
22405
|
init_board();
|
|
22406
|
+
init_swatches();
|
|
22369
22407
|
init_holidays();
|
|
22370
22408
|
init_soundings();
|
|
22371
22409
|
/** A task's logged time for get_task: the total and the latest entries. */
|
|
@@ -22645,15 +22683,57 @@ function overdue(t, today) {
|
|
|
22645
22683
|
const due = dayOf(t.dueDate);
|
|
22646
22684
|
return isOpen(t) && due != null && due < today;
|
|
22647
22685
|
}
|
|
22648
|
-
|
|
22686
|
+
/**
|
|
22687
|
+
* The board's own name for a task's column when its project renamed it
|
|
22688
|
+
* ("In progress" for doing), so a status reads as the board says it.
|
|
22689
|
+
*/
|
|
22690
|
+
function columnName(data, t) {
|
|
22691
|
+
const label = boardConfigOf(data, t.projectId).columns.find((c) => c.status === t.status)?.label.trim();
|
|
22692
|
+
return label && label !== TASK_STATUS_LABEL[t.status] ? label : null;
|
|
22693
|
+
}
|
|
22694
|
+
/**
|
|
22695
|
+
* A milestone as agents see it: its program, due day, whether it's reached or
|
|
22696
|
+
* late, the tasks tied to it, its sign-off and the projects it waits on.
|
|
22697
|
+
*/
|
|
22698
|
+
function milestoneOut(data, m, today, progress = milestoneProgress(data)) {
|
|
22699
|
+
const people = byId(data.people);
|
|
22700
|
+
const projects = byId(data.projects);
|
|
22701
|
+
const p = progress.get(m.id);
|
|
22702
|
+
const due = dayOf(m.dueDate);
|
|
22703
|
+
const waits = blockerIds(m).map((id) => projects.get(id)).filter((x) => Boolean(x));
|
|
22704
|
+
return {
|
|
22705
|
+
id: m.id,
|
|
22706
|
+
name: m.name,
|
|
22707
|
+
program: data.programs.find((x) => x.id === m.programId)?.name ?? null,
|
|
22708
|
+
due,
|
|
22709
|
+
reached: m.done,
|
|
22710
|
+
...!m.done && due && due < today ? { late: true } : {},
|
|
22711
|
+
...p ? { tasks: {
|
|
22712
|
+
done: p.done,
|
|
22713
|
+
total: p.total
|
|
22714
|
+
} } : {},
|
|
22715
|
+
...m.approverId ? { signOff: {
|
|
22716
|
+
by: nameOf$2(people, m.approverId),
|
|
22717
|
+
status: m.approvalStatus ?? "pending",
|
|
22718
|
+
...m.approvalNote ? { note: m.approvalNote } : {}
|
|
22719
|
+
} } : {},
|
|
22720
|
+
...waits.length ? { waitsOn: waits.map((x) => ({
|
|
22721
|
+
project: x.name,
|
|
22722
|
+
done: x.health === "done"
|
|
22723
|
+
})) } : {}
|
|
22724
|
+
};
|
|
22725
|
+
}
|
|
22726
|
+
function taskLine(t, people, projects, all, data) {
|
|
22649
22727
|
const waitingOn = blockersOf(t, all).filter(isOpen).length;
|
|
22650
22728
|
const project = projects.get(t.projectId);
|
|
22651
22729
|
const key = taskKey(t, project);
|
|
22730
|
+
const column = data ? columnName(data, t) : null;
|
|
22652
22731
|
return {
|
|
22653
22732
|
id: t.id,
|
|
22654
22733
|
...key ? { key } : {},
|
|
22655
22734
|
title: t.title,
|
|
22656
22735
|
status: t.status,
|
|
22736
|
+
...column ? { column } : {},
|
|
22657
22737
|
position: boardPositions(all).get(t.id) ?? null,
|
|
22658
22738
|
priority: t.priority,
|
|
22659
22739
|
due: dayOf(t.dueDate),
|
|
@@ -22703,10 +22783,10 @@ function weekOut(data, personId, monday) {
|
|
|
22703
22783
|
tasks: d.items.map((item) => ({
|
|
22704
22784
|
placement: item.id,
|
|
22705
22785
|
hours: item.hours,
|
|
22706
|
-
...taskLine(item.task, people, projects, data.tasks)
|
|
22786
|
+
...taskLine(item.task, people, projects, data.tasks, data)
|
|
22707
22787
|
}))
|
|
22708
22788
|
})),
|
|
22709
|
-
unplanned: plan.unplanned.slice(0, 200).map((t) => taskLine(t, people, projects, data.tasks)),
|
|
22789
|
+
unplanned: plan.unplanned.slice(0, 200).map((t) => taskLine(t, people, projects, data.tasks, data)),
|
|
22710
22790
|
unplannedCount: plan.unplanned.length,
|
|
22711
22791
|
inReviewCount: plan.inReview.length,
|
|
22712
22792
|
review: {
|
|
@@ -22715,8 +22795,8 @@ function weekOut(data, personId, monday) {
|
|
|
22715
22795
|
done: review.doneCount,
|
|
22716
22796
|
plannedHours: review.plannedHours,
|
|
22717
22797
|
doneHours: review.doneHours,
|
|
22718
|
-
stillOpen: review.planned.filter((p) => !p.done).map((p) => taskLine(p.task, people, projects, data.tasks)),
|
|
22719
|
-
alsoDone: review.alsoDone.map((t) => taskLine(t, people, projects, data.tasks))
|
|
22798
|
+
stillOpen: review.planned.filter((p) => !p.done).map((p) => taskLine(p.task, people, projects, data.tasks, data)),
|
|
22799
|
+
alsoDone: review.alsoDone.map((t) => taskLine(t, people, projects, data.tasks, data))
|
|
22720
22800
|
}
|
|
22721
22801
|
};
|
|
22722
22802
|
}
|
|
@@ -22891,11 +22971,13 @@ const HARBOR_TOOLS = [
|
|
|
22891
22971
|
id: pg.id,
|
|
22892
22972
|
name: pg.name,
|
|
22893
22973
|
health: pg.health,
|
|
22974
|
+
starts: dayOf(pg.startDate),
|
|
22894
22975
|
ends: dayOf(pg.endDate),
|
|
22895
22976
|
projects: (projectsByProgram.get(pg.id) ?? []).map((pj) => ({
|
|
22896
22977
|
id: pj.id,
|
|
22897
22978
|
name: pj.name,
|
|
22898
22979
|
health: pj.health,
|
|
22980
|
+
starts: dayOf(pj.startDate),
|
|
22899
22981
|
ends: dayOf(pj.endDate),
|
|
22900
22982
|
openTasks: openByProject.get(pj.id)?.open ?? 0,
|
|
22901
22983
|
lateTasks: openByProject.get(pj.id)?.late ?? 0
|
|
@@ -23007,7 +23089,7 @@ const HARBOR_TOOLS = [
|
|
|
23007
23089
|
const tasks = data.tasks.filter((t) => t.projectId === p.id);
|
|
23008
23090
|
const byStatus = Object.fromEntries(STATUSES.map((s) => [s, 0]));
|
|
23009
23091
|
for (const t of tasks) byStatus[t.status] += 1;
|
|
23010
|
-
const late = tasks.filter((t) => overdue(t, today)).sort((a, b) => (a.dueDate ?? "").localeCompare(b.dueDate ?? "")).slice(0, 10).map((t) => taskLine(t, people, projects, data.tasks));
|
|
23092
|
+
const late = tasks.filter((t) => overdue(t, today)).sort((a, b) => (a.dueDate ?? "").localeCompare(b.dueDate ?? "")).slice(0, 10).map((t) => taskLine(t, people, projects, data.tasks, data));
|
|
23011
23093
|
const waitsOn = (p.blockedByIds ?? (p.blockedById ? [p.blockedById] : [])).map((id) => projects.get(id)).filter((x) => Boolean(x)).map((x) => ({
|
|
23012
23094
|
id: x.id,
|
|
23013
23095
|
name: x.name,
|
|
@@ -23109,7 +23191,7 @@ const HARBOR_TOOLS = [
|
|
|
23109
23191
|
return {
|
|
23110
23192
|
...doneMatches ? { doneMatches } : {},
|
|
23111
23193
|
total: list.length,
|
|
23112
|
-
tasks: list.slice(input.offset, input.offset + input.limit).map((t) => taskLine(t, people, projects, data.tasks))
|
|
23194
|
+
tasks: list.slice(input.offset, input.offset + input.limit).map((t) => taskLine(t, people, projects, data.tasks, data))
|
|
23113
23195
|
};
|
|
23114
23196
|
}
|
|
23115
23197
|
}),
|
|
@@ -23151,6 +23233,7 @@ const HARBOR_TOOLS = [
|
|
|
23151
23233
|
title: t.title,
|
|
23152
23234
|
description: t.description,
|
|
23153
23235
|
status: t.status,
|
|
23236
|
+
...((c) => c ? { column: c } : {})(columnName(data, t)),
|
|
23154
23237
|
position: boardPositions(data.tasks).get(t.id) ?? null,
|
|
23155
23238
|
priority: t.priority,
|
|
23156
23239
|
due: dayOf(t.dueDate),
|
|
@@ -23275,8 +23358,8 @@ const HARBOR_TOOLS = [
|
|
|
23275
23358
|
you: mine.name,
|
|
23276
23359
|
openTasks: open.length,
|
|
23277
23360
|
late: open.filter((t) => overdue(t, today)).length,
|
|
23278
|
-
tasks: open.slice(0, input.limit).map((t) => taskLine(t, people, projects, data.tasks)),
|
|
23279
|
-
alsoOn: tasksAlsoOn(data, mine.id).sort((a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999")).slice(0, input.limit).map((t) => taskLine(t, people, projects, data.tasks)),
|
|
23361
|
+
tasks: open.slice(0, input.limit).map((t) => taskLine(t, people, projects, data.tasks, data)),
|
|
23362
|
+
alsoOn: tasksAlsoOn(data, mine.id).sort((a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999")).slice(0, input.limit).map((t) => taskLine(t, people, projects, data.tasks, data)),
|
|
23280
23363
|
waitingOnYou: waitingOnYou.slice(0, 20),
|
|
23281
23364
|
raid,
|
|
23282
23365
|
soundingsToAnswer: soundingsWaitingOn(data, mine.id, ctx.now).map((t) => ({
|
|
@@ -23741,6 +23824,30 @@ const HARBOR_TOOLS = [
|
|
|
23741
23824
|
};
|
|
23742
23825
|
}
|
|
23743
23826
|
}),
|
|
23827
|
+
tool({
|
|
23828
|
+
name: "list_milestones",
|
|
23829
|
+
title: "Milestones",
|
|
23830
|
+
description: "A program's milestones (or every program's, or the program of one project), soonest first: each with its due day, whether it's reached or late, how many of the tasks tied to it are done, its sign-off (who signs it off, and pending, approved or changes_requested with their note) and the projects it waits on. Reached ones only with done. set_milestone adds or changes one." + DATA_NOTE,
|
|
23831
|
+
input: object({
|
|
23832
|
+
harbor: harborArg,
|
|
23833
|
+
program: string().min(1).max(200).optional().describe("Program id or name."),
|
|
23834
|
+
project: string().min(1).max(200).optional().describe("Project id or name: its program's milestones."),
|
|
23835
|
+
done: boolean().default(false).describe("Include milestones already reached.")
|
|
23836
|
+
}),
|
|
23837
|
+
run: (ctx, input) => {
|
|
23838
|
+
const { data } = ctx;
|
|
23839
|
+
const today = todayISO(ctx.now);
|
|
23840
|
+
const programId = input.project ? findOne(data.projects, input.project, "project").programId : input.program ? findOne(data.programs, input.program, "program").id : null;
|
|
23841
|
+
const all = data.milestones.filter((m) => !programId || m.programId === programId).sort((a, b) => (a.dueDate ?? "").localeCompare(b.dueDate ?? "") || a.name.localeCompare(b.name));
|
|
23842
|
+
const shown = input.done ? all : all.filter((m) => !m.done);
|
|
23843
|
+
const progress = milestoneProgress(data);
|
|
23844
|
+
return {
|
|
23845
|
+
...programId ? { program: data.programs.find((p) => p.id === programId)?.name } : {},
|
|
23846
|
+
milestones: shown.map((m) => milestoneOut(data, m, today, progress)),
|
|
23847
|
+
...shown.length < all.length ? { reachedHidden: all.length - shown.length } : {}
|
|
23848
|
+
};
|
|
23849
|
+
}
|
|
23850
|
+
}),
|
|
23744
23851
|
tool({
|
|
23745
23852
|
name: "list_messages",
|
|
23746
23853
|
title: "A project's messages",
|
|
@@ -24195,7 +24302,7 @@ const HARBOR_TOOLS = [
|
|
|
24195
24302
|
inFog: fog.length,
|
|
24196
24303
|
fogAfterDays: 21,
|
|
24197
24304
|
tasks: fog.slice(input.offset, input.offset + input.limit).map((f) => ({
|
|
24198
|
-
...taskLine(f.task, people, projects, data.tasks),
|
|
24305
|
+
...taskLine(f.task, people, projects, data.tasks, data),
|
|
24199
24306
|
untouchedDays: f.days,
|
|
24200
24307
|
untouched: untouchedText(f.days),
|
|
24201
24308
|
thick: f.level === "thick",
|
|
@@ -25033,6 +25140,7 @@ function sessionSlice({ set, get, write }) {
|
|
|
25033
25140
|
}
|
|
25034
25141
|
};
|
|
25035
25142
|
}
|
|
25143
|
+
var ACTIVITY_KEPT;
|
|
25036
25144
|
var init_session = __esmMin((() => {
|
|
25037
25145
|
init_board();
|
|
25038
25146
|
init_derived_health();
|
|
@@ -25044,6 +25152,7 @@ var init_session = __esmMin((() => {
|
|
|
25044
25152
|
init_view_only();
|
|
25045
25153
|
init_fog();
|
|
25046
25154
|
init_format();
|
|
25155
|
+
ACTIVITY_KEPT = 40;
|
|
25047
25156
|
}));
|
|
25048
25157
|
|
|
25049
25158
|
// src/lib/first-week.ts
|
|
@@ -26506,6 +26615,28 @@ var init_raid_history = __esmMin((() => {}));
|
|
|
26506
26615
|
|
|
26507
26616
|
// src/lib/slices/milestones-raid.ts
|
|
26508
26617
|
function milestonesRaidSlice({ get, write }) {
|
|
26618
|
+
/** A new message in one conversation, or an answer under one of its messages. */
|
|
26619
|
+
const post = (place, name, body, replyToId) => {
|
|
26620
|
+
const s = get();
|
|
26621
|
+
if (messageProblem(body)) return null;
|
|
26622
|
+
const root = replyRoot(s.reflections, place, replyToId);
|
|
26623
|
+
const me = s.currentUserId;
|
|
26624
|
+
const text = messageText(body);
|
|
26625
|
+
const row = {
|
|
26626
|
+
id: uid("msg"),
|
|
26627
|
+
projectId: place.kind === "project" ? place.id : "",
|
|
26628
|
+
...place.kind === "program" ? { programId: place.id } : {},
|
|
26629
|
+
kind: "message",
|
|
26630
|
+
authorId: me,
|
|
26631
|
+
body: text,
|
|
26632
|
+
createdAt: stamp(),
|
|
26633
|
+
mentionedIds: messageMentions(text, s.people, me),
|
|
26634
|
+
...root ? { replyToId: root } : {}
|
|
26635
|
+
};
|
|
26636
|
+
write({ reflections: [...s.reflections ?? [], row] });
|
|
26637
|
+
get().log(`${root ? "replied" : "wrote"} in ${name}’s messages`, placePath(place));
|
|
26638
|
+
return row.id;
|
|
26639
|
+
};
|
|
26509
26640
|
return {
|
|
26510
26641
|
toggleMilestone: (id) => {
|
|
26511
26642
|
if (get().milestones.find((m) => m.id === id)?.approverId) return;
|
|
@@ -26811,25 +26942,21 @@ function milestonesRaidSlice({ get, write }) {
|
|
|
26811
26942
|
write({ reflections: [...s.reflections ?? [], row] });
|
|
26812
26943
|
return row.id;
|
|
26813
26944
|
},
|
|
26814
|
-
postMessage: (projectId, body) => {
|
|
26815
|
-
const
|
|
26816
|
-
if (messageProblem(body)) return null;
|
|
26817
|
-
const project = s.projects.find((p) => p.id === projectId);
|
|
26945
|
+
postMessage: (projectId, body, replyToId) => {
|
|
26946
|
+
const project = get().projects.find((p) => p.id === projectId);
|
|
26818
26947
|
if (!project) return null;
|
|
26819
|
-
|
|
26820
|
-
|
|
26821
|
-
|
|
26822
|
-
|
|
26823
|
-
|
|
26824
|
-
|
|
26825
|
-
|
|
26826
|
-
|
|
26827
|
-
|
|
26828
|
-
|
|
26829
|
-
|
|
26830
|
-
|
|
26831
|
-
get().log(`wrote in ${project.name}’s messages`, `/projects/${projectId}?view=messages`);
|
|
26832
|
-
return row.id;
|
|
26948
|
+
return post({
|
|
26949
|
+
kind: "project",
|
|
26950
|
+
id: projectId
|
|
26951
|
+
}, project.name, body, replyToId);
|
|
26952
|
+
},
|
|
26953
|
+
postProgramMessage: (programId, body, replyToId) => {
|
|
26954
|
+
const program = get().programs.find((p) => p.id === programId);
|
|
26955
|
+
if (!program) return null;
|
|
26956
|
+
return post({
|
|
26957
|
+
kind: "program",
|
|
26958
|
+
id: programId
|
|
26959
|
+
}, program.name, body, replyToId);
|
|
26833
26960
|
},
|
|
26834
26961
|
editMessage: (id, body) => {
|
|
26835
26962
|
const s = get();
|
|
@@ -28312,6 +28439,11 @@ var init_shore = __esmMin((() => {
|
|
|
28312
28439
|
kind: "notes",
|
|
28313
28440
|
label: "Notes for later, when they open",
|
|
28314
28441
|
hint: "A note someone left on one of its projects, posted the day it opens."
|
|
28442
|
+
},
|
|
28443
|
+
{
|
|
28444
|
+
kind: "messages",
|
|
28445
|
+
label: "Messages",
|
|
28446
|
+
hint: "Each new message and reply in the program’s messages and its projects’, with a link to answer in Harbor."
|
|
28315
28447
|
}
|
|
28316
28448
|
];
|
|
28317
28449
|
}));
|
|
@@ -28395,6 +28527,7 @@ function memoryHarbor(start, opts) {
|
|
|
28395
28527
|
forecast: false,
|
|
28396
28528
|
health: false,
|
|
28397
28529
|
notes: false,
|
|
28530
|
+
messages: false,
|
|
28398
28531
|
...row,
|
|
28399
28532
|
...ask.url ? { url } : {}
|
|
28400
28533
|
};
|
|
@@ -28409,6 +28542,7 @@ function memoryHarbor(start, opts) {
|
|
|
28409
28542
|
forecast: row.forecast,
|
|
28410
28543
|
health: row.health,
|
|
28411
28544
|
notes: row.notes,
|
|
28545
|
+
messages: row.messages,
|
|
28412
28546
|
failures: 0
|
|
28413
28547
|
} : null;
|
|
28414
28548
|
if (ask.test) {
|
|
@@ -34749,6 +34883,7 @@ function sinceText(iso, now) {
|
|
|
34749
34883
|
|
|
34750
34884
|
// src/tui/watch-run.ts
|
|
34751
34885
|
init_clock();
|
|
34886
|
+
init_session();
|
|
34752
34887
|
init_plain();
|
|
34753
34888
|
init_repo_sync();
|
|
34754
34889
|
init_store();
|
|
@@ -34815,7 +34950,18 @@ async function watchOn(opts) {
|
|
|
34815
34950
|
if (earlier.length === 0) out(dim(since ? `Nothing since ${since}.` : "Nothing logged yet."));
|
|
34816
34951
|
else out(dim(since ? `Since ${since}:` : "Lately:"));
|
|
34817
34952
|
}
|
|
34953
|
+
const log = store.getState().activity;
|
|
34954
|
+
const oldest = log.reduce((o, a) => !o || a.at < o ? a.at : o, null);
|
|
34955
|
+
const cut = oldest && log.length >= 40 && (!opts.since || opts.since < oldest) ? oldest : null;
|
|
34956
|
+
const capped = !opts.since && !project && log.length > earlier.length;
|
|
34957
|
+
if (opts.json) {
|
|
34958
|
+
if (cut) out(JSON.stringify({
|
|
34959
|
+
note: `Only the latest ${40} changes are kept, back to ${cut}; History on the website has every one.`,
|
|
34960
|
+
keptFrom: cut
|
|
34961
|
+
}));
|
|
34962
|
+
} else if (opts.since && cut) out(dim(`Only the latest ${40} changes are kept, back to ${wallClock(new Date(cut)).slice(0, 16).replace("T", " ")}; History on the website has every one.`));
|
|
34818
34963
|
for (const e of earlier) print(e.line, new Date(e.at), true);
|
|
34964
|
+
if (opts.once && capped && !opts.json) out(dim(`The latest ${earlier.length} of ${log.length}; --since TIME shows every one since then.`));
|
|
34819
34965
|
if (opts.once) return;
|
|
34820
34966
|
if (!opts.json && earlier.length > 0) out(dim("New changes print below as they land."));
|
|
34821
34967
|
const narrow = (d) => project ? {
|