@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 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 a spreadsheet`);
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
- ` ${i.what}${i.due && i.kind !== "project" && i.kind !== "program" ? ` (${i.due})` : ""} → ${i.to ?? "nobody yet"}`,
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",