@workser/cli 0.6.23 → 0.6.26

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/AGENTS.md CHANGED
@@ -133,16 +133,18 @@ workser requirement list | show <id> | create "<title>" --body <t> | update <id>
133
133
  workser doc list | show <id> [--markdown]
134
134
  workser doc create "<title>" --markdown <text> | doc update <id> --markdown <text>
135
135
  ```
136
- **Read before you plan** — `board list` and `decision list` tell you what someone is
137
- already doing and what this project chose on purpose, so you don't re-file work or
138
- quietly reverse a decision.
139
-
140
- **A plan with phases goes on this task's Subtasks list, not the Board.** Use one
141
- `task subtask add` per phase. If you spot a bad title, role, note, or file scope after
142
- creation, correct that existing row with `task subtask update <id>`; do not say it is
143
- locked, create a replacement, or duplicate the plan. Write the plan narrative once
144
- as `doc create --markdown` with no `--work-item`, and record a real tradeoff with
145
- `decision create`.
136
+ **Read before you plan** — `goal list`, `task list`, and `decision list` tell you
137
+ what the team is delivering, what someone is already doing, and what this project
138
+ chose on purpose, so you don't re-file work or quietly reverse a decision.
139
+
140
+ **Choose the planning level before creating work.** A business outcome with two or
141
+ more owner-visible deliveries is a goal: `goal create` proposes its ordered phases,
142
+ then you stop until the owner agrees the shape. A task is one delivery; `task subtask
143
+ add` records the specialist steps inside it. Do not call subtasks phases, and do not
144
+ create the goal's tasks in the proposal turn. If you spot a bad title, role, note, or
145
+ file scope after creation, correct that existing row with `task subtask update <id>`;
146
+ do not create a replacement or duplicate the plan. The legacy Board is not a planning
147
+ surface for agents.
146
148
 
147
149
  **Keep it true as you work.** `board move <id> in-progress` when you pick it up,
148
150
  `in-review` when it's ready to look at, `board close <id>` when it's done and
@@ -265,12 +267,13 @@ keep your own context lean and get a specialized second perspective; a non-zero
265
267
  ## Example
266
268
  ```bash
267
269
  workser status --json # orient: which project, last deploy
268
- workser board list --json # what's tracked; decision list for what's decided
269
- workser board create "Phase 1 — …" --status in-progress --json # phased plan -> cards
270
- workser doc create "Plan" --markdown "…" --json # …plus the plan doc
270
+ workser goal list --json # long-running outcomes and their phases
271
+ workser task list --json # the work already filed
272
+ workser decision list --json # what the project chose on purpose
273
+ # For a big outcome: goal create ... --phase ... --json, then STOP for agreement.
274
+ # For one delivery: task create ..., then task subtask add ... for its steps.
271
275
  workser env set STRIPE_KEY=sk_live_… --json
272
276
  # … you write the app code with your normal tools …
273
277
  workser deploy --prod --watch --json # -> .data.url is the live URL
274
- workser board close <id> --json # the Board now matches reality
275
278
  ```
276
279
  Report results to the user in plain language, not raw JSON.
package/dist/index.js CHANGED
@@ -3584,6 +3584,7 @@ function ownerOnly(opts) {
3584
3584
 
3585
3585
  // src/output.ts
3586
3586
  var OUT_OF_SCOPE_EXIT = 7;
3587
+ var BILLING_EXIT = 8;
3587
3588
  var mode = "text";
3588
3589
  var quiet = false;
3589
3590
  function configureOutput(opts) {
@@ -3645,6 +3646,18 @@ function fail(err) {
3645
3646
  process.stderr.write(import_picocolors.default.dim(" Approve the action in Workser Orbit, then retry.\n"));
3646
3647
  } else if (e.code === "owner_only") {
3647
3648
  process.stderr.write(import_picocolors.default.dim(" This is an owner action \u2014 do it in the Workser Orbit app.\n"));
3649
+ } else if (e.code === "insufficient_credits") {
3650
+ process.stderr.write(
3651
+ import_picocolors.default.dim(
3652
+ " The organization is out of AI credits. Top up in Workser Orbit (Billing) \u2014 retrying will not help.\n"
3653
+ )
3654
+ );
3655
+ } else if (e.code === "image_quota_reached") {
3656
+ process.stderr.write(
3657
+ import_picocolors.default.dim(
3658
+ " This month's plan allowance is spent. Upgrade the plan, or wait for the month to roll over \u2014 retrying will not help.\n"
3659
+ )
3660
+ );
3648
3661
  }
3649
3662
  }
3650
3663
  process.exit(exitCodeFor(e));
@@ -3655,6 +3668,9 @@ function exitCodeFor(e) {
3655
3668
  if (e.code === "awaiting_approval") return 5;
3656
3669
  if (e.code === "owner_only") return OWNER_ONLY_EXIT;
3657
3670
  if (e.code === "out_of_scope") return OUT_OF_SCOPE_EXIT;
3671
+ if (e.code === "insufficient_credits" || e.code === "image_quota_reached") {
3672
+ return BILLING_EXIT;
3673
+ }
3658
3674
  return 1;
3659
3675
  }
3660
3676
 
@@ -4976,6 +4992,7 @@ up front \u2014 most of the apps a goal will touch don't exist when it's propose
4976
4992
  workser image generate "<prompt>" # alias: workser image gen
4977
4993
  -r, --reference <url...> # condition on existing images (up to 4)
4978
4994
  -o, --output <path> # also download the first image locally
4995
+ workser image usage # can I generate right now, and what would stop me
4979
4996
  \`\`\`
4980
4997
 
4981
4998
  Returns the generated image's public URL, so the usual move is to generate, then use
@@ -4999,6 +5016,32 @@ workser image gen "same van, from the side" -r https://\u2026 -o ./public/van.pn
4999
5016
  - **Placeholder art is not a deliverable.** Generating a hero image to unblock a
5000
5017
  layout is fine; shipping it as the user's brand asset without asking is not.
5001
5018
 
5019
+ ## This is metered, and it can be refused
5020
+
5021
+ Every image is billed to the organization's credit wallet, and each plan includes
5022
+ a monthly number of them. Three gates run server-side before anything is drawn \u2014
5023
+ the plan tier, the monthly allowance, then the wallet \u2014 so a call can come back
5024
+ refused having spent nothing.
5025
+
5026
+ \`\`\`bash
5027
+ workser image usage --json # {"limit":10,"used":3,"remaining":7,
5028
+ # "credits":{"available":41.2,"requiredPerImage":4.12,"sufficient":true},
5029
+ # "canGenerate":true,"blockedBy":null}
5030
+ \`\`\`
5031
+
5032
+ - **Check before a batch, not after.** Planning six images is a plan that needs
5033
+ six times \`requiredPerImage\` in the wallet. \`workser image usage\` answers that
5034
+ before any of them cost anything; \`blockedBy\` names which gate would stop you
5035
+ (\`plan\`, \`quota\`, \`credits\`), and it exits non-zero when generation is blocked.
5036
+ - **A refusal is final, not a hiccup \u2014 do NOT retry it.** \`insufficient_credits\`
5037
+ (HTTP 402) and \`image_quota_reached\` (403) come back as those exact codes in
5038
+ \`--json\`, and the CLI exits 8 for both. Retrying cannot succeed and every
5039
+ attempt is another paid call the owner did not ask for. Stop, and tell the
5040
+ owner what to do: top up credits, or upgrade the plan.
5041
+ - **You cannot see their balance any other way**, so do not guess at it from how
5042
+ many images you have already made \u2014 a refusal at image four of six leaves the
5043
+ work half-done and looks like a bug.
5044
+
5002
5045
  ## Understanding media you can't natively see or hear
5003
5046
 
5004
5047
  The fallback for a text-only model, or media you have no other way to reach: describe
@@ -5325,14 +5368,17 @@ the same way when the task touches documented behaviour.
5325
5368
  \`--infra\` match on the server, so the answer covers the whole project rather
5326
5369
  than the first page of it. \`decision tag <id>\` files one that already exists.
5327
5370
 
5328
- ## Work with phases \u2192 subtasks + a plan doc, before you build
5371
+ ## Work with one task's steps \u2192 subtasks + a plan doc, before you build
5329
5372
 
5330
- The moment you split a task into more than one phase, file it \u2014 not afterwards,
5331
- and not only in your reply, which is gone once the conversation scrolls.
5373
+ Do not confuse two levels of planning. A **goal phase** is an owner-visible
5374
+ delivery across tasks; create it with \`workser goal create\` and wait for agreement
5375
+ before filing work. A **subtask** is one teammate's implementation step inside a
5376
+ single approved task. File those steps before building \u2014 not afterwards, and not
5377
+ only in your reply, which is gone once the conversation scrolls.
5332
5378
 
5333
5379
  \`\`\`bash
5334
- # the phases \u2014 this task's own subtask list, not the Board
5335
- workser task subtask add "Phase 1 \u2014 schema + migration" --role api --note "\u2026"
5380
+ # the implementation steps \u2014 this task's own subtask list, not the Board
5381
+ workser task subtask add "Create schema + migration" --role api --note "\u2026"
5336
5382
 
5337
5383
  # the plan's narrative, ONE doc, deliberately NOT linked to a subtask
5338
5384
  workser doc create "Checkout \u2014 plan" --kind plan --markdown "$(cat plan.md)" --json
@@ -5352,9 +5398,9 @@ Never tell the user a subtask is locked, and never file a second one alongside
5352
5398
  the wrong one \u2014 a plan with a duplicate phase in it is a plan nobody can read
5353
5399
  the progress of.
5354
5400
 
5355
- **Don't pass \`--work-item\` for a multi-phase plan.** A linked document renders on
5356
- its card and is *hidden* from the Docs panel; a plan spanning three phases
5357
- belongs to the project, not to phase 1.
5401
+ **Don't pass \`--work-item\` for a goal-wide plan.** A linked document renders on
5402
+ its card and is *hidden* from the Docs panel; a plan spanning several goal phases
5403
+ belongs to the project, not to the first task.
5358
5404
 
5359
5405
  The bar: if the user closed this conversation now, the subtask list should still
5360
5406
  show what's left and the doc should still explain the plan to whoever continues
@@ -9113,6 +9159,32 @@ function registerImage(program3) {
9113
9159
  ok(res, () => line(res?.answer ?? ""));
9114
9160
  })
9115
9161
  );
9162
+ image.command("usage").description(
9163
+ "Whether an image can be generated right now \u2014 monthly allowance, credit balance, and what would block it"
9164
+ ).action(
9165
+ action(async ({ ctx }) => {
9166
+ const projectId = requireProject(ctx);
9167
+ const status = await api(
9168
+ ctx,
9169
+ `/v1/projects/${projectId}/images/usage`
9170
+ );
9171
+ ok(status, () => {
9172
+ const monthly = status.limit === null ? "no monthly limit on this plan" : `${status.used} of ${status.limit} used this month, ${status.remaining} left`;
9173
+ line(`This month: ${monthly}`);
9174
+ line(
9175
+ `Credits: ${status.credits.available.toFixed(2)} available, about ${status.credits.requiredPerImage.toFixed(2)} per image`
9176
+ );
9177
+ if (status.canGenerate) {
9178
+ success("Can generate now");
9179
+ } else {
9180
+ warn(
9181
+ status.blockedBy === "credits" ? "Blocked: not enough organization credits. Top up in Workser Orbit (Billing)." : status.blockedBy === "quota" ? "Blocked: this month's plan allowance is spent. Upgrade, or wait for the month to roll over." : "Blocked: this plan cannot generate images. Upgrade to Spark or higher."
9182
+ );
9183
+ }
9184
+ });
9185
+ if (!status.canGenerate) process.exitCode = BILLING_EXIT;
9186
+ })
9187
+ );
9116
9188
  }
9117
9189
  async function download(url, output) {
9118
9190
  const target = resolve4(output);
@@ -10502,153 +10574,18 @@ function paceLine(pace) {
10502
10574
  }
10503
10575
 
10504
10576
  // src/role-guard.ts
10505
- var READS = [
10506
- // `requirement` sits beside `doc` and `decision` because it is the same kind
10507
- // of thing and was simply forgotten: the verb shipped, no role could run it,
10508
- // and on 2026-08-23 a channel PM reported to the owner that "requirements are
10509
- // not readable by this PM role" — which was exactly true, for every role.
10510
- "task",
10511
- "board",
10512
- "doc",
10513
- "decision",
10514
- "requirement",
10515
- "memory",
10516
- "search",
10517
- "verify",
10518
- "logs",
10519
- "status",
10520
- "help",
10521
- "whoami",
10522
- "login",
10523
- "auth",
10524
- "project",
10525
- "open",
10526
- "doctor",
10527
- // Both READ and report. `scan` reads files and shells out to npm; `health`
10528
- // makes a GET request to an address that is already public. Neither can
10529
- // change anything, which is why the roles that exist to look — qa, security,
10530
- // sre, analyst — get them without getting anything else.
10531
- "scan",
10532
- "health",
10533
- // Read-only views of what is running. Added with Phase 6a: an SRE that can
10534
- // read logs but cannot list deployments or read the app's address is being
10535
- // asked to diagnose an outage with one eye shut.
10536
- "urls",
10537
- "deployments",
10538
- // Reading the plan and what is used against it. An agent proposing "add
10539
- // another project" can only sensibly propose it if it can find out the plan
10540
- // allows two and two already exist.
10541
- "usage",
10542
- // RECORDING WHAT YOU PRODUCED IS NOT CHANGING THE PROJECT.
10543
- // Every dispatched role is told, in its own preamble, to run `workser
10544
- // artifact list` before non-trivial work and `workser artifact add` when it
10545
- // finishes something the owner should get. Five roles — pm, qa, security,
10546
- // analyst, sre — were then refused the verb here, so the instruction and the
10547
- // guard contradicted each other: the agent kept trying to obey an order this
10548
- // file would not let it obey, and burned its run doing it. It belongs beside
10549
- // `doc`, `decision` and `board`, which are already in this list and also
10550
- // have `create` subcommands — these write the project's RECORD, not the
10551
- // project. What stops a reviewer editing code is its filesystem mode, and
10552
- // that is untouched.
10553
- "artifact",
10554
- // LEAVING A FACT FOR THE TEAM IS NOT CHANGING THE PROJECT — the same
10555
- // argument as `artifact` directly above, and it belongs in READS rather than
10556
- // BUILDS on purpose. The roles that DISCOVER things are the reading ones: a
10557
- // tester that found the real cause, an analyst that found the actual column
10558
- // name, a security engineer that found where a key is read from. A shared
10559
- // memory only builders could write to would be missing most of what is worth
10560
- // sharing. See the daemon's `team-memory.ts`.
10561
- "note"
10562
- ];
10563
- var BUILDS = [
10564
- ...READS,
10565
- "app",
10566
- "env",
10567
- "db",
10568
- "storage",
10569
- "checkpoint",
10570
- "image",
10571
- "design",
10572
- "ask",
10573
- "sync",
10574
- "tool",
10575
- "workflow",
10576
- "neon",
10577
- "business"
10578
- ];
10579
- var ROLE_VERBS = {
10580
- pm: [...READS, "ask", "app"],
10581
- // `note` reaches this via READS.
10582
- architect: [...BUILDS, "versions"],
10583
- web: BUILDS,
10584
- api: BUILDS,
10585
- mobile: BUILDS,
10586
- python: BUILDS,
10587
- automation: BUILDS,
10588
- designer: BUILDS,
10589
- qa: READS,
10590
- security: READS,
10591
- analyst: READS,
10592
- sre: [...READS, "deploy", "domain", "versions"],
10593
- devops: [...BUILDS, "deploy", "domain", "versions"]
10594
- // NOTE: `deployments` reaches READS above, so every role can LIST and
10595
- // INSPECT. That is correct — history is a read. The two verbs that change
10596
- // production (`promote`, `rollback`) are not gated here at all, and must not
10597
- // be: they are gated in the DAEMON, as `deploy.prod`, which is a door "just
10598
- // do it" cannot open. A second, verb-name-based rule here would be a weaker
10599
- // copy of a control that already works.
10600
- };
10601
10577
  var NEVER = {
10602
- // Approving is the owner's, full stop. An agent that can approve the plan it
10603
- // proposed has removed the only gate this product has.
10604
10578
  "task approval": "Only the owner can approve a plan."
10605
10579
  };
10606
- var READ_PAIRS = /* @__PURE__ */ new Set([
10607
- // The project's own brand and design notes.
10608
- "design show",
10609
- // What exists, and what it is wired to.
10610
- "app list",
10611
- "app tools",
10612
- // Shape, never contents-by-arbitrary-SQL.
10613
- "db list",
10614
- "db tables",
10615
- "db schema",
10616
- // What is stored, not what is in it.
10617
- "storage list",
10618
- "storage ls",
10619
- "checkpoint list",
10620
- "workflow list",
10621
- "workflow get",
10622
- "workflow runs",
10623
- "workflow nodes",
10624
- "tool list",
10625
- // The business data an analyst exists to read.
10626
- "business resources",
10627
- "business list",
10628
- "business get",
10629
- // The database service's own inventory.
10630
- "neon status",
10631
- "neon list"
10632
- ]);
10633
10580
  function assertRoleMayRun(argv) {
10634
10581
  const role = (process.env.WORKSER_ROLE ?? "").trim();
10635
10582
  if (!role) return;
10636
10583
  const commandArgv = stripLeadingGlobalOptions(argv);
10637
- const verb = commandArgv[0];
10638
- if (!verb) return;
10584
+ if (!commandArgv[0]) return;
10639
10585
  const pair = `${commandArgv[0]} ${commandArgv[1] ?? ""}`.trim();
10640
10586
  if (NEVER[pair] && !(pair === "task approval" && (commandArgv[2] === "request" || !commandArgv[2]))) {
10641
10587
  throw new WorkserError(NEVER[pair], { code: "role_forbidden" });
10642
10588
  }
10643
- if (READ_PAIRS.has(pair)) return;
10644
- const allowed = ROLE_VERBS[role];
10645
- const list = allowed ?? READS;
10646
- if (!list.includes(verb)) {
10647
- throw new WorkserError(
10648
- `The ${role} role can't run \`workser ${verb}\`. Report what you found instead, and the step that owns this will do it.`,
10649
- { code: "role_forbidden" }
10650
- );
10651
- }
10652
10589
  }
10653
10590
  function stripLeadingGlobalOptions(argv) {
10654
10591
  const takesValue = /* @__PURE__ */ new Set([
@@ -12353,7 +12290,7 @@ function colour(d) {
12353
12290
 
12354
12291
  // src/index.ts
12355
12292
  var pkg = {
12356
- version: true ? "0.6.23" : "0.0.0-dev"
12293
+ version: true ? "0.6.26" : "0.0.0-dev"
12357
12294
  };
12358
12295
  var program2 = new Command();
12359
12296
  program2.name("workser").description(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workser/cli",
3
- "version": "0.6.23",
3
+ "version": "0.6.26",
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",
@@ -89,11 +89,10 @@ move between its projects (`--project <id>`, or `cd`); another org returns
89
89
  `workser decision list --json` (so you don't quietly reverse a decision) and
90
90
  `workser design show --json` before writing UI. This project outlives your
91
91
  session; that context is how you don't start from zero.
92
- 3. **A phased plan goes on the subtask list, never the Board.** Phases are
93
- `workser task subtask add` — not `board create`, which makes a second,
94
- driftable "the plan" the task page never reads. Write the narrative once as
95
- `doc create`, plus `decision create` for a real tradeoff; a plan in your reply
96
- alone is gone when the conversation scrolls. `workser help sdlc-entities`.
92
+ 3. **Choose the planning level first.** Two-plus owner-visible deliveries are a
93
+ **goal**: read `workser help goals`, run `goal create`, then stop for agreement.
94
+ One delivery is a task; `task subtask add` records its implementation steps.
95
+ Goal phases are owner checkpoints across tasks; subtasks are not phases.
97
96
  4. **Stay in your lane.** On `owner_only` (exit 6) or `out_of_scope` (exit 7),
98
97
  don't retry or look for a workaround — tell the user, then continue.
99
98
  Provisioning the *pinned project's own* db/bucket/auth is allowed (it may be
@@ -109,12 +108,12 @@ move between its projects (`--project <id>`, or `cd`); another org returns
109
108
  `git reset --hard`, `DROP`/`TRUNCATE`, `curl | sh`, …) are refused by Workser's
110
109
  safety policy — don't attempt them; use migrations + scoped changes instead.
111
110
 
112
- ## Typical flow: build → ship
111
+ ## Typical flow: one task → build → ship
113
112
 
114
113
  ```bash
115
114
  workser status --json # 1. orient
116
115
  workser decision list --json # 2. what's already decided
117
- workser task subtask add "Phase 2 — …" --json # 3. phases subtasks
116
+ workser task subtask add "Implement …" --json # 3. steps inside this task
118
117
  workser doc create "Plan" --markdown "…" --json # 4. the narrative, once
119
118
  workser db create --json # 5. provision infra (idempotent)
120
119
  workser env set STRIPE_KEY=sk_live_… --json # 6. configure it
@@ -123,6 +122,15 @@ workser verify --json # 7. green build is the bar
123
122
  workser deploy --prod --watch --json # 8. ship → stable *.workser.app URL
124
123
  ```
125
124
 
125
+ For a larger outcome, propose the shape and stop:
126
+
127
+ ```bash
128
+ workser goal create "Launch checkout" \
129
+ --phase "Cart" --phase "Payment" --phase "Receipts" \
130
+ --outcome "A customer can buy something and get a receipt" --json
131
+ # After agreement, link phase tasks with --goal and --phase.
132
+ ```
133
+
126
134
  ## Reading results
127
135
 
128
136
  - Success: use `.data` (`.data.url` after deploy, a `.data` array after `list`).
@@ -11,6 +11,7 @@ commands: [image, video, audio]
11
11
  workser image generate "<prompt>" # alias: workser image gen
12
12
  -r, --reference <url...> # condition on existing images (up to 4)
13
13
  -o, --output <path> # also download the first image locally
14
+ workser image usage # can I generate right now, and what would stop me
14
15
  ```
15
16
 
16
17
  Returns the generated image's public URL, so the usual move is to generate, then use
@@ -34,6 +35,32 @@ workser image gen "same van, from the side" -r https://… -o ./public/van.png -
34
35
  - **Placeholder art is not a deliverable.** Generating a hero image to unblock a
35
36
  layout is fine; shipping it as the user's brand asset without asking is not.
36
37
 
38
+ ## This is metered, and it can be refused
39
+
40
+ Every image is billed to the organization's credit wallet, and each plan includes
41
+ a monthly number of them. Three gates run server-side before anything is drawn —
42
+ the plan tier, the monthly allowance, then the wallet — so a call can come back
43
+ refused having spent nothing.
44
+
45
+ ```bash
46
+ workser image usage --json # {"limit":10,"used":3,"remaining":7,
47
+ # "credits":{"available":41.2,"requiredPerImage":4.12,"sufficient":true},
48
+ # "canGenerate":true,"blockedBy":null}
49
+ ```
50
+
51
+ - **Check before a batch, not after.** Planning six images is a plan that needs
52
+ six times `requiredPerImage` in the wallet. `workser image usage` answers that
53
+ before any of them cost anything; `blockedBy` names which gate would stop you
54
+ (`plan`, `quota`, `credits`), and it exits non-zero when generation is blocked.
55
+ - **A refusal is final, not a hiccup — do NOT retry it.** `insufficient_credits`
56
+ (HTTP 402) and `image_quota_reached` (403) come back as those exact codes in
57
+ `--json`, and the CLI exits 8 for both. Retrying cannot succeed and every
58
+ attempt is another paid call the owner did not ask for. Stop, and tell the
59
+ owner what to do: top up credits, or upgrade the plan.
60
+ - **You cannot see their balance any other way**, so do not guess at it from how
61
+ many images you have already made — a refusal at image four of six leaves the
62
+ work half-done and looks like a bug.
63
+
37
64
  ## Understanding media you can't natively see or hear
38
65
 
39
66
  The fallback for a text-only model, or media you have no other way to reach: describe
@@ -58,14 +58,17 @@ the same way when the task touches documented behaviour.
58
58
  `--infra` match on the server, so the answer covers the whole project rather
59
59
  than the first page of it. `decision tag <id>` files one that already exists.
60
60
 
61
- ## Work with phases → subtasks + a plan doc, before you build
61
+ ## Work with one task's steps → subtasks + a plan doc, before you build
62
62
 
63
- The moment you split a task into more than one phase, file it — not afterwards,
64
- and not only in your reply, which is gone once the conversation scrolls.
63
+ Do not confuse two levels of planning. A **goal phase** is an owner-visible
64
+ delivery across tasks; create it with `workser goal create` and wait for agreement
65
+ before filing work. A **subtask** is one teammate's implementation step inside a
66
+ single approved task. File those steps before building — not afterwards, and not
67
+ only in your reply, which is gone once the conversation scrolls.
65
68
 
66
69
  ```bash
67
- # the phases — this task's own subtask list, not the Board
68
- workser task subtask add "Phase 1 — schema + migration" --role api --note "…"
70
+ # the implementation steps — this task's own subtask list, not the Board
71
+ workser task subtask add "Create schema + migration" --role api --note "…"
69
72
 
70
73
  # the plan's narrative, ONE doc, deliberately NOT linked to a subtask
71
74
  workser doc create "Checkout — plan" --kind plan --markdown "$(cat plan.md)" --json
@@ -85,9 +88,9 @@ Never tell the user a subtask is locked, and never file a second one alongside
85
88
  the wrong one — a plan with a duplicate phase in it is a plan nobody can read
86
89
  the progress of.
87
90
 
88
- **Don't pass `--work-item` for a multi-phase plan.** A linked document renders on
89
- its card and is *hidden* from the Docs panel; a plan spanning three phases
90
- belongs to the project, not to phase 1.
91
+ **Don't pass `--work-item` for a goal-wide plan.** A linked document renders on
92
+ its card and is *hidden* from the Docs panel; a plan spanning several goal phases
93
+ belongs to the project, not to the first task.
91
94
 
92
95
  The bar: if the user closed this conversation now, the subtask list should still
93
96
  show what's left and the doc should still explain the plan to whoever continues