@harborppm/cli 0.35.0 → 0.37.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/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,
@@ -85,6 +86,14 @@ export const HELP = `harbor: Harbor from the terminal
85
86
  harbor roadmap PROJECT [--dates|--no-dates] [--off]
86
87
  publish a project's public roadmap
87
88
 
89
+ harbor init [PROJECT] [--hook] keep a project's tasks as files in
90
+ this repository (.harbor/), one
91
+ per task, in step both ways
92
+ harbor init --local [NAME] a plan in this folder alone, no
93
+ account; harbor here opens it
94
+ harbor sync [--quiet] send the files' edits to Harbor
95
+ and bring its changes back
96
+
88
97
  harbor program "NAME" [--portfolio P] [--lead me|NAME] [--starts DATE]
89
98
  [--ends DATE]
90
99
  harbor project "NAME" --program P [--lead me|NAME] [--starts DATE]
@@ -125,6 +134,11 @@ export const HELP = `harbor: Harbor from the terminal
125
134
  [--keep ID,ID] [--confirm]
126
135
  hand someone's work to the crew
127
136
  harbor handback PERSON give it back after time away
137
+ harbor agent [--agent NAME] the tasks given to an AI agent on
138
+ the crew, in the order to do them
139
+ harbor agent report TASK "WHAT YOU DID" [--start] [--agent NAME]
140
+ the agent's note; moves the task
141
+ to In review (--start: doing)
128
142
 
129
143
  Every command takes --json, and --harbor NAME when you're in more than one
130
144
  harbor (or set one with harbor use). harbor help COMMAND shows just that one.
@@ -383,6 +397,30 @@ export function voyageForTerminal(r, project = r.project) {
383
397
 
384
398
  /** Commands whose forms do different things: one line each, saying which read and which write. */
385
399
  const DETAIL = {
400
+ init: [
401
+ " harbor init [PROJECT] [--hook] writes: .harbor/ at the top of this",
402
+ " repository, one file per task in",
403
+ " PROJECT (a key like RR or a name;",
404
+ " the folder's name when left out)",
405
+ " harbor init --local [NAME] writes: a plan that lives only in",
406
+ " this folder, with no account;",
407
+ " harbor here opens it full screen",
408
+ "",
409
+ " Each file has title, status, owner, due, priority and waits between --- lines,",
410
+ " and the task's notes below. Edit them, add new ones (any name; Harbor gives",
411
+ " them a key), review them in pull requests, then run harbor sync. --hook adds",
412
+ " a git post-commit hook that runs harbor sync after each commit. Run init on",
413
+ " a local plan to move its tasks into PROJECT.",
414
+ ],
415
+ sync: [
416
+ " harbor sync [--quiet] writes: sends what the files changed to",
417
+ " Harbor and rewrites the files from it",
418
+ "",
419
+ " A field you changed and nobody changed on the website goes to Harbor; one",
420
+ " changed in both places keeps the website's, and says so. Removing a file",
421
+ " doesn't delete the task (mark it done instead). On a fresh clone, run",
422
+ " harbor sync before editing. --quiet prints only problems.",
423
+ ],
386
424
  limit: [
387
425
  " harbor limit PROJECT reads its board's column limits",
388
426
  " harbor limit PROJECT COLUMN N writes: COLUMN takes at most N tasks",
@@ -434,6 +472,33 @@ const DETAIL = {
434
472
  "",
435
473
  ' PERSON is a name, an id, or "me".',
436
474
  ],
475
+ handback: [
476
+ " harbor handback PERSON writes, straight away: gives back",
477
+ " what was handed over for their time",
478
+ " away and is still with whoever took",
479
+ " it, and stops showing them as away",
480
+ "",
481
+ " Anything someone else has since taken, finished or closed stays where it",
482
+ " is. If PERSON isn't marked away, it says so and changes nothing.",
483
+ " harbor handover PERSON --until DATE is what marks someone away.",
484
+ "",
485
+ ' PERSON is a name, an id, or "me".',
486
+ ],
487
+ agent: [
488
+ " harbor agent reads the tasks people gave the crew's",
489
+ " agent (an AI crew member on People),",
490
+ " in progress first, with their notes",
491
+ ' harbor agent report TASK "TEXT" writes: the agent\'s note on the task,',
492
+ " and moves it to In review for a person",
493
+ " to check",
494
+ ' harbor agent report TASK "TEXT" --start',
495
+ " writes: the note, and moves it to",
496
+ " In progress",
497
+ "",
498
+ " --agent NAME picks the agent when the crew has more than one. An agent",
499
+ " never marks its own work Done; a person does, on the website or in the",
500
+ " terminal app.",
501
+ ],
437
502
  voyage: [
438
503
  " harbor voyage PROJECT reads a Done project's voyage log",
439
504
  ' harbor voyage PROJECT "YOUR LINE" writes: adds or changes your line in it',
@@ -609,6 +674,15 @@ export async function run(argv, io = {}) {
609
674
  }
610
675
  if (!terminal) throw new HarborError("The full-screen app needs a terminal.");
611
676
  const { values } = parseArgs(rest, { flags: [], options: ["harbor", "server"] });
677
+ // In a folder with a local plan (harbor init --local), the app opens on it.
678
+ if (!values.harbor && !values.server) {
679
+ const bundled = await loadApp();
680
+ const dir = await bundled.localPlanAt(io.cwd ?? process.cwd());
681
+ if (dir) {
682
+ await bundled.runLocalApp({ dir, theme: (await readSettings(env)).theme });
683
+ return 0;
684
+ }
685
+ }
612
686
  let creds = await readCredentials(env);
613
687
  if (!hasFullAccess(creds)) {
614
688
  out(
@@ -807,6 +881,75 @@ export async function run(argv, io = {}) {
807
881
  return 0;
808
882
  }
809
883
 
884
+ if (command === "init" || command === "sync") {
885
+ const init = command === "init";
886
+ const { values, positionals } = parseArgs(rest, {
887
+ flags: init ? ["local", "hook", "json"] : ["quiet", "json"],
888
+ options: init ? ["harbor"] : [],
889
+ });
890
+ const cwd = io.cwd ?? process.cwd();
891
+ const lines = [];
892
+ const say = values.json
893
+ ? (line) => lines.push(line)
894
+ : values.quiet
895
+ ? (line) => {
896
+ if (!line.startsWith("⚓")) out(plain(line));
897
+ }
898
+ : (line) => out(plain(line));
899
+ const app = await loadApp();
900
+ if (init && values.local) {
901
+ if (positionals.length > 1)
902
+ throw new HarborError(`harbor init --local takes one name; put it in quotes.`);
903
+ const config = await app.initLocalHere({ cwd, name: positionals[0], say });
904
+ if (values.json) out(jsonText({ plan: config, notes: lines }));
905
+ return 0;
906
+ }
907
+ if (positionals.length > (init ? 1 : 0))
908
+ throw new HarborError(
909
+ init
910
+ ? "harbor init takes one project; put a name with spaces in quotes."
911
+ : "harbor sync takes no words; it syncs the plan of the folder you're in.",
912
+ );
913
+ // Plans move through /api/app, as the full-screen app does: full access.
914
+ const transport = async () => {
915
+ const creds = await readCredentials(env);
916
+ if (!hasFullAccess(creds))
917
+ throw new HarborError(
918
+ creds
919
+ ? "This sign-in can only read. Run harbor login (without --read-only) to keep plans in step."
920
+ : "Not signed in. Run harbor login, or harbor signup if you're new; harbor init --local needs no account.",
921
+ );
922
+ return app.planTransport(
923
+ tokenSource({ creds, save: (c) => writeCredentials(c, env), fetcher }),
924
+ fetcher,
925
+ );
926
+ };
927
+ if (init) {
928
+ const creds = await readCredentials(env);
929
+ const config = await app.initHere({
930
+ cwd,
931
+ transport: await transport(),
932
+ project: positionals[0],
933
+ hook: Boolean(values.hook),
934
+ say,
935
+ pickHarbor: (list) => {
936
+ if (values.harbor) return app.harborNamed(list, values.harbor);
937
+ const saved = list.find((h) => h.id === creds?.harbor);
938
+ if (saved) return saved;
939
+ if (list.length === 1) return list[0];
940
+ throw new HarborError(
941
+ `You're in ${list.length} harbors (${list.map((h) => h.name).join(", ")}). Add --harbor NAME, or run harbor use NAME first.`,
942
+ );
943
+ },
944
+ });
945
+ if (values.json) out(jsonText({ plan: config, notes: lines }));
946
+ return 0;
947
+ }
948
+ const report = await app.syncHere({ cwd, transport, say });
949
+ if (values.json) out(jsonText({ ...(report ?? { local: true }), notes: lines }));
950
+ return 0;
951
+ }
952
+
810
953
  // A mistyped command says so, signed in or not.
811
954
  if (commandHelp(command) === null) {
812
955
  throw new HarborError(noCommand(command));
@@ -1461,6 +1604,25 @@ export async function run(argv, io = {}) {
1461
1604
  },
1462
1605
  );
1463
1606
  }
1607
+ case "agent": {
1608
+ const { positionals, values, harbor } = parse(spec(["start"], ["agent"]));
1609
+ if (positionals[0] !== "report") {
1610
+ none(positionals);
1611
+ return call("agent_work", { harbor, agent: values.agent }, agentWorkText);
1612
+ }
1613
+ const [, task, ...words] = positionals;
1614
+ const note = words.join(" ").trim();
1615
+ if (!task || !note)
1616
+ throw new HarborError(
1617
+ 'Give the task and what the agent did: harbor agent report RR-4 "Drafted it".',
1618
+ );
1619
+ return call(
1620
+ "agent_update",
1621
+ { harbor, task, note, status: values.start ? "doing" : "review", agent: values.agent },
1622
+ (r) =>
1623
+ `${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.`,
1624
+ );
1625
+ }
1464
1626
  case "handback": {
1465
1627
  const { positionals, harbor } = parse(spec());
1466
1628
  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.37.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",