@harborppm/cli 0.97.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 +387 -62
- package/lib/cli.mjs +581 -85
- package/lib/format.mjs +125 -4
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -3376,7 +3376,7 @@ var init_thanks = __esmMin((() => {
|
|
|
3376
3376
|
// src/lib/charter-lines.ts
|
|
3377
3377
|
/** Nothing written on any of the five lines. */
|
|
3378
3378
|
function charterEmpty(charter) {
|
|
3379
|
-
return !charter || CHARTER_FIELDS.every((f) => !clean(charter[f]));
|
|
3379
|
+
return !charter || CHARTER_FIELDS.every((f) => !clean$1(charter[f]));
|
|
3380
3380
|
}
|
|
3381
3381
|
/**
|
|
3382
3382
|
* The charter after a change to some of its lines: trimmed, and null once
|
|
@@ -3396,7 +3396,7 @@ function withCharter(current, patch, by, at) {
|
|
|
3396
3396
|
}
|
|
3397
3397
|
return charterEmpty(next) ? null : next;
|
|
3398
3398
|
}
|
|
3399
|
-
var CHARTER_FIELDS, clean;
|
|
3399
|
+
var CHARTER_FIELDS, clean$1;
|
|
3400
3400
|
var init_charter_lines = __esmMin((() => {
|
|
3401
3401
|
CHARTER_FIELDS = [
|
|
3402
3402
|
"why",
|
|
@@ -3405,7 +3405,7 @@ var init_charter_lines = __esmMin((() => {
|
|
|
3405
3405
|
"doneWhen",
|
|
3406
3406
|
"callsBy"
|
|
3407
3407
|
];
|
|
3408
|
-
clean = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
3408
|
+
clean$1 = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
3409
3409
|
}));
|
|
3410
3410
|
|
|
3411
3411
|
// src/lib/charter.ts
|
|
@@ -3434,7 +3434,7 @@ function charterLines(charter) {
|
|
|
3434
3434
|
return CHARTER_FIELDS.map((field) => ({
|
|
3435
3435
|
field,
|
|
3436
3436
|
label: CHARTER_LABEL[field],
|
|
3437
|
-
text: clean(charter[field])
|
|
3437
|
+
text: clean$1(charter[field])
|
|
3438
3438
|
})).filter((l) => l.text);
|
|
3439
3439
|
}
|
|
3440
3440
|
function charterFilled(charter) {
|
|
@@ -3502,7 +3502,7 @@ function charterCheck(charter, tasks) {
|
|
|
3502
3502
|
};
|
|
3503
3503
|
});
|
|
3504
3504
|
return {
|
|
3505
|
-
doneWhen: clean(charter.doneWhen) || null,
|
|
3505
|
+
doneWhen: clean$1(charter.doneWhen) || null,
|
|
3506
3506
|
delivered,
|
|
3507
3507
|
crept: outOfScope(charter, tasks),
|
|
3508
3508
|
outItems: charterItems(charter.outScope).length
|
|
@@ -3524,6 +3524,18 @@ function charterSentence(check) {
|
|
|
3524
3524
|
return `Against the charter: ${bits.join("; ")}.`;
|
|
3525
3525
|
}
|
|
3526
3526
|
|
|
3527
|
+
// src/lib/finished-on.ts
|
|
3528
|
+
/**
|
|
3529
|
+
* When the project was marked Done: its last move to Done in its health
|
|
3530
|
+
* history, else its last finished task, else its end date. Kept on its own so
|
|
3531
|
+
* the inbox can ask it without the voyage log's code.
|
|
3532
|
+
*/
|
|
3533
|
+
function finishedOn(project, tasks) {
|
|
3534
|
+
const marked = [...project.healthHistory ?? []].reverse().find((h) => h.health === "done");
|
|
3535
|
+
if (marked) return marked.at.slice(0, 10);
|
|
3536
|
+
return (tasks.map((t) => t.completedAt).filter((x) => Boolean(x)).sort().at(-1) ?? project.endDate).slice(0, 10);
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3527
3539
|
// src/lib/voyage-log.ts
|
|
3528
3540
|
init_date_fns();
|
|
3529
3541
|
init_blockers();
|
|
@@ -3531,12 +3543,6 @@ init_date_text();
|
|
|
3531
3543
|
init_premortem();
|
|
3532
3544
|
const day$8 = (iso) => parseIso(localDay(iso));
|
|
3533
3545
|
const between = (a, b) => differenceInCalendarDays(day$8(b), day$8(a));
|
|
3534
|
-
/** When the project was marked Done: its last move to Done in its health history. */
|
|
3535
|
-
function finishedOn(project, tasks) {
|
|
3536
|
-
const marked = [...project.healthHistory ?? []].reverse().find((h) => h.health === "done");
|
|
3537
|
-
if (marked) return marked.at.slice(0, 10);
|
|
3538
|
-
return (tasks.map((t) => t.completedAt).filter((x) => Boolean(x)).sort().at(-1) ?? project.endDate).slice(0, 10);
|
|
3539
|
-
}
|
|
3540
3546
|
function voyageLog(data, projectId) {
|
|
3541
3547
|
const project = data.projects.find((p) => p.id === projectId);
|
|
3542
3548
|
if (!project) return null;
|
|
@@ -5321,7 +5327,7 @@ function cascadeDelete(data, kind, id) {
|
|
|
5321
5327
|
taskIds: r.taskIds.filter((t) => !taskIds.has(t))
|
|
5322
5328
|
})),
|
|
5323
5329
|
spendEntries: (data.spendEntries ?? []).filter((e) => !projectIds.has(e.projectId)),
|
|
5324
|
-
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)))
|
|
5325
5331
|
},
|
|
5326
5332
|
counts: {
|
|
5327
5333
|
programs: programIds.size,
|
|
@@ -10560,10 +10566,144 @@ function HistoryView({ active, width, height, go }) {
|
|
|
10560
10566
|
});
|
|
10561
10567
|
}
|
|
10562
10568
|
|
|
10569
|
+
// src/lib/landfall-lines.ts
|
|
10570
|
+
function isLandfallDays(n) {
|
|
10571
|
+
return LANDFALL_DAYS.includes(n);
|
|
10572
|
+
}
|
|
10573
|
+
/** Why a landfall can't be saved as given, in words for the person; null when it can. */
|
|
10574
|
+
function landfallProblem(input) {
|
|
10575
|
+
const expect = clean(input.expect);
|
|
10576
|
+
if (!expect) return "Say what landfall should look like.";
|
|
10577
|
+
if (expect.length > 300) return `Keep it under ${300} characters.`;
|
|
10578
|
+
if (!isLandfallDays(input.afterDays)) return "Pick 30, 90 or 180 days.";
|
|
10579
|
+
if (clean(input.measure).length > 60) return `Name the number in under ${60} characters.`;
|
|
10580
|
+
return null;
|
|
10581
|
+
}
|
|
10582
|
+
/** Why an answer can't be saved as given; null when it can. */
|
|
10583
|
+
function landfallAnswerProblem(input) {
|
|
10584
|
+
if (!LANDFALL_RESULTS.includes(input.result)) return "Say yes, partly or no.";
|
|
10585
|
+
if (input.value != null && (typeof input.value !== "number" || !Number.isFinite(input.value))) return "The number has to be a number.";
|
|
10586
|
+
if (typeof input.value === "number" && Math.abs(input.value) > 0xe8d4a51000) return "That number is too big.";
|
|
10587
|
+
if (clean(input.note).length > 500) return `Keep the note under ${500} characters.`;
|
|
10588
|
+
return null;
|
|
10589
|
+
}
|
|
10590
|
+
/**
|
|
10591
|
+
* The landfall after someone sets or changes what it looks like. An answer
|
|
10592
|
+
* already given stays: changing the words after the fact doesn't unask it.
|
|
10593
|
+
* Throws on bad input, so callers check `landfallProblem` first.
|
|
10594
|
+
*/
|
|
10595
|
+
function withLandfall(current, input, by, at) {
|
|
10596
|
+
const problem = landfallProblem(input);
|
|
10597
|
+
if (problem) throw new Error(problem);
|
|
10598
|
+
const measure = clean(input.measure);
|
|
10599
|
+
return {
|
|
10600
|
+
...current ?? {},
|
|
10601
|
+
expect: clean(input.expect),
|
|
10602
|
+
afterDays: input.afterDays,
|
|
10603
|
+
measure: measure || null,
|
|
10604
|
+
setAt: current?.setAt ?? at,
|
|
10605
|
+
setById: current?.setById ?? by
|
|
10606
|
+
};
|
|
10607
|
+
}
|
|
10608
|
+
/** The landfall with an answer, or with the answer taken back (null) so it asks again. */
|
|
10609
|
+
function withLandfallAnswer(current, answer, by, at) {
|
|
10610
|
+
if (!answer) {
|
|
10611
|
+
const { result: _r, value: _v, note: _n, answeredAt: _a, answeredById: _b, ...rest } = current;
|
|
10612
|
+
return rest;
|
|
10613
|
+
}
|
|
10614
|
+
const problem = landfallAnswerProblem(answer);
|
|
10615
|
+
if (problem) throw new Error(problem);
|
|
10616
|
+
const note = clean(answer.note);
|
|
10617
|
+
return {
|
|
10618
|
+
...current,
|
|
10619
|
+
result: answer.result,
|
|
10620
|
+
value: answer.value ?? null,
|
|
10621
|
+
note: note || null,
|
|
10622
|
+
answeredAt: at,
|
|
10623
|
+
answeredById: by
|
|
10624
|
+
};
|
|
10625
|
+
}
|
|
10626
|
+
var LANDFALL_DAYS, LANDFALL_EXPECT_MAX, LANDFALL_MEASURE_MAX, LANDFALL_NOTE_MAX, LANDFALL_RESULTS, clean;
|
|
10627
|
+
var init_landfall_lines = __esmMin((() => {
|
|
10628
|
+
LANDFALL_DAYS = [
|
|
10629
|
+
30,
|
|
10630
|
+
90,
|
|
10631
|
+
180
|
|
10632
|
+
];
|
|
10633
|
+
LANDFALL_EXPECT_MAX = 300;
|
|
10634
|
+
LANDFALL_MEASURE_MAX = 60;
|
|
10635
|
+
LANDFALL_NOTE_MAX = 500;
|
|
10636
|
+
LANDFALL_RESULTS = [
|
|
10637
|
+
"yes",
|
|
10638
|
+
"partly",
|
|
10639
|
+
"no"
|
|
10640
|
+
];
|
|
10641
|
+
clean = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
10642
|
+
}));
|
|
10643
|
+
|
|
10644
|
+
// src/lib/landfall.ts
|
|
10645
|
+
init_date_fns();
|
|
10646
|
+
init_date_text();
|
|
10647
|
+
function plusDays(iso, days) {
|
|
10648
|
+
return addDays(parseIso(iso.slice(0, 10)), days).toISOString().slice(0, 10);
|
|
10649
|
+
}
|
|
10650
|
+
/** The day to look: that many days after the project was marked Done. */
|
|
10651
|
+
function landfallCheckOn(project, tasks) {
|
|
10652
|
+
if (!project.landfall) return null;
|
|
10653
|
+
return plusDays(finishedOn(project, tasks), project.landfall.afterDays);
|
|
10654
|
+
}
|
|
10655
|
+
function landfallState(project, tasks, today) {
|
|
10656
|
+
const lf = project.landfall;
|
|
10657
|
+
if (lf?.result) return "answered";
|
|
10658
|
+
if (project.health !== "done") return "sailing";
|
|
10659
|
+
if (!lf) return "unset";
|
|
10660
|
+
const on = landfallCheckOn(project, tasks);
|
|
10661
|
+
return on && on <= today ? "due" : "waiting";
|
|
10662
|
+
}
|
|
10663
|
+
/** Who is asked: the project's owner, else (their person gone) the program's. */
|
|
10664
|
+
function landfallAskee(data, project) {
|
|
10665
|
+
const here = (id) => id ? data.people.find((p) => p.id === id) : void 0;
|
|
10666
|
+
return here(project.ownerId) ?? here(data.programs.find((p) => p.id === project.programId)?.ownerId);
|
|
10667
|
+
}
|
|
10668
|
+
/** A project's landfall as agents and the CLI's --json read it; null while there's none. */
|
|
10669
|
+
function landfallJson(project, tasks, today, people) {
|
|
10670
|
+
const lf = project.landfall;
|
|
10671
|
+
if (!lf) return null;
|
|
10672
|
+
const answered = Boolean(lf.result);
|
|
10673
|
+
return {
|
|
10674
|
+
expect: lf.expect,
|
|
10675
|
+
afterDays: lf.afterDays,
|
|
10676
|
+
measure: lf.measure ?? null,
|
|
10677
|
+
state: landfallState(project, tasks, today),
|
|
10678
|
+
checkOn: project.health === "done" ? landfallCheckOn(project, tasks) : null,
|
|
10679
|
+
result: lf.result ?? null,
|
|
10680
|
+
...answered ? {
|
|
10681
|
+
value: lf.value ?? null,
|
|
10682
|
+
note: lf.note ?? null,
|
|
10683
|
+
answeredOn: lf.answeredAt?.slice(0, 10) ?? null,
|
|
10684
|
+
answeredBy: people.find((p) => p.id === lf.answeredById)?.name ?? null
|
|
10685
|
+
} : {}
|
|
10686
|
+
};
|
|
10687
|
+
}
|
|
10688
|
+
|
|
10563
10689
|
// src/lib/project-messages.ts
|
|
10564
|
-
/**
|
|
10565
|
-
function
|
|
10566
|
-
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;
|
|
10567
10707
|
}
|
|
10568
10708
|
/** Why a message can't be posted, or null. */
|
|
10569
10709
|
function messageProblem(body) {
|
|
@@ -10582,25 +10722,30 @@ function mayChangeMessage(message, personId, role, action) {
|
|
|
10582
10722
|
return action === "delete" && role === "owner";
|
|
10583
10723
|
}
|
|
10584
10724
|
/**
|
|
10585
|
-
* What a person's Inbox says about
|
|
10586
|
-
*
|
|
10587
|
-
*
|
|
10588
|
-
*
|
|
10589
|
-
*
|
|
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.
|
|
10590
10732
|
*/
|
|
10591
10733
|
function messageLines(data, personId, since) {
|
|
10592
|
-
const done = new Set(data.projects.filter((p) => p.health === "done").map((p) => p.id));
|
|
10593
|
-
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();
|
|
10594
10737
|
for (const r of data.reflections ?? []) {
|
|
10595
10738
|
if (!isMessage(r)) continue;
|
|
10596
|
-
|
|
10739
|
+
if (r.authorId === personId) mine.add(r.id);
|
|
10740
|
+
const key = placeKey(placeOf(r));
|
|
10741
|
+
const list = byPlace.get(key);
|
|
10597
10742
|
if (list) list.push(r);
|
|
10598
|
-
else
|
|
10743
|
+
else byPlace.set(key, [r]);
|
|
10599
10744
|
}
|
|
10600
10745
|
const out = [];
|
|
10601
|
-
for (const [
|
|
10602
|
-
const finished = done.has(
|
|
10603
|
-
list.sort(
|
|
10746
|
+
for (const [key, list] of byPlace) {
|
|
10747
|
+
const finished = done.has(key);
|
|
10748
|
+
list.sort(oldestFirst);
|
|
10604
10749
|
let joined = false;
|
|
10605
10750
|
let replies = [];
|
|
10606
10751
|
for (const m of list) {
|
|
@@ -10618,7 +10763,8 @@ function messageLines(data, personId, since) {
|
|
|
10618
10763
|
});
|
|
10619
10764
|
continue;
|
|
10620
10765
|
}
|
|
10621
|
-
|
|
10766
|
+
const toMe = m.replyToId != null && mine.has(m.replyToId);
|
|
10767
|
+
if (joined && !finished || toMe) replies.push(m);
|
|
10622
10768
|
}
|
|
10623
10769
|
const latest = replies.at(-1);
|
|
10624
10770
|
if (latest) out.push({
|
|
@@ -10629,11 +10775,26 @@ function messageLines(data, personId, since) {
|
|
|
10629
10775
|
}
|
|
10630
10776
|
return out;
|
|
10631
10777
|
}
|
|
10632
|
-
var MESSAGE_MAX, isMessage, messageText;
|
|
10778
|
+
var placeOf, placeKey, inPlace, placePath, MESSAGE_MAX, isMessage, oldestFirst, messagesOf, messageText;
|
|
10633
10779
|
var init_project_messages = __esmMin((() => {
|
|
10634
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`;
|
|
10635
10791
|
MESSAGE_MAX = 4e3;
|
|
10636
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
|
+
});
|
|
10637
10798
|
messageText = (body) => body.trim().slice(0, MESSAGE_MAX);
|
|
10638
10799
|
}));
|
|
10639
10800
|
|
|
@@ -10682,6 +10843,7 @@ const KIND_RANK = {
|
|
|
10682
10843
|
waiting: 0,
|
|
10683
10844
|
approval: 1,
|
|
10684
10845
|
review: 2,
|
|
10846
|
+
landfall: 2.5,
|
|
10685
10847
|
sounding: 3,
|
|
10686
10848
|
assigned: 4,
|
|
10687
10849
|
idle: 5,
|
|
@@ -10694,6 +10856,7 @@ const PING_KIND_LABEL = {
|
|
|
10694
10856
|
waiting: "Waiting on you",
|
|
10695
10857
|
approval: "Sign-offs",
|
|
10696
10858
|
review: "Reviews due",
|
|
10859
|
+
landfall: "Landfall",
|
|
10697
10860
|
sounding: "Soundings",
|
|
10698
10861
|
assigned: "Assigned to you",
|
|
10699
10862
|
idle: "Quiet blockers",
|
|
@@ -10827,6 +10990,41 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
10827
10990
|
personId: null
|
|
10828
10991
|
});
|
|
10829
10992
|
}
|
|
10993
|
+
const askSince = todayISO(subDays(now, 14));
|
|
10994
|
+
for (const project of data.projects) {
|
|
10995
|
+
if (project.health !== "done" && !project.landfall?.result) continue;
|
|
10996
|
+
if (landfallAskee(data, project)?.id !== personId) continue;
|
|
10997
|
+
const tasks = tasksOf(data.tasks, project.id);
|
|
10998
|
+
const state = landfallState(project, tasks, today);
|
|
10999
|
+
if (state === "unset") {
|
|
11000
|
+
const finished = finishedOn(project, tasks);
|
|
11001
|
+
if (finished < askSince) continue;
|
|
11002
|
+
pings.push({
|
|
11003
|
+
id: `landfall-set:${project.id}:${finished}`,
|
|
11004
|
+
kind: "landfall",
|
|
11005
|
+
title: `${project.name} is done. What should landfall look like?`,
|
|
11006
|
+
detail: "Say what should be true in 30, 90 or 180 days, and Harbor asks you then.",
|
|
11007
|
+
at: finished,
|
|
11008
|
+
projectId: project.id,
|
|
11009
|
+
landfall: "set",
|
|
11010
|
+
personId: null,
|
|
11011
|
+
href: `/projects/${project.id}?view=log`
|
|
11012
|
+
});
|
|
11013
|
+
} else if (state === "due") {
|
|
11014
|
+
const on = landfallCheckOn(project, tasks);
|
|
11015
|
+
pings.push({
|
|
11016
|
+
id: `landfall:${project.id}:${on}`,
|
|
11017
|
+
kind: "landfall",
|
|
11018
|
+
title: `Did ${project.name} reach landfall?`,
|
|
11019
|
+
detail: `“${project.landfall.expect}”`,
|
|
11020
|
+
at: on,
|
|
11021
|
+
projectId: project.id,
|
|
11022
|
+
landfall: "check",
|
|
11023
|
+
personId: null,
|
|
11024
|
+
href: `/projects/${project.id}?view=log`
|
|
11025
|
+
});
|
|
11026
|
+
}
|
|
11027
|
+
}
|
|
10830
11028
|
for (const constraint of constraintsOwnedBy(data, personId)) {
|
|
10831
11029
|
if (!constraintIdle(data, constraint)) continue;
|
|
10832
11030
|
const program = data.programs.find((p) => p.id === constraint.programId);
|
|
@@ -10949,14 +11147,15 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
10949
11147
|
});
|
|
10950
11148
|
}
|
|
10951
11149
|
for (const line of messageLines(data, personId, since)) {
|
|
10952
|
-
const
|
|
10953
|
-
|
|
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;
|
|
10954
11153
|
const names = line.authorIds.map((id) => {
|
|
10955
11154
|
const who = data.people.find((p) => p.id === id);
|
|
10956
11155
|
return who ? firstName$3(who.name) : "Someone";
|
|
10957
11156
|
});
|
|
10958
11157
|
const snippet = excerpt(line.message.body);
|
|
10959
|
-
const where = `in ${
|
|
11158
|
+
const where = `in ${holder.name}’s messages`;
|
|
10960
11159
|
pings.push({
|
|
10961
11160
|
id: `message-${line.kind}:${line.message.id}`,
|
|
10962
11161
|
kind: line.kind,
|
|
@@ -10964,7 +11163,7 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
10964
11163
|
detail: snippet ? `${where} — ${snippet}` : where,
|
|
10965
11164
|
at: line.message.createdAt,
|
|
10966
11165
|
personId: line.message.authorId,
|
|
10967
|
-
href:
|
|
11166
|
+
href: placePath(place)
|
|
10968
11167
|
});
|
|
10969
11168
|
}
|
|
10970
11169
|
for (const task of data.tasks) {
|
|
@@ -22176,6 +22375,7 @@ function approvalsWaitingOn(tasks, personId) {
|
|
|
22176
22375
|
// src/lib/mcp/tools.ts
|
|
22177
22376
|
init_project_messages();
|
|
22178
22377
|
init_subtasks();
|
|
22378
|
+
init_blockers();
|
|
22179
22379
|
init_thanks();
|
|
22180
22380
|
init_task_keys();
|
|
22181
22381
|
init_brief();
|
|
@@ -22203,6 +22403,7 @@ init_task_people();
|
|
|
22203
22403
|
init_fog();
|
|
22204
22404
|
init_passage();
|
|
22205
22405
|
init_board();
|
|
22406
|
+
init_swatches();
|
|
22206
22407
|
init_holidays();
|
|
22207
22408
|
init_soundings();
|
|
22208
22409
|
/** A task's logged time for get_task: the total and the latest entries. */
|
|
@@ -22482,15 +22683,57 @@ function overdue(t, today) {
|
|
|
22482
22683
|
const due = dayOf(t.dueDate);
|
|
22483
22684
|
return isOpen(t) && due != null && due < today;
|
|
22484
22685
|
}
|
|
22485
|
-
|
|
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) {
|
|
22486
22727
|
const waitingOn = blockersOf(t, all).filter(isOpen).length;
|
|
22487
22728
|
const project = projects.get(t.projectId);
|
|
22488
22729
|
const key = taskKey(t, project);
|
|
22730
|
+
const column = data ? columnName(data, t) : null;
|
|
22489
22731
|
return {
|
|
22490
22732
|
id: t.id,
|
|
22491
22733
|
...key ? { key } : {},
|
|
22492
22734
|
title: t.title,
|
|
22493
22735
|
status: t.status,
|
|
22736
|
+
...column ? { column } : {},
|
|
22494
22737
|
position: boardPositions(all).get(t.id) ?? null,
|
|
22495
22738
|
priority: t.priority,
|
|
22496
22739
|
due: dayOf(t.dueDate),
|
|
@@ -22540,10 +22783,10 @@ function weekOut(data, personId, monday) {
|
|
|
22540
22783
|
tasks: d.items.map((item) => ({
|
|
22541
22784
|
placement: item.id,
|
|
22542
22785
|
hours: item.hours,
|
|
22543
|
-
...taskLine(item.task, people, projects, data.tasks)
|
|
22786
|
+
...taskLine(item.task, people, projects, data.tasks, data)
|
|
22544
22787
|
}))
|
|
22545
22788
|
})),
|
|
22546
|
-
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)),
|
|
22547
22790
|
unplannedCount: plan.unplanned.length,
|
|
22548
22791
|
inReviewCount: plan.inReview.length,
|
|
22549
22792
|
review: {
|
|
@@ -22552,8 +22795,8 @@ function weekOut(data, personId, monday) {
|
|
|
22552
22795
|
done: review.doneCount,
|
|
22553
22796
|
plannedHours: review.plannedHours,
|
|
22554
22797
|
doneHours: review.doneHours,
|
|
22555
|
-
stillOpen: review.planned.filter((p) => !p.done).map((p) => taskLine(p.task, people, projects, data.tasks)),
|
|
22556
|
-
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))
|
|
22557
22800
|
}
|
|
22558
22801
|
};
|
|
22559
22802
|
}
|
|
@@ -22728,11 +22971,13 @@ const HARBOR_TOOLS = [
|
|
|
22728
22971
|
id: pg.id,
|
|
22729
22972
|
name: pg.name,
|
|
22730
22973
|
health: pg.health,
|
|
22974
|
+
starts: dayOf(pg.startDate),
|
|
22731
22975
|
ends: dayOf(pg.endDate),
|
|
22732
22976
|
projects: (projectsByProgram.get(pg.id) ?? []).map((pj) => ({
|
|
22733
22977
|
id: pj.id,
|
|
22734
22978
|
name: pj.name,
|
|
22735
22979
|
health: pj.health,
|
|
22980
|
+
starts: dayOf(pj.startDate),
|
|
22736
22981
|
ends: dayOf(pj.endDate),
|
|
22737
22982
|
openTasks: openByProject.get(pj.id)?.open ?? 0,
|
|
22738
22983
|
lateTasks: openByProject.get(pj.id)?.late ?? 0
|
|
@@ -22828,7 +23073,7 @@ const HARBOR_TOOLS = [
|
|
|
22828
23073
|
tool({
|
|
22829
23074
|
name: "get_project",
|
|
22830
23075
|
title: "Project details",
|
|
22831
|
-
description: "One project: health and why, dates, budget, task counts, late tasks, what it waits on, milestones and open risks, when it will likely finish at the crew's recent pace (finish: shortHistory when there's under two weeks of it; lastDue names the open task whose due date the dates wait for, when the pace alone would finish sooner), notes the crew left for later (notesForLater: a sealed one shows only who left it and what opens it, unless it's the person's own), its charter (why, in, out, doneWhen, callsBy; null until someone writes one, and outOfScope lists open tasks whose titles name something it leaves out), the project's own task fields (fields: name, type and options, when it has any), and its url on the website." + DATA_NOTE,
|
|
23076
|
+
description: "One project: health and why, dates, budget, task counts, late tasks, what it waits on, milestones and open risks, when it will likely finish at the crew's recent pace (finish: shortHistory when there's under two weeks of it; lastDue names the open task whose due date the dates wait for, when the pace alone would finish sooner), notes the crew left for later (notesForLater: a sealed one shows only who left it and what opens it, unless it's the person's own), its charter (why, in, out, doneWhen, callsBy; null until someone writes one, and outOfScope lists open tasks whose titles name something it leaves out), its landfall (what should be true some days after it's Done, when Harbor asks, and the answer: yes, partly or no with a number; null until someone sets one; state is sailing while not Done, unset, waiting, due or answered), the project's own task fields (fields: name, type and options, when it has any), and its url on the website." + DATA_NOTE,
|
|
22832
23077
|
input: object({
|
|
22833
23078
|
harbor: harborArg,
|
|
22834
23079
|
project: string().min(1).max(200).describe("Project id or name.")
|
|
@@ -22844,7 +23089,7 @@ const HARBOR_TOOLS = [
|
|
|
22844
23089
|
const tasks = data.tasks.filter((t) => t.projectId === p.id);
|
|
22845
23090
|
const byStatus = Object.fromEntries(STATUSES.map((s) => [s, 0]));
|
|
22846
23091
|
for (const t of tasks) byStatus[t.status] += 1;
|
|
22847
|
-
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));
|
|
22848
23093
|
const waitsOn = (p.blockedByIds ?? (p.blockedById ? [p.blockedById] : [])).map((id) => projects.get(id)).filter((x) => Boolean(x)).map((x) => ({
|
|
22849
23094
|
id: x.id,
|
|
22850
23095
|
name: x.name,
|
|
@@ -22892,6 +23137,7 @@ const HARBOR_TOOLS = [
|
|
|
22892
23137
|
...p.customFields?.length ? { fields: fieldDefsOut(p) } : {},
|
|
22893
23138
|
finish: finishForecast(p, tasks, ctx.now, spendEntriesOf(data, p.id), data),
|
|
22894
23139
|
...charterOf(p, tasks),
|
|
23140
|
+
landfall: landfallJson(p, tasks, today, data.people),
|
|
22895
23141
|
...notesOf(ctx, p),
|
|
22896
23142
|
...hillOf(p, today),
|
|
22897
23143
|
...webLink(ctx, { project: p.id })
|
|
@@ -22945,7 +23191,7 @@ const HARBOR_TOOLS = [
|
|
|
22945
23191
|
return {
|
|
22946
23192
|
...doneMatches ? { doneMatches } : {},
|
|
22947
23193
|
total: list.length,
|
|
22948
|
-
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))
|
|
22949
23195
|
};
|
|
22950
23196
|
}
|
|
22951
23197
|
}),
|
|
@@ -22987,6 +23233,7 @@ const HARBOR_TOOLS = [
|
|
|
22987
23233
|
title: t.title,
|
|
22988
23234
|
description: t.description,
|
|
22989
23235
|
status: t.status,
|
|
23236
|
+
...((c) => c ? { column: c } : {})(columnName(data, t)),
|
|
22990
23237
|
position: boardPositions(data.tasks).get(t.id) ?? null,
|
|
22991
23238
|
priority: t.priority,
|
|
22992
23239
|
due: dayOf(t.dueDate),
|
|
@@ -23111,8 +23358,8 @@ const HARBOR_TOOLS = [
|
|
|
23111
23358
|
you: mine.name,
|
|
23112
23359
|
openTasks: open.length,
|
|
23113
23360
|
late: open.filter((t) => overdue(t, today)).length,
|
|
23114
|
-
tasks: open.slice(0, input.limit).map((t) => taskLine(t, people, projects, data.tasks)),
|
|
23115
|
-
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)),
|
|
23116
23363
|
waitingOnYou: waitingOnYou.slice(0, 20),
|
|
23117
23364
|
raid,
|
|
23118
23365
|
soundingsToAnswer: soundingsWaitingOn(data, mine.id, ctx.now).map((t) => ({
|
|
@@ -23577,6 +23824,30 @@ const HARBOR_TOOLS = [
|
|
|
23577
23824
|
};
|
|
23578
23825
|
}
|
|
23579
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
|
+
}),
|
|
23580
23851
|
tool({
|
|
23581
23852
|
name: "list_messages",
|
|
23582
23853
|
title: "A project's messages",
|
|
@@ -24031,7 +24302,7 @@ const HARBOR_TOOLS = [
|
|
|
24031
24302
|
inFog: fog.length,
|
|
24032
24303
|
fogAfterDays: 21,
|
|
24033
24304
|
tasks: fog.slice(input.offset, input.offset + input.limit).map((f) => ({
|
|
24034
|
-
...taskLine(f.task, people, projects, data.tasks),
|
|
24305
|
+
...taskLine(f.task, people, projects, data.tasks, data),
|
|
24035
24306
|
untouchedDays: f.days,
|
|
24036
24307
|
untouched: untouchedText(f.days),
|
|
24037
24308
|
thick: f.level === "thick",
|
|
@@ -24869,6 +25140,7 @@ function sessionSlice({ set, get, write }) {
|
|
|
24869
25140
|
}
|
|
24870
25141
|
};
|
|
24871
25142
|
}
|
|
25143
|
+
var ACTIVITY_KEPT;
|
|
24872
25144
|
var init_session = __esmMin((() => {
|
|
24873
25145
|
init_board();
|
|
24874
25146
|
init_derived_health();
|
|
@@ -24880,6 +25152,7 @@ var init_session = __esmMin((() => {
|
|
|
24880
25152
|
init_view_only();
|
|
24881
25153
|
init_fog();
|
|
24882
25154
|
init_format();
|
|
25155
|
+
ACTIVITY_KEPT = 40;
|
|
24883
25156
|
}));
|
|
24884
25157
|
|
|
24885
25158
|
// src/lib/first-week.ts
|
|
@@ -25068,6 +25341,20 @@ function hierarchySlice({ get, write }) {
|
|
|
25068
25341
|
charter: withCharter(p.charter, patch, by, stamp())
|
|
25069
25342
|
} : p) });
|
|
25070
25343
|
},
|
|
25344
|
+
setLandfall: (projectId, input) => {
|
|
25345
|
+
const by = get().currentUserId;
|
|
25346
|
+
write({ projects: get().projects.map((p) => p.id === projectId ? {
|
|
25347
|
+
...p,
|
|
25348
|
+
landfall: input ? withLandfall(p.landfall, input, by, stamp()) : null
|
|
25349
|
+
} : p) });
|
|
25350
|
+
},
|
|
25351
|
+
answerLandfall: (projectId, answer) => {
|
|
25352
|
+
const by = get().currentUserId;
|
|
25353
|
+
write({ projects: get().projects.map((p) => p.id === projectId && p.landfall ? {
|
|
25354
|
+
...p,
|
|
25355
|
+
landfall: withLandfallAnswer(p.landfall, answer, by, stamp())
|
|
25356
|
+
} : p) });
|
|
25357
|
+
},
|
|
25071
25358
|
addCustomField: (projectId, def) => {
|
|
25072
25359
|
const id = uid("cf");
|
|
25073
25360
|
write({ projects: get().projects.map((p) => p.id === projectId ? {
|
|
@@ -25429,6 +25716,7 @@ var init_hierarchy = __esmMin((() => {
|
|
|
25429
25716
|
init_scuttle_rows();
|
|
25430
25717
|
init_hill();
|
|
25431
25718
|
init_charter_lines();
|
|
25719
|
+
init_landfall_lines();
|
|
25432
25720
|
init_format();
|
|
25433
25721
|
init_empty();
|
|
25434
25722
|
init_first_week();
|
|
@@ -26327,6 +26615,28 @@ var init_raid_history = __esmMin((() => {}));
|
|
|
26327
26615
|
|
|
26328
26616
|
// src/lib/slices/milestones-raid.ts
|
|
26329
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
|
+
};
|
|
26330
26640
|
return {
|
|
26331
26641
|
toggleMilestone: (id) => {
|
|
26332
26642
|
if (get().milestones.find((m) => m.id === id)?.approverId) return;
|
|
@@ -26632,25 +26942,21 @@ function milestonesRaidSlice({ get, write }) {
|
|
|
26632
26942
|
write({ reflections: [...s.reflections ?? [], row] });
|
|
26633
26943
|
return row.id;
|
|
26634
26944
|
},
|
|
26635
|
-
postMessage: (projectId, body) => {
|
|
26636
|
-
const
|
|
26637
|
-
if (messageProblem(body)) return null;
|
|
26638
|
-
const project = s.projects.find((p) => p.id === projectId);
|
|
26945
|
+
postMessage: (projectId, body, replyToId) => {
|
|
26946
|
+
const project = get().projects.find((p) => p.id === projectId);
|
|
26639
26947
|
if (!project) return null;
|
|
26640
|
-
|
|
26641
|
-
|
|
26642
|
-
|
|
26643
|
-
|
|
26644
|
-
|
|
26645
|
-
|
|
26646
|
-
|
|
26647
|
-
|
|
26648
|
-
|
|
26649
|
-
|
|
26650
|
-
|
|
26651
|
-
|
|
26652
|
-
get().log(`wrote in ${project.name}’s messages`, `/projects/${projectId}?view=messages`);
|
|
26653
|
-
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);
|
|
26654
26960
|
},
|
|
26655
26961
|
editMessage: (id, body) => {
|
|
26656
26962
|
const s = get();
|
|
@@ -28133,6 +28439,11 @@ var init_shore = __esmMin((() => {
|
|
|
28133
28439
|
kind: "notes",
|
|
28134
28440
|
label: "Notes for later, when they open",
|
|
28135
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."
|
|
28136
28447
|
}
|
|
28137
28448
|
];
|
|
28138
28449
|
}));
|
|
@@ -28216,6 +28527,7 @@ function memoryHarbor(start, opts) {
|
|
|
28216
28527
|
forecast: false,
|
|
28217
28528
|
health: false,
|
|
28218
28529
|
notes: false,
|
|
28530
|
+
messages: false,
|
|
28219
28531
|
...row,
|
|
28220
28532
|
...ask.url ? { url } : {}
|
|
28221
28533
|
};
|
|
@@ -28230,6 +28542,7 @@ function memoryHarbor(start, opts) {
|
|
|
28230
28542
|
forecast: row.forecast,
|
|
28231
28543
|
health: row.health,
|
|
28232
28544
|
notes: row.notes,
|
|
28545
|
+
messages: row.messages,
|
|
28233
28546
|
failures: 0
|
|
28234
28547
|
} : null;
|
|
28235
28548
|
if (ask.test) {
|
|
@@ -34570,6 +34883,7 @@ function sinceText(iso, now) {
|
|
|
34570
34883
|
|
|
34571
34884
|
// src/tui/watch-run.ts
|
|
34572
34885
|
init_clock();
|
|
34886
|
+
init_session();
|
|
34573
34887
|
init_plain();
|
|
34574
34888
|
init_repo_sync();
|
|
34575
34889
|
init_store();
|
|
@@ -34636,7 +34950,18 @@ async function watchOn(opts) {
|
|
|
34636
34950
|
if (earlier.length === 0) out(dim(since ? `Nothing since ${since}.` : "Nothing logged yet."));
|
|
34637
34951
|
else out(dim(since ? `Since ${since}:` : "Lately:"));
|
|
34638
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.`));
|
|
34639
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.`));
|
|
34640
34965
|
if (opts.once) return;
|
|
34641
34966
|
if (!opts.json && earlier.length > 0) out(dim("New changes print below as they land."));
|
|
34642
34967
|
const narrow = (d) => project ? {
|