@workser/cli 0.6.1 → 0.6.2

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/index.js CHANGED
@@ -4485,6 +4485,76 @@ same rule.
4485
4485
  - **Cloud and local are different environments.** \`env set\` configures the
4486
4486
  cloud; the files in the app folder configure this computer. Don't hand-edit
4487
4487
  one to change the other.
4488
+ `
4489
+ },
4490
+ {
4491
+ topic: "goals",
4492
+ title: "Business goals & phases",
4493
+ summary: "The level above a task \u2014 a business requirement too big for one sitting, and the ordered phases that deliver it.",
4494
+ commands: ["goal"],
4495
+ source: "skills/workser/reference/goals.md",
4496
+ body: `# Business goals & phases
4497
+
4498
+ A **goal** is a business requirement \u2014 "customers can buy from my site" \u2014 that
4499
+ does not fit in one task. It carries an ordered list of **phase names**; a task
4500
+ joins one by carrying that name (\`workser task create --goal <id> --phase <name>\`).
4501
+ A phase has no existence apart from the tasks inside it, so it cannot drift from
4502
+ the work it names.
4503
+
4504
+ \`\`\`
4505
+ workser goal list
4506
+ workser goal show <id>
4507
+ workser goal create <title> --phase <name...> [--outcome <text>]
4508
+ [--criteria <json>]
4509
+ workser goal update <id> [--title <text>] [--outcome <text>]
4510
+ [--phase <name...>] [--status <value>]
4511
+ workser goal check <id> <criterionId> --phase <name> (--pass|--fail|--reset) [--note <text>]
4512
+ \`\`\`
4513
+
4514
+ Status is one of: proposed, agreed, working, delivered, abandoned.
4515
+
4516
+ ## A goal has to be argued for
4517
+
4518
+ Most requests are one task and should stay one task. Turning a two-hour job into
4519
+ four milestones buries the owner in ceremony before anything is built \u2014 use
4520
+ \`workser task create\` unless you can genuinely name two-plus slices that each
4521
+ deliver something the owner would notice.
4522
+
4523
+ \`\`\`
4524
+ workser goal create "Launch checkout" \\
4525
+ --phase "Cart" --phase "Payment" --phase "Receipts" \\
4526
+ --outcome "A customer can buy something and get a receipt"
4527
+ \`\`\`
4528
+
4529
+ \`--phase\` needs 2\u20136 names. Propose the shape only \u2014 nothing is created until the
4530
+ owner agrees it; planning phase four now is waste, since it will change once
4531
+ phase one is real.
4532
+
4533
+ ## Acceptance criteria are agreed with the shape
4534
+
4535
+ \`--criteria\` maps each phase name to the owner's own sentences about what "done"
4536
+ means for it, e.g. \`'{"Payment":["A customer can pay by card and gets a receipt"]}'\`.
4537
+ Written afterwards they only describe what got built; written with the shape they
4538
+ can still change the plan.
4539
+
4540
+ Record whether one is met with \`workser goal check\`:
4541
+
4542
+ \`\`\`
4543
+ workser goal check g_123 c1 --phase Payment --pass \\
4544
+ --note "Tested a card payment end to end; receipt emailed."
4545
+ \`\`\`
4546
+
4547
+ \`--note\` is required on \`--pass\` \u2014 a tick the owner cannot verify is worse than no
4548
+ tick.
4549
+
4550
+ ## Joining tasks to a goal
4551
+
4552
+ \`\`\`
4553
+ workser task create "Build the payment form" --goal g_123 --phase Payment
4554
+ \`\`\`
4555
+
4556
+ A goal's apps and progress are derived from the tasks that join it, not declared
4557
+ up front \u2014 most of the apps a goal will touch don't exist when it's proposed.
4488
4558
  `
4489
4559
  },
4490
4560
  {
@@ -7858,7 +7928,22 @@ var TYPES = [
7858
7928
  "approval",
7859
7929
  "confirmation",
7860
7930
  "file_upload",
7861
- "information"
7931
+ "information",
7932
+ /**
7933
+ * "Add an app to this project, and do it for me."
7934
+ *
7935
+ * NOT an approval with a well-worded message. What the user is agreeing to
7936
+ * is real infrastructure — a repository, hosting, a folder on their machine,
7937
+ * sometimes a database — and they are entitled to see WHICH app before they
7938
+ * agree, which a sentence and a Yes button cannot show. Needs `--app-type`;
7939
+ * `--app-name` is what it will be called.
7940
+ *
7941
+ * You cannot create an app yourself, and that is deliberate: an agent must
7942
+ * not be able to provision things on somebody's account by deciding to. This
7943
+ * asks. Their click is what creates it, and the answer tells you the new
7944
+ * app's id so the rest of your plan can be scoped to it.
7945
+ */
7946
+ "create_app"
7862
7947
  ];
7863
7948
  function registerAsk(program3) {
7864
7949
  program3.command("ask <message>").description("Ask the user a question and wait for their answer").option(
@@ -7870,6 +7955,12 @@ function registerAsk(program3) {
7870
7955
  "a choice (repeat for each; implies --type choice)",
7871
7956
  collect,
7872
7957
  []
7958
+ ).option(
7959
+ "--app-type <type>",
7960
+ "create_app only: web | mobile | api | worker | cron | python"
7961
+ ).option(
7962
+ "--app-name <name>",
7963
+ "create_app only: what the app should be called"
7873
7964
  ).option("--priority <level>", "low | normal | high | urgent", "normal").option(
7874
7965
  "--timeout <seconds>",
7875
7966
  "how long to wait before giving up (default: 600, max: 3600)",
@@ -7896,6 +7987,12 @@ function registerAsk(program3) {
7896
7987
  { code: "bad_request" }
7897
7988
  );
7898
7989
  }
7990
+ if (type === "create_app" && !opts.appType) {
7991
+ throw new WorkserError(
7992
+ "Say which kind of app: `--app-type mobile`.",
7993
+ { code: "bad_request" }
7994
+ );
7995
+ }
7899
7996
  const timeoutSeconds = Number(opts.timeout);
7900
7997
  if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
7901
7998
  throw new WorkserError("--timeout must be a positive number.", {
@@ -7909,6 +8006,8 @@ function registerAsk(program3) {
7909
8006
  title: opts.title || deriveTitle(message),
7910
8007
  message,
7911
8008
  options: options.length ? options : void 0,
8009
+ appType: opts.appType,
8010
+ appName: opts.appName,
7912
8011
  priority: opts.priority,
7913
8012
  timeoutSeconds
7914
8013
  }
@@ -8203,10 +8302,18 @@ function registerTask(program3) {
8203
8302
  action(async ({ ctx, args }) => {
8204
8303
  const id = args[0] || ctx.parentTaskId || resolveTaskId(ctx);
8205
8304
  const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
8206
- ok(row, () => printTask(row));
8305
+ const goalId = row.goal_id;
8306
+ const goal = goalId ? await api(
8307
+ ctx,
8308
+ `/v1/project-goals/${encodeURIComponent(goalId)}`
8309
+ ).catch(() => null) : null;
8310
+ ok({ ...row, goal }, () => printTask(row, goal));
8207
8311
  })
8208
8312
  );
8209
- task.command("create <title>").description("Open a new root task for the project team").option("--note <text>", "context, constraints and expected outcome").option("--kind <value>", `what it produces (${KINDS2.join(" | ")})`).option("--label <value...>", "labels to put on the task").option("--app <id...>", "apps the task touches").option("--infra <ref...>", "shared setup it touches").action(
8313
+ task.command("create <title>").description("Open a new root task for the project team").option("--note <text>", "context, constraints and expected outcome").option("--kind <value>", `what it produces (${KINDS2.join(" | ")})`).option("--label <value...>", "labels to put on the task").option("--app <id...>", "apps the task touches").option("--infra <ref...>", "shared setup it touches").option("--goal <id>", "the business goal this delivers part of").option(
8314
+ "--phase <name>",
8315
+ "which slice of that goal \u2014 must match one of its phase names"
8316
+ ).action(
8210
8317
  action(async ({ ctx, args, opts }) => {
8211
8318
  requireProject(ctx);
8212
8319
  if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
@@ -8219,6 +8326,10 @@ function registerTask(program3) {
8219
8326
  summary: opts.note,
8220
8327
  category: opts.kind,
8221
8328
  labels: opts.label,
8329
+ // The level above this task, when it has one. Most tasks do not —
8330
+ // a goal has to be argued for, never assumed.
8331
+ goalId: opts.goal,
8332
+ phase: opts.phase,
8222
8333
  targets: [
8223
8334
  ...(opts.app ?? []).map((appId) => ({ kind: "app", appId })),
8224
8335
  ...(opts.infra ?? []).map((ref) => ({ kind: "infra", ref }))
@@ -8271,13 +8382,13 @@ function registerTask(program3) {
8271
8382
  });
8272
8383
  })
8273
8384
  );
8274
- const subtask = task.command("subtask").description("The steps a task is broken into");
8385
+ const subtask = task.command("subtask").description("The subtasks a task is broken into");
8275
8386
  subtask.command("add <title>").description(
8276
- 'Add one step, e.g. `workser task subtask add "Build the upload screen" --role web`'
8277
- ).option("--task <id>", "the parent task (defaults to the task this run is inside)").option("--role <value>", `who does it (${ROLES.join(" | ")})`).option("--kind <value>", `what it produces (${KINDS2.join(" | ")})`).option("--note <text>", "one sentence on what this step does").option(
8387
+ 'Add one subtask, e.g. `workser task subtask add "Build the upload screen" --role web`'
8388
+ ).option("--task <id>", "the parent task (defaults to the task this run is inside)").option("--role <value>", `who does it (${ROLES.join(" | ")})`).option("--kind <value>", `what it produces (${KINDS2.join(" | ")})`).option("--note <text>", "one sentence on what this subtask does").option(
8278
8389
  "--app <id...>",
8279
- "the app this step is for \u2014 one id runs the step inside that app's folder; leave it off and the step runs at the project, seeing every app"
8280
- ).option("--infra <ref...>", "shared setup it touches (database | storage | auth | hosting | jobs)").option("--scope <path...>", "files or folders THIS step owns").option("--depends-on <id...>", "steps that must finish first (key or id)").action(
8390
+ "the app this subtask is for \u2014 one id runs it inside that app's folder; leave it off and it runs at the project, seeing every app"
8391
+ ).option("--infra <ref...>", "shared setup it touches (database | storage | auth | hosting | jobs)").option("--scope <path...>", "files or folders THIS subtask owns").option("--depends-on <id...>", "subtasks that must finish first (key or id)").action(
8281
8392
  action(async ({ ctx, args, opts }) => {
8282
8393
  const parent = resolveTaskId(ctx, opts.task);
8283
8394
  if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
@@ -8304,21 +8415,21 @@ function registerTask(program3) {
8304
8415
  });
8305
8416
  })
8306
8417
  );
8307
- subtask.command("list [taskId]").description("The steps of a task, in order").action(
8418
+ subtask.command("list [taskId]").description("The subtasks of a task, in order").action(
8308
8419
  action(async ({ ctx, args }) => {
8309
8420
  const id = resolveTaskId(ctx, args[0]);
8310
8421
  const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
8311
8422
  const rows = row.subtasks ?? [];
8312
8423
  ok(rows, () => {
8313
8424
  if (!rows.length) {
8314
- line(import_picocolors28.default.dim("No steps yet."));
8425
+ line(import_picocolors28.default.dim("No subtasks yet."));
8315
8426
  return;
8316
8427
  }
8317
8428
  rows.forEach((r, i) => line(formatSubtask(r, i + 1)));
8318
8429
  });
8319
8430
  })
8320
8431
  );
8321
- subtask.command("update <id>").description("Change a step \u2014 its title, its role, what it touches").option("--title <text>").option("--note <text>").option("--role <value>", ROLES.join(" | ")).option("--kind <value>", KINDS2.join(" | ")).option("--scope <path...>", "replaces the step's scope entirely").action(
8432
+ subtask.command("update <id>").description("Change a subtask \u2014 its title, its role, what it touches").option("--title <text>").option("--note <text>").option("--role <value>", ROLES.join(" | ")).option("--kind <value>", KINDS2.join(" | ")).option("--scope <path...>", "replaces the subtask's scope entirely").action(
8322
8433
  action(async ({ ctx, args, opts }) => {
8323
8434
  if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
8324
8435
  if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
@@ -8339,7 +8450,7 @@ function registerTask(program3) {
8339
8450
  ok(row, () => line(`${import_picocolors28.default.green("updated")} ${import_picocolors28.default.bold(row.title)}`));
8340
8451
  })
8341
8452
  );
8342
- subtask.command("remove <id>").description("Drop a step from the plan (only before the work starts)").action(
8453
+ subtask.command("remove <id>").description("Remove a subtask (only before the work starts)").action(
8343
8454
  action(async ({ ctx, args }) => {
8344
8455
  await api(ctx, `/v1/project-tasks/${encodeURIComponent(args[0])}`, {
8345
8456
  method: "DELETE"
@@ -8359,7 +8470,7 @@ function registerTask(program3) {
8359
8470
  })
8360
8471
  );
8361
8472
  task.command("resume [id]").description(
8362
- "Put a step the owner stopped back in the queue \u2014 only when they ask you to carry on"
8473
+ "Put a task the owner stopped back in the queue \u2014 only when they ask you to carry on"
8363
8474
  ).action(
8364
8475
  action(async ({ ctx, args }) => {
8365
8476
  const id = resolveTaskId(ctx, args[0]);
@@ -8442,7 +8553,7 @@ function registerTask(program3) {
8442
8553
  ok(row, () => line(`${import_picocolors28.default.green(row.approval_state)} ${import_picocolors28.default.bold(row.title)}`));
8443
8554
  })
8444
8555
  );
8445
- task.command("done [id]").description("Record what a step produced, and move it to ready").option("--summary <text>", "what changed, in the owner's words").action(
8556
+ task.command("done [id]").description("Record what a subtask produced, and move it to ready").option("--summary <text>", "what changed, in the owner's words").action(
8446
8557
  action(async ({ ctx, args, opts }) => {
8447
8558
  const id = resolveTaskId(ctx, args[0]);
8448
8559
  await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`, {
@@ -8483,7 +8594,7 @@ async function resolveDeps(ctx, parentId, given) {
8483
8594
  const id = byKey.get(g);
8484
8595
  if (!id) {
8485
8596
  throw new WorkserError(
8486
- `"${g}" is not a step of this task. Run \`workser task subtask list\` to see them.`,
8597
+ `"${g}" is not a subtask of this task. Run \`workser task subtask list\` to see them.`,
8487
8598
  { code: "bad_request" }
8488
8599
  );
8489
8600
  }
@@ -8515,11 +8626,12 @@ function formatSubtask(r, index) {
8515
8626
  return `${head}
8516
8627
  ${import_picocolors28.default.dim(wrapped)}`;
8517
8628
  }
8518
- function printTask(row) {
8629
+ function printTask(row, goal) {
8519
8630
  line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
8520
8631
  line(`${statusTag2(row.status)} approval: ${row.approval_state}`);
8521
8632
  if (row.summary) line(`
8522
8633
  ${row.summary}`);
8634
+ if (goal) printGoalContext(row, goal);
8523
8635
  if (row.targets?.length) {
8524
8636
  line(
8525
8637
  `
@@ -8528,23 +8640,53 @@ touches: ${row.targets.map((t) => t.appName ?? t.ref ?? t.kind).join(", ")}`
8528
8640
  }
8529
8641
  if (row.subtasks?.length) {
8530
8642
  line(`
8531
- ${import_picocolors28.default.bold("steps")}`);
8643
+ ${import_picocolors28.default.bold("subtasks")}`);
8532
8644
  row.subtasks.forEach((s, i) => line(formatSubtask(s, i + 1)));
8533
8645
  line(
8534
8646
  import_picocolors28.default.dim(
8535
8647
  `
8536
- Run \`workser artifact list\` to see what these steps produced,`
8648
+ Run \`workser artifact list\` to see what these subtasks produced,`
8537
8649
  )
8538
8650
  );
8539
8651
  line(
8540
8652
  import_picocolors28.default.dim(
8541
- `or \`workser artifact list --step <id>\` for one step's output alone.`
8653
+ `or \`workser artifact list --step <id>\` for one subtask's output alone.`
8542
8654
  )
8543
8655
  );
8544
8656
  } else {
8545
- line(import_picocolors28.default.dim("\nNo steps yet."));
8657
+ line(import_picocolors28.default.dim("\nNo subtasks yet."));
8546
8658
  }
8547
8659
  }
8660
+ function printGoalContext(row, goal) {
8661
+ const mine = row.phase ?? null;
8662
+ line(`
8663
+ ${import_picocolors28.default.bold("part of")}: ${goal.title}`);
8664
+ if (goal.outcome) line(import_picocolors28.default.dim(`done means: ${goal.outcome}`));
8665
+ const progress = goal.progress ?? [];
8666
+ for (const p of progress) {
8667
+ const where = p.total === 0 ? "not started" : `${p.done} of ${p.total} done`;
8668
+ const here = mine && p.name === mine ? import_picocolors28.default.cyan(" <- this task") : "";
8669
+ const mark = p.state === "done" ? import_picocolors28.default.green("*") : import_picocolors28.default.dim("-");
8670
+ line(` ${mark} ${p.name} \u2014 ${where}${here}`);
8671
+ }
8672
+ const next = progress.find((p) => p.state !== "done");
8673
+ const running = progress.some((p) => p.state === "working");
8674
+ if (next && !running) {
8675
+ line(
8676
+ import_picocolors28.default.dim(
8677
+ `
8678
+ Nothing is running. The next part waiting is "${next.name}" \u2014 offer it to them in a sentence rather than starting it unasked.`
8679
+ )
8680
+ );
8681
+ } else if (!next) {
8682
+ line(import_picocolors28.default.dim("\nEvery part of this plan is done. Say so, and offer to wrap it up."));
8683
+ }
8684
+ line(
8685
+ import_picocolors28.default.dim(
8686
+ "The plan is a reference for what was agreed, not a rule about what may run. Work can start on any part at any time if they ask for it."
8687
+ )
8688
+ );
8689
+ }
8548
8690
  function statusTag2(status) {
8549
8691
  switch (status) {
8550
8692
  case "ready":
@@ -8562,6 +8704,219 @@ function statusTag2(status) {
8562
8704
  }
8563
8705
  }
8564
8706
 
8707
+ // src/commands/goal.ts
8708
+ var import_picocolors29 = __toESM(require_picocolors(), 1);
8709
+ var STATUSES3 = ["proposed", "agreed", "working", "delivered", "abandoned"];
8710
+ function registerGoal(program3) {
8711
+ const goal = program3.command("goal").description("Business goals and the phases that deliver them");
8712
+ goal.command("list").description("What this project is working towards").action(
8713
+ action(async ({ ctx }) => {
8714
+ requireProject(ctx);
8715
+ const rows = await api(ctx, "/v1/project-goals", {
8716
+ query: { projectId: ctx.projectId }
8717
+ }) ?? [];
8718
+ ok(rows, () => {
8719
+ if (!rows.length) {
8720
+ line(import_picocolors29.default.dim("No goals yet \u2014 every task here stands on its own."));
8721
+ return;
8722
+ }
8723
+ for (const g of rows) line(formatGoal(g));
8724
+ });
8725
+ })
8726
+ );
8727
+ goal.command("show <id>").description("One goal: its phases, and how far each has got").action(
8728
+ action(async ({ ctx, args }) => {
8729
+ const row = await api(
8730
+ ctx,
8731
+ `/v1/project-goals/${encodeURIComponent(String(args[0]))}`
8732
+ );
8733
+ ok(row, () => printGoal(row));
8734
+ })
8735
+ );
8736
+ goal.command("create <title>").description(
8737
+ "Propose a business goal and the phases that deliver it \u2014 the owner agrees the shape before any task exists"
8738
+ ).option(
8739
+ "--outcome <text>",
8740
+ "what 'done' means for the whole thing, in the owner's words"
8741
+ ).option(
8742
+ "--phase <name...>",
8743
+ "ordered phase names \u2014 each must deliver something the owner would notice"
8744
+ ).option(
8745
+ "--criteria <json>",
8746
+ `what "done" means per phase, in the OWNER's words: '{"Checkout":["A customer can pay by card and gets a receipt"]}'`
8747
+ ).action(
8748
+ action(async ({ ctx, args, opts }) => {
8749
+ requireProject(ctx);
8750
+ const phases = opts.phase ?? [];
8751
+ if (phases.length < 2) {
8752
+ throw new WorkserError(
8753
+ "A goal needs at least two phases. If you can't name two slices that each deliver something the owner would notice, this is one task \u2014 use `workser task create`."
8754
+ );
8755
+ }
8756
+ if (phases.length > 6) {
8757
+ throw new WorkserError(
8758
+ "More than six phases is a conversation, not a plan. Propose the first few and agree the rest as you go."
8759
+ );
8760
+ }
8761
+ let criteria = {};
8762
+ if (opts.criteria) {
8763
+ try {
8764
+ criteria = JSON.parse(opts.criteria);
8765
+ } catch {
8766
+ throw new WorkserError(
8767
+ `--criteria must be JSON mapping each phase name to a list of sentences, e.g. '{"Checkout":["A customer can pay by card"]}'`
8768
+ );
8769
+ }
8770
+ const unknown = Object.keys(criteria).filter(
8771
+ (name) => !phases.includes(name)
8772
+ );
8773
+ if (unknown.length) {
8774
+ throw new WorkserError(
8775
+ `--criteria names phases that aren't in this goal: ${unknown.join(", ")}. Phases are: ${phases.join(", ")}.`
8776
+ );
8777
+ }
8778
+ }
8779
+ const row = await api(ctx, "/v1/project-goals", {
8780
+ body: {
8781
+ projectId: ctx.projectId,
8782
+ title: args[0],
8783
+ outcome: opts.outcome,
8784
+ phases: phases.map((name) => ({
8785
+ name,
8786
+ criteria: (criteria[name] ?? []).map((text, i) => ({
8787
+ id: `c${i + 1}`,
8788
+ text,
8789
+ met: null
8790
+ }))
8791
+ })),
8792
+ channelId: ctx.projectChannelId,
8793
+ sourceMessageId: ctx.projectChannelMessageId
8794
+ }
8795
+ });
8796
+ ok(row, () => {
8797
+ success(`Proposed ${import_picocolors29.default.bold(row?.title ?? "goal")}`);
8798
+ printGoal(row);
8799
+ line(
8800
+ import_picocolors29.default.dim(
8801
+ "\nNothing has been created yet. The owner agrees the shape first."
8802
+ )
8803
+ );
8804
+ });
8805
+ })
8806
+ );
8807
+ goal.command("check <id> <criterionId>").description("Record whether an acceptance criterion is met").requiredOption("--phase <name>", "which part of the plan it belongs to").option("--pass", "it is met").option("--fail", "it is not met").option("--reset", "back to unchecked").option("--note <text>", "what you checked, and how").action(
8808
+ action(async ({ ctx, args, opts }) => {
8809
+ const chosen = [opts.pass, opts.fail, opts.reset].filter(Boolean);
8810
+ if (chosen.length !== 1) {
8811
+ throw new WorkserError(
8812
+ "Pass exactly one of --pass, --fail or --reset."
8813
+ );
8814
+ }
8815
+ if (opts.pass && !opts.note) {
8816
+ throw new WorkserError(
8817
+ "--note is required on a pass: say what you checked. A tick the owner cannot verify is worse than no tick."
8818
+ );
8819
+ }
8820
+ const met = opts.pass ? true : opts.fail ? false : null;
8821
+ const row = await api(
8822
+ ctx,
8823
+ `/v1/project-goals/${encodeURIComponent(String(args[0]))}/criteria/${encodeURIComponent(String(args[1]))}`,
8824
+ { method: "POST", body: { phase: opts.phase, met, note: opts.note } }
8825
+ );
8826
+ ok(
8827
+ row,
8828
+ () => line(
8829
+ row?.recorded ? met === true ? import_picocolors29.default.green("recorded \u2014 met") : met === false ? import_picocolors29.default.yellow("recorded \u2014 not met") : import_picocolors29.default.dim("recorded \u2014 back to unchecked") : import_picocolors29.default.yellow(
8830
+ "couldn't record it \u2014 check the goal id, the phase name and the criterion id"
8831
+ )
8832
+ )
8833
+ );
8834
+ })
8835
+ );
8836
+ goal.command("update <id>").description("Change the shape, or move the goal along").option("--title <text>", "the owner's words for what they want").option("--outcome <text>", "what 'done' means").option("--phase <name...>", "replace the ordered phase list").option("--status <value>", `one of: ${STATUSES3.join(" | ")}`).action(
8837
+ action(async ({ ctx, args, opts }) => {
8838
+ if (opts.status && !STATUSES3.includes(opts.status)) {
8839
+ throw new WorkserError(
8840
+ `Unknown status "${opts.status}". Use one of: ${STATUSES3.join(", ")}.`
8841
+ );
8842
+ }
8843
+ const row = await api(
8844
+ ctx,
8845
+ `/v1/project-goals/${encodeURIComponent(String(args[0]))}`,
8846
+ {
8847
+ method: "PATCH",
8848
+ body: {
8849
+ title: opts.title,
8850
+ outcome: opts.outcome,
8851
+ phases: opts.phase,
8852
+ status: opts.status
8853
+ }
8854
+ }
8855
+ );
8856
+ ok(row, () => printGoal(row));
8857
+ })
8858
+ );
8859
+ }
8860
+ function appScope(g) {
8861
+ const n = g.appIds?.length ?? 0;
8862
+ if (!n) return "";
8863
+ return import_picocolors29.default.dim(` ${n} part${n === 1 ? "" : "s"} of the system`);
8864
+ }
8865
+ function formatGoal(g) {
8866
+ const where = g.currentPhase && g.status !== "delivered" ? import_picocolors29.default.dim(` now: ${g.currentPhase}`) : "";
8867
+ const count = g.taskTotal != null ? import_picocolors29.default.dim(` ${g.taskDone}/${g.taskTotal} done`) : "";
8868
+ return `${statusTag3(g.status)} ${g.title}${appScope(g)}${where}${count} ${import_picocolors29.default.dim(g.id)}`;
8869
+ }
8870
+ function printGoal(g) {
8871
+ if (!g) return;
8872
+ line(`${import_picocolors29.default.bold(g.title)} ${import_picocolors29.default.dim(g.id)}`);
8873
+ line(statusTag3(g.status));
8874
+ if (g.outcome) line(`
8875
+ done means: ${g.outcome}`);
8876
+ const progress = g.progress ?? [];
8877
+ if (!progress.length) {
8878
+ line(import_picocolors29.default.dim("\nNo phases agreed yet."));
8879
+ return;
8880
+ }
8881
+ line(`
8882
+ ${import_picocolors29.default.bold("phases")}`);
8883
+ for (const p of progress) {
8884
+ const bar2 = p.total > 0 ? `${p.done}/${p.total} done` : import_picocolors29.default.dim("nothing filed yet");
8885
+ const mark = p.state === "done" ? import_picocolors29.default.green("done") : p.state === "working" ? import_picocolors29.default.blue("working") : import_picocolors29.default.dim("waiting");
8886
+ line(
8887
+ ` ${import_picocolors29.default.dim(String(p.index).padStart(2, "0"))} ${p.name} ${mark} ${import_picocolors29.default.dim(bar2)}`
8888
+ );
8889
+ for (const c of p.criteria ?? []) {
8890
+ const tick = c.met === true ? import_picocolors29.default.green("\u2713") : c.met === false ? import_picocolors29.default.red("\u2717") : import_picocolors29.default.dim("\xB7");
8891
+ line(` ${tick} ${c.text} ${import_picocolors29.default.dim(c.id)}`);
8892
+ if (c.note) line(import_picocolors29.default.dim(` ${c.note}`));
8893
+ }
8894
+ }
8895
+ const current = progress.find((p) => p.name === g.currentPhase);
8896
+ if (current) {
8897
+ line(
8898
+ import_picocolors29.default.dim(
8899
+ `
8900
+ ${current.name} \u2014 ${current.done} of ${current.total} done (phase ${current.index} of ${progress.length}).`
8901
+ )
8902
+ );
8903
+ }
8904
+ }
8905
+ function statusTag3(status) {
8906
+ switch (status) {
8907
+ case "agreed":
8908
+ return import_picocolors29.default.cyan("[agreed]");
8909
+ case "working":
8910
+ return import_picocolors29.default.blue("[working]");
8911
+ case "delivered":
8912
+ return import_picocolors29.default.green("[delivered]");
8913
+ case "abandoned":
8914
+ return import_picocolors29.default.dim("[abandoned]");
8915
+ default:
8916
+ return import_picocolors29.default.yellow("[proposed]");
8917
+ }
8918
+ }
8919
+
8565
8920
  // src/role-guard.ts
8566
8921
  var READS = [
8567
8922
  // `requirement` sits beside `doc` and `decision` because it is the same kind
@@ -8599,7 +8954,19 @@ var READS = [
8599
8954
  // Reading the plan and what is used against it. An agent proposing "add
8600
8955
  // another project" can only sensibly propose it if it can find out the plan
8601
8956
  // allows two and two already exist.
8602
- "usage"
8957
+ "usage",
8958
+ // RECORDING WHAT YOU PRODUCED IS NOT CHANGING THE PROJECT.
8959
+ // Every dispatched role is told, in its own preamble, to run `workser
8960
+ // artifact list` before non-trivial work and `workser artifact add` when it
8961
+ // finishes something the owner should get. Five roles — pm, qa, security,
8962
+ // analyst, sre — were then refused the verb here, so the instruction and the
8963
+ // guard contradicted each other: the agent kept trying to obey an order this
8964
+ // file would not let it obey, and burned its run doing it. It belongs beside
8965
+ // `doc`, `decision` and `board`, which are already in this list and also
8966
+ // have `create` subcommands — these write the project's RECORD, not the
8967
+ // project. What stops a reviewer editing code is its filesystem mode, and
8968
+ // that is untouched.
8969
+ "artifact"
8603
8970
  ];
8604
8971
  var BUILDS = [
8605
8972
  ...READS,
@@ -8608,7 +8975,6 @@ var BUILDS = [
8608
8975
  "db",
8609
8976
  "storage",
8610
8977
  "checkpoint",
8611
- "artifact",
8612
8978
  "image",
8613
8979
  "design",
8614
8980
  "ask",
@@ -8644,6 +9010,33 @@ var NEVER = {
8644
9010
  // proposed has removed the only gate this product has.
8645
9011
  "task approval": "Only the owner can approve a plan."
8646
9012
  };
9013
+ var READ_PAIRS = /* @__PURE__ */ new Set([
9014
+ // The project's own brand and design notes.
9015
+ "design show",
9016
+ // What exists, and what it is wired to.
9017
+ "app list",
9018
+ "app tools",
9019
+ // Shape, never contents-by-arbitrary-SQL.
9020
+ "db list",
9021
+ "db tables",
9022
+ "db schema",
9023
+ // What is stored, not what is in it.
9024
+ "storage list",
9025
+ "storage ls",
9026
+ "checkpoint list",
9027
+ "workflow list",
9028
+ "workflow get",
9029
+ "workflow runs",
9030
+ "workflow nodes",
9031
+ "tool list",
9032
+ // The business data an analyst exists to read.
9033
+ "business resources",
9034
+ "business list",
9035
+ "business get",
9036
+ // The database service's own inventory.
9037
+ "neon status",
9038
+ "neon list"
9039
+ ]);
8647
9040
  function assertRoleMayRun(argv) {
8648
9041
  const role = (process.env.WORKSER_ROLE ?? "").trim();
8649
9042
  if (!role) return;
@@ -8654,6 +9047,7 @@ function assertRoleMayRun(argv) {
8654
9047
  if (NEVER[pair] && !(pair === "task approval" && (commandArgv[2] === "request" || !commandArgv[2]))) {
8655
9048
  throw new WorkserError(NEVER[pair], { code: "role_forbidden" });
8656
9049
  }
9050
+ if (READ_PAIRS.has(pair)) return;
8657
9051
  const allowed = ROLE_VERBS[role];
8658
9052
  const list = allowed ?? READS;
8659
9053
  if (!list.includes(verb)) {
@@ -8694,7 +9088,7 @@ function stripLeadingGlobalOptions(argv) {
8694
9088
  }
8695
9089
 
8696
9090
  // src/commands/decision.ts
8697
- var import_picocolors29 = __toESM(require_picocolors(), 1);
9091
+ var import_picocolors30 = __toESM(require_picocolors(), 1);
8698
9092
  function registerDecision(program3) {
8699
9093
  const decision = program3.command("decision").description("Read and record the project's architecture decisions");
8700
9094
  decision.command("list").description("Every decision on record \u2014 read this before changing how something works").option("--limit <n>", "cap the number returned (newest first)").action(
@@ -8707,11 +9101,11 @@ function registerDecision(program3) {
8707
9101
  rows = applyLimit(rows, opts.limit);
8708
9102
  ok(rows, () => {
8709
9103
  if (!rows.length) {
8710
- line(import_picocolors29.default.dim("No decisions recorded yet."));
9104
+ line(import_picocolors30.default.dim("No decisions recorded yet."));
8711
9105
  return;
8712
9106
  }
8713
9107
  for (const r of rows) {
8714
- line(`${import_picocolors29.default.dim(r.id)} ${import_picocolors29.default.dim(shortDate(r.createdAt))} ${r.title}`);
9108
+ line(`${import_picocolors30.default.dim(r.id)} ${import_picocolors30.default.dim(shortDate(r.createdAt))} ${r.title}`);
8715
9109
  line(` ${truncate(r.decision, 100)}`);
8716
9110
  }
8717
9111
  });
@@ -8725,16 +9119,16 @@ function registerDecision(program3) {
8725
9119
  `/v1/projects/${projectId}/architecture-decisions/${args[0]}`
8726
9120
  );
8727
9121
  ok(row, () => {
8728
- line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
8729
- line(import_picocolors29.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
9122
+ line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
9123
+ line(import_picocolors30.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
8730
9124
  line(`
8731
- ${import_picocolors29.default.bold("Context")}
9125
+ ${import_picocolors30.default.bold("Context")}
8732
9126
  ${row.context}`);
8733
9127
  line(`
8734
- ${import_picocolors29.default.bold("Decision")}
9128
+ ${import_picocolors30.default.bold("Decision")}
8735
9129
  ${row.decision}`);
8736
9130
  if (row.consequences) line(`
8737
- ${import_picocolors29.default.bold("Consequences")}
9131
+ ${import_picocolors30.default.bold("Consequences")}
8738
9132
  ${row.consequences}`);
8739
9133
  });
8740
9134
  })
@@ -8763,7 +9157,7 @@ ${row.consequences}`);
8763
9157
  refId: row?.id,
8764
9158
  output: { decision: row }
8765
9159
  });
8766
- ok(row, () => line(`Recorded decision ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
9160
+ ok(row, () => line(`Recorded decision ${import_picocolors30.default.bold(row?.id ?? "")} \u2014 ${title}`));
8767
9161
  })
8768
9162
  );
8769
9163
  const requirement = program3.command("requirement").description("Read and record the project's requirements");
@@ -8775,11 +9169,11 @@ ${row.consequences}`);
8775
9169
  rows = applyLimit(rows, opts.limit);
8776
9170
  ok(rows, () => {
8777
9171
  if (!rows.length) {
8778
- line(import_picocolors29.default.dim("No requirements recorded yet."));
9172
+ line(import_picocolors30.default.dim("No requirements recorded yet."));
8779
9173
  return;
8780
9174
  }
8781
9175
  for (const r of rows) {
8782
- line(`${import_picocolors29.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
9176
+ line(`${import_picocolors30.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
8783
9177
  }
8784
9178
  });
8785
9179
  })
@@ -8792,8 +9186,8 @@ ${row.consequences}`);
8792
9186
  `/v1/projects/${projectId}/requirements/${args[0]}`
8793
9187
  );
8794
9188
  ok(row, () => {
8795
- line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
8796
- line(import_picocolors29.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
9189
+ line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
9190
+ line(import_picocolors30.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
8797
9191
  line(`
8798
9192
  ${row.body}`);
8799
9193
  });
@@ -8819,7 +9213,7 @@ ${row.body}`);
8819
9213
  refId: row?.id,
8820
9214
  output: { requirement: row }
8821
9215
  });
8822
- ok(row, () => line(`Recorded requirement ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
9216
+ ok(row, () => line(`Recorded requirement ${import_picocolors30.default.bold(row?.id ?? "")} \u2014 ${title}`));
8823
9217
  })
8824
9218
  );
8825
9219
  requirement.command("update <id>").description(
@@ -8848,7 +9242,7 @@ ${row.body}`);
8848
9242
  code: "bad_request"
8849
9243
  });
8850
9244
  }
8851
- ok(row, () => line(`Updated requirement ${import_picocolors29.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
9245
+ ok(row, () => line(`Updated requirement ${import_picocolors30.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
8852
9246
  })
8853
9247
  );
8854
9248
  }
@@ -8871,7 +9265,7 @@ function shortDate(iso) {
8871
9265
  }
8872
9266
 
8873
9267
  // src/commands/doc.ts
8874
- var import_picocolors30 = __toESM(require_picocolors(), 1);
9268
+ var import_picocolors31 = __toESM(require_picocolors(), 1);
8875
9269
 
8876
9270
  // src/mermaid-fences.ts
8877
9271
  function hasDiagram(code) {
@@ -8916,13 +9310,13 @@ function registerDoc(program3) {
8916
9310
  }) ?? [];
8917
9311
  ok(rows, () => {
8918
9312
  if (!rows.length) {
8919
- line(import_picocolors30.default.dim("No documents yet."));
9313
+ line(import_picocolors31.default.dim("No documents yet."));
8920
9314
  return;
8921
9315
  }
8922
9316
  for (const r of rows) {
8923
- const link = r.workItemId ? import_picocolors30.default.dim(` \u21B3 ${r.workItemId}`) : "";
8924
- const file = r.filePath ? import_picocolors30.default.dim(` ${r.filePath}`) : "";
8925
- line(`${import_picocolors30.default.dim(r.id)} ${r.title}${link}${file}`);
9317
+ const link = r.workItemId ? import_picocolors31.default.dim(` \u21B3 ${r.workItemId}`) : "";
9318
+ const file = r.filePath ? import_picocolors31.default.dim(` ${r.filePath}`) : "";
9319
+ line(`${import_picocolors31.default.dim(r.id)} ${r.title}${link}${file}`);
8926
9320
  }
8927
9321
  });
8928
9322
  })
@@ -8936,17 +9330,17 @@ function registerDoc(program3) {
8936
9330
  );
8937
9331
  if (opts.markdown) {
8938
9332
  ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
8939
- line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
9333
+ line(`${import_picocolors31.default.bold(row.title)} ${import_picocolors31.default.dim(row.id)}`);
8940
9334
  line(
8941
- row.filePath ? `Read it at ${import_picocolors30.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors30.default.dim("This document has no markdown mirror on disk yet.")
9335
+ row.filePath ? `Read it at ${import_picocolors31.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors31.default.dim("This document has no markdown mirror on disk yet.")
8942
9336
  );
8943
9337
  });
8944
9338
  return;
8945
9339
  }
8946
9340
  ok(row, () => {
8947
- line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
8948
- if (row.workItemId) line(import_picocolors30.default.dim(`linked to work item ${row.workItemId}`));
8949
- if (row.filePath) line(import_picocolors30.default.dim(`markdown mirror: ${row.filePath}`));
9341
+ line(`${import_picocolors31.default.bold(row.title)} ${import_picocolors31.default.dim(row.id)}`);
9342
+ if (row.workItemId) line(import_picocolors31.default.dim(`linked to work item ${row.workItemId}`));
9343
+ if (row.filePath) line(import_picocolors31.default.dim(`markdown mirror: ${row.filePath}`));
8950
9344
  line("");
8951
9345
  line(row.contentJson);
8952
9346
  });
@@ -8975,7 +9369,7 @@ function registerDoc(program3) {
8975
9369
  refId: row?.id,
8976
9370
  output: { document: row }
8977
9371
  });
8978
- ok(row, () => line(`Created document ${import_picocolors30.default.bold(row?.id ?? "")} \u2014 ${title}`));
9372
+ ok(row, () => line(`Created document ${import_picocolors31.default.bold(row?.id ?? "")} \u2014 ${title}`));
8979
9373
  })
8980
9374
  );
8981
9375
  doc.command("diagram <id>").description(
@@ -9001,19 +9395,19 @@ function registerDoc(program3) {
9001
9395
  diagrams: diagrams.map((code) => ({ kind: diagramKind(code), code }))
9002
9396
  };
9003
9397
  ok(payload, () => {
9004
- line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
9398
+ line(`${import_picocolors31.default.bold(row.title)} ${import_picocolors31.default.dim(row.id)}`);
9005
9399
  if (markdown === null) {
9006
9400
  line(
9007
- import_picocolors30.default.dim(
9401
+ import_picocolors31.default.dim(
9008
9402
  row.filePath ? `Could not read ${row.filePath} from this folder.` : "This document has no markdown mirror on disk yet."
9009
9403
  )
9010
9404
  );
9011
9405
  } else if (!diagrams.length) {
9012
- line(import_picocolors30.default.dim("No diagrams in this document."));
9406
+ line(import_picocolors31.default.dim("No diagrams in this document."));
9013
9407
  } else {
9014
9408
  for (const [i, code] of diagrams.entries()) {
9015
9409
  const kind = diagramKind(code) ?? "diagram";
9016
- line(`${import_picocolors30.default.dim(String(i + 1))} ${kind} ${import_picocolors30.default.dim(`${code.split("\n").length} lines`)}`);
9410
+ line(`${import_picocolors31.default.dim(String(i + 1))} ${kind} ${import_picocolors31.default.dim(`${code.split("\n").length} lines`)}`);
9017
9411
  }
9018
9412
  }
9019
9413
  });
@@ -9049,7 +9443,7 @@ function registerDoc(program3) {
9049
9443
  code: "bad_request"
9050
9444
  });
9051
9445
  }
9052
- ok(row, () => line(`Updated document ${import_picocolors30.default.bold(row.id)} \u2014 ${row.title}`));
9446
+ ok(row, () => line(`Updated document ${import_picocolors31.default.bold(row.id)} \u2014 ${row.title}`));
9053
9447
  })
9054
9448
  );
9055
9449
  }
@@ -9063,7 +9457,7 @@ function readMirror(cwd, filePath) {
9063
9457
  }
9064
9458
 
9065
9459
  // src/commands/design.ts
9066
- var import_picocolors31 = __toESM(require_picocolors(), 1);
9460
+ var import_picocolors32 = __toESM(require_picocolors(), 1);
9067
9461
  function registerDesign(program3) {
9068
9462
  const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
9069
9463
  design.command("show").description("Show this project's brand \u2014 read it before writing any UI").option("--raw", "print the generated token files verbatim instead of a summary").action(
@@ -9077,11 +9471,11 @@ function registerDesign(program3) {
9077
9471
  if (opts.raw) {
9078
9472
  ok(files, () => {
9079
9473
  if (!files.length) {
9080
- line(import_picocolors31.default.dim("No brand set for this project."));
9474
+ line(import_picocolors32.default.dim("No brand set for this project."));
9081
9475
  return;
9082
9476
  }
9083
9477
  for (const f of files) {
9084
- line(import_picocolors31.default.bold(f.path));
9478
+ line(import_picocolors32.default.bold(f.path));
9085
9479
  line(f.contents);
9086
9480
  line("");
9087
9481
  }
@@ -9098,21 +9492,21 @@ function registerDesign(program3) {
9098
9492
  } : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
9099
9493
  ok(summary, () => {
9100
9494
  if (!tokens) {
9101
- line(import_picocolors31.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
9495
+ line(import_picocolors32.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
9102
9496
  return;
9103
9497
  }
9104
9498
  for (const [name, value] of Object.entries(tokens.brand)) {
9105
- line(`${import_picocolors31.default.dim(name.padEnd(12))} ${value}`);
9499
+ line(`${import_picocolors32.default.dim(name.padEnd(12))} ${value}`);
9106
9500
  }
9107
9501
  for (const [name, value] of Object.entries(tokens.color)) {
9108
- line(`${import_picocolors31.default.dim(`color.${name}`.padEnd(12))} ${value}`);
9502
+ line(`${import_picocolors32.default.dim(`color.${name}`.padEnd(12))} ${value}`);
9109
9503
  }
9110
9504
  for (const [name, value] of Object.entries(tokens.font)) {
9111
- line(`${import_picocolors31.default.dim(`font.${name}`.padEnd(12))} ${value}`);
9505
+ line(`${import_picocolors32.default.dim(`font.${name}`.padEnd(12))} ${value}`);
9112
9506
  }
9113
9507
  line("");
9114
9508
  line(
9115
- import_picocolors31.default.dim(
9509
+ import_picocolors32.default.dim(
9116
9510
  `Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
9117
9511
  )
9118
9512
  );
@@ -9143,7 +9537,7 @@ function unwrap(group) {
9143
9537
  }
9144
9538
 
9145
9539
  // src/commands/api.ts
9146
- var import_picocolors32 = __toESM(require_picocolors(), 1);
9540
+ var import_picocolors33 = __toESM(require_picocolors(), 1);
9147
9541
  import { readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
9148
9542
  import { join as join3, relative, sep } from "path";
9149
9543
 
@@ -9257,10 +9651,10 @@ function registerApi(program3) {
9257
9651
  const appId = requireApp(opts.app);
9258
9652
  const res = await api(ctx, `/v1/apps/${encodeURIComponent(appId)}/api/requests`);
9259
9653
  ok(res, () => {
9260
- for (const note of res?.notes ?? []) line(import_picocolors32.default.dim(note));
9654
+ for (const note of res?.notes ?? []) line(import_picocolors33.default.dim(note));
9261
9655
  for (const r of res?.requests ?? []) {
9262
9656
  line(
9263
- `${import_picocolors32.default.dim(r.method.padEnd(6))}${r.path}${r.note ? import_picocolors32.default.dim(` ${r.note}`) : ""}`
9657
+ `${import_picocolors33.default.dim(r.method.padEnd(6))}${r.path}${r.note ? import_picocolors33.default.dim(` ${r.note}`) : ""}`
9264
9658
  );
9265
9659
  }
9266
9660
  });
@@ -9294,14 +9688,14 @@ function registerApi(program3) {
9294
9688
  );
9295
9689
  ok(res, () => {
9296
9690
  if (!res?.ok) {
9297
- line(import_picocolors32.default.red(res?.error ?? "The service did not answer."));
9691
+ line(import_picocolors33.default.red(res?.error ?? "The service did not answer."));
9298
9692
  return;
9299
9693
  }
9300
9694
  const code = `${res.status}${res.statusText ? ` ${res.statusText}` : ""}`;
9301
- const colour2 = res.status && res.status < 300 ? import_picocolors32.default.green : res.status && res.status < 500 ? import_picocolors32.default.yellow : import_picocolors32.default.red;
9302
- line(`${colour2(code)} ${import_picocolors32.default.dim(`${res.durationMs}ms ${res.url}`)}`);
9695
+ const colour2 = res.status && res.status < 300 ? import_picocolors33.default.green : res.status && res.status < 500 ? import_picocolors33.default.yellow : import_picocolors33.default.red;
9696
+ line(`${colour2(code)} ${import_picocolors33.default.dim(`${res.durationMs}ms ${res.url}`)}`);
9303
9697
  if (res.body) line(res.body);
9304
- if (res.truncated) line(import_picocolors32.default.dim("(answer truncated)"));
9698
+ if (res.truncated) line(import_picocolors33.default.dim("(answer truncated)"));
9305
9699
  });
9306
9700
  if (!res?.ok) process.exitCode = 1;
9307
9701
  })
@@ -9321,15 +9715,15 @@ function registerApi(program3) {
9321
9715
  for (const r of report.routes) {
9322
9716
  const known = report.missing.some((m) => m.path === r.path);
9323
9717
  line(
9324
- `${known ? import_picocolors32.default.yellow("undocumented") : import_picocolors32.default.green("documented ")} ${r.path}${import_picocolors32.default.dim(` ${r.file}`)}`
9718
+ `${known ? import_picocolors33.default.yellow("undocumented") : import_picocolors33.default.green("documented ")} ${r.path}${import_picocolors33.default.dim(` ${r.file}`)}`
9325
9719
  );
9326
9720
  }
9327
9721
  for (const p of report.stale) {
9328
- line(`${import_picocolors32.default.dim("in spec only ")} ${p}`);
9722
+ line(`${import_picocolors33.default.dim("in spec only ")} ${p}`);
9329
9723
  }
9330
9724
  line("");
9331
9725
  if (report.ok && specFile) success(summary);
9332
- else line(import_picocolors32.default.yellow(summary));
9726
+ else line(import_picocolors33.default.yellow(summary));
9333
9727
  });
9334
9728
  if (opts.check && !report.ok) {
9335
9729
  throw new WorkserError(summary, { code: "bad_request" });
@@ -9400,7 +9794,7 @@ function listRepoFiles(root, maxDepth = 8) {
9400
9794
  }
9401
9795
 
9402
9796
  // src/commands/analysis.ts
9403
- var import_picocolors33 = __toESM(require_picocolors(), 1);
9797
+ var import_picocolors34 = __toESM(require_picocolors(), 1);
9404
9798
  import { readFileSync as readFileSync4 } from "fs";
9405
9799
  function registerAnalysis(program3) {
9406
9800
  const cmd = program3.command("analysis").description("Run Python analysis locally, recorded in the task");
@@ -9410,14 +9804,14 @@ function registerAnalysis(program3) {
9410
9804
  const res = await api(ctx, path);
9411
9805
  ok(res, () => {
9412
9806
  line(
9413
- `${res?.available ? import_picocolors33.default.green("python") : import_picocolors33.default.red("python")} ${res?.version ?? "not found"} ${import_picocolors33.default.dim(res?.python ?? "")}`
9807
+ `${res?.available ? import_picocolors34.default.green("python") : import_picocolors34.default.red("python")} ${res?.version ?? "not found"} ${import_picocolors34.default.dim(res?.python ?? "")}`
9414
9808
  );
9415
9809
  for (const lib of res?.libraries ?? []) {
9416
9810
  line(
9417
- `${lib.present ? import_picocolors33.default.green(lib.name) : import_picocolors33.default.yellow(lib.name)}${import_picocolors33.default.dim(lib.present ? "" : " missing")}`
9811
+ `${lib.present ? import_picocolors34.default.green(lib.name) : import_picocolors34.default.yellow(lib.name)}${import_picocolors34.default.dim(lib.present ? "" : " missing")}`
9418
9812
  );
9419
9813
  }
9420
- for (const note of res?.notes ?? []) line(import_picocolors33.default.dim(note));
9814
+ for (const note of res?.notes ?? []) line(import_picocolors34.default.dim(note));
9421
9815
  });
9422
9816
  if (!res?.available) process.exitCode = 1;
9423
9817
  })
@@ -9439,15 +9833,15 @@ function registerAnalysis(program3) {
9439
9833
  );
9440
9834
  ok(res, () => {
9441
9835
  if (res?.stdout) line(res.stdout.replace(/\n$/, ""));
9442
- if (res?.stderr) line(import_picocolors33.default.dim(res.stderr.replace(/\n$/, "")));
9443
- if (res?.truncated) line(import_picocolors33.default.dim("(output truncated)"));
9836
+ if (res?.stderr) line(import_picocolors34.default.dim(res.stderr.replace(/\n$/, "")));
9837
+ if (res?.truncated) line(import_picocolors34.default.dim("(output truncated)"));
9444
9838
  const took = `${Math.round((res?.durationMs ?? 0) / 100) / 10}s`;
9445
9839
  line(
9446
- res?.ok ? import_picocolors33.default.green(`\u2713 ${res.summary ?? "It finished."}`) + import_picocolors33.default.dim(` ${took}`) : import_picocolors33.default.yellow(res?.summary ?? "It did not finish.") + import_picocolors33.default.dim(` ${took}`)
9840
+ res?.ok ? import_picocolors34.default.green(`\u2713 ${res.summary ?? "It finished."}`) + import_picocolors34.default.dim(` ${took}`) : import_picocolors34.default.yellow(res?.summary ?? "It did not finish.") + import_picocolors34.default.dim(` ${took}`)
9447
9841
  );
9448
9842
  if (res && !res.sandboxed) {
9449
9843
  line(
9450
- import_picocolors33.default.dim(
9844
+ import_picocolors34.default.dim(
9451
9845
  "This platform has no OS sandbox, so the script ran with your own file access."
9452
9846
  )
9453
9847
  );
@@ -9475,7 +9869,7 @@ function readCode(file, inline) {
9475
9869
  }
9476
9870
 
9477
9871
  // src/commands/scan.ts
9478
- var import_picocolors34 = __toESM(require_picocolors(), 1);
9872
+ var import_picocolors35 = __toESM(require_picocolors(), 1);
9479
9873
  import { spawnSync as spawnSync2 } from "child_process";
9480
9874
  import { existsSync as existsSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
9481
9875
  import { join as join4, relative as relative2, sep as sep2 } from "path";
@@ -9759,22 +10153,22 @@ function runPermissions(cwd, findings, checked, skipped) {
9759
10153
  }
9760
10154
  function print2(report, summary) {
9761
10155
  for (const s of report.skipped) {
9762
- line(`${import_picocolors34.default.yellow("not checked")} ${s.check}${import_picocolors34.default.dim(` \u2014 ${s.reason}`)}`);
10156
+ line(`${import_picocolors35.default.yellow("not checked")} ${s.check}${import_picocolors35.default.dim(` \u2014 ${s.reason}`)}`);
9763
10157
  }
9764
10158
  for (const f of report.findings) {
9765
- const where = f.file ? import_picocolors34.default.dim(` ${f.file}${f.line ? `:${f.line}` : ""}`) : "";
10159
+ const where = f.file ? import_picocolors35.default.dim(` ${f.file}${f.line ? `:${f.line}` : ""}`) : "";
9766
10160
  line(`${severityTag(f.severity)} ${f.title}${where}`);
9767
- line(` ${import_picocolors34.default.dim(f.fix)}`);
10161
+ line(` ${import_picocolors35.default.dim(f.fix)}`);
9768
10162
  }
9769
10163
  if (report.findings.length || report.skipped.length) line("");
9770
10164
  if (report.ok && !report.skipped.length) success(summary);
9771
- else if (report.ok) line(import_picocolors34.default.yellow(summary));
9772
- else line(import_picocolors34.default.red(summary));
10165
+ else if (report.ok) line(import_picocolors35.default.yellow(summary));
10166
+ else line(import_picocolors35.default.red(summary));
9773
10167
  }
9774
10168
  function severityTag(severity) {
9775
- if (severity === "high") return import_picocolors34.default.red("serious ");
9776
- if (severity === "medium") return import_picocolors34.default.yellow("worth fixing");
9777
- return import_picocolors34.default.dim("minor ");
10169
+ if (severity === "high") return import_picocolors35.default.red("serious ");
10170
+ if (severity === "medium") return import_picocolors35.default.yellow("worth fixing");
10171
+ return import_picocolors35.default.dim("minor ");
9778
10172
  }
9779
10173
  function git(cwd, args) {
9780
10174
  try {
@@ -9835,7 +10229,7 @@ function listRepoFiles2(root, maxDepth = 8) {
9835
10229
  }
9836
10230
 
9837
10231
  // src/commands/health.ts
9838
- var import_picocolors35 = __toESM(require_picocolors(), 1);
10232
+ var import_picocolors36 = __toESM(require_picocolors(), 1);
9839
10233
  function registerHealth(program3) {
9840
10234
  program3.command("health").description("Check that the published apps in this project are still answering").option("--app <webAppId>", "check one app rather than all of them").action(
9841
10235
  action(async ({ ctx, opts }) => {
@@ -9849,16 +10243,16 @@ function registerHealth(program3) {
9849
10243
  }
9850
10244
  function print3(res) {
9851
10245
  if (!res?.checks?.length) {
9852
- line(import_picocolors35.default.dim(res?.note ?? "Nothing to check."));
10246
+ line(import_picocolors36.default.dim(res?.note ?? "Nothing to check."));
9853
10247
  return;
9854
10248
  }
9855
10249
  for (const c of res.checks) {
9856
- const mark = c.ok ? import_picocolors35.default.green("up ") : import_picocolors35.default.red("down");
9857
- const timing = import_picocolors35.default.dim(`${c.ms}ms`);
9858
- const detail = c.ok ? timing : import_picocolors35.default.dim(`${c.error ?? "no answer"}${c.failures > 1 ? ` \xB7 ${c.failures} in a row` : ""}`);
9859
- line(` ${mark} ${c.appName} ${import_picocolors35.default.dim(`(${c.environment})`)} ${c.url} ${detail}`);
10250
+ const mark = c.ok ? import_picocolors36.default.green("up ") : import_picocolors36.default.red("down");
10251
+ const timing = import_picocolors36.default.dim(`${c.ms}ms`);
10252
+ const detail = c.ok ? timing : import_picocolors36.default.dim(`${c.error ?? "no answer"}${c.failures > 1 ? ` \xB7 ${c.failures} in a row` : ""}`);
10253
+ line(` ${mark} ${c.appName} ${import_picocolors36.default.dim(`(${c.environment})`)} ${c.url} ${detail}`);
9860
10254
  if (c.incidentOpened) {
9861
- line(import_picocolors35.default.yellow(` An incident has been opened on the board for this.`));
10255
+ line(import_picocolors36.default.yellow(` An incident has been opened on the board for this.`));
9862
10256
  }
9863
10257
  }
9864
10258
  const down = res.checks.filter((c) => !c.ok);
@@ -9871,14 +10265,14 @@ function print3(res) {
9871
10265
  }
9872
10266
  const production = down.filter((c) => c.environment === "production").length;
9873
10267
  line(
9874
- import_picocolors35.default.red(
10268
+ import_picocolors36.default.red(
9875
10269
  `${down.length} of ${res.checks.length} not answering` + (production ? ` \u2014 ${production} customer-facing.` : " (preview only).")
9876
10270
  )
9877
10271
  );
9878
10272
  }
9879
10273
 
9880
10274
  // src/commands/urls.ts
9881
- var import_picocolors36 = __toESM(require_picocolors(), 1);
10275
+ var import_picocolors37 = __toESM(require_picocolors(), 1);
9882
10276
  function registerUrls(program3) {
9883
10277
  program3.command("urls").description("The stable preview and production addresses of every app in this project").option("--app <webAppId>", "just one app").action(
9884
10278
  action(async ({ ctx, opts }) => {
@@ -9892,20 +10286,20 @@ function registerUrls(program3) {
9892
10286
  const summary = urlsSummary(rows);
9893
10287
  ok({ rows, summary }, () => {
9894
10288
  for (const row of rows) {
9895
- const label = import_picocolors36.default.dim(row.environment.padEnd(10));
9896
- const value = row.url ? import_picocolors36.default.cyan(row.url) : import_picocolors36.default.dim(row.note ?? "not published");
10289
+ const label = import_picocolors37.default.dim(row.environment.padEnd(10));
10290
+ const value = row.url ? import_picocolors37.default.cyan(row.url) : import_picocolors37.default.dim(row.note ?? "not published");
9897
10291
  line(` ${row.appName.padEnd(22)} ${label} ${value}`);
9898
10292
  }
9899
10293
  line("");
9900
10294
  if (rows.some((r) => r.url)) success(summary);
9901
- else line(import_picocolors36.default.yellow(summary));
10295
+ else line(import_picocolors37.default.yellow(summary));
9902
10296
  });
9903
10297
  })
9904
10298
  );
9905
10299
  }
9906
10300
 
9907
10301
  // src/commands/deployments.ts
9908
- var import_picocolors37 = __toESM(require_picocolors(), 1);
10302
+ var import_picocolors38 = __toESM(require_picocolors(), 1);
9909
10303
  function registerDeployments(program3) {
9910
10304
  const cmd = program3.command("deployments").description("Deployment history, and putting a build in front of customers");
9911
10305
  cmd.command("list").description("What has been built, newest first").option("--app <webAppId>", "just one app (default: every app in the project)").option("--env <environment>", "preview or production").option("--limit <n>", "how many to show", "20").action(
@@ -9923,7 +10317,7 @@ function registerDeployments(program3) {
9923
10317
  ok(res, () => {
9924
10318
  if (!items.length) {
9925
10319
  return line(
9926
- import_picocolors37.default.dim(
10320
+ import_picocolors38.default.dim(
9927
10321
  environment ? `Nothing has been deployed to ${environment} yet.` : "Nothing has been deployed yet. `workser deploy` builds the first one."
9928
10322
  )
9929
10323
  );
@@ -9943,13 +10337,13 @@ function registerDeployments(program3) {
9943
10337
  ).catch(() => null) : null;
9944
10338
  ok({ ...dep, logs }, () => {
9945
10339
  line(formatDeployment(dep));
9946
- if (dep?.error_message) line(import_picocolors37.default.red(` ${dep.error_message}`));
10340
+ if (dep?.error_message) line(import_picocolors38.default.red(` ${dep.error_message}`));
9947
10341
  const events = logs?.events ?? [];
9948
10342
  for (const e of events) {
9949
- line(` ${import_picocolors37.default.dim(String(e.type ?? "log"))} ${e.text ?? ""}`);
10343
+ line(` ${import_picocolors38.default.dim(String(e.type ?? "log"))} ${e.text ?? ""}`);
9950
10344
  }
9951
10345
  if (opts.logs && !events.length) {
9952
- line(import_picocolors37.default.dim(" That build produced no output."));
10346
+ line(import_picocolors38.default.dim(" That build produced no output."));
9953
10347
  }
9954
10348
  });
9955
10349
  })
@@ -9993,16 +10387,16 @@ function printPromoted(res, version) {
9993
10387
  const what = version === null ? "the latest build" : `version ${version}`;
9994
10388
  const url = res.url ?? res.vercel_url;
9995
10389
  success(`Production is being rebuilt from ${what}.`);
9996
- if (url) line(import_picocolors37.default.dim(`It will be at ${url}`));
9997
- line(import_picocolors37.default.dim("`workser deploy status` follows it."));
10390
+ if (url) line(import_picocolors38.default.dim(`It will be at ${url}`));
10391
+ line(import_picocolors38.default.dim("`workser deploy status` follows it."));
9998
10392
  }
9999
10393
  function formatDeployment(d) {
10000
10394
  if (!d) return "";
10001
- const version = d.version !== void 0 ? import_picocolors37.default.yellow(`v${d.version}`) : import_picocolors37.default.dim("v?");
10002
- const env = import_picocolors37.default.dim((d.environment ?? "?").padEnd(10));
10395
+ const version = d.version !== void 0 ? import_picocolors38.default.yellow(`v${d.version}`) : import_picocolors38.default.dim("v?");
10396
+ const env = import_picocolors38.default.dim((d.environment ?? "?").padEnd(10));
10003
10397
  const app = d.webAppName ? `${d.webAppName} ` : "";
10004
- const when = import_picocolors37.default.dim(formatTime2(d.created_at));
10005
- const url = d.url ? " " + import_picocolors37.default.cyan(d.url) : "";
10398
+ const when = import_picocolors38.default.dim(formatTime2(d.created_at));
10399
+ const url = d.url ? " " + import_picocolors38.default.cyan(d.url) : "";
10006
10400
  return `${version} ${env} ${colorStatus(d.status ?? "")} ${app}${when}${url}`;
10007
10401
  }
10008
10402
  function formatTime2(t) {
@@ -10012,7 +10406,7 @@ function formatTime2(t) {
10012
10406
  }
10013
10407
 
10014
10408
  // src/commands/usage.ts
10015
- var import_picocolors38 = __toESM(require_picocolors(), 1);
10409
+ var import_picocolors39 = __toESM(require_picocolors(), 1);
10016
10410
 
10017
10411
  // src/usage.ts
10018
10412
  var NEAR_LIMIT_FRACTION = 0.8;
@@ -10107,7 +10501,7 @@ function registerUsage(program3) {
10107
10501
  function print4(report) {
10108
10502
  const dims = report.dimensions ?? [];
10109
10503
  if (!dims.length) {
10110
- return line(import_picocolors38.default.dim("Nothing to measure for this project yet."));
10504
+ return line(import_picocolors39.default.dim("Nothing to measure for this project yet."));
10111
10505
  }
10112
10506
  const width = Math.max(...dims.map((d) => d.label.length));
10113
10507
  for (const d of dims) {
@@ -10116,23 +10510,23 @@ function print4(report) {
10116
10510
  line("");
10117
10511
  const summary = usageSummary(report);
10118
10512
  const worst = dims.map(usageState);
10119
- if (worst.includes("over")) line(import_picocolors38.default.red(summary));
10513
+ if (worst.includes("over")) line(import_picocolors39.default.red(summary));
10120
10514
  else if (worst.includes("near") || worst.includes("unknown"))
10121
- line(import_picocolors38.default.yellow(summary));
10515
+ line(import_picocolors39.default.yellow(summary));
10122
10516
  else success(summary);
10123
10517
  }
10124
10518
  function gauge(d, _labelWidth) {
10125
10519
  const drawn = bar(d);
10126
- return drawn ? ` ${import_picocolors38.default.dim(drawn)}` : "";
10520
+ return drawn ? ` ${import_picocolors39.default.dim(drawn)}` : "";
10127
10521
  }
10128
10522
  function colour(d) {
10129
10523
  switch (usageState(d)) {
10130
10524
  case "over":
10131
- return d.kind === "hard" ? import_picocolors38.default.red : import_picocolors38.default.yellow;
10525
+ return d.kind === "hard" ? import_picocolors39.default.red : import_picocolors39.default.yellow;
10132
10526
  case "near":
10133
- return import_picocolors38.default.yellow;
10527
+ return import_picocolors39.default.yellow;
10134
10528
  case "unknown":
10135
- return import_picocolors38.default.dim;
10529
+ return import_picocolors39.default.dim;
10136
10530
  default:
10137
10531
  return (s) => s;
10138
10532
  }
@@ -10140,7 +10534,7 @@ function colour(d) {
10140
10534
 
10141
10535
  // src/index.ts
10142
10536
  var pkg = {
10143
- version: true ? "0.6.1" : "0.0.0-dev"
10537
+ version: true ? "0.6.2" : "0.0.0-dev"
10144
10538
  };
10145
10539
  var program2 = new Command();
10146
10540
  program2.name("workser").description(
@@ -10193,6 +10587,7 @@ registerAsk(program2);
10193
10587
  registerSearch(program2);
10194
10588
  registerBoard(program2);
10195
10589
  registerTask(program2);
10590
+ registerGoal(program2);
10196
10591
  registerDecision(program2);
10197
10592
  registerDoc(program2);
10198
10593
  registerDesign(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workser/cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Workser CLI — give your local AI agent native DevOps & infrastructure on Workser. The agent runs `workser …` to provision, deploy, and manage real apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,7 @@ load.
18
18
 
19
19
  | You need to… | Commands | Run |
20
20
  | --- | --- | --- |
21
+ | Track a business requirement too big for one task, and its phases | `goal …` | `workser help goals` |
21
22
  | See what was decided or written down for this project | `decision …`, `requirement …`, `doc …` | `workser help sdlc-entities` |
22
23
  | Follow the project's brand — colours, fonts, logo | `design …` | `workser help brand` |
23
24
  | Provision or query Postgres; list end users | `db …`, `auth …` | `workser help database` |
@@ -0,0 +1,69 @@
1
+ ---
2
+ topic: goals
3
+ title: Business goals & phases
4
+ summary: The level above a task — a business requirement too big for one sitting, and the ordered phases that deliver it.
5
+ commands: [goal]
6
+ ---
7
+
8
+ # Business goals & phases
9
+
10
+ A **goal** is a business requirement — "customers can buy from my site" — that
11
+ does not fit in one task. It carries an ordered list of **phase names**; a task
12
+ joins one by carrying that name (`workser task create --goal <id> --phase <name>`).
13
+ A phase has no existence apart from the tasks inside it, so it cannot drift from
14
+ the work it names.
15
+
16
+ ```
17
+ workser goal list
18
+ workser goal show <id>
19
+ workser goal create <title> --phase <name...> [--outcome <text>]
20
+ [--criteria <json>]
21
+ workser goal update <id> [--title <text>] [--outcome <text>]
22
+ [--phase <name...>] [--status <value>]
23
+ workser goal check <id> <criterionId> --phase <name> (--pass|--fail|--reset) [--note <text>]
24
+ ```
25
+
26
+ Status is one of: proposed, agreed, working, delivered, abandoned.
27
+
28
+ ## A goal has to be argued for
29
+
30
+ Most requests are one task and should stay one task. Turning a two-hour job into
31
+ four milestones buries the owner in ceremony before anything is built — use
32
+ `workser task create` unless you can genuinely name two-plus slices that each
33
+ deliver something the owner would notice.
34
+
35
+ ```
36
+ workser goal create "Launch checkout" \
37
+ --phase "Cart" --phase "Payment" --phase "Receipts" \
38
+ --outcome "A customer can buy something and get a receipt"
39
+ ```
40
+
41
+ `--phase` needs 2–6 names. Propose the shape only — nothing is created until the
42
+ owner agrees it; planning phase four now is waste, since it will change once
43
+ phase one is real.
44
+
45
+ ## Acceptance criteria are agreed with the shape
46
+
47
+ `--criteria` maps each phase name to the owner's own sentences about what "done"
48
+ means for it, e.g. `'{"Payment":["A customer can pay by card and gets a receipt"]}'`.
49
+ Written afterwards they only describe what got built; written with the shape they
50
+ can still change the plan.
51
+
52
+ Record whether one is met with `workser goal check`:
53
+
54
+ ```
55
+ workser goal check g_123 c1 --phase Payment --pass \
56
+ --note "Tested a card payment end to end; receipt emailed."
57
+ ```
58
+
59
+ `--note` is required on `--pass` — a tick the owner cannot verify is worse than no
60
+ tick.
61
+
62
+ ## Joining tasks to a goal
63
+
64
+ ```
65
+ workser task create "Build the payment form" --goal g_123 --phase Payment
66
+ ```
67
+
68
+ A goal's apps and progress are derived from the tasks that join it, not declared
69
+ up front — most of the apps a goal will touch don't exist when it's proposed.