@workser/cli 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4404,6 +4404,89 @@ workser storage get <key> [dest] # download an object (or print its URL)
4404
4404
  infrastructure on the project's own Neon branch, available only on dedicated tenancy
4405
4405
  in a supported region. They are different stores \u2014 a file put in one is not visible
4406
4406
  in the other. See \`reference/neon-backend.md\`.
4407
+ `
4408
+ },
4409
+ {
4410
+ topic: "tasks",
4411
+ title: "Project tasks & subtasks",
4412
+ summary: "The ticket you are working inside: read it, break it into steps, and ask before starting.",
4413
+ commands: ["task"],
4414
+ source: "skills/workser/reference/tasks.md",
4415
+ body: `# Project tasks & subtasks
4416
+
4417
+ A **project task** is a ticket the owner filed. You are usually running inside
4418
+ one \u2014 Orbit sets \`WORKSER_PROJECT_TASK_ID\` on your process, so every command
4419
+ below defaults to it and you rarely pass an id at all.
4420
+
4421
+ \`\`\`
4422
+ workser task list [--status <value>] [--label <value>] [--limit <n>]
4423
+ workser task show [id] # the task you are in, with its steps
4424
+
4425
+ workser task subtask add <title> [--role <value>] [--kind <value>]
4426
+ [--note <text>] [--app <id...>]
4427
+ [--infra <ref...>] [--scope <path...>]
4428
+ [--depends-on <key...>]
4429
+ workser task subtask list [taskId]
4430
+ workser task subtask update <id> [--title|--note|--role|--kind|--scope]
4431
+ workser task subtask remove <id>
4432
+
4433
+ workser task can-start [id] # may work begin? refuses until approved
4434
+ workser task approval request # tell the owner the plan is ready
4435
+ workser task move <id> <status>
4436
+ workser task done [id] --summary <text>
4437
+ \`\`\`
4438
+
4439
+ ## This is not \`workser board\`
4440
+
4441
+ \`board\` is the Orbit Board \u2014 a human's list of work items. \`task\` is the AI Tech
4442
+ Team's own table. Filing your plan on the Board puts it somewhere the owner's
4443
+ task page never reads: they see "created work item" and an empty plan. Use
4444
+ \`task subtask add\`.
4445
+
4446
+ ## Planning a task
4447
+
4448
+ Read the project first, then propose. One \`subtask add\` per step:
4449
+
4450
+ \`\`\`
4451
+ workser task subtask add "Build the upload endpoint" \\
4452
+ --role api --kind service \\
4453
+ --note "Accept a file, work out its type, hand it to the right analyzer." \\
4454
+ --scope src/app/api/analyze/route.ts
4455
+
4456
+ workser task subtask add "Check every supported file type end to end" \\
4457
+ --role qa --depends-on RIZZ-15
4458
+ \`\`\`
4459
+
4460
+ \`--role\` is one of: pm, architect, web, api, automation, qa.
4461
+ \`--kind\` is what the step produces: data_reports, web, mobile, service,
4462
+ automation, docs. It is not the same fact as the role \u2014 the same engineer
4463
+ writing a screen, the docs for it and the service behind it is three kinds of
4464
+ work.
4465
+
4466
+ \`--scope\` is what that step OWNS. Two steps naming the same file cannot run at
4467
+ the same time, so keeping scopes apart is what lets the team work in parallel.
4468
+ \`--depends-on\` takes the keys you read off \`task show\`.
4469
+
4470
+ Between three and six steps. If it needs more, say the task is too big instead.
4471
+ The step that CHECKS work must not be the same role as the one that built it.
4472
+
4473
+ ## Nothing runs until the owner approves
4474
+
4475
+ \`\`\`
4476
+ workser task can-start
4477
+ \`\`\`
4478
+
4479
+ This exits non-zero, with the reason, until they have approved the plan \u2014 that
4480
+ refusal is the product working, not an error to route around. Ask with
4481
+ \`workser task approval request\`; only a person can answer.
4482
+
4483
+ ## Finishing a step
4484
+
4485
+ \`\`\`
4486
+ workser task done --summary "The report now shows cost per KOL, with six months of history."
4487
+ \`\`\`
4488
+
4489
+ Write the summary for someone who runs a business and does not read code.
4407
4490
  `
4408
4491
  },
4409
4492
  {
@@ -4632,7 +4715,18 @@ function buildContext(opts) {
4632
4715
  const projectId = opts.project || process.env.WORKSER_PROJECT_ID || link?.projectId || session.defaultProjectId;
4633
4716
  const runId = process.env.WORKSER_RUN_ID || void 0;
4634
4717
  const conversationId = process.env.WORKSER_CONVERSATION_ID || void 0;
4635
- return { endpoint, socketPath, token, mode: mode2, cwd, projectId, runId, conversationId };
4718
+ const projectTaskId = process.env.WORKSER_PROJECT_TASK_ID || void 0;
4719
+ return {
4720
+ endpoint,
4721
+ socketPath,
4722
+ token,
4723
+ mode: mode2,
4724
+ cwd,
4725
+ projectId,
4726
+ runId,
4727
+ conversationId,
4728
+ projectTaskId
4729
+ };
4636
4730
  }
4637
4731
  function runTarget(ctx) {
4638
4732
  return ctx.runId || "current";
@@ -6941,8 +7035,278 @@ function collect2(value, previous) {
6941
7035
  return [...previous, value];
6942
7036
  }
6943
7037
 
6944
- // src/commands/decision.ts
7038
+ // src/commands/task.ts
6945
7039
  var import_picocolors27 = __toESM(require_picocolors(), 1);
7040
+ var STATUSES2 = ["todo", "working", "checking", "ready", "accepted", "archived"];
7041
+ var ROLES = ["pm", "architect", "web", "api", "automation", "qa"];
7042
+ var KINDS2 = ["data_reports", "web", "mobile", "service", "automation", "docs"];
7043
+ function registerTask(program3) {
7044
+ const task = program3.command("task").description("The project's tasks and their subtasks (AI Tech Team)");
7045
+ task.command("list").description("List the board's tasks \u2014 run this before starting anything").option("--status <value>", `only tasks in this status (${STATUSES2.join(" | ")})`).option("--label <value>", "only tasks carrying this label").option("--limit <n>", "cap the number of tasks returned").action(
7046
+ action(async ({ ctx, opts }) => {
7047
+ requireProject(ctx);
7048
+ if (opts.status !== void 0) assertOneOf("--status", opts.status, STATUSES2);
7049
+ const rows = await api(ctx, "/v1/project-tasks", {
7050
+ query: {
7051
+ status: opts.status,
7052
+ label: opts.label,
7053
+ limit: opts.limit
7054
+ }
7055
+ }) ?? [];
7056
+ ok(rows, () => {
7057
+ if (!rows.length) {
7058
+ line(import_picocolors27.default.dim("No tasks on the board yet."));
7059
+ return;
7060
+ }
7061
+ for (const r of rows) line(formatRow2(r));
7062
+ });
7063
+ })
7064
+ );
7065
+ task.command("show [id]").description(
7066
+ "Show one task with its subtasks. Defaults to the task this run is inside."
7067
+ ).action(
7068
+ action(async ({ ctx, args }) => {
7069
+ const id = resolveTaskId(ctx, args[0]);
7070
+ const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
7071
+ ok(row, () => printTask(row));
7072
+ })
7073
+ );
7074
+ const subtask = task.command("subtask").description("The steps a task is broken into");
7075
+ subtask.command("add <title>").description(
7076
+ 'Add one step, e.g. `workser task subtask add "Build the upload screen" --role web`'
7077
+ ).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(
7078
+ action(async ({ ctx, args, opts }) => {
7079
+ const parent = resolveTaskId(ctx, opts.task);
7080
+ if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
7081
+ if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
7082
+ const dependsOn = await resolveDeps(ctx, parent, opts.dependsOn ?? []);
7083
+ const row = await api(ctx, "/v1/project-tasks", {
7084
+ body: {
7085
+ parentTaskId: parent,
7086
+ title: args[0],
7087
+ summary: opts.note,
7088
+ role: opts.role,
7089
+ category: opts.kind,
7090
+ scopePaths: opts.scope,
7091
+ dependsOn,
7092
+ targets: [
7093
+ ...(opts.app ?? []).map((appId) => ({ kind: "app", appId })),
7094
+ ...(opts.infra ?? []).map((ref) => ({ kind: "infra", ref }))
7095
+ ]
7096
+ }
7097
+ });
7098
+ ok(row, () => {
7099
+ line(`${import_picocolors27.default.green("added")} ${import_picocolors27.default.bold(row.title)} ${import_picocolors27.default.dim(row.key ?? row.id)}`);
7100
+ if (row.role) line(import_picocolors27.default.dim(`role: ${row.role}`));
7101
+ });
7102
+ })
7103
+ );
7104
+ subtask.command("list [taskId]").description("The steps of a task, in order").action(
7105
+ action(async ({ ctx, args }) => {
7106
+ const id = resolveTaskId(ctx, args[0]);
7107
+ const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
7108
+ const rows = row.subtasks ?? [];
7109
+ ok(rows, () => {
7110
+ if (!rows.length) {
7111
+ line(import_picocolors27.default.dim("No steps yet."));
7112
+ return;
7113
+ }
7114
+ rows.forEach((r, i) => line(formatSubtask(r, i + 1)));
7115
+ });
7116
+ })
7117
+ );
7118
+ subtask.command("update <id>").description("Change a step \u2014 its title, its role, what it touches").option("--title <text>").option("--note <text>").option("--role <value>", ROLES.join(" | ")).option("--kind <value>", KINDS2.join(" | ")).option("--scope <path...>", "replaces the step's scope entirely").action(
7119
+ action(async ({ ctx, args, opts }) => {
7120
+ if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
7121
+ if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
7122
+ const row = await api(
7123
+ ctx,
7124
+ `/v1/project-tasks/${encodeURIComponent(args[0])}`,
7125
+ {
7126
+ method: "PATCH",
7127
+ body: {
7128
+ title: opts.title,
7129
+ summary: opts.note,
7130
+ role: opts.role,
7131
+ category: opts.kind,
7132
+ scopePaths: opts.scope
7133
+ }
7134
+ }
7135
+ );
7136
+ ok(row, () => line(`${import_picocolors27.default.green("updated")} ${import_picocolors27.default.bold(row.title)}`));
7137
+ })
7138
+ );
7139
+ subtask.command("remove <id>").description("Drop a step from the plan (only before the work starts)").action(
7140
+ action(async ({ ctx, args }) => {
7141
+ await api(ctx, `/v1/project-tasks/${encodeURIComponent(args[0])}`, {
7142
+ method: "DELETE"
7143
+ });
7144
+ ok({ removed: args[0] }, () => line(import_picocolors27.default.green("removed")));
7145
+ })
7146
+ );
7147
+ task.command("move <id> <status>").description(`Move a task or step along the board (${STATUSES2.join(" | ")})`).action(
7148
+ action(async ({ ctx, args }) => {
7149
+ assertOneOf("<status>", args[1], STATUSES2);
7150
+ const row = await api(
7151
+ ctx,
7152
+ `/v1/project-tasks/${encodeURIComponent(args[0])}/move`,
7153
+ { body: { status: args[1] } }
7154
+ );
7155
+ ok(row, () => line(`${import_picocolors27.default.green("moved")} ${import_picocolors27.default.bold(row.title)} \u2192 ${args[1]}`));
7156
+ })
7157
+ );
7158
+ task.command("can-start [id]").description("Ask whether work on this task may begin. Refuses until the owner approves.").action(
7159
+ action(async ({ ctx, args }) => {
7160
+ const id = resolveTaskId(ctx, args[0]);
7161
+ const row = await api(
7162
+ ctx,
7163
+ `/v1/project-tasks/${encodeURIComponent(id)}/dispatch-check`,
7164
+ { method: "POST" }
7165
+ );
7166
+ ok(row, () => line(import_picocolors27.default.green("approved \u2014 you may start")));
7167
+ })
7168
+ );
7169
+ task.command("approval").description("Ask the owner to approve the plan, or record their decision").argument("<request|approve|decline>").option("--task <id>", "defaults to the task this run is inside").option("--note <text>", "why").action(
7170
+ action(async ({ ctx, args, opts }) => {
7171
+ const id = resolveTaskId(ctx, opts.task);
7172
+ const what = args[0];
7173
+ if (what === "request") {
7174
+ const row2 = await api(
7175
+ ctx,
7176
+ `/v1/project-tasks/${encodeURIComponent(id)}`
7177
+ );
7178
+ ok({ awaiting: row2.approval_state === "awaiting", task: row2 }, () => {
7179
+ line(
7180
+ row2.approval_state === "awaiting" ? import_picocolors27.default.yellow("The plan is waiting on the owner. They see it in the task.") : `Already ${row2.approval_state}.`
7181
+ );
7182
+ });
7183
+ return;
7184
+ }
7185
+ if (what !== "approve" && what !== "decline") {
7186
+ throw new WorkserError(
7187
+ `Expected "request", "approve" or "decline", got "${what}".`,
7188
+ { code: "bad_request" }
7189
+ );
7190
+ }
7191
+ const row = await api(
7192
+ ctx,
7193
+ `/v1/project-tasks/${encodeURIComponent(id)}/approval`,
7194
+ {
7195
+ body: {
7196
+ decision: what === "approve" ? "approved" : "declined",
7197
+ note: opts.note
7198
+ }
7199
+ }
7200
+ );
7201
+ ok(row, () => line(`${import_picocolors27.default.green(row.approval_state)} ${import_picocolors27.default.bold(row.title)}`));
7202
+ })
7203
+ );
7204
+ task.command("done [id]").description("Record what a step produced, and move it to ready").option("--summary <text>", "what changed, in the owner's words").action(
7205
+ action(async ({ ctx, args, opts }) => {
7206
+ const id = resolveTaskId(ctx, args[0]);
7207
+ await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`, {
7208
+ method: "PATCH",
7209
+ body: { resultSummary: opts.summary }
7210
+ });
7211
+ const row = await api(
7212
+ ctx,
7213
+ `/v1/project-tasks/${encodeURIComponent(id)}/move`,
7214
+ { body: { status: "ready" } }
7215
+ );
7216
+ ok(row, () => line(`${import_picocolors27.default.green("ready")} ${import_picocolors27.default.bold(row.title)}`));
7217
+ })
7218
+ );
7219
+ }
7220
+ function resolveTaskId(ctx, given) {
7221
+ const id = given || ctx.projectTaskId;
7222
+ if (!id) {
7223
+ throw new WorkserError(
7224
+ "No task. This run isn't inside one (WORKSER_PROJECT_TASK_ID is unset), so pass --task <id or key>.",
7225
+ { code: "no_task" }
7226
+ );
7227
+ }
7228
+ return id;
7229
+ }
7230
+ async function resolveDeps(ctx, parentId, given) {
7231
+ if (!given.length) return void 0;
7232
+ const parent = await api(
7233
+ ctx,
7234
+ `/v1/project-tasks/${encodeURIComponent(parentId)}`
7235
+ );
7236
+ const byKey = /* @__PURE__ */ new Map();
7237
+ for (const s of parent.subtasks ?? []) {
7238
+ byKey.set(s.id, s.id);
7239
+ if (s.key) byKey.set(s.key, s.id);
7240
+ }
7241
+ return given.map((g) => {
7242
+ const id = byKey.get(g);
7243
+ if (!id) {
7244
+ throw new WorkserError(
7245
+ `"${g}" is not a step of this task. Run \`workser task subtask list\` to see them.`,
7246
+ { code: "bad_request" }
7247
+ );
7248
+ }
7249
+ return id;
7250
+ });
7251
+ }
7252
+ function assertOneOf(flag, value, allowed) {
7253
+ if (!allowed.includes(value)) {
7254
+ throw new WorkserError(
7255
+ `${flag} must be one of: ${allowed.join(", ")} \u2014 got "${value}".`,
7256
+ { code: "bad_request" }
7257
+ );
7258
+ }
7259
+ }
7260
+ function formatRow2(r) {
7261
+ const key = import_picocolors27.default.dim((r.key ?? r.id.slice(0, 8)).padEnd(10));
7262
+ const steps = r.subtaskTotal ? import_picocolors27.default.dim(` ${r.subtaskDone}/${r.subtaskTotal}`) : "";
7263
+ const gate = r.approval_state === "awaiting" ? import_picocolors27.default.yellow(" awaiting approval") : "";
7264
+ return `${key} ${statusTag2(r.status)} ${r.title}${steps}${gate}`;
7265
+ }
7266
+ function formatSubtask(r, index) {
7267
+ const n = import_picocolors27.default.dim(String(index).padStart(2, "0"));
7268
+ const role = r.role ? import_picocolors27.default.dim(` [${r.role}]`) : "";
7269
+ const scope = r.scope_paths?.length ? import_picocolors27.default.dim(` owns: ${r.scope_paths.join(", ")}`) : "";
7270
+ return `${n} ${statusTag2(r.status)} ${r.title}${role}${scope}`;
7271
+ }
7272
+ function printTask(row) {
7273
+ line(`${import_picocolors27.default.bold(row.title)} ${import_picocolors27.default.dim(row.key ?? row.id)}`);
7274
+ line(`${statusTag2(row.status)} approval: ${row.approval_state}`);
7275
+ if (row.summary) line(`
7276
+ ${row.summary}`);
7277
+ if (row.targets?.length) {
7278
+ line(
7279
+ `
7280
+ touches: ${row.targets.map((t) => t.appName ?? t.ref ?? t.kind).join(", ")}`
7281
+ );
7282
+ }
7283
+ if (row.subtasks?.length) {
7284
+ line(`
7285
+ ${import_picocolors27.default.bold("steps")}`);
7286
+ row.subtasks.forEach((s, i) => line(formatSubtask(s, i + 1)));
7287
+ } else {
7288
+ line(import_picocolors27.default.dim("\nNo steps yet."));
7289
+ }
7290
+ }
7291
+ function statusTag2(status) {
7292
+ switch (status) {
7293
+ case "ready":
7294
+ return import_picocolors27.default.green("[ready]");
7295
+ case "working":
7296
+ return import_picocolors27.default.blue("[working]");
7297
+ case "checking":
7298
+ return import_picocolors27.default.cyan("[checking]");
7299
+ case "accepted":
7300
+ return import_picocolors27.default.green("[accepted]");
7301
+ case "archived":
7302
+ return import_picocolors27.default.dim("[archived]");
7303
+ default:
7304
+ return import_picocolors27.default.dim("[todo]");
7305
+ }
7306
+ }
7307
+
7308
+ // src/commands/decision.ts
7309
+ var import_picocolors28 = __toESM(require_picocolors(), 1);
6946
7310
  function registerDecision(program3) {
6947
7311
  const decision = program3.command("decision").description("Read and record the project's architecture decisions");
6948
7312
  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(
@@ -6955,11 +7319,11 @@ function registerDecision(program3) {
6955
7319
  rows = applyLimit(rows, opts.limit);
6956
7320
  ok(rows, () => {
6957
7321
  if (!rows.length) {
6958
- line(import_picocolors27.default.dim("No decisions recorded yet."));
7322
+ line(import_picocolors28.default.dim("No decisions recorded yet."));
6959
7323
  return;
6960
7324
  }
6961
7325
  for (const r of rows) {
6962
- line(`${import_picocolors27.default.dim(r.id)} ${import_picocolors27.default.dim(shortDate(r.createdAt))} ${r.title}`);
7326
+ line(`${import_picocolors28.default.dim(r.id)} ${import_picocolors28.default.dim(shortDate(r.createdAt))} ${r.title}`);
6963
7327
  line(` ${truncate(r.decision, 100)}`);
6964
7328
  }
6965
7329
  });
@@ -6973,16 +7337,16 @@ function registerDecision(program3) {
6973
7337
  `/v1/projects/${projectId}/architecture-decisions/${args[0]}`
6974
7338
  );
6975
7339
  ok(row, () => {
6976
- line(`${import_picocolors27.default.bold(row.title)} ${import_picocolors27.default.dim(row.id)}`);
6977
- line(import_picocolors27.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
7340
+ line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7341
+ line(import_picocolors28.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
6978
7342
  line(`
6979
- ${import_picocolors27.default.bold("Context")}
7343
+ ${import_picocolors28.default.bold("Context")}
6980
7344
  ${row.context}`);
6981
7345
  line(`
6982
- ${import_picocolors27.default.bold("Decision")}
7346
+ ${import_picocolors28.default.bold("Decision")}
6983
7347
  ${row.decision}`);
6984
7348
  if (row.consequences) line(`
6985
- ${import_picocolors27.default.bold("Consequences")}
7349
+ ${import_picocolors28.default.bold("Consequences")}
6986
7350
  ${row.consequences}`);
6987
7351
  });
6988
7352
  })
@@ -7011,7 +7375,7 @@ ${row.consequences}`);
7011
7375
  refId: row?.id,
7012
7376
  output: { decision: row }
7013
7377
  });
7014
- ok(row, () => line(`Recorded decision ${import_picocolors27.default.bold(row?.id ?? "")} \u2014 ${title}`));
7378
+ ok(row, () => line(`Recorded decision ${import_picocolors28.default.bold(row?.id ?? "")} \u2014 ${title}`));
7015
7379
  })
7016
7380
  );
7017
7381
  const requirement = program3.command("requirement").description("Read and record the project's requirements");
@@ -7023,11 +7387,11 @@ ${row.consequences}`);
7023
7387
  rows = applyLimit(rows, opts.limit);
7024
7388
  ok(rows, () => {
7025
7389
  if (!rows.length) {
7026
- line(import_picocolors27.default.dim("No requirements recorded yet."));
7390
+ line(import_picocolors28.default.dim("No requirements recorded yet."));
7027
7391
  return;
7028
7392
  }
7029
7393
  for (const r of rows) {
7030
- line(`${import_picocolors27.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
7394
+ line(`${import_picocolors28.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
7031
7395
  }
7032
7396
  });
7033
7397
  })
@@ -7040,8 +7404,8 @@ ${row.consequences}`);
7040
7404
  `/v1/projects/${projectId}/requirements/${args[0]}`
7041
7405
  );
7042
7406
  ok(row, () => {
7043
- line(`${import_picocolors27.default.bold(row.title)} ${import_picocolors27.default.dim(row.id)}`);
7044
- line(import_picocolors27.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
7407
+ line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7408
+ line(import_picocolors28.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
7045
7409
  line(`
7046
7410
  ${row.body}`);
7047
7411
  });
@@ -7067,7 +7431,7 @@ ${row.body}`);
7067
7431
  refId: row?.id,
7068
7432
  output: { requirement: row }
7069
7433
  });
7070
- ok(row, () => line(`Recorded requirement ${import_picocolors27.default.bold(row?.id ?? "")} \u2014 ${title}`));
7434
+ ok(row, () => line(`Recorded requirement ${import_picocolors28.default.bold(row?.id ?? "")} \u2014 ${title}`));
7071
7435
  })
7072
7436
  );
7073
7437
  requirement.command("update <id>").description(
@@ -7096,7 +7460,7 @@ ${row.body}`);
7096
7460
  code: "bad_request"
7097
7461
  });
7098
7462
  }
7099
- ok(row, () => line(`Updated requirement ${import_picocolors27.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
7463
+ ok(row, () => line(`Updated requirement ${import_picocolors28.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
7100
7464
  })
7101
7465
  );
7102
7466
  }
@@ -7119,7 +7483,7 @@ function shortDate(iso) {
7119
7483
  }
7120
7484
 
7121
7485
  // src/commands/doc.ts
7122
- var import_picocolors28 = __toESM(require_picocolors(), 1);
7486
+ var import_picocolors29 = __toESM(require_picocolors(), 1);
7123
7487
  function registerDoc(program3) {
7124
7488
  const doc = program3.command("doc").description("Read and write project documents");
7125
7489
  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(
@@ -7130,13 +7494,13 @@ function registerDoc(program3) {
7130
7494
  }) ?? [];
7131
7495
  ok(rows, () => {
7132
7496
  if (!rows.length) {
7133
- line(import_picocolors28.default.dim("No documents yet."));
7497
+ line(import_picocolors29.default.dim("No documents yet."));
7134
7498
  return;
7135
7499
  }
7136
7500
  for (const r of rows) {
7137
- const link = r.workItemId ? import_picocolors28.default.dim(` \u21B3 ${r.workItemId}`) : "";
7138
- const file = r.filePath ? import_picocolors28.default.dim(` ${r.filePath}`) : "";
7139
- line(`${import_picocolors28.default.dim(r.id)} ${r.title}${link}${file}`);
7501
+ const link = r.workItemId ? import_picocolors29.default.dim(` \u21B3 ${r.workItemId}`) : "";
7502
+ const file = r.filePath ? import_picocolors29.default.dim(` ${r.filePath}`) : "";
7503
+ line(`${import_picocolors29.default.dim(r.id)} ${r.title}${link}${file}`);
7140
7504
  }
7141
7505
  });
7142
7506
  })
@@ -7150,17 +7514,17 @@ function registerDoc(program3) {
7150
7514
  );
7151
7515
  if (opts.markdown) {
7152
7516
  ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
7153
- line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7517
+ line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
7154
7518
  line(
7155
- row.filePath ? `Read it at ${import_picocolors28.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors28.default.dim("This document has no markdown mirror on disk yet.")
7519
+ row.filePath ? `Read it at ${import_picocolors29.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors29.default.dim("This document has no markdown mirror on disk yet.")
7156
7520
  );
7157
7521
  });
7158
7522
  return;
7159
7523
  }
7160
7524
  ok(row, () => {
7161
- line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7162
- if (row.workItemId) line(import_picocolors28.default.dim(`linked to work item ${row.workItemId}`));
7163
- if (row.filePath) line(import_picocolors28.default.dim(`markdown mirror: ${row.filePath}`));
7525
+ line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
7526
+ if (row.workItemId) line(import_picocolors29.default.dim(`linked to work item ${row.workItemId}`));
7527
+ if (row.filePath) line(import_picocolors29.default.dim(`markdown mirror: ${row.filePath}`));
7164
7528
  line("");
7165
7529
  line(row.contentJson);
7166
7530
  });
@@ -7189,7 +7553,7 @@ function registerDoc(program3) {
7189
7553
  refId: row?.id,
7190
7554
  output: { document: row }
7191
7555
  });
7192
- ok(row, () => line(`Created document ${import_picocolors28.default.bold(row?.id ?? "")} \u2014 ${title}`));
7556
+ ok(row, () => line(`Created document ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
7193
7557
  })
7194
7558
  );
7195
7559
  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(
@@ -7216,13 +7580,13 @@ function registerDoc(program3) {
7216
7580
  code: "bad_request"
7217
7581
  });
7218
7582
  }
7219
- ok(row, () => line(`Updated document ${import_picocolors28.default.bold(row.id)} \u2014 ${row.title}`));
7583
+ ok(row, () => line(`Updated document ${import_picocolors29.default.bold(row.id)} \u2014 ${row.title}`));
7220
7584
  })
7221
7585
  );
7222
7586
  }
7223
7587
 
7224
7588
  // src/commands/design.ts
7225
- var import_picocolors29 = __toESM(require_picocolors(), 1);
7589
+ var import_picocolors30 = __toESM(require_picocolors(), 1);
7226
7590
  function registerDesign(program3) {
7227
7591
  const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
7228
7592
  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(
@@ -7236,11 +7600,11 @@ function registerDesign(program3) {
7236
7600
  if (opts.raw) {
7237
7601
  ok(files, () => {
7238
7602
  if (!files.length) {
7239
- line(import_picocolors29.default.dim("No brand set for this project."));
7603
+ line(import_picocolors30.default.dim("No brand set for this project."));
7240
7604
  return;
7241
7605
  }
7242
7606
  for (const f of files) {
7243
- line(import_picocolors29.default.bold(f.path));
7607
+ line(import_picocolors30.default.bold(f.path));
7244
7608
  line(f.contents);
7245
7609
  line("");
7246
7610
  }
@@ -7257,21 +7621,21 @@ function registerDesign(program3) {
7257
7621
  } : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
7258
7622
  ok(summary, () => {
7259
7623
  if (!tokens) {
7260
- line(import_picocolors29.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
7624
+ line(import_picocolors30.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
7261
7625
  return;
7262
7626
  }
7263
7627
  for (const [name, value] of Object.entries(tokens.brand)) {
7264
- line(`${import_picocolors29.default.dim(name.padEnd(12))} ${value}`);
7628
+ line(`${import_picocolors30.default.dim(name.padEnd(12))} ${value}`);
7265
7629
  }
7266
7630
  for (const [name, value] of Object.entries(tokens.color)) {
7267
- line(`${import_picocolors29.default.dim(`color.${name}`.padEnd(12))} ${value}`);
7631
+ line(`${import_picocolors30.default.dim(`color.${name}`.padEnd(12))} ${value}`);
7268
7632
  }
7269
7633
  for (const [name, value] of Object.entries(tokens.font)) {
7270
- line(`${import_picocolors29.default.dim(`font.${name}`.padEnd(12))} ${value}`);
7634
+ line(`${import_picocolors30.default.dim(`font.${name}`.padEnd(12))} ${value}`);
7271
7635
  }
7272
7636
  line("");
7273
7637
  line(
7274
- import_picocolors29.default.dim(
7638
+ import_picocolors30.default.dim(
7275
7639
  `Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
7276
7640
  )
7277
7641
  );
@@ -7303,7 +7667,7 @@ function unwrap(group) {
7303
7667
 
7304
7668
  // src/index.ts
7305
7669
  var pkg = {
7306
- version: true ? "0.2.7" : "0.0.0-dev"
7670
+ version: true ? "0.3.0" : "0.0.0-dev"
7307
7671
  };
7308
7672
  var program2 = new Command();
7309
7673
  program2.name("workser").description(
@@ -7348,6 +7712,7 @@ registerImage(program2);
7348
7712
  registerAsk(program2);
7349
7713
  registerSearch(program2);
7350
7714
  registerBoard(program2);
7715
+ registerTask(program2);
7351
7716
  registerDecision(program2);
7352
7717
  registerDoc(program2);
7353
7718
  registerDesign(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workser/cli",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
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",
@@ -0,0 +1,82 @@
1
+ ---
2
+ topic: tasks
3
+ title: Project tasks & subtasks
4
+ summary: The ticket you are working inside: read it, break it into steps, and ask before starting.
5
+ commands: [task]
6
+ ---
7
+
8
+ # Project tasks & subtasks
9
+
10
+ A **project task** is a ticket the owner filed. You are usually running inside
11
+ one — Orbit sets `WORKSER_PROJECT_TASK_ID` on your process, so every command
12
+ below defaults to it and you rarely pass an id at all.
13
+
14
+ ```
15
+ workser task list [--status <value>] [--label <value>] [--limit <n>]
16
+ workser task show [id] # the task you are in, with its steps
17
+
18
+ workser task subtask add <title> [--role <value>] [--kind <value>]
19
+ [--note <text>] [--app <id...>]
20
+ [--infra <ref...>] [--scope <path...>]
21
+ [--depends-on <key...>]
22
+ workser task subtask list [taskId]
23
+ workser task subtask update <id> [--title|--note|--role|--kind|--scope]
24
+ workser task subtask remove <id>
25
+
26
+ workser task can-start [id] # may work begin? refuses until approved
27
+ workser task approval request # tell the owner the plan is ready
28
+ workser task move <id> <status>
29
+ workser task done [id] --summary <text>
30
+ ```
31
+
32
+ ## This is not `workser board`
33
+
34
+ `board` is the Orbit Board — a human's list of work items. `task` is the AI Tech
35
+ Team's own table. Filing your plan on the Board puts it somewhere the owner's
36
+ task page never reads: they see "created work item" and an empty plan. Use
37
+ `task subtask add`.
38
+
39
+ ## Planning a task
40
+
41
+ Read the project first, then propose. One `subtask add` per step:
42
+
43
+ ```
44
+ workser task subtask add "Build the upload endpoint" \
45
+ --role api --kind service \
46
+ --note "Accept a file, work out its type, hand it to the right analyzer." \
47
+ --scope src/app/api/analyze/route.ts
48
+
49
+ workser task subtask add "Check every supported file type end to end" \
50
+ --role qa --depends-on RIZZ-15
51
+ ```
52
+
53
+ `--role` is one of: pm, architect, web, api, automation, qa.
54
+ `--kind` is what the step produces: data_reports, web, mobile, service,
55
+ automation, docs. It is not the same fact as the role — the same engineer
56
+ writing a screen, the docs for it and the service behind it is three kinds of
57
+ work.
58
+
59
+ `--scope` is what that step OWNS. Two steps naming the same file cannot run at
60
+ the same time, so keeping scopes apart is what lets the team work in parallel.
61
+ `--depends-on` takes the keys you read off `task show`.
62
+
63
+ Between three and six steps. If it needs more, say the task is too big instead.
64
+ The step that CHECKS work must not be the same role as the one that built it.
65
+
66
+ ## Nothing runs until the owner approves
67
+
68
+ ```
69
+ workser task can-start
70
+ ```
71
+
72
+ This exits non-zero, with the reason, until they have approved the plan — that
73
+ refusal is the product working, not an error to route around. Ask with
74
+ `workser task approval request`; only a person can answer.
75
+
76
+ ## Finishing a step
77
+
78
+ ```
79
+ workser task done --summary "The report now shows cost per KOL, with six months of history."
80
+ ```
81
+
82
+ Write the summary for someone who runs a business and does not read code.