@workser/cli 0.2.2 → 0.2.4

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
@@ -3709,6 +3709,67 @@ workflow produces a result the app needs, its final node has to POST back to a
3709
3709
  webhook route in the app. Build only the trigger and the workflow runs perfectly
3710
3710
  while nothing ever appears in the product. The app-side receiver is covered in the
3711
3711
  \`workser-sdk\` skill under workflows.
3712
+ `
3713
+ },
3714
+ {
3715
+ topic: "brand",
3716
+ title: "The project's brand",
3717
+ summary: "Read the owner's colours, fonts and logo before writing UI, and wire the generated design tokens into the app's theme.",
3718
+ commands: ["design"],
3719
+ source: "skills/workser/reference/brand.md",
3720
+ body: `# The project's brand
3721
+
3722
+ \`\`\`
3723
+ workser design show [--raw]
3724
+ \`\`\`
3725
+
3726
+ The owner sets their brand once in Workser (logo, colours, fonts). Everything
3727
+ you build should follow it. Read it **before writing any UI**:
3728
+
3729
+ \`\`\`bash
3730
+ workser design show --json
3731
+ \`\`\`
3732
+
3733
+ \`\`\`json
3734
+ {"ok":true,"data":{"hasBrand":true,
3735
+ "colors":{"primary":"#1f7a4d","accent":"#f5a623"},
3736
+ "fonts":{"heading":"Inter","body":"Inter"},
3737
+ "brand":{"name":"Green Grocer"},
3738
+ "files":["design/tokens.json","design/tokens.css"]}}
3739
+ \`\`\`
3740
+
3741
+ This asks the server, which derives the answer from the brand record itself \u2014 so
3742
+ it is correct even if nothing has been written into the working tree yet. That's
3743
+ the reason to use it instead of looking for a file and guessing when it's absent.
3744
+
3745
+ \`"hasBrand": false\` is an ordinary state \u2014 most projects have no brand set.
3746
+ Choose sensible styling yourself; **don't** stop and ask the user to define a
3747
+ brand first.
3748
+
3749
+ ## Using it in the code
3750
+
3751
+ The same values are generated into the working tree as two files:
3752
+
3753
+ - \`design/tokens.json\` \u2014 the tokens in DTCG form
3754
+ - \`design/tokens.css\` \u2014 plain CSS custom properties (\`--ws-color-*\`, \`--ws-font-*\`)
3755
+
3756
+ Wire \`design/tokens.css\` into the app's theme the way this app's setup expects \u2014
3757
+ it assumes no particular Tailwind version, so map it rather than pasting it.
3758
+
3759
+ **Both files are generated and overwritten wholesale.** Never hand-edit them;
3760
+ your edit disappears on the next sync. \`workser design show --raw\` prints them
3761
+ verbatim if you need to see exactly what's in them.
3762
+
3763
+ ## Changing the brand is the owner's job
3764
+
3765
+ There is no \`workser design set\` \u2014 the brand is one record the owner controls in
3766
+ the Workser app, and it drives everything generated from it. If the user asks you
3767
+ to change their brand colour, tell them where it lives (Design \u2192 Brand in Orbit)
3768
+ rather than writing the colour into the app by hand, which would drift from every
3769
+ other surface the brand feeds.
3770
+
3771
+ For generating artwork *in* the brand's palette, see \`workser help images\` \u2014
3772
+ put the colours from \`design show\` into the prompt.
3712
3773
  `
3713
3774
  },
3714
3775
  {
@@ -4090,26 +4151,194 @@ The user can configure **roles** \u2014 named specialists each backed by a local
4090
4151
  (e.g. \`qa\` \u2192 codex, \`designer\` \u2192 claude_code).
4091
4152
 
4092
4153
  \`\`\`
4093
- workser agent list # main agent + configured roles (+ which are runnable)
4094
- workser agent run <role> "<task>" # delegate a focused subtask (runs isolated)
4154
+ workser agent list # main agent + configured roles + which agents are connected
4155
+ workser agent run <role> "<task>" # delegate to a CONFIGURED role (runs isolated)
4156
+ workser agent spawn <agent> "<task>" [--role <label>] [--instructions <text>] [--model <model>]
4157
+ # spin up a TEMPORARY teammate on any connected agent CLI
4095
4158
  workser agent main # show the configured main agent
4096
4159
  \`\`\`
4097
4160
 
4098
4161
  ## How to use it
4099
4162
 
4100
- Run \`workser agent list --json\` first \u2014 it tells you which roles exist **and** which
4101
- are actually runnable on this machine. Delegating to a role that isn't installed just
4102
- fails.
4163
+ Run \`workser agent list --json\` first \u2014 it tells you which roles exist, which are
4164
+ actually runnable on this machine, and which agent CLIs are connected at all
4165
+ (\`spawnable\`). Delegating to a role that isn't installed, or spawning an agent that
4166
+ isn't connected, just fails.
4167
+
4168
+ \`workser agent run <role> "<task>" --json\` runs a **configured** role as an isolated
4169
+ local subagent and returns \`{role, agent, output, exitCode}\`.
4170
+
4171
+ \`workser agent spawn <agent> "<task>" --json\` does the same thing without a
4172
+ pre-configured role \u2014 \`<agent>\` is one of \`claude_code|codex|kimi|opencode|grok\`, any
4173
+ of which can be connected on this machine. Use it to fan work out in parallel for
4174
+ one-off work with no matching role: e.g. spawn \`codex\` to research a topic while you
4175
+ keep working, or run a second \`claude_code\` instance on a different part of the same
4176
+ task. \`--instructions\` sets that teammate's system prompt for this one run \u2014 give it
4177
+ real scope, not just a repeat of the task.
4178
+
4179
+ - Hand off focused subtasks \u2014 review this diff, design this screen, research this API \u2014
4180
+ to keep your own context lean and get a specialized second perspective.
4181
+ - **A non-zero \`exitCode\` means the run failed.** Surface that; don't quietly treat
4182
+ empty output as success.
4183
+ - The subagent doesn't share your context, configured role or spawned. Put everything
4184
+ it needs in the task string (and \`--instructions\` for a spawn); it cannot see the
4185
+ conversation you're in.
4186
+ - Never give a subagent \u2014 configured or spawned \u2014 a task that tells it to delegate or
4187
+ spawn further. It has no supervision loop to stop a runaway chain.
4188
+ `
4189
+ },
4190
+ {
4191
+ topic: "sdlc-entities",
4192
+ title: "Board cards, decisions, requirements, and docs",
4193
+ summary: "Read what this project already tracks and decided, keep the Board honest as you work, and record what a future maintainer will need.",
4194
+ commands: ["board", "decision", "requirement", "doc"],
4195
+ source: "skills/workser/reference/sdlc-entities.md",
4196
+ body: `# Board cards, decisions, requirements, and docs
4197
+
4198
+ These are the project's memory across sessions. They write to the **same tables**
4199
+ the Orbit desktop's Board, Project Memory, and Docs panels use, so anything here
4200
+ appears there too \u2014 and (when this CLI runs inside an Orbit-spawned agent run)
4201
+ as an inline card in the conversation you're working in.
4202
+
4203
+ \`\`\`
4204
+ workser board list [--status <value>] [--label <value>] [--limit <n>]
4205
+ workser board show <id>
4206
+ workser board create <title> [--description <text>] [--status <value>]
4207
+ [--priority <value>] [--label <value>]
4208
+ [--owner <name>] [--milestone <id>]
4209
+ workser board update <id> [--title|--description|--status|--priority
4210
+ |--label|--owner|--milestone ...]
4211
+ workser board move <id> <backlog|in-progress|in-review|done>
4212
+ workser board close <id>
4213
+
4214
+ workser decision list [--limit <n>]
4215
+ workser decision show <id>
4216
+ workser decision create <title> --context <text> --decision <text>
4217
+ [--consequences <text>]
4218
+
4219
+ workser requirement list [--status <value>] [--limit <n>]
4220
+ workser requirement show <id>
4221
+ workser requirement create <title> --body <text> [--status <text>]
4222
+ workser requirement update <id> [--title <text>] [--body <text>] [--status <text>]
4223
+
4224
+ workser doc list [--work-item <id>]
4225
+ workser doc show <id> [--markdown]
4226
+ workser doc create <title> [--work-item <id>] [--markdown <text>]
4227
+ [--content-json <json>]
4228
+ workser doc update <id> [--title <text>] [--markdown <text>]
4229
+ \`\`\`
4230
+
4231
+ ## Read first \u2014 this is the part that matters
4232
+
4233
+ Before starting anything beyond a trivial edit:
4103
4234
 
4104
- \`workser agent run <role> "<task>" --json\` runs the role as an **isolated local
4105
- subagent** with its own context and returns \`{role, agent, output, exitCode}\`.
4235
+ \`\`\`
4236
+ workser board list --json # what's already tracked (don't re-file it)
4237
+ workser decision list --json # what was already decided (don't reverse it)
4238
+ \`\`\`
4239
+
4240
+ The project outlives your session. A decision recorded three weeks ago is the
4241
+ only thing standing between you and quietly undoing a choice someone made on
4242
+ purpose \u2014 \`workser decision show <id>\` gives you the context and consequences,
4243
+ not just the title. Reach for \`workser doc list\` / \`workser requirement list\`
4244
+ the same way when the task touches documented behaviour.
4245
+
4246
+ ## Keep the Board honest while you work
4247
+
4248
+ A Board still reading \`backlog\` after the feature shipped tells the user the
4249
+ opposite of the truth. Moving the card is part of finishing the work:
4106
4250
 
4107
- - Hand off focused subtasks \u2014 review this diff, design this screen \u2014 to keep your own
4108
- context lean and get a specialized second perspective.
4109
- - **A non-zero \`exitCode\` means the role's run failed.** Surface that; don't quietly
4110
- treat empty output as success.
4111
- - The subagent doesn't share your context. Put everything it needs in the task
4112
- string; it cannot see the conversation you're in.
4251
+ \`\`\`
4252
+ workser board move <id> in-progress # you picked it up
4253
+ workser board move <id> in-review # ready for the user to look at
4254
+ workser board close <id> # done and verified
4255
+ \`\`\`
4256
+
4257
+ \`--status\` is one of \`backlog | in-progress | in-review | done\` (default
4258
+ \`backlog\`). \`--priority\` is one of \`low | normal | high | urgent\` (default
4259
+ \`normal\`). \`--label\` repeats for more than one label:
4260
+
4261
+ \`\`\`
4262
+ workser board create "Fix the login bug" --status in-progress --priority high \\
4263
+ --label bug --label auth
4264
+ \`\`\`
4265
+
4266
+ \`board update\` replaces the labels you pass rather than merging them, and
4267
+ touches only the fields you name. There is no \`board delete\` \u2014 \`done\` is the
4268
+ terminal state for finished work, and removing a card the user filed is theirs
4269
+ to do in Orbit.
4270
+
4271
+ ## Decisions are append-only
4272
+
4273
+ \`decision create\` is for something with real tradeoffs worth a paper trail:
4274
+ \`--context\` is why it came up, \`--decision\` is what was decided,
4275
+ \`--consequences\` is the follow-on effects. There is deliberately **no
4276
+ \`decision update\`** \u2014 a decision record states what was decided at a point in
4277
+ time. When it stops being right, record a new decision that supersedes it and
4278
+ say so in its \`--context\`. Editing the history is how a decision log stops
4279
+ being worth reading.
4280
+
4281
+ Requirements are different: they legitimately move along, so they do have
4282
+ \`update\`.
4283
+
4284
+ \`\`\`
4285
+ workser requirement create "Support SSO" --body "Enterprise customers need SAML." \\
4286
+ --status proposed
4287
+ workser requirement update <id> --status done
4288
+ \`\`\`
4289
+
4290
+ ## Docs
4291
+
4292
+ \`--markdown\` is the normal way to write one. The body is stored both as the
4293
+ rich-text content the Docs panel renders and as a git-tracked markdown mirror
4294
+ at \`.workser/docs/<id>.md\` \u2014 \`workser doc show <id> --markdown\` reports that
4295
+ path so you can read the file with your normal tools.
4296
+
4297
+ Revise the page that exists rather than creating a second copy of it:
4298
+
4299
+ \`\`\`
4300
+ workser doc list --json # is there already a page for this?
4301
+ workser doc update <id> --markdown "$(cat updated.md)"
4302
+ \`\`\`
4303
+
4304
+ \`--work-item <id>\` links a document to a Board card (a card has at most one).
4305
+
4306
+ ## When to record, and when not to
4307
+
4308
+ Record what a future maintainer would need: follow-up work you found but didn't
4309
+ do, a choice between real alternatives, a behaviour worth writing down. Don't
4310
+ narrate every small step \u2014 and never treat filing a card as a substitute for
4311
+ the work. Creating a Board card that says "fix the bug" is not fixing the bug.
4312
+ `
4313
+ },
4314
+ {
4315
+ topic: "search",
4316
+ title: "Web search",
4317
+ summary: "Google-grounded web search, run server-side \u2014 the API key never reaches you.",
4318
+ commands: ["search"],
4319
+ source: "skills/workser/reference/search.md",
4320
+ body: `# Web search \u2014 ask the internet, not just the model
4321
+
4322
+ \`\`\`
4323
+ workser search "<query>" [-n, --max-results <n>] # Google-grounded web search
4324
+ \`\`\`
4325
+
4326
+ ## Why this exists
4327
+
4328
+ A local agent can drive a browser, but it can't "search the web" without a URL
4329
+ already in hand \u2014 \`browser\` automation needs somewhere to point. This runs a
4330
+ real, grounded Google search server-side (core-api holds the API key; it never
4331
+ reaches the daemon, the CLI, or you) and returns a synthesized answer plus the
4332
+ source links behind it.
4333
+
4334
+ ## Using it well
4335
+
4336
+ - Prefer this over guessing or asking the user when you need current facts,
4337
+ prices, docs for a library, or anything time-sensitive.
4338
+ - The \`answer\` is a synthesized summary \u2014 the \`results\` list is the sources it
4339
+ drew from. Cite or open one of those links if the user needs to verify the
4340
+ claim themselves.
4341
+ - Not project-scoped: it's a general research tool, not tied to a project's data.
4113
4342
  `
4114
4343
  },
4115
4344
  {
@@ -4308,7 +4537,8 @@ function buildContext(opts) {
4308
4537
  const link = readProjectLink(cwd);
4309
4538
  const projectId = opts.project || process.env.WORKSER_PROJECT_ID || link?.projectId || session.defaultProjectId;
4310
4539
  const runId = process.env.WORKSER_RUN_ID || void 0;
4311
- return { endpoint, socketPath, token, mode: mode2, cwd, projectId, runId };
4540
+ const conversationId = process.env.WORKSER_CONVERSATION_ID || void 0;
4541
+ return { endpoint, socketPath, token, mode: mode2, cwd, projectId, runId, conversationId };
4312
4542
  }
4313
4543
  function runTarget(ctx) {
4314
4544
  return ctx.runId || "current";
@@ -5432,6 +5662,7 @@ function maskToken(token) {
5432
5662
 
5433
5663
  // src/commands/agent.ts
5434
5664
  var import_picocolors14 = __toESM(require_picocolors(), 1);
5665
+ var SPAWNABLE_AGENTS = ["claude_code", "codex", "kimi", "opencode", "grok"];
5435
5666
  function registerAgent(program3) {
5436
5667
  const agent = program3.command("agent").description("Delegate focused subtasks to your configured agent roles (each runs isolated)");
5437
5668
  agent.command("list").description("List the main agent (+ backup) and the configured subagents").action(
@@ -5448,9 +5679,19 @@ function registerAgent(program3) {
5448
5679
  );
5449
5680
  }
5450
5681
  const roles = cfg?.roles ?? [];
5451
- if (!roles.length) return line(import_picocolors14.default.dim("No subagents configured. Add them in the Workser Orbit Agents screen."));
5452
- line(import_picocolors14.default.bold("subagents:"));
5453
- for (const r of roles) line(" " + formatRole(r));
5682
+ if (!roles.length) {
5683
+ line(import_picocolors14.default.dim("No subagents configured. Add them in the Workser Orbit Agents screen."));
5684
+ } else {
5685
+ line(import_picocolors14.default.bold("subagents:"));
5686
+ for (const r of roles) line(" " + formatRole(r));
5687
+ }
5688
+ const detected = cfg?.detected ?? [];
5689
+ const spawnable = detected.filter(
5690
+ (d) => d?.installed && d?.authed !== false
5691
+ );
5692
+ line(
5693
+ import_picocolors14.default.bold("spawnable (workser agent spawn <agent>):") + " " + (spawnable.length ? spawnable.map((d) => toContractId(d.id)).join(", ") : import_picocolors14.default.dim("none connected"))
5694
+ );
5454
5695
  });
5455
5696
  })
5456
5697
  );
@@ -5484,6 +5725,38 @@ function registerAgent(program3) {
5484
5725
  if (exitCode !== 0) process.exitCode = exitCode;
5485
5726
  })
5486
5727
  );
5728
+ agent.command("spawn <agent> <task...>").description(
5729
+ `Spin up a TEMPORARY teammate on any connected agent CLI (not a saved role) for a one-off task. <agent>: ${SPAWNABLE_AGENTS.join("|")}`
5730
+ ).option("--role <label>", "display label for this run (default: <agent>)").option(
5731
+ "--instructions <text>",
5732
+ "system prompt for this one run \u2014 this teammate's expertise/scope"
5733
+ ).option("--model <model>", "model override for the backing CLI").option("--effort <level>", "reasoning effort, where the backing CLI supports it").action(
5734
+ action(async ({ ctx, args, opts }) => {
5735
+ const spawnAgent = args[0];
5736
+ const task = args[1].join(" ");
5737
+ const res = await api(ctx, "/v1/agents/run", {
5738
+ body: {
5739
+ agent: spawnAgent,
5740
+ task,
5741
+ role: opts.role,
5742
+ instructions: opts.instructions,
5743
+ model: opts.model,
5744
+ effort: opts.effort
5745
+ }
5746
+ });
5747
+ const exitCode = res?.exitCode ?? 0;
5748
+ ok(res, () => {
5749
+ if (res?.output) line(res.output);
5750
+ if (exitCode !== 0) {
5751
+ warn(`Spawned ${spawnAgent} exited with code ${exitCode}.`);
5752
+ }
5753
+ });
5754
+ if (exitCode !== 0) process.exitCode = exitCode;
5755
+ })
5756
+ );
5757
+ }
5758
+ function toContractId(id) {
5759
+ return id === "claude" ? "claude_code" : id;
5487
5760
  }
5488
5761
  function formatRole(r) {
5489
5762
  const label = import_picocolors14.default.yellow(r.role);
@@ -6118,9 +6391,588 @@ function extract(response) {
6118
6391
  return String(response);
6119
6392
  }
6120
6393
 
6394
+ // src/commands/search.ts
6395
+ var import_picocolors23 = __toESM(require_picocolors(), 1);
6396
+ function registerSearch(program3) {
6397
+ program3.command("search <query>").description("Search the web (Google-grounded, server-side)").option("-n, --max-results <n>", "max results", "5").action(
6398
+ action(async ({ ctx, args, opts }) => {
6399
+ const res = await api(ctx, "/v1/search", {
6400
+ query: { q: args[0], maxResults: opts.maxResults }
6401
+ });
6402
+ ok(res, () => {
6403
+ if (res?.answer) {
6404
+ line(res.answer);
6405
+ line("");
6406
+ }
6407
+ const results = res?.results ?? [];
6408
+ if (!results.length) return line(import_picocolors23.default.dim("No results."));
6409
+ for (const r of results) {
6410
+ line(`${r.title || import_picocolors23.default.dim("(untitled)")} ${import_picocolors23.default.dim(r.url)}`);
6411
+ }
6412
+ });
6413
+ })
6414
+ );
6415
+ }
6416
+
6417
+ // src/commands/board.ts
6418
+ var import_picocolors24 = __toESM(require_picocolors(), 1);
6419
+
6420
+ // src/commands/record-step.ts
6421
+ async function recordEntityStep(ctx, opts) {
6422
+ if (!ctx.runId) return;
6423
+ await api(ctx, `/v1/runs/${ctx.runId}/steps`, {
6424
+ body: {
6425
+ title: opts.title,
6426
+ refType: opts.refType,
6427
+ refId: opts.refId,
6428
+ output: opts.output
6429
+ }
6430
+ }).catch(() => void 0);
6431
+ }
6432
+
6433
+ // src/commands/board.ts
6434
+ var STATUSES = ["backlog", "in-progress", "in-review", "done"];
6435
+ var PRIORITIES = ["low", "normal", "high", "urgent"];
6436
+ function registerBoard(program3) {
6437
+ const board = program3.command("board").description("Read and update the project's Board (work items)");
6438
+ board.command("list").description("List the Board's cards \u2014 run this before starting work").option("--status <value>", `only cards in this status (${STATUSES.join(" | ")})`).option("--label <value>", "only cards carrying this label").option("--limit <n>", "cap the number of cards returned").action(
6439
+ action(async ({ ctx, opts }) => {
6440
+ const projectId = requireProject(ctx);
6441
+ if (opts.status !== void 0) assertStatus(opts.status);
6442
+ const all = await api(ctx, `/v1/projects/${projectId}/work-items`) ?? [];
6443
+ let rows = all;
6444
+ if (opts.status) rows = rows.filter((r) => r.status === opts.status);
6445
+ if (opts.label) rows = rows.filter((r) => (r.labels ?? []).includes(opts.label));
6446
+ if (opts.limit) {
6447
+ const n = Number(opts.limit);
6448
+ if (!Number.isFinite(n) || n < 1) {
6449
+ throw new WorkserError(`--limit must be a positive number, got "${opts.limit}".`, {
6450
+ code: "bad_request"
6451
+ });
6452
+ }
6453
+ rows = rows.slice(0, n);
6454
+ }
6455
+ ok(rows, () => {
6456
+ if (!rows.length) {
6457
+ line(import_picocolors24.default.dim("No cards on the Board yet."));
6458
+ return;
6459
+ }
6460
+ for (const r of rows) line(formatRow(r));
6461
+ });
6462
+ })
6463
+ );
6464
+ board.command("show <id>").description("Show one card in full, e.g. `workser board show <id>`").action(
6465
+ action(async ({ ctx, args }) => {
6466
+ const projectId = requireProject(ctx);
6467
+ const row = await api(
6468
+ ctx,
6469
+ `/v1/projects/${projectId}/work-items/${args[0]}`
6470
+ );
6471
+ ok(row, () => {
6472
+ line(`${import_picocolors24.default.bold(row.title)} ${import_picocolors24.default.dim(row.id)}`);
6473
+ line(`${statusTag(row.status)} priority ${row.priority}`);
6474
+ if (row.ownerHuman) line(`owner: ${row.ownerHuman}`);
6475
+ if (row.labels?.length) line(`labels: ${row.labels.join(", ")}`);
6476
+ if (row.description) line(`
6477
+ ${row.description}`);
6478
+ });
6479
+ })
6480
+ );
6481
+ board.command("create <title>").description('Create a work item, e.g. `workser board create "Fix the login bug"`').option("--description <text>", "longer description").option("--status <value>", `${STATUSES.join(" | ")} (default: backlog)`).option("--priority <value>", `${PRIORITIES.join(" | ")} (default: normal)`).option(
6482
+ "--label <value>",
6483
+ "a label to attach (repeat for more)",
6484
+ collect2,
6485
+ []
6486
+ ).option("--owner <name>", "the human who owns this card").option("--milestone <id>", "milestone id to attach this card to").action(
6487
+ action(async ({ ctx, args, opts }) => {
6488
+ const projectId = requireProject(ctx);
6489
+ const title = String(args[0] ?? "").trim();
6490
+ if (!title) {
6491
+ throw new WorkserError("A work item needs a title.", { code: "bad_request" });
6492
+ }
6493
+ if (opts.status !== void 0) assertStatus(opts.status);
6494
+ if (opts.priority !== void 0) assertPriority(opts.priority);
6495
+ const labels = opts.label;
6496
+ const row = await api(ctx, `/v1/projects/${projectId}/work-items`, {
6497
+ body: {
6498
+ title,
6499
+ description: opts.description,
6500
+ status: opts.status,
6501
+ priority: opts.priority,
6502
+ labels: labels.length ? labels : void 0,
6503
+ ownerHuman: opts.owner,
6504
+ milestoneId: opts.milestone
6505
+ }
6506
+ });
6507
+ await recordEntityStep(ctx, {
6508
+ title: `Created work item: ${title}`,
6509
+ refType: "agent_created_work_item",
6510
+ refId: row?.id,
6511
+ output: { workItem: row }
6512
+ });
6513
+ ok(row, () => line(`Created work item ${import_picocolors24.default.bold(row?.id ?? "")} \u2014 ${title}`));
6514
+ })
6515
+ );
6516
+ board.command("update <id>").description("Change fields on a card \u2014 pass only what changes").option("--title <text>", "new title").option("--description <text>", "new description").option("--status <value>", STATUSES.join(" | ")).option("--priority <value>", PRIORITIES.join(" | ")).option(
6517
+ "--label <value>",
6518
+ "replace the card's labels with these (repeat for more)",
6519
+ collect2,
6520
+ []
6521
+ ).option("--owner <name>", "the human who owns this card").option("--milestone <id>", "milestone id to attach this card to").action(
6522
+ action(async ({ ctx, args, opts }) => {
6523
+ const projectId = requireProject(ctx);
6524
+ if (opts.status !== void 0) assertStatus(opts.status);
6525
+ if (opts.priority !== void 0) assertPriority(opts.priority);
6526
+ const labels = opts.label;
6527
+ const body = {
6528
+ title: opts.title,
6529
+ description: opts.description,
6530
+ status: opts.status,
6531
+ priority: opts.priority,
6532
+ // Commander's default `[]` is indistinguishable from "the user asked
6533
+ // for zero labels", so an empty array means "not specified" here.
6534
+ // Clearing labels is a rare enough intent to leave to the UI rather
6535
+ // than overload a flag that cannot express it unambiguously.
6536
+ labels: labels.length ? labels : void 0,
6537
+ ownerHuman: opts.owner,
6538
+ milestoneId: opts.milestone
6539
+ };
6540
+ if (Object.values(body).every((v) => v === void 0)) {
6541
+ throw new WorkserError(
6542
+ "Nothing to update \u2014 pass at least one of --title, --description, --status, --priority, --label, --owner, --milestone.",
6543
+ { code: "bad_request" }
6544
+ );
6545
+ }
6546
+ const row = await patchItem(ctx, projectId, String(args[0]), body);
6547
+ ok(row, () => line(`Updated ${import_picocolors24.default.bold(row.id)} \u2014 ${row.title} ${statusTag(row.status)}`));
6548
+ })
6549
+ );
6550
+ board.command("move <id> <status>").description(
6551
+ `Move a card to ${STATUSES.join(" | ")} \u2014 the one-liner for keeping the Board honest`
6552
+ ).action(
6553
+ action(async ({ ctx, args }) => {
6554
+ const projectId = requireProject(ctx);
6555
+ const status = String(args[1]);
6556
+ assertStatus(status);
6557
+ const row = await patchItem(ctx, projectId, String(args[0]), { status });
6558
+ ok(row, () => line(`Moved ${import_picocolors24.default.bold(row.title)} \u2192 ${statusTag(row.status)}`));
6559
+ })
6560
+ );
6561
+ board.command("close <id>").description("Shorthand for `board move <id> done`").action(
6562
+ action(async ({ ctx, args }) => {
6563
+ const projectId = requireProject(ctx);
6564
+ const row = await patchItem(ctx, projectId, String(args[0]), { status: "done" });
6565
+ ok(row, () => line(`Closed ${import_picocolors24.default.bold(row.title)} ${statusTag(row.status)}`));
6566
+ })
6567
+ );
6568
+ }
6569
+ async function patchItem(ctx, projectId, id, body) {
6570
+ const row = await api(ctx, `/v1/projects/${projectId}/work-items/${id}`, {
6571
+ method: "PATCH",
6572
+ body
6573
+ });
6574
+ if (!row) {
6575
+ throw new WorkserError(`No work item with id "${id}" on this project.`, {
6576
+ code: "bad_request"
6577
+ });
6578
+ }
6579
+ return row;
6580
+ }
6581
+ function assertStatus(value) {
6582
+ if (!STATUSES.includes(value)) {
6583
+ throw new WorkserError(
6584
+ `Unknown status "${value}". Use one of: ${STATUSES.join(", ")}.`,
6585
+ { code: "bad_request" }
6586
+ );
6587
+ }
6588
+ }
6589
+ function assertPriority(value) {
6590
+ if (!PRIORITIES.includes(value)) {
6591
+ throw new WorkserError(
6592
+ `Unknown priority "${value}". Use one of: ${PRIORITIES.join(", ")}.`,
6593
+ { code: "bad_request" }
6594
+ );
6595
+ }
6596
+ }
6597
+ function formatRow(r) {
6598
+ const labels = r.labels?.length ? import_picocolors24.default.dim(` [${r.labels.join(", ")}]`) : "";
6599
+ const owner = r.ownerHuman ? import_picocolors24.default.dim(` @${r.ownerHuman}`) : "";
6600
+ return `${import_picocolors24.default.dim(r.id)} ${statusTag(r.status)} ${r.title}${labels}${owner}`;
6601
+ }
6602
+ function statusTag(status) {
6603
+ const label = status.padEnd(11);
6604
+ if (status === "done") return import_picocolors24.default.green(label);
6605
+ if (status === "in-progress") return import_picocolors24.default.yellow(label);
6606
+ if (status === "in-review") return import_picocolors24.default.cyan(label);
6607
+ return import_picocolors24.default.dim(label);
6608
+ }
6609
+ function collect2(value, previous) {
6610
+ return [...previous, value];
6611
+ }
6612
+
6613
+ // src/commands/decision.ts
6614
+ var import_picocolors25 = __toESM(require_picocolors(), 1);
6615
+ function registerDecision(program3) {
6616
+ const decision = program3.command("decision").description("Read and record the project's architecture decisions");
6617
+ 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(
6618
+ action(async ({ ctx, opts }) => {
6619
+ const projectId = requireProject(ctx);
6620
+ let rows = await api(
6621
+ ctx,
6622
+ `/v1/projects/${projectId}/architecture-decisions`
6623
+ ) ?? [];
6624
+ rows = applyLimit(rows, opts.limit);
6625
+ ok(rows, () => {
6626
+ if (!rows.length) {
6627
+ line(import_picocolors25.default.dim("No decisions recorded yet."));
6628
+ return;
6629
+ }
6630
+ for (const r of rows) {
6631
+ line(`${import_picocolors25.default.dim(r.id)} ${import_picocolors25.default.dim(shortDate(r.createdAt))} ${r.title}`);
6632
+ line(` ${truncate(r.decision, 100)}`);
6633
+ }
6634
+ });
6635
+ })
6636
+ );
6637
+ decision.command("show <id>").description("The full record \u2014 context, decision, and consequences").action(
6638
+ action(async ({ ctx, args }) => {
6639
+ const projectId = requireProject(ctx);
6640
+ const row = await api(
6641
+ ctx,
6642
+ `/v1/projects/${projectId}/architecture-decisions/${args[0]}`
6643
+ );
6644
+ ok(row, () => {
6645
+ line(`${import_picocolors25.default.bold(row.title)} ${import_picocolors25.default.dim(row.id)}`);
6646
+ line(import_picocolors25.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
6647
+ line(`
6648
+ ${import_picocolors25.default.bold("Context")}
6649
+ ${row.context}`);
6650
+ line(`
6651
+ ${import_picocolors25.default.bold("Decision")}
6652
+ ${row.decision}`);
6653
+ if (row.consequences) line(`
6654
+ ${import_picocolors25.default.bold("Consequences")}
6655
+ ${row.consequences}`);
6656
+ });
6657
+ })
6658
+ );
6659
+ decision.command("create <title>").description(
6660
+ 'workser decision create "Use Postgres" --context ... --decision ... [--consequences ...]'
6661
+ ).requiredOption("--context <text>", "why this decision was needed").requiredOption("--decision <text>", "what was decided").option("--consequences <text>", "tradeoffs / follow-on effects").action(
6662
+ action(async ({ ctx, args, opts }) => {
6663
+ const projectId = requireProject(ctx);
6664
+ const title = String(args[0] ?? "").trim();
6665
+ if (!title) {
6666
+ throw new WorkserError("A decision needs a title.", { code: "bad_request" });
6667
+ }
6668
+ const row = await api(ctx, `/v1/projects/${projectId}/architecture-decisions`, {
6669
+ body: {
6670
+ title,
6671
+ context: opts.context,
6672
+ decision: opts.decision,
6673
+ consequences: opts.consequences,
6674
+ conversationId: ctx.conversationId
6675
+ }
6676
+ });
6677
+ await recordEntityStep(ctx, {
6678
+ title: `Recorded decision: ${title}`,
6679
+ refType: "agent_created_decision",
6680
+ refId: row?.id,
6681
+ output: { decision: row }
6682
+ });
6683
+ ok(row, () => line(`Recorded decision ${import_picocolors25.default.bold(row?.id ?? "")} \u2014 ${title}`));
6684
+ })
6685
+ );
6686
+ const requirement = program3.command("requirement").description("Read and record the project's requirements");
6687
+ requirement.command("list").description("Every requirement on record \u2014 read this before deciding what to build").option("--status <value>", "only requirements in this status, e.g. accepted").option("--limit <n>", "cap the number returned (newest first)").action(
6688
+ action(async ({ ctx, opts }) => {
6689
+ const projectId = requireProject(ctx);
6690
+ let rows = await api(ctx, `/v1/projects/${projectId}/requirements`) ?? [];
6691
+ if (opts.status) rows = rows.filter((r) => r.status === opts.status);
6692
+ rows = applyLimit(rows, opts.limit);
6693
+ ok(rows, () => {
6694
+ if (!rows.length) {
6695
+ line(import_picocolors25.default.dim("No requirements recorded yet."));
6696
+ return;
6697
+ }
6698
+ for (const r of rows) {
6699
+ line(`${import_picocolors25.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
6700
+ }
6701
+ });
6702
+ })
6703
+ );
6704
+ requirement.command("show <id>").description("The full requirement text").action(
6705
+ action(async ({ ctx, args }) => {
6706
+ const projectId = requireProject(ctx);
6707
+ const row = await api(
6708
+ ctx,
6709
+ `/v1/projects/${projectId}/requirements/${args[0]}`
6710
+ );
6711
+ ok(row, () => {
6712
+ line(`${import_picocolors25.default.bold(row.title)} ${import_picocolors25.default.dim(row.id)}`);
6713
+ line(import_picocolors25.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
6714
+ line(`
6715
+ ${row.body}`);
6716
+ });
6717
+ })
6718
+ );
6719
+ requirement.command("create <title>").description('workser requirement create "Support SSO" --body ... [--status ...]').requiredOption("--body <text>", "the requirement's text").option("--status <text>", "e.g. proposed | accepted | done").action(
6720
+ action(async ({ ctx, args, opts }) => {
6721
+ const projectId = requireProject(ctx);
6722
+ const title = String(args[0] ?? "").trim();
6723
+ if (!title) {
6724
+ throw new WorkserError("A requirement needs a title.", { code: "bad_request" });
6725
+ }
6726
+ const row = await api(ctx, `/v1/projects/${projectId}/requirements`, {
6727
+ body: {
6728
+ title,
6729
+ body: opts.body,
6730
+ status: opts.status
6731
+ }
6732
+ });
6733
+ await recordEntityStep(ctx, {
6734
+ title: `Recorded requirement: ${title}`,
6735
+ refType: "agent_created_requirement",
6736
+ refId: row?.id,
6737
+ output: { requirement: row }
6738
+ });
6739
+ ok(row, () => line(`Recorded requirement ${import_picocolors25.default.bold(row?.id ?? "")} \u2014 ${title}`));
6740
+ })
6741
+ );
6742
+ requirement.command("update <id>").description(
6743
+ "Move a requirement along, e.g. `workser requirement update <id> --status done`"
6744
+ ).option("--title <text>", "new title").option("--body <text>", "new requirement text").option("--status <text>", "e.g. proposed | accepted | done").action(
6745
+ action(async ({ ctx, args, opts }) => {
6746
+ const projectId = requireProject(ctx);
6747
+ const body = {
6748
+ title: opts.title,
6749
+ body: opts.body,
6750
+ status: opts.status
6751
+ };
6752
+ if (Object.values(body).every((v) => v === void 0)) {
6753
+ throw new WorkserError(
6754
+ "Nothing to update \u2014 pass at least one of --title, --body, --status.",
6755
+ { code: "bad_request" }
6756
+ );
6757
+ }
6758
+ const row = await api(
6759
+ ctx,
6760
+ `/v1/projects/${projectId}/requirements/${args[0]}`,
6761
+ { method: "PATCH", body }
6762
+ );
6763
+ if (!row) {
6764
+ throw new WorkserError(`No requirement with id "${args[0]}" on this project.`, {
6765
+ code: "bad_request"
6766
+ });
6767
+ }
6768
+ ok(row, () => line(`Updated requirement ${import_picocolors25.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
6769
+ })
6770
+ );
6771
+ }
6772
+ function applyLimit(rows, limit) {
6773
+ if (limit === void 0) return rows;
6774
+ const n = Number(limit);
6775
+ if (!Number.isFinite(n) || n < 1) {
6776
+ throw new WorkserError(`--limit must be a positive number, got "${limit}".`, {
6777
+ code: "bad_request"
6778
+ });
6779
+ }
6780
+ return rows.slice(0, n);
6781
+ }
6782
+ function truncate(text, max) {
6783
+ const flat = text.replace(/\s+/g, " ").trim();
6784
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
6785
+ }
6786
+ function shortDate(iso) {
6787
+ return iso ? iso.slice(0, 10) : "";
6788
+ }
6789
+
6790
+ // src/commands/doc.ts
6791
+ var import_picocolors26 = __toESM(require_picocolors(), 1);
6792
+ function registerDoc(program3) {
6793
+ const doc = program3.command("doc").description("Read and write project documents");
6794
+ doc.command("list").description("List the project's documents \u2014 check here before writing a new one").option("--work-item <id>", "the document linked to this card, if there is one").action(
6795
+ action(async ({ ctx, opts }) => {
6796
+ const projectId = requireProject(ctx);
6797
+ const rows = await api(ctx, `/v1/projects/${projectId}/documents`, {
6798
+ query: { workItemId: opts.workItem }
6799
+ }) ?? [];
6800
+ ok(rows, () => {
6801
+ if (!rows.length) {
6802
+ line(import_picocolors26.default.dim("No documents yet."));
6803
+ return;
6804
+ }
6805
+ for (const r of rows) {
6806
+ const link = r.workItemId ? import_picocolors26.default.dim(` \u21B3 ${r.workItemId}`) : "";
6807
+ const file = r.filePath ? import_picocolors26.default.dim(` ${r.filePath}`) : "";
6808
+ line(`${import_picocolors26.default.dim(r.id)} ${r.title}${link}${file}`);
6809
+ }
6810
+ });
6811
+ })
6812
+ );
6813
+ doc.command("show <id>").description("Show a document \u2014 `--markdown` prints the file mirror's path to read").option("--markdown", "report the repo-relative markdown mirror instead of the block content").action(
6814
+ action(async ({ ctx, args, opts }) => {
6815
+ const projectId = requireProject(ctx);
6816
+ const row = await api(
6817
+ ctx,
6818
+ `/v1/projects/${projectId}/documents/${args[0]}`
6819
+ );
6820
+ if (opts.markdown) {
6821
+ ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
6822
+ line(`${import_picocolors26.default.bold(row.title)} ${import_picocolors26.default.dim(row.id)}`);
6823
+ line(
6824
+ row.filePath ? `Read it at ${import_picocolors26.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors26.default.dim("This document has no markdown mirror on disk yet.")
6825
+ );
6826
+ });
6827
+ return;
6828
+ }
6829
+ ok(row, () => {
6830
+ line(`${import_picocolors26.default.bold(row.title)} ${import_picocolors26.default.dim(row.id)}`);
6831
+ if (row.workItemId) line(import_picocolors26.default.dim(`linked to work item ${row.workItemId}`));
6832
+ if (row.filePath) line(import_picocolors26.default.dim(`markdown mirror: ${row.filePath}`));
6833
+ line("");
6834
+ line(row.contentJson);
6835
+ });
6836
+ })
6837
+ );
6838
+ doc.command("create <title>").description(
6839
+ 'workser doc create "Onboarding" [--work-item <id>] [--markdown ...] [--content-json ...]'
6840
+ ).option("--work-item <id>", "link this document to a work item").option("--markdown <text>", "document body as markdown").option("--content-json <json>", "document body as rich-text content JSON").action(
6841
+ action(async ({ ctx, args, opts }) => {
6842
+ const projectId = requireProject(ctx);
6843
+ const title = String(args[0] ?? "").trim();
6844
+ if (!title) {
6845
+ throw new WorkserError("A document needs a title.", { code: "bad_request" });
6846
+ }
6847
+ const row = await api(ctx, `/v1/projects/${projectId}/documents`, {
6848
+ body: {
6849
+ title,
6850
+ workItemId: opts.workItem,
6851
+ markdown: opts.markdown,
6852
+ contentJson: opts.contentJson
6853
+ }
6854
+ });
6855
+ await recordEntityStep(ctx, {
6856
+ title: `Created document: ${title}`,
6857
+ refType: "agent_created_document",
6858
+ refId: row?.id,
6859
+ output: { document: row }
6860
+ });
6861
+ ok(row, () => line(`Created document ${import_picocolors26.default.bold(row?.id ?? "")} \u2014 ${title}`));
6862
+ })
6863
+ );
6864
+ doc.command("update <id>").description("Revise an existing document rather than creating a second copy of it").option("--title <text>", "new title").option("--markdown <text>", "replace the body with this markdown").option("--content-json <json>", "replace the body with this rich-text content JSON").action(
6865
+ action(async ({ ctx, args, opts }) => {
6866
+ const projectId = requireProject(ctx);
6867
+ const body = {
6868
+ title: opts.title,
6869
+ markdown: opts.markdown,
6870
+ contentJson: opts.contentJson
6871
+ };
6872
+ if (Object.values(body).every((v) => v === void 0)) {
6873
+ throw new WorkserError(
6874
+ "Nothing to update \u2014 pass at least one of --title, --markdown, --content-json.",
6875
+ { code: "bad_request" }
6876
+ );
6877
+ }
6878
+ const row = await api(
6879
+ ctx,
6880
+ `/v1/projects/${projectId}/documents/${args[0]}`,
6881
+ { method: "PATCH", body }
6882
+ );
6883
+ if (!row) {
6884
+ throw new WorkserError(`No document with id "${args[0]}" on this project.`, {
6885
+ code: "bad_request"
6886
+ });
6887
+ }
6888
+ ok(row, () => line(`Updated document ${import_picocolors26.default.bold(row.id)} \u2014 ${row.title}`));
6889
+ })
6890
+ );
6891
+ }
6892
+
6893
+ // src/commands/design.ts
6894
+ var import_picocolors27 = __toESM(require_picocolors(), 1);
6895
+ function registerDesign(program3) {
6896
+ const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
6897
+ 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(
6898
+ action(async ({ ctx, opts }) => {
6899
+ const projectId = requireProject(ctx);
6900
+ const res = await api(
6901
+ ctx,
6902
+ `/v1/projects/${projectId}/design/files`
6903
+ );
6904
+ const files = res?.files ?? [];
6905
+ if (opts.raw) {
6906
+ ok(files, () => {
6907
+ if (!files.length) {
6908
+ line(import_picocolors27.default.dim("No brand set for this project."));
6909
+ return;
6910
+ }
6911
+ for (const f of files) {
6912
+ line(import_picocolors27.default.bold(f.path));
6913
+ line(f.contents);
6914
+ line("");
6915
+ }
6916
+ });
6917
+ return;
6918
+ }
6919
+ const tokens = parseTokens(files);
6920
+ const summary = tokens ? {
6921
+ hasBrand: true,
6922
+ colors: tokens.color,
6923
+ fonts: tokens.font,
6924
+ brand: tokens.brand,
6925
+ files: files.map((f) => f.path)
6926
+ } : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
6927
+ ok(summary, () => {
6928
+ if (!tokens) {
6929
+ line(import_picocolors27.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
6930
+ return;
6931
+ }
6932
+ for (const [name, value] of Object.entries(tokens.brand)) {
6933
+ line(`${import_picocolors27.default.dim(name.padEnd(12))} ${value}`);
6934
+ }
6935
+ for (const [name, value] of Object.entries(tokens.color)) {
6936
+ line(`${import_picocolors27.default.dim(`color.${name}`.padEnd(12))} ${value}`);
6937
+ }
6938
+ for (const [name, value] of Object.entries(tokens.font)) {
6939
+ line(`${import_picocolors27.default.dim(`font.${name}`.padEnd(12))} ${value}`);
6940
+ }
6941
+ line("");
6942
+ line(
6943
+ import_picocolors27.default.dim(
6944
+ `Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
6945
+ )
6946
+ );
6947
+ });
6948
+ })
6949
+ );
6950
+ }
6951
+ function parseTokens(files) {
6952
+ const doc = files.find((f) => f.path.endsWith("tokens.json"));
6953
+ if (!doc) return null;
6954
+ try {
6955
+ const parsed = JSON.parse(doc.contents);
6956
+ return {
6957
+ color: unwrap(parsed.color),
6958
+ font: unwrap(parsed.font),
6959
+ brand: Object.fromEntries(
6960
+ Object.entries(parsed.brand ?? {}).map(([k, v]) => [k, String(v)])
6961
+ )
6962
+ };
6963
+ } catch {
6964
+ return null;
6965
+ }
6966
+ }
6967
+ function unwrap(group) {
6968
+ return Object.fromEntries(
6969
+ Object.entries(group ?? {}).map(([k, v]) => [k, v?.$value ?? ""])
6970
+ );
6971
+ }
6972
+
6121
6973
  // src/index.ts
6122
6974
  var pkg = {
6123
- version: true ? "0.2.2" : "0.0.0-dev"
6975
+ version: true ? "0.2.4" : "0.0.0-dev"
6124
6976
  };
6125
6977
  var program2 = new Command();
6126
6978
  program2.name("workser").description(
@@ -6161,4 +7013,9 @@ registerBusiness(program2);
6161
7013
  registerArtifact(program2);
6162
7014
  registerImage(program2);
6163
7015
  registerAsk(program2);
7016
+ registerSearch(program2);
7017
+ registerBoard(program2);
7018
+ registerDecision(program2);
7019
+ registerDoc(program2);
7020
+ registerDesign(program2);
6164
7021
  program2.parseAsync(process.argv).catch((e) => fail(e));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workser/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",
@@ -21,6 +21,8 @@ command. Don't print them all; you are paying for every line you load.
21
21
 
22
22
  | You need to… | Commands | Run |
23
23
  | --- | --- | --- |
24
+ | See what's tracked, decided, or written down for this project | `board …`, `decision …`, `requirement …`, `doc …` | `workser help sdlc-entities` |
25
+ | Follow the project's brand — colours, fonts, logo | `design …` | `workser help brand` |
24
26
  | Provision or query Postgres; list end users | `db …`, `auth …` | `workser help database` |
25
27
  | Deploy, set env vars, read logs, check a domain | `deploy`, `env …`, `logs`, `versions`, `domain`, `open` | `workser help deploy` |
26
28
  | Put files in the project's bucket | `storage …` | `workser help storage` |
@@ -73,6 +75,11 @@ Owner-only, for reference: `project create` · `project use` · `env rm` · `dom
73
75
  `{"ok":true,"data":...}` or `{"ok":false,"error":{"code","message",...}}`. Parse it.
74
76
  2. **Orient first.** Run `workser status --json` to see the connection, the pinned
75
77
  project, and the latest deploy before acting. You don't pick or switch projects.
78
+ For anything beyond a trivial edit, also read what the project already knows:
79
+ `workser board list --json` (what's tracked), `workser decision list --json`
80
+ (what was already decided, so you don't quietly reverse it), and
81
+ `workser design show --json` before writing UI. This project outlives your
82
+ session; that context is how you don't start from zero.
76
83
  3. **Stay in your lane.** `error.code = "owner_only"` (exit 6) means the action is
77
84
  reserved for the owner in Orbit. Don't retry or look for a workaround — tell the
78
85
  user, then continue. Provisioning the *pinned project's own* db / bucket / auth is
@@ -93,10 +100,15 @@ Owner-only, for reference: `project create` · `project use` · `env rm` · `dom
93
100
 
94
101
  ```bash
95
102
  workser status --json # 1. orient (project is already pinned)
96
- workser db create --json # 2. provision infra the app needs (idempotent)
97
- workser env set STRIPE_KEY=sk_live_… --json # 3. configure it
103
+ workser board list --json # 2. what's already tracked
104
+ workser decision list --json # …and already decided
105
+ workser board move <id> in-progress --json # 3. claim the card you're doing
106
+ workser db create --json # 4. provision infra the app needs (idempotent)
107
+ workser env set STRIPE_KEY=sk_live_… --json # 5. configure it
98
108
  # … you write the app code with your normal tools …
99
- workser deploy --prod --watch --json # 4. ship; returns the live URL
109
+ workser verify --json # 6. green build is the bar for "done"
110
+ workser deploy --prod --watch --json # 7. ship; returns the live URL
111
+ workser board close <id> --json # 8. the Board now matches reality
100
112
  ```
101
113
 
102
114
  Provisioning the pinned project's own database / bucket / auth is yours to do
@@ -0,0 +1,60 @@
1
+ ---
2
+ topic: brand
3
+ title: The project's brand
4
+ summary: Read the owner's colours, fonts and logo before writing UI, and wire the generated design tokens into the app's theme.
5
+ commands: [design]
6
+ ---
7
+
8
+ # The project's brand
9
+
10
+ ```
11
+ workser design show [--raw]
12
+ ```
13
+
14
+ The owner sets their brand once in Workser (logo, colours, fonts). Everything
15
+ you build should follow it. Read it **before writing any UI**:
16
+
17
+ ```bash
18
+ workser design show --json
19
+ ```
20
+
21
+ ```json
22
+ {"ok":true,"data":{"hasBrand":true,
23
+ "colors":{"primary":"#1f7a4d","accent":"#f5a623"},
24
+ "fonts":{"heading":"Inter","body":"Inter"},
25
+ "brand":{"name":"Green Grocer"},
26
+ "files":["design/tokens.json","design/tokens.css"]}}
27
+ ```
28
+
29
+ This asks the server, which derives the answer from the brand record itself — so
30
+ it is correct even if nothing has been written into the working tree yet. That's
31
+ the reason to use it instead of looking for a file and guessing when it's absent.
32
+
33
+ `"hasBrand": false` is an ordinary state — most projects have no brand set.
34
+ Choose sensible styling yourself; **don't** stop and ask the user to define a
35
+ brand first.
36
+
37
+ ## Using it in the code
38
+
39
+ The same values are generated into the working tree as two files:
40
+
41
+ - `design/tokens.json` — the tokens in DTCG form
42
+ - `design/tokens.css` — plain CSS custom properties (`--ws-color-*`, `--ws-font-*`)
43
+
44
+ Wire `design/tokens.css` into the app's theme the way this app's setup expects —
45
+ it assumes no particular Tailwind version, so map it rather than pasting it.
46
+
47
+ **Both files are generated and overwritten wholesale.** Never hand-edit them;
48
+ your edit disappears on the next sync. `workser design show --raw` prints them
49
+ verbatim if you need to see exactly what's in them.
50
+
51
+ ## Changing the brand is the owner's job
52
+
53
+ There is no `workser design set` — the brand is one record the owner controls in
54
+ the Workser app, and it drives everything generated from it. If the user asks you
55
+ to change their brand colour, tell them where it lives (Design → Brand in Orbit)
56
+ rather than writing the colour into the app by hand, which would drift from every
57
+ other surface the brand feeds.
58
+
59
+ For generating artwork *in* the brand's palette, see `workser help images` —
60
+ put the colours from `design show` into the prompt.
@@ -11,23 +11,37 @@ The user can configure **roles** — named specialists each backed by a local CL
11
11
  (e.g. `qa` → codex, `designer` → claude_code).
12
12
 
13
13
  ```
14
- workser agent list # main agent + configured roles (+ which are runnable)
15
- workser agent run <role> "<task>" # delegate a focused subtask (runs isolated)
14
+ workser agent list # main agent + configured roles + which agents are connected
15
+ workser agent run <role> "<task>" # delegate to a CONFIGURED role (runs isolated)
16
+ workser agent spawn <agent> "<task>" [--role <label>] [--instructions <text>] [--model <model>]
17
+ # spin up a TEMPORARY teammate on any connected agent CLI
16
18
  workser agent main # show the configured main agent
17
19
  ```
18
20
 
19
21
  ## How to use it
20
22
 
21
- Run `workser agent list --json` first — it tells you which roles exist **and** which
22
- are actually runnable on this machine. Delegating to a role that isn't installed just
23
- fails.
23
+ Run `workser agent list --json` first — it tells you which roles exist, which are
24
+ actually runnable on this machine, and which agent CLIs are connected at all
25
+ (`spawnable`). Delegating to a role that isn't installed, or spawning an agent that
26
+ isn't connected, just fails.
24
27
 
25
- `workser agent run <role> "<task>" --json` runs the role as an **isolated local
26
- subagent** with its own context and returns `{role, agent, output, exitCode}`.
28
+ `workser agent run <role> "<task>" --json` runs a **configured** role as an isolated
29
+ local subagent and returns `{role, agent, output, exitCode}`.
27
30
 
28
- - Hand off focused subtasks review this diff, design this screen — to keep your own
29
- context lean and get a specialized second perspective.
30
- - **A non-zero `exitCode` means the role's run failed.** Surface that; don't quietly
31
- treat empty output as success.
32
- - The subagent doesn't share your context. Put everything it needs in the task
33
- string; it cannot see the conversation you're in.
31
+ `workser agent spawn <agent> "<task>" --json` does the same thing without a
32
+ pre-configured role `<agent>` is one of `claude_code|codex|kimi|opencode|grok`, any
33
+ of which can be connected on this machine. Use it to fan work out in parallel for
34
+ one-off work with no matching role: e.g. spawn `codex` to research a topic while you
35
+ keep working, or run a second `claude_code` instance on a different part of the same
36
+ task. `--instructions` sets that teammate's system prompt for this one run — give it
37
+ real scope, not just a repeat of the task.
38
+
39
+ - Hand off focused subtasks — review this diff, design this screen, research this API —
40
+ to keep your own context lean and get a specialized second perspective.
41
+ - **A non-zero `exitCode` means the run failed.** Surface that; don't quietly treat
42
+ empty output as success.
43
+ - The subagent doesn't share your context, configured role or spawned. Put everything
44
+ it needs in the task string (and `--instructions` for a spawn); it cannot see the
45
+ conversation you're in.
46
+ - Never give a subagent — configured or spawned — a task that tells it to delegate or
47
+ spawn further. It has no supervision loop to stop a runaway chain.
@@ -0,0 +1,123 @@
1
+ ---
2
+ topic: sdlc-entities
3
+ title: Board cards, decisions, requirements, and docs
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.
5
+ commands: [board, decision, requirement, doc]
6
+ ---
7
+
8
+ # Board cards, decisions, requirements, and docs
9
+
10
+ These are the project's memory across sessions. They write to the **same tables**
11
+ the Orbit desktop's Board, Project Memory, and Docs panels use, so anything here
12
+ appears there too — and (when this CLI runs inside an Orbit-spawned agent run)
13
+ as an inline card in the conversation you're working in.
14
+
15
+ ```
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
+ workser decision list [--limit <n>]
27
+ workser decision show <id>
28
+ workser decision create <title> --context <text> --decision <text>
29
+ [--consequences <text>]
30
+
31
+ workser requirement list [--status <value>] [--limit <n>]
32
+ workser requirement show <id>
33
+ workser requirement create <title> --body <text> [--status <text>]
34
+ workser requirement update <id> [--title <text>] [--body <text>] [--status <text>]
35
+
36
+ workser doc list [--work-item <id>]
37
+ workser doc show <id> [--markdown]
38
+ workser doc create <title> [--work-item <id>] [--markdown <text>]
39
+ [--content-json <json>]
40
+ workser doc update <id> [--title <text>] [--markdown <text>]
41
+ ```
42
+
43
+ ## Read first — this is the part that matters
44
+
45
+ Before starting anything beyond a trivial edit:
46
+
47
+ ```
48
+ workser board list --json # what's already tracked (don't re-file it)
49
+ workser decision list --json # what was already decided (don't reverse it)
50
+ ```
51
+
52
+ The project outlives your session. A decision recorded three weeks ago is the
53
+ only thing standing between you and quietly undoing a choice someone made on
54
+ purpose — `workser decision show <id>` gives you the context and consequences,
55
+ not just the title. Reach for `workser doc list` / `workser requirement list`
56
+ the same way when the task touches documented behaviour.
57
+
58
+ ## Keep the Board honest while you work
59
+
60
+ A Board still reading `backlog` after the feature shipped tells the user the
61
+ opposite of the truth. Moving the card is part of finishing the work:
62
+
63
+ ```
64
+ workser board move <id> in-progress # you picked it up
65
+ workser board move <id> in-review # ready for the user to look at
66
+ workser board close <id> # done and verified
67
+ ```
68
+
69
+ `--status` is one of `backlog | in-progress | in-review | done` (default
70
+ `backlog`). `--priority` is one of `low | normal | high | urgent` (default
71
+ `normal`). `--label` repeats for more than one label:
72
+
73
+ ```
74
+ workser board create "Fix the login bug" --status in-progress --priority high \
75
+ --label bug --label auth
76
+ ```
77
+
78
+ `board update` replaces the labels you pass rather than merging them, and
79
+ touches only the fields you name. There is no `board delete` — `done` is the
80
+ terminal state for finished work, and removing a card the user filed is theirs
81
+ to do in Orbit.
82
+
83
+ ## Decisions are append-only
84
+
85
+ `decision create` is for something with real tradeoffs worth a paper trail:
86
+ `--context` is why it came up, `--decision` is what was decided,
87
+ `--consequences` is the follow-on effects. There is deliberately **no
88
+ `decision update`** — a decision record states what was decided at a point in
89
+ time. When it stops being right, record a new decision that supersedes it and
90
+ say so in its `--context`. Editing the history is how a decision log stops
91
+ being worth reading.
92
+
93
+ Requirements are different: they legitimately move along, so they do have
94
+ `update`.
95
+
96
+ ```
97
+ workser requirement create "Support SSO" --body "Enterprise customers need SAML." \
98
+ --status proposed
99
+ workser requirement update <id> --status done
100
+ ```
101
+
102
+ ## Docs
103
+
104
+ `--markdown` is the normal way to write one. The body is stored both as the
105
+ rich-text content the Docs panel renders and as a git-tracked markdown mirror
106
+ at `.workser/docs/<id>.md` — `workser doc show <id> --markdown` reports that
107
+ path so you can read the file with your normal tools.
108
+
109
+ Revise the page that exists rather than creating a second copy of it:
110
+
111
+ ```
112
+ workser doc list --json # is there already a page for this?
113
+ workser doc update <id> --markdown "$(cat updated.md)"
114
+ ```
115
+
116
+ `--work-item <id>` links a document to a Board card (a card has at most one).
117
+
118
+ ## When to record, and when not to
119
+
120
+ Record what a future maintainer would need: follow-up work you found but didn't
121
+ do, a choice between real alternatives, a behaviour worth writing down. Don't
122
+ narrate every small step — and never treat filing a card as a substitute for
123
+ the work. Creating a Board card that says "fix the bug" is not fixing the bug.
@@ -0,0 +1,29 @@
1
+ ---
2
+ topic: search
3
+ title: Web search
4
+ summary: Google-grounded web search, run server-side — the API key never reaches you.
5
+ commands: [search]
6
+ ---
7
+
8
+ # Web search — ask the internet, not just the model
9
+
10
+ ```
11
+ workser search "<query>" [-n, --max-results <n>] # Google-grounded web search
12
+ ```
13
+
14
+ ## Why this exists
15
+
16
+ A local agent can drive a browser, but it can't "search the web" without a URL
17
+ already in hand — `browser` automation needs somewhere to point. This runs a
18
+ real, grounded Google search server-side (core-api holds the API key; it never
19
+ reaches the daemon, the CLI, or you) and returns a synthesized answer plus the
20
+ source links behind it.
21
+
22
+ ## Using it well
23
+
24
+ - Prefer this over guessing or asking the user when you need current facts,
25
+ prices, docs for a library, or anything time-sensitive.
26
+ - The `answer` is a synthesized summary — the `results` list is the sources it
27
+ drew from. Cite or open one of those links if the user needs to verify the
28
+ claim themselves.
29
+ - Not project-scoped: it's a general research tool, not tied to a project's data.