taskchef 1.0.2 → 2.0.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.
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "taskchef",
3
+ "version": "2.0.0",
4
+ "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
+ "author": {
6
+ "name": "Favo Yang",
7
+ "url": "https://github.com/favoyang"
8
+ },
9
+ "homepage": "https://github.com/favoyang/taskchef#readme",
10
+ "repository": "https://github.com/favoyang/taskchef",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "codex",
14
+ "delegation",
15
+ "dispatcher",
16
+ "tasks"
17
+ ],
18
+ "skills": "./skills/",
19
+ "interface": {
20
+ "displayName": "TaskChef",
21
+ "shortDescription": "Dispatch work to the right Codex project task.",
22
+ "longDescription": "Create a data-only dispatcher workspace, route actionable requests to independently openable Codex project tasks, and reconcile saved task states on demand.",
23
+ "developerName": "Favo Yang",
24
+ "category": "Productivity",
25
+ "capabilities": [
26
+ "Read",
27
+ "Write"
28
+ ],
29
+ "websiteURL": "https://github.com/favoyang/taskchef",
30
+ "defaultPrompt": [
31
+ "$taskchef-bootstrap Set up TaskChef in this folder.",
32
+ "$taskchef-delegate Dispatch this request to the right project.",
33
+ "$taskchef-reconcile Fix outdated TaskChef task states."
34
+ ],
35
+ "brandColor": "#D97706"
36
+ }
37
+ }
package/BACKLOG.md CHANGED
@@ -84,5 +84,5 @@ This document contains capabilities intentionally excluded from the v1 MVP.
84
84
 
85
85
  - Add GitHub automation beyond storing PR and issue URLs.
86
86
  - Evaluate automatic project discovery instead of an explicit configured list.
87
- - Consider npm registry publication only after the GitHub-source installation
88
- and local managed-checkout workflows are stable.
87
+ - Evaluate marketplace discovery and update automation after the shared plugin
88
+ catalog has real usage.
package/README.md CHANGED
@@ -11,33 +11,52 @@ The canonical contract is [SPEC.md](SPEC.md). Deferred ideas are in
11
11
 
12
12
  TaskChef requires Node.js 18 or newer and Git.
13
13
 
14
- Install the CLI and its bundled skills from npm:
14
+ Add the shared Favo Yang plugin marketplace, then install TaskChef:
15
15
 
16
16
  ```sh
17
- npm install --global taskchef
17
+ codex plugin marketplace add favoyang/codex-plugins
18
+ codex plugin add taskchef@favoyang-plugins
18
19
  ```
19
20
 
20
- Then initialize a dispatcher workspace. Initialization links the three bundled
21
- TaskChef skills into that workspace; no separate skill installation is needed.
21
+ Open or create the folder that will hold TaskChef's dispatcher data, start a
22
+ new Codex task there, and ask:
23
+
24
+ ```text
25
+ $taskchef-bootstrap Set up TaskChef in this folder and help me choose projects.
26
+ ```
27
+
28
+ The plugin provides all three TaskChef skills and its deterministic CLI runtime;
29
+ dispatcher workspaces do not install or link skills themselves.
30
+
31
+ For headless CLI use, install the npm package with
32
+ `npm install --global taskchef`. Contributors can run `node bin/taskchef.js`
33
+ directly from a source checkout.
34
+
35
+ ## Updating
36
+
37
+ Refresh the marketplace and reinstall TaskChef:
22
38
 
23
39
  ```sh
24
- taskchef workspace init --workspace <workspace>
25
- taskchef doctor --workspace <workspace>
40
+ codex plugin marketplace upgrade favoyang-plugins
41
+ codex plugin add taskchef@favoyang-plugins
26
42
  ```
27
43
 
28
- Contributors working from a source checkout can run `node bin/taskchef.js`
29
- directly; this managed skills workspace installs the checkout CLI and skills
30
- with symlinks. To install an unreleased revision, use
31
- `npm install --global github:favoyang/taskchef`.
44
+ Start a new Codex task to load the updated skills.
45
+
46
+ ### Migrating from 1.x
47
+
48
+ TaskChef 2.x moves skill ownership from dispatcher-workspace symlinks to the
49
+ installed plugin. Run `$taskchef-bootstrap` once in an existing workspace to
50
+ remove the three legacy TaskChef links; unrelated `.agents` content is
51
+ preserved. The deprecated `ensureWorkspaceSkills()` export remains available
52
+ for compatibility but reports plugin-provided skills instead of creating
53
+ workspace links.
32
54
 
33
55
  ## Workspace
34
56
 
35
57
  ```text
36
58
  AGENTS.md
37
59
  taskchef.json
38
- .agents/skills/taskchef-bootstrap -> <source>/skills/taskchef-bootstrap
39
- .agents/skills/taskchef-delegate -> <source>/skills/taskchef-delegate
40
- .agents/skills/taskchef-reconcile -> <source>/skills/taskchef-reconcile
41
60
  tasks/<task-id>/task.json
42
61
  ```
43
62
 
@@ -49,7 +68,9 @@ taskchef doctor --workspace <workspace>
49
68
  ```
50
69
 
51
70
  Initialization is idempotent. It creates an empty configuration when missing
52
- and preserves existing configured projects.
71
+ and preserves existing configured projects. When upgrading from the earlier
72
+ workspace-linked distribution, initialization removes the three legacy
73
+ TaskChef skill symlinks and preserves unrelated `.agents` content.
53
74
 
54
75
  ## Projects
55
76
 
@@ -145,15 +166,22 @@ feat!: change the workspace data contract
145
166
 
146
167
  Publishing uses npm trusted publishing from `.github/workflows/release.yml`.
147
168
  The workflow runs the test suite, validates the npm tarball, publishes the
148
- calculated version, creates the GitHub release, and commits the updated
149
- `package.json` version back to `main`.
169
+ calculated version after synchronizing the plugin manifest, creates the GitHub
170
+ release, commits the synchronized version files back to `main`, and pins the TaskChef entry
171
+ in `favoyang/codex-plugins` to that exact npm version.
172
+
173
+ The release job requires an Actions secret named `MARKETPLACE_DEPLOY_KEY`.
174
+ Store the private half of a dedicated SSH deploy key there, and add its public
175
+ half to `favoyang/codex-plugins` with write access. The key must be scoped only
176
+ to that catalog repository; the workflow's repository-scoped `GITHUB_TOKEN`
177
+ cannot update another repository.
150
178
 
151
179
  ## Development
152
180
 
153
181
  ```sh
154
182
  npm test
155
183
  npm pack --dry-run
156
- npx -y -p semantic-release@25 -p @semantic-release/git semantic-release --dry-run
184
+ npx -y -p semantic-release@25 -p @semantic-release/exec -p @semantic-release/git semantic-release --dry-run
157
185
  ```
158
186
 
159
187
  ## Boundaries
package/SPEC.md CHANGED
@@ -35,11 +35,6 @@ Multiple ongoing tasks may target the same project.
35
35
  taskchef-workspace/
36
36
  ├── AGENTS.md
37
37
  ├── taskchef.json
38
- ├── .agents/
39
- │ └── skills/
40
- │ ├── taskchef-bootstrap -> <TaskChef bootstrap skill>
41
- │ ├── taskchef-delegate -> <TaskChef delegate skill>
42
- │ └── taskchef-reconcile -> <TaskChef reconciliation skill>
43
38
  └── tasks/
44
39
  └── <task-id>/
45
40
  └── task.json
@@ -61,12 +56,15 @@ for user-requested refreshes or repairs of outdated task states.
61
56
  does exist, bootstrap preserves unrelated user content and adds or refreshes
62
57
  only the TaskChef managed block. Repeating the merge is idempotent. Malformed
63
58
  or duplicate TaskChef markers fail safely instead of overwriting the file.
64
- Initialization also installs all three TaskChef skill links. It is idempotent,
65
- takes no configuration input, creates `{ "schemaVersion": 1, "projects": [] }`
66
- when configuration is missing, and preserves existing configured projects.
59
+ The installed TaskChef plugin provides all three skills outside the dispatcher
60
+ workspace. Initialization is idempotent, takes no configuration input, creates
61
+ `{ "schemaVersion": 1, "projects": [] }` when configuration is missing, and
62
+ preserves existing configured projects. It removes only the three legacy
63
+ TaskChef skill symlinks from earlier workspaces and preserves unrelated
64
+ `.agents` content.
67
65
 
68
- `doctor` diagnoses configuration, task storage, managed instructions, skill
69
- links, project paths, and task records without modifying the workspace.
66
+ `doctor` diagnoses configuration, task storage, managed instructions, project
67
+ paths, and task records without modifying the workspace.
70
68
 
71
69
  ## Configuration
72
70
 
@@ -285,8 +283,9 @@ TaskChef is local, interactive, task-focused, and asynchronous. It does not incl
285
283
  - one-active-task-per-project restrictions;
286
284
  - transcript or hidden-reasoning collection;
287
285
  - arbitrary Codex task discovery;
288
- - npm registry publication; the supported distribution is a GitHub-source
289
- global install or a managed source checkout.
286
+ - bundled development-only runtimes in dispatcher workspaces. The Codex plugin
287
+ marketplace distributes the three skills, and the npm package provides the
288
+ supported headless CLI.
290
289
 
291
290
  See `BACKLOG.md` for deferred capabilities and experiments.
292
291
 
@@ -306,7 +305,7 @@ The MVP is successful when:
306
305
  without reading finished executor threads; ordinary delegation does not.
307
306
  8. Status and result snapshots are updated correctly.
308
307
  9. Two ongoing tasks may target the same project without data collisions.
309
- 10. The workspace contains dispatcher instructions, configuration, task
310
- records, and the three TaskChef skill links.
308
+ 10. The workspace contains only dispatcher instructions, configuration, and
309
+ task records; the installed plugin owns the TaskChef skills.
311
310
  11. Bootstrap creates or idempotently merges the TaskChef-managed `AGENTS.md`
312
311
  block without overwriting unrelated instructions.
@@ -5,7 +5,7 @@ This repository is a TaskChef dispatcher workspace.
5
5
 
6
6
  - Use `$taskchef-bootstrap` when initializing or refreshing this workspace,
7
7
  changing or listing its configured projects, running TaskChef doctor,
8
- repairing its managed instructions, or upgrading its TaskChef skill links.
8
+ or repairing its managed instructions.
9
9
  - For every actionable work request, use `$taskchef-delegate`
10
10
  automatically, even when the user does not explicitly say "delegate" or
11
11
  mention TaskChef.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -18,6 +18,7 @@
18
18
  "taskchef": "bin/taskchef.js"
19
19
  },
20
20
  "files": [
21
+ ".codex-plugin",
21
22
  "assets",
22
23
  "BACKLOG.md",
23
24
  "bin",
@@ -31,6 +32,9 @@
31
32
  "engines": {
32
33
  "node": ">=18"
33
34
  },
35
+ "devDependencies": {
36
+ "yaml": "^2.9.0"
37
+ },
34
38
  "scripts": {
35
39
  "test": "node --test tests/taskchef.test.js"
36
40
  }
@@ -1,21 +1,21 @@
1
1
  ---
2
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."
3
+ description: "Initialize, diagnose, or refresh TaskChef dispatcher workspaces, project configuration, and managed AGENTS.md instructions. Use when creating a TaskChef workspace, adding, importing, listing, or removing configured projects, running TaskChef doctor, or repairing dispatcher setup. Do not dispatch user work or reconcile executor threads."
4
4
  ---
5
5
 
6
6
  # TaskChef Bootstrap
7
7
 
8
8
  Initialize or refresh a data-only TaskChef dispatcher workspace.
9
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
10
+ Resolve this skill directory with `realpath`. The TaskChef plugin root is two
11
+ parents above the skill directory. Invoke `<plugin-root>/bin/taskchef.js` for
12
12
  all deterministic workspace operations.
13
13
 
14
14
  ## Boundaries
15
15
 
16
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.
17
+ - Keep only `AGENTS.md`, `taskchef.json`, and `tasks/*/task.json` in a
18
+ dispatcher workspace.
19
19
  - Do not dispatch tasks or reconcile executor threads during bootstrap unless
20
20
  the user separately requests those actions.
21
21
  - Never use collaboration agents, hooks, schedules, polling, or daemons.
@@ -24,7 +24,8 @@ all deterministic workspace operations.
24
24
 
25
25
  1. Run `workspace init --json`. It takes no stdin, creates an empty
26
26
  configuration when missing, and idempotently creates or refreshes the task
27
- directory, managed instructions, and all three skill links.
27
+ directory and managed instructions. The installed plugin provides all three
28
+ TaskChef skills outside the dispatcher workspace.
28
29
  2. Run `doctor --json` after setup or when the user asks to diagnose the
29
30
  workspace. Doctor is read-only; rerun `workspace init --json` to repair the
30
31
  managed scaffold.
@@ -7,15 +7,15 @@ description: "Dispatch actionable requests from an initialized TaskChef workspac
7
7
 
8
8
  Create real Codex tasks from a TaskChef data workspace and return immediately.
9
9
 
10
- Resolve this linked skill with `realpath`. The TaskChef source root is two
10
+ Resolve this skill directory with `realpath`. The TaskChef plugin root is two
11
11
  parents above the skill directory. Use the TaskChef executable under that root
12
12
  for all deterministic workspace and task-record operations.
13
13
 
14
14
  ## Boundaries
15
15
 
16
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.
17
+ - Keep only `AGENTS.md`, `taskchef.json`, and `tasks/*/task.json` in a
18
+ dispatcher workspace.
19
19
  - Use real Codex tasks, never collaboration or subagent tools.
20
20
  - Never use hooks, callbacks, schedules, polling, daemons, or event logs.
21
21
  - Never wait for delegated work after executor creation.
@@ -24,7 +24,7 @@ for all deterministic workspace and task-record operations.
24
24
  ## Dispatch
25
25
 
26
26
  1. Run
27
- `<source-root>/bin/taskchef.js project list --json --workspace <workspace>`
27
+ `<plugin-root>/bin/taskchef.js project list --json --workspace <workspace>`
28
28
  to load and validate the configured routing targets. Use
29
29
  `$taskchef-bootstrap` if the workspace is missing or unhealthy.
30
30
  2. Split the request into the smallest independently useful outcomes. Include
@@ -34,15 +34,15 @@ for all deterministic workspace and task-record operations.
34
34
  clear project match.
35
35
  4. Resolve native projects once and require the exact configured path.
36
36
  5. For an explicit retry, require the exact task ID and run
37
- `<source-root>/bin/taskchef.js task show <task-id> --json --workspace <workspace>`.
37
+ `<plugin-root>/bin/taskchef.js task show <task-id> --json --workspace <workspace>`.
38
38
  Reuse the record only when its status is `pending`; ask for the task ID when
39
39
  it is missing and reject retries of non-pending records. For new work, run
40
- `<source-root>/bin/taskchef.js task create --json --workspace <workspace>`
40
+ `<plugin-root>/bin/taskchef.js task create --json --workspace <workspace>`
41
41
  with the task record JSON on stdin before executor creation.
42
42
  6. Create one real Codex task per record using the exact saved project and a
43
43
  local environment on its executor host.
44
44
  7. Immediately run
45
- `<source-root>/bin/taskchef.js task update <task-id> --json --workspace <workspace>`
45
+ `<plugin-root>/bin/taskchef.js task update <task-id> --json --workspace <workspace>`
46
46
  with the `running` status and returned `threadId` on stdin. Never persist
47
47
  `hostId`.
48
48
  8. Leave a failed creation pending. Do not invent an ID or delete the record.
@@ -7,8 +7,8 @@ description: "Reconcile active TaskChef task records with their visible Codex ex
7
7
 
8
8
  Refresh TaskChef's recorded task state from native Codex threads exactly once.
9
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
10
+ Resolve this skill directory with `realpath`. The TaskChef plugin root is two
11
+ parents above the skill directory. Invoke `<plugin-root>/bin/taskchef.js` for
12
12
  all deterministic record operations.
13
13
 
14
14
  ## Reconcile
package/src/cli.js CHANGED
@@ -107,7 +107,7 @@ async function initialize(args) {
107
107
  `Workspace: ${value.workspace}`,
108
108
  `Configuration: ${value.config.action}`,
109
109
  `Instructions: ${value.instructions.action}`,
110
- `Skills: ${value.skills.skills.map((skill) => `${skill.name}=${skill.action}`).join(", ")}`,
110
+ `Legacy skill links removed: ${value.legacySkills.removed.length}`,
111
111
  ].join("\n"));
112
112
  return 0;
113
113
  }
package/src/workspace.js CHANGED
@@ -4,13 +4,12 @@ import {
4
4
  lstat,
5
5
  mkdir,
6
6
  readFile,
7
- readlink,
8
7
  readdir,
9
8
  realpath,
10
9
  link,
11
10
  rename,
11
+ rmdir,
12
12
  stat,
13
- symlink,
14
13
  unlink,
15
14
  writeFile,
16
15
  } from "node:fs/promises";
@@ -244,38 +243,73 @@ export async function ensureWorkspaceInstructions(workspaceRoot) {
244
243
  return { path: filePath, action };
245
244
  }
246
245
 
247
- async function ensureSkillLink(skillsDirectory, skillName) {
248
- const source = path.join(SKILLS_SOURCE_ROOT, skillName);
249
- const destination = path.join(skillsDirectory, skillName);
250
- await canonicalDirectory(source);
251
- const details = await lstat(destination).catch((error) => {
246
+ async function findLegacySkillLinks(workspaceRoot) {
247
+ const agentsDirectory = path.join(workspaceRoot, ".agents");
248
+ const agentsDetails = await lstat(agentsDirectory).catch((error) => {
252
249
  if (error.code === "ENOENT") return null;
253
250
  throw error;
254
251
  });
255
- if (details === null) {
256
- await symlink(source, destination, "dir");
257
- return { name: skillName, path: destination, action: "created" };
252
+ if (agentsDetails === null) return { agentsDirectory: null, skillsDirectory: null, links: [] };
253
+ if (agentsDetails.isSymbolicLink() || !agentsDetails.isDirectory()) {
254
+ return { agentsDirectory: null, skillsDirectory: null, links: [] };
258
255
  }
259
- if (!details.isSymbolicLink()) {
260
- throw new Error(`TaskChef skill path exists and is not a symlink: ${destination}`);
256
+
257
+ const skillsDirectory = path.join(agentsDirectory, "skills");
258
+ const skillsDetails = await lstat(skillsDirectory).catch((error) => {
259
+ if (error.code === "ENOENT") return null;
260
+ throw error;
261
+ });
262
+ if (skillsDetails === null) return { agentsDirectory, skillsDirectory, links: [] };
263
+ if (skillsDetails.isSymbolicLink() || !skillsDetails.isDirectory()) {
264
+ return { agentsDirectory, skillsDirectory: null, links: [] };
265
+ }
266
+
267
+ const links = [];
268
+ for (const skillName of TASKCHEF_SKILL_NAMES) {
269
+ const skillPath = path.join(skillsDirectory, skillName);
270
+ const details = await lstat(skillPath).catch((error) => {
271
+ if (error.code === "ENOENT") return null;
272
+ throw error;
273
+ });
274
+ if (details === null) continue;
275
+ if (!details.isSymbolicLink()) {
276
+ throw new Error(`legacy TaskChef skill path is not a symlink: ${skillPath}`);
277
+ }
278
+ links.push({ name: skillName, path: skillPath });
279
+ }
280
+ return { agentsDirectory, skillsDirectory, links };
281
+ }
282
+
283
+ async function removeLegacySkillLinks(workspaceRoot) {
284
+ const legacy = await findLegacySkillLinks(workspaceRoot);
285
+ for (const link of legacy.links) await unlink(link.path);
286
+ const removedDirectories = [];
287
+ if (legacy.skillsDirectory && (await readdir(legacy.skillsDirectory)).length === 0) {
288
+ await rmdir(legacy.skillsDirectory);
289
+ removedDirectories.push(legacy.skillsDirectory);
261
290
  }
262
- const linked = path.resolve(path.dirname(destination), await readlink(destination));
263
- if (linked !== source) {
264
- await unlink(destination);
265
- await symlink(source, destination, "dir");
266
- return { name: skillName, path: destination, action: "updated" };
291
+ if (legacy.agentsDirectory && (await readdir(legacy.agentsDirectory).catch((error) => {
292
+ if (error.code === "ENOENT") return null;
293
+ throw error;
294
+ }))?.length === 0) {
295
+ await rmdir(legacy.agentsDirectory);
296
+ removedDirectories.push(legacy.agentsDirectory);
267
297
  }
268
- return { name: skillName, path: destination, action: "unchanged" };
298
+ return { removed: legacy.links, removedDirectories };
269
299
  }
270
300
 
271
301
  export async function ensureWorkspaceSkills(workspaceRoot) {
272
302
  const root = await realpath(path.resolve(workspaceRoot));
273
- const skillsDirectory = await ensureManagedDirectory(root, ".agents", "skills");
274
- const skills = [];
275
- for (const skillName of TASKCHEF_SKILL_NAMES) {
276
- skills.push(await ensureSkillLink(skillsDirectory, skillName));
277
- }
278
- return { directory: skillsDirectory, skills };
303
+ const legacySkills = await removeLegacySkillLinks(root);
304
+ return {
305
+ directory: null,
306
+ skills: TASKCHEF_SKILL_NAMES.map((name) => ({
307
+ name,
308
+ path: path.join(SKILLS_SOURCE_ROOT, name),
309
+ action: "provided-by-plugin",
310
+ })),
311
+ legacySkills,
312
+ };
279
313
  }
280
314
 
281
315
  export async function canonicalGitRoot(projectPath) {
@@ -485,6 +519,7 @@ export async function initializeWorkspace(workspaceRoot) {
485
519
  const requestedRoot = path.resolve(workspaceRoot);
486
520
  await mkdir(requestedRoot, { recursive: true });
487
521
  const root = await realpath(requestedRoot);
522
+ const { legacySkills } = await ensureWorkspaceSkills(root);
488
523
  await ensureManagedDirectory(root, "tasks");
489
524
  const configPath = path.join(root, "taskchef.json");
490
525
  const configExists = await managedRegularFileExists(configPath);
@@ -496,16 +531,12 @@ export async function initializeWorkspace(workspaceRoot) {
496
531
  if (!configExists) await unlink(configPath).catch(() => {});
497
532
  throw error;
498
533
  });
499
- const skills = await ensureWorkspaceSkills(root).catch(async (error) => {
500
- if (!configExists) await unlink(configPath).catch(() => {});
501
- throw error;
502
- });
503
534
  return {
504
535
  workspace: root,
505
536
  config: { path: configPath, action: configExists ? "unchanged" : "created", value: config },
506
537
  tasks: { path: path.join(root, "tasks"), action: "ready" },
507
538
  instructions,
508
- skills,
539
+ legacySkills,
509
540
  };
510
541
  }
511
542
 
@@ -834,18 +865,13 @@ export async function doctorWorkspace(workspaceRoot) {
834
865
  }
835
866
  return "managed AGENTS.md instructions current";
836
867
  });
837
- for (const skillName of TASKCHEF_SKILL_NAMES) {
838
- await check(`skill:${skillName}`, async () => {
839
- const destination = path.join(root, ".agents", "skills", skillName);
840
- const details = await lstat(destination);
841
- if (!details.isSymbolicLink()) throw new Error("skill path is not a symlink");
842
- const linked = path.resolve(path.dirname(destination), await readlink(destination));
843
- const expected = path.join(SKILLS_SOURCE_ROOT, skillName);
844
- if (linked !== expected) throw new Error(`unexpected target: ${linked}`);
845
- await canonicalDirectory(expected);
846
- return "skill link valid";
847
- });
848
- }
868
+ await check("legacy-skill-links", async () => {
869
+ const legacy = await findLegacySkillLinks(root);
870
+ if (legacy.links.length > 0) {
871
+ throw new Error("legacy TaskChef skill links remain; run workspace init to remove them");
872
+ }
873
+ return "no legacy TaskChef skill links";
874
+ });
849
875
  await check("task-records", async () => {
850
876
  const taskRoot = path.join(root, "tasks");
851
877
  const entries = await readdir(taskRoot, { withFileTypes: true });