taskchef 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: taskchef-bootstrap
3
+ description: "Initialize, diagnose, or refresh TaskChef dispatcher workspaces, project configuration, managed AGENTS.md instructions, and TaskChef skill links. Use when creating a TaskChef workspace, adding, importing, listing, or removing configured projects, running TaskChef doctor, repairing dispatcher setup, or upgrading installed TaskChef skills. Do not dispatch user work or reconcile executor threads."
4
+ ---
5
+
6
+ # TaskChef Bootstrap
7
+
8
+ Initialize or refresh a data-only TaskChef dispatcher workspace.
9
+
10
+ Resolve this linked skill with `realpath`. The TaskChef source root is two
11
+ parents above the skill directory. Invoke `<source-root>/bin/taskchef.js` for
12
+ all deterministic workspace operations.
13
+
14
+ ## Boundaries
15
+
16
+ - Keep implementation, tests, and reports in the TaskChef source repository.
17
+ - Keep only `AGENTS.md`, `taskchef.json`, `tasks/*/task.json`, and the three
18
+ TaskChef skill links in a dispatcher workspace.
19
+ - Do not dispatch tasks or reconcile executor threads during bootstrap unless
20
+ the user separately requests those actions.
21
+ - Never use collaboration agents, hooks, schedules, polling, or daemons.
22
+
23
+ ## Initialize and repair
24
+
25
+ 1. Run `workspace init --json`. It takes no stdin, creates an empty
26
+ configuration when missing, and idempotently creates or refreshes the task
27
+ directory, managed instructions, and all three skill links.
28
+ 2. Run `doctor --json` after setup or when the user asks to diagnose the
29
+ workspace. Doctor is read-only; rerun `workspace init --json` to repair the
30
+ managed scaffold.
31
+ 3. Report the actions or failed checks. End without dispatching unless the user
32
+ separately requested work.
33
+
34
+ ## Configure projects
35
+
36
+ 1. List native Codex projects once when discovery is necessary. Configure only
37
+ projects local to the TaskChef workspace's execution host. Remote connection
38
+ projects are outside the v1 contract.
39
+ 2. Add one project with `project add <path>`, normally supplying `--name` and a
40
+ curated `--description`. The CLI detects Git status, exact Git root, and a
41
+ canonical GitHub `origin`; use `--no-github` or `--github-repo` only to
42
+ override detection.
43
+ 3. Bulk import with `project import <file|-> --json`. Input is a JSON array of
44
+ objects containing `path` plus optional `name`, `description`, and
45
+ `githubRepo`. Import merges by canonical path and preserves an existing name
46
+ or description when omitted. Use `--replace` only when the user explicitly
47
+ requests replacement.
48
+ 4. Inspect configured projects with `project list --json`. Remove by name with
49
+ `project remove`; require explicit user intent before `--force` when task
50
+ records reference the project.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "TaskChef Bootstrap"
3
+ short_description: "Bootstrap TaskChef dispatcher workspaces"
4
+ default_prompt: "Use $taskchef-bootstrap to initialize or refresh this TaskChef dispatcher workspace."
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: taskchef-delegate
3
+ description: "Dispatch actionable requests from an initialized TaskChef workspace into independently openable Codex project tasks. Use for ordinary work requests in a TaskChef workspace, explicit delegation, splitting work across projects, or retrying pending executor creation. Dispatch must return immediately and must never use subagents, hooks, schedules, or foreground waiting. Use taskchef-bootstrap instead for workspace setup or configuration changes."
4
+ ---
5
+
6
+ # TaskChef Delegate
7
+
8
+ Create real Codex tasks from a TaskChef data workspace and return immediately.
9
+ Use `taskchef-reconcile` first when the current prompt has active recorded tasks.
10
+
11
+ Resolve this linked skill with `realpath`. The TaskChef source root is two
12
+ parents above the skill directory. Invoke `<source-root>/bin/taskchef.js` for
13
+ all deterministic workspace and task-record operations.
14
+
15
+ ## Boundaries
16
+
17
+ - Keep implementation, tests, and reports in the TaskChef source repository.
18
+ - Keep only `AGENTS.md`, `taskchef.json`, `tasks/*/task.json`, and the three
19
+ TaskChef skill links in a dispatcher workspace.
20
+ - Use real Codex tasks, never collaboration or subagent tools.
21
+ - Never use hooks, callbacks, schedules, polling, daemons, or event logs.
22
+ - Never wait for delegated work after executor creation.
23
+ - Never collect transcripts or hidden reasoning.
24
+
25
+ ## Dispatch
26
+
27
+ 1. Run `project list --json` to load and validate the configured routing
28
+ targets. Use `taskchef-bootstrap` if the workspace is missing or unhealthy.
29
+ 2. Split the request into the smallest independently useful outcomes. Include
30
+ constraints, expected testing, and reporting in every instruction.
31
+ 3. Classify against configured `name`, `githubRepo`, and `description`. Use
32
+ `path` only as checkout identity. Ask when metadata does not produce one
33
+ clear project match.
34
+ 4. Resolve native projects once and require the exact configured path.
35
+ 5. Reuse a matching pending record for an explicit retry; otherwise create one
36
+ pending record before executor creation.
37
+ 6. Create one real Codex task per record using the exact saved project and a
38
+ local environment on its executor host.
39
+ 7. Immediately record every returned `threadId` with status `running`. Never
40
+ persist `hostId`.
41
+ 8. Leave a failed creation pending. Do not invent an ID or delete the record.
42
+ 9. Return immediately with a created-thread directive for every success. Do
43
+ not read or wait for a newly created executor.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "TaskChef Delegate"
3
+ short_description: "Dispatch work into visible Codex tasks"
4
+ default_prompt: "Use $taskchef-delegate to route this request into independently openable Codex tasks."
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: taskchef-reconcile
3
+ description: "Reconcile active TaskChef task records with their visible Codex executor threads. Use at the start of ordinary prompts in a TaskChef workspace, for TaskChef status or progress requests, after delegated work may have completed or blocked, or for an explicitly requested full refresh. Performs one bounded snapshot pass and never polls or waits for future activity."
4
+ ---
5
+
6
+ # TaskChef Reconcile
7
+
8
+ Refresh TaskChef's recorded task state from native Codex threads exactly once.
9
+
10
+ Resolve this linked skill with `realpath`. The TaskChef source root is two
11
+ parents above the skill directory. Invoke `<source-root>/bin/taskchef.js` for
12
+ all deterministic record operations.
13
+
14
+ ## Reconcile
15
+
16
+ 1. Run `task reconcile-candidates --json` once. It returns only `running` and
17
+ `blocked` tasks with thread IDs. Do not scan pending or finished records.
18
+ 2. Query every returned thread exactly once using immediate native snapshots,
19
+ with no more than eight targets per call.
20
+ 3. Do not wait for future activity and do not poll.
21
+ 4. Map an active attempt to `running`, a user-input or external dependency to
22
+ `blocked`, and any concluded attempt to `finished`, including failed or
23
+ partial attempts.
24
+ 5. Preserve an existing result unless the thread provides a meaningful newer
25
+ report. Results contain exactly `message`, `githubPRs`, and `githubIssues`.
26
+ 6. Update each changed record once with `task update`.
27
+ 7. Report a concise snapshot and continue with the user's request. If there
28
+ were no candidates or changes, say so briefly.
29
+
30
+ Use `task reconcile-candidates --include-finished --json` only when the user
31
+ explicitly requests a full refresh or says a finished executor received new
32
+ work. A finished task with a new active attempt becomes `running` again.
33
+
34
+ The native thread is authoritative between reconciliations. Executor reports
35
+ are evidence, not independently verified completion guarantees.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "TaskChef Reconcile"
3
+ short_description: "Reconcile visible TaskChef task state"
4
+ default_prompt: "Use $taskchef-reconcile to refresh active TaskChef task statuses and results once."
package/src/cli.js ADDED
@@ -0,0 +1,303 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ addProject,
6
+ buildReconciliationCandidates,
7
+ buildTaskSummary,
8
+ createTask,
9
+ doctorWorkspace,
10
+ filterTasks,
11
+ importProjects,
12
+ initializeWorkspace,
13
+ listProjects,
14
+ readTask,
15
+ removeProject,
16
+ updateTask,
17
+ } from "./workspace.js";
18
+
19
+ async function readStdin() {
20
+ let input = "";
21
+ process.stdin.setEncoding("utf8");
22
+ for await (const chunk of process.stdin) input += chunk;
23
+ return input;
24
+ }
25
+
26
+ async function readJsonStdin() {
27
+ const input = await readStdin();
28
+ if (input.trim().length === 0) throw new Error("expected JSON on standard input");
29
+ return JSON.parse(input);
30
+ }
31
+
32
+ function option(args, name, fallback) {
33
+ const index = args.indexOf(name);
34
+ if (index === -1) return fallback;
35
+ if (!args[index + 1] || args[index + 1].startsWith("--")) {
36
+ throw new Error(`${name} requires a value`);
37
+ }
38
+ return args[index + 1];
39
+ }
40
+
41
+ function options(args, name) {
42
+ const values = [];
43
+ for (let index = 0; index < args.length; index += 1) {
44
+ if (args[index] !== name) continue;
45
+ if (!args[index + 1] || args[index + 1].startsWith("--")) {
46
+ throw new Error(`${name} requires a value`);
47
+ }
48
+ values.push(args[index + 1]);
49
+ index += 1;
50
+ }
51
+ return values;
52
+ }
53
+
54
+ function validateCommandArgs(
55
+ args,
56
+ startIndex,
57
+ { values = [], switches = [], repeatable = [] } = {},
58
+ ) {
59
+ const valueOptions = new Set(values);
60
+ const booleanOptions = new Set(switches);
61
+ const repeatableOptions = new Set(repeatable);
62
+ const seen = new Set();
63
+ for (let index = startIndex; index < args.length; index += 1) {
64
+ const token = args[index];
65
+ if (!token.startsWith("--") || token.includes("=")) {
66
+ throw new Error(`unexpected argument: ${token}`);
67
+ }
68
+ if (!valueOptions.has(token) && !booleanOptions.has(token)) {
69
+ throw new Error(`unsupported option: ${token}`);
70
+ }
71
+ if (seen.has(token) && !repeatableOptions.has(token)) {
72
+ throw new Error(`duplicate option: ${token}`);
73
+ }
74
+ seen.add(token);
75
+ if (valueOptions.has(token)) {
76
+ if (!args[index + 1] || args[index + 1].startsWith("--")) {
77
+ throw new Error(`${token} requires a value`);
78
+ }
79
+ index += 1;
80
+ }
81
+ }
82
+ }
83
+
84
+ function workspaceRoot(args) {
85
+ return path.resolve(option(args, "--workspace", process.cwd()));
86
+ }
87
+
88
+ function print(value, args, human) {
89
+ if (args.includes("--json")) {
90
+ process.stdout.write(`${JSON.stringify(value)}\n`);
91
+ return;
92
+ }
93
+ process.stdout.write(`${human ? human(value) : JSON.stringify(value, null, 2)}\n`);
94
+ }
95
+
96
+ function table(headers, rows) {
97
+ const widths = headers.map((header, index) =>
98
+ Math.max(header.length, ...rows.map((row) => String(row[index]).length)));
99
+ const format = (row) => row.map((value, index) => String(value).padEnd(widths[index])).join(" ");
100
+ return [format(headers), ...rows.map(format)].join("\n");
101
+ }
102
+
103
+ async function initialize(args) {
104
+ validateCommandArgs(args, 2, { values: ["--workspace"], switches: ["--json"] });
105
+ const result = await initializeWorkspace(workspaceRoot(args));
106
+ print(result, args, (value) => [
107
+ `Workspace: ${value.workspace}`,
108
+ `Configuration: ${value.config.action}`,
109
+ `Instructions: ${value.instructions.action}`,
110
+ `Skills: ${value.skills.skills.map((skill) => `${skill.name}=${skill.action}`).join(", ")}`,
111
+ ].join("\n"));
112
+ return 0;
113
+ }
114
+
115
+ async function doctor(args) {
116
+ validateCommandArgs(args, 1, { values: ["--workspace"], switches: ["--json"] });
117
+ const result = await doctorWorkspace(workspaceRoot(args));
118
+ print(result, args, (value) => value.checks
119
+ .map((check) => `${check.status === "pass" ? "✓" : "✗"} ${check.name}: ${check.message}`)
120
+ .join("\n"));
121
+ return result.ok ? 0 : 1;
122
+ }
123
+
124
+ async function projectAdd(args) {
125
+ if (!args[2] || args[2].startsWith("--")) throw new Error("project add requires a path");
126
+ validateCommandArgs(args, 3, {
127
+ values: ["--workspace", "--name", "--description", "--github-repo"],
128
+ switches: ["--json", "--no-github"],
129
+ });
130
+ if (args.includes("--no-github") && args.includes("--github-repo")) {
131
+ throw new Error("--no-github and --github-repo cannot be used together");
132
+ }
133
+ const input = { path: args[2] };
134
+ const name = option(args, "--name", null);
135
+ const description = option(args, "--description", null);
136
+ if (name !== null) input.name = name;
137
+ if (description !== null) input.description = description;
138
+ if (args.includes("--no-github")) input.githubRepo = null;
139
+ else if (args.includes("--github-repo")) input.githubRepo = option(args, "--github-repo");
140
+ const project = await addProject(workspaceRoot(args), input);
141
+ print(project, args, (value) => `Added ${value.name}: ${value.path}`);
142
+ return 0;
143
+ }
144
+
145
+ async function projectImport(args) {
146
+ const hasSource = Boolean(args[2] && !args[2].startsWith("--"));
147
+ const source = hasSource ? args[2] : "-";
148
+ validateCommandArgs(args, hasSource ? 3 : 2, {
149
+ values: ["--workspace"],
150
+ switches: ["--json", "--replace"],
151
+ });
152
+ const content = source === "-"
153
+ ? await readStdin()
154
+ : await readFile(path.resolve(source), "utf8");
155
+ if (content.trim().length === 0) throw new Error("project import input is empty");
156
+ const result = await importProjects(workspaceRoot(args), JSON.parse(content), {
157
+ replace: args.includes("--replace"),
158
+ });
159
+ print(result, args, (value) =>
160
+ `Imported ${value.importedCount} project(s); ${value.projectCount} configured (${value.mode}).`);
161
+ return 0;
162
+ }
163
+
164
+ async function projectList(args) {
165
+ validateCommandArgs(args, 2, { values: ["--workspace"], switches: ["--json"] });
166
+ const projects = await listProjects(workspaceRoot(args));
167
+ print({ projectCount: projects.length, projects }, args, (value) => table(
168
+ ["NAME", "KIND", "PATH"],
169
+ value.projects.map((project) => [
170
+ project.name,
171
+ project.isGitRepository ? "git" : "folder",
172
+ project.path,
173
+ ]),
174
+ ));
175
+ return 0;
176
+ }
177
+
178
+ async function projectRemove(args) {
179
+ if (!args[2] || args[2].startsWith("--")) throw new Error("project remove requires a name");
180
+ validateCommandArgs(args, 3, {
181
+ values: ["--workspace"],
182
+ switches: ["--json", "--force"],
183
+ });
184
+ const result = await removeProject(workspaceRoot(args), args[2], {
185
+ force: args.includes("--force"),
186
+ });
187
+ print(result, args, (value) => `Removed ${value.project.name}: ${value.project.path}`);
188
+ return 0;
189
+ }
190
+
191
+ async function create(args) {
192
+ validateCommandArgs(args, 2, { values: ["--workspace"], switches: ["--json"] });
193
+ const task = await createTask(workspaceRoot(args), await readJsonStdin());
194
+ print(task, args, (value) => `Created ${value.id}: ${value.title} (${value.status})`);
195
+ return 0;
196
+ }
197
+
198
+ async function update(args) {
199
+ validateCommandArgs(args, 3, { values: ["--workspace"], switches: ["--json"] });
200
+ const task = await updateTask(workspaceRoot(args), args[2], await readJsonStdin());
201
+ print(task, args, (value) => `Updated ${value.id}: ${value.status}`);
202
+ return 0;
203
+ }
204
+
205
+ async function show(args) {
206
+ validateCommandArgs(args, 3, { values: ["--workspace"], switches: ["--json"] });
207
+ print(await readTask(workspaceRoot(args), args[2]), args);
208
+ return 0;
209
+ }
210
+
211
+ async function taskList(args) {
212
+ validateCommandArgs(args, 2, {
213
+ values: ["--workspace", "--status", "--project"],
214
+ switches: ["--json"],
215
+ repeatable: ["--status"],
216
+ });
217
+ const tasks = await filterTasks(workspaceRoot(args), {
218
+ statuses: options(args, "--status"),
219
+ project: option(args, "--project", null),
220
+ });
221
+ const projects = await listProjects(workspaceRoot(args));
222
+ const projectNames = new Map(projects.map((project) => [project.path, project.name]));
223
+ const result = { taskCount: tasks.length, tasks };
224
+ print(result, args, (value) => table(
225
+ ["ID", "STATUS", "PROJECT", "TITLE"],
226
+ value.tasks.map((task) => [
227
+ task.id,
228
+ task.status,
229
+ projectNames.get(task.project) ?? path.basename(task.project),
230
+ task.title,
231
+ ]),
232
+ ));
233
+ return 0;
234
+ }
235
+
236
+ async function taskSummary(args) {
237
+ validateCommandArgs(args, 2, { values: ["--workspace"], switches: ["--json"] });
238
+ const summary = await buildTaskSummary(workspaceRoot(args));
239
+ print(summary, args, (value) => [
240
+ `Tasks: ${value.taskCount}`,
241
+ ...Object.entries(value.statusCounts).map(([status, count]) => `${status}: ${count}`),
242
+ ].join("\n"));
243
+ return 0;
244
+ }
245
+
246
+ async function reconciliationCandidates(args) {
247
+ validateCommandArgs(args, 2, {
248
+ values: ["--workspace"],
249
+ switches: ["--json", "--include-finished"],
250
+ });
251
+ const result = await buildReconciliationCandidates(workspaceRoot(args), {
252
+ includeFinished: args.includes("--include-finished"),
253
+ });
254
+ print(result, args);
255
+ return 0;
256
+ }
257
+
258
+ function usage() {
259
+ process.stdout.write(`TaskChef workspace utility
260
+
261
+ Usage:
262
+ taskchef help
263
+ taskchef doctor [--json] [--workspace <path>]
264
+ taskchef workspace init [--json] [--workspace <path>]
265
+ taskchef project add <path> [--name <name>] [--description <text>] [--github-repo <url> | --no-github] [--json] [--workspace <path>]
266
+ taskchef project import [<file> | -] [--replace] [--json] [--workspace <path>]
267
+ taskchef project list [--json] [--workspace <path>]
268
+ taskchef project remove <name> [--force] [--json] [--workspace <path>]
269
+ taskchef task create [--json] [--workspace <path>]
270
+ taskchef task update <task-id> [--json] [--workspace <path>]
271
+ taskchef task show <task-id> [--json] [--workspace <path>]
272
+ taskchef task list [--status <status>]... [--project <name-or-path>] [--json] [--workspace <path>]
273
+ taskchef task summary [--json] [--workspace <path>]
274
+ taskchef task reconcile-candidates [--include-finished] [--json] [--workspace <path>]
275
+
276
+ Task create and update read JSON from standard input. Project import reads a
277
+ JSON array from a file, or from standard input when the source is '-' or omitted.
278
+ `);
279
+ }
280
+
281
+ export async function runCli(args) {
282
+ if (args.length === 0 || args[0] === "help" || args[0] === "--help") {
283
+ usage();
284
+ return 0;
285
+ }
286
+ if (args[0] === "doctor") return doctor(args);
287
+ if (args[0] === "workspace" && args[1] === "init") return initialize(args);
288
+ if (args[0] === "project" && args[1] === "add") return projectAdd(args);
289
+ if (args[0] === "project" && args[1] === "import") return projectImport(args);
290
+ if (args[0] === "project" && args[1] === "list") return projectList(args);
291
+ if (args[0] === "project" && args[1] === "remove") return projectRemove(args);
292
+ if (args[0] === "task" && args[1] === "create") return create(args);
293
+ if (args[0] === "task" && args[1] === "update" && args[2]) return update(args);
294
+ if (args[0] === "task" && args[1] === "show" && args[2]) return show(args);
295
+ if (args[0] === "task" && args[1] === "list") return taskList(args);
296
+ if (args[0] === "task" && args[1] === "summary") return taskSummary(args);
297
+ if (args[0] === "task" && args[1] === "reconcile-candidates") {
298
+ return reconciliationCandidates(args);
299
+ }
300
+ process.stderr.write(`Unknown command: ${args.join(" ")}\n`);
301
+ usage();
302
+ return 2;
303
+ }