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