taskchef 7.2.0 → 7.4.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.2.0",
3
+ "version": "7.4.0",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/README.md CHANGED
@@ -33,7 +33,8 @@ npm install --global taskchef
33
33
 
34
34
  The plugin provides four skills and a local MCP server. The npm installation
35
35
  puts the `taskchef` CLI on `PATH`. TaskChef installs no hooks, schedules,
36
- daemons, or background identity search.
36
+ daemons, login items, system services, or background identity search and needs
37
+ no elevated permissions.
37
38
 
38
39
  ## Bootstrap and configure
39
40
 
@@ -87,17 +88,24 @@ $taskchef-delegate In payments, add structured logs for failed retries and test
87
88
 
88
89
  TaskChef prepares a UUID and marker, persists the task before native creation,
89
90
  creates the executor, and returns its task link. New executor instructions keep
90
- the assignment visible immediately after the marker and end with an explicit
91
- `$taskchef-executor` invocation. That skill reads the executor's own
92
- `CODEX_THREAD_ID`, self-links, and reports lifecycle state. Independent outcomes
93
- may become separate executors; dependent work should stay together.
91
+ the assignment visible from the first line, then place the correlation marker
92
+ immediately before an explicit `$taskchef-executor` invocation. That skill
93
+ reads the executor's own `CODEX_THREAD_ID`, self-links, and reports lifecycle
94
+ state. Independent outcomes may become separate executors; dependent work
95
+ should stay together.
96
+
97
+ At the start of every dispatcher turn, the managed workspace instructions ask
98
+ the MCP server to best-effort ensure the dashboard. A startup failure never
99
+ blocks an answer, report, or delegation. Every dispatcher response ends with
100
+ the stable [TaskChef Dashboard](http://127.0.0.1:3210/) link; a created-task
101
+ directive remains on the preceding line so dispatch still returns immediately.
94
102
 
95
103
  For example, TaskChef generates this shape:
96
104
 
97
105
  ```text
98
- <!-- taskchef_id=c0f010ff-84f2-4838-a69d-0ff1f5d721d7 -->
99
106
  Fix duplicate charges after a retry and add a regression test.
100
107
 
108
+ <!-- taskchef_id=c0f010ff-84f2-4838-a69d-0ff1f5d721d7 -->
101
109
  Use $taskchef-executor to execute and report this delegated TaskChef assignment.
102
110
  ```
103
111
 
@@ -145,12 +153,35 @@ an unambiguous eight-character prefix.
145
153
 
146
154
  ## Dashboard
147
155
 
156
+ Dispatcher turns call the input-free `ensure_dashboard` MCP tool. It starts at
157
+ most one dashboard inside the existing TaskChef MCP process on
158
+ `127.0.0.1:3210`, or reuses a listener only when its bounded `/api/health`
159
+ identity proves the exact TaskChef/dashboard-server version and the same
160
+ canonical workspace. The response says `started` or `reused` and includes the
161
+ stable URL, canonical workspace, and versions.
162
+
163
+ The in-process dashboard closes with the MCP process. Closing Codex or reloading
164
+ the plugin may therefore stop the dashboard; the next dispatcher turn restores
165
+ it. TaskChef adds no OS-persistent component.
166
+
167
+ For manual development, run the foreground CLI:
168
+
148
169
  ```sh
149
170
  taskchef dashboard
171
+ taskchef dashboard --port 3211
150
172
  ```
151
173
 
152
174
  The loopback dashboard watches `tasks.jsonl`, groups current states, and opens
153
175
  linked Codex tasks. It does not mutate TaskChef data and prints its local URL.
176
+ When a compatible foreground dashboard already owns port 3210,
177
+ `ensure_dashboard` reuses it but does not take ownership. If an unknown,
178
+ different-workspace, or stale-version process owns the port, TaskChef reports a
179
+ concise conflict and never kills or replaces that process. The foreground CLI
180
+ similarly asks you to stop the listener or choose another `--port`.
181
+
182
+ The health endpoint contains only a fixed service marker, health schema,
183
+ TaskChef version, dashboard-server version, and canonical workspace. It exposes
184
+ no task data, credentials, environment variables, process control, or secrets.
154
185
 
155
186
  ## Common recovery
156
187
 
@@ -3,6 +3,9 @@
3
3
 
4
4
  This folder is the canonical per-user TaskChef dispatcher workspace.
5
5
 
6
+ - Early in every dispatcher turn, best-effort call the TaskChef
7
+ `ensure_dashboard` MCP tool. Dashboard startup failure must not block direct
8
+ TaskChef answers, reporting, or delegation.
6
9
  - Use `$taskchef-bootstrap` when initializing or refreshing this workspace,
7
10
  changing or listing its configured projects, running TaskChef doctor,
8
11
  or repairing its managed instructions.
@@ -20,4 +23,10 @@ This folder is the canonical per-user TaskChef dispatcher workspace.
20
23
  write status or results to this workspace.
21
24
  - Explicit invocations of TaskChef skills from other Codex projects use this
22
25
  same workspace and task history through TaskChef's global resolution rules.
26
+ - Every final response in this dispatcher workspace must end with this exact
27
+ clickable link on its final non-empty line:
28
+ `[TaskChef Dashboard](http://127.0.0.1:3210/)`. Keep any
29
+ `::created-thread{...}` directive required by delegation on its own line
30
+ immediately before the dashboard link so the immediate-return contract still
31
+ holds. Include the link even when `ensure_dashboard` failed.
23
32
  <!-- taskchef:dispatcher-instructions:end -->
@@ -108,7 +108,7 @@ also insert an interpretation layer between user and worker.
108
108
 
109
109
  ### TaskChef
110
110
 
111
- **Fact:** `prepare_dispatch` allocates a TaskChef UUID and exact first-line
111
+ **Fact:** `prepare_dispatch` allocates a TaskChef UUID and exact correlation
112
112
  marker. `record_task` persists the marked instruction before native task
113
113
  creation. The executor then reads its own `CODEX_THREAD_ID` and calls
114
114
  `link_task`; the dispatcher neither searches recent tasks nor repairs identity.
package/docs/spec.md CHANGED
@@ -18,7 +18,7 @@ is dated research, not contract.
18
18
  | **Delegated task** | One independently useful outcome represented by one TaskChef task UUID and snapshot. |
19
19
  | **Executor** | The native Codex task created to own and perform one delegated task. |
20
20
  | **Task record** | One complete JSON object in `tasks.jsonl`; it contains immutable intent/project fields and mutable identity/result fields. |
21
- | **Marker** | The exact first instruction line `<!-- taskchef_id=<lowercase full UUID> -->`; new instructions begin the assignment on the next line. |
21
+ | **Marker** | The exact correlation line `<!-- taskchef_id=<lowercase full UUID> -->`; new instructions place it after the complete assignment and immediately before the executor-skill invocation. |
22
22
  | **Record-before-create** | Persisting a link-pending task before asking Codex to create its executor. |
23
23
  | **Self-linking** | The executor's one-way registration of its own canonical Codex UUIDv7 from `CODEX_THREAD_ID`. |
24
24
  | **Link-pending** | A working task whose `threadId` is null and `updatedBy` is `dispatcher`. |
@@ -39,8 +39,8 @@ is dated research, not contract.
39
39
  privacy, and idempotency. It MUST NOT dispatch the owned assignment again.
40
40
  - `taskchef-report` MUST own on-demand reporting. It MUST NOT poll or persist
41
41
  inferred state.
42
- - The MCP server MUST expose four primary lifecycle tools plus the deprecated
43
- `report_result` compatibility alias specified below.
42
+ - The MCP server MUST expose `ensure_dashboard`, four primary lifecycle tools,
43
+ and the deprecated `report_result` compatibility alias specified below.
44
44
  - The CLI MAY administer and inspect the workspace, but MUST NOT provide a
45
45
  second agent lifecycle protocol.
46
46
  - The dashboard MUST be read-only with respect to dispatcher files.
@@ -100,12 +100,20 @@ fields MUST NOT change after recording.
100
100
 
101
101
  ## Required lifecycle
102
102
 
103
+ At the start of every dispatcher turn, the dispatcher SHOULD call
104
+ `ensure_dashboard` best-effort. Failure MUST NOT block direct TaskChef answers,
105
+ reporting, or delegation. Every dispatcher final response MUST end with the
106
+ exact clickable `[TaskChef Dashboard](http://127.0.0.1:3210/)` link even when
107
+ ensure failed. A created-thread directive MUST remain on its own line before
108
+ the final link, preserving the delegate skill's immediate-return contract.
109
+
103
110
  1. The dispatcher MUST call `prepare_dispatch` once per outcome.
104
111
  2. It MUST choose exactly one configured project and exact native-project path.
105
- 3. It MUST build the instruction with the returned marker as line 1, the user's
106
- outcome beginning on line 2, and exactly one concise explicit
107
- `$taskchef-executor` invocation at the end after one blank line. It MUST NOT
108
- inline the executor protocol into a new instruction.
112
+ 3. It MUST build the instruction with the user's outcome beginning on line 1
113
+ and remaining uninterrupted, followed by one blank line, the returned
114
+ marker, and exactly one concise explicit `$taskchef-executor` invocation on
115
+ the final line. It MUST NOT inline the executor protocol into a new
116
+ instruction.
109
117
  4. It MUST call `record_task` with `threadId: null` before native creation.
110
118
  5. It MUST create exactly one native Codex executor and return immediately.
111
119
  6. The executor MUST read its own `CODEX_THREAD_ID` and call `link_task`
@@ -122,10 +130,15 @@ If native creation fails after recording, the dispatcher MUST call
122
130
  A link failure MUST remain visible and retryable; the executor MUST report it
123
131
  visibly and MUST NOT continue substantive work.
124
132
 
125
- Previously recorded instructions with the former blank line and inline
126
- executor protocol MUST remain marker-readable and executable. Their
133
+ Previously recorded instructions with a first-line HTML marker, the older
134
+ first-line `# taskchef_id=<full UUID>` heading, or the former blank line and
135
+ inline executor protocol MUST remain marker-readable and executable. Their
127
136
  `report_result` calls MUST remain supported by the deprecated alias. New
128
- instructions MUST use the explicit executor skill contract above.
137
+ instructions MUST use the trailing marker and explicit executor skill contract
138
+ above. A historical first-line instruction with an executor-skill invocation
139
+ MUST contain exactly one invocation as its final line. A former inline-protocol
140
+ instruction MUST retain non-whitespace task-specific content beyond its known
141
+ lifecycle paragraphs.
129
142
 
130
143
  `needs_input` MUST mean a semantic user decision or missing fact. A native
131
144
  approval prompt MUST remain live Codex state and MUST NOT be stored as
@@ -138,6 +151,43 @@ path. Success returns both one text content item and the stated structured
138
151
  object. Validation, marker, identity, uniqueness, freshness, or filesystem
139
152
  failures are surfaced as tool errors and MUST NOT partially mutate the log.
140
153
 
154
+ ### `ensure_dashboard`
155
+
156
+ **Caller:** dispatcher. **Mutation:** starts at most one in-process loopback
157
+ HTTP server; it does not mutate dispatcher workspace files.
158
+
159
+ **Input:** empty object.
160
+
161
+ **Structured output:**
162
+
163
+ ```text
164
+ { dashboard: {
165
+ action: "started" | "reused",
166
+ url: "http://127.0.0.1:3210/",
167
+ workspace: string,
168
+ taskchefVersion: string,
169
+ serverVersion: string
170
+ } }
171
+ ```
172
+
173
+ Calls MUST serialize within one MCP process. The first call starts an owned
174
+ dashboard or reuses an exact compatible listener; later and concurrent calls
175
+ are idempotent and report reuse after the single start. The stable default MUST
176
+ bind only to `127.0.0.1:3210` and MUST NOT accept a model-supplied workspace,
177
+ host, or port.
178
+
179
+ Before reuse, TaskChef MUST query a bounded loopback identity endpoint and
180
+ require the exact fixed service/schema, TaskChef version, dashboard-server
181
+ version, and canonical workspace. An unknown, malformed, different-workspace,
182
+ or stale-version listener MUST produce a concise actionable conflict. TaskChef
183
+ MUST NOT kill, replace, signal, or otherwise control that listener. A startup
184
+ failure MUST leave no owned listener. The MCP server MUST close its owned
185
+ dashboard when its transport or process shuts down; it MUST NOT close a reused
186
+ external foreground server.
187
+
188
+ **Annotations:** `readOnlyHint: false`, `destructiveHint: false`,
189
+ `openWorldHint: false`.
190
+
141
191
  ### `prepare_dispatch`
142
192
 
143
193
  **Caller:** dispatcher. **Mutation:** none.
@@ -176,7 +226,7 @@ new preparation values, though it writes no state.
176
226
  | `id` | Non-empty string; MUST equal the instruction marker. |
177
227
  | `project` | Non-empty configured project path. |
178
228
  | `title` | Non-empty string. |
179
- | `instruction` | Non-empty string beginning with the exact marker and at least one following instruction line. |
229
+ | `instruction` | Non-empty string containing exactly one accepted marker and a non-empty assignment. New instructions use the required trailing marker and executor-invocation scaffold; historical first-line forms remain accepted. |
180
230
  | `threadId` | Literal null. |
181
231
 
182
232
  **Structured output:** `{ task: Task }`.
@@ -253,7 +303,13 @@ reports MAY read a selected task once when metadata is newer or evidence is
253
303
  uncertain. Reports MUST NOT poll or classify assistant prose.
254
304
 
255
305
  The dashboard MUST bind only to loopback, validate the current workspace
256
- snapshot, and avoid sessions or shared client state. Direct thread navigation
306
+ snapshot, and avoid sessions or shared client state. `GET /api/health` MUST
307
+ return only the bounded service identity, health schema, exact TaskChef and
308
+ dashboard-server versions, and canonical workspace. It MUST NOT return task
309
+ records, secrets, credentials, environment values, or process-control data.
310
+ Identity remains available while an already-started monitor retains its last
311
+ valid snapshot after a later invalid task log; an invalid initial log MAY fail
312
+ startup safely. Direct thread navigation
257
313
  MUST require a canonical Codex UUIDv7. Otherwise it MAY open the revalidated
258
314
  configured project. Project paths from task history MUST be matched against
259
315
  current configuration before use.
@@ -273,3 +329,9 @@ loopback origin, and current configuration before acting.
273
329
  TaskChef MUST NOT use lifecycle hooks, schedules, polling, recent-thread search,
274
330
  transcript search, title matching, hidden reasoning, or token usage to discover
275
331
  identity or infer semantic results.
332
+
333
+ TaskChef MUST NOT install or require daemons, launchd agents, login items,
334
+ system services, cron jobs, hooks, privileged components, or elevated/system
335
+ permissions for dashboard availability. Availability is best-effort while the
336
+ owning Codex/plugin MCP process is alive and is not guaranteed while Codex is
337
+ closed.
package/docs/workflows.md CHANGED
@@ -14,16 +14,55 @@ research.
14
14
  | `skills/taskchef-executor/SKILL.md` | Own, self-link, execute, and report every executor turn. |
15
15
  | `skills/taskchef-bootstrap/SKILL.md` | Initialize current workspace and configure projects. |
16
16
  | `skills/taskchef-report/SKILL.md` | Select cached tasks and perform bounded live checks. |
17
- | `src/mcp.js` | Four primary lifecycle tools, one deprecated alias, and MCP annotations. |
17
+ | `src/mcp.js` | Dashboard ensure, four primary lifecycle tools, one deprecated alias, shutdown ownership, and MCP annotations. |
18
18
  | `src/delegation.js` | UUID marker, concise executor-skill invocation shape, and creation-failure handling. |
19
19
  | `src/workspace.js` | Current schemas, validation, locking, atomic JSONL writes, linking, and result freshness. |
20
20
  | `src/cli.js` | Administration, inspection, diagnostics, and dashboard startup. |
21
- | `src/dashboard.js` | Validated snapshots, SSE fan-out, and bounded open actions. |
21
+ | `src/dashboard.js` | Versioned health identity, validated snapshots, SSE fan-out, and bounded open actions. |
22
+ | `src/dashboard-manager.js` | Concurrent singleton ensure, exact listener reuse, conflicts, and owned shutdown. |
22
23
 
23
24
  The MCP process resolves `TASKCHEF_WORKSPACE` once and never accepts a model
24
25
  supplied path. The CLI resolves `--workspace`, then the environment, then the
25
26
  per-user default.
26
27
 
28
+ ## Dispatcher dashboard lifecycle
29
+
30
+ The generated managed `AGENTS.md` block makes dashboard maintenance a
31
+ best-effort prelude to every dispatcher turn and keeps response ordering
32
+ centralized instead of duplicating it across delegate/report skills.
33
+
34
+ ```mermaid
35
+ sequenceDiagram
36
+ autonumber
37
+ participant D as Dispatcher
38
+ participant M as TaskChef MCP
39
+ participant H as Loopback health
40
+ participant S as Dashboard server
41
+ D->>M: ensure_dashboard()
42
+ M->>M: Serialize concurrent ensure calls
43
+ M->>H: GET 127.0.0.1:3210/api/health
44
+ alt Exact service, versions, and canonical workspace
45
+ H-->>M: Bounded compatible identity
46
+ M-->>D: reused, URL, workspace, versions
47
+ else No listener
48
+ H--xM: Connection refused
49
+ M->>S: Start in this MCP process on 127.0.0.1:3210
50
+ S-->>M: Owned server
51
+ M-->>D: started, URL, workspace, versions
52
+ else Unknown, stale, or different workspace
53
+ H-->>M: Missing or incompatible identity
54
+ M-->>D: Actionable conflict, listener untouched
55
+ end
56
+ Note over D: Continue even when ensure failed
57
+ D-->>D: Answer, report, or dispatch
58
+ Note over D: Created-thread directive, when any, precedes final dashboard link
59
+ ```
60
+
61
+ When the MCP transport or plugin process closes, it closes only the server it
62
+ started. A compatible foreground `taskchef dashboard` listener may be reused
63
+ but remains owned by that CLI process. No TaskChef path terminates an unknown
64
+ listener or installs OS persistence.
65
+
27
66
  ## Normal delegation and self-linking
28
67
 
29
68
  The dispatcher uses native Codex project discovery for routing and MCP for
@@ -68,10 +107,11 @@ Record-before-create makes native creation failure observable. Executor
68
107
  self-linking removes dispatcher-side polling, task search, title matching, and
69
108
  parent/child identity inference.
70
109
 
71
- The generated task body begins immediately after the first-line marker and
72
- ends with one explicit `$taskchef-executor` invocation. Older recorded tasks
73
- whose marker is followed by a blank line and inline protocol remain readable;
74
- the deprecated `report_result` alias preserves their semantic callbacks.
110
+ The generated task begins with the complete assignment, then places its marker
111
+ immediately before one explicit `$taskchef-executor` invocation. Older
112
+ recorded tasks with first-line HTML or heading markers and former inline
113
+ protocol remain readable; the deprecated `report_result` alias preserves their
114
+ semantic callbacks.
75
115
 
76
116
  ## State reporting
77
117
 
@@ -234,8 +274,9 @@ sequenceDiagram
234
274
  ```
235
275
 
236
276
  The dashboard binds to `127.0.0.1`, has no shared session state, limits
237
- request bodies, and checks origin/authority for stateful local actions. Historical
238
- project paths are untrusted until matched against current configuration.
277
+ request bodies, and checks origin/authority for stateful local actions. Its
278
+ bounded identity endpoint contains no task data or secrets. Historical project
279
+ paths are untrusted until matched against current configuration.
239
280
 
240
281
  ## Concurrency and trust boundaries
241
282
 
package/index.js CHANGED
@@ -57,10 +57,19 @@ export {
57
57
  } from "./src/codex-app.js";
58
58
 
59
59
  export {
60
+ DASHBOARD_HEALTH_MAX_BYTES,
61
+ DASHBOARD_HEALTH_PATH,
60
62
  DashboardMonitor,
61
63
  createDashboardServer,
62
64
  dashboardAuthority,
63
65
  sortTasksByMeaningfulUpdate,
64
66
  } from "./src/dashboard.js";
65
67
 
68
+ export {
69
+ createDashboardManager,
70
+ readDashboardIdentity,
71
+ } from "./src/dashboard-manager.js";
72
+
73
+ export { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./src/version.js";
74
+
66
75
  export { createTaskChefMcpServer } from "./src/mcp.js";
package/mcp/server.js CHANGED
@@ -5,3 +5,15 @@ import { createTaskChefMcpServer } from "../src/mcp.js";
5
5
 
6
6
  const server = createTaskChefMcpServer();
7
7
  await server.connect(new StdioServerTransport());
8
+
9
+ let shutdownPromise = null;
10
+ const shutdown = () => {
11
+ shutdownPromise ??= server.close().catch((error) => {
12
+ process.stderr.write(`TaskChef MCP shutdown failed: ${error.message}\n`);
13
+ process.exitCode = 1;
14
+ });
15
+ return shutdownPromise;
16
+ };
17
+
18
+ process.once("SIGINT", shutdown);
19
+ process.once("SIGTERM", shutdown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.2.0",
3
+ "version": "7.4.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -19,8 +19,10 @@ all deterministic workspace operations.
19
19
  dispatcher workspace. Preserve unrelated user-owned paths.
20
20
  - Do not dispatch tasks or report on executor threads during bootstrap unless
21
21
  the user separately requests those actions.
22
- - Never create hooks, schedules, polling, or daemons. TaskChef executors
23
- self-link through the installed MCP server.
22
+ - Never create hooks, schedules, polling, daemons, login items, or system
23
+ services. The managed dispatcher instructions own best-effort dashboard
24
+ startup and final-link guidance; bootstrap only refreshes that managed block.
25
+ TaskChef executors self-link through the installed MCP server.
24
26
 
25
27
  ## Initialize and repair
26
28
 
@@ -10,11 +10,18 @@ and return immediately.
10
10
 
11
11
  ## Invocation boundary
12
12
 
13
- A task whose initial structured `codexDelegation.input` starts with an exact
14
- `<!-- taskchef_id=<full UUID> -->` marker already owns that delegated
15
- assignment. Execute it in the current task. Do not re-dispatch it merely
16
- because it concerns TaskChef or a configured project. Explicit requests to
17
- delegate separate work remain valid.
13
+ A task whose initial structured `codexDelegation.input` contains either the
14
+ exact new trailing TaskChef marker plus final `$taskchef-executor` invocation,
15
+ an exact first-line HTML marker, or the historical first-line
16
+ `# taskchef_id=<full UUID>` heading already owns that delegated assignment.
17
+ This includes former inline-protocol tasks that lack the skill invocation.
18
+ An owned instruction must have exactly one accepted marker and a non-whitespace
19
+ task-specific assignment. If it contains an executor-skill invocation, require
20
+ exactly one as the final line. Marker-only, duplicate-marker, scaffold-only, or
21
+ misplaced-invocation inputs are not valid delegated tasks. Execute a valid one
22
+ in the current task. Do not re-dispatch it merely because it concerns TaskChef
23
+ or a configured project. Explicit requests to delegate separate work remain
24
+ valid.
18
25
 
19
26
  Use the bundled `prepare_dispatch`, `record_task`, and `report_state` MCP tools
20
27
  directly. Never fall back to shell writes. If a required tool is unavailable,
@@ -43,11 +50,13 @@ stop and report that the TaskChef plugin must be reloaded or installed.
43
50
  match and an exact native-project path. Ask instead of guessing.
44
51
  4. Build each executor instruction in this exact shape:
45
52
 
46
- - Keep the preparation's exact marker as the first line.
47
- - Begin the actual assignment on the second line, with no blank line after
48
- the marker.
49
- - End the instruction after one blank line with exactly:
53
+ - Begin with the actual assignment on the first line and keep its complete
54
+ body uninterrupted.
55
+ - After the assignment, add one blank line and the preparation's exact
56
+ marker on its own line.
57
+ - Immediately after the marker, end the instruction with exactly:
50
58
  `Use $taskchef-executor to execute and report this delegated TaskChef assignment.`
59
+ - Include exactly one marker and exactly one executor-skill invocation.
51
60
  - Do not inline executor ownership, identity, linking, or result-reporting
52
61
  protocol. The explicitly invoked executor skill owns those mechanics.
53
62
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: taskchef-executor
3
- description: "Execute an assignment whose first instruction line is an exact TaskChef task marker, including executor ownership, self-linking, per-turn lifecycle reporting, identity safety, and final semantic state. Use when explicitly invoked by a delegated TaskChef instruction or when resuming that same executor task. Do not use to dispatch work or report on other TaskChef tasks."
3
+ description: "Execute an assignment carrying either the new exact TaskChef marker-plus-invocation scaffold or an accepted historical first-line TaskChef marker or inline protocol. Includes executor ownership, self-linking, per-turn lifecycle reporting, identity safety, and final semantic state. Use when explicitly invoked by a new delegated instruction or when resuming the same new or historical executor task. Do not use to dispatch work or report on other TaskChef tasks."
4
4
  ---
5
5
 
6
6
  # TaskChef Executor
@@ -9,10 +9,12 @@ Own and execute the delegated assignment in the current Codex task. Do not
9
9
  re-dispatch it merely because it concerns TaskChef or a configured project.
10
10
  Explicit requests to delegate separate work remain valid.
11
11
 
12
- The instruction's exact first line is
13
- `<!-- taskchef_id=<full UUID> -->`. Treat that UUID as the TaskChef task ID.
14
- The assignment is the remaining instruction body; the final explicit skill
15
- invocation is lifecycle scaffolding, not part of the requested deliverable.
12
+ New instructions present the complete assignment first, followed by one blank
13
+ line, the exact `<!-- taskchef_id=<full UUID> -->` marker, and the final
14
+ explicit skill invocation. Treat that UUID as the TaskChef task ID. The
15
+ assignment is everything before the blank line that precedes the marker; the
16
+ marker and invocation are lifecycle scaffolding, not part of the deliverable.
17
+ Require exactly one marker and do not infer an ID from similar prose.
16
18
 
17
19
  ## Start every execution turn
18
20
 
@@ -66,4 +68,12 @@ Existing delegated tasks may include the former inline ownership, linking, and
66
68
  re-dispatching. Prefer `report_state` when available. If an older installed
67
69
  TaskChef exposes only `report_result`, follow its inline protocol; after an
68
70
  upgrade, the deprecated `report_result` alias remains available for exact
69
- legacy retries.
71
+ legacy retries. Also accept historical instructions whose exact HTML marker is
72
+ the first line, with or without the former blank line, and the older exact
73
+ first-line `# taskchef_id=<full UUID>` heading. These compatibility forms do
74
+ not change the identity or lifecycle rules above. For either first-line form,
75
+ the assignment follows the marker. Ignore the final executor invocation and
76
+ any recognizable former inline ownership, linking, working-state, or
77
+ result-reporting paragraphs as lifecycle scaffolding; execute the remaining
78
+ task-specific body. Require non-whitespace task-specific content and never
79
+ treat an invocation by itself as an assignment.
package/src/cli.js CHANGED
@@ -409,10 +409,22 @@ async function dashboard(args) {
409
409
  values: ["--port", "--workspace"],
410
410
  switches: ["--json"],
411
411
  });
412
- const server = await createDashboardServer({
413
- workspace: workspaceRoot(args),
414
- port: dashboardPort(args),
415
- });
412
+ const port = dashboardPort(args);
413
+ let server;
414
+ try {
415
+ server = await createDashboardServer({
416
+ workspace: workspaceRoot(args),
417
+ port,
418
+ });
419
+ } catch (error) {
420
+ if (error?.code === "EADDRINUSE") {
421
+ throw new Error(
422
+ `dashboard port 127.0.0.1:${port} is already in use; `
423
+ + "stop the existing listener or choose another --port (TaskChef will not terminate it)",
424
+ );
425
+ }
426
+ throw error;
427
+ }
416
428
  print({
417
429
  schemaVersion: 1,
418
430
  url: server.url,
@@ -461,8 +473,10 @@ Project import reads a JSON
461
473
  array from a file, or from standard input when the source is '-' or omitted.
462
474
  Workspace resolution precedence is --workspace, TASKCHEF_WORKSPACE, then
463
475
  ~/.agents/taskchef.
464
- The dashboard binds to 127.0.0.1 and reads the canonical task log without
465
- modifying dispatcher-workspace files.
476
+ The foreground dashboard binds to 127.0.0.1 and reads the canonical task log
477
+ without modifying dispatcher-workspace files. The dispatcher MCP may reuse a
478
+ compatible foreground server on port 3210; neither mode terminates a listener
479
+ that already occupies its requested port.
466
480
  `);
467
481
  }
468
482
 
@@ -0,0 +1,183 @@
1
+ import http from "node:http";
2
+ import { realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ DASHBOARD_HEALTH_MAX_BYTES,
7
+ DASHBOARD_HEALTH_PATH,
8
+ createDashboardServer,
9
+ dashboardAuthority,
10
+ } from "./dashboard.js";
11
+ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
12
+
13
+ const DEFAULT_HOST = "127.0.0.1";
14
+ const DEFAULT_PORT = 3210;
15
+ const HEALTH_TIMEOUT_MS = 750;
16
+
17
+ function expectedIdentity(workspace, taskchefVersion, serverVersion) {
18
+ return {
19
+ schemaVersion: 1,
20
+ service: "taskchef-dashboard",
21
+ taskchefVersion,
22
+ serverVersion,
23
+ workspace,
24
+ };
25
+ }
26
+
27
+ function isExactIdentity(value, expected) {
28
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
29
+ const keys = Object.keys(value).sort();
30
+ const expectedKeys = Object.keys(expected).sort();
31
+ return keys.length === expectedKeys.length
32
+ && keys.every((key, index) => key === expectedKeys[index])
33
+ && expectedKeys.every((key) => value[key] === expected[key]);
34
+ }
35
+
36
+ function listenerConflict(url, detail) {
37
+ return new Error(
38
+ `TaskChef dashboard port conflict at ${url} ${detail} `
39
+ + "Stop that listener or choose another port for the foreground dashboard CLI; TaskChef will not terminate it.",
40
+ );
41
+ }
42
+
43
+ export function readDashboardIdentity({
44
+ host = DEFAULT_HOST,
45
+ port = DEFAULT_PORT,
46
+ maximumBytes = DASHBOARD_HEALTH_MAX_BYTES,
47
+ timeoutMs = HEALTH_TIMEOUT_MS,
48
+ } = {}) {
49
+ return new Promise((resolve, reject) => {
50
+ let settled = false;
51
+ const finish = (error, value) => {
52
+ if (settled) return;
53
+ settled = true;
54
+ clearTimeout(deadline);
55
+ if (error) reject(error);
56
+ else resolve(value);
57
+ };
58
+ const request = http.get({
59
+ host,
60
+ port,
61
+ path: DASHBOARD_HEALTH_PATH,
62
+ headers: {
63
+ Accept: "application/json",
64
+ Host: dashboardAuthority(host, port),
65
+ },
66
+ }, (response) => {
67
+ const chunks = [];
68
+ let total = 0;
69
+ response.on("data", (chunk) => {
70
+ total += chunk.length;
71
+ if (total > maximumBytes) {
72
+ const error = new Error("dashboard health response exceeds the identity limit");
73
+ finish(error);
74
+ request.destroy(error);
75
+ return;
76
+ }
77
+ chunks.push(chunk);
78
+ });
79
+ response.on("end", () => {
80
+ if (response.statusCode !== 200) {
81
+ finish(new Error(`dashboard health returned HTTP ${response.statusCode}`));
82
+ return;
83
+ }
84
+ try {
85
+ finish(null, JSON.parse(Buffer.concat(chunks, total).toString("utf8")));
86
+ } catch {
87
+ finish(new Error("dashboard health returned invalid JSON"));
88
+ }
89
+ });
90
+ });
91
+ const deadline = setTimeout(() => {
92
+ const error = new Error("dashboard health request timed out");
93
+ finish(error);
94
+ request.destroy(error);
95
+ }, timeoutMs);
96
+ request.on("error", (error) => finish(error));
97
+ });
98
+ }
99
+
100
+ function listenerAbsent(error) {
101
+ return error?.code === "ECONNREFUSED" || error?.code === "EHOSTUNREACH";
102
+ }
103
+
104
+ export function createDashboardManager({
105
+ workspace,
106
+ host = DEFAULT_HOST,
107
+ port = DEFAULT_PORT,
108
+ taskchefVersion = TASKCHEF_VERSION,
109
+ serverVersion = DASHBOARD_SERVER_VERSION,
110
+ createServer = createDashboardServer,
111
+ readIdentity = readDashboardIdentity,
112
+ } = {}) {
113
+ let canonicalWorkspace;
114
+ let ownedServer = null;
115
+ let ensurePromise = null;
116
+ let closePromise = null;
117
+
118
+ const publicResult = (action) => ({
119
+ action,
120
+ url: `http://${dashboardAuthority(host, ownedServer?.port ?? port)}/`,
121
+ workspace: canonicalWorkspace,
122
+ taskchefVersion,
123
+ serverVersion,
124
+ });
125
+
126
+ const probe = async () => {
127
+ const url = `http://${dashboardAuthority(host, port)}/`;
128
+ let identity;
129
+ try {
130
+ identity = await readIdentity({ host, port });
131
+ } catch (error) {
132
+ if (listenerAbsent(error)) return false;
133
+ throw listenerConflict(url, `is occupied but did not return a compatible identity (${error.message}).`);
134
+ }
135
+ const expected = expectedIdentity(canonicalWorkspace, taskchefVersion, serverVersion);
136
+ if (!isExactIdentity(identity, expected)) {
137
+ throw listenerConflict(url, "belongs to an unknown, stale, or different-workspace service.");
138
+ }
139
+ return true;
140
+ };
141
+
142
+ const ensureOnce = async () => {
143
+ canonicalWorkspace ??= await realpath(path.resolve(workspace));
144
+ if (ownedServer) return publicResult("reused");
145
+ if (await probe()) return publicResult("reused");
146
+ try {
147
+ ownedServer = await createServer({
148
+ workspace: canonicalWorkspace,
149
+ host,
150
+ port,
151
+ taskchefVersion,
152
+ serverVersion,
153
+ });
154
+ return publicResult("started");
155
+ } catch (error) {
156
+ if (error?.code !== "EADDRINUSE") throw error;
157
+ if (await probe()) return publicResult("reused");
158
+ throw error;
159
+ }
160
+ };
161
+
162
+ return {
163
+ async ensure() {
164
+ if (closePromise) throw new Error("TaskChef dashboard manager is shutting down");
165
+ if (ensurePromise) {
166
+ await ensurePromise;
167
+ return publicResult("reused");
168
+ }
169
+ ensurePromise = ensureOnce().finally(() => { ensurePromise = null; });
170
+ return ensurePromise;
171
+ },
172
+ async close() {
173
+ closePromise ??= (async () => {
174
+ await ensurePromise?.catch(() => {});
175
+ const server = ownedServer;
176
+ ownedServer = null;
177
+ await server?.close();
178
+ })();
179
+ return closePromise;
180
+ },
181
+ get owned() { return ownedServer !== null; },
182
+ };
183
+ }
package/src/dashboard.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  parseTaskLogContent,
17
17
  readConfig,
18
18
  } from "./workspace.js";
19
+ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
19
20
 
20
21
  const TASKS_FILE_NAME = "tasks.jsonl";
21
22
  const STATIC_ROOT = fileURLToPath(new URL("./dashboard/", import.meta.url));
@@ -24,6 +25,8 @@ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1"]);
24
25
  const DEFAULT_MAX_FILE_BYTES = 16 * 1024 * 1024;
25
26
  const DEFAULT_MAX_TASKS = 2_000;
26
27
  const DEFAULT_MAX_EVENT_CLIENTS = 16;
28
+ export const DASHBOARD_HEALTH_PATH = "/api/health";
29
+ export const DASHBOARD_HEALTH_MAX_BYTES = 8 * 1024;
27
30
  const CONTENT_SECURITY_POLICY = [
28
31
  "default-src 'self'",
29
32
  "base-uri 'none'",
@@ -402,6 +405,8 @@ export async function createDashboardServer({
402
405
  monitorOptions = {},
403
406
  openProject = null,
404
407
  openThread = null,
408
+ taskchefVersion = TASKCHEF_VERSION,
409
+ serverVersion = DASHBOARD_SERVER_VERSION,
405
410
  } = {}) {
406
411
  if (!LOOPBACK_HOSTS.has(host)) {
407
412
  throw new Error("dashboard host must be a loopback address");
@@ -414,6 +419,17 @@ export async function createDashboardServer({
414
419
  }
415
420
  const monitor = new DashboardMonitor(workspace, monitorOptions);
416
421
  await monitor.start();
422
+ const identity = Object.freeze({
423
+ schemaVersion: 1,
424
+ service: "taskchef-dashboard",
425
+ taskchefVersion,
426
+ serverVersion,
427
+ workspace: monitor.workspace,
428
+ });
429
+ if (Buffer.byteLength(`${JSON.stringify(identity)}\n`) > DASHBOARD_HEALTH_MAX_BYTES) {
430
+ monitor.close();
431
+ throw new Error("dashboard identity exceeds the health response limit");
432
+ }
417
433
  const clients = new Set();
418
434
  let allowedAuthority;
419
435
  let allowedOrigin;
@@ -446,6 +462,16 @@ export async function createDashboardServer({
446
462
  return;
447
463
  }
448
464
 
465
+ if (url.pathname === DASHBOARD_HEALTH_PATH && (method === "GET" || method === "HEAD")) {
466
+ if (method === "HEAD") {
467
+ response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
468
+ response.end();
469
+ } else {
470
+ sendJson(response, 200, identity);
471
+ }
472
+ return;
473
+ }
474
+
449
475
  if (url.pathname === "/api/snapshot" && (method === "GET" || method === "HEAD")) {
450
476
  if (method === "HEAD") {
451
477
  response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
@@ -574,6 +600,7 @@ export async function createDashboardServer({
574
600
  port: boundPort,
575
601
  origin: allowedOrigin,
576
602
  url: `${allowedOrigin}/`,
603
+ identity,
577
604
  monitor,
578
605
  get eventClientCount() { return clients.size; },
579
606
  async close() {
package/src/delegation.js CHANGED
@@ -9,11 +9,34 @@ export const EXECUTOR_WORKING_PARAGRAPH = "After a successful initial link, and
9
9
  /** @deprecated Historical v7 inline-prompt snapshot. New delegations use taskchef-executor. */
10
10
  export const EXECUTOR_RESULT_PARAGRAPH = "Before ending, read this exact Codex thread again and call TaskChef report_state for the same current working turn with status completed, needs_input, or failed and a concise summary. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.";
11
11
  export const EXECUTOR_SKILL_INVOCATION = "Use $taskchef-executor to execute and report this delegated TaskChef assignment.";
12
+ const HISTORICAL_RESULT_WITH_TURN_PARAGRAPH = "Before ending, call the TaskChef report_result MCP tool with the marked task ID, this executor's self-linked thread ID, the current turn ID from an exact native read of that same thread, completed, needs_input, or failed, and a concise summary. Never reuse a prior turn ID after a follow-up. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.";
13
+ const HISTORICAL_RESULT_PARAGRAPH = "Before ending, call the TaskChef report_result MCP tool with completed, needs_input, or failed and a concise summary. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.";
14
+ const HISTORICAL_EXECUTOR_SCAFFOLD_LINES = new Set([
15
+ EXECUTOR_OWNERSHIP_PARAGRAPH,
16
+ EXECUTOR_LINK_PARAGRAPH,
17
+ EXECUTOR_WORKING_PARAGRAPH,
18
+ EXECUTOR_RESULT_PARAGRAPH,
19
+ HISTORICAL_RESULT_WITH_TURN_PARAGRAPH,
20
+ HISTORICAL_RESULT_PARAGRAPH,
21
+ ]);
22
+ const HISTORICAL_INLINE_PROTOCOLS = [
23
+ [EXECUTOR_OWNERSHIP_PARAGRAPH, EXECUTOR_LINK_PARAGRAPH, EXECUTOR_WORKING_PARAGRAPH, EXECUTOR_RESULT_PARAGRAPH],
24
+ [EXECUTOR_OWNERSHIP_PARAGRAPH, EXECUTOR_LINK_PARAGRAPH, HISTORICAL_RESULT_WITH_TURN_PARAGRAPH],
25
+ [EXECUTOR_OWNERSHIP_PARAGRAPH, HISTORICAL_RESULT_PARAGRAPH],
26
+ [EXECUTOR_OWNERSHIP_PARAGRAPH],
27
+ ];
28
+
29
+ function hasTaskSpecificContent(lines) {
30
+ return lines.some((line) => (
31
+ line.trim().length > 0 && !HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line)
32
+ ));
33
+ }
12
34
 
13
35
  const UUID_SOURCE = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
14
36
  const UUID_PATTERN = new RegExp(`^${UUID_SOURCE}$`);
15
37
  const CODEX_UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
16
38
  const TASKCHEF_MARKER_PATTERN = new RegExp(`^<!-- taskchef_id=(${UUID_SOURCE}) -->$`);
39
+ const LEGACY_TASKCHEF_MARKER_PATTERN = new RegExp(`^# taskchef_id=(${UUID_SOURCE})$`);
17
40
 
18
41
  function requireObject(value, name) {
19
42
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object`);
@@ -89,10 +112,66 @@ export function taskChefMarker(taskId) {
89
112
 
90
113
  export function parseTaskChefMarker(instruction) {
91
114
  if (typeof instruction !== "string") return null;
92
- const firstLine = instruction.split(/\r?\n/, 1)[0];
93
- const currentMatch = firstLine.match(TASKCHEF_MARKER_PATTERN);
94
- if (currentMatch === null) return null;
95
- return /^[^\r\n]*\r?\n[\s\S]+$/.test(instruction) ? currentMatch[1] : null;
115
+ const lines = instruction.split(/\r\n|\r|\n/);
116
+ const hasHistoricalAssignment = () => {
117
+ const rest = lines.slice(1);
118
+ const executorSkillInvocationIndices = rest.flatMap((line, index) => (
119
+ line === EXECUTOR_SKILL_INVOCATION ? [index] : []
120
+ ));
121
+ if (executorSkillInvocationIndices.length > 0) {
122
+ const bodyLines = rest.slice(0, -2);
123
+ return executorSkillInvocationIndices.length === 1
124
+ && executorSkillInvocationIndices[0] === rest.length - 1
125
+ && lines.at(-1) === EXECUTOR_SKILL_INVOCATION
126
+ && lines.at(-2) === ""
127
+ && bodyLines.length > 0
128
+ && bodyLines[0].trim().length > 0
129
+ && bodyLines.at(-1).trim().length > 0
130
+ && hasTaskSpecificContent(bodyLines)
131
+ && !bodyLines.some((line) => HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line));
132
+ }
133
+
134
+ const containsInlineScaffold = rest.some((line) => HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line));
135
+ if (!containsInlineScaffold) return rest.join("\n").trim().length > 0;
136
+ return HISTORICAL_INLINE_PROTOCOLS.some((protocol) => {
137
+ const prefix = ["", ...protocol.flatMap((line) => [line, ""])];
138
+ if (!prefix.every((line, index) => rest[index] === line)) return false;
139
+ const bodyLines = rest.slice(prefix.length);
140
+ return hasTaskSpecificContent(bodyLines)
141
+ && !bodyLines.some((line) => HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line));
142
+ });
143
+ };
144
+ const currentMatches = lines.flatMap((line, index) => {
145
+ const match = line.match(TASKCHEF_MARKER_PATTERN);
146
+ return match === null ? [] : [{ id: match[1], index }];
147
+ });
148
+ const legacyMatches = lines.flatMap((line, index) => {
149
+ const match = line.match(LEGACY_TASKCHEF_MARKER_PATTERN);
150
+ return match === null ? [] : [{ id: match[1], index }];
151
+ });
152
+ if (currentMatches.length + legacyMatches.length !== 1) return null;
153
+
154
+ if (legacyMatches.length === 1) {
155
+ const [{ id, index }] = legacyMatches;
156
+ if (index !== 0) return null;
157
+ return hasHistoricalAssignment() ? id : null;
158
+ }
159
+
160
+ const [{ id, index }] = currentMatches;
161
+ if (index === 0) {
162
+ return hasHistoricalAssignment() ? id : null;
163
+ }
164
+ const executorSkillReferences = instruction.match(/\$taskchef-executor\b/gi) ?? [];
165
+ const isTrailingScaffold = index === lines.length - 2
166
+ && index >= 2
167
+ && lines[0].trim().length > 0
168
+ && lines.at(-3) === ""
169
+ && lines.at(-4).trim().length > 0
170
+ && lines.at(-1) === EXECUTOR_SKILL_INVOCATION
171
+ && hasTaskSpecificContent(lines.slice(0, index - 1))
172
+ && !lines.slice(0, index - 1).some((line) => HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line))
173
+ && executorSkillReferences.length === 1;
174
+ return isTrailingScaffold ? id : null;
96
175
  }
97
176
 
98
177
  export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
@@ -101,14 +180,25 @@ export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
101
180
  throw new Error("instruction must begin with useful task content on its first line");
102
181
  }
103
182
  const body = rawBody.replace(/(?:(?:\r\n|\r|\n)[^\S\r\n]*)+$/, "");
104
- if (parseTaskChefMarker(body) !== null) throw new Error("instruction already contains a TaskChef marker");
183
+ if (body.split(/\r\n|\r|\n/).some((line) => (
184
+ TASKCHEF_MARKER_PATTERN.test(line) || LEGACY_TASKCHEF_MARKER_PATTERN.test(line)
185
+ ))) {
186
+ throw new Error("instruction already contains a TaskChef marker");
187
+ }
105
188
  if (/\$taskchef-executor\b/i.test(body)) {
106
189
  throw new Error("instruction contains a reserved TaskChef executor skill reference");
107
190
  }
191
+ const bodyLines = body.split(/\r\n|\r|\n/);
192
+ if (!hasTaskSpecificContent(bodyLines)) {
193
+ throw new Error("instruction must contain task-specific content, not only TaskChef lifecycle scaffolding");
194
+ }
195
+ if (bodyLines.some((line) => HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line))) {
196
+ throw new Error("instruction contains reserved historical TaskChef lifecycle scaffolding");
197
+ }
108
198
  const id = requireUuid(taskId);
109
199
  return {
110
200
  id,
111
- instruction: `${taskChefMarker(id)}\n${body}\n\n${EXECUTOR_SKILL_INVOCATION}`,
201
+ instruction: `${body}\n\n${taskChefMarker(id)}\n${EXECUTOR_SKILL_INVOCATION}`,
112
202
  };
113
203
  }
114
204
 
package/src/mcp.js CHANGED
@@ -8,7 +8,9 @@ import {
8
8
  reportTaskResult,
9
9
  } from "./workspace.js";
10
10
  import { parseTaskChefMarker } from "./delegation.js";
11
+ import { createDashboardManager } from "./dashboard-manager.js";
11
12
  import { resolveWorkspacePath } from "./workspace-path.js";
13
+ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
12
14
 
13
15
  const projectSchema = z.object({
14
16
  name: z.string(),
@@ -49,6 +51,14 @@ const preparationSchema = z.object({
49
51
  projects: z.array(projectSchema),
50
52
  });
51
53
 
54
+ const dashboardSchema = z.object({
55
+ action: z.enum(["started", "reused"]),
56
+ url: z.string().url(),
57
+ workspace: z.string(),
58
+ taskchefVersion: z.string(),
59
+ serverVersion: z.string(),
60
+ });
61
+
52
62
  function toolResult(key, value, message) {
53
63
  return {
54
64
  structuredContent: { [key]: value },
@@ -63,15 +73,53 @@ export function createTaskChefMcpServer({
63
73
  reportResult = reportTaskResult,
64
74
  reportState = reportTaskState,
65
75
  link = linkTask,
76
+ dashboardManager = createDashboardManager({ workspace }),
66
77
  } = {}) {
67
78
  const server = new McpServer(
68
- { name: "taskchef", version: "1.0.0" },
79
+ { name: "taskchef", version: TASKCHEF_VERSION },
69
80
  {
70
81
  instructions:
71
82
  "Prepare with prepare_dispatch, call record_task before creating the Codex task, then create it natively and return immediately. Follow the active TaskChef skill for role-specific sequencing of the identity and state tools.",
72
83
  },
73
84
  );
74
85
 
86
+ const originalClose = server.close.bind(server);
87
+ let closePromise = null;
88
+ server.close = async () => {
89
+ closePromise ??= (async () => {
90
+ await dashboardManager.close();
91
+ await originalClose();
92
+ })();
93
+ return closePromise;
94
+ };
95
+ server.server.onclose = () => {
96
+ void dashboardManager.close();
97
+ };
98
+
99
+ server.registerTool(
100
+ "ensure_dashboard",
101
+ {
102
+ title: "Ensure TaskChef dashboard",
103
+ description:
104
+ "Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts one dashboard inside this MCP process or reuses only an exact compatible TaskChef dashboard for the same canonical workspace; unknown listeners are never terminated or replaced.",
105
+ inputSchema: {},
106
+ outputSchema: { dashboard: dashboardSchema },
107
+ annotations: {
108
+ readOnlyHint: false,
109
+ destructiveHint: false,
110
+ openWorldHint: false,
111
+ },
112
+ },
113
+ async () => {
114
+ const dashboard = await dashboardManager.ensure();
115
+ return toolResult(
116
+ "dashboard",
117
+ dashboard,
118
+ `${dashboard.action === "started" ? "Started" : "Reused"} TaskChef dashboard ${dashboard.url}`,
119
+ );
120
+ },
121
+ );
122
+
75
123
  server.registerTool(
76
124
  "prepare_dispatch",
77
125
  {
@@ -114,7 +162,7 @@ export function createTaskChefMcpServer({
114
162
  },
115
163
  async (input) => {
116
164
  if (parseTaskChefMarker(input.instruction) !== input.id) {
117
- throw new Error("record_task instruction must start with its exact TaskChef marker");
165
+ throw new Error("record_task instruction must contain its exact TaskChef marker in an accepted scaffold");
118
166
  }
119
167
  const task = await record(workspace, input);
120
168
  return toolResult("task", task, `Recorded TaskChef task ${task.id}.`);
package/src/version.js ADDED
@@ -0,0 +1,7 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const packageMetadata = require("../package.json");
5
+
6
+ export const TASKCHEF_VERSION = packageMetadata.version;
7
+ export const DASHBOARD_SERVER_VERSION = "1";
package/src/workspace.js CHANGED
@@ -737,7 +737,7 @@ async function validateDispatchShape(dispatch, name = "task") {
737
737
  id,
738
738
  project,
739
739
  title: requireString(dispatch.title, `${name}.title`).trim(),
740
- instruction: requireString(dispatch.instruction, `${name}.instruction`).trim(),
740
+ instruction: requireString(dispatch.instruction, `${name}.instruction`),
741
741
  threadId: dispatch.threadId === null
742
742
  ? null
743
743
  : normalizeDurableThreadId(dispatch.threadId, `${name}.threadId`),