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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +37 -6
- package/assets/taskchef-dispatcher-instructions.md +9 -0
- package/docs/firstmate-taskchef-comparison.md +1 -1
- package/docs/spec.md +74 -12
- package/docs/workflows.md +49 -8
- package/index.js +9 -0
- package/mcp/server.js +12 -0
- package/package.json +1 -1
- package/skills/taskchef-bootstrap/SKILL.md +4 -2
- package/skills/taskchef-delegate/SKILL.md +18 -9
- package/skills/taskchef-executor/SKILL.md +16 -6
- package/src/cli.js +20 -6
- package/src/dashboard-manager.js +183 -0
- package/src/dashboard.js +27 -0
- package/src/delegation.js +96 -6
- package/src/mcp.js +50 -2
- package/src/version.js +7 -0
- package/src/workspace.js +1 -1
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
|
|
91
|
-
`$taskchef-executor` invocation. That skill
|
|
92
|
-
`CODEX_THREAD_ID`, self-links, and reports lifecycle
|
|
93
|
-
may become separate executors; dependent work
|
|
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
|
|
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
|
|
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
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
inline the executor protocol into a new
|
|
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
|
|
126
|
-
|
|
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
|
|
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
|
|
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.
|
|
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` |
|
|
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` |
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
the deprecated `report_result` alias preserves their
|
|
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.
|
|
238
|
-
|
|
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
|
@@ -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,
|
|
23
|
-
|
|
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`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
13
|
-
`<!-- taskchef_id=<full UUID>
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
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
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
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 (
|
|
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: `${
|
|
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:
|
|
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
|
|
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
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`)
|
|
740
|
+
instruction: requireString(dispatch.instruction, `${name}.instruction`),
|
|
741
741
|
threadId: dispatch.threadId === null
|
|
742
742
|
? null
|
|
743
743
|
: normalizeDurableThreadId(dispatch.threadId, `${name}.threadId`),
|