@harborppm/cli 0.35.0 → 0.36.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 +4 -0
- package/dist/app.mjs +52 -4
- package/lib/cli.mjs +52 -0
- package/lib/format.mjs +40 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,10 +132,14 @@ harbor note PROJECT ["NOTE"] [--opens DATE|--milestone NAME|--off-track]
|
|
|
132
132
|
harbor limit PROJECT [COLUMN N|none]
|
|
133
133
|
harbor handover PERSON [--to NAME] [--until DATE] [--note TEXT] [--keep ID,ID] [--confirm]
|
|
134
134
|
harbor handback PERSON
|
|
135
|
+
harbor agent [--agent NAME]
|
|
136
|
+
harbor agent report TASK "WHAT YOU DID" [--start] [--agent NAME]
|
|
135
137
|
```
|
|
136
138
|
|
|
137
139
|
`harbor handover Priya` lists everything Priya carries (open tasks, sign-offs waiting on her, risks and issues she owns, projects and programs she leads) and who should take each, from who has room this week. Nothing moves until you add `--confirm`. `--until DATE` is for time away: Priya shows as away on People, and `harbor handback Priya` gives back whatever is still with whoever took it. `--note` goes with each task and risk as a comment.
|
|
138
140
|
|
|
141
|
+
Agents on the crew: put an AI app on People with Add an agent and assign it tasks like anyone else. An agent working through the CLI (Claude Code, say) runs `harbor agent` for its tasks, in progress first, with their notes, checklists and latest comments. When it's done, `harbor agent report RR-4 "Drafted 12 questions; two need Dana"` adds its note as the agent's comment and moves the task to In review for a person to check. An agent never marks its own work Done.
|
|
142
|
+
|
|
139
143
|
`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.
|
|
140
144
|
|
|
141
145
|
`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 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.
|
package/dist/app.mjs
CHANGED
|
@@ -10798,7 +10798,7 @@ function hierarchySlice({ get, write }) {
|
|
|
10798
10798
|
const name = rows.portfolios?.[0]?.name ?? rows.programs?.[0]?.name ?? rows.projects?.[0]?.name;
|
|
10799
10799
|
if (name) get().log(`put back “${name}”`);
|
|
10800
10800
|
},
|
|
10801
|
-
importRows: (rows) => {
|
|
10801
|
+
importRows: (rows, from = "a spreadsheet") => {
|
|
10802
10802
|
const state = get();
|
|
10803
10803
|
const have = (current) => new Set(current.map((r) => r.id));
|
|
10804
10804
|
const add = (current, more) => {
|
|
@@ -10809,10 +10809,11 @@ function hierarchySlice({ get, write }) {
|
|
|
10809
10809
|
programs: add(state.programs, rows.programs),
|
|
10810
10810
|
projects: add(state.projects, rows.projects),
|
|
10811
10811
|
tasks: add(state.tasks, rows.tasks),
|
|
10812
|
-
people: add(state.people, rows.people)
|
|
10812
|
+
people: add(state.people, rows.people),
|
|
10813
|
+
raidItems: add(state.raidItems, rows.raidItems)
|
|
10813
10814
|
});
|
|
10814
10815
|
const n = rows.tasks?.length ?? 0;
|
|
10815
|
-
get().log(`imported ${n} task${n === 1 ? "" : "s"} from
|
|
10816
|
+
get().log(`imported ${n} task${n === 1 ? "" : "s"} from ${from}`);
|
|
10816
10817
|
},
|
|
10817
10818
|
dropRows: (ids) => {
|
|
10818
10819
|
const state = get();
|
|
@@ -10820,7 +10821,8 @@ function hierarchySlice({ get, write }) {
|
|
|
10820
10821
|
programs: new Set(ids.programs ?? []),
|
|
10821
10822
|
projects: new Set(ids.projects ?? []),
|
|
10822
10823
|
tasks: new Set(ids.tasks ?? []),
|
|
10823
|
-
people: new Set(ids.people ?? [])
|
|
10824
|
+
people: new Set(ids.people ?? []),
|
|
10825
|
+
raidItems: new Set(ids.raidItems ?? [])
|
|
10824
10826
|
};
|
|
10825
10827
|
const tasks = state.tasks.filter((r) => !gone.tasks.has(r.id));
|
|
10826
10828
|
if (gone.people.size) {
|
|
@@ -10831,6 +10833,7 @@ function hierarchySlice({ get, write }) {
|
|
|
10831
10833
|
projects: state.projects.filter((r) => !gone.projects.has(r.id)),
|
|
10832
10834
|
tasks,
|
|
10833
10835
|
people: gone.people.size ? state.people.filter((r) => !gone.people.has(r.id)) : state.people,
|
|
10836
|
+
raidItems: gone.raidItems.size ? state.raidItems.filter((r) => !gone.raidItems.has(r.id)) : state.raidItems,
|
|
10834
10837
|
selectedTaskId: gone.tasks.has(state.selectedTaskId ?? "") ? null : state.selectedTaskId
|
|
10835
10838
|
});
|
|
10836
10839
|
},
|
|
@@ -11148,6 +11151,27 @@ function tasksSlice({ set, get, write }) {
|
|
|
11148
11151
|
else get().log(`commented on “${task.title}”`, `/projects/${task.projectId}`);
|
|
11149
11152
|
}
|
|
11150
11153
|
},
|
|
11154
|
+
agentUpdate: (agentId, taskId, body, status, opts) => {
|
|
11155
|
+
const task = get().tasks.find((t) => t.id === taskId);
|
|
11156
|
+
const text = body.trim();
|
|
11157
|
+
if (!task || !text) return;
|
|
11158
|
+
if (task.status !== status) get().moveTask(taskId, status, { allowOverCap: opts?.allowOverCap });
|
|
11159
|
+
const at = stamp();
|
|
11160
|
+
write({
|
|
11161
|
+
comments: [...get().comments, {
|
|
11162
|
+
id: uid("c"),
|
|
11163
|
+
taskId,
|
|
11164
|
+
authorId: agentId,
|
|
11165
|
+
body: text,
|
|
11166
|
+
createdAt: at,
|
|
11167
|
+
mentionedIds: parseMentions(text, get().people).filter((id) => id !== agentId)
|
|
11168
|
+
}],
|
|
11169
|
+
people: get().people.map((p) => p.id === agentId ? {
|
|
11170
|
+
...p,
|
|
11171
|
+
lastSeenAt: at
|
|
11172
|
+
} : p)
|
|
11173
|
+
});
|
|
11174
|
+
},
|
|
11151
11175
|
updateComment: (id, body) => {
|
|
11152
11176
|
const comment = get().comments.find((c) => c.id === id);
|
|
11153
11177
|
if (!comment || comment.authorId !== get().currentUserId) return;
|
|
@@ -11238,6 +11262,13 @@ function tasksSlice({ set, get, write }) {
|
|
|
11238
11262
|
for (const id of ids) have.add(id);
|
|
11239
11263
|
set({ readPingIds: [...have] });
|
|
11240
11264
|
},
|
|
11265
|
+
tickVoyage: (ids) => {
|
|
11266
|
+
const tick = new Set(ids);
|
|
11267
|
+
const ticked = get().tasks.filter((t) => tick.has(t.id) && t.status !== "done");
|
|
11268
|
+
if (ticked.length === 0) return;
|
|
11269
|
+
write({ tasks: get().tasks.map((t) => tick.has(t.id) && t.status !== "done" ? withStatus(t, "done") : t) });
|
|
11270
|
+
for (const t of ticked) get().log(`ticked off “${t.title}” on First voyage`, `/projects/${t.projectId}`);
|
|
11271
|
+
},
|
|
11241
11272
|
toggleFocus: (taskId) => {
|
|
11242
11273
|
const personId = get().currentUserId;
|
|
11243
11274
|
const date = todayISO();
|
|
@@ -11688,6 +11719,23 @@ function peopleSpendSlice({ set, get, write }) {
|
|
|
11688
11719
|
get().log(`handed ${person.name}’s work back`, "/people");
|
|
11689
11720
|
return applied.moved.length;
|
|
11690
11721
|
},
|
|
11722
|
+
addAgent: (a) => {
|
|
11723
|
+
const id = uid("p");
|
|
11724
|
+
const row = {
|
|
11725
|
+
id,
|
|
11726
|
+
name: a.name.trim() || "Claude",
|
|
11727
|
+
role: "Agent",
|
|
11728
|
+
initials: "AI",
|
|
11729
|
+
swatch: a.swatch,
|
|
11730
|
+
location: "",
|
|
11731
|
+
kind: "agent",
|
|
11732
|
+
addedById: get().currentUserId,
|
|
11733
|
+
lastSeenAt: null
|
|
11734
|
+
};
|
|
11735
|
+
set({ people: [...get().people, row] });
|
|
11736
|
+
get().log(`put ${row.name} on the crew as an agent`, "/people");
|
|
11737
|
+
return id;
|
|
11738
|
+
},
|
|
11691
11739
|
updatePerson: (id, patch) => {
|
|
11692
11740
|
write({ people: get().people.map((p) => p.id === id ? {
|
|
11693
11741
|
...p,
|
package/lib/cli.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
notesText,
|
|
30
30
|
voyageText,
|
|
31
31
|
handoverText,
|
|
32
|
+
agentWorkText,
|
|
32
33
|
raidList,
|
|
33
34
|
programTree,
|
|
34
35
|
projectList,
|
|
@@ -125,6 +126,11 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
125
126
|
[--keep ID,ID] [--confirm]
|
|
126
127
|
hand someone's work to the crew
|
|
127
128
|
harbor handback PERSON give it back after time away
|
|
129
|
+
harbor agent [--agent NAME] the tasks given to an AI agent on
|
|
130
|
+
the crew, in the order to do them
|
|
131
|
+
harbor agent report TASK "WHAT YOU DID" [--start] [--agent NAME]
|
|
132
|
+
the agent's note; moves the task
|
|
133
|
+
to In review (--start: doing)
|
|
128
134
|
|
|
129
135
|
Every command takes --json, and --harbor NAME when you're in more than one
|
|
130
136
|
harbor (or set one with harbor use). harbor help COMMAND shows just that one.
|
|
@@ -434,6 +440,33 @@ const DETAIL = {
|
|
|
434
440
|
"",
|
|
435
441
|
' PERSON is a name, an id, or "me".',
|
|
436
442
|
],
|
|
443
|
+
handback: [
|
|
444
|
+
" harbor handback PERSON writes, straight away: gives back",
|
|
445
|
+
" what was handed over for their time",
|
|
446
|
+
" away and is still with whoever took",
|
|
447
|
+
" it, and stops showing them as away",
|
|
448
|
+
"",
|
|
449
|
+
" Anything someone else has since taken, finished or closed stays where it",
|
|
450
|
+
" is. If PERSON isn't marked away, it says so and changes nothing.",
|
|
451
|
+
" harbor handover PERSON --until DATE is what marks someone away.",
|
|
452
|
+
"",
|
|
453
|
+
' PERSON is a name, an id, or "me".',
|
|
454
|
+
],
|
|
455
|
+
agent: [
|
|
456
|
+
" harbor agent reads the tasks people gave the crew's",
|
|
457
|
+
" agent (an AI crew member on People),",
|
|
458
|
+
" in progress first, with their notes",
|
|
459
|
+
' harbor agent report TASK "TEXT" writes: the agent\'s note on the task,',
|
|
460
|
+
" and moves it to In review for a person",
|
|
461
|
+
" to check",
|
|
462
|
+
' harbor agent report TASK "TEXT" --start',
|
|
463
|
+
" writes: the note, and moves it to",
|
|
464
|
+
" In progress",
|
|
465
|
+
"",
|
|
466
|
+
" --agent NAME picks the agent when the crew has more than one. An agent",
|
|
467
|
+
" never marks its own work Done; a person does, on the website or in the",
|
|
468
|
+
" terminal app.",
|
|
469
|
+
],
|
|
437
470
|
voyage: [
|
|
438
471
|
" harbor voyage PROJECT reads a Done project's voyage log",
|
|
439
472
|
' harbor voyage PROJECT "YOUR LINE" writes: adds or changes your line in it',
|
|
@@ -1461,6 +1494,25 @@ export async function run(argv, io = {}) {
|
|
|
1461
1494
|
},
|
|
1462
1495
|
);
|
|
1463
1496
|
}
|
|
1497
|
+
case "agent": {
|
|
1498
|
+
const { positionals, values, harbor } = parse(spec(["start"], ["agent"]));
|
|
1499
|
+
if (positionals[0] !== "report") {
|
|
1500
|
+
none(positionals);
|
|
1501
|
+
return call("agent_work", { harbor, agent: values.agent }, agentWorkText);
|
|
1502
|
+
}
|
|
1503
|
+
const [, task, ...words] = positionals;
|
|
1504
|
+
const note = words.join(" ").trim();
|
|
1505
|
+
if (!task || !note)
|
|
1506
|
+
throw new HarborError(
|
|
1507
|
+
'Give the task and what the agent did: harbor agent report RR-4 "Drafted it".',
|
|
1508
|
+
);
|
|
1509
|
+
return call(
|
|
1510
|
+
"agent_update",
|
|
1511
|
+
{ harbor, task, note, status: values.start ? "doing" : "review", agent: values.agent },
|
|
1512
|
+
(r) =>
|
|
1513
|
+
`${r.updated.key ?? r.updated.title} is ${r.updated.status === "review" ? "in review for a person to check" : "in progress"}, with ${r.commentedAs}'s note.`,
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1464
1516
|
case "handback": {
|
|
1465
1517
|
const { positionals, harbor } = parse(spec());
|
|
1466
1518
|
const person = positionals.join(" ").trim();
|
package/lib/format.mjs
CHANGED
|
@@ -412,9 +412,19 @@ export function handoverText(r, { confirmHint = true } = {}) {
|
|
|
412
412
|
group = i.group;
|
|
413
413
|
lines.push("", `${group}:`);
|
|
414
414
|
}
|
|
415
|
+
const due = i.due && i.kind !== "project" && i.kind !== "program" ? ` (${i.due})` : "";
|
|
416
|
+
lines.push(` ${i.what}${due}${i.to ? ` → ${i.to}` : ""}`);
|
|
417
|
+
}
|
|
418
|
+
if (r.nobodyToTake) {
|
|
415
419
|
lines.push(
|
|
416
|
-
|
|
420
|
+
"",
|
|
421
|
+
`${r.nobodyToTake} Invite someone with harbor invite EMAIL, then run this again.`,
|
|
417
422
|
);
|
|
423
|
+
if (confirmHint)
|
|
424
|
+
lines.push(
|
|
425
|
+
`Until then, --confirm moves nothing; with --until DATE it only marks ${r.person} as away.`,
|
|
426
|
+
);
|
|
427
|
+
return lines.join("\n");
|
|
418
428
|
}
|
|
419
429
|
const room = (r.takers ?? []).filter((t) => t.note).map((t) => `${t.who} ${t.note}`);
|
|
420
430
|
if (room.length) lines.push("", `This week: ${room.join(", ")}.`);
|
|
@@ -425,3 +435,32 @@ export function handoverText(r, { confirmHint = true } = {}) {
|
|
|
425
435
|
);
|
|
426
436
|
return lines.join("\n");
|
|
427
437
|
}
|
|
438
|
+
|
|
439
|
+
/** agent_work: the agent's tasks in the order to do them, then what waits on a person. */
|
|
440
|
+
export function agentWorkText(r) {
|
|
441
|
+
if (!r.agent) return r.note;
|
|
442
|
+
const lines = [`Work for ${r.agent}${r.addedBy ? ` (added by ${r.addedBy})` : ""}`];
|
|
443
|
+
if (!r.tasks?.length) lines.push("", "Nothing to do: no open tasks are assigned to it.");
|
|
444
|
+
for (const t of r.tasks ?? []) {
|
|
445
|
+
const bits = [t.status === "doing" ? "in progress" : t.status, t.priority];
|
|
446
|
+
if (t.due) bits.push(`due ${t.due}`);
|
|
447
|
+
lines.push(
|
|
448
|
+
"",
|
|
449
|
+
`${t.key ?? t.id} ${t.title} (${bits.join(", ")}${t.project ? `, ${t.project}` : ""})`,
|
|
450
|
+
);
|
|
451
|
+
if (t.notes) lines.push(...t.notes.split("\n").map((l) => ` ${l}`));
|
|
452
|
+
for (const c of t.checklist ?? []) lines.push(` [${c.done ? "x" : " "}] ${c.step}`);
|
|
453
|
+
for (const c of t.comments ?? []) lines.push(` ${c.author ?? "Someone"}: ${c.body}`);
|
|
454
|
+
}
|
|
455
|
+
if (r.total > (r.tasks ?? []).length) lines.push("", `…and ${r.total - r.tasks.length} more.`);
|
|
456
|
+
if (r.forReview?.length)
|
|
457
|
+
lines.push(
|
|
458
|
+
"",
|
|
459
|
+
`Waiting for a person to check: ${r.forReview.map((t) => t.key ?? t.title).join(", ")}.`,
|
|
460
|
+
);
|
|
461
|
+
lines.push(
|
|
462
|
+
"",
|
|
463
|
+
'When a task is done, harbor agent report TASK "WHAT YOU DID" moves it to In review for a person to check.',
|
|
464
|
+
);
|
|
465
|
+
return lines.join("\n");
|
|
466
|
+
}
|