@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 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` says what it would add first.
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 (set on the portfolio's page on the website), off track first, with each key result's progress. `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.
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
- /** This project's messages, oldest first. */
10691
- function messagesOf(reflections, projectId) {
10692
- return (reflections ?? []).filter((r) => r.projectId === projectId && isMessage(r)).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
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 project messages since `since`: each
10712
- * message that @mentions them, and for each project they've written in, the
10713
- * messages others posted after their last one (a mention among them is
10714
- * already its own line). A finished project's replies are left out, but a
10715
- * mention there still reaches you: a wrap-up "thanks @Dana" is meant to.
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 byProject = /* @__PURE__ */ new Map();
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
- const list = byProject.get(r.projectId);
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 byProject.set(r.projectId, [r]);
10743
+ else byPlace.set(key, [r]);
10725
10744
  }
10726
10745
  const out = [];
10727
- for (const [projectId, list] of byProject) {
10728
- const finished = done.has(projectId);
10729
- list.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
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
- if (joined && !finished) replies.push(m);
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 project = data.projects.find((p) => p.id === line.message.projectId);
11116
- if (!project) continue;
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 ${project.name}’s messages`;
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: `/projects/${project.id}?view=messages`
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
- function taskLine(t, people, projects, all) {
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 s = get();
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
- const me = s.currentUserId;
26820
- const text = messageText(body);
26821
- const row = {
26822
- id: uid("msg"),
26823
- projectId,
26824
- kind: "message",
26825
- authorId: me,
26826
- body: text,
26827
- createdAt: stamp(),
26828
- mentionedIds: messageMentions(text, s.people, me)
26829
- };
26830
- write({ reflections: [...s.reflections ?? [], row] });
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 ? {