@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 +482 -37
- package/package.json +1 -1
- package/skills/workser/reference/tasks.md +82 -0
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
|
-
|
|
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/
|
|
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(
|
|
7397
|
+
line(import_picocolors28.default.dim("No decisions recorded yet."));
|
|
6959
7398
|
return;
|
|
6960
7399
|
}
|
|
6961
7400
|
for (const r of rows) {
|
|
6962
|
-
line(`${
|
|
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(`${
|
|
6977
|
-
line(
|
|
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
|
-
${
|
|
7418
|
+
${import_picocolors28.default.bold("Context")}
|
|
6980
7419
|
${row.context}`);
|
|
6981
7420
|
line(`
|
|
6982
|
-
${
|
|
7421
|
+
${import_picocolors28.default.bold("Decision")}
|
|
6983
7422
|
${row.decision}`);
|
|
6984
7423
|
if (row.consequences) line(`
|
|
6985
|
-
${
|
|
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 ${
|
|
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(
|
|
7465
|
+
line(import_picocolors28.default.dim("No requirements recorded yet."));
|
|
7027
7466
|
return;
|
|
7028
7467
|
}
|
|
7029
7468
|
for (const r of rows) {
|
|
7030
|
-
line(`${
|
|
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(`${
|
|
7044
|
-
line(
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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(
|
|
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 ?
|
|
7138
|
-
const file = r.filePath ?
|
|
7139
|
-
line(`${
|
|
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(`${
|
|
7592
|
+
line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
|
|
7154
7593
|
line(
|
|
7155
|
-
row.filePath ? `Read it at ${
|
|
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(`${
|
|
7162
|
-
if (row.workItemId) line(
|
|
7163
|
-
if (row.filePath) line(
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
7709
|
+
line(`${import_picocolors30.default.dim(`font.${name}`.padEnd(12))} ${value}`);
|
|
7271
7710
|
}
|
|
7272
7711
|
line("");
|
|
7273
7712
|
line(
|
|
7274
|
-
|
|
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.
|
|
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.
|
|
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.
|