@workser/cli 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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,353 @@ 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/role-guard.ts
7309
+ var READS = [
7310
+ "task",
7311
+ "board",
7312
+ "doc",
7313
+ "decision",
7314
+ "memory",
7315
+ "search",
7316
+ "verify",
7317
+ "logs",
7318
+ "status",
7319
+ "help",
7320
+ "whoami",
7321
+ "login",
7322
+ "auth",
7323
+ "project",
7324
+ "open",
7325
+ "doctor"
7326
+ ];
7327
+ var BUILDS = [
7328
+ ...READS,
7329
+ "app",
7330
+ "env",
7331
+ "db",
7332
+ "storage",
7333
+ "checkpoint",
7334
+ "artifact",
7335
+ "image",
7336
+ "design",
7337
+ "ask",
7338
+ "sync",
7339
+ "tool",
7340
+ "workflow",
7341
+ "neon",
7342
+ "business"
7343
+ ];
7344
+ var ROLE_VERBS = {
7345
+ pm: [...READS, "ask", "app"],
7346
+ architect: [...BUILDS, "versions"],
7347
+ web: BUILDS,
7348
+ api: BUILDS,
7349
+ mobile: BUILDS,
7350
+ python: BUILDS,
7351
+ automation: BUILDS,
7352
+ designer: BUILDS,
7353
+ qa: READS,
7354
+ security: READS,
7355
+ analyst: READS,
7356
+ sre: [...READS, "deploy", "domain", "versions"],
7357
+ devops: [...BUILDS, "deploy", "domain", "versions"]
7358
+ };
7359
+ var NEVER = {
7360
+ // Approving is the owner's, full stop. An agent that can approve the plan it
7361
+ // proposed has removed the only gate this product has.
7362
+ "task approval": "Only the owner can approve a plan."
7363
+ };
7364
+ function assertRoleMayRun(argv) {
7365
+ const role = (process.env.WORKSER_ROLE ?? "").trim();
7366
+ if (!role) return;
7367
+ const verb = argv[0];
7368
+ if (!verb) return;
7369
+ const pair = `${argv[0]} ${argv[1] ?? ""}`.trim();
7370
+ if (NEVER[pair] && !(pair === "task approval" && (argv[2] === "request" || !argv[2]))) {
7371
+ throw new WorkserError(NEVER[pair], { code: "role_forbidden" });
7372
+ }
7373
+ const allowed = ROLE_VERBS[role];
7374
+ const list = allowed ?? READS;
7375
+ if (!list.includes(verb)) {
7376
+ throw new WorkserError(
7377
+ `The ${role} role can't run \`workser ${verb}\`. Report what you found instead, and the step that owns this will do it.`,
7378
+ { code: "role_forbidden" }
7379
+ );
7380
+ }
7381
+ }
7382
+
7383
+ // src/commands/decision.ts
7384
+ var import_picocolors28 = __toESM(require_picocolors(), 1);
6946
7385
  function registerDecision(program3) {
6947
7386
  const decision = program3.command("decision").description("Read and record the project's architecture decisions");
6948
7387
  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 +7394,11 @@ function registerDecision(program3) {
6955
7394
  rows = applyLimit(rows, opts.limit);
6956
7395
  ok(rows, () => {
6957
7396
  if (!rows.length) {
6958
- line(import_picocolors27.default.dim("No decisions recorded yet."));
7397
+ line(import_picocolors28.default.dim("No decisions recorded yet."));
6959
7398
  return;
6960
7399
  }
6961
7400
  for (const r of rows) {
6962
- line(`${import_picocolors27.default.dim(r.id)} ${import_picocolors27.default.dim(shortDate(r.createdAt))} ${r.title}`);
7401
+ line(`${import_picocolors28.default.dim(r.id)} ${import_picocolors28.default.dim(shortDate(r.createdAt))} ${r.title}`);
6963
7402
  line(` ${truncate(r.decision, 100)}`);
6964
7403
  }
6965
7404
  });
@@ -6973,16 +7412,16 @@ function registerDecision(program3) {
6973
7412
  `/v1/projects/${projectId}/architecture-decisions/${args[0]}`
6974
7413
  );
6975
7414
  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)}`));
7415
+ line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7416
+ line(import_picocolors28.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
6978
7417
  line(`
6979
- ${import_picocolors27.default.bold("Context")}
7418
+ ${import_picocolors28.default.bold("Context")}
6980
7419
  ${row.context}`);
6981
7420
  line(`
6982
- ${import_picocolors27.default.bold("Decision")}
7421
+ ${import_picocolors28.default.bold("Decision")}
6983
7422
  ${row.decision}`);
6984
7423
  if (row.consequences) line(`
6985
- ${import_picocolors27.default.bold("Consequences")}
7424
+ ${import_picocolors28.default.bold("Consequences")}
6986
7425
  ${row.consequences}`);
6987
7426
  });
6988
7427
  })
@@ -7011,7 +7450,7 @@ ${row.consequences}`);
7011
7450
  refId: row?.id,
7012
7451
  output: { decision: row }
7013
7452
  });
7014
- ok(row, () => line(`Recorded decision ${import_picocolors27.default.bold(row?.id ?? "")} \u2014 ${title}`));
7453
+ ok(row, () => line(`Recorded decision ${import_picocolors28.default.bold(row?.id ?? "")} \u2014 ${title}`));
7015
7454
  })
7016
7455
  );
7017
7456
  const requirement = program3.command("requirement").description("Read and record the project's requirements");
@@ -7023,11 +7462,11 @@ ${row.consequences}`);
7023
7462
  rows = applyLimit(rows, opts.limit);
7024
7463
  ok(rows, () => {
7025
7464
  if (!rows.length) {
7026
- line(import_picocolors27.default.dim("No requirements recorded yet."));
7465
+ line(import_picocolors28.default.dim("No requirements recorded yet."));
7027
7466
  return;
7028
7467
  }
7029
7468
  for (const r of rows) {
7030
- line(`${import_picocolors27.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
7469
+ line(`${import_picocolors28.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
7031
7470
  }
7032
7471
  });
7033
7472
  })
@@ -7040,8 +7479,8 @@ ${row.consequences}`);
7040
7479
  `/v1/projects/${projectId}/requirements/${args[0]}`
7041
7480
  );
7042
7481
  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)}`));
7482
+ line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7483
+ line(import_picocolors28.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
7045
7484
  line(`
7046
7485
  ${row.body}`);
7047
7486
  });
@@ -7067,7 +7506,7 @@ ${row.body}`);
7067
7506
  refId: row?.id,
7068
7507
  output: { requirement: row }
7069
7508
  });
7070
- ok(row, () => line(`Recorded requirement ${import_picocolors27.default.bold(row?.id ?? "")} \u2014 ${title}`));
7509
+ ok(row, () => line(`Recorded requirement ${import_picocolors28.default.bold(row?.id ?? "")} \u2014 ${title}`));
7071
7510
  })
7072
7511
  );
7073
7512
  requirement.command("update <id>").description(
@@ -7096,7 +7535,7 @@ ${row.body}`);
7096
7535
  code: "bad_request"
7097
7536
  });
7098
7537
  }
7099
- ok(row, () => line(`Updated requirement ${import_picocolors27.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
7538
+ ok(row, () => line(`Updated requirement ${import_picocolors28.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
7100
7539
  })
7101
7540
  );
7102
7541
  }
@@ -7119,7 +7558,7 @@ function shortDate(iso) {
7119
7558
  }
7120
7559
 
7121
7560
  // src/commands/doc.ts
7122
- var import_picocolors28 = __toESM(require_picocolors(), 1);
7561
+ var import_picocolors29 = __toESM(require_picocolors(), 1);
7123
7562
  function registerDoc(program3) {
7124
7563
  const doc = program3.command("doc").description("Read and write project documents");
7125
7564
  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 +7569,13 @@ function registerDoc(program3) {
7130
7569
  }) ?? [];
7131
7570
  ok(rows, () => {
7132
7571
  if (!rows.length) {
7133
- line(import_picocolors28.default.dim("No documents yet."));
7572
+ line(import_picocolors29.default.dim("No documents yet."));
7134
7573
  return;
7135
7574
  }
7136
7575
  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}`);
7576
+ const link = r.workItemId ? import_picocolors29.default.dim(` \u21B3 ${r.workItemId}`) : "";
7577
+ const file = r.filePath ? import_picocolors29.default.dim(` ${r.filePath}`) : "";
7578
+ line(`${import_picocolors29.default.dim(r.id)} ${r.title}${link}${file}`);
7140
7579
  }
7141
7580
  });
7142
7581
  })
@@ -7150,17 +7589,17 @@ function registerDoc(program3) {
7150
7589
  );
7151
7590
  if (opts.markdown) {
7152
7591
  ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
7153
- line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7592
+ line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
7154
7593
  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.")
7594
+ 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
7595
  );
7157
7596
  });
7158
7597
  return;
7159
7598
  }
7160
7599
  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}`));
7600
+ line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
7601
+ if (row.workItemId) line(import_picocolors29.default.dim(`linked to work item ${row.workItemId}`));
7602
+ if (row.filePath) line(import_picocolors29.default.dim(`markdown mirror: ${row.filePath}`));
7164
7603
  line("");
7165
7604
  line(row.contentJson);
7166
7605
  });
@@ -7189,7 +7628,7 @@ function registerDoc(program3) {
7189
7628
  refId: row?.id,
7190
7629
  output: { document: row }
7191
7630
  });
7192
- ok(row, () => line(`Created document ${import_picocolors28.default.bold(row?.id ?? "")} \u2014 ${title}`));
7631
+ ok(row, () => line(`Created document ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
7193
7632
  })
7194
7633
  );
7195
7634
  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 +7655,13 @@ function registerDoc(program3) {
7216
7655
  code: "bad_request"
7217
7656
  });
7218
7657
  }
7219
- ok(row, () => line(`Updated document ${import_picocolors28.default.bold(row.id)} \u2014 ${row.title}`));
7658
+ ok(row, () => line(`Updated document ${import_picocolors29.default.bold(row.id)} \u2014 ${row.title}`));
7220
7659
  })
7221
7660
  );
7222
7661
  }
7223
7662
 
7224
7663
  // src/commands/design.ts
7225
- var import_picocolors29 = __toESM(require_picocolors(), 1);
7664
+ var import_picocolors30 = __toESM(require_picocolors(), 1);
7226
7665
  function registerDesign(program3) {
7227
7666
  const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
7228
7667
  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 +7675,11 @@ function registerDesign(program3) {
7236
7675
  if (opts.raw) {
7237
7676
  ok(files, () => {
7238
7677
  if (!files.length) {
7239
- line(import_picocolors29.default.dim("No brand set for this project."));
7678
+ line(import_picocolors30.default.dim("No brand set for this project."));
7240
7679
  return;
7241
7680
  }
7242
7681
  for (const f of files) {
7243
- line(import_picocolors29.default.bold(f.path));
7682
+ line(import_picocolors30.default.bold(f.path));
7244
7683
  line(f.contents);
7245
7684
  line("");
7246
7685
  }
@@ -7257,21 +7696,21 @@ function registerDesign(program3) {
7257
7696
  } : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
7258
7697
  ok(summary, () => {
7259
7698
  if (!tokens) {
7260
- line(import_picocolors29.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
7699
+ line(import_picocolors30.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
7261
7700
  return;
7262
7701
  }
7263
7702
  for (const [name, value] of Object.entries(tokens.brand)) {
7264
- line(`${import_picocolors29.default.dim(name.padEnd(12))} ${value}`);
7703
+ line(`${import_picocolors30.default.dim(name.padEnd(12))} ${value}`);
7265
7704
  }
7266
7705
  for (const [name, value] of Object.entries(tokens.color)) {
7267
- line(`${import_picocolors29.default.dim(`color.${name}`.padEnd(12))} ${value}`);
7706
+ line(`${import_picocolors30.default.dim(`color.${name}`.padEnd(12))} ${value}`);
7268
7707
  }
7269
7708
  for (const [name, value] of Object.entries(tokens.font)) {
7270
- line(`${import_picocolors29.default.dim(`font.${name}`.padEnd(12))} ${value}`);
7709
+ line(`${import_picocolors30.default.dim(`font.${name}`.padEnd(12))} ${value}`);
7271
7710
  }
7272
7711
  line("");
7273
7712
  line(
7274
- import_picocolors29.default.dim(
7713
+ import_picocolors30.default.dim(
7275
7714
  `Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
7276
7715
  )
7277
7716
  );
@@ -7303,7 +7742,7 @@ function unwrap(group) {
7303
7742
 
7304
7743
  // src/index.ts
7305
7744
  var pkg = {
7306
- version: true ? "0.2.7" : "0.0.0-dev"
7745
+ version: true ? "0.3.1" : "0.0.0-dev"
7307
7746
  };
7308
7747
  var program2 = new Command();
7309
7748
  program2.name("workser").description(
@@ -7348,7 +7787,13 @@ registerImage(program2);
7348
7787
  registerAsk(program2);
7349
7788
  registerSearch(program2);
7350
7789
  registerBoard(program2);
7790
+ registerTask(program2);
7351
7791
  registerDecision(program2);
7352
7792
  registerDoc(program2);
7353
7793
  registerDesign(program2);
7794
+ try {
7795
+ assertRoleMayRun(process.argv.slice(2));
7796
+ } catch (e) {
7797
+ fail(e);
7798
+ }
7354
7799
  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.7",
3
+ "version": "0.3.1",
4
4
  "description": "Workser CLI — give your local AI agent native DevOps & infrastructure on Workser. The agent runs `workser …` to provision, deploy, and manage real apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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.