@workser/cli 0.6.0 → 0.6.1

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
@@ -4685,28 +4685,27 @@ real scope, not just a repeat of the task.
4685
4685
  },
4686
4686
  {
4687
4687
  topic: "sdlc-entities",
4688
- title: "Board cards, decisions and requirements",
4689
- summary: "Read what this project already tracks and decided, keep the Board honest as you work, and record what a future maintainer will need.",
4688
+ title: "Decisions and requirements",
4689
+ summary: "Read what this project already decided, and record what a future maintainer will need. Phased work itself is subtasks, not board cards \u2014 see `workser help tasks`.",
4690
4690
  commands: ["board", "decision", "requirement"],
4691
4691
  source: "skills/workser/reference/sdlc-entities.md",
4692
- body: `# Board cards, decisions and requirements
4692
+ body: `# Decisions and requirements
4693
4693
 
4694
4694
  These are the project's memory across sessions. They write to the **same tables**
4695
- the Orbit desktop's Board and Project Memory panels use, so anything here appears
4696
- there too \u2014 and, inside an Orbit-spawned run, as an inline card in the
4697
- conversation. Documents have their own guide: \`workser help docs\`.
4695
+ the Orbit desktop's Project Memory panel uses, so anything here appears there too
4696
+ \u2014 and, inside an Orbit-spawned run, as an inline card in the conversation.
4697
+ Documents have their own guide: \`workser help docs\`. Phased work is tracked as
4698
+ subtasks, not here \u2014 see \`workser help tasks\`.
4699
+
4700
+ > **The Board (\`workser board ...\`) is deprecated for agent use.** It used to be
4701
+ > where a multi-phase plan went, one card per phase \u2014 and the phase was ALSO a
4702
+ > subtask the planning turn had just filed for the same piece of work. That gave
4703
+ > a task two competing plans, one of which this task's own page never reads and
4704
+ > nothing kept in sync with the other. Phases are \`project_tasks\` subtasks now,
4705
+ > full stop: \`workser task subtask add\`. Do not run \`workser board create\` for
4706
+ > planned work \u2014 see \`workser help tasks\`.
4698
4707
 
4699
4708
  \`\`\`
4700
- workser board list [--status <value>] [--label <value>] [--limit <n>]
4701
- workser board show <id>
4702
- workser board create <title> [--description <text>] [--status <value>]
4703
- [--priority <value>] [--label <value>]
4704
- [--owner <name>] [--milestone <id>]
4705
- workser board update <id> [--title|--description|--status|--priority
4706
- |--label|--owner|--milestone ...]
4707
- workser board move <id> <backlog|in-progress|in-review|done>
4708
- workser board close <id>
4709
-
4710
4709
  workser decision list [--limit <n>]
4711
4710
  workser decision show <id>
4712
4711
  workser decision create <title> --context <text> --decision <text>
@@ -4724,7 +4723,6 @@ workser requirement update <id> [--title <text>] [--body <text>] [--status <text
4724
4723
  Before starting anything beyond a trivial edit:
4725
4724
 
4726
4725
  \`\`\`
4727
- workser board list --json # what's already tracked (don't re-file it)
4728
4726
  workser decision list --json # what was already decided (don't reverse it)
4729
4727
  \`\`\`
4730
4728
 
@@ -4734,19 +4732,18 @@ purpose \u2014 \`workser decision show <id>\` gives you the context and conseque
4734
4732
  not just the title. Reach for \`workser doc list\` / \`workser requirement list\`
4735
4733
  the same way when the task touches documented behaviour.
4736
4734
 
4737
- ## Work with phases \u2192 cards + a plan doc, before you build
4735
+ ## Work with phases \u2192 subtasks + a plan doc, before you build
4738
4736
 
4739
4737
  The moment you split a task into more than one phase, file it \u2014 not afterwards,
4740
4738
  and not only in your reply, which is gone once the conversation scrolls.
4741
4739
 
4742
4740
  \`\`\`bash
4743
- # one card per phase; only the one you're doing goes to in-progress
4744
- workser board create "Phase 1 \u2014 schema + migration" \\
4745
- --description "Add orders/line_items tables and the migration." \\
4746
- --status in-progress --json
4747
- workser board create "Phase 2 \u2014 checkout API" --description "\u2026" --json
4741
+ # the phases themselves \u2014 this task's own subtask list, not the Board
4742
+ workser task subtask add "Phase 1 \u2014 schema + migration" --role api \\
4743
+ --note "Add orders/line_items tables and the migration."
4744
+ workser task subtask add "Phase 2 \u2014 checkout API" --role api --note "\u2026"
4748
4745
 
4749
- # the plan itself, ONE doc, deliberately NOT linked to a card
4746
+ # the plan's narrative, ONE doc, deliberately NOT linked to a subtask
4750
4747
  workser doc create "Checkout \u2014 implementation plan" --markdown "$(cat plan.md)" --json
4751
4748
 
4752
4749
  # the approach, if the plan settled something with real alternatives
@@ -4757,32 +4754,9 @@ workser decision create "Carts live server-side" --context "\u2026" --decision "
4757
4754
  its card and is *hidden* from the Docs panel; a plan spanning three phases
4758
4755
  belongs to the project, not to phase 1.
4759
4756
 
4760
- The bar: if the user closed this conversation now, the Board should still show
4761
- what's left and the doc should still explain the plan to whoever continues it.
4762
-
4763
- ## Keep the Board honest while you work
4764
-
4765
- A Board still reading \`backlog\` after the feature shipped tells the user the
4766
- opposite of the truth. Moving the card is part of finishing the work:
4767
-
4768
- \`\`\`
4769
- workser board move <id> in-progress # you picked it up
4770
- workser board move <id> in-review # ready for the user to look at
4771
- workser board close <id> # done and verified
4772
- \`\`\`
4773
-
4774
- \`--status\` is one of \`backlog | in-progress | in-review | done\` (default
4775
- \`backlog\`). \`--priority\` is one of \`low | normal | high | urgent\` (default
4776
- \`normal\`). \`--label\` repeats for more than one label:
4777
-
4778
- \`\`\`
4779
- workser board create "Fix the login bug" --status in-progress --priority high \\
4780
- --label bug --label auth
4781
- \`\`\`
4782
-
4783
- \`board update\` replaces the labels you pass rather than merging them, and
4784
- touches only the fields you name. There is no \`board delete\`: \`done\` is the
4785
- terminal state, and removing a card the user filed is theirs to do in Orbit.
4757
+ The bar: if the user closed this conversation now, the subtask list should still
4758
+ show what's left and the doc should still explain the plan to whoever continues
4759
+ it.
4786
4760
 
4787
4761
  ## Decisions are append-only
4788
4762
 
@@ -4894,6 +4868,9 @@ below defaults to it and you rarely pass an id at all.
4894
4868
  \`\`\`
4895
4869
  workser task list [--status <value>] [--label <value>] [--limit <n>]
4896
4870
  workser task show [id] # the task you are in, with its steps
4871
+ workser task create <title> [--note <text>] [--kind <value>]
4872
+ [--label <value...>] [--app <id...>]
4873
+ [--infra <ref...>]
4897
4874
 
4898
4875
  workser task subtask add <title> [--role <value>] [--kind <value>]
4899
4876
  [--note <text>] [--app <id...>]
@@ -4917,6 +4894,16 @@ Team's own table. Filing your plan on the Board puts it somewhere the owner's
4917
4894
  task page never reads: they see "created work item" and an empty plan. Use
4918
4895
  \`task subtask add\`.
4919
4896
 
4897
+ ## Opening work from a project channel
4898
+
4899
+ When a project-channel conversation produces actionable work, the PM may record
4900
+ it with \`workser task create\`. Orbit supplies the channel and source-message IDs;
4901
+ the command records them and posts the new task card as a Project Manager message
4902
+ automatically. Do not invent or ask for those IDs.
4903
+
4904
+ Opening a task does **not** approve it or start implementation. The task remains
4905
+ awaiting the owner. Never approve or dispatch a task you opened yourself.
4906
+
4920
4907
  ## Planning a task
4921
4908
 
4922
4909
  Read the project first, then propose. One \`subtask add\` per step:
@@ -5161,11 +5148,11 @@ function registerHelp(program3) {
5161
5148
  var import_picocolors3 = __toESM(require_picocolors(), 1);
5162
5149
 
5163
5150
  // src/context.ts
5164
- import { resolve } from "path";
5151
+ import { resolve as resolve2 } from "path";
5165
5152
 
5166
5153
  // src/config.ts
5167
5154
  import { homedir } from "os";
5168
- import { join } from "path";
5155
+ import { dirname, join, resolve } from "path";
5169
5156
  import {
5170
5157
  chmodSync,
5171
5158
  existsSync,
@@ -5173,6 +5160,8 @@ import {
5173
5160
  readFileSync,
5174
5161
  writeFileSync
5175
5162
  } from "fs";
5163
+ var PROJECT_MARKER = ".workser-project";
5164
+ var APP_MARKER = ".workser-app";
5176
5165
  var GLOBAL_DIR = join(homedir(), ".workser");
5177
5166
  var SESSION_FILE = join(GLOBAL_DIR, "session.json");
5178
5167
  function readSession() {
@@ -5203,10 +5192,82 @@ function projectLinkPath(cwd) {
5203
5192
  return join(cwd, ".workser", "project.json");
5204
5193
  }
5205
5194
  function readProjectLink(cwd) {
5195
+ const found = readFolderIdentity(cwd);
5196
+ if (!found) return null;
5197
+ return {
5198
+ projectId: found.projectId,
5199
+ name: found.projectName,
5200
+ orgId: found.orgId,
5201
+ root: found.projectRoot
5202
+ };
5203
+ }
5204
+ function readFolderIdentity(cwd) {
5205
+ let app = null;
5206
+ for (const dir of ancestors(cwd)) {
5207
+ if (!app) {
5208
+ const marker2 = readJson(join(dir, APP_MARKER));
5209
+ if (marker2 && typeof marker2.appId === "string") {
5210
+ app = { marker: marker2, root: dir };
5211
+ }
5212
+ }
5213
+ const link = readJson(projectLinkPath(dir));
5214
+ if (link && typeof link.projectId === "string") {
5215
+ return {
5216
+ projectRoot: dir,
5217
+ projectId: link.projectId,
5218
+ projectName: link.name,
5219
+ orgId: link.orgId,
5220
+ ...appFields(app)
5221
+ };
5222
+ }
5223
+ const marker = readJson(join(dir, PROJECT_MARKER));
5224
+ if (marker && typeof marker.projectId === "string") {
5225
+ return {
5226
+ projectRoot: dir,
5227
+ projectId: marker.projectId,
5228
+ projectName: marker.projectName ?? void 0,
5229
+ orgId: marker.orgId ?? void 0,
5230
+ ...appFields(app)
5231
+ };
5232
+ }
5233
+ }
5234
+ if (app && typeof app.marker.projectId === "string") {
5235
+ return {
5236
+ projectRoot: app.root,
5237
+ projectId: app.marker.projectId,
5238
+ projectName: app.marker.projectName ?? void 0,
5239
+ orgId: app.marker.orgId ?? void 0,
5240
+ ...appFields(app)
5241
+ };
5242
+ }
5243
+ return null;
5244
+ }
5245
+ function appFields(app) {
5246
+ if (!app) return {};
5247
+ return {
5248
+ appId: app.marker.appId,
5249
+ appName: app.marker.appName ?? void 0,
5250
+ appRoot: app.root
5251
+ };
5252
+ }
5253
+ function ancestors(cwd) {
5254
+ const out = [];
5255
+ const home = homedir();
5256
+ let dir = resolve(cwd);
5257
+ for (let i = 0; i < 64; i++) {
5258
+ out.push(dir);
5259
+ if (dir === home) break;
5260
+ const parent = dirname(dir);
5261
+ if (parent === dir) break;
5262
+ dir = parent;
5263
+ }
5264
+ return out;
5265
+ }
5266
+ function readJson(file) {
5206
5267
  try {
5207
- const p = projectLinkPath(cwd);
5208
- if (!existsSync(p)) return null;
5209
- return JSON.parse(readFileSync(p, "utf8"));
5268
+ if (!existsSync(file)) return null;
5269
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
5270
+ return parsed && typeof parsed === "object" ? parsed : null;
5210
5271
  } catch {
5211
5272
  return null;
5212
5273
  }
@@ -5245,7 +5306,7 @@ function cloudBaseUrl() {
5245
5306
  // src/context.ts
5246
5307
  function buildContext(opts) {
5247
5308
  const session = readSession();
5248
- const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
5309
+ const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
5249
5310
  const token = opts.token || process.env.WORKSER_TOKEN || session.token;
5250
5311
  const overridden = Boolean(
5251
5312
  opts.endpoint || opts.token || process.env.WORKSER_DAEMON_URL || process.env.WORKSER_TOKEN
@@ -5254,11 +5315,18 @@ function buildContext(opts) {
5254
5315
  const endpointRaw = socketPath ? "http://localhost" : opts.endpoint || process.env.WORKSER_DAEMON_URL || session.endpoint || cloudBaseUrl();
5255
5316
  const endpoint = endpointRaw.replace(/\/+$/, "");
5256
5317
  const mode2 = socketPath ? "daemon" : /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/i.test(endpoint) ? "daemon" : "cloud";
5257
- const link = readProjectLink(cwd);
5258
- const projectId = opts.project || process.env.WORKSER_PROJECT_ID || link?.projectId || session.defaultProjectId;
5318
+ const folder = readFolderIdentity(cwd);
5319
+ const projectId = opts.project || process.env.WORKSER_PROJECT_ID || folder?.projectId || session.defaultProjectId;
5320
+ const inThisProject = folder?.projectId === projectId;
5259
5321
  const runId = process.env.WORKSER_RUN_ID || void 0;
5260
5322
  const conversationId = process.env.WORKSER_CONVERSATION_ID || void 0;
5261
5323
  const projectTaskId = process.env.WORKSER_PROJECT_TASK_ID || void 0;
5324
+ const parentTaskId = process.env.WORKSER_PARENT_TASK_ID || void 0;
5325
+ const projectChannelId = process.env.WORKSER_PROJECT_CHANNEL_ID || void 0;
5326
+ const projectChannelMessageId = process.env.WORKSER_PROJECT_CHANNEL_MESSAGE_ID || void 0;
5327
+ const agentRole = process.env.WORKSER_AGENT_ROLE || void 0;
5328
+ const agentType = process.env.WORKSER_AGENT_TYPE || void 0;
5329
+ const agentModel = process.env.WORKSER_AGENT_MODEL || void 0;
5262
5330
  return {
5263
5331
  endpoint,
5264
5332
  socketPath,
@@ -5266,9 +5334,18 @@ function buildContext(opts) {
5266
5334
  mode: mode2,
5267
5335
  cwd,
5268
5336
  projectId,
5337
+ projectRoot: inThisProject ? folder?.projectRoot : void 0,
5338
+ appId: inThisProject ? folder?.appId : void 0,
5339
+ appName: inThisProject ? folder?.appName : void 0,
5269
5340
  runId,
5270
5341
  conversationId,
5271
- projectTaskId
5342
+ projectTaskId,
5343
+ parentTaskId,
5344
+ projectChannelId,
5345
+ projectChannelMessageId,
5346
+ agentRole,
5347
+ agentType,
5348
+ agentModel
5272
5349
  };
5273
5350
  }
5274
5351
  function runTarget(ctx) {
@@ -5304,7 +5381,7 @@ This shell is talking to ${ctx.endpoint} instead of a local app.
5304
5381
  function requireProject(ctx) {
5305
5382
  if (!ctx.projectId) {
5306
5383
  throw new WorkserError(
5307
- "No project selected. Run `workser project use <id>` or pass --project <id>.",
5384
+ "No project selected.\n\nWorkser keeps each project in its own folder \u2014 `~/workser/<org>/<project>/`,\nwith that project's apps inside it. Running from anywhere in that tree is\nenough; this shell is not in one.\n\n \u2022 cd into the project's folder (Workser Orbit's Files tab shows where it is), or\n \u2022 pass --project <id> for a one-off.",
5308
5385
  { code: "no_project" }
5309
5386
  );
5310
5387
  }
@@ -5329,7 +5406,7 @@ function action(fn) {
5329
5406
  // src/client.ts
5330
5407
  import * as http from "http";
5331
5408
  function requestOverSocket(socketPath, pathWithQuery, init) {
5332
- return new Promise((resolve4, reject) => {
5409
+ return new Promise((resolve5, reject) => {
5333
5410
  const req = http.request(
5334
5411
  { socketPath, path: pathWithQuery, method: init.method, headers: init.headers },
5335
5412
  (res) => {
@@ -5338,7 +5415,7 @@ function requestOverSocket(socketPath, pathWithQuery, init) {
5338
5415
  res.on("data", (c) => text += c);
5339
5416
  res.on(
5340
5417
  "end",
5341
- () => resolve4({
5418
+ () => resolve5({
5342
5419
  ok: (res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300,
5343
5420
  status: res.statusCode ?? 0,
5344
5421
  statusText: res.statusMessage ?? "",
@@ -5435,13 +5512,28 @@ function registerStatus(program3) {
5435
5512
  program3.command("status").description("Show connection, current workspace/project, and latest deploy").action(
5436
5513
  action(async ({ ctx }) => {
5437
5514
  const data = await api(ctx, "/v1/status", { query: { project: ctx.projectId } });
5438
- ok(data, () => {
5515
+ const merged = {
5516
+ ...data,
5517
+ folder: {
5518
+ cwd: ctx.cwd,
5519
+ projectRoot: ctx.projectRoot ?? null,
5520
+ appId: ctx.appId ?? null,
5521
+ appName: ctx.appName ?? null
5522
+ }
5523
+ };
5524
+ ok(merged, () => {
5439
5525
  line(import_picocolors3.default.bold("Workser") + import_picocolors3.default.dim(` (${ctx.mode} \xB7 ${ctx.endpoint})`));
5440
5526
  line(` user: ${data.user?.email ?? "\u2014"}`);
5441
5527
  line(` workspace: ${data.workspace?.name ?? "\u2014"}`);
5442
5528
  line(
5443
5529
  ` project: ${data.project?.name ?? "\u2014"}` + (data.project?.id ? import_picocolors3.default.dim(` (${data.project.id})`) : "")
5444
5530
  );
5531
+ if (ctx.projectRoot) line(` folder: ${import_picocolors3.default.dim(ctx.projectRoot)}`);
5532
+ if (ctx.appId) {
5533
+ line(
5534
+ ` app: ${ctx.appName ?? "\u2014"}` + import_picocolors3.default.dim(` (${ctx.appId})`)
5535
+ );
5536
+ }
5445
5537
  if (data.latestDeploy) {
5446
5538
  line(
5447
5539
  ` deploy: ${colorStatus(data.latestDeploy.status)}` + (data.latestDeploy.url ? ` ${import_picocolors3.default.cyan(data.latestDeploy.url)}` : "")
@@ -5778,7 +5870,7 @@ function registerAuth(program3) {
5778
5870
  // src/commands/storage.ts
5779
5871
  var import_picocolors7 = __toESM(require_picocolors(), 1);
5780
5872
  import { readFile, writeFile } from "fs/promises";
5781
- import { basename, dirname } from "path";
5873
+ import { basename, dirname as dirname2 } from "path";
5782
5874
  function registerStorage(program3) {
5783
5875
  const storage = program3.command("storage").description("Provision and work with the project's object storage (Cloudflare R2)");
5784
5876
  storage.command("create [name]").description("Provision a storage bucket for the project (idempotent)").action(
@@ -5824,7 +5916,7 @@ function registerStorage(program3) {
5824
5916
  const bytes = await readFile(localPath).catch(() => {
5825
5917
  throw new WorkserError(`Can't read local file: ${localPath}`, { code: "bad_input" });
5826
5918
  });
5827
- const dir = dirname(key);
5919
+ const dir = dirname2(key);
5828
5920
  const res = await api(ctx, `/v1/projects/${projectId}/storage/upload-base64`, {
5829
5921
  body: {
5830
5922
  filename: basename(key),
@@ -6980,7 +7072,10 @@ function registerAgent(program3) {
6980
7072
  ).option("--role <label>", "display label for this run (default: <agent>)").option(
6981
7073
  "--instructions <text>",
6982
7074
  "system prompt for this one run \u2014 this teammate's expertise/scope"
6983
- ).option("--model <model>", "model override for the backing CLI").option("--effort <level>", "reasoning effort, where the backing CLI supports it").action(
7075
+ ).option("--model <model>", "model override for the backing CLI").option("--effort <level>", "reasoning effort, where the backing CLI supports it").option(
7076
+ "--read-only",
7077
+ "inspection only \u2014 the teammate runs in the CLI's read-only sandbox and cannot change anything"
7078
+ ).action(
6984
7079
  action(async ({ ctx, args, opts }) => {
6985
7080
  const spawnAgent = args[0];
6986
7081
  const task = args[1].join(" ");
@@ -6991,7 +7086,10 @@ function registerAgent(program3) {
6991
7086
  role: opts.role,
6992
7087
  instructions: opts.instructions,
6993
7088
  model: opts.model,
6994
- effort: opts.effort
7089
+ effort: opts.effort,
7090
+ // Enforced by the backing CLI's own sandbox, not by asking the
7091
+ // spawned agent to behave — see the daemon's `/agents/run`.
7092
+ readOnly: opts.readOnly === true
6995
7093
  }
6996
7094
  });
6997
7095
  const exitCode = res?.exitCode ?? 0;
@@ -7517,7 +7615,7 @@ function businessPath(projectId, resource, subpath) {
7517
7615
  // src/commands/artifact.ts
7518
7616
  var import_picocolors24 = __toESM(require_picocolors(), 1);
7519
7617
  import { existsSync as existsSync2, statSync } from "fs";
7520
- import { resolve as resolve2, basename as basename3 } from "path";
7618
+ import { resolve as resolve3, basename as basename3 } from "path";
7521
7619
  var KINDS = [
7522
7620
  "file",
7523
7621
  "folder",
@@ -7566,7 +7664,7 @@ function registerArtifact(program3) {
7566
7664
  let absPath;
7567
7665
  let kind = opts.kind;
7568
7666
  if (rawPath) {
7569
- absPath = resolve2(ctx.cwd, rawPath);
7667
+ absPath = resolve3(ctx.cwd, rawPath);
7570
7668
  if (!existsSync2(absPath)) {
7571
7669
  throw new WorkserError(
7572
7670
  `Nothing at ${absPath}. Register the file after you've written it.`,
@@ -7617,6 +7715,30 @@ function registerArtifact(program3) {
7617
7715
  );
7618
7716
  })
7619
7717
  );
7718
+ artifact.command("list").description("What this task's steps have produced so far").option("--task <id>", "a specific task (default: the plan this run is in)").option("--step <id>", "one step's output alone, across every attempt").option("--mine", "only what THIS step has produced").action(
7719
+ action(async ({ ctx, opts }) => {
7720
+ const stepId = opts.step || (opts.mine ? ctx.projectTaskId : void 0);
7721
+ if (opts.mine && !stepId) {
7722
+ throw new WorkserError(
7723
+ "--mine needs a step: this run isn't inside one (WORKSER_PROJECT_TASK_ID is unset)."
7724
+ );
7725
+ }
7726
+ let path;
7727
+ if (stepId) {
7728
+ path = `/v1/project-subtasks/${encodeURIComponent(stepId)}/artifacts`;
7729
+ } else {
7730
+ const taskId = opts.task || ctx.parentTaskId || ctx.projectTaskId;
7731
+ if (!taskId) {
7732
+ throw new WorkserError(
7733
+ "No task. This run isn't inside one, so pass --task <id> or --step <id>."
7734
+ );
7735
+ }
7736
+ path = `/v1/project-tasks/${encodeURIComponent(taskId)}/artifacts?scope=all`;
7737
+ }
7738
+ const rows = await api(ctx, path) ?? [];
7739
+ ok(rows, () => printArtifacts(rows));
7740
+ })
7741
+ );
7620
7742
  artifact.command("run").description("Show the task/conversation this agent run is attached to").action(
7621
7743
  action(async ({ ctx }) => {
7622
7744
  const res = await api(ctx, `/v1/runs/${runTarget(ctx)}`);
@@ -7624,6 +7746,31 @@ function registerArtifact(program3) {
7624
7746
  })
7625
7747
  );
7626
7748
  }
7749
+ function printArtifacts(rows) {
7750
+ if (!rows.length) {
7751
+ line(import_picocolors24.default.dim("Nothing produced yet."));
7752
+ return;
7753
+ }
7754
+ const byStep = /* @__PURE__ */ new Map();
7755
+ for (const r of rows) {
7756
+ const list = byStep.get(r.subtask_id) ?? [];
7757
+ list.push(r);
7758
+ byStep.set(r.subtask_id, list);
7759
+ }
7760
+ for (const [stepId, items] of byStep) {
7761
+ line(import_picocolors24.default.bold(`step ${stepId}`));
7762
+ for (const a of items) {
7763
+ const flag = a.promoted_at ? import_picocolors24.default.green(" *") : " ";
7764
+ const where = a.local_path || a.cloud_url || "";
7765
+ line(
7766
+ `${flag} ${import_picocolors24.default.dim(`[${a.kind}]`)} ${a.title ?? "(untitled)"}` + (where ? import_picocolors24.default.dim(` ${where}`) : "")
7767
+ );
7768
+ if (a.description) line(import_picocolors24.default.dim(` ${a.description}`));
7769
+ }
7770
+ line("");
7771
+ }
7772
+ line(import_picocolors24.default.dim("* = handed over as a deliverable; the rest is working material."));
7773
+ }
7627
7774
  function printRun(run) {
7628
7775
  if (!run) return;
7629
7776
  line(` run ${import_picocolors24.default.bold(run.runId)}`);
@@ -7635,7 +7782,7 @@ function printRun(run) {
7635
7782
 
7636
7783
  // src/commands/image.ts
7637
7784
  import { writeFile as writeFile4, mkdir } from "fs/promises";
7638
- import { dirname as dirname2, resolve as resolve3 } from "path";
7785
+ import { dirname as dirname3, resolve as resolve4 } from "path";
7639
7786
  function registerImage(program3) {
7640
7787
  const image = program3.command("image").description("Generate images from a text prompt");
7641
7788
  image.command("generate <prompt>").alias("gen").description("Generate an image and return its public URL").option(
@@ -7690,7 +7837,7 @@ function registerImage(program3) {
7690
7837
  );
7691
7838
  }
7692
7839
  async function download(url, output) {
7693
- const target = resolve3(output);
7840
+ const target = resolve4(output);
7694
7841
  const res = await fetch(url);
7695
7842
  if (!res.ok) {
7696
7843
  throw new WorkserError(
@@ -7698,7 +7845,7 @@ async function download(url, output) {
7698
7845
  { code: "download_failed" }
7699
7846
  );
7700
7847
  }
7701
- await mkdir(dirname2(target), { recursive: true });
7848
+ await mkdir(dirname3(target), { recursive: true });
7702
7849
  await writeFile4(target, Buffer.from(await res.arrayBuffer()));
7703
7850
  return target;
7704
7851
  }
@@ -8051,18 +8198,86 @@ function registerTask(program3) {
8051
8198
  })
8052
8199
  );
8053
8200
  task.command("show [id]").description(
8054
- "Show one task with its subtasks. Defaults to the task this run is inside."
8201
+ "Show a task with its subtasks, what each one did, and what they produced. Defaults to the plan this run is part of."
8055
8202
  ).action(
8056
8203
  action(async ({ ctx, args }) => {
8057
- const id = resolveTaskId(ctx, args[0]);
8204
+ const id = args[0] || ctx.parentTaskId || resolveTaskId(ctx);
8058
8205
  const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
8059
8206
  ok(row, () => printTask(row));
8060
8207
  })
8061
8208
  );
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(
8210
+ action(async ({ ctx, args, opts }) => {
8211
+ requireProject(ctx);
8212
+ if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
8213
+ const hasChannelOrigin = Boolean(
8214
+ ctx.projectChannelId && ctx.projectChannelMessageId
8215
+ );
8216
+ const row = await api(ctx, "/v1/project-tasks", {
8217
+ body: {
8218
+ title: args[0],
8219
+ summary: opts.note,
8220
+ category: opts.kind,
8221
+ labels: opts.label,
8222
+ targets: [
8223
+ ...(opts.app ?? []).map((appId) => ({ kind: "app", appId })),
8224
+ ...(opts.infra ?? []).map((ref) => ({ kind: "infra", ref }))
8225
+ ],
8226
+ ...hasChannelOrigin ? {
8227
+ channelId: ctx.projectChannelId,
8228
+ createdFromMessageId: ctx.projectChannelMessageId,
8229
+ createdByKind: "agent"
8230
+ } : {}
8231
+ }
8232
+ });
8233
+ let channelMessage;
8234
+ let channelMessageError;
8235
+ if (hasChannelOrigin && ctx.projectChannelId && ctx.projectChannelMessageId) {
8236
+ try {
8237
+ channelMessage = await api(
8238
+ ctx,
8239
+ `/v1/project-channels/${encodeURIComponent(ctx.projectChannelId)}/messages`,
8240
+ {
8241
+ body: {
8242
+ content: "",
8243
+ agentRole: ctx.agentRole ?? "pm",
8244
+ agentType: ctx.agentType ?? "workser",
8245
+ agentModel: ctx.agentModel ?? "Agent default",
8246
+ attachments: [{ resourceType: "task", resourceId: row.id }]
8247
+ }
8248
+ }
8249
+ );
8250
+ } catch (error) {
8251
+ const failure = error instanceof WorkserError ? error : new WorkserError(String(error));
8252
+ channelMessageError = {
8253
+ code: failure.code,
8254
+ message: failure.message
8255
+ };
8256
+ }
8257
+ }
8258
+ const result = {
8259
+ ...row,
8260
+ ...channelMessage ? { channelMessage } : {},
8261
+ ...channelMessageError ? { channelMessageError } : {}
8262
+ };
8263
+ ok(result, () => {
8264
+ line(`${import_picocolors28.default.green("opened")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
8265
+ if (channelMessage) line(import_picocolors28.default.dim("posted to the channel as Project Manager"));
8266
+ if (channelMessageError) {
8267
+ warn(
8268
+ `Task opened, but its Project Manager card could not be posted: ${channelMessageError.message}`
8269
+ );
8270
+ }
8271
+ });
8272
+ })
8273
+ );
8062
8274
  const subtask = task.command("subtask").description("The steps a task is broken into");
8063
8275
  subtask.command("add <title>").description(
8064
8276
  'Add one step, e.g. `workser task subtask add "Build the upload screen" --role web`'
8065
- ).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("--app <id...>", "app ids this step touches").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(
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(
8278
+ "--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(
8066
8281
  action(async ({ ctx, args, opts }) => {
8067
8282
  const parent = resolveTaskId(ctx, opts.task);
8068
8283
  if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
@@ -8143,6 +8358,24 @@ function registerTask(program3) {
8143
8358
  ok(row, () => line(`${import_picocolors28.default.green("moved")} ${import_picocolors28.default.bold(row.title)} \u2192 ${args[1]}`));
8144
8359
  })
8145
8360
  );
8361
+ 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"
8363
+ ).action(
8364
+ action(async ({ ctx, args }) => {
8365
+ const id = resolveTaskId(ctx, args[0]);
8366
+ const row = await api(
8367
+ ctx,
8368
+ `/v1/project-tasks/${encodeURIComponent(id)}/retry`,
8369
+ { method: "POST" }
8370
+ );
8371
+ ok(
8372
+ row,
8373
+ () => line(
8374
+ row?.queued ? `${import_picocolors28.default.green("resumed")} \u2014 it is back in the queue and will start on the next dispatch` : import_picocolors28.default.yellow("could not resume it \u2014 check the step id")
8375
+ )
8376
+ );
8377
+ })
8378
+ );
8146
8379
  subtask.command("send-back <id>").description("Send a finished step back to be done again, with the reason").requiredOption("--note <text>", "what was wrong with it").action(
8147
8380
  action(async ({ ctx, args, opts }) => {
8148
8381
  const row = await api(
@@ -8275,7 +8508,12 @@ function formatSubtask(r, index) {
8275
8508
  const n = import_picocolors28.default.dim(String(index).padStart(2, "0"));
8276
8509
  const role = r.role ? import_picocolors28.default.dim(` [${r.role}]`) : "";
8277
8510
  const scope = r.scope_paths?.length ? import_picocolors28.default.dim(` owns: ${r.scope_paths.join(", ")}`) : "";
8278
- return `${n} ${statusTag2(r.status)} ${r.title}${role}${scope}`;
8511
+ const head = `${n} ${statusTag2(r.status)} ${r.title}${role}${scope}`;
8512
+ const summary = (r.result_summary ?? "").trim();
8513
+ if (!summary) return head;
8514
+ const wrapped = summary.split("\n").map((l) => ` ${l}`).join("\n");
8515
+ return `${head}
8516
+ ${import_picocolors28.default.dim(wrapped)}`;
8279
8517
  }
8280
8518
  function printTask(row) {
8281
8519
  line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
@@ -8292,6 +8530,17 @@ touches: ${row.targets.map((t) => t.appName ?? t.ref ?? t.kind).join(", ")}`
8292
8530
  line(`
8293
8531
  ${import_picocolors28.default.bold("steps")}`);
8294
8532
  row.subtasks.forEach((s, i) => line(formatSubtask(s, i + 1)));
8533
+ line(
8534
+ import_picocolors28.default.dim(
8535
+ `
8536
+ Run \`workser artifact list\` to see what these steps produced,`
8537
+ )
8538
+ );
8539
+ line(
8540
+ import_picocolors28.default.dim(
8541
+ `or \`workser artifact list --step <id>\` for one step's output alone.`
8542
+ )
8543
+ );
8295
8544
  } else {
8296
8545
  line(import_picocolors28.default.dim("\nNo steps yet."));
8297
8546
  }
@@ -8315,10 +8564,15 @@ function statusTag2(status) {
8315
8564
 
8316
8565
  // src/role-guard.ts
8317
8566
  var READS = [
8567
+ // `requirement` sits beside `doc` and `decision` because it is the same kind
8568
+ // of thing and was simply forgotten: the verb shipped, no role could run it,
8569
+ // and on 2026-08-23 a channel PM reported to the owner that "requirements are
8570
+ // not readable by this PM role" — which was exactly true, for every role.
8318
8571
  "task",
8319
8572
  "board",
8320
8573
  "doc",
8321
8574
  "decision",
8575
+ "requirement",
8322
8576
  "memory",
8323
8577
  "search",
8324
8578
  "verify",
@@ -8393,10 +8647,11 @@ var NEVER = {
8393
8647
  function assertRoleMayRun(argv) {
8394
8648
  const role = (process.env.WORKSER_ROLE ?? "").trim();
8395
8649
  if (!role) return;
8396
- const verb = argv[0];
8650
+ const commandArgv = stripLeadingGlobalOptions(argv);
8651
+ const verb = commandArgv[0];
8397
8652
  if (!verb) return;
8398
- const pair = `${argv[0]} ${argv[1] ?? ""}`.trim();
8399
- if (NEVER[pair] && !(pair === "task approval" && (argv[2] === "request" || !argv[2]))) {
8653
+ const pair = `${commandArgv[0]} ${commandArgv[1] ?? ""}`.trim();
8654
+ if (NEVER[pair] && !(pair === "task approval" && (commandArgv[2] === "request" || !commandArgv[2]))) {
8400
8655
  throw new WorkserError(NEVER[pair], { code: "role_forbidden" });
8401
8656
  }
8402
8657
  const allowed = ROLE_VERBS[role];
@@ -8408,6 +8663,35 @@ function assertRoleMayRun(argv) {
8408
8663
  );
8409
8664
  }
8410
8665
  }
8666
+ function stripLeadingGlobalOptions(argv) {
8667
+ const takesValue = /* @__PURE__ */ new Set([
8668
+ "-p",
8669
+ "--project",
8670
+ "-C",
8671
+ "--cwd",
8672
+ "--endpoint",
8673
+ "--token"
8674
+ ]);
8675
+ const flags = /* @__PURE__ */ new Set(["--json", "-q", "--quiet", "-v", "--version"]);
8676
+ let index = 0;
8677
+ while (index < argv.length) {
8678
+ const token = argv[index];
8679
+ if (flags.has(token)) {
8680
+ index += 1;
8681
+ continue;
8682
+ }
8683
+ if (takesValue.has(token)) {
8684
+ index += 2;
8685
+ continue;
8686
+ }
8687
+ if (token.startsWith("--project=") || token.startsWith("--cwd=") || token.startsWith("--endpoint=") || token.startsWith("--token=")) {
8688
+ index += 1;
8689
+ continue;
8690
+ }
8691
+ break;
8692
+ }
8693
+ return argv.slice(index);
8694
+ }
8411
8695
 
8412
8696
  // src/commands/decision.ts
8413
8697
  var import_picocolors29 = __toESM(require_picocolors(), 1);
@@ -9856,7 +10140,7 @@ function colour(d) {
9856
10140
 
9857
10141
  // src/index.ts
9858
10142
  var pkg = {
9859
- version: true ? "0.6.0" : "0.0.0-dev"
10143
+ version: true ? "0.6.1" : "0.0.0-dev"
9860
10144
  };
9861
10145
  var program2 = new Command();
9862
10146
  program2.name("workser").description(
@@ -9912,6 +10196,10 @@ registerTask(program2);
9912
10196
  registerDecision(program2);
9913
10197
  registerDoc(program2);
9914
10198
  registerDesign(program2);
10199
+ configureOutput({
10200
+ json: process.argv.includes("--json"),
10201
+ quiet: process.argv.includes("--quiet") || process.argv.includes("-q")
10202
+ });
9915
10203
  try {
9916
10204
  assertRoleMayRun(process.argv.slice(2));
9917
10205
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workser/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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,7 +18,7 @@ load.
18
18
 
19
19
  | You need to… | Commands | Run |
20
20
  | --- | --- | --- |
21
- | See what's tracked, decided, or written down for this project | `board …`, `decision …`, `requirement …`, `doc …` | `workser help sdlc-entities` |
21
+ | See what was decided or written down for this project | `decision …`, `requirement …`, `doc …` | `workser help sdlc-entities` |
22
22
  | Follow the project's brand — colours, fonts, logo | `design …` | `workser help brand` |
23
23
  | Provision or query Postgres; list end users | `db …`, `auth …` | `workser help database` |
24
24
  | Deploy, set env vars, read logs, check a domain | `deploy`, `env …`, `logs`, `versions`, `domain`, `open` | `workser help deploy` |
@@ -70,15 +70,16 @@ pick or switch it.
70
70
  2. **Orient first.** Run `workser status --json` to see the connection, the pinned
71
71
  project, and the latest deploy before acting. You don't pick or switch projects.
72
72
  For anything beyond a trivial edit, also read what the project already knows:
73
- `workser board list --json` (what's tracked), `workser decision list --json`
74
- (what was already decided, so you don't quietly reverse it), and
75
- `workser design show --json` before writing UI. This project outlives your
76
- session; that context is how you don't start from zero.
77
- 3. **A phased plan goes on the Board before you build it.** One `board create`
78
- card per phase, the plan itself as one `doc create` (no `--work-item`, or it's
79
- hidden from the Docs panel), and a `decision create` if it settled a real
80
- tradeoff. A plan that lives only in your reply is gone when the conversation
81
- scrolls. Details: `workser help sdlc-entities`.
73
+ `workser decision list --json` (what was already decided, so you don't quietly
74
+ reverse it), and `workser design show --json` before writing UI. This project
75
+ outlives your session; that context is how you don't start from zero.
76
+ 3. **A phased plan goes on the subtask list, never the Board.** Phases are
77
+ `workser task subtask add` (`workser help tasks`) not `board create`,
78
+ which makes a second, driftable "the plan" the task page never reads.
79
+ Write the narrative once as `doc create` (no `--work-item`, or it's hidden
80
+ from the Docs panel), plus `decision create` for a real tradeoff. A plan
81
+ in your reply alone is gone when the conversation scrolls. Details:
82
+ `workser help sdlc-entities`.
82
83
  4. **Stay in your lane.** `error.code = "owner_only"` (exit 6) means the action is
83
84
  reserved for the owner in Orbit. Don't retry or look for a workaround — tell the
84
85
  user, then continue. Provisioning the *pinned project's own* db / bucket / auth is
@@ -98,17 +99,15 @@ pick or switch it.
98
99
  ## Typical flow: build → ship
99
100
 
100
101
  ```bash
101
- workser status --json # 1. orient (project is already pinned)
102
- workser board list --json # 2. what's already tracked
103
- workser decision list --json # …and already decided
104
- workser board create "Phase 2 …" --json # 3. phases? file them + the plan doc
105
- workser board move <id> in-progress --json # 4. claim the card you're doing
106
- workser db create --json # 5. provision infra the app needs (idempotent)
107
- workser env set STRIPE_KEY=sk_live_… --json # 6. configure it
102
+ workser status --json # 1. orient (project is already pinned)
103
+ workser decision list --json # 2. what's already decided
104
+ workser task subtask add "Phase 2 — …" --json # 3. phases? file them as subtasks
105
+ workser doc create "Plan" --markdown "…" --json # 4. the plan's narrative, once
106
+ workser db create --json # 5. provision infra the app needs (idempotent)
107
+ workser env set STRIPE_KEY=sk_live_… --json # 6. configure it
108
108
  # … you write the app code with your normal tools …
109
- workser verify --json # 7. green build is the bar for "done"
110
- workser deploy --prod --watch --json # 8. ship; returns the stable *.workser.app URL
111
- workser board close <id> --json # 9. the Board now matches reality
109
+ workser verify --json # 7. green build is the bar for "done"
110
+ workser deploy --prod --watch --json # 8. ship; returns the stable *.workser.app URL
112
111
  ```
113
112
 
114
113
  ## Reading results
@@ -1,28 +1,27 @@
1
1
  ---
2
2
  topic: sdlc-entities
3
- title: Board cards, decisions and requirements
4
- summary: Read what this project already tracks and decided, keep the Board honest as you work, and record what a future maintainer will need.
3
+ title: Decisions and requirements
4
+ summary: Read what this project already decided, and record what a future maintainer will need. Phased work itself is subtasks, not board cards see `workser help tasks`.
5
5
  commands: [board, decision, requirement]
6
6
  ---
7
7
 
8
- # Board cards, decisions and requirements
8
+ # Decisions and requirements
9
9
 
10
10
  These are the project's memory across sessions. They write to the **same tables**
11
- the Orbit desktop's Board and Project Memory panels use, so anything here appears
12
- there too — and, inside an Orbit-spawned run, as an inline card in the
13
- conversation. Documents have their own guide: `workser help docs`.
11
+ the Orbit desktop's Project Memory panel uses, so anything here appears there too
12
+ — and, inside an Orbit-spawned run, as an inline card in the conversation.
13
+ Documents have their own guide: `workser help docs`. Phased work is tracked as
14
+ subtasks, not here — see `workser help tasks`.
15
+
16
+ > **The Board (`workser board ...`) is deprecated for agent use.** It used to be
17
+ > where a multi-phase plan went, one card per phase — and the phase was ALSO a
18
+ > subtask the planning turn had just filed for the same piece of work. That gave
19
+ > a task two competing plans, one of which this task's own page never reads and
20
+ > nothing kept in sync with the other. Phases are `project_tasks` subtasks now,
21
+ > full stop: `workser task subtask add`. Do not run `workser board create` for
22
+ > planned work — see `workser help tasks`.
14
23
 
15
24
  ```
16
- workser board list [--status <value>] [--label <value>] [--limit <n>]
17
- workser board show <id>
18
- workser board create <title> [--description <text>] [--status <value>]
19
- [--priority <value>] [--label <value>]
20
- [--owner <name>] [--milestone <id>]
21
- workser board update <id> [--title|--description|--status|--priority
22
- |--label|--owner|--milestone ...]
23
- workser board move <id> <backlog|in-progress|in-review|done>
24
- workser board close <id>
25
-
26
25
  workser decision list [--limit <n>]
27
26
  workser decision show <id>
28
27
  workser decision create <title> --context <text> --decision <text>
@@ -40,7 +39,6 @@ workser requirement update <id> [--title <text>] [--body <text>] [--status <text
40
39
  Before starting anything beyond a trivial edit:
41
40
 
42
41
  ```
43
- workser board list --json # what's already tracked (don't re-file it)
44
42
  workser decision list --json # what was already decided (don't reverse it)
45
43
  ```
46
44
 
@@ -50,19 +48,18 @@ purpose — `workser decision show <id>` gives you the context and consequences,
50
48
  not just the title. Reach for `workser doc list` / `workser requirement list`
51
49
  the same way when the task touches documented behaviour.
52
50
 
53
- ## Work with phases → cards + a plan doc, before you build
51
+ ## Work with phases → subtasks + a plan doc, before you build
54
52
 
55
53
  The moment you split a task into more than one phase, file it — not afterwards,
56
54
  and not only in your reply, which is gone once the conversation scrolls.
57
55
 
58
56
  ```bash
59
- # one card per phase; only the one you're doing goes to in-progress
60
- workser board create "Phase 1 — schema + migration" \
61
- --description "Add orders/line_items tables and the migration." \
62
- --status in-progress --json
63
- workser board create "Phase 2 — checkout API" --description "…" --json
57
+ # the phases themselves this task's own subtask list, not the Board
58
+ workser task subtask add "Phase 1 — schema + migration" --role api \
59
+ --note "Add orders/line_items tables and the migration."
60
+ workser task subtask add "Phase 2 — checkout API" --role api --note "…"
64
61
 
65
- # the plan itself, ONE doc, deliberately NOT linked to a card
62
+ # the plan's narrative, ONE doc, deliberately NOT linked to a subtask
66
63
  workser doc create "Checkout — implementation plan" --markdown "$(cat plan.md)" --json
67
64
 
68
65
  # the approach, if the plan settled something with real alternatives
@@ -73,32 +70,9 @@ workser decision create "Carts live server-side" --context "…" --decision "…
73
70
  its card and is *hidden* from the Docs panel; a plan spanning three phases
74
71
  belongs to the project, not to phase 1.
75
72
 
76
- The bar: if the user closed this conversation now, the Board should still show
77
- what's left and the doc should still explain the plan to whoever continues it.
78
-
79
- ## Keep the Board honest while you work
80
-
81
- A Board still reading `backlog` after the feature shipped tells the user the
82
- opposite of the truth. Moving the card is part of finishing the work:
83
-
84
- ```
85
- workser board move <id> in-progress # you picked it up
86
- workser board move <id> in-review # ready for the user to look at
87
- workser board close <id> # done and verified
88
- ```
89
-
90
- `--status` is one of `backlog | in-progress | in-review | done` (default
91
- `backlog`). `--priority` is one of `low | normal | high | urgent` (default
92
- `normal`). `--label` repeats for more than one label:
93
-
94
- ```
95
- workser board create "Fix the login bug" --status in-progress --priority high \
96
- --label bug --label auth
97
- ```
98
-
99
- `board update` replaces the labels you pass rather than merging them, and
100
- touches only the fields you name. There is no `board delete`: `done` is the
101
- terminal state, and removing a card the user filed is theirs to do in Orbit.
73
+ The bar: if the user closed this conversation now, the subtask list should still
74
+ show what's left and the doc should still explain the plan to whoever continues
75
+ it.
102
76
 
103
77
  ## Decisions are append-only
104
78
 
@@ -14,6 +14,9 @@ below defaults to it and you rarely pass an id at all.
14
14
  ```
15
15
  workser task list [--status <value>] [--label <value>] [--limit <n>]
16
16
  workser task show [id] # the task you are in, with its steps
17
+ workser task create <title> [--note <text>] [--kind <value>]
18
+ [--label <value...>] [--app <id...>]
19
+ [--infra <ref...>]
17
20
 
18
21
  workser task subtask add <title> [--role <value>] [--kind <value>]
19
22
  [--note <text>] [--app <id...>]
@@ -37,6 +40,16 @@ Team's own table. Filing your plan on the Board puts it somewhere the owner's
37
40
  task page never reads: they see "created work item" and an empty plan. Use
38
41
  `task subtask add`.
39
42
 
43
+ ## Opening work from a project channel
44
+
45
+ When a project-channel conversation produces actionable work, the PM may record
46
+ it with `workser task create`. Orbit supplies the channel and source-message IDs;
47
+ the command records them and posts the new task card as a Project Manager message
48
+ automatically. Do not invent or ask for those IDs.
49
+
50
+ Opening a task does **not** approve it or start implementation. The task remains
51
+ awaiting the owner. Never approve or dispatch a task you opened yourself.
52
+
40
53
  ## Planning a task
41
54
 
42
55
  Read the project first, then propose. One `subtask add` per step: