agent-yes 1.226.1 → 1.227.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{SUPPORTED_CLIS-xM8S6xqo.js → SUPPORTED_CLIS-CFd1V2O5.js} +2 -2
- package/dist/SUPPORTED_CLIS-CJiLdm01.js +11 -0
- package/dist/{agent-yes.config-DmW_TVFI.js → agent-yes.config-0G11aPWe.js} +2 -2
- package/dist/{agentShare-DSYm87vH.js → agentShare-C_rydTfo.js} +7 -7
- package/dist/{callback-DlzoC3fU.js → callback-BV3AhWeB.js} +5 -5
- package/dist/callback-BgWZmpqC.js +12 -0
- package/dist/{callbackCore-DSxbO_6a.js → callbackCore-B3DNe_SP.js} +1 -1
- package/dist/{callbackCore-CgnOT1Cp.js → callbackCore-BlNdDILy.js} +1 -1
- package/dist/cli.js +9 -9
- package/dist/{configShared-nnIwDYY-.js → configShared-YKfS6NMe.js} +1 -1
- package/dist/{cwdConflictWarn-CnxkLs7T.js → cwdConflictWarn-CJZRp4S0.js} +1 -1
- package/dist/{e2e-D5jaSHyY.js → e2e-DB7BDdEA.js} +1 -1
- package/dist/{expose-CKWXAkzK.js → expose-GWRXTGpj.js} +1 -1
- package/dist/{forkNested-DJ2QzBsc.js → forkNested-BWdv2mXC.js} +1 -1
- package/dist/index.js +3 -4
- package/dist/{nodeRuntime-CR0NwnnO.js → nodeRuntime-CgXWHe9p.js} +1 -1
- package/dist/{notifyDaemon-4qxS_FjO.js → notifyDaemon-CyLvaAwc.js} +6 -6
- package/dist/{openBrowser-D9VVE6cq.js → openBrowser-CSfI8zCG.js} +1 -1
- package/dist/{remotes-DZ_G2wkM.js → remotes-BCgowsiu.js} +3 -3
- package/dist/{remotes-BtXom7Rp.js → remotes-ZqD0FHl_.js} +3 -3
- package/dist/{rustBinary-KhrcXvB6.js → rustBinary-DHgMI1uK.js} +2 -2
- package/dist/{schedule-HHBT6eGE.js → schedule-0p4V_khb.js} +7 -7
- package/dist/{serve-BeHHkDIn.js → serve-C2KCUxTP.js} +29 -29
- package/dist/{setup-nAQJffmr.js → setup-CCKS4rtW.js} +3 -3
- package/dist/{share-BwEUEHfy.js → share-CrvxZO7o.js} +2 -2
- package/dist/{share-B7VP4_UF.js → share-ieE7D0C1.js} +2 -2
- package/dist/{spawnGate-CfcpmQmk.js → spawnGate-CIC7b7nm.js} +1 -1
- package/dist/{spawnGate-ChYCj6Dm.js → spawnGate-CeFYH-Ib.js} +2 -2
- package/dist/{subcommands-DYeJJ2Rh.js → subcommands-CsVEI376.js} +5 -5
- package/dist/{subcommands-D8zJxifk.js → subcommands-FTkZkqKy.js} +15 -15
- package/dist/{todoAutomation-fxtfT_44.js → todoAutomation-Cwnh4aWo.js} +300 -4
- package/dist/todoCli-6v1CYdxN.js +427 -0
- package/dist/{tray-BZqJtEmL.js → tray-DBt8X-Qj.js} +1 -1
- package/dist/{ts-yPc1x2-7.js → ts-CT75CpeM.js} +3 -3
- package/dist/{versionChecker-8h_rjT-C.js → versionChecker-Bd09mqkZ.js} +2 -2
- package/dist/{webrtcLink-DknpJQ0y.js → webrtcLink-CjckgIZe.js} +2 -2
- package/dist/{webrtcRemote-C974zjSt.js → webrtcRemote-BMzEt9Cp.js} +3 -3
- package/dist/{workspaceConfig-BRl-GwSN.js → workspaceConfig-De5tgjeO.js} +1 -1
- package/dist/{ws-YCNt3rNz.js → ws-B4zHUwc6.js} +7 -7
- package/package.json +1 -1
- package/ts/todoCli.spec.ts +80 -5
- package/ts/todoCli.ts +471 -336
- package/ts/todoStore.spec.ts +58 -0
- package/ts/todoStore.ts +42 -0
- package/dist/SUPPORTED_CLIS-C5vBin1r.js +0 -11
- package/dist/callback-BmgFE-2j.js +0 -12
- package/dist/todoCli-D8bL164c.js +0 -336
- package/dist/todoDigest-DSTG9ksk.js +0 -284
- package/dist/todoDigest-gKP1XibO.js +0 -3
package/ts/todoCli.ts
CHANGED
|
@@ -15,25 +15,34 @@
|
|
|
15
15
|
* invoking with an explicit `--root`, or by calling `openStore()` as a
|
|
16
16
|
* library from its own code instead of through this CLI.
|
|
17
17
|
*
|
|
18
|
-
* Argument parsing:
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
18
|
+
* Argument parsing: a real yargs COMMAND TREE — one `yargs(argv)` instance
|
|
19
|
+
* with a `CommandModule` per verb registered via `.command()`, `--root`/
|
|
20
|
+
* `--format` declared once as `global: true` options on that same instance —
|
|
21
|
+
* matching the pattern `tools/symval-dev-cli`'s `sv` CLI already uses
|
|
22
|
+
* (`todoCommand`/`lsCmd` etc. in that repo's `commands/todo.ts`), per taku's
|
|
23
|
+
* explicit feedback that this gives better help behavior for free: a real
|
|
24
|
+
* `.command()` tree yields an auto-generated `ay todo --help` listing every
|
|
25
|
+
* verb with its description, and per-verb `--help`/usage, neither of which
|
|
26
|
+
* a hand-rolled switch dispatch provides out of the box.
|
|
27
|
+
*
|
|
28
|
+
* An EARLIER version of this file used a hand-rolled switch with a separate
|
|
29
|
+
* `yargs(args)` parse PER verb (declaring `--root`/`--format` on each one via
|
|
30
|
+
* a shared `commonOptions()` helper) specifically to avoid an even earlier
|
|
31
|
+
* bug where a single outer parse swallowed every verb's own flags. That
|
|
32
|
+
* workaround is no longer needed: the swallowing bug was a property of an
|
|
33
|
+
* outer parse with NO knowledge of subcommand-specific options, not of
|
|
34
|
+
* single-parse command trees in general — a genuine yargs `.command()` tree
|
|
35
|
+
* (this file, now) natively supports `global: true` options that behave
|
|
36
|
+
* correctly regardless of position, which is exactly the position-
|
|
37
|
+
* dependence class of bug the per-verb-parse workaround existed to avoid in
|
|
38
|
+
* the first place.
|
|
30
39
|
*/
|
|
31
40
|
|
|
32
|
-
import yargs, { type Argv } from "yargs";
|
|
41
|
+
import yargs, { type Argv, type CommandModule } from "yargs";
|
|
33
42
|
import { openStore, CycleError, type TodoRecord } from "./todoStore.ts";
|
|
34
43
|
import { isKnownKind, LIFECYCLES, type LifecycleKind } from "./todoLifecycle.ts";
|
|
35
44
|
import { describeBlock, type TodoBlock } from "./todoBlock.ts";
|
|
36
|
-
import { renderDigest, renderTree } from "./todoDigest.ts";
|
|
45
|
+
import { renderDigest, renderTree, buildTreeJSON, unblockedTasks } from "./todoDigest.ts";
|
|
37
46
|
import { reconcileTodos, type LiveAgent } from "./todoAutomation.ts";
|
|
38
47
|
import { readGlobalPids } from "./globalPidIndex.ts";
|
|
39
48
|
|
|
@@ -53,13 +62,14 @@ function renderRecord(t: TodoRecord): string {
|
|
|
53
62
|
`${t._id} [${t.state}] ${t.summary}`,
|
|
54
63
|
`kind: ${t.kind}${t.targetTier ? ` tier:${t.targetTier}` : ""}`,
|
|
55
64
|
...(t.owner ? [`owner: ${t.owner}`] : []),
|
|
65
|
+
...(t.acceptanceCriteria ? [`acceptanceCriteria: ${t.acceptanceCriteria}`] : []),
|
|
56
66
|
...(t.block ? [`block: ${describeBlock(t.block)}`] : []),
|
|
57
67
|
...(t.blockedBy.length ? [`blockedBy: ${t.blockedBy.join(", ")}`] : []),
|
|
58
68
|
...(t.tags.length ? [`tags: ${t.tags.join(", ")}`] : []),
|
|
59
69
|
...(t.satisfiedGates.length ? [`satisfiedGates: ${t.satisfiedGates.join(", ")}`] : []),
|
|
60
70
|
...(t.verifyEvidence.length
|
|
61
71
|
? [
|
|
62
|
-
`evidence: ${t.verifyEvidence.map((e) => `${e.gate} by ${e.validator}${e.link ? ` (${e.link})` : ""}`).join("; ")}`,
|
|
72
|
+
`evidence: ${t.verifyEvidence.map((e) => `${e.gate} by ${e.validator}${e.link ? ` (${e.link})` : ""}${e.acceptanceCriteriaAtApproval ? ` [criteria: ${e.acceptanceCriteriaAtApproval}]` : ""}`).join("; ")}`,
|
|
63
73
|
]
|
|
64
74
|
: []),
|
|
65
75
|
`created: ${t.createdAt}`,
|
|
@@ -82,348 +92,473 @@ function renderList(tasks: TodoRecord[]): string {
|
|
|
82
92
|
return [line(header), ...rows.map(line)].join("\n");
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
interface
|
|
95
|
+
interface GlobalOpts {
|
|
86
96
|
root: string;
|
|
87
97
|
format: "table" | "json";
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
function emit(opts:
|
|
100
|
+
function emit(opts: GlobalOpts, obj: unknown, human: string): void {
|
|
91
101
|
process.stdout.write((opts.format === "json" ? JSON.stringify(obj, null, 2) : human) + "\n");
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
const newCmd: CommandModule<
|
|
105
|
+
GlobalOpts,
|
|
106
|
+
GlobalOpts & {
|
|
107
|
+
summary: string[];
|
|
108
|
+
kind: string | undefined;
|
|
109
|
+
description: string | undefined;
|
|
110
|
+
tier: string | undefined;
|
|
111
|
+
"acceptance-criteria": string | undefined;
|
|
112
|
+
owner: string | undefined;
|
|
113
|
+
tag: string[] | undefined;
|
|
114
|
+
dep: string[] | undefined;
|
|
115
|
+
}
|
|
116
|
+
> = {
|
|
117
|
+
command: "new <summary..>",
|
|
118
|
+
describe: "create a new task",
|
|
119
|
+
builder: (y) =>
|
|
120
|
+
y
|
|
121
|
+
.positional("summary", {
|
|
122
|
+
type: "string",
|
|
123
|
+
array: true,
|
|
124
|
+
demandOption: true,
|
|
125
|
+
describe:
|
|
126
|
+
"task summary (unquoted words are joined — quote if it must contain a literal --flag)",
|
|
127
|
+
})
|
|
128
|
+
.option("kind", { type: "string", describe: `one of: ${Object.keys(LIFECYCLES).join(", ")}` })
|
|
129
|
+
.option("description", { type: "string" })
|
|
130
|
+
.option("tier", { type: "string", describe: "targetTier, e.g. canary-done / shipped-done" })
|
|
131
|
+
.option("acceptance-criteria", {
|
|
132
|
+
type: "string",
|
|
133
|
+
describe:
|
|
134
|
+
"definition of done for this task — what an independent validator should check before approving/verifying it",
|
|
135
|
+
})
|
|
136
|
+
.option("owner", { type: "string" })
|
|
137
|
+
.option("tag", { type: "string", array: true })
|
|
138
|
+
.option("dep", { type: "string", array: true, describe: "blocker task id(s)" }),
|
|
139
|
+
handler: async (argv) => {
|
|
140
|
+
const store = await openStore(argv.root);
|
|
141
|
+
// Join ALL summary words: an unquoted summary like
|
|
142
|
+
// `ay todo new write the spec --kind doc` arrives as multiple array
|
|
143
|
+
// entries under yargs' `<summary..>` variadic positional — using only
|
|
144
|
+
// the first would silently truncate it (codex-review Important, from
|
|
145
|
+
// the previous hand-rolled-positional design; the variadic positional
|
|
146
|
+
// here still needs the same join, it just collects the array for us).
|
|
147
|
+
const summary = argv.summary.map(String).join(" ");
|
|
148
|
+
if (!summary) {
|
|
149
|
+
fail(
|
|
150
|
+
"usage: ay todo new <summary> --kind <kind> [--description ...] [--tier ...] [--acceptance-criteria ...] [--owner ...] [--tag t]... [--dep id]...",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const rec = await store.create({
|
|
154
|
+
summary,
|
|
155
|
+
kind: parseKind(argv.kind),
|
|
156
|
+
description: argv.description,
|
|
157
|
+
targetTier: argv.tier,
|
|
158
|
+
acceptanceCriteria: argv["acceptance-criteria"],
|
|
159
|
+
owner: argv.owner,
|
|
160
|
+
tags: argv.tag ?? [],
|
|
161
|
+
blockedBy: argv.dep ?? [],
|
|
162
|
+
});
|
|
163
|
+
emit(argv, rec, `created ${rec._id}\n${renderRecord(rec)}`);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
104
166
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
167
|
+
const lsCmd: CommandModule<
|
|
168
|
+
GlobalOpts,
|
|
169
|
+
GlobalOpts & {
|
|
170
|
+
kind: string | undefined;
|
|
171
|
+
state: string | undefined;
|
|
172
|
+
owner: string | undefined;
|
|
173
|
+
tag: string | undefined;
|
|
174
|
+
blocked: boolean;
|
|
175
|
+
}
|
|
176
|
+
> = {
|
|
177
|
+
command: "ls",
|
|
178
|
+
describe: "list tasks (filter by --kind, --state, --owner, --tag, --blocked)",
|
|
179
|
+
builder: (y) =>
|
|
180
|
+
y
|
|
181
|
+
.option("kind", { type: "string" })
|
|
182
|
+
.option("state", { type: "string" })
|
|
183
|
+
.option("owner", { type: "string" })
|
|
184
|
+
.option("tag", { type: "string" })
|
|
185
|
+
.option("blocked", { type: "boolean", default: false }),
|
|
186
|
+
handler: async (argv) => {
|
|
187
|
+
const store = await openStore(argv.root);
|
|
188
|
+
const tasks = store.list({
|
|
189
|
+
kind: argv.kind ? parseKind(argv.kind) : undefined,
|
|
190
|
+
state: argv.state,
|
|
191
|
+
owner: argv.owner,
|
|
192
|
+
tag: argv.tag,
|
|
193
|
+
blocked: argv.blocked,
|
|
194
|
+
});
|
|
195
|
+
emit(argv, tasks, renderList(tasks));
|
|
196
|
+
},
|
|
197
|
+
};
|
|
109
198
|
|
|
110
|
-
|
|
111
|
-
|
|
199
|
+
const getCmd: CommandModule<GlobalOpts, GlobalOpts & { id: string }> = {
|
|
200
|
+
command: "get <id>",
|
|
201
|
+
describe: "show one task",
|
|
202
|
+
builder: (y) => y.positional("id", { type: "string", demandOption: true }),
|
|
203
|
+
handler: async (argv) => {
|
|
204
|
+
const store = await openStore(argv.root);
|
|
205
|
+
const rec = store.get(argv.id);
|
|
206
|
+
if (!rec) fail(`no such task: ${argv.id}`);
|
|
207
|
+
emit(argv, rec, renderRecord(rec));
|
|
208
|
+
},
|
|
209
|
+
};
|
|
112
210
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
emit(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
const transitionCmd: CommandModule<GlobalOpts, GlobalOpts & { id: string; toState: string }> = {
|
|
212
|
+
command: "transition <id> <toState>",
|
|
213
|
+
describe: "move a task to a new state (fails naming the missing gate if one is required)",
|
|
214
|
+
builder: (y) =>
|
|
215
|
+
y
|
|
216
|
+
.positional("id", { type: "string", demandOption: true })
|
|
217
|
+
.positional("toState", { type: "string", demandOption: true }),
|
|
218
|
+
handler: async (argv) => {
|
|
219
|
+
const store = await openStore(argv.root);
|
|
220
|
+
const rec = await store.transition(argv.id, argv.toState);
|
|
221
|
+
emit(argv, rec, `transitioned ${rec._id} -> ${rec.state}\n${renderRecord(rec)}`);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const approveCmd: CommandModule<
|
|
226
|
+
GlobalOpts,
|
|
227
|
+
GlobalOpts & {
|
|
228
|
+
id: string;
|
|
229
|
+
gate: string;
|
|
230
|
+
validatorIdentity: string;
|
|
231
|
+
note: string | undefined;
|
|
232
|
+
link: string | undefined;
|
|
233
|
+
}
|
|
234
|
+
> = {
|
|
235
|
+
command: "approve <id> <gate> <validatorIdentity>",
|
|
236
|
+
describe: "manually satisfy a non-registered gate as a DIFFERENT identity from the task's owner",
|
|
237
|
+
builder: (y) =>
|
|
238
|
+
y
|
|
239
|
+
.positional("id", { type: "string", demandOption: true })
|
|
240
|
+
.positional("gate", { type: "string", demandOption: true })
|
|
241
|
+
.positional("validatorIdentity", { type: "string", demandOption: true })
|
|
242
|
+
.option("note", { type: "string" })
|
|
243
|
+
.option("link", { type: "string" }),
|
|
244
|
+
handler: async (argv) => {
|
|
245
|
+
const store = await openStore(argv.root);
|
|
246
|
+
const rec = await store.approve(argv.id, argv.gate, argv.validatorIdentity, {
|
|
247
|
+
note: argv.note,
|
|
248
|
+
link: argv.link,
|
|
249
|
+
});
|
|
250
|
+
emit(
|
|
251
|
+
argv,
|
|
252
|
+
rec,
|
|
253
|
+
`approved "${argv.gate}" on ${rec._id} (validator: ${argv.validatorIdentity})\n${renderRecord(rec)}`,
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const verifyCmd: CommandModule<GlobalOpts, GlobalOpts & { id: string; gate: string | undefined }> =
|
|
259
|
+
{
|
|
260
|
+
command: "verify <id> [gate]",
|
|
261
|
+
describe: "re-run a registered gate for a task and apply its result",
|
|
262
|
+
builder: (y) =>
|
|
263
|
+
y
|
|
264
|
+
.positional("id", { type: "string", demandOption: true })
|
|
265
|
+
.positional("gate", { type: "string" }),
|
|
266
|
+
handler: async (argv) => {
|
|
267
|
+
const store = await openStore(argv.root);
|
|
268
|
+
const rec = await store.verify(argv.id, argv.gate || undefined);
|
|
269
|
+
emit(argv, rec, `verified ${rec._id} -> ${rec.state}\n${renderRecord(rec)}`);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const BLOCK_TYPES = [
|
|
274
|
+
"blocked-by-task",
|
|
275
|
+
"blocked-by-human",
|
|
276
|
+
"blocked-by-external",
|
|
277
|
+
"waiting-on-agent",
|
|
278
|
+
] as const;
|
|
279
|
+
|
|
280
|
+
const blockCmd: CommandModule<
|
|
281
|
+
GlobalOpts,
|
|
282
|
+
GlobalOpts & {
|
|
283
|
+
id: string;
|
|
284
|
+
type: (typeof BLOCK_TYPES)[number] | undefined;
|
|
285
|
+
task: string | undefined;
|
|
286
|
+
who: string | undefined;
|
|
287
|
+
question: string | undefined;
|
|
288
|
+
options: string[] | undefined;
|
|
289
|
+
signal: string | undefined;
|
|
290
|
+
agent: string | undefined;
|
|
291
|
+
}
|
|
292
|
+
> = {
|
|
293
|
+
command: "block <id>",
|
|
294
|
+
describe:
|
|
295
|
+
"mark a task blocked (--type blocked-by-task|blocked-by-human|blocked-by-external|waiting-on-agent)",
|
|
296
|
+
builder: (y) =>
|
|
297
|
+
y
|
|
298
|
+
.positional("id", { type: "string", demandOption: true })
|
|
299
|
+
.option("type", { type: "string", choices: BLOCK_TYPES })
|
|
300
|
+
.option("task", { type: "string", describe: "required for --type blocked-by-task" })
|
|
301
|
+
.option("who", { type: "string", describe: "required for --type blocked-by-human" })
|
|
302
|
+
.option("question", { type: "string" })
|
|
303
|
+
.option("options", { type: "string", array: true })
|
|
304
|
+
.option("signal", { type: "string", describe: "required for --type blocked-by-external" })
|
|
305
|
+
.option("agent", { type: "string", describe: "required for --type waiting-on-agent" }),
|
|
306
|
+
handler: async (argv) => {
|
|
307
|
+
if (!argv.type) {
|
|
308
|
+
fail(
|
|
309
|
+
"usage: ay todo block <id> --type <blocked-by-task|blocked-by-human|blocked-by-external|waiting-on-agent> ...",
|
|
310
|
+
);
|
|
185
311
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
312
|
+
let block: TodoBlock;
|
|
313
|
+
switch (argv.type) {
|
|
314
|
+
case "blocked-by-task":
|
|
315
|
+
if (!argv.task) fail("--task <id> is required for --type blocked-by-task");
|
|
316
|
+
block = { type: "blocked-by-task", taskId: argv.task };
|
|
317
|
+
break;
|
|
318
|
+
case "blocked-by-human":
|
|
319
|
+
if (!argv.who) fail("--who <name> is required for --type blocked-by-human");
|
|
320
|
+
block = {
|
|
321
|
+
type: "blocked-by-human",
|
|
322
|
+
who: argv.who,
|
|
323
|
+
question: argv.question,
|
|
324
|
+
options: argv.options,
|
|
325
|
+
};
|
|
326
|
+
break;
|
|
327
|
+
case "blocked-by-external":
|
|
328
|
+
if (!argv.signal) fail("--signal <name> is required for --type blocked-by-external");
|
|
329
|
+
block = { type: "blocked-by-external", signal: argv.signal };
|
|
330
|
+
break;
|
|
331
|
+
case "waiting-on-agent":
|
|
332
|
+
if (!argv.agent) fail("--agent <id> is required for --type waiting-on-agent");
|
|
333
|
+
block = { type: "waiting-on-agent", agentId: argv.agent };
|
|
334
|
+
break;
|
|
196
335
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
336
|
+
const store = await openStore(argv.root);
|
|
337
|
+
const rec = await store.setBlock(argv.id, block);
|
|
338
|
+
emit(argv, rec, `blocked ${rec._id}: ${describeBlock(block)}\n${renderRecord(rec)}`);
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const unblockCmd: CommandModule<GlobalOpts, GlobalOpts & { id: string }> = {
|
|
343
|
+
command: "unblock <id>",
|
|
344
|
+
describe: "clear a task's block (does not touch blockedBy — see `dep`)",
|
|
345
|
+
builder: (y) => y.positional("id", { type: "string", demandOption: true }),
|
|
346
|
+
handler: async (argv) => {
|
|
347
|
+
const store = await openStore(argv.root);
|
|
348
|
+
const rec = await store.setBlock(argv.id, null);
|
|
349
|
+
emit(argv, rec, `unblocked ${rec._id}\n${renderRecord(rec)}`);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const setCriteriaCmd: CommandModule<GlobalOpts, GlobalOpts & { id: string; text: string[] }> = {
|
|
354
|
+
command: "set-criteria <id> <text..>",
|
|
355
|
+
describe: "set or update a task's acceptance criteria (definition of done)",
|
|
356
|
+
builder: (y) =>
|
|
357
|
+
y
|
|
358
|
+
.positional("id", { type: "string", demandOption: true })
|
|
359
|
+
.positional("text", { type: "string", array: true, demandOption: true }),
|
|
360
|
+
handler: async (argv) => {
|
|
361
|
+
const store = await openStore(argv.root);
|
|
362
|
+
const text = argv.text.map(String).join(" ");
|
|
363
|
+
const rec = await store.setAcceptanceCriteria(argv.id, text);
|
|
364
|
+
emit(argv, rec, `set acceptance criteria on ${rec._id}\n${renderRecord(rec)}`);
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const depCmd: CommandModule<
|
|
369
|
+
GlobalOpts,
|
|
370
|
+
GlobalOpts & { verb: "add" | "rm"; id: string; blockerId: string }
|
|
371
|
+
> = {
|
|
372
|
+
command: "dep <verb> <id> <blockerId>",
|
|
373
|
+
describe:
|
|
374
|
+
"manage task dependencies: dep add T2 T1 (T2 waits for T1) / dep rm T2 T1. Cycles are rejected",
|
|
375
|
+
builder: (y) =>
|
|
376
|
+
y
|
|
377
|
+
.positional("verb", { type: "string", choices: ["add", "rm"] as const, demandOption: true })
|
|
378
|
+
.positional("id", {
|
|
379
|
+
type: "string",
|
|
380
|
+
demandOption: true,
|
|
381
|
+
describe: "the task that is blocked",
|
|
382
|
+
})
|
|
383
|
+
.positional("blockerId", {
|
|
384
|
+
type: "string",
|
|
385
|
+
demandOption: true,
|
|
386
|
+
describe: "task id it waits for",
|
|
387
|
+
}),
|
|
388
|
+
handler: async (argv) => {
|
|
389
|
+
const store = await openStore(argv.root);
|
|
390
|
+
try {
|
|
391
|
+
const rec =
|
|
392
|
+
argv.verb === "add"
|
|
393
|
+
? await store.addDep(argv.id, argv.blockerId)
|
|
394
|
+
: await store.rmDep(argv.id, argv.blockerId);
|
|
214
395
|
emit(
|
|
215
|
-
|
|
396
|
+
argv,
|
|
216
397
|
rec,
|
|
217
|
-
|
|
398
|
+
`${argv.verb === "add" ? "added" : "removed"} dep ${argv.blockerId} on ${rec._id}\n${renderRecord(rec)}`,
|
|
218
399
|
);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const sub = commonOptions(yargs(args)).help(false).exitProcess(false);
|
|
223
|
-
const a = await sub.parseAsync();
|
|
224
|
-
const opts = commonOptsOf(a);
|
|
225
|
-
const store = await openStore(opts.root);
|
|
226
|
-
const [id, gate] = (a._ as (string | number)[]).map(String);
|
|
227
|
-
if (!id) fail("usage: ay todo verify <id> [gateName]");
|
|
228
|
-
const rec = await store.verify(id, gate || undefined);
|
|
229
|
-
emit(opts, rec, `verified ${rec._id} -> ${rec.state}\n${renderRecord(rec)}`);
|
|
230
|
-
return 0;
|
|
231
|
-
}
|
|
232
|
-
case "block": {
|
|
233
|
-
const sub = commonOptions(yargs(args))
|
|
234
|
-
.option("type", {
|
|
235
|
-
type: "string",
|
|
236
|
-
choices: [
|
|
237
|
-
"blocked-by-task",
|
|
238
|
-
"blocked-by-human",
|
|
239
|
-
"blocked-by-external",
|
|
240
|
-
"waiting-on-agent",
|
|
241
|
-
] as const,
|
|
242
|
-
})
|
|
243
|
-
.option("task", { type: "string" })
|
|
244
|
-
.option("who", { type: "string" })
|
|
245
|
-
.option("question", { type: "string" })
|
|
246
|
-
.option("options", { type: "string", array: true })
|
|
247
|
-
.option("signal", { type: "string" })
|
|
248
|
-
.option("agent", { type: "string" })
|
|
249
|
-
.help(false)
|
|
250
|
-
.exitProcess(false);
|
|
251
|
-
const a = await sub.parseAsync();
|
|
252
|
-
const opts = commonOptsOf(a);
|
|
253
|
-
const store = await openStore(opts.root);
|
|
254
|
-
const id = String(a._[0] ?? "");
|
|
255
|
-
if (!id || !a.type) {
|
|
256
|
-
fail(
|
|
257
|
-
"usage: ay todo block <id> --type <blocked-by-task|blocked-by-human|blocked-by-external|waiting-on-agent> ...",
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
let block: TodoBlock;
|
|
261
|
-
switch (a.type) {
|
|
262
|
-
case "blocked-by-task":
|
|
263
|
-
if (!a.task) fail("--task <id> is required for --type blocked-by-task");
|
|
264
|
-
block = { type: "blocked-by-task", taskId: a.task as string };
|
|
265
|
-
break;
|
|
266
|
-
case "blocked-by-human":
|
|
267
|
-
if (!a.who) fail("--who <name> is required for --type blocked-by-human");
|
|
268
|
-
block = {
|
|
269
|
-
type: "blocked-by-human",
|
|
270
|
-
who: a.who as string,
|
|
271
|
-
question: a.question as string | undefined,
|
|
272
|
-
options: a.options as string[] | undefined,
|
|
273
|
-
};
|
|
274
|
-
break;
|
|
275
|
-
case "blocked-by-external":
|
|
276
|
-
if (!a.signal) fail("--signal <name> is required for --type blocked-by-external");
|
|
277
|
-
block = { type: "blocked-by-external", signal: a.signal as string };
|
|
278
|
-
break;
|
|
279
|
-
case "waiting-on-agent":
|
|
280
|
-
if (!a.agent) fail("--agent <id> is required for --type waiting-on-agent");
|
|
281
|
-
block = { type: "waiting-on-agent", agentId: a.agent as string };
|
|
282
|
-
break;
|
|
283
|
-
default:
|
|
284
|
-
fail(`unknown block type: ${a.type}`);
|
|
285
|
-
}
|
|
286
|
-
const rec = await store.setBlock(id, block);
|
|
287
|
-
emit(opts, rec, `blocked ${rec._id}: ${describeBlock(block)}\n${renderRecord(rec)}`);
|
|
288
|
-
return 0;
|
|
289
|
-
}
|
|
290
|
-
case "unblock": {
|
|
291
|
-
const sub = commonOptions(yargs(args)).help(false).exitProcess(false);
|
|
292
|
-
const a = await sub.parseAsync();
|
|
293
|
-
const opts = commonOptsOf(a);
|
|
294
|
-
const store = await openStore(opts.root);
|
|
295
|
-
const id = String(a._[0] ?? "");
|
|
296
|
-
if (!id) fail("usage: ay todo unblock <id>");
|
|
297
|
-
const rec = await store.setBlock(id, null);
|
|
298
|
-
emit(opts, rec, `unblocked ${rec._id}\n${renderRecord(rec)}`);
|
|
299
|
-
return 0;
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (err instanceof CycleError) fail(err.message);
|
|
402
|
+
throw err;
|
|
300
403
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
`${verb2 === "add" ? "added" : "removed"} dep ${blockerId} on ${rec._id}\n${renderRecord(rec)}`,
|
|
317
|
-
);
|
|
318
|
-
return 0;
|
|
319
|
-
} catch (err) {
|
|
320
|
-
if (err instanceof CycleError) fail(err.message);
|
|
321
|
-
throw err;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
case "tree": {
|
|
325
|
-
const sub = commonOptions(yargs(args)).help(false).exitProcess(false);
|
|
326
|
-
const a = await sub.parseAsync();
|
|
327
|
-
const opts = commonOptsOf(a);
|
|
328
|
-
const store = await openStore(opts.root);
|
|
329
|
-
const rootId = a._[0] !== undefined ? String(a._[0]) : undefined;
|
|
330
|
-
const tasks = store.all();
|
|
331
|
-
if (opts.format === "json") {
|
|
332
|
-
const { buildTreeJSON } = await import("./todoDigest.ts");
|
|
333
|
-
emit(opts, buildTreeJSON(tasks, rootId), "");
|
|
334
|
-
} else {
|
|
335
|
-
process.stdout.write(renderTree(tasks, rootId) + "\n");
|
|
336
|
-
}
|
|
337
|
-
return 0;
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const treeCmd: CommandModule<GlobalOpts, GlobalOpts & { id: string | undefined }> = {
|
|
408
|
+
command: "tree [id]",
|
|
409
|
+
describe: "dependency tree (children = what a task waits for). Default: all dependency roots",
|
|
410
|
+
builder: (y) =>
|
|
411
|
+
y.positional("id", { type: "string", describe: "root task id (default: all roots)" }),
|
|
412
|
+
handler: async (argv) => {
|
|
413
|
+
const store = await openStore(argv.root);
|
|
414
|
+
const tasks = store.all();
|
|
415
|
+
if (argv.format === "json") {
|
|
416
|
+
emit(argv, buildTreeJSON(tasks, argv.id), "");
|
|
417
|
+
} else {
|
|
418
|
+
process.stdout.write(renderTree(tasks, argv.id) + "\n");
|
|
338
419
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const digestCmd: CommandModule<GlobalOpts, GlobalOpts> = {
|
|
424
|
+
command: "digest",
|
|
425
|
+
describe: "per-tag board: state counts, blockers, unblocked tasks",
|
|
426
|
+
handler: async (argv) => {
|
|
427
|
+
const store = await openStore(argv.root);
|
|
428
|
+
const tasks = store.all();
|
|
429
|
+
if (argv.format === "json") {
|
|
430
|
+
emit(argv, { tasks, unblocked: unblockedTasks(tasks).map((t) => t._id) }, "");
|
|
431
|
+
} else {
|
|
432
|
+
process.stdout.write(renderDigest(tasks) + "\n");
|
|
352
433
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const reconcileCmd: CommandModule<GlobalOpts, GlobalOpts> = {
|
|
438
|
+
command: "reconcile",
|
|
439
|
+
describe:
|
|
440
|
+
"apply automation: orphan dead-owned tasks, clear stale waiting-on-agent blocks, auto-verify, report unblocked tasks",
|
|
441
|
+
handler: async (argv) => {
|
|
442
|
+
const store = await openStore(argv.root);
|
|
443
|
+
// `liveOnly: false`: a task's owner needs to be checked against a KNOWN
|
|
444
|
+
// agent whose latest record says `exited`, not just the currently-live
|
|
445
|
+
// set — an exited-but-recorded agent is exactly the orphan signal (see
|
|
446
|
+
// todoAutomation.ts's `deadOwnerAgent`).
|
|
447
|
+
const agents: LiveAgent[] = await readGlobalPids({ liveOnly: false });
|
|
448
|
+
const registeredGates = new Set(store.registeredGateNames());
|
|
449
|
+
const actions = reconcileTodos(store.all(), agents, registeredGates);
|
|
365
450
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
451
|
+
// Every action is applied against a FRESH record inside the store (see
|
|
452
|
+
// `markOrphaned`/`clearWaitingOnAgentBlock`), since the snapshot
|
|
453
|
+
// `reconcileTodos` decided from can be stale by the time we get here
|
|
454
|
+
// (another process may have reassigned the task, changed its block,
|
|
455
|
+
// etc. — codex-review Important). A per-action failure is reported as a
|
|
456
|
+
// skip, not an aborted reconcile: the remaining actions still apply.
|
|
457
|
+
const applied: string[] = [];
|
|
458
|
+
for (const action of actions) {
|
|
459
|
+
try {
|
|
460
|
+
switch (action.type) {
|
|
461
|
+
case "orphan": {
|
|
462
|
+
await store.markOrphaned(action.taskId, action.expectedOwner, action.candidates);
|
|
463
|
+
applied.push(
|
|
464
|
+
`orphaned ${action.taskId} (was ${action.from}) — reassign candidates: ${action.candidates.join(", ") || "(none idle)"}`,
|
|
465
|
+
);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
case "clear-waiting-on-agent": {
|
|
469
|
+
await store.clearWaitingOnAgentBlock(action.taskId, action.expectedAgentId);
|
|
470
|
+
applied.push(
|
|
471
|
+
`cleared waiting-on-agent block on ${action.taskId} (agent ${action.expectedAgentId})`,
|
|
472
|
+
);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
case "auto-verify": {
|
|
476
|
+
// A registered gate reporting not-passed (or throwing for any
|
|
477
|
+
// other reason, e.g. a concurrent state change) is a real "not
|
|
478
|
+
// verified yet" outcome, reported as such rather than silently
|
|
479
|
+
// claimed as success — reconcile just tries again next call
|
|
480
|
+
// (codex-review Important: an earlier version swallowed every
|
|
481
|
+
// failure and still reported "auto-verified").
|
|
482
|
+
const result = await store.verify(action.taskId);
|
|
483
|
+
applied.push(`auto-verified ${action.taskId} -> ${result.state}`);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case "notify-unblocked": {
|
|
487
|
+
// Delivery to the owning agent's own inbox is not wired yet (the
|
|
488
|
+
// notify system addresses parent<->child pid trees, a different
|
|
489
|
+
// relationship than an arbitrary task owner) — surfaced here, on
|
|
490
|
+
// every call (see todoAutomation.ts), so it is visible rather
|
|
491
|
+
// than silently dropped or falsely retired.
|
|
492
|
+
applied.push(`${action.taskId} is now unblocked (owner: ${action.owner})`);
|
|
493
|
+
break;
|
|
410
494
|
}
|
|
411
|
-
} catch (err) {
|
|
412
|
-
applied.push(
|
|
413
|
-
`skipped ${action.type} on ${action.taskId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
414
|
-
);
|
|
415
495
|
}
|
|
496
|
+
} catch (err) {
|
|
497
|
+
applied.push(
|
|
498
|
+
`skipped ${action.type} on ${action.taskId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
499
|
+
);
|
|
416
500
|
}
|
|
417
|
-
emit(
|
|
418
|
-
opts,
|
|
419
|
-
{ actions, applied },
|
|
420
|
-
applied.length ? applied.join("\n") : "(nothing to reconcile)",
|
|
421
|
-
);
|
|
422
|
-
return 0;
|
|
423
501
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
)
|
|
428
|
-
|
|
502
|
+
emit(
|
|
503
|
+
argv,
|
|
504
|
+
{ actions, applied },
|
|
505
|
+
applied.length ? applied.join("\n") : "(nothing to reconcile)",
|
|
506
|
+
);
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
export async function runTodoSubcommand(rest0: string[]): Promise<number> {
|
|
511
|
+
await yargs(rest0)
|
|
512
|
+
.scriptName("ay todo")
|
|
513
|
+
.option("root", {
|
|
514
|
+
type: "string",
|
|
515
|
+
default: process.cwd(),
|
|
516
|
+
describe: "project root holding .agent-yes/todos.jsonl",
|
|
517
|
+
global: true,
|
|
518
|
+
})
|
|
519
|
+
.option("format", {
|
|
520
|
+
choices: ["table", "json"] as const,
|
|
521
|
+
default: "table" as const,
|
|
522
|
+
describe: "output format",
|
|
523
|
+
global: true,
|
|
524
|
+
})
|
|
525
|
+
.check((argv) => {
|
|
526
|
+
if ((argv.root as string) === "") fail("--root must not be empty");
|
|
527
|
+
return true;
|
|
528
|
+
})
|
|
529
|
+
.command(newCmd)
|
|
530
|
+
.command(lsCmd)
|
|
531
|
+
.command(getCmd)
|
|
532
|
+
.command(transitionCmd)
|
|
533
|
+
.command(approveCmd)
|
|
534
|
+
.command(verifyCmd)
|
|
535
|
+
.command(setCriteriaCmd)
|
|
536
|
+
.command(blockCmd)
|
|
537
|
+
.command(unblockCmd)
|
|
538
|
+
.command(depCmd)
|
|
539
|
+
.command(treeCmd)
|
|
540
|
+
.command(digestCmd)
|
|
541
|
+
.command(reconcileCmd)
|
|
542
|
+
.demandCommand(
|
|
543
|
+
1,
|
|
544
|
+
'unknown "ay todo" verb (expected: new/ls/get/transition/approve/verify/set-criteria/block/unblock/dep/tree/digest/reconcile)',
|
|
545
|
+
)
|
|
546
|
+
.strict()
|
|
547
|
+
.help()
|
|
548
|
+
.version(false)
|
|
549
|
+
.exitProcess(false)
|
|
550
|
+
.fail((msg, err) => {
|
|
551
|
+
// Default yargs behavior on a validation failure (unknown/missing
|
|
552
|
+
// command, failed .check()) is to print usage and, with
|
|
553
|
+
// exitProcess(false), silently resolve rather than reject — the exact
|
|
554
|
+
// opposite of every existing caller's expectation (they `await` this
|
|
555
|
+
// function and `.rejects.toThrow(...)` in tests, matching the rest of
|
|
556
|
+
// this codebase's convention of surfacing errors as thrown Errors, not
|
|
557
|
+
// silent exit codes). Re-throwing here is what makes that true for
|
|
558
|
+
// yargs' own validation errors, not just for `fail()` calls inside a
|
|
559
|
+
// handler body.
|
|
560
|
+
throw err ?? new Error(msg);
|
|
561
|
+
})
|
|
562
|
+
.parseAsync();
|
|
563
|
+
return 0;
|
|
429
564
|
}
|