@harborppm/cli 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.mjs +259 -5
- package/lib/cli.mjs +66 -1
- package/lib/format.mjs +38 -0
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -2274,9 +2274,158 @@ var init_premortem = __esmMin((() => {
|
|
|
2274
2274
|
}));
|
|
2275
2275
|
|
|
2276
2276
|
//#endregion
|
|
2277
|
-
//#region src/lib/
|
|
2277
|
+
//#region src/lib/charter-lines.ts
|
|
2278
|
+
/** Nothing written on any of the five lines. */
|
|
2279
|
+
function charterEmpty(charter) {
|
|
2280
|
+
return !charter || CHARTER_FIELDS.every((f) => !clean(charter[f]));
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* The charter after a change to some of its lines: trimmed, and null once
|
|
2284
|
+
* every line is empty so a cleared charter reads as never written.
|
|
2285
|
+
*/
|
|
2286
|
+
function withCharter(current, patch, by, at) {
|
|
2287
|
+
const next = {
|
|
2288
|
+
...current ?? {},
|
|
2289
|
+
updatedAt: at,
|
|
2290
|
+
updatedById: by
|
|
2291
|
+
};
|
|
2292
|
+
for (const field of CHARTER_FIELDS) {
|
|
2293
|
+
if (!(field in patch)) continue;
|
|
2294
|
+
const text = (patch[field] ?? "").trim();
|
|
2295
|
+
if (text) next[field] = text;
|
|
2296
|
+
else delete next[field];
|
|
2297
|
+
}
|
|
2298
|
+
return charterEmpty(next) ? null : next;
|
|
2299
|
+
}
|
|
2300
|
+
var CHARTER_FIELDS, clean;
|
|
2301
|
+
var init_charter_lines = __esmMin((() => {
|
|
2302
|
+
CHARTER_FIELDS = [
|
|
2303
|
+
"why",
|
|
2304
|
+
"inScope",
|
|
2305
|
+
"outScope",
|
|
2306
|
+
"doneWhen",
|
|
2307
|
+
"callsBy"
|
|
2308
|
+
];
|
|
2309
|
+
clean = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
2310
|
+
}));
|
|
2311
|
+
|
|
2312
|
+
//#endregion
|
|
2313
|
+
//#region src/lib/charter.ts
|
|
2278
2314
|
init_indexes();
|
|
2279
2315
|
init_date_fns();
|
|
2316
|
+
init_date_text();
|
|
2317
|
+
init_premortem();
|
|
2318
|
+
init_charter_lines();
|
|
2319
|
+
const CHARTER_LABEL = {
|
|
2320
|
+
why: "Why",
|
|
2321
|
+
inScope: "In",
|
|
2322
|
+
outScope: "Out",
|
|
2323
|
+
doneWhen: "Done when",
|
|
2324
|
+
callsBy: "Calls made by"
|
|
2325
|
+
};
|
|
2326
|
+
/** Each line's name for agents and the CLI's --json: plain words, no "Scope". */
|
|
2327
|
+
const CHARTER_KEY = {
|
|
2328
|
+
why: "why",
|
|
2329
|
+
inScope: "in",
|
|
2330
|
+
outScope: "out",
|
|
2331
|
+
doneWhen: "doneWhen",
|
|
2332
|
+
callsBy: "callsBy"
|
|
2333
|
+
};
|
|
2334
|
+
/** The lines written so far, in order. */
|
|
2335
|
+
function charterLines(charter) {
|
|
2336
|
+
if (!charter) return [];
|
|
2337
|
+
return CHARTER_FIELDS.map((field) => ({
|
|
2338
|
+
field,
|
|
2339
|
+
label: CHARTER_LABEL[field],
|
|
2340
|
+
text: clean(charter[field])
|
|
2341
|
+
})).filter((l) => l.text);
|
|
2342
|
+
}
|
|
2343
|
+
function charterFilled(charter) {
|
|
2344
|
+
return charterLines(charter).length;
|
|
2345
|
+
}
|
|
2346
|
+
/** A week after the start, an empty charter is worth a nudge. */
|
|
2347
|
+
const CHARTER_GRACE_DAYS = 7;
|
|
2348
|
+
/**
|
|
2349
|
+
* A project whose charter is still blank a week after it started, while
|
|
2350
|
+
* it's still running. Done and paused ones are left alone.
|
|
2351
|
+
*/
|
|
2352
|
+
function charterOverdue(project, today) {
|
|
2353
|
+
if (project.health === "done" || project.health === "paused") return null;
|
|
2354
|
+
if (!charterEmpty(project.charter)) return null;
|
|
2355
|
+
const days = differenceInCalendarDays(parseIso(today), parseIso(project.startDate.slice(0, 10)));
|
|
2356
|
+
return days >= 7 ? days : null;
|
|
2357
|
+
}
|
|
2358
|
+
/** The items of an In or Out line: split on commas, semicolons, "and" lists and new lines. */
|
|
2359
|
+
function charterItems(text) {
|
|
2360
|
+
return (text ?? "").split(/[\n,;•]+/).map((s) => s.replace(/^\s*(?:and|or|-|\*)\s+/i, "").trim()).filter((s) => contentWords(s).size > 0);
|
|
2361
|
+
}
|
|
2362
|
+
/** True when a task's title names an item: every meaningful word of a short item, or two of a longer one. */
|
|
2363
|
+
function names(item, title) {
|
|
2364
|
+
let shared = 0;
|
|
2365
|
+
for (const w of item) if (title.has(w)) shared += 1;
|
|
2366
|
+
return item.size <= 2 ? shared === item.size : shared >= 2;
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Tasks whose titles name something the charter put out of scope, newest
|
|
2370
|
+
* first: the "I thought that was in" list. Tasks made before the charter
|
|
2371
|
+
* was last written are left out when `since` is given, so writing an Out
|
|
2372
|
+
* line about work already on the board doesn't flag it.
|
|
2373
|
+
*/
|
|
2374
|
+
function outOfScope(charter, tasks, opts = {}) {
|
|
2375
|
+
const items = charterItems(charter?.outScope).map((item) => ({
|
|
2376
|
+
item,
|
|
2377
|
+
words: contentWords(item)
|
|
2378
|
+
}));
|
|
2379
|
+
if (items.length === 0) return [];
|
|
2380
|
+
const out = [];
|
|
2381
|
+
for (const task of tasks) {
|
|
2382
|
+
if (opts.since && task.createdAt < opts.since) continue;
|
|
2383
|
+
const title = contentWords(task.title);
|
|
2384
|
+
const hit = items.find((i) => names(i.words, title));
|
|
2385
|
+
if (hit) out.push({
|
|
2386
|
+
task,
|
|
2387
|
+
item: hit.item
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
return out.sort((a, b) => b.task.createdAt.localeCompare(a.task.createdAt));
|
|
2391
|
+
}
|
|
2392
|
+
/** The voyage log's look at the charter: did the In list get done, did the Out list stay out. */
|
|
2393
|
+
function charterCheck(charter, tasks) {
|
|
2394
|
+
if (charterEmpty(charter)) return null;
|
|
2395
|
+
const finished = tasks.filter((t) => t.status === "done").map((t) => contentWords(t.title));
|
|
2396
|
+
const delivered = charterItems(charter.inScope).map((item) => {
|
|
2397
|
+
const words = contentWords(item);
|
|
2398
|
+
return {
|
|
2399
|
+
item,
|
|
2400
|
+
done: finished.filter((title) => names(words, title)).length
|
|
2401
|
+
};
|
|
2402
|
+
});
|
|
2403
|
+
return {
|
|
2404
|
+
doneWhen: clean(charter.doneWhen) || null,
|
|
2405
|
+
delivered,
|
|
2406
|
+
crept: outOfScope(charter, tasks),
|
|
2407
|
+
outItems: charterItems(charter.outScope).length
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
/** One sentence for the voyage log's story; null when there's no charter. */
|
|
2411
|
+
function charterSentence(check) {
|
|
2412
|
+
if (!check) return null;
|
|
2413
|
+
const bits = [];
|
|
2414
|
+
if (check.delivered.length) {
|
|
2415
|
+
const missing = check.delivered.filter((d) => d.done === 0).map((d) => d.item);
|
|
2416
|
+
bits.push(missing.length === 0 ? "every item the charter put in has finished work against it" : `${check.delivered.length - missing.length} of ${check.delivered.length} items the charter put in have finished work against them (not: ${missing.join(", ")})`);
|
|
2417
|
+
}
|
|
2418
|
+
if (check.crept.length) {
|
|
2419
|
+
const items = [...new Set(check.crept.map((c) => c.item))];
|
|
2420
|
+
bits.push(`${check.crept.length} task${check.crept.length === 1 ? "" : "s"} touched what it put out (${items.join(", ")})`);
|
|
2421
|
+
} else if (check.outItems > 0) bits.push("nothing it put out crept in");
|
|
2422
|
+
if (!bits.length) return null;
|
|
2423
|
+
return `Against the charter: ${bits.join("; ")}.`;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
//#endregion
|
|
2427
|
+
//#region src/lib/voyage-log.ts
|
|
2428
|
+
init_date_fns();
|
|
2280
2429
|
init_blockers();
|
|
2281
2430
|
init_date_text();
|
|
2282
2431
|
init_premortem();
|
|
@@ -2342,6 +2491,7 @@ function voyageLog(data, projectId) {
|
|
|
2342
2491
|
slips,
|
|
2343
2492
|
risks,
|
|
2344
2493
|
issues: issues.length,
|
|
2494
|
+
charter: charterCheck(project.charter, tasks),
|
|
2345
2495
|
decisions,
|
|
2346
2496
|
carriers,
|
|
2347
2497
|
budget: project.budget,
|
|
@@ -2390,6 +2540,8 @@ function voyageStory(log) {
|
|
|
2390
2540
|
out.push(s.waitedOn ? `${late}; it waited on “${s.waitedOn.title}”, done ${md$2(s.waitedOn.completedAt)}.` : `${late}.`);
|
|
2391
2541
|
}
|
|
2392
2542
|
for (const r of log.risks.filter((x) => x.happened)) out.push(`The risk “${r.risk.title}” came true${r.guessed ? " (an issue names it)" : ""}.`);
|
|
2543
|
+
const against = charterSentence(log.charter);
|
|
2544
|
+
if (against) out.push(against);
|
|
2393
2545
|
if (log.decisions.length) {
|
|
2394
2546
|
const changed = log.decisions.filter((d) => d.status === "superseded").length;
|
|
2395
2547
|
out.push(`${plural$4(log.decisions.length, "decision was", "decisions were")} written down${changed ? `; ${changed === 1 ? "one was" : `${changed} were`} changed later` : ""}.`);
|
|
@@ -5026,6 +5178,29 @@ function premortemNote(data, project, now, today) {
|
|
|
5026
5178
|
const pending = answersOf(data.reflections, project.id).filter(isPending).length;
|
|
5027
5179
|
return pending ? `pre-mortem: ${pending} to sort · P` : "pre-mortem · P";
|
|
5028
5180
|
}
|
|
5181
|
+
/**
|
|
5182
|
+
* The charter in a few words for the project's header: how many of its five
|
|
5183
|
+
* lines are written and any open task that names what it leaves out, or a
|
|
5184
|
+
* nudge (warned once it's a week overdue). Null on a finished project with none.
|
|
5185
|
+
*/
|
|
5186
|
+
function charterNote(project, tasks, today) {
|
|
5187
|
+
const filled = charterFilled(project.charter);
|
|
5188
|
+
if (filled === 0) {
|
|
5189
|
+
if (project.health === "done") return null;
|
|
5190
|
+
return {
|
|
5191
|
+
text: "no charter yet · e writes it",
|
|
5192
|
+
warn: charterOverdue(project, today) !== null
|
|
5193
|
+
};
|
|
5194
|
+
}
|
|
5195
|
+
const creep = outOfScope(project.charter, tasksOf(tasks, project.id).filter((t) => t.status !== "done")).length;
|
|
5196
|
+
return creep ? {
|
|
5197
|
+
text: `charter: ${creep} task${creep === 1 ? "" : "s"} look out of scope · e`,
|
|
5198
|
+
warn: true
|
|
5199
|
+
} : {
|
|
5200
|
+
text: `charter ${filled} of 5 · e`,
|
|
5201
|
+
warn: false
|
|
5202
|
+
};
|
|
5203
|
+
}
|
|
5029
5204
|
const ROLE_LABEL = {
|
|
5030
5205
|
owner: "Owner",
|
|
5031
5206
|
member: "Can edit",
|
|
@@ -7416,6 +7591,24 @@ function reviewPrograms(data, opts = {}) {
|
|
|
7416
7591
|
const { ownerId, portfolioId } = opts;
|
|
7417
7592
|
return data.programs.filter((p) => (!portfolioId || p.portfolioId === portfolioId) && (!ownerId || programTouches(p, data, ownerId))).sort((a, b) => rankHealth(a.health) - rankHealth(b.health) || a.endDate.localeCompare(b.endDate));
|
|
7418
7593
|
}
|
|
7594
|
+
/**
|
|
7595
|
+
* The projects in these programs whose charter is still blank a week after
|
|
7596
|
+
* they started (src/lib/charter.ts), longest-running first: the review's
|
|
7597
|
+
* nudge to write the five lines.
|
|
7598
|
+
*/
|
|
7599
|
+
function blankCharters(data, programs, today) {
|
|
7600
|
+
const ids = new Set(programs.filter((p) => p.health !== "done").map((p) => p.id));
|
|
7601
|
+
const out = [];
|
|
7602
|
+
for (const project of data.projects) {
|
|
7603
|
+
if (!ids.has(project.programId)) continue;
|
|
7604
|
+
const days = charterOverdue(project, today);
|
|
7605
|
+
if (days !== null) out.push({
|
|
7606
|
+
project,
|
|
7607
|
+
days
|
|
7608
|
+
});
|
|
7609
|
+
}
|
|
7610
|
+
return out.sort((a, b) => b.days - a.days || a.project.name.localeCompare(b.project.name));
|
|
7611
|
+
}
|
|
7419
7612
|
|
|
7420
7613
|
//#endregion
|
|
7421
7614
|
//#region src/tui/moves.ts
|
|
@@ -10070,7 +10263,17 @@ function ProjectPanel({ id, width, height, go, active, onClose }) {
|
|
|
10070
10263
|
initial: project.description,
|
|
10071
10264
|
onSave: (v) => up({ description: v })
|
|
10072
10265
|
}
|
|
10073
|
-
}
|
|
10266
|
+
},
|
|
10267
|
+
...CHARTER_FIELDS.map((f) => ({
|
|
10268
|
+
label: `Charter: ${CHARTER_LABEL[f].toLowerCase()}`,
|
|
10269
|
+
value: project.charter?.[f]?.replace(/\s+/g, " ") || "Not written",
|
|
10270
|
+
color: project.charter?.[f] ? void 0 : theme.muted,
|
|
10271
|
+
edit: {
|
|
10272
|
+
kind: "text",
|
|
10273
|
+
initial: project.charter?.[f] ?? "",
|
|
10274
|
+
onSave: (v) => store.getState().setCharter(id, { [f]: v })
|
|
10275
|
+
}
|
|
10276
|
+
}))
|
|
10074
10277
|
];
|
|
10075
10278
|
return /* @__PURE__ */ jsx(DetailPanel, {
|
|
10076
10279
|
title: /* @__PURE__ */ jsx(Text, {
|
|
@@ -10708,6 +10911,8 @@ function ProjectsView({ active, width, height, go }) {
|
|
|
10708
10911
|
}
|
|
10709
10912
|
});
|
|
10710
10913
|
}
|
|
10914
|
+
/** Projects a week in without a charter, in a few words; null for none. */
|
|
10915
|
+
const charterGap = (n) => n ? `${n} blank charter${n === 1 ? "" : "s"}` : null;
|
|
10711
10916
|
/**
|
|
10712
10917
|
* The review, as on the website: programs worst first, then by end date,
|
|
10713
10918
|
* each with its lead, how much is late, where it sits on the priority matrix
|
|
@@ -10773,7 +10978,8 @@ function ProgramsView({ active, width, height, go }) {
|
|
|
10773
10978
|
late ? `${late} late` : null,
|
|
10774
10979
|
slip >= 100 ? `slipping costs ${dollars$1(slip)}` : null,
|
|
10775
10980
|
isRated(program) ? quadrantOf(program, maxBudget, rules) : null,
|
|
10776
|
-
program.nextMove ? `next: ${program.nextMove}` : "no next move"
|
|
10981
|
+
program.nextMove ? `next: ${program.nextMove}` : "no next move",
|
|
10982
|
+
charterGap(blankCharters({ projects }, [program], day).length)
|
|
10777
10983
|
].filter(Boolean).join(" · ")
|
|
10778
10984
|
};
|
|
10779
10985
|
});
|
|
@@ -18297,6 +18503,24 @@ function decisionOut(data, r, people) {
|
|
|
18297
18503
|
};
|
|
18298
18504
|
}
|
|
18299
18505
|
const tool = (t) => t;
|
|
18506
|
+
/** A project's charter, as get_project gives it: the lines written, and open tasks that name what it leaves out. */
|
|
18507
|
+
function charterOf(p, tasks) {
|
|
18508
|
+
const lines = charterLines(p.charter);
|
|
18509
|
+
if (lines.length === 0) return { charter: null };
|
|
18510
|
+
const creep = outOfScope(p.charter, tasks.filter((t) => t.status !== "done")).slice(0, 10);
|
|
18511
|
+
return {
|
|
18512
|
+
charter: {
|
|
18513
|
+
...Object.fromEntries(lines.map((l) => [CHARTER_KEY[l.field], l.text])),
|
|
18514
|
+
...p.charter?.updatedAt ? { updated: dayOf(p.charter.updatedAt) } : {}
|
|
18515
|
+
},
|
|
18516
|
+
...creep.length ? { outOfScope: creep.map((c) => ({
|
|
18517
|
+
task: taskKey(c.task, p) ?? c.task.id,
|
|
18518
|
+
title: c.task.title,
|
|
18519
|
+
names: c.item,
|
|
18520
|
+
added: dayOf(c.task.createdAt)
|
|
18521
|
+
})) } : {}
|
|
18522
|
+
};
|
|
18523
|
+
}
|
|
18300
18524
|
/** A project's notes for later, as get_project gives them: sealed ones without their text unless they're the caller's. */
|
|
18301
18525
|
function notesOf(ctx, p) {
|
|
18302
18526
|
const notes = bottlesOf(ctx.data.reflections, p.id);
|
|
@@ -18466,7 +18690,7 @@ const HARBOR_TOOLS = [
|
|
|
18466
18690
|
tool({
|
|
18467
18691
|
name: "get_project",
|
|
18468
18692
|
title: "Project details",
|
|
18469
|
-
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), and its url on the website." + DATA_NOTE,
|
|
18693
|
+
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), and its url on the website." + DATA_NOTE,
|
|
18470
18694
|
input: object({
|
|
18471
18695
|
harbor: harborArg,
|
|
18472
18696
|
project: string().min(1).max(200).describe("Project id or name.")
|
|
@@ -18526,6 +18750,7 @@ const HARBOR_TOOLS = [
|
|
|
18526
18750
|
milestones,
|
|
18527
18751
|
openRaid: raid,
|
|
18528
18752
|
finish: finishForecast(p, tasks, ctx.now, spendEntriesOf(data, p.id), data.reflections),
|
|
18753
|
+
...charterOf(p, tasks),
|
|
18529
18754
|
...notesOf(ctx, p),
|
|
18530
18755
|
...hillOf(p, today),
|
|
18531
18756
|
...webLink(ctx, { project: p.id })
|
|
@@ -18844,7 +19069,7 @@ const HARBOR_TOOLS = [
|
|
|
18844
19069
|
tool({
|
|
18845
19070
|
name: "program_review",
|
|
18846
19071
|
title: "The review",
|
|
18847
|
-
description: "The website's Review: programs worst health first, then soonest end, each with its lead, end date, health (and the note when set by hand), late tasks, where it sits on the priority matrix and its next move. Done programs only with includeDone." + DATA_NOTE,
|
|
19072
|
+
description: "The website's Review: programs worst health first, then soonest end, each with its lead, end date, health (and the note when set by hand), late tasks, where it sits on the priority matrix and its next move. Done programs only with includeDone. blankCharters lists running projects in them whose charter is still empty a week after they started (set_charter writes one)." + DATA_NOTE,
|
|
18848
19073
|
input: object({
|
|
18849
19074
|
harbor: harborArg,
|
|
18850
19075
|
owner: string().max(200).optional().describe("\"me\", or a person's id or name: programs they lead, lead a project in or hold a task in."),
|
|
@@ -18881,6 +19106,11 @@ const HARBOR_TOOLS = [
|
|
|
18881
19106
|
nextMove: p.nextMove?.trim() || null
|
|
18882
19107
|
})),
|
|
18883
19108
|
done: all.length - listed.length,
|
|
19109
|
+
blankCharters: blankCharters(data, listed, todayISO(ctx.now)).map((b) => ({
|
|
19110
|
+
project: b.project.name,
|
|
19111
|
+
program: data.programs.find((x) => x.id === b.project.programId)?.name ?? null,
|
|
19112
|
+
startedDaysAgo: b.days
|
|
19113
|
+
})),
|
|
18884
19114
|
onlyOneKnows: lifeboats(data, ctx.now, { programIds: new Set(listed.map((p) => p.id)) }).slice(0, 25).map((l) => lifeboatOut(data, l, people))
|
|
18885
19115
|
};
|
|
18886
19116
|
}
|
|
@@ -18964,6 +19194,17 @@ const HARBOR_TOOLS = [
|
|
|
18964
19194
|
cameTrue: r.happened,
|
|
18965
19195
|
...r.guessed ? { guessed: true } : {}
|
|
18966
19196
|
})),
|
|
19197
|
+
...log.charter ? { charter: {
|
|
19198
|
+
doneWhen: log.charter.doneWhen,
|
|
19199
|
+
inItems: log.charter.delivered.map((d) => ({
|
|
19200
|
+
item: d.item,
|
|
19201
|
+
finishedTasks: d.done
|
|
19202
|
+
})),
|
|
19203
|
+
outOfScopeTasks: log.charter.crept.map((c) => ({
|
|
19204
|
+
title: c.task.title,
|
|
19205
|
+
names: c.item
|
|
19206
|
+
}))
|
|
19207
|
+
} } : {},
|
|
18967
19208
|
decisions: log.decisions.map((d) => ({
|
|
18968
19209
|
title: d.title,
|
|
18969
19210
|
on: dayOf(d.decidedAt),
|
|
@@ -20226,6 +20467,13 @@ function hierarchySlice({ get, write }) {
|
|
|
20226
20467
|
hillMovedAt: stamp()
|
|
20227
20468
|
} : p) });
|
|
20228
20469
|
},
|
|
20470
|
+
setCharter: (projectId, patch) => {
|
|
20471
|
+
const by = get().currentUserId;
|
|
20472
|
+
write({ projects: get().projects.map((p) => p.id === projectId ? {
|
|
20473
|
+
...p,
|
|
20474
|
+
charter: withCharter(p.charter, patch, by, stamp())
|
|
20475
|
+
} : p) });
|
|
20476
|
+
},
|
|
20229
20477
|
addCustomField: (projectId, def) => {
|
|
20230
20478
|
const id = uid("cf");
|
|
20231
20479
|
write({ projects: get().projects.map((p) => p.id === projectId ? {
|
|
@@ -20502,6 +20750,7 @@ var init_hierarchy = __esmMin((() => {
|
|
|
20502
20750
|
init_derived_health();
|
|
20503
20751
|
init_shared();
|
|
20504
20752
|
init_hill();
|
|
20753
|
+
init_charter_lines();
|
|
20505
20754
|
init_sample_project();
|
|
20506
20755
|
init_format();
|
|
20507
20756
|
init_empty();
|
|
@@ -25611,6 +25860,7 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
25611
25860
|
reflections,
|
|
25612
25861
|
raidItems
|
|
25613
25862
|
}, project, clockNow(), day);
|
|
25863
|
+
const charter = charterNote(project, tasks, day);
|
|
25614
25864
|
const opened = freshlyOpened({
|
|
25615
25865
|
reflections,
|
|
25616
25866
|
milestones
|
|
@@ -25699,6 +25949,10 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
25699
25949
|
pmNote ? /* @__PURE__ */ jsxs(Text, {
|
|
25700
25950
|
color: theme.muted,
|
|
25701
25951
|
children: [" · ", pmNote]
|
|
25952
|
+
}) : null,
|
|
25953
|
+
charter ? /* @__PURE__ */ jsxs(Text, {
|
|
25954
|
+
color: charter.warn ? theme.warn : theme.muted,
|
|
25955
|
+
children: [" · ", charter.text]
|
|
25702
25956
|
}) : null
|
|
25703
25957
|
]
|
|
25704
25958
|
}),
|
package/lib/cli.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
myWork,
|
|
34
34
|
premortemText,
|
|
35
35
|
notesText,
|
|
36
|
+
charterText,
|
|
36
37
|
daysText,
|
|
37
38
|
soundingText,
|
|
38
39
|
lifeboatsText,
|
|
@@ -205,6 +206,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
205
206
|
answer or read it
|
|
206
207
|
harbor voyage PROJECT ["YOUR LINE"] a Done project's voyage log, and
|
|
207
208
|
your line in it
|
|
209
|
+
harbor charter PROJECT [--why T] [--in T] [--out T] [--done-when T]
|
|
210
|
+
[--calls T]
|
|
211
|
+
a project's five-line charter:
|
|
212
|
+
read it, or write some lines
|
|
208
213
|
harbor note PROJECT ["NOTE"] [--opens DATE|--milestone NAME|--off-track]
|
|
209
214
|
a note for later: sealed until
|
|
210
215
|
its day, milestone, or Off track
|
|
@@ -423,6 +428,10 @@ const FLAG = {
|
|
|
423
428
|
text: "the next move",
|
|
424
429
|
waitsOn: "--waits-on",
|
|
425
430
|
repeat: "--repeat",
|
|
431
|
+
in: "--in",
|
|
432
|
+
out: "--out",
|
|
433
|
+
doneWhen: "--done-when",
|
|
434
|
+
callsBy: "--calls",
|
|
426
435
|
description: "--notes",
|
|
427
436
|
body: "the comment",
|
|
428
437
|
column: "the column",
|
|
@@ -701,6 +710,18 @@ const DETAIL = {
|
|
|
701
710
|
" --no-people. A task already in its project is left alone: by its key",
|
|
702
711
|
" (a Key column, as harbor export writes) or by title.",
|
|
703
712
|
],
|
|
713
|
+
charter: [
|
|
714
|
+
" harbor charter PROJECT reads its charter, and any open task",
|
|
715
|
+
" that names something it leaves out",
|
|
716
|
+
' harbor charter PROJECT --why "TEXT" writes: that line; give any of',
|
|
717
|
+
" --why, --in, --out, --done-when and",
|
|
718
|
+
" --calls at once",
|
|
719
|
+
"",
|
|
720
|
+
" none clears a line: --out none. Separate items in --in and --out",
|
|
721
|
+
" with commas, so Harbor can spot a task that names one. The review lists",
|
|
722
|
+
" running projects still without a charter a week after they started.",
|
|
723
|
+
" PROJECT is a name, a key like RR, or an id.",
|
|
724
|
+
],
|
|
704
725
|
sound: [
|
|
705
726
|
" harbor sound TASK reads its sounding: who hasn't answered,",
|
|
706
727
|
" and the spread once everyone has",
|
|
@@ -2083,7 +2104,13 @@ export async function run(argv, io = {}) {
|
|
|
2083
2104
|
words && r.programs.length === 0
|
|
2084
2105
|
? `No program in the review matches "${words}".`
|
|
2085
2106
|
: reviewList(r),
|
|
2086
|
-
(r) =>
|
|
2107
|
+
(r) => {
|
|
2108
|
+
if (!words) return r;
|
|
2109
|
+
const programs = r.programs.filter((p) => nameHas(p.name, words));
|
|
2110
|
+
const names = new Set(programs.map((p) => p.name));
|
|
2111
|
+
const blankCharters = (r.blankCharters ?? []).filter((b) => names.has(b.program));
|
|
2112
|
+
return { ...r, programs, blankCharters };
|
|
2113
|
+
},
|
|
2087
2114
|
);
|
|
2088
2115
|
}
|
|
2089
2116
|
const status = { start: "doing", submit: "review", done: "done" }[command];
|
|
@@ -2405,6 +2432,44 @@ export async function run(argv, io = {}) {
|
|
|
2405
2432
|
(r) => `Sealed your note on ${r.sealed.project}. It ${r.sealed.opens}.`,
|
|
2406
2433
|
);
|
|
2407
2434
|
}
|
|
2435
|
+
case "charter": {
|
|
2436
|
+
const { positionals, values, harbor } = parse(
|
|
2437
|
+
spec([], ["why", "in", "out", "done-when", "calls"]),
|
|
2438
|
+
);
|
|
2439
|
+
const project = one(positionals, 'project: harbor charter "NAME"');
|
|
2440
|
+
// none clears a line, as --due none does.
|
|
2441
|
+
const line = (v) => (v.trim().toLowerCase() === "none" ? "" : v);
|
|
2442
|
+
const lines = Object.fromEntries(
|
|
2443
|
+
[
|
|
2444
|
+
["why", values.why],
|
|
2445
|
+
["in", values.in],
|
|
2446
|
+
["out", values.out],
|
|
2447
|
+
["doneWhen", values["done-when"]],
|
|
2448
|
+
["callsBy", values.calls],
|
|
2449
|
+
]
|
|
2450
|
+
.filter(([, v]) => v !== undefined)
|
|
2451
|
+
.map(([k, v]) => [k, line(v)]),
|
|
2452
|
+
);
|
|
2453
|
+
if (Object.keys(lines).length === 0) {
|
|
2454
|
+
// --json: the project's charter (null until written) and any out-of-scope tasks.
|
|
2455
|
+
return call(
|
|
2456
|
+
"get_project",
|
|
2457
|
+
{ harbor, project },
|
|
2458
|
+
(r) => charterText(r),
|
|
2459
|
+
(r) => ({
|
|
2460
|
+
id: r.id,
|
|
2461
|
+
project: r.name,
|
|
2462
|
+
charter: r.charter ?? null,
|
|
2463
|
+
...(r.outOfScope ? { outOfScope: r.outOfScope } : {}),
|
|
2464
|
+
}),
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
return call("set_charter", { harbor, project, ...lines }, (r) =>
|
|
2468
|
+
r.charter
|
|
2469
|
+
? `Saved ${r.project}'s charter.\n\n${charterText({ project: r.project, charter: r.charter })}`
|
|
2470
|
+
: `Cleared ${r.project}'s charter.`,
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2408
2473
|
case "sound": {
|
|
2409
2474
|
const { positionals, values, harbor } = parse(spec([], ["why", "ask", "settle"]));
|
|
2410
2475
|
const [task, days, ...extra] = positionals;
|
package/lib/format.mjs
CHANGED
|
@@ -344,6 +344,14 @@ export function reviewList(r) {
|
|
|
344
344
|
out.push(`${p.name} (${bits.join(" · ")})`);
|
|
345
345
|
out.push(` ${p.nextMove ? `next: ${p.nextMove}` : "no next move yet"}`);
|
|
346
346
|
}
|
|
347
|
+
const blank = r.blankCharters ?? [];
|
|
348
|
+
if (blank.length) {
|
|
349
|
+
out.push("", "Charters still blank a week in:");
|
|
350
|
+
for (const b of blank.slice(0, 8))
|
|
351
|
+
out.push(` ${b.project} (started ${b.startedDaysAgo} days ago)`);
|
|
352
|
+
if (blank.length > 8) out.push(` and ${blank.length - 8} more`);
|
|
353
|
+
out.push(' harbor charter "PROJECT" --why "…" writes one.');
|
|
354
|
+
}
|
|
347
355
|
return out.join("\n") + more;
|
|
348
356
|
}
|
|
349
357
|
|
|
@@ -650,6 +658,36 @@ export function importText(r) {
|
|
|
650
658
|
}
|
|
651
659
|
|
|
652
660
|
/** A project's notes for later, as get_project lists them. */
|
|
661
|
+
const CHARTER_LINES = [
|
|
662
|
+
["why", "Why"],
|
|
663
|
+
["in", "In"],
|
|
664
|
+
["out", "Out"],
|
|
665
|
+
["doneWhen", "Done when"],
|
|
666
|
+
["callsBy", "Calls made by"],
|
|
667
|
+
];
|
|
668
|
+
|
|
669
|
+
/** A project's charter as get_project or set_charter gives it, with open tasks that name what it leaves out. */
|
|
670
|
+
export function charterText(r) {
|
|
671
|
+
const project = r.name ?? r.project;
|
|
672
|
+
if (!r.charter) {
|
|
673
|
+
return `${project} has no charter yet. Write one, a line at a time:\n harbor charter "${project}" --why "…" --in "…, …" --out "…, …" --done-when "…" --calls "…"`;
|
|
674
|
+
}
|
|
675
|
+
const lines = [`Charter · ${project}`];
|
|
676
|
+
const width = Math.max(...CHARTER_LINES.map(([, label]) => label.length));
|
|
677
|
+
for (const [key, label] of CHARTER_LINES) {
|
|
678
|
+
if (r.charter[key]) lines.push(` ${label.padEnd(width)} ${r.charter[key]}`);
|
|
679
|
+
}
|
|
680
|
+
if (r.outOfScope?.length) {
|
|
681
|
+
lines.push("", "Out of scope?");
|
|
682
|
+
for (const t of r.outOfScope) {
|
|
683
|
+
lines.push(
|
|
684
|
+
` ${t.task} ${t.title} (added ${t.added}; the charter leaves out "${t.names}")`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return lines.join("\n");
|
|
689
|
+
}
|
|
690
|
+
|
|
653
691
|
export function notesText(project, notes) {
|
|
654
692
|
if (!notes?.length)
|
|
655
693
|
return `No notes for later on ${project}. Leave one: harbor note "${project}" "NOTE" --opens +30`;
|