@workser/cli 0.6.0 → 0.6.2
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 +876 -193
- package/package.json +1 -1
- package/skills/workser/SKILL.md +20 -20
- package/skills/workser/reference/goals.md +69 -0
- package/skills/workser/reference/sdlc-entities.md +24 -50
- package/skills/workser/reference/tasks.md +13 -0
package/dist/index.js
CHANGED
|
@@ -4485,6 +4485,76 @@ same rule.
|
|
|
4485
4485
|
- **Cloud and local are different environments.** \`env set\` configures the
|
|
4486
4486
|
cloud; the files in the app folder configure this computer. Don't hand-edit
|
|
4487
4487
|
one to change the other.
|
|
4488
|
+
`
|
|
4489
|
+
},
|
|
4490
|
+
{
|
|
4491
|
+
topic: "goals",
|
|
4492
|
+
title: "Business goals & phases",
|
|
4493
|
+
summary: "The level above a task \u2014 a business requirement too big for one sitting, and the ordered phases that deliver it.",
|
|
4494
|
+
commands: ["goal"],
|
|
4495
|
+
source: "skills/workser/reference/goals.md",
|
|
4496
|
+
body: `# Business goals & phases
|
|
4497
|
+
|
|
4498
|
+
A **goal** is a business requirement \u2014 "customers can buy from my site" \u2014 that
|
|
4499
|
+
does not fit in one task. It carries an ordered list of **phase names**; a task
|
|
4500
|
+
joins one by carrying that name (\`workser task create --goal <id> --phase <name>\`).
|
|
4501
|
+
A phase has no existence apart from the tasks inside it, so it cannot drift from
|
|
4502
|
+
the work it names.
|
|
4503
|
+
|
|
4504
|
+
\`\`\`
|
|
4505
|
+
workser goal list
|
|
4506
|
+
workser goal show <id>
|
|
4507
|
+
workser goal create <title> --phase <name...> [--outcome <text>]
|
|
4508
|
+
[--criteria <json>]
|
|
4509
|
+
workser goal update <id> [--title <text>] [--outcome <text>]
|
|
4510
|
+
[--phase <name...>] [--status <value>]
|
|
4511
|
+
workser goal check <id> <criterionId> --phase <name> (--pass|--fail|--reset) [--note <text>]
|
|
4512
|
+
\`\`\`
|
|
4513
|
+
|
|
4514
|
+
Status is one of: proposed, agreed, working, delivered, abandoned.
|
|
4515
|
+
|
|
4516
|
+
## A goal has to be argued for
|
|
4517
|
+
|
|
4518
|
+
Most requests are one task and should stay one task. Turning a two-hour job into
|
|
4519
|
+
four milestones buries the owner in ceremony before anything is built \u2014 use
|
|
4520
|
+
\`workser task create\` unless you can genuinely name two-plus slices that each
|
|
4521
|
+
deliver something the owner would notice.
|
|
4522
|
+
|
|
4523
|
+
\`\`\`
|
|
4524
|
+
workser goal create "Launch checkout" \\
|
|
4525
|
+
--phase "Cart" --phase "Payment" --phase "Receipts" \\
|
|
4526
|
+
--outcome "A customer can buy something and get a receipt"
|
|
4527
|
+
\`\`\`
|
|
4528
|
+
|
|
4529
|
+
\`--phase\` needs 2\u20136 names. Propose the shape only \u2014 nothing is created until the
|
|
4530
|
+
owner agrees it; planning phase four now is waste, since it will change once
|
|
4531
|
+
phase one is real.
|
|
4532
|
+
|
|
4533
|
+
## Acceptance criteria are agreed with the shape
|
|
4534
|
+
|
|
4535
|
+
\`--criteria\` maps each phase name to the owner's own sentences about what "done"
|
|
4536
|
+
means for it, e.g. \`'{"Payment":["A customer can pay by card and gets a receipt"]}'\`.
|
|
4537
|
+
Written afterwards they only describe what got built; written with the shape they
|
|
4538
|
+
can still change the plan.
|
|
4539
|
+
|
|
4540
|
+
Record whether one is met with \`workser goal check\`:
|
|
4541
|
+
|
|
4542
|
+
\`\`\`
|
|
4543
|
+
workser goal check g_123 c1 --phase Payment --pass \\
|
|
4544
|
+
--note "Tested a card payment end to end; receipt emailed."
|
|
4545
|
+
\`\`\`
|
|
4546
|
+
|
|
4547
|
+
\`--note\` is required on \`--pass\` \u2014 a tick the owner cannot verify is worse than no
|
|
4548
|
+
tick.
|
|
4549
|
+
|
|
4550
|
+
## Joining tasks to a goal
|
|
4551
|
+
|
|
4552
|
+
\`\`\`
|
|
4553
|
+
workser task create "Build the payment form" --goal g_123 --phase Payment
|
|
4554
|
+
\`\`\`
|
|
4555
|
+
|
|
4556
|
+
A goal's apps and progress are derived from the tasks that join it, not declared
|
|
4557
|
+
up front \u2014 most of the apps a goal will touch don't exist when it's proposed.
|
|
4488
4558
|
`
|
|
4489
4559
|
},
|
|
4490
4560
|
{
|
|
@@ -4685,28 +4755,27 @@ real scope, not just a repeat of the task.
|
|
|
4685
4755
|
},
|
|
4686
4756
|
{
|
|
4687
4757
|
topic: "sdlc-entities",
|
|
4688
|
-
title: "
|
|
4689
|
-
summary: "Read what this project already
|
|
4758
|
+
title: "Decisions and requirements",
|
|
4759
|
+
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
4760
|
commands: ["board", "decision", "requirement"],
|
|
4691
4761
|
source: "skills/workser/reference/sdlc-entities.md",
|
|
4692
|
-
body: `#
|
|
4762
|
+
body: `# Decisions and requirements
|
|
4693
4763
|
|
|
4694
4764
|
These are the project's memory across sessions. They write to the **same tables**
|
|
4695
|
-
the Orbit desktop's
|
|
4696
|
-
|
|
4697
|
-
|
|
4765
|
+
the Orbit desktop's Project Memory panel uses, so anything here appears there too
|
|
4766
|
+
\u2014 and, inside an Orbit-spawned run, as an inline card in the conversation.
|
|
4767
|
+
Documents have their own guide: \`workser help docs\`. Phased work is tracked as
|
|
4768
|
+
subtasks, not here \u2014 see \`workser help tasks\`.
|
|
4769
|
+
|
|
4770
|
+
> **The Board (\`workser board ...\`) is deprecated for agent use.** It used to be
|
|
4771
|
+
> where a multi-phase plan went, one card per phase \u2014 and the phase was ALSO a
|
|
4772
|
+
> subtask the planning turn had just filed for the same piece of work. That gave
|
|
4773
|
+
> a task two competing plans, one of which this task's own page never reads and
|
|
4774
|
+
> nothing kept in sync with the other. Phases are \`project_tasks\` subtasks now,
|
|
4775
|
+
> full stop: \`workser task subtask add\`. Do not run \`workser board create\` for
|
|
4776
|
+
> planned work \u2014 see \`workser help tasks\`.
|
|
4698
4777
|
|
|
4699
4778
|
\`\`\`
|
|
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
4779
|
workser decision list [--limit <n>]
|
|
4711
4780
|
workser decision show <id>
|
|
4712
4781
|
workser decision create <title> --context <text> --decision <text>
|
|
@@ -4724,7 +4793,6 @@ workser requirement update <id> [--title <text>] [--body <text>] [--status <text
|
|
|
4724
4793
|
Before starting anything beyond a trivial edit:
|
|
4725
4794
|
|
|
4726
4795
|
\`\`\`
|
|
4727
|
-
workser board list --json # what's already tracked (don't re-file it)
|
|
4728
4796
|
workser decision list --json # what was already decided (don't reverse it)
|
|
4729
4797
|
\`\`\`
|
|
4730
4798
|
|
|
@@ -4734,19 +4802,18 @@ purpose \u2014 \`workser decision show <id>\` gives you the context and conseque
|
|
|
4734
4802
|
not just the title. Reach for \`workser doc list\` / \`workser requirement list\`
|
|
4735
4803
|
the same way when the task touches documented behaviour.
|
|
4736
4804
|
|
|
4737
|
-
## Work with phases \u2192
|
|
4805
|
+
## Work with phases \u2192 subtasks + a plan doc, before you build
|
|
4738
4806
|
|
|
4739
4807
|
The moment you split a task into more than one phase, file it \u2014 not afterwards,
|
|
4740
4808
|
and not only in your reply, which is gone once the conversation scrolls.
|
|
4741
4809
|
|
|
4742
4810
|
\`\`\`bash
|
|
4743
|
-
#
|
|
4744
|
-
workser
|
|
4745
|
-
--
|
|
4746
|
-
|
|
4747
|
-
workser board create "Phase 2 \u2014 checkout API" --description "\u2026" --json
|
|
4811
|
+
# the phases themselves \u2014 this task's own subtask list, not the Board
|
|
4812
|
+
workser task subtask add "Phase 1 \u2014 schema + migration" --role api \\
|
|
4813
|
+
--note "Add orders/line_items tables and the migration."
|
|
4814
|
+
workser task subtask add "Phase 2 \u2014 checkout API" --role api --note "\u2026"
|
|
4748
4815
|
|
|
4749
|
-
# the plan
|
|
4816
|
+
# the plan's narrative, ONE doc, deliberately NOT linked to a subtask
|
|
4750
4817
|
workser doc create "Checkout \u2014 implementation plan" --markdown "$(cat plan.md)" --json
|
|
4751
4818
|
|
|
4752
4819
|
# the approach, if the plan settled something with real alternatives
|
|
@@ -4757,32 +4824,9 @@ workser decision create "Carts live server-side" --context "\u2026" --decision "
|
|
|
4757
4824
|
its card and is *hidden* from the Docs panel; a plan spanning three phases
|
|
4758
4825
|
belongs to the project, not to phase 1.
|
|
4759
4826
|
|
|
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.
|
|
4827
|
+
The bar: if the user closed this conversation now, the subtask list should still
|
|
4828
|
+
show what's left and the doc should still explain the plan to whoever continues
|
|
4829
|
+
it.
|
|
4786
4830
|
|
|
4787
4831
|
## Decisions are append-only
|
|
4788
4832
|
|
|
@@ -4894,6 +4938,9 @@ below defaults to it and you rarely pass an id at all.
|
|
|
4894
4938
|
\`\`\`
|
|
4895
4939
|
workser task list [--status <value>] [--label <value>] [--limit <n>]
|
|
4896
4940
|
workser task show [id] # the task you are in, with its steps
|
|
4941
|
+
workser task create <title> [--note <text>] [--kind <value>]
|
|
4942
|
+
[--label <value...>] [--app <id...>]
|
|
4943
|
+
[--infra <ref...>]
|
|
4897
4944
|
|
|
4898
4945
|
workser task subtask add <title> [--role <value>] [--kind <value>]
|
|
4899
4946
|
[--note <text>] [--app <id...>]
|
|
@@ -4917,6 +4964,16 @@ Team's own table. Filing your plan on the Board puts it somewhere the owner's
|
|
|
4917
4964
|
task page never reads: they see "created work item" and an empty plan. Use
|
|
4918
4965
|
\`task subtask add\`.
|
|
4919
4966
|
|
|
4967
|
+
## Opening work from a project channel
|
|
4968
|
+
|
|
4969
|
+
When a project-channel conversation produces actionable work, the PM may record
|
|
4970
|
+
it with \`workser task create\`. Orbit supplies the channel and source-message IDs;
|
|
4971
|
+
the command records them and posts the new task card as a Project Manager message
|
|
4972
|
+
automatically. Do not invent or ask for those IDs.
|
|
4973
|
+
|
|
4974
|
+
Opening a task does **not** approve it or start implementation. The task remains
|
|
4975
|
+
awaiting the owner. Never approve or dispatch a task you opened yourself.
|
|
4976
|
+
|
|
4920
4977
|
## Planning a task
|
|
4921
4978
|
|
|
4922
4979
|
Read the project first, then propose. One \`subtask add\` per step:
|
|
@@ -5161,11 +5218,11 @@ function registerHelp(program3) {
|
|
|
5161
5218
|
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
5162
5219
|
|
|
5163
5220
|
// src/context.ts
|
|
5164
|
-
import { resolve } from "path";
|
|
5221
|
+
import { resolve as resolve2 } from "path";
|
|
5165
5222
|
|
|
5166
5223
|
// src/config.ts
|
|
5167
5224
|
import { homedir } from "os";
|
|
5168
|
-
import { join } from "path";
|
|
5225
|
+
import { dirname, join, resolve } from "path";
|
|
5169
5226
|
import {
|
|
5170
5227
|
chmodSync,
|
|
5171
5228
|
existsSync,
|
|
@@ -5173,6 +5230,8 @@ import {
|
|
|
5173
5230
|
readFileSync,
|
|
5174
5231
|
writeFileSync
|
|
5175
5232
|
} from "fs";
|
|
5233
|
+
var PROJECT_MARKER = ".workser-project";
|
|
5234
|
+
var APP_MARKER = ".workser-app";
|
|
5176
5235
|
var GLOBAL_DIR = join(homedir(), ".workser");
|
|
5177
5236
|
var SESSION_FILE = join(GLOBAL_DIR, "session.json");
|
|
5178
5237
|
function readSession() {
|
|
@@ -5203,10 +5262,82 @@ function projectLinkPath(cwd) {
|
|
|
5203
5262
|
return join(cwd, ".workser", "project.json");
|
|
5204
5263
|
}
|
|
5205
5264
|
function readProjectLink(cwd) {
|
|
5265
|
+
const found = readFolderIdentity(cwd);
|
|
5266
|
+
if (!found) return null;
|
|
5267
|
+
return {
|
|
5268
|
+
projectId: found.projectId,
|
|
5269
|
+
name: found.projectName,
|
|
5270
|
+
orgId: found.orgId,
|
|
5271
|
+
root: found.projectRoot
|
|
5272
|
+
};
|
|
5273
|
+
}
|
|
5274
|
+
function readFolderIdentity(cwd) {
|
|
5275
|
+
let app = null;
|
|
5276
|
+
for (const dir of ancestors(cwd)) {
|
|
5277
|
+
if (!app) {
|
|
5278
|
+
const marker2 = readJson(join(dir, APP_MARKER));
|
|
5279
|
+
if (marker2 && typeof marker2.appId === "string") {
|
|
5280
|
+
app = { marker: marker2, root: dir };
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
const link = readJson(projectLinkPath(dir));
|
|
5284
|
+
if (link && typeof link.projectId === "string") {
|
|
5285
|
+
return {
|
|
5286
|
+
projectRoot: dir,
|
|
5287
|
+
projectId: link.projectId,
|
|
5288
|
+
projectName: link.name,
|
|
5289
|
+
orgId: link.orgId,
|
|
5290
|
+
...appFields(app)
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5293
|
+
const marker = readJson(join(dir, PROJECT_MARKER));
|
|
5294
|
+
if (marker && typeof marker.projectId === "string") {
|
|
5295
|
+
return {
|
|
5296
|
+
projectRoot: dir,
|
|
5297
|
+
projectId: marker.projectId,
|
|
5298
|
+
projectName: marker.projectName ?? void 0,
|
|
5299
|
+
orgId: marker.orgId ?? void 0,
|
|
5300
|
+
...appFields(app)
|
|
5301
|
+
};
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
if (app && typeof app.marker.projectId === "string") {
|
|
5305
|
+
return {
|
|
5306
|
+
projectRoot: app.root,
|
|
5307
|
+
projectId: app.marker.projectId,
|
|
5308
|
+
projectName: app.marker.projectName ?? void 0,
|
|
5309
|
+
orgId: app.marker.orgId ?? void 0,
|
|
5310
|
+
...appFields(app)
|
|
5311
|
+
};
|
|
5312
|
+
}
|
|
5313
|
+
return null;
|
|
5314
|
+
}
|
|
5315
|
+
function appFields(app) {
|
|
5316
|
+
if (!app) return {};
|
|
5317
|
+
return {
|
|
5318
|
+
appId: app.marker.appId,
|
|
5319
|
+
appName: app.marker.appName ?? void 0,
|
|
5320
|
+
appRoot: app.root
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
function ancestors(cwd) {
|
|
5324
|
+
const out = [];
|
|
5325
|
+
const home = homedir();
|
|
5326
|
+
let dir = resolve(cwd);
|
|
5327
|
+
for (let i = 0; i < 64; i++) {
|
|
5328
|
+
out.push(dir);
|
|
5329
|
+
if (dir === home) break;
|
|
5330
|
+
const parent = dirname(dir);
|
|
5331
|
+
if (parent === dir) break;
|
|
5332
|
+
dir = parent;
|
|
5333
|
+
}
|
|
5334
|
+
return out;
|
|
5335
|
+
}
|
|
5336
|
+
function readJson(file) {
|
|
5206
5337
|
try {
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
return
|
|
5338
|
+
if (!existsSync(file)) return null;
|
|
5339
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
5340
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
5210
5341
|
} catch {
|
|
5211
5342
|
return null;
|
|
5212
5343
|
}
|
|
@@ -5245,7 +5376,7 @@ function cloudBaseUrl() {
|
|
|
5245
5376
|
// src/context.ts
|
|
5246
5377
|
function buildContext(opts) {
|
|
5247
5378
|
const session = readSession();
|
|
5248
|
-
const cwd = opts.cwd ?
|
|
5379
|
+
const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
|
|
5249
5380
|
const token = opts.token || process.env.WORKSER_TOKEN || session.token;
|
|
5250
5381
|
const overridden = Boolean(
|
|
5251
5382
|
opts.endpoint || opts.token || process.env.WORKSER_DAEMON_URL || process.env.WORKSER_TOKEN
|
|
@@ -5254,11 +5385,18 @@ function buildContext(opts) {
|
|
|
5254
5385
|
const endpointRaw = socketPath ? "http://localhost" : opts.endpoint || process.env.WORKSER_DAEMON_URL || session.endpoint || cloudBaseUrl();
|
|
5255
5386
|
const endpoint = endpointRaw.replace(/\/+$/, "");
|
|
5256
5387
|
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 ||
|
|
5388
|
+
const folder = readFolderIdentity(cwd);
|
|
5389
|
+
const projectId = opts.project || process.env.WORKSER_PROJECT_ID || folder?.projectId || session.defaultProjectId;
|
|
5390
|
+
const inThisProject = folder?.projectId === projectId;
|
|
5259
5391
|
const runId = process.env.WORKSER_RUN_ID || void 0;
|
|
5260
5392
|
const conversationId = process.env.WORKSER_CONVERSATION_ID || void 0;
|
|
5261
5393
|
const projectTaskId = process.env.WORKSER_PROJECT_TASK_ID || void 0;
|
|
5394
|
+
const parentTaskId = process.env.WORKSER_PARENT_TASK_ID || void 0;
|
|
5395
|
+
const projectChannelId = process.env.WORKSER_PROJECT_CHANNEL_ID || void 0;
|
|
5396
|
+
const projectChannelMessageId = process.env.WORKSER_PROJECT_CHANNEL_MESSAGE_ID || void 0;
|
|
5397
|
+
const agentRole = process.env.WORKSER_AGENT_ROLE || void 0;
|
|
5398
|
+
const agentType = process.env.WORKSER_AGENT_TYPE || void 0;
|
|
5399
|
+
const agentModel = process.env.WORKSER_AGENT_MODEL || void 0;
|
|
5262
5400
|
return {
|
|
5263
5401
|
endpoint,
|
|
5264
5402
|
socketPath,
|
|
@@ -5266,9 +5404,18 @@ function buildContext(opts) {
|
|
|
5266
5404
|
mode: mode2,
|
|
5267
5405
|
cwd,
|
|
5268
5406
|
projectId,
|
|
5407
|
+
projectRoot: inThisProject ? folder?.projectRoot : void 0,
|
|
5408
|
+
appId: inThisProject ? folder?.appId : void 0,
|
|
5409
|
+
appName: inThisProject ? folder?.appName : void 0,
|
|
5269
5410
|
runId,
|
|
5270
5411
|
conversationId,
|
|
5271
|
-
projectTaskId
|
|
5412
|
+
projectTaskId,
|
|
5413
|
+
parentTaskId,
|
|
5414
|
+
projectChannelId,
|
|
5415
|
+
projectChannelMessageId,
|
|
5416
|
+
agentRole,
|
|
5417
|
+
agentType,
|
|
5418
|
+
agentModel
|
|
5272
5419
|
};
|
|
5273
5420
|
}
|
|
5274
5421
|
function runTarget(ctx) {
|
|
@@ -5304,7 +5451,7 @@ This shell is talking to ${ctx.endpoint} instead of a local app.
|
|
|
5304
5451
|
function requireProject(ctx) {
|
|
5305
5452
|
if (!ctx.projectId) {
|
|
5306
5453
|
throw new WorkserError(
|
|
5307
|
-
"No project selected
|
|
5454
|
+
"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
5455
|
{ code: "no_project" }
|
|
5309
5456
|
);
|
|
5310
5457
|
}
|
|
@@ -5329,7 +5476,7 @@ function action(fn) {
|
|
|
5329
5476
|
// src/client.ts
|
|
5330
5477
|
import * as http from "http";
|
|
5331
5478
|
function requestOverSocket(socketPath, pathWithQuery, init) {
|
|
5332
|
-
return new Promise((
|
|
5479
|
+
return new Promise((resolve5, reject) => {
|
|
5333
5480
|
const req = http.request(
|
|
5334
5481
|
{ socketPath, path: pathWithQuery, method: init.method, headers: init.headers },
|
|
5335
5482
|
(res) => {
|
|
@@ -5338,7 +5485,7 @@ function requestOverSocket(socketPath, pathWithQuery, init) {
|
|
|
5338
5485
|
res.on("data", (c) => text += c);
|
|
5339
5486
|
res.on(
|
|
5340
5487
|
"end",
|
|
5341
|
-
() =>
|
|
5488
|
+
() => resolve5({
|
|
5342
5489
|
ok: (res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300,
|
|
5343
5490
|
status: res.statusCode ?? 0,
|
|
5344
5491
|
statusText: res.statusMessage ?? "",
|
|
@@ -5435,13 +5582,28 @@ function registerStatus(program3) {
|
|
|
5435
5582
|
program3.command("status").description("Show connection, current workspace/project, and latest deploy").action(
|
|
5436
5583
|
action(async ({ ctx }) => {
|
|
5437
5584
|
const data = await api(ctx, "/v1/status", { query: { project: ctx.projectId } });
|
|
5438
|
-
|
|
5585
|
+
const merged = {
|
|
5586
|
+
...data,
|
|
5587
|
+
folder: {
|
|
5588
|
+
cwd: ctx.cwd,
|
|
5589
|
+
projectRoot: ctx.projectRoot ?? null,
|
|
5590
|
+
appId: ctx.appId ?? null,
|
|
5591
|
+
appName: ctx.appName ?? null
|
|
5592
|
+
}
|
|
5593
|
+
};
|
|
5594
|
+
ok(merged, () => {
|
|
5439
5595
|
line(import_picocolors3.default.bold("Workser") + import_picocolors3.default.dim(` (${ctx.mode} \xB7 ${ctx.endpoint})`));
|
|
5440
5596
|
line(` user: ${data.user?.email ?? "\u2014"}`);
|
|
5441
5597
|
line(` workspace: ${data.workspace?.name ?? "\u2014"}`);
|
|
5442
5598
|
line(
|
|
5443
5599
|
` project: ${data.project?.name ?? "\u2014"}` + (data.project?.id ? import_picocolors3.default.dim(` (${data.project.id})`) : "")
|
|
5444
5600
|
);
|
|
5601
|
+
if (ctx.projectRoot) line(` folder: ${import_picocolors3.default.dim(ctx.projectRoot)}`);
|
|
5602
|
+
if (ctx.appId) {
|
|
5603
|
+
line(
|
|
5604
|
+
` app: ${ctx.appName ?? "\u2014"}` + import_picocolors3.default.dim(` (${ctx.appId})`)
|
|
5605
|
+
);
|
|
5606
|
+
}
|
|
5445
5607
|
if (data.latestDeploy) {
|
|
5446
5608
|
line(
|
|
5447
5609
|
` deploy: ${colorStatus(data.latestDeploy.status)}` + (data.latestDeploy.url ? ` ${import_picocolors3.default.cyan(data.latestDeploy.url)}` : "")
|
|
@@ -5778,7 +5940,7 @@ function registerAuth(program3) {
|
|
|
5778
5940
|
// src/commands/storage.ts
|
|
5779
5941
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
5780
5942
|
import { readFile, writeFile } from "fs/promises";
|
|
5781
|
-
import { basename, dirname } from "path";
|
|
5943
|
+
import { basename, dirname as dirname2 } from "path";
|
|
5782
5944
|
function registerStorage(program3) {
|
|
5783
5945
|
const storage = program3.command("storage").description("Provision and work with the project's object storage (Cloudflare R2)");
|
|
5784
5946
|
storage.command("create [name]").description("Provision a storage bucket for the project (idempotent)").action(
|
|
@@ -5824,7 +5986,7 @@ function registerStorage(program3) {
|
|
|
5824
5986
|
const bytes = await readFile(localPath).catch(() => {
|
|
5825
5987
|
throw new WorkserError(`Can't read local file: ${localPath}`, { code: "bad_input" });
|
|
5826
5988
|
});
|
|
5827
|
-
const dir =
|
|
5989
|
+
const dir = dirname2(key);
|
|
5828
5990
|
const res = await api(ctx, `/v1/projects/${projectId}/storage/upload-base64`, {
|
|
5829
5991
|
body: {
|
|
5830
5992
|
filename: basename(key),
|
|
@@ -6980,7 +7142,10 @@ function registerAgent(program3) {
|
|
|
6980
7142
|
).option("--role <label>", "display label for this run (default: <agent>)").option(
|
|
6981
7143
|
"--instructions <text>",
|
|
6982
7144
|
"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").
|
|
7145
|
+
).option("--model <model>", "model override for the backing CLI").option("--effort <level>", "reasoning effort, where the backing CLI supports it").option(
|
|
7146
|
+
"--read-only",
|
|
7147
|
+
"inspection only \u2014 the teammate runs in the CLI's read-only sandbox and cannot change anything"
|
|
7148
|
+
).action(
|
|
6984
7149
|
action(async ({ ctx, args, opts }) => {
|
|
6985
7150
|
const spawnAgent = args[0];
|
|
6986
7151
|
const task = args[1].join(" ");
|
|
@@ -6991,7 +7156,10 @@ function registerAgent(program3) {
|
|
|
6991
7156
|
role: opts.role,
|
|
6992
7157
|
instructions: opts.instructions,
|
|
6993
7158
|
model: opts.model,
|
|
6994
|
-
effort: opts.effort
|
|
7159
|
+
effort: opts.effort,
|
|
7160
|
+
// Enforced by the backing CLI's own sandbox, not by asking the
|
|
7161
|
+
// spawned agent to behave — see the daemon's `/agents/run`.
|
|
7162
|
+
readOnly: opts.readOnly === true
|
|
6995
7163
|
}
|
|
6996
7164
|
});
|
|
6997
7165
|
const exitCode = res?.exitCode ?? 0;
|
|
@@ -7517,7 +7685,7 @@ function businessPath(projectId, resource, subpath) {
|
|
|
7517
7685
|
// src/commands/artifact.ts
|
|
7518
7686
|
var import_picocolors24 = __toESM(require_picocolors(), 1);
|
|
7519
7687
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
7520
|
-
import { resolve as
|
|
7688
|
+
import { resolve as resolve3, basename as basename3 } from "path";
|
|
7521
7689
|
var KINDS = [
|
|
7522
7690
|
"file",
|
|
7523
7691
|
"folder",
|
|
@@ -7566,7 +7734,7 @@ function registerArtifact(program3) {
|
|
|
7566
7734
|
let absPath;
|
|
7567
7735
|
let kind = opts.kind;
|
|
7568
7736
|
if (rawPath) {
|
|
7569
|
-
absPath =
|
|
7737
|
+
absPath = resolve3(ctx.cwd, rawPath);
|
|
7570
7738
|
if (!existsSync2(absPath)) {
|
|
7571
7739
|
throw new WorkserError(
|
|
7572
7740
|
`Nothing at ${absPath}. Register the file after you've written it.`,
|
|
@@ -7617,6 +7785,30 @@ function registerArtifact(program3) {
|
|
|
7617
7785
|
);
|
|
7618
7786
|
})
|
|
7619
7787
|
);
|
|
7788
|
+
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(
|
|
7789
|
+
action(async ({ ctx, opts }) => {
|
|
7790
|
+
const stepId = opts.step || (opts.mine ? ctx.projectTaskId : void 0);
|
|
7791
|
+
if (opts.mine && !stepId) {
|
|
7792
|
+
throw new WorkserError(
|
|
7793
|
+
"--mine needs a step: this run isn't inside one (WORKSER_PROJECT_TASK_ID is unset)."
|
|
7794
|
+
);
|
|
7795
|
+
}
|
|
7796
|
+
let path;
|
|
7797
|
+
if (stepId) {
|
|
7798
|
+
path = `/v1/project-subtasks/${encodeURIComponent(stepId)}/artifacts`;
|
|
7799
|
+
} else {
|
|
7800
|
+
const taskId = opts.task || ctx.parentTaskId || ctx.projectTaskId;
|
|
7801
|
+
if (!taskId) {
|
|
7802
|
+
throw new WorkserError(
|
|
7803
|
+
"No task. This run isn't inside one, so pass --task <id> or --step <id>."
|
|
7804
|
+
);
|
|
7805
|
+
}
|
|
7806
|
+
path = `/v1/project-tasks/${encodeURIComponent(taskId)}/artifacts?scope=all`;
|
|
7807
|
+
}
|
|
7808
|
+
const rows = await api(ctx, path) ?? [];
|
|
7809
|
+
ok(rows, () => printArtifacts(rows));
|
|
7810
|
+
})
|
|
7811
|
+
);
|
|
7620
7812
|
artifact.command("run").description("Show the task/conversation this agent run is attached to").action(
|
|
7621
7813
|
action(async ({ ctx }) => {
|
|
7622
7814
|
const res = await api(ctx, `/v1/runs/${runTarget(ctx)}`);
|
|
@@ -7624,6 +7816,31 @@ function registerArtifact(program3) {
|
|
|
7624
7816
|
})
|
|
7625
7817
|
);
|
|
7626
7818
|
}
|
|
7819
|
+
function printArtifacts(rows) {
|
|
7820
|
+
if (!rows.length) {
|
|
7821
|
+
line(import_picocolors24.default.dim("Nothing produced yet."));
|
|
7822
|
+
return;
|
|
7823
|
+
}
|
|
7824
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
7825
|
+
for (const r of rows) {
|
|
7826
|
+
const list = byStep.get(r.subtask_id) ?? [];
|
|
7827
|
+
list.push(r);
|
|
7828
|
+
byStep.set(r.subtask_id, list);
|
|
7829
|
+
}
|
|
7830
|
+
for (const [stepId, items] of byStep) {
|
|
7831
|
+
line(import_picocolors24.default.bold(`step ${stepId}`));
|
|
7832
|
+
for (const a of items) {
|
|
7833
|
+
const flag = a.promoted_at ? import_picocolors24.default.green(" *") : " ";
|
|
7834
|
+
const where = a.local_path || a.cloud_url || "";
|
|
7835
|
+
line(
|
|
7836
|
+
`${flag} ${import_picocolors24.default.dim(`[${a.kind}]`)} ${a.title ?? "(untitled)"}` + (where ? import_picocolors24.default.dim(` ${where}`) : "")
|
|
7837
|
+
);
|
|
7838
|
+
if (a.description) line(import_picocolors24.default.dim(` ${a.description}`));
|
|
7839
|
+
}
|
|
7840
|
+
line("");
|
|
7841
|
+
}
|
|
7842
|
+
line(import_picocolors24.default.dim("* = handed over as a deliverable; the rest is working material."));
|
|
7843
|
+
}
|
|
7627
7844
|
function printRun(run) {
|
|
7628
7845
|
if (!run) return;
|
|
7629
7846
|
line(` run ${import_picocolors24.default.bold(run.runId)}`);
|
|
@@ -7635,7 +7852,7 @@ function printRun(run) {
|
|
|
7635
7852
|
|
|
7636
7853
|
// src/commands/image.ts
|
|
7637
7854
|
import { writeFile as writeFile4, mkdir } from "fs/promises";
|
|
7638
|
-
import { dirname as
|
|
7855
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
7639
7856
|
function registerImage(program3) {
|
|
7640
7857
|
const image = program3.command("image").description("Generate images from a text prompt");
|
|
7641
7858
|
image.command("generate <prompt>").alias("gen").description("Generate an image and return its public URL").option(
|
|
@@ -7690,7 +7907,7 @@ function registerImage(program3) {
|
|
|
7690
7907
|
);
|
|
7691
7908
|
}
|
|
7692
7909
|
async function download(url, output) {
|
|
7693
|
-
const target =
|
|
7910
|
+
const target = resolve4(output);
|
|
7694
7911
|
const res = await fetch(url);
|
|
7695
7912
|
if (!res.ok) {
|
|
7696
7913
|
throw new WorkserError(
|
|
@@ -7698,7 +7915,7 @@ async function download(url, output) {
|
|
|
7698
7915
|
{ code: "download_failed" }
|
|
7699
7916
|
);
|
|
7700
7917
|
}
|
|
7701
|
-
await mkdir(
|
|
7918
|
+
await mkdir(dirname3(target), { recursive: true });
|
|
7702
7919
|
await writeFile4(target, Buffer.from(await res.arrayBuffer()));
|
|
7703
7920
|
return target;
|
|
7704
7921
|
}
|
|
@@ -7711,7 +7928,22 @@ var TYPES = [
|
|
|
7711
7928
|
"approval",
|
|
7712
7929
|
"confirmation",
|
|
7713
7930
|
"file_upload",
|
|
7714
|
-
"information"
|
|
7931
|
+
"information",
|
|
7932
|
+
/**
|
|
7933
|
+
* "Add an app to this project, and do it for me."
|
|
7934
|
+
*
|
|
7935
|
+
* NOT an approval with a well-worded message. What the user is agreeing to
|
|
7936
|
+
* is real infrastructure — a repository, hosting, a folder on their machine,
|
|
7937
|
+
* sometimes a database — and they are entitled to see WHICH app before they
|
|
7938
|
+
* agree, which a sentence and a Yes button cannot show. Needs `--app-type`;
|
|
7939
|
+
* `--app-name` is what it will be called.
|
|
7940
|
+
*
|
|
7941
|
+
* You cannot create an app yourself, and that is deliberate: an agent must
|
|
7942
|
+
* not be able to provision things on somebody's account by deciding to. This
|
|
7943
|
+
* asks. Their click is what creates it, and the answer tells you the new
|
|
7944
|
+
* app's id so the rest of your plan can be scoped to it.
|
|
7945
|
+
*/
|
|
7946
|
+
"create_app"
|
|
7715
7947
|
];
|
|
7716
7948
|
function registerAsk(program3) {
|
|
7717
7949
|
program3.command("ask <message>").description("Ask the user a question and wait for their answer").option(
|
|
@@ -7723,6 +7955,12 @@ function registerAsk(program3) {
|
|
|
7723
7955
|
"a choice (repeat for each; implies --type choice)",
|
|
7724
7956
|
collect,
|
|
7725
7957
|
[]
|
|
7958
|
+
).option(
|
|
7959
|
+
"--app-type <type>",
|
|
7960
|
+
"create_app only: web | mobile | api | worker | cron | python"
|
|
7961
|
+
).option(
|
|
7962
|
+
"--app-name <name>",
|
|
7963
|
+
"create_app only: what the app should be called"
|
|
7726
7964
|
).option("--priority <level>", "low | normal | high | urgent", "normal").option(
|
|
7727
7965
|
"--timeout <seconds>",
|
|
7728
7966
|
"how long to wait before giving up (default: 600, max: 3600)",
|
|
@@ -7749,6 +7987,12 @@ function registerAsk(program3) {
|
|
|
7749
7987
|
{ code: "bad_request" }
|
|
7750
7988
|
);
|
|
7751
7989
|
}
|
|
7990
|
+
if (type === "create_app" && !opts.appType) {
|
|
7991
|
+
throw new WorkserError(
|
|
7992
|
+
"Say which kind of app: `--app-type mobile`.",
|
|
7993
|
+
{ code: "bad_request" }
|
|
7994
|
+
);
|
|
7995
|
+
}
|
|
7752
7996
|
const timeoutSeconds = Number(opts.timeout);
|
|
7753
7997
|
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
|
|
7754
7998
|
throw new WorkserError("--timeout must be a positive number.", {
|
|
@@ -7762,6 +8006,8 @@ function registerAsk(program3) {
|
|
|
7762
8006
|
title: opts.title || deriveTitle(message),
|
|
7763
8007
|
message,
|
|
7764
8008
|
options: options.length ? options : void 0,
|
|
8009
|
+
appType: opts.appType,
|
|
8010
|
+
appName: opts.appName,
|
|
7765
8011
|
priority: opts.priority,
|
|
7766
8012
|
timeoutSeconds
|
|
7767
8013
|
}
|
|
@@ -8051,18 +8297,98 @@ function registerTask(program3) {
|
|
|
8051
8297
|
})
|
|
8052
8298
|
);
|
|
8053
8299
|
task.command("show [id]").description(
|
|
8054
|
-
"Show
|
|
8300
|
+
"Show a task with its subtasks, what each one did, and what they produced. Defaults to the plan this run is part of."
|
|
8055
8301
|
).action(
|
|
8056
8302
|
action(async ({ ctx, args }) => {
|
|
8057
|
-
const id =
|
|
8303
|
+
const id = args[0] || ctx.parentTaskId || resolveTaskId(ctx);
|
|
8058
8304
|
const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
|
|
8059
|
-
|
|
8305
|
+
const goalId = row.goal_id;
|
|
8306
|
+
const goal = goalId ? await api(
|
|
8307
|
+
ctx,
|
|
8308
|
+
`/v1/project-goals/${encodeURIComponent(goalId)}`
|
|
8309
|
+
).catch(() => null) : null;
|
|
8310
|
+
ok({ ...row, goal }, () => printTask(row, goal));
|
|
8060
8311
|
})
|
|
8061
8312
|
);
|
|
8062
|
-
|
|
8313
|
+
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").option("--goal <id>", "the business goal this delivers part of").option(
|
|
8314
|
+
"--phase <name>",
|
|
8315
|
+
"which slice of that goal \u2014 must match one of its phase names"
|
|
8316
|
+
).action(
|
|
8317
|
+
action(async ({ ctx, args, opts }) => {
|
|
8318
|
+
requireProject(ctx);
|
|
8319
|
+
if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
|
|
8320
|
+
const hasChannelOrigin = Boolean(
|
|
8321
|
+
ctx.projectChannelId && ctx.projectChannelMessageId
|
|
8322
|
+
);
|
|
8323
|
+
const row = await api(ctx, "/v1/project-tasks", {
|
|
8324
|
+
body: {
|
|
8325
|
+
title: args[0],
|
|
8326
|
+
summary: opts.note,
|
|
8327
|
+
category: opts.kind,
|
|
8328
|
+
labels: opts.label,
|
|
8329
|
+
// The level above this task, when it has one. Most tasks do not —
|
|
8330
|
+
// a goal has to be argued for, never assumed.
|
|
8331
|
+
goalId: opts.goal,
|
|
8332
|
+
phase: opts.phase,
|
|
8333
|
+
targets: [
|
|
8334
|
+
...(opts.app ?? []).map((appId) => ({ kind: "app", appId })),
|
|
8335
|
+
...(opts.infra ?? []).map((ref) => ({ kind: "infra", ref }))
|
|
8336
|
+
],
|
|
8337
|
+
...hasChannelOrigin ? {
|
|
8338
|
+
channelId: ctx.projectChannelId,
|
|
8339
|
+
createdFromMessageId: ctx.projectChannelMessageId,
|
|
8340
|
+
createdByKind: "agent"
|
|
8341
|
+
} : {}
|
|
8342
|
+
}
|
|
8343
|
+
});
|
|
8344
|
+
let channelMessage;
|
|
8345
|
+
let channelMessageError;
|
|
8346
|
+
if (hasChannelOrigin && ctx.projectChannelId && ctx.projectChannelMessageId) {
|
|
8347
|
+
try {
|
|
8348
|
+
channelMessage = await api(
|
|
8349
|
+
ctx,
|
|
8350
|
+
`/v1/project-channels/${encodeURIComponent(ctx.projectChannelId)}/messages`,
|
|
8351
|
+
{
|
|
8352
|
+
body: {
|
|
8353
|
+
content: "",
|
|
8354
|
+
agentRole: ctx.agentRole ?? "pm",
|
|
8355
|
+
agentType: ctx.agentType ?? "workser",
|
|
8356
|
+
agentModel: ctx.agentModel ?? "Agent default",
|
|
8357
|
+
attachments: [{ resourceType: "task", resourceId: row.id }]
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
);
|
|
8361
|
+
} catch (error) {
|
|
8362
|
+
const failure = error instanceof WorkserError ? error : new WorkserError(String(error));
|
|
8363
|
+
channelMessageError = {
|
|
8364
|
+
code: failure.code,
|
|
8365
|
+
message: failure.message
|
|
8366
|
+
};
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
const result = {
|
|
8370
|
+
...row,
|
|
8371
|
+
...channelMessage ? { channelMessage } : {},
|
|
8372
|
+
...channelMessageError ? { channelMessageError } : {}
|
|
8373
|
+
};
|
|
8374
|
+
ok(result, () => {
|
|
8375
|
+
line(`${import_picocolors28.default.green("opened")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
|
|
8376
|
+
if (channelMessage) line(import_picocolors28.default.dim("posted to the channel as Project Manager"));
|
|
8377
|
+
if (channelMessageError) {
|
|
8378
|
+
warn(
|
|
8379
|
+
`Task opened, but its Project Manager card could not be posted: ${channelMessageError.message}`
|
|
8380
|
+
);
|
|
8381
|
+
}
|
|
8382
|
+
});
|
|
8383
|
+
})
|
|
8384
|
+
);
|
|
8385
|
+
const subtask = task.command("subtask").description("The subtasks a task is broken into");
|
|
8063
8386
|
subtask.command("add <title>").description(
|
|
8064
|
-
'Add one
|
|
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
|
|
8387
|
+
'Add one subtask, e.g. `workser task subtask add "Build the upload screen" --role web`'
|
|
8388
|
+
).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 subtask does").option(
|
|
8389
|
+
"--app <id...>",
|
|
8390
|
+
"the app this subtask is for \u2014 one id runs it inside that app's folder; leave it off and it runs at the project, seeing every app"
|
|
8391
|
+
).option("--infra <ref...>", "shared setup it touches (database | storage | auth | hosting | jobs)").option("--scope <path...>", "files or folders THIS subtask owns").option("--depends-on <id...>", "subtasks that must finish first (key or id)").action(
|
|
8066
8392
|
action(async ({ ctx, args, opts }) => {
|
|
8067
8393
|
const parent = resolveTaskId(ctx, opts.task);
|
|
8068
8394
|
if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
|
|
@@ -8089,21 +8415,21 @@ function registerTask(program3) {
|
|
|
8089
8415
|
});
|
|
8090
8416
|
})
|
|
8091
8417
|
);
|
|
8092
|
-
subtask.command("list [taskId]").description("The
|
|
8418
|
+
subtask.command("list [taskId]").description("The subtasks of a task, in order").action(
|
|
8093
8419
|
action(async ({ ctx, args }) => {
|
|
8094
8420
|
const id = resolveTaskId(ctx, args[0]);
|
|
8095
8421
|
const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
|
|
8096
8422
|
const rows = row.subtasks ?? [];
|
|
8097
8423
|
ok(rows, () => {
|
|
8098
8424
|
if (!rows.length) {
|
|
8099
|
-
line(import_picocolors28.default.dim("No
|
|
8425
|
+
line(import_picocolors28.default.dim("No subtasks yet."));
|
|
8100
8426
|
return;
|
|
8101
8427
|
}
|
|
8102
8428
|
rows.forEach((r, i) => line(formatSubtask(r, i + 1)));
|
|
8103
8429
|
});
|
|
8104
8430
|
})
|
|
8105
8431
|
);
|
|
8106
|
-
subtask.command("update <id>").description("Change a
|
|
8432
|
+
subtask.command("update <id>").description("Change a subtask \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 subtask's scope entirely").action(
|
|
8107
8433
|
action(async ({ ctx, args, opts }) => {
|
|
8108
8434
|
if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
|
|
8109
8435
|
if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
|
|
@@ -8124,7 +8450,7 @@ function registerTask(program3) {
|
|
|
8124
8450
|
ok(row, () => line(`${import_picocolors28.default.green("updated")} ${import_picocolors28.default.bold(row.title)}`));
|
|
8125
8451
|
})
|
|
8126
8452
|
);
|
|
8127
|
-
subtask.command("remove <id>").description("
|
|
8453
|
+
subtask.command("remove <id>").description("Remove a subtask (only before the work starts)").action(
|
|
8128
8454
|
action(async ({ ctx, args }) => {
|
|
8129
8455
|
await api(ctx, `/v1/project-tasks/${encodeURIComponent(args[0])}`, {
|
|
8130
8456
|
method: "DELETE"
|
|
@@ -8143,6 +8469,24 @@ function registerTask(program3) {
|
|
|
8143
8469
|
ok(row, () => line(`${import_picocolors28.default.green("moved")} ${import_picocolors28.default.bold(row.title)} \u2192 ${args[1]}`));
|
|
8144
8470
|
})
|
|
8145
8471
|
);
|
|
8472
|
+
task.command("resume [id]").description(
|
|
8473
|
+
"Put a task the owner stopped back in the queue \u2014 only when they ask you to carry on"
|
|
8474
|
+
).action(
|
|
8475
|
+
action(async ({ ctx, args }) => {
|
|
8476
|
+
const id = resolveTaskId(ctx, args[0]);
|
|
8477
|
+
const row = await api(
|
|
8478
|
+
ctx,
|
|
8479
|
+
`/v1/project-tasks/${encodeURIComponent(id)}/retry`,
|
|
8480
|
+
{ method: "POST" }
|
|
8481
|
+
);
|
|
8482
|
+
ok(
|
|
8483
|
+
row,
|
|
8484
|
+
() => line(
|
|
8485
|
+
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")
|
|
8486
|
+
)
|
|
8487
|
+
);
|
|
8488
|
+
})
|
|
8489
|
+
);
|
|
8146
8490
|
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
8491
|
action(async ({ ctx, args, opts }) => {
|
|
8148
8492
|
const row = await api(
|
|
@@ -8209,7 +8553,7 @@ function registerTask(program3) {
|
|
|
8209
8553
|
ok(row, () => line(`${import_picocolors28.default.green(row.approval_state)} ${import_picocolors28.default.bold(row.title)}`));
|
|
8210
8554
|
})
|
|
8211
8555
|
);
|
|
8212
|
-
task.command("done [id]").description("Record what a
|
|
8556
|
+
task.command("done [id]").description("Record what a subtask produced, and move it to ready").option("--summary <text>", "what changed, in the owner's words").action(
|
|
8213
8557
|
action(async ({ ctx, args, opts }) => {
|
|
8214
8558
|
const id = resolveTaskId(ctx, args[0]);
|
|
8215
8559
|
await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`, {
|
|
@@ -8250,7 +8594,7 @@ async function resolveDeps(ctx, parentId, given) {
|
|
|
8250
8594
|
const id = byKey.get(g);
|
|
8251
8595
|
if (!id) {
|
|
8252
8596
|
throw new WorkserError(
|
|
8253
|
-
`"${g}" is not a
|
|
8597
|
+
`"${g}" is not a subtask of this task. Run \`workser task subtask list\` to see them.`,
|
|
8254
8598
|
{ code: "bad_request" }
|
|
8255
8599
|
);
|
|
8256
8600
|
}
|
|
@@ -8275,13 +8619,19 @@ function formatSubtask(r, index) {
|
|
|
8275
8619
|
const n = import_picocolors28.default.dim(String(index).padStart(2, "0"));
|
|
8276
8620
|
const role = r.role ? import_picocolors28.default.dim(` [${r.role}]`) : "";
|
|
8277
8621
|
const scope = r.scope_paths?.length ? import_picocolors28.default.dim(` owns: ${r.scope_paths.join(", ")}`) : "";
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8622
|
+
const head = `${n} ${statusTag2(r.status)} ${r.title}${role}${scope}`;
|
|
8623
|
+
const summary = (r.result_summary ?? "").trim();
|
|
8624
|
+
if (!summary) return head;
|
|
8625
|
+
const wrapped = summary.split("\n").map((l) => ` ${l}`).join("\n");
|
|
8626
|
+
return `${head}
|
|
8627
|
+
${import_picocolors28.default.dim(wrapped)}`;
|
|
8628
|
+
}
|
|
8629
|
+
function printTask(row, goal) {
|
|
8281
8630
|
line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
|
|
8282
8631
|
line(`${statusTag2(row.status)} approval: ${row.approval_state}`);
|
|
8283
8632
|
if (row.summary) line(`
|
|
8284
8633
|
${row.summary}`);
|
|
8634
|
+
if (goal) printGoalContext(row, goal);
|
|
8285
8635
|
if (row.targets?.length) {
|
|
8286
8636
|
line(
|
|
8287
8637
|
`
|
|
@@ -8290,11 +8640,52 @@ touches: ${row.targets.map((t) => t.appName ?? t.ref ?? t.kind).join(", ")}`
|
|
|
8290
8640
|
}
|
|
8291
8641
|
if (row.subtasks?.length) {
|
|
8292
8642
|
line(`
|
|
8293
|
-
${import_picocolors28.default.bold("
|
|
8643
|
+
${import_picocolors28.default.bold("subtasks")}`);
|
|
8294
8644
|
row.subtasks.forEach((s, i) => line(formatSubtask(s, i + 1)));
|
|
8645
|
+
line(
|
|
8646
|
+
import_picocolors28.default.dim(
|
|
8647
|
+
`
|
|
8648
|
+
Run \`workser artifact list\` to see what these subtasks produced,`
|
|
8649
|
+
)
|
|
8650
|
+
);
|
|
8651
|
+
line(
|
|
8652
|
+
import_picocolors28.default.dim(
|
|
8653
|
+
`or \`workser artifact list --step <id>\` for one subtask's output alone.`
|
|
8654
|
+
)
|
|
8655
|
+
);
|
|
8295
8656
|
} else {
|
|
8296
|
-
line(import_picocolors28.default.dim("\nNo
|
|
8657
|
+
line(import_picocolors28.default.dim("\nNo subtasks yet."));
|
|
8658
|
+
}
|
|
8659
|
+
}
|
|
8660
|
+
function printGoalContext(row, goal) {
|
|
8661
|
+
const mine = row.phase ?? null;
|
|
8662
|
+
line(`
|
|
8663
|
+
${import_picocolors28.default.bold("part of")}: ${goal.title}`);
|
|
8664
|
+
if (goal.outcome) line(import_picocolors28.default.dim(`done means: ${goal.outcome}`));
|
|
8665
|
+
const progress = goal.progress ?? [];
|
|
8666
|
+
for (const p of progress) {
|
|
8667
|
+
const where = p.total === 0 ? "not started" : `${p.done} of ${p.total} done`;
|
|
8668
|
+
const here = mine && p.name === mine ? import_picocolors28.default.cyan(" <- this task") : "";
|
|
8669
|
+
const mark = p.state === "done" ? import_picocolors28.default.green("*") : import_picocolors28.default.dim("-");
|
|
8670
|
+
line(` ${mark} ${p.name} \u2014 ${where}${here}`);
|
|
8671
|
+
}
|
|
8672
|
+
const next = progress.find((p) => p.state !== "done");
|
|
8673
|
+
const running = progress.some((p) => p.state === "working");
|
|
8674
|
+
if (next && !running) {
|
|
8675
|
+
line(
|
|
8676
|
+
import_picocolors28.default.dim(
|
|
8677
|
+
`
|
|
8678
|
+
Nothing is running. The next part waiting is "${next.name}" \u2014 offer it to them in a sentence rather than starting it unasked.`
|
|
8679
|
+
)
|
|
8680
|
+
);
|
|
8681
|
+
} else if (!next) {
|
|
8682
|
+
line(import_picocolors28.default.dim("\nEvery part of this plan is done. Say so, and offer to wrap it up."));
|
|
8297
8683
|
}
|
|
8684
|
+
line(
|
|
8685
|
+
import_picocolors28.default.dim(
|
|
8686
|
+
"The plan is a reference for what was agreed, not a rule about what may run. Work can start on any part at any time if they ask for it."
|
|
8687
|
+
)
|
|
8688
|
+
);
|
|
8298
8689
|
}
|
|
8299
8690
|
function statusTag2(status) {
|
|
8300
8691
|
switch (status) {
|
|
@@ -8313,12 +8704,230 @@ function statusTag2(status) {
|
|
|
8313
8704
|
}
|
|
8314
8705
|
}
|
|
8315
8706
|
|
|
8707
|
+
// src/commands/goal.ts
|
|
8708
|
+
var import_picocolors29 = __toESM(require_picocolors(), 1);
|
|
8709
|
+
var STATUSES3 = ["proposed", "agreed", "working", "delivered", "abandoned"];
|
|
8710
|
+
function registerGoal(program3) {
|
|
8711
|
+
const goal = program3.command("goal").description("Business goals and the phases that deliver them");
|
|
8712
|
+
goal.command("list").description("What this project is working towards").action(
|
|
8713
|
+
action(async ({ ctx }) => {
|
|
8714
|
+
requireProject(ctx);
|
|
8715
|
+
const rows = await api(ctx, "/v1/project-goals", {
|
|
8716
|
+
query: { projectId: ctx.projectId }
|
|
8717
|
+
}) ?? [];
|
|
8718
|
+
ok(rows, () => {
|
|
8719
|
+
if (!rows.length) {
|
|
8720
|
+
line(import_picocolors29.default.dim("No goals yet \u2014 every task here stands on its own."));
|
|
8721
|
+
return;
|
|
8722
|
+
}
|
|
8723
|
+
for (const g of rows) line(formatGoal(g));
|
|
8724
|
+
});
|
|
8725
|
+
})
|
|
8726
|
+
);
|
|
8727
|
+
goal.command("show <id>").description("One goal: its phases, and how far each has got").action(
|
|
8728
|
+
action(async ({ ctx, args }) => {
|
|
8729
|
+
const row = await api(
|
|
8730
|
+
ctx,
|
|
8731
|
+
`/v1/project-goals/${encodeURIComponent(String(args[0]))}`
|
|
8732
|
+
);
|
|
8733
|
+
ok(row, () => printGoal(row));
|
|
8734
|
+
})
|
|
8735
|
+
);
|
|
8736
|
+
goal.command("create <title>").description(
|
|
8737
|
+
"Propose a business goal and the phases that deliver it \u2014 the owner agrees the shape before any task exists"
|
|
8738
|
+
).option(
|
|
8739
|
+
"--outcome <text>",
|
|
8740
|
+
"what 'done' means for the whole thing, in the owner's words"
|
|
8741
|
+
).option(
|
|
8742
|
+
"--phase <name...>",
|
|
8743
|
+
"ordered phase names \u2014 each must deliver something the owner would notice"
|
|
8744
|
+
).option(
|
|
8745
|
+
"--criteria <json>",
|
|
8746
|
+
`what "done" means per phase, in the OWNER's words: '{"Checkout":["A customer can pay by card and gets a receipt"]}'`
|
|
8747
|
+
).action(
|
|
8748
|
+
action(async ({ ctx, args, opts }) => {
|
|
8749
|
+
requireProject(ctx);
|
|
8750
|
+
const phases = opts.phase ?? [];
|
|
8751
|
+
if (phases.length < 2) {
|
|
8752
|
+
throw new WorkserError(
|
|
8753
|
+
"A goal needs at least two phases. If you can't name two slices that each deliver something the owner would notice, this is one task \u2014 use `workser task create`."
|
|
8754
|
+
);
|
|
8755
|
+
}
|
|
8756
|
+
if (phases.length > 6) {
|
|
8757
|
+
throw new WorkserError(
|
|
8758
|
+
"More than six phases is a conversation, not a plan. Propose the first few and agree the rest as you go."
|
|
8759
|
+
);
|
|
8760
|
+
}
|
|
8761
|
+
let criteria = {};
|
|
8762
|
+
if (opts.criteria) {
|
|
8763
|
+
try {
|
|
8764
|
+
criteria = JSON.parse(opts.criteria);
|
|
8765
|
+
} catch {
|
|
8766
|
+
throw new WorkserError(
|
|
8767
|
+
`--criteria must be JSON mapping each phase name to a list of sentences, e.g. '{"Checkout":["A customer can pay by card"]}'`
|
|
8768
|
+
);
|
|
8769
|
+
}
|
|
8770
|
+
const unknown = Object.keys(criteria).filter(
|
|
8771
|
+
(name) => !phases.includes(name)
|
|
8772
|
+
);
|
|
8773
|
+
if (unknown.length) {
|
|
8774
|
+
throw new WorkserError(
|
|
8775
|
+
`--criteria names phases that aren't in this goal: ${unknown.join(", ")}. Phases are: ${phases.join(", ")}.`
|
|
8776
|
+
);
|
|
8777
|
+
}
|
|
8778
|
+
}
|
|
8779
|
+
const row = await api(ctx, "/v1/project-goals", {
|
|
8780
|
+
body: {
|
|
8781
|
+
projectId: ctx.projectId,
|
|
8782
|
+
title: args[0],
|
|
8783
|
+
outcome: opts.outcome,
|
|
8784
|
+
phases: phases.map((name) => ({
|
|
8785
|
+
name,
|
|
8786
|
+
criteria: (criteria[name] ?? []).map((text, i) => ({
|
|
8787
|
+
id: `c${i + 1}`,
|
|
8788
|
+
text,
|
|
8789
|
+
met: null
|
|
8790
|
+
}))
|
|
8791
|
+
})),
|
|
8792
|
+
channelId: ctx.projectChannelId,
|
|
8793
|
+
sourceMessageId: ctx.projectChannelMessageId
|
|
8794
|
+
}
|
|
8795
|
+
});
|
|
8796
|
+
ok(row, () => {
|
|
8797
|
+
success(`Proposed ${import_picocolors29.default.bold(row?.title ?? "goal")}`);
|
|
8798
|
+
printGoal(row);
|
|
8799
|
+
line(
|
|
8800
|
+
import_picocolors29.default.dim(
|
|
8801
|
+
"\nNothing has been created yet. The owner agrees the shape first."
|
|
8802
|
+
)
|
|
8803
|
+
);
|
|
8804
|
+
});
|
|
8805
|
+
})
|
|
8806
|
+
);
|
|
8807
|
+
goal.command("check <id> <criterionId>").description("Record whether an acceptance criterion is met").requiredOption("--phase <name>", "which part of the plan it belongs to").option("--pass", "it is met").option("--fail", "it is not met").option("--reset", "back to unchecked").option("--note <text>", "what you checked, and how").action(
|
|
8808
|
+
action(async ({ ctx, args, opts }) => {
|
|
8809
|
+
const chosen = [opts.pass, opts.fail, opts.reset].filter(Boolean);
|
|
8810
|
+
if (chosen.length !== 1) {
|
|
8811
|
+
throw new WorkserError(
|
|
8812
|
+
"Pass exactly one of --pass, --fail or --reset."
|
|
8813
|
+
);
|
|
8814
|
+
}
|
|
8815
|
+
if (opts.pass && !opts.note) {
|
|
8816
|
+
throw new WorkserError(
|
|
8817
|
+
"--note is required on a pass: say what you checked. A tick the owner cannot verify is worse than no tick."
|
|
8818
|
+
);
|
|
8819
|
+
}
|
|
8820
|
+
const met = opts.pass ? true : opts.fail ? false : null;
|
|
8821
|
+
const row = await api(
|
|
8822
|
+
ctx,
|
|
8823
|
+
`/v1/project-goals/${encodeURIComponent(String(args[0]))}/criteria/${encodeURIComponent(String(args[1]))}`,
|
|
8824
|
+
{ method: "POST", body: { phase: opts.phase, met, note: opts.note } }
|
|
8825
|
+
);
|
|
8826
|
+
ok(
|
|
8827
|
+
row,
|
|
8828
|
+
() => line(
|
|
8829
|
+
row?.recorded ? met === true ? import_picocolors29.default.green("recorded \u2014 met") : met === false ? import_picocolors29.default.yellow("recorded \u2014 not met") : import_picocolors29.default.dim("recorded \u2014 back to unchecked") : import_picocolors29.default.yellow(
|
|
8830
|
+
"couldn't record it \u2014 check the goal id, the phase name and the criterion id"
|
|
8831
|
+
)
|
|
8832
|
+
)
|
|
8833
|
+
);
|
|
8834
|
+
})
|
|
8835
|
+
);
|
|
8836
|
+
goal.command("update <id>").description("Change the shape, or move the goal along").option("--title <text>", "the owner's words for what they want").option("--outcome <text>", "what 'done' means").option("--phase <name...>", "replace the ordered phase list").option("--status <value>", `one of: ${STATUSES3.join(" | ")}`).action(
|
|
8837
|
+
action(async ({ ctx, args, opts }) => {
|
|
8838
|
+
if (opts.status && !STATUSES3.includes(opts.status)) {
|
|
8839
|
+
throw new WorkserError(
|
|
8840
|
+
`Unknown status "${opts.status}". Use one of: ${STATUSES3.join(", ")}.`
|
|
8841
|
+
);
|
|
8842
|
+
}
|
|
8843
|
+
const row = await api(
|
|
8844
|
+
ctx,
|
|
8845
|
+
`/v1/project-goals/${encodeURIComponent(String(args[0]))}`,
|
|
8846
|
+
{
|
|
8847
|
+
method: "PATCH",
|
|
8848
|
+
body: {
|
|
8849
|
+
title: opts.title,
|
|
8850
|
+
outcome: opts.outcome,
|
|
8851
|
+
phases: opts.phase,
|
|
8852
|
+
status: opts.status
|
|
8853
|
+
}
|
|
8854
|
+
}
|
|
8855
|
+
);
|
|
8856
|
+
ok(row, () => printGoal(row));
|
|
8857
|
+
})
|
|
8858
|
+
);
|
|
8859
|
+
}
|
|
8860
|
+
function appScope(g) {
|
|
8861
|
+
const n = g.appIds?.length ?? 0;
|
|
8862
|
+
if (!n) return "";
|
|
8863
|
+
return import_picocolors29.default.dim(` ${n} part${n === 1 ? "" : "s"} of the system`);
|
|
8864
|
+
}
|
|
8865
|
+
function formatGoal(g) {
|
|
8866
|
+
const where = g.currentPhase && g.status !== "delivered" ? import_picocolors29.default.dim(` now: ${g.currentPhase}`) : "";
|
|
8867
|
+
const count = g.taskTotal != null ? import_picocolors29.default.dim(` ${g.taskDone}/${g.taskTotal} done`) : "";
|
|
8868
|
+
return `${statusTag3(g.status)} ${g.title}${appScope(g)}${where}${count} ${import_picocolors29.default.dim(g.id)}`;
|
|
8869
|
+
}
|
|
8870
|
+
function printGoal(g) {
|
|
8871
|
+
if (!g) return;
|
|
8872
|
+
line(`${import_picocolors29.default.bold(g.title)} ${import_picocolors29.default.dim(g.id)}`);
|
|
8873
|
+
line(statusTag3(g.status));
|
|
8874
|
+
if (g.outcome) line(`
|
|
8875
|
+
done means: ${g.outcome}`);
|
|
8876
|
+
const progress = g.progress ?? [];
|
|
8877
|
+
if (!progress.length) {
|
|
8878
|
+
line(import_picocolors29.default.dim("\nNo phases agreed yet."));
|
|
8879
|
+
return;
|
|
8880
|
+
}
|
|
8881
|
+
line(`
|
|
8882
|
+
${import_picocolors29.default.bold("phases")}`);
|
|
8883
|
+
for (const p of progress) {
|
|
8884
|
+
const bar2 = p.total > 0 ? `${p.done}/${p.total} done` : import_picocolors29.default.dim("nothing filed yet");
|
|
8885
|
+
const mark = p.state === "done" ? import_picocolors29.default.green("done") : p.state === "working" ? import_picocolors29.default.blue("working") : import_picocolors29.default.dim("waiting");
|
|
8886
|
+
line(
|
|
8887
|
+
` ${import_picocolors29.default.dim(String(p.index).padStart(2, "0"))} ${p.name} ${mark} ${import_picocolors29.default.dim(bar2)}`
|
|
8888
|
+
);
|
|
8889
|
+
for (const c of p.criteria ?? []) {
|
|
8890
|
+
const tick = c.met === true ? import_picocolors29.default.green("\u2713") : c.met === false ? import_picocolors29.default.red("\u2717") : import_picocolors29.default.dim("\xB7");
|
|
8891
|
+
line(` ${tick} ${c.text} ${import_picocolors29.default.dim(c.id)}`);
|
|
8892
|
+
if (c.note) line(import_picocolors29.default.dim(` ${c.note}`));
|
|
8893
|
+
}
|
|
8894
|
+
}
|
|
8895
|
+
const current = progress.find((p) => p.name === g.currentPhase);
|
|
8896
|
+
if (current) {
|
|
8897
|
+
line(
|
|
8898
|
+
import_picocolors29.default.dim(
|
|
8899
|
+
`
|
|
8900
|
+
${current.name} \u2014 ${current.done} of ${current.total} done (phase ${current.index} of ${progress.length}).`
|
|
8901
|
+
)
|
|
8902
|
+
);
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
function statusTag3(status) {
|
|
8906
|
+
switch (status) {
|
|
8907
|
+
case "agreed":
|
|
8908
|
+
return import_picocolors29.default.cyan("[agreed]");
|
|
8909
|
+
case "working":
|
|
8910
|
+
return import_picocolors29.default.blue("[working]");
|
|
8911
|
+
case "delivered":
|
|
8912
|
+
return import_picocolors29.default.green("[delivered]");
|
|
8913
|
+
case "abandoned":
|
|
8914
|
+
return import_picocolors29.default.dim("[abandoned]");
|
|
8915
|
+
default:
|
|
8916
|
+
return import_picocolors29.default.yellow("[proposed]");
|
|
8917
|
+
}
|
|
8918
|
+
}
|
|
8919
|
+
|
|
8316
8920
|
// src/role-guard.ts
|
|
8317
8921
|
var READS = [
|
|
8922
|
+
// `requirement` sits beside `doc` and `decision` because it is the same kind
|
|
8923
|
+
// of thing and was simply forgotten: the verb shipped, no role could run it,
|
|
8924
|
+
// and on 2026-08-23 a channel PM reported to the owner that "requirements are
|
|
8925
|
+
// not readable by this PM role" — which was exactly true, for every role.
|
|
8318
8926
|
"task",
|
|
8319
8927
|
"board",
|
|
8320
8928
|
"doc",
|
|
8321
8929
|
"decision",
|
|
8930
|
+
"requirement",
|
|
8322
8931
|
"memory",
|
|
8323
8932
|
"search",
|
|
8324
8933
|
"verify",
|
|
@@ -8345,7 +8954,19 @@ var READS = [
|
|
|
8345
8954
|
// Reading the plan and what is used against it. An agent proposing "add
|
|
8346
8955
|
// another project" can only sensibly propose it if it can find out the plan
|
|
8347
8956
|
// allows two and two already exist.
|
|
8348
|
-
"usage"
|
|
8957
|
+
"usage",
|
|
8958
|
+
// RECORDING WHAT YOU PRODUCED IS NOT CHANGING THE PROJECT.
|
|
8959
|
+
// Every dispatched role is told, in its own preamble, to run `workser
|
|
8960
|
+
// artifact list` before non-trivial work and `workser artifact add` when it
|
|
8961
|
+
// finishes something the owner should get. Five roles — pm, qa, security,
|
|
8962
|
+
// analyst, sre — were then refused the verb here, so the instruction and the
|
|
8963
|
+
// guard contradicted each other: the agent kept trying to obey an order this
|
|
8964
|
+
// file would not let it obey, and burned its run doing it. It belongs beside
|
|
8965
|
+
// `doc`, `decision` and `board`, which are already in this list and also
|
|
8966
|
+
// have `create` subcommands — these write the project's RECORD, not the
|
|
8967
|
+
// project. What stops a reviewer editing code is its filesystem mode, and
|
|
8968
|
+
// that is untouched.
|
|
8969
|
+
"artifact"
|
|
8349
8970
|
];
|
|
8350
8971
|
var BUILDS = [
|
|
8351
8972
|
...READS,
|
|
@@ -8354,7 +8975,6 @@ var BUILDS = [
|
|
|
8354
8975
|
"db",
|
|
8355
8976
|
"storage",
|
|
8356
8977
|
"checkpoint",
|
|
8357
|
-
"artifact",
|
|
8358
8978
|
"image",
|
|
8359
8979
|
"design",
|
|
8360
8980
|
"ask",
|
|
@@ -8390,15 +9010,44 @@ var NEVER = {
|
|
|
8390
9010
|
// proposed has removed the only gate this product has.
|
|
8391
9011
|
"task approval": "Only the owner can approve a plan."
|
|
8392
9012
|
};
|
|
9013
|
+
var READ_PAIRS = /* @__PURE__ */ new Set([
|
|
9014
|
+
// The project's own brand and design notes.
|
|
9015
|
+
"design show",
|
|
9016
|
+
// What exists, and what it is wired to.
|
|
9017
|
+
"app list",
|
|
9018
|
+
"app tools",
|
|
9019
|
+
// Shape, never contents-by-arbitrary-SQL.
|
|
9020
|
+
"db list",
|
|
9021
|
+
"db tables",
|
|
9022
|
+
"db schema",
|
|
9023
|
+
// What is stored, not what is in it.
|
|
9024
|
+
"storage list",
|
|
9025
|
+
"storage ls",
|
|
9026
|
+
"checkpoint list",
|
|
9027
|
+
"workflow list",
|
|
9028
|
+
"workflow get",
|
|
9029
|
+
"workflow runs",
|
|
9030
|
+
"workflow nodes",
|
|
9031
|
+
"tool list",
|
|
9032
|
+
// The business data an analyst exists to read.
|
|
9033
|
+
"business resources",
|
|
9034
|
+
"business list",
|
|
9035
|
+
"business get",
|
|
9036
|
+
// The database service's own inventory.
|
|
9037
|
+
"neon status",
|
|
9038
|
+
"neon list"
|
|
9039
|
+
]);
|
|
8393
9040
|
function assertRoleMayRun(argv) {
|
|
8394
9041
|
const role = (process.env.WORKSER_ROLE ?? "").trim();
|
|
8395
9042
|
if (!role) return;
|
|
8396
|
-
const
|
|
9043
|
+
const commandArgv = stripLeadingGlobalOptions(argv);
|
|
9044
|
+
const verb = commandArgv[0];
|
|
8397
9045
|
if (!verb) return;
|
|
8398
|
-
const pair = `${
|
|
8399
|
-
if (NEVER[pair] && !(pair === "task approval" && (
|
|
9046
|
+
const pair = `${commandArgv[0]} ${commandArgv[1] ?? ""}`.trim();
|
|
9047
|
+
if (NEVER[pair] && !(pair === "task approval" && (commandArgv[2] === "request" || !commandArgv[2]))) {
|
|
8400
9048
|
throw new WorkserError(NEVER[pair], { code: "role_forbidden" });
|
|
8401
9049
|
}
|
|
9050
|
+
if (READ_PAIRS.has(pair)) return;
|
|
8402
9051
|
const allowed = ROLE_VERBS[role];
|
|
8403
9052
|
const list = allowed ?? READS;
|
|
8404
9053
|
if (!list.includes(verb)) {
|
|
@@ -8408,9 +9057,38 @@ function assertRoleMayRun(argv) {
|
|
|
8408
9057
|
);
|
|
8409
9058
|
}
|
|
8410
9059
|
}
|
|
9060
|
+
function stripLeadingGlobalOptions(argv) {
|
|
9061
|
+
const takesValue = /* @__PURE__ */ new Set([
|
|
9062
|
+
"-p",
|
|
9063
|
+
"--project",
|
|
9064
|
+
"-C",
|
|
9065
|
+
"--cwd",
|
|
9066
|
+
"--endpoint",
|
|
9067
|
+
"--token"
|
|
9068
|
+
]);
|
|
9069
|
+
const flags = /* @__PURE__ */ new Set(["--json", "-q", "--quiet", "-v", "--version"]);
|
|
9070
|
+
let index = 0;
|
|
9071
|
+
while (index < argv.length) {
|
|
9072
|
+
const token = argv[index];
|
|
9073
|
+
if (flags.has(token)) {
|
|
9074
|
+
index += 1;
|
|
9075
|
+
continue;
|
|
9076
|
+
}
|
|
9077
|
+
if (takesValue.has(token)) {
|
|
9078
|
+
index += 2;
|
|
9079
|
+
continue;
|
|
9080
|
+
}
|
|
9081
|
+
if (token.startsWith("--project=") || token.startsWith("--cwd=") || token.startsWith("--endpoint=") || token.startsWith("--token=")) {
|
|
9082
|
+
index += 1;
|
|
9083
|
+
continue;
|
|
9084
|
+
}
|
|
9085
|
+
break;
|
|
9086
|
+
}
|
|
9087
|
+
return argv.slice(index);
|
|
9088
|
+
}
|
|
8411
9089
|
|
|
8412
9090
|
// src/commands/decision.ts
|
|
8413
|
-
var
|
|
9091
|
+
var import_picocolors30 = __toESM(require_picocolors(), 1);
|
|
8414
9092
|
function registerDecision(program3) {
|
|
8415
9093
|
const decision = program3.command("decision").description("Read and record the project's architecture decisions");
|
|
8416
9094
|
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(
|
|
@@ -8423,11 +9101,11 @@ function registerDecision(program3) {
|
|
|
8423
9101
|
rows = applyLimit(rows, opts.limit);
|
|
8424
9102
|
ok(rows, () => {
|
|
8425
9103
|
if (!rows.length) {
|
|
8426
|
-
line(
|
|
9104
|
+
line(import_picocolors30.default.dim("No decisions recorded yet."));
|
|
8427
9105
|
return;
|
|
8428
9106
|
}
|
|
8429
9107
|
for (const r of rows) {
|
|
8430
|
-
line(`${
|
|
9108
|
+
line(`${import_picocolors30.default.dim(r.id)} ${import_picocolors30.default.dim(shortDate(r.createdAt))} ${r.title}`);
|
|
8431
9109
|
line(` ${truncate(r.decision, 100)}`);
|
|
8432
9110
|
}
|
|
8433
9111
|
});
|
|
@@ -8441,16 +9119,16 @@ function registerDecision(program3) {
|
|
|
8441
9119
|
`/v1/projects/${projectId}/architecture-decisions/${args[0]}`
|
|
8442
9120
|
);
|
|
8443
9121
|
ok(row, () => {
|
|
8444
|
-
line(`${
|
|
8445
|
-
line(
|
|
9122
|
+
line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
|
|
9123
|
+
line(import_picocolors30.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
|
|
8446
9124
|
line(`
|
|
8447
|
-
${
|
|
9125
|
+
${import_picocolors30.default.bold("Context")}
|
|
8448
9126
|
${row.context}`);
|
|
8449
9127
|
line(`
|
|
8450
|
-
${
|
|
9128
|
+
${import_picocolors30.default.bold("Decision")}
|
|
8451
9129
|
${row.decision}`);
|
|
8452
9130
|
if (row.consequences) line(`
|
|
8453
|
-
${
|
|
9131
|
+
${import_picocolors30.default.bold("Consequences")}
|
|
8454
9132
|
${row.consequences}`);
|
|
8455
9133
|
});
|
|
8456
9134
|
})
|
|
@@ -8479,7 +9157,7 @@ ${row.consequences}`);
|
|
|
8479
9157
|
refId: row?.id,
|
|
8480
9158
|
output: { decision: row }
|
|
8481
9159
|
});
|
|
8482
|
-
ok(row, () => line(`Recorded decision ${
|
|
9160
|
+
ok(row, () => line(`Recorded decision ${import_picocolors30.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
8483
9161
|
})
|
|
8484
9162
|
);
|
|
8485
9163
|
const requirement = program3.command("requirement").description("Read and record the project's requirements");
|
|
@@ -8491,11 +9169,11 @@ ${row.consequences}`);
|
|
|
8491
9169
|
rows = applyLimit(rows, opts.limit);
|
|
8492
9170
|
ok(rows, () => {
|
|
8493
9171
|
if (!rows.length) {
|
|
8494
|
-
line(
|
|
9172
|
+
line(import_picocolors30.default.dim("No requirements recorded yet."));
|
|
8495
9173
|
return;
|
|
8496
9174
|
}
|
|
8497
9175
|
for (const r of rows) {
|
|
8498
|
-
line(`${
|
|
9176
|
+
line(`${import_picocolors30.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
|
|
8499
9177
|
}
|
|
8500
9178
|
});
|
|
8501
9179
|
})
|
|
@@ -8508,8 +9186,8 @@ ${row.consequences}`);
|
|
|
8508
9186
|
`/v1/projects/${projectId}/requirements/${args[0]}`
|
|
8509
9187
|
);
|
|
8510
9188
|
ok(row, () => {
|
|
8511
|
-
line(`${
|
|
8512
|
-
line(
|
|
9189
|
+
line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
|
|
9190
|
+
line(import_picocolors30.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
|
|
8513
9191
|
line(`
|
|
8514
9192
|
${row.body}`);
|
|
8515
9193
|
});
|
|
@@ -8535,7 +9213,7 @@ ${row.body}`);
|
|
|
8535
9213
|
refId: row?.id,
|
|
8536
9214
|
output: { requirement: row }
|
|
8537
9215
|
});
|
|
8538
|
-
ok(row, () => line(`Recorded requirement ${
|
|
9216
|
+
ok(row, () => line(`Recorded requirement ${import_picocolors30.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
8539
9217
|
})
|
|
8540
9218
|
);
|
|
8541
9219
|
requirement.command("update <id>").description(
|
|
@@ -8564,7 +9242,7 @@ ${row.body}`);
|
|
|
8564
9242
|
code: "bad_request"
|
|
8565
9243
|
});
|
|
8566
9244
|
}
|
|
8567
|
-
ok(row, () => line(`Updated requirement ${
|
|
9245
|
+
ok(row, () => line(`Updated requirement ${import_picocolors30.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
|
|
8568
9246
|
})
|
|
8569
9247
|
);
|
|
8570
9248
|
}
|
|
@@ -8587,7 +9265,7 @@ function shortDate(iso) {
|
|
|
8587
9265
|
}
|
|
8588
9266
|
|
|
8589
9267
|
// src/commands/doc.ts
|
|
8590
|
-
var
|
|
9268
|
+
var import_picocolors31 = __toESM(require_picocolors(), 1);
|
|
8591
9269
|
|
|
8592
9270
|
// src/mermaid-fences.ts
|
|
8593
9271
|
function hasDiagram(code) {
|
|
@@ -8632,13 +9310,13 @@ function registerDoc(program3) {
|
|
|
8632
9310
|
}) ?? [];
|
|
8633
9311
|
ok(rows, () => {
|
|
8634
9312
|
if (!rows.length) {
|
|
8635
|
-
line(
|
|
9313
|
+
line(import_picocolors31.default.dim("No documents yet."));
|
|
8636
9314
|
return;
|
|
8637
9315
|
}
|
|
8638
9316
|
for (const r of rows) {
|
|
8639
|
-
const link = r.workItemId ?
|
|
8640
|
-
const file = r.filePath ?
|
|
8641
|
-
line(`${
|
|
9317
|
+
const link = r.workItemId ? import_picocolors31.default.dim(` \u21B3 ${r.workItemId}`) : "";
|
|
9318
|
+
const file = r.filePath ? import_picocolors31.default.dim(` ${r.filePath}`) : "";
|
|
9319
|
+
line(`${import_picocolors31.default.dim(r.id)} ${r.title}${link}${file}`);
|
|
8642
9320
|
}
|
|
8643
9321
|
});
|
|
8644
9322
|
})
|
|
@@ -8652,17 +9330,17 @@ function registerDoc(program3) {
|
|
|
8652
9330
|
);
|
|
8653
9331
|
if (opts.markdown) {
|
|
8654
9332
|
ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
|
|
8655
|
-
line(`${
|
|
9333
|
+
line(`${import_picocolors31.default.bold(row.title)} ${import_picocolors31.default.dim(row.id)}`);
|
|
8656
9334
|
line(
|
|
8657
|
-
row.filePath ? `Read it at ${
|
|
9335
|
+
row.filePath ? `Read it at ${import_picocolors31.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors31.default.dim("This document has no markdown mirror on disk yet.")
|
|
8658
9336
|
);
|
|
8659
9337
|
});
|
|
8660
9338
|
return;
|
|
8661
9339
|
}
|
|
8662
9340
|
ok(row, () => {
|
|
8663
|
-
line(`${
|
|
8664
|
-
if (row.workItemId) line(
|
|
8665
|
-
if (row.filePath) line(
|
|
9341
|
+
line(`${import_picocolors31.default.bold(row.title)} ${import_picocolors31.default.dim(row.id)}`);
|
|
9342
|
+
if (row.workItemId) line(import_picocolors31.default.dim(`linked to work item ${row.workItemId}`));
|
|
9343
|
+
if (row.filePath) line(import_picocolors31.default.dim(`markdown mirror: ${row.filePath}`));
|
|
8666
9344
|
line("");
|
|
8667
9345
|
line(row.contentJson);
|
|
8668
9346
|
});
|
|
@@ -8691,7 +9369,7 @@ function registerDoc(program3) {
|
|
|
8691
9369
|
refId: row?.id,
|
|
8692
9370
|
output: { document: row }
|
|
8693
9371
|
});
|
|
8694
|
-
ok(row, () => line(`Created document ${
|
|
9372
|
+
ok(row, () => line(`Created document ${import_picocolors31.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
8695
9373
|
})
|
|
8696
9374
|
);
|
|
8697
9375
|
doc.command("diagram <id>").description(
|
|
@@ -8717,19 +9395,19 @@ function registerDoc(program3) {
|
|
|
8717
9395
|
diagrams: diagrams.map((code) => ({ kind: diagramKind(code), code }))
|
|
8718
9396
|
};
|
|
8719
9397
|
ok(payload, () => {
|
|
8720
|
-
line(`${
|
|
9398
|
+
line(`${import_picocolors31.default.bold(row.title)} ${import_picocolors31.default.dim(row.id)}`);
|
|
8721
9399
|
if (markdown === null) {
|
|
8722
9400
|
line(
|
|
8723
|
-
|
|
9401
|
+
import_picocolors31.default.dim(
|
|
8724
9402
|
row.filePath ? `Could not read ${row.filePath} from this folder.` : "This document has no markdown mirror on disk yet."
|
|
8725
9403
|
)
|
|
8726
9404
|
);
|
|
8727
9405
|
} else if (!diagrams.length) {
|
|
8728
|
-
line(
|
|
9406
|
+
line(import_picocolors31.default.dim("No diagrams in this document."));
|
|
8729
9407
|
} else {
|
|
8730
9408
|
for (const [i, code] of diagrams.entries()) {
|
|
8731
9409
|
const kind = diagramKind(code) ?? "diagram";
|
|
8732
|
-
line(`${
|
|
9410
|
+
line(`${import_picocolors31.default.dim(String(i + 1))} ${kind} ${import_picocolors31.default.dim(`${code.split("\n").length} lines`)}`);
|
|
8733
9411
|
}
|
|
8734
9412
|
}
|
|
8735
9413
|
});
|
|
@@ -8765,7 +9443,7 @@ function registerDoc(program3) {
|
|
|
8765
9443
|
code: "bad_request"
|
|
8766
9444
|
});
|
|
8767
9445
|
}
|
|
8768
|
-
ok(row, () => line(`Updated document ${
|
|
9446
|
+
ok(row, () => line(`Updated document ${import_picocolors31.default.bold(row.id)} \u2014 ${row.title}`));
|
|
8769
9447
|
})
|
|
8770
9448
|
);
|
|
8771
9449
|
}
|
|
@@ -8779,7 +9457,7 @@ function readMirror(cwd, filePath) {
|
|
|
8779
9457
|
}
|
|
8780
9458
|
|
|
8781
9459
|
// src/commands/design.ts
|
|
8782
|
-
var
|
|
9460
|
+
var import_picocolors32 = __toESM(require_picocolors(), 1);
|
|
8783
9461
|
function registerDesign(program3) {
|
|
8784
9462
|
const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
|
|
8785
9463
|
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(
|
|
@@ -8793,11 +9471,11 @@ function registerDesign(program3) {
|
|
|
8793
9471
|
if (opts.raw) {
|
|
8794
9472
|
ok(files, () => {
|
|
8795
9473
|
if (!files.length) {
|
|
8796
|
-
line(
|
|
9474
|
+
line(import_picocolors32.default.dim("No brand set for this project."));
|
|
8797
9475
|
return;
|
|
8798
9476
|
}
|
|
8799
9477
|
for (const f of files) {
|
|
8800
|
-
line(
|
|
9478
|
+
line(import_picocolors32.default.bold(f.path));
|
|
8801
9479
|
line(f.contents);
|
|
8802
9480
|
line("");
|
|
8803
9481
|
}
|
|
@@ -8814,21 +9492,21 @@ function registerDesign(program3) {
|
|
|
8814
9492
|
} : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
|
|
8815
9493
|
ok(summary, () => {
|
|
8816
9494
|
if (!tokens) {
|
|
8817
|
-
line(
|
|
9495
|
+
line(import_picocolors32.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
|
|
8818
9496
|
return;
|
|
8819
9497
|
}
|
|
8820
9498
|
for (const [name, value] of Object.entries(tokens.brand)) {
|
|
8821
|
-
line(`${
|
|
9499
|
+
line(`${import_picocolors32.default.dim(name.padEnd(12))} ${value}`);
|
|
8822
9500
|
}
|
|
8823
9501
|
for (const [name, value] of Object.entries(tokens.color)) {
|
|
8824
|
-
line(`${
|
|
9502
|
+
line(`${import_picocolors32.default.dim(`color.${name}`.padEnd(12))} ${value}`);
|
|
8825
9503
|
}
|
|
8826
9504
|
for (const [name, value] of Object.entries(tokens.font)) {
|
|
8827
|
-
line(`${
|
|
9505
|
+
line(`${import_picocolors32.default.dim(`font.${name}`.padEnd(12))} ${value}`);
|
|
8828
9506
|
}
|
|
8829
9507
|
line("");
|
|
8830
9508
|
line(
|
|
8831
|
-
|
|
9509
|
+
import_picocolors32.default.dim(
|
|
8832
9510
|
`Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
|
|
8833
9511
|
)
|
|
8834
9512
|
);
|
|
@@ -8859,7 +9537,7 @@ function unwrap(group) {
|
|
|
8859
9537
|
}
|
|
8860
9538
|
|
|
8861
9539
|
// src/commands/api.ts
|
|
8862
|
-
var
|
|
9540
|
+
var import_picocolors33 = __toESM(require_picocolors(), 1);
|
|
8863
9541
|
import { readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
8864
9542
|
import { join as join3, relative, sep } from "path";
|
|
8865
9543
|
|
|
@@ -8973,10 +9651,10 @@ function registerApi(program3) {
|
|
|
8973
9651
|
const appId = requireApp(opts.app);
|
|
8974
9652
|
const res = await api(ctx, `/v1/apps/${encodeURIComponent(appId)}/api/requests`);
|
|
8975
9653
|
ok(res, () => {
|
|
8976
|
-
for (const note of res?.notes ?? []) line(
|
|
9654
|
+
for (const note of res?.notes ?? []) line(import_picocolors33.default.dim(note));
|
|
8977
9655
|
for (const r of res?.requests ?? []) {
|
|
8978
9656
|
line(
|
|
8979
|
-
`${
|
|
9657
|
+
`${import_picocolors33.default.dim(r.method.padEnd(6))}${r.path}${r.note ? import_picocolors33.default.dim(` ${r.note}`) : ""}`
|
|
8980
9658
|
);
|
|
8981
9659
|
}
|
|
8982
9660
|
});
|
|
@@ -9010,14 +9688,14 @@ function registerApi(program3) {
|
|
|
9010
9688
|
);
|
|
9011
9689
|
ok(res, () => {
|
|
9012
9690
|
if (!res?.ok) {
|
|
9013
|
-
line(
|
|
9691
|
+
line(import_picocolors33.default.red(res?.error ?? "The service did not answer."));
|
|
9014
9692
|
return;
|
|
9015
9693
|
}
|
|
9016
9694
|
const code = `${res.status}${res.statusText ? ` ${res.statusText}` : ""}`;
|
|
9017
|
-
const colour2 = res.status && res.status < 300 ?
|
|
9018
|
-
line(`${colour2(code)} ${
|
|
9695
|
+
const colour2 = res.status && res.status < 300 ? import_picocolors33.default.green : res.status && res.status < 500 ? import_picocolors33.default.yellow : import_picocolors33.default.red;
|
|
9696
|
+
line(`${colour2(code)} ${import_picocolors33.default.dim(`${res.durationMs}ms ${res.url}`)}`);
|
|
9019
9697
|
if (res.body) line(res.body);
|
|
9020
|
-
if (res.truncated) line(
|
|
9698
|
+
if (res.truncated) line(import_picocolors33.default.dim("(answer truncated)"));
|
|
9021
9699
|
});
|
|
9022
9700
|
if (!res?.ok) process.exitCode = 1;
|
|
9023
9701
|
})
|
|
@@ -9037,15 +9715,15 @@ function registerApi(program3) {
|
|
|
9037
9715
|
for (const r of report.routes) {
|
|
9038
9716
|
const known = report.missing.some((m) => m.path === r.path);
|
|
9039
9717
|
line(
|
|
9040
|
-
`${known ?
|
|
9718
|
+
`${known ? import_picocolors33.default.yellow("undocumented") : import_picocolors33.default.green("documented ")} ${r.path}${import_picocolors33.default.dim(` ${r.file}`)}`
|
|
9041
9719
|
);
|
|
9042
9720
|
}
|
|
9043
9721
|
for (const p of report.stale) {
|
|
9044
|
-
line(`${
|
|
9722
|
+
line(`${import_picocolors33.default.dim("in spec only ")} ${p}`);
|
|
9045
9723
|
}
|
|
9046
9724
|
line("");
|
|
9047
9725
|
if (report.ok && specFile) success(summary);
|
|
9048
|
-
else line(
|
|
9726
|
+
else line(import_picocolors33.default.yellow(summary));
|
|
9049
9727
|
});
|
|
9050
9728
|
if (opts.check && !report.ok) {
|
|
9051
9729
|
throw new WorkserError(summary, { code: "bad_request" });
|
|
@@ -9116,7 +9794,7 @@ function listRepoFiles(root, maxDepth = 8) {
|
|
|
9116
9794
|
}
|
|
9117
9795
|
|
|
9118
9796
|
// src/commands/analysis.ts
|
|
9119
|
-
var
|
|
9797
|
+
var import_picocolors34 = __toESM(require_picocolors(), 1);
|
|
9120
9798
|
import { readFileSync as readFileSync4 } from "fs";
|
|
9121
9799
|
function registerAnalysis(program3) {
|
|
9122
9800
|
const cmd = program3.command("analysis").description("Run Python analysis locally, recorded in the task");
|
|
@@ -9126,14 +9804,14 @@ function registerAnalysis(program3) {
|
|
|
9126
9804
|
const res = await api(ctx, path);
|
|
9127
9805
|
ok(res, () => {
|
|
9128
9806
|
line(
|
|
9129
|
-
`${res?.available ?
|
|
9807
|
+
`${res?.available ? import_picocolors34.default.green("python") : import_picocolors34.default.red("python")} ${res?.version ?? "not found"} ${import_picocolors34.default.dim(res?.python ?? "")}`
|
|
9130
9808
|
);
|
|
9131
9809
|
for (const lib of res?.libraries ?? []) {
|
|
9132
9810
|
line(
|
|
9133
|
-
`${lib.present ?
|
|
9811
|
+
`${lib.present ? import_picocolors34.default.green(lib.name) : import_picocolors34.default.yellow(lib.name)}${import_picocolors34.default.dim(lib.present ? "" : " missing")}`
|
|
9134
9812
|
);
|
|
9135
9813
|
}
|
|
9136
|
-
for (const note of res?.notes ?? []) line(
|
|
9814
|
+
for (const note of res?.notes ?? []) line(import_picocolors34.default.dim(note));
|
|
9137
9815
|
});
|
|
9138
9816
|
if (!res?.available) process.exitCode = 1;
|
|
9139
9817
|
})
|
|
@@ -9155,15 +9833,15 @@ function registerAnalysis(program3) {
|
|
|
9155
9833
|
);
|
|
9156
9834
|
ok(res, () => {
|
|
9157
9835
|
if (res?.stdout) line(res.stdout.replace(/\n$/, ""));
|
|
9158
|
-
if (res?.stderr) line(
|
|
9159
|
-
if (res?.truncated) line(
|
|
9836
|
+
if (res?.stderr) line(import_picocolors34.default.dim(res.stderr.replace(/\n$/, "")));
|
|
9837
|
+
if (res?.truncated) line(import_picocolors34.default.dim("(output truncated)"));
|
|
9160
9838
|
const took = `${Math.round((res?.durationMs ?? 0) / 100) / 10}s`;
|
|
9161
9839
|
line(
|
|
9162
|
-
res?.ok ?
|
|
9840
|
+
res?.ok ? import_picocolors34.default.green(`\u2713 ${res.summary ?? "It finished."}`) + import_picocolors34.default.dim(` ${took}`) : import_picocolors34.default.yellow(res?.summary ?? "It did not finish.") + import_picocolors34.default.dim(` ${took}`)
|
|
9163
9841
|
);
|
|
9164
9842
|
if (res && !res.sandboxed) {
|
|
9165
9843
|
line(
|
|
9166
|
-
|
|
9844
|
+
import_picocolors34.default.dim(
|
|
9167
9845
|
"This platform has no OS sandbox, so the script ran with your own file access."
|
|
9168
9846
|
)
|
|
9169
9847
|
);
|
|
@@ -9191,7 +9869,7 @@ function readCode(file, inline) {
|
|
|
9191
9869
|
}
|
|
9192
9870
|
|
|
9193
9871
|
// src/commands/scan.ts
|
|
9194
|
-
var
|
|
9872
|
+
var import_picocolors35 = __toESM(require_picocolors(), 1);
|
|
9195
9873
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
9196
9874
|
import { existsSync as existsSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
9197
9875
|
import { join as join4, relative as relative2, sep as sep2 } from "path";
|
|
@@ -9475,22 +10153,22 @@ function runPermissions(cwd, findings, checked, skipped) {
|
|
|
9475
10153
|
}
|
|
9476
10154
|
function print2(report, summary) {
|
|
9477
10155
|
for (const s of report.skipped) {
|
|
9478
|
-
line(`${
|
|
10156
|
+
line(`${import_picocolors35.default.yellow("not checked")} ${s.check}${import_picocolors35.default.dim(` \u2014 ${s.reason}`)}`);
|
|
9479
10157
|
}
|
|
9480
10158
|
for (const f of report.findings) {
|
|
9481
|
-
const where = f.file ?
|
|
10159
|
+
const where = f.file ? import_picocolors35.default.dim(` ${f.file}${f.line ? `:${f.line}` : ""}`) : "";
|
|
9482
10160
|
line(`${severityTag(f.severity)} ${f.title}${where}`);
|
|
9483
|
-
line(` ${
|
|
10161
|
+
line(` ${import_picocolors35.default.dim(f.fix)}`);
|
|
9484
10162
|
}
|
|
9485
10163
|
if (report.findings.length || report.skipped.length) line("");
|
|
9486
10164
|
if (report.ok && !report.skipped.length) success(summary);
|
|
9487
|
-
else if (report.ok) line(
|
|
9488
|
-
else line(
|
|
10165
|
+
else if (report.ok) line(import_picocolors35.default.yellow(summary));
|
|
10166
|
+
else line(import_picocolors35.default.red(summary));
|
|
9489
10167
|
}
|
|
9490
10168
|
function severityTag(severity) {
|
|
9491
|
-
if (severity === "high") return
|
|
9492
|
-
if (severity === "medium") return
|
|
9493
|
-
return
|
|
10169
|
+
if (severity === "high") return import_picocolors35.default.red("serious ");
|
|
10170
|
+
if (severity === "medium") return import_picocolors35.default.yellow("worth fixing");
|
|
10171
|
+
return import_picocolors35.default.dim("minor ");
|
|
9494
10172
|
}
|
|
9495
10173
|
function git(cwd, args) {
|
|
9496
10174
|
try {
|
|
@@ -9551,7 +10229,7 @@ function listRepoFiles2(root, maxDepth = 8) {
|
|
|
9551
10229
|
}
|
|
9552
10230
|
|
|
9553
10231
|
// src/commands/health.ts
|
|
9554
|
-
var
|
|
10232
|
+
var import_picocolors36 = __toESM(require_picocolors(), 1);
|
|
9555
10233
|
function registerHealth(program3) {
|
|
9556
10234
|
program3.command("health").description("Check that the published apps in this project are still answering").option("--app <webAppId>", "check one app rather than all of them").action(
|
|
9557
10235
|
action(async ({ ctx, opts }) => {
|
|
@@ -9565,16 +10243,16 @@ function registerHealth(program3) {
|
|
|
9565
10243
|
}
|
|
9566
10244
|
function print3(res) {
|
|
9567
10245
|
if (!res?.checks?.length) {
|
|
9568
|
-
line(
|
|
10246
|
+
line(import_picocolors36.default.dim(res?.note ?? "Nothing to check."));
|
|
9569
10247
|
return;
|
|
9570
10248
|
}
|
|
9571
10249
|
for (const c of res.checks) {
|
|
9572
|
-
const mark = c.ok ?
|
|
9573
|
-
const timing =
|
|
9574
|
-
const detail = c.ok ? timing :
|
|
9575
|
-
line(` ${mark} ${c.appName} ${
|
|
10250
|
+
const mark = c.ok ? import_picocolors36.default.green("up ") : import_picocolors36.default.red("down");
|
|
10251
|
+
const timing = import_picocolors36.default.dim(`${c.ms}ms`);
|
|
10252
|
+
const detail = c.ok ? timing : import_picocolors36.default.dim(`${c.error ?? "no answer"}${c.failures > 1 ? ` \xB7 ${c.failures} in a row` : ""}`);
|
|
10253
|
+
line(` ${mark} ${c.appName} ${import_picocolors36.default.dim(`(${c.environment})`)} ${c.url} ${detail}`);
|
|
9576
10254
|
if (c.incidentOpened) {
|
|
9577
|
-
line(
|
|
10255
|
+
line(import_picocolors36.default.yellow(` An incident has been opened on the board for this.`));
|
|
9578
10256
|
}
|
|
9579
10257
|
}
|
|
9580
10258
|
const down = res.checks.filter((c) => !c.ok);
|
|
@@ -9587,14 +10265,14 @@ function print3(res) {
|
|
|
9587
10265
|
}
|
|
9588
10266
|
const production = down.filter((c) => c.environment === "production").length;
|
|
9589
10267
|
line(
|
|
9590
|
-
|
|
10268
|
+
import_picocolors36.default.red(
|
|
9591
10269
|
`${down.length} of ${res.checks.length} not answering` + (production ? ` \u2014 ${production} customer-facing.` : " (preview only).")
|
|
9592
10270
|
)
|
|
9593
10271
|
);
|
|
9594
10272
|
}
|
|
9595
10273
|
|
|
9596
10274
|
// src/commands/urls.ts
|
|
9597
|
-
var
|
|
10275
|
+
var import_picocolors37 = __toESM(require_picocolors(), 1);
|
|
9598
10276
|
function registerUrls(program3) {
|
|
9599
10277
|
program3.command("urls").description("The stable preview and production addresses of every app in this project").option("--app <webAppId>", "just one app").action(
|
|
9600
10278
|
action(async ({ ctx, opts }) => {
|
|
@@ -9608,20 +10286,20 @@ function registerUrls(program3) {
|
|
|
9608
10286
|
const summary = urlsSummary(rows);
|
|
9609
10287
|
ok({ rows, summary }, () => {
|
|
9610
10288
|
for (const row of rows) {
|
|
9611
|
-
const label =
|
|
9612
|
-
const value = row.url ?
|
|
10289
|
+
const label = import_picocolors37.default.dim(row.environment.padEnd(10));
|
|
10290
|
+
const value = row.url ? import_picocolors37.default.cyan(row.url) : import_picocolors37.default.dim(row.note ?? "not published");
|
|
9613
10291
|
line(` ${row.appName.padEnd(22)} ${label} ${value}`);
|
|
9614
10292
|
}
|
|
9615
10293
|
line("");
|
|
9616
10294
|
if (rows.some((r) => r.url)) success(summary);
|
|
9617
|
-
else line(
|
|
10295
|
+
else line(import_picocolors37.default.yellow(summary));
|
|
9618
10296
|
});
|
|
9619
10297
|
})
|
|
9620
10298
|
);
|
|
9621
10299
|
}
|
|
9622
10300
|
|
|
9623
10301
|
// src/commands/deployments.ts
|
|
9624
|
-
var
|
|
10302
|
+
var import_picocolors38 = __toESM(require_picocolors(), 1);
|
|
9625
10303
|
function registerDeployments(program3) {
|
|
9626
10304
|
const cmd = program3.command("deployments").description("Deployment history, and putting a build in front of customers");
|
|
9627
10305
|
cmd.command("list").description("What has been built, newest first").option("--app <webAppId>", "just one app (default: every app in the project)").option("--env <environment>", "preview or production").option("--limit <n>", "how many to show", "20").action(
|
|
@@ -9639,7 +10317,7 @@ function registerDeployments(program3) {
|
|
|
9639
10317
|
ok(res, () => {
|
|
9640
10318
|
if (!items.length) {
|
|
9641
10319
|
return line(
|
|
9642
|
-
|
|
10320
|
+
import_picocolors38.default.dim(
|
|
9643
10321
|
environment ? `Nothing has been deployed to ${environment} yet.` : "Nothing has been deployed yet. `workser deploy` builds the first one."
|
|
9644
10322
|
)
|
|
9645
10323
|
);
|
|
@@ -9659,13 +10337,13 @@ function registerDeployments(program3) {
|
|
|
9659
10337
|
).catch(() => null) : null;
|
|
9660
10338
|
ok({ ...dep, logs }, () => {
|
|
9661
10339
|
line(formatDeployment(dep));
|
|
9662
|
-
if (dep?.error_message) line(
|
|
10340
|
+
if (dep?.error_message) line(import_picocolors38.default.red(` ${dep.error_message}`));
|
|
9663
10341
|
const events = logs?.events ?? [];
|
|
9664
10342
|
for (const e of events) {
|
|
9665
|
-
line(` ${
|
|
10343
|
+
line(` ${import_picocolors38.default.dim(String(e.type ?? "log"))} ${e.text ?? ""}`);
|
|
9666
10344
|
}
|
|
9667
10345
|
if (opts.logs && !events.length) {
|
|
9668
|
-
line(
|
|
10346
|
+
line(import_picocolors38.default.dim(" That build produced no output."));
|
|
9669
10347
|
}
|
|
9670
10348
|
});
|
|
9671
10349
|
})
|
|
@@ -9709,16 +10387,16 @@ function printPromoted(res, version) {
|
|
|
9709
10387
|
const what = version === null ? "the latest build" : `version ${version}`;
|
|
9710
10388
|
const url = res.url ?? res.vercel_url;
|
|
9711
10389
|
success(`Production is being rebuilt from ${what}.`);
|
|
9712
|
-
if (url) line(
|
|
9713
|
-
line(
|
|
10390
|
+
if (url) line(import_picocolors38.default.dim(`It will be at ${url}`));
|
|
10391
|
+
line(import_picocolors38.default.dim("`workser deploy status` follows it."));
|
|
9714
10392
|
}
|
|
9715
10393
|
function formatDeployment(d) {
|
|
9716
10394
|
if (!d) return "";
|
|
9717
|
-
const version = d.version !== void 0 ?
|
|
9718
|
-
const env =
|
|
10395
|
+
const version = d.version !== void 0 ? import_picocolors38.default.yellow(`v${d.version}`) : import_picocolors38.default.dim("v?");
|
|
10396
|
+
const env = import_picocolors38.default.dim((d.environment ?? "?").padEnd(10));
|
|
9719
10397
|
const app = d.webAppName ? `${d.webAppName} ` : "";
|
|
9720
|
-
const when =
|
|
9721
|
-
const url = d.url ? " " +
|
|
10398
|
+
const when = import_picocolors38.default.dim(formatTime2(d.created_at));
|
|
10399
|
+
const url = d.url ? " " + import_picocolors38.default.cyan(d.url) : "";
|
|
9722
10400
|
return `${version} ${env} ${colorStatus(d.status ?? "")} ${app}${when}${url}`;
|
|
9723
10401
|
}
|
|
9724
10402
|
function formatTime2(t) {
|
|
@@ -9728,7 +10406,7 @@ function formatTime2(t) {
|
|
|
9728
10406
|
}
|
|
9729
10407
|
|
|
9730
10408
|
// src/commands/usage.ts
|
|
9731
|
-
var
|
|
10409
|
+
var import_picocolors39 = __toESM(require_picocolors(), 1);
|
|
9732
10410
|
|
|
9733
10411
|
// src/usage.ts
|
|
9734
10412
|
var NEAR_LIMIT_FRACTION = 0.8;
|
|
@@ -9823,7 +10501,7 @@ function registerUsage(program3) {
|
|
|
9823
10501
|
function print4(report) {
|
|
9824
10502
|
const dims = report.dimensions ?? [];
|
|
9825
10503
|
if (!dims.length) {
|
|
9826
|
-
return line(
|
|
10504
|
+
return line(import_picocolors39.default.dim("Nothing to measure for this project yet."));
|
|
9827
10505
|
}
|
|
9828
10506
|
const width = Math.max(...dims.map((d) => d.label.length));
|
|
9829
10507
|
for (const d of dims) {
|
|
@@ -9832,23 +10510,23 @@ function print4(report) {
|
|
|
9832
10510
|
line("");
|
|
9833
10511
|
const summary = usageSummary(report);
|
|
9834
10512
|
const worst = dims.map(usageState);
|
|
9835
|
-
if (worst.includes("over")) line(
|
|
10513
|
+
if (worst.includes("over")) line(import_picocolors39.default.red(summary));
|
|
9836
10514
|
else if (worst.includes("near") || worst.includes("unknown"))
|
|
9837
|
-
line(
|
|
10515
|
+
line(import_picocolors39.default.yellow(summary));
|
|
9838
10516
|
else success(summary);
|
|
9839
10517
|
}
|
|
9840
10518
|
function gauge(d, _labelWidth) {
|
|
9841
10519
|
const drawn = bar(d);
|
|
9842
|
-
return drawn ? ` ${
|
|
10520
|
+
return drawn ? ` ${import_picocolors39.default.dim(drawn)}` : "";
|
|
9843
10521
|
}
|
|
9844
10522
|
function colour(d) {
|
|
9845
10523
|
switch (usageState(d)) {
|
|
9846
10524
|
case "over":
|
|
9847
|
-
return d.kind === "hard" ?
|
|
10525
|
+
return d.kind === "hard" ? import_picocolors39.default.red : import_picocolors39.default.yellow;
|
|
9848
10526
|
case "near":
|
|
9849
|
-
return
|
|
10527
|
+
return import_picocolors39.default.yellow;
|
|
9850
10528
|
case "unknown":
|
|
9851
|
-
return
|
|
10529
|
+
return import_picocolors39.default.dim;
|
|
9852
10530
|
default:
|
|
9853
10531
|
return (s) => s;
|
|
9854
10532
|
}
|
|
@@ -9856,7 +10534,7 @@ function colour(d) {
|
|
|
9856
10534
|
|
|
9857
10535
|
// src/index.ts
|
|
9858
10536
|
var pkg = {
|
|
9859
|
-
version: true ? "0.6.
|
|
10537
|
+
version: true ? "0.6.2" : "0.0.0-dev"
|
|
9860
10538
|
};
|
|
9861
10539
|
var program2 = new Command();
|
|
9862
10540
|
program2.name("workser").description(
|
|
@@ -9909,9 +10587,14 @@ registerAsk(program2);
|
|
|
9909
10587
|
registerSearch(program2);
|
|
9910
10588
|
registerBoard(program2);
|
|
9911
10589
|
registerTask(program2);
|
|
10590
|
+
registerGoal(program2);
|
|
9912
10591
|
registerDecision(program2);
|
|
9913
10592
|
registerDoc(program2);
|
|
9914
10593
|
registerDesign(program2);
|
|
10594
|
+
configureOutput({
|
|
10595
|
+
json: process.argv.includes("--json"),
|
|
10596
|
+
quiet: process.argv.includes("--quiet") || process.argv.includes("-q")
|
|
10597
|
+
});
|
|
9915
10598
|
try {
|
|
9916
10599
|
assertRoleMayRun(process.argv.slice(2));
|
|
9917
10600
|
} catch (e) {
|