taskchef 7.17.0 → 7.18.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.17.0",
3
+ "version": "7.18.0",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
@@ -31,7 +31,7 @@
31
31
  "defaultPrompt": [
32
32
  "$taskchef-bootstrap Set up TaskChef and index my local Codex projects.",
33
33
  "$taskchef-delegate Do X in Y project.",
34
- "Start and open the TaskChef dashboard.",
34
+ "$taskchef-dashboard Ensure and open the TaskChef dashboard.",
35
35
  "$taskchef-copilot Summarize recent delegated work and tell me what needs attention."
36
36
  ],
37
37
  "brandColor": "#D97706",
package/README.md CHANGED
@@ -31,7 +31,7 @@ codex plugin add taskchef@favoyang-plugins
31
31
  npm install --global taskchef
32
32
  ```
33
33
 
34
- The plugin provides four skills and a local MCP server. The npm installation
34
+ The plugin provides five skills and a local MCP server. The npm installation
35
35
  puts the `taskchef` CLI on `PATH`. TaskChef installs no hooks, schedules,
36
36
  daemons, login items, system services, or background identity search and needs
37
37
  no elevated permissions.
@@ -48,7 +48,7 @@ The canonical workspace is `~/.agents/taskchef`. TaskChef owns only:
48
48
 
49
49
  ```text
50
50
  AGENTS.md managed dispatcher instructions plus user additions
51
- taskchef.json schema-2 Codex project index and delegation metadata
51
+ taskchef.json schema-2 Codex project index, dashboard preference, and delegation metadata
52
52
  tasks.jsonl one task snapshot per line (schema 9; schema 4-8 migration supported)
53
53
  ```
54
54
 
@@ -129,9 +129,10 @@ correlation marker. That skill reads the executor's own `CODEX_THREAD_ID`, self-
129
129
  reports lifecycle state. Independent outcomes may become separate executors;
130
130
  dependent work should stay together.
131
131
 
132
- At the start of every dispatcher turn, the managed workspace instructions ask
133
- the MCP server to best-effort ensure the dashboard. A startup failure never
134
- blocks an answer, report, or delegation. Every dispatcher response ends with
132
+ Activating the TaskChef MCP process best-effort ensures the dashboard by
133
+ default. The managed workspace instructions also retry it at the start of each
134
+ dispatcher turn. A startup failure never blocks MCP tools, an answer, report,
135
+ or delegation. Every dispatcher response ends with
135
136
  the stable [TaskChef Dashboard](http://127.0.0.1:3210/) link; a created-task
136
137
  directive remains on the preceding line so dispatch still returns immediately.
137
138
 
@@ -219,13 +220,14 @@ eight-character prefix.
219
220
 
220
221
  ## Dashboard
221
222
 
222
- Ask the dispatcher to make the dashboard available and open it:
223
+ Use the packaged recovery skill to make the dashboard available and open it:
223
224
 
224
225
  ```text
225
- Start and open the TaskChef dashboard.
226
+ $taskchef-dashboard Ensure and open the TaskChef dashboard.
226
227
  ```
227
228
 
228
- Dispatcher turns call the input-free `ensure_dashboard` MCP tool. It starts at
229
+ MCP activation and `$taskchef-dashboard` call the input-free `ensure_dashboard`
230
+ MCP tool. It starts at
229
231
  most one dashboard inside the existing TaskChef MCP process on
230
232
  `127.0.0.1:3210`, or reuses a listener only when its bounded `/api/health`
231
233
  identity proves the exact TaskChef/dashboard-server version and the same
@@ -233,8 +235,22 @@ canonical workspace. The response says `started` or `reused` and includes the
233
235
  stable URL, canonical workspace, and versions.
234
236
 
235
237
  The in-process dashboard closes with the MCP process. Closing Codex or reloading
236
- the plugin may therefore stop the dashboard; the next dispatcher turn restores
237
- it. TaskChef adds no OS-persistent component.
238
+ the plugin may therefore stop the dashboard; activating the next TaskChef MCP
239
+ process normally restores it. TaskChef adds no OS-persistent component.
240
+
241
+ Autostart is enabled when `dashboard` is absent and in new workspaces. To opt
242
+ out, add this optional exact object to `taskchef.json` while retaining its other
243
+ fields:
244
+
245
+ ```json
246
+ "dashboard": { "autostart": false }
247
+ ```
248
+
249
+ The manual `$taskchef-dashboard` recovery skill remains available when
250
+ autostart is disabled or fails. It reports whether the compatible dashboard was
251
+ started or reused and always returns its canonical clickable URL. It may open
252
+ the returned localhost URL in an available in-app browser, but browser failure
253
+ does not make dashboard recovery fail.
238
254
 
239
255
  For manual development, run the foreground CLI:
240
256
 
@@ -322,6 +338,21 @@ codex plugin marketplace upgrade favoyang-plugins
322
338
  codex plugin add taskchef@favoyang-plugins
323
339
  ```
324
340
 
341
+ Replacing or installing plugin files cannot execute autostart by itself. After
342
+ installation, activate or reload the new TaskChef MCP process; installation
343
+ does not necessarily reload Codex. Then run `$taskchef-dashboard` (or call
344
+ `ensure_dashboard`) and verify the returned dashboard identity has all of:
345
+
346
+ - the expected released TaskChef version;
347
+ - the expected dashboard protocol `serverVersion`;
348
+ - the canonical TaskChef workspace path;
349
+ - the canonical `http://127.0.0.1:3210/` URL.
350
+
351
+ The release-install sequence is therefore: install plugin, activate or reload
352
+ the new MCP process, ensure the dashboard, then verify TaskChef version,
353
+ protocol `serverVersion`, canonical workspace, and URL. Exact-compatible
354
+ servers may be reused; unknown listeners remain untouched.
355
+
325
356
  ## Development
326
357
 
327
358
  ```sh
@@ -6,10 +6,8 @@ This folder is the canonical per-user TaskChef dispatcher workspace.
6
6
  - Early in every dispatcher turn, best-effort call the TaskChef
7
7
  `ensure_dashboard` MCP tool. Dashboard startup failure must not block direct
8
8
  TaskChef answers, reporting, or delegation.
9
- - A request to start or open the TaskChef dashboard is TaskChef maintenance,
10
- not delegated project work. Call `ensure_dashboard`, then open or navigate to
11
- its returned URL when an in-app browser capability is available. Otherwise,
12
- return the URL as a clickable link.
9
+ - A request to start, recover, or open the TaskChef dashboard is TaskChef
10
+ maintenance, not delegated project work. Use `$taskchef-dashboard`.
13
11
  - Use `$taskchef-bootstrap` when initializing or refreshing this workspace,
14
12
  changing or listing its configured projects, running TaskChef doctor,
15
13
  or repairing its managed instructions.
@@ -70,7 +70,7 @@ responsibility until an outcome is landed, transferred, or safely preserved.
70
70
  | Dimension | TaskChef 7 | FirstMate at `038d0f7` |
71
71
  | --- | --- | --- |
72
72
  | Primary goal | Put multi-project work into the right visible Codex task and make it findable later. | Let one person direct a supervised crew across projects through one liaison. |
73
- | Product form | Codex plugin: four skills, local MCP server, data CLI, and loopback dashboard. | Cloneable agent distribution: instructions, internal skills, scripts, policies, and private on-disk state. |
73
+ | Product form | Codex plugin: five skills, local MCP server, data CLI, and loopback dashboard. | Cloneable agent distribution: instructions, internal skills, scripts, policies, and private on-disk state. |
74
74
  | Runtime | Native Codex desktop tasks are the executors and live source of truth. | Harness-driven workers in tmux by default, with documented alternative session backends; Codex can be a harness, but Codex desktop is not a runtime backend. |
75
75
  | Coordinator lifetime | Dispatcher returns immediately and does not supervise. | First mate remains active and uses watcher/guard mechanisms to supervise meaningful events. |
76
76
  | Project isolation | Delegates to the configured Codex project; isolation follows native Codex/project behavior. | Ship and scout workers require separate Treehouse- or backend-managed worktrees. |
@@ -219,7 +219,7 @@ harness adapter.
219
219
  | --- | --- | --- |
220
220
  | Codex | Built around native Codex projects, tasks, thread reads, and desktop deep links. | Codex is one verified harness; desktop tasks are not its worker runtime. |
221
221
  | GitHub | Configured repository URLs aid routing; delivery remains the executor project's concern. | GitHub CLI, PR state, and configured delivery modes are part of supervised shipping workflows. |
222
- | Skills | Four plugin skills with narrow bootstrap, delegate, executor, and conversational coordination responsibilities. | Internal firstmate-only skills plus standalone public skills; AGENTS.md routes conditional procedures. |
222
+ | Skills | Five plugin skills with narrow bootstrap, dashboard recovery, delegate, executor, and conversational coordination responsibilities. | Internal firstmate-only skills plus standalone public skills; AGENTS.md routes conditional procedures. |
223
223
  | MCP | Four primary local TaskChef tools plus one deprecated compatibility alias own deterministic identity and state writes. | FirstMate describes itself as an agent distribution rather than an MCP product; deterministic behavior lives largely in scripts. |
224
224
  | Runtime extensions | Extend plugin skills/MCP/CLI/dashboard or compose native Codex capabilities. | Add or verify harness adapters, session backends, dispatch profiles, operational scripts, and optional integrations. |
225
225
 
package/docs/spec.md CHANGED
@@ -29,7 +29,7 @@ is dated research, not contract.
29
29
  | **Turn reference** | Required lifecycle identity for one executor prompt. It is the native Codex turn ID when available, otherwise a retained client-generated UUID. |
30
30
  | **Current turn ID** | Optional Codex metadata for the reported prompt; null when native turn reading is unavailable. |
31
31
  | **Dashboard** | The loopback, read-only UI derived from validated workspace snapshots and bounded native actions. |
32
- | **Skill** | One packaged agent procedure: `taskchef-bootstrap`, `taskchef-delegate`, `taskchef-executor`, or `taskchef-copilot`. |
32
+ | **Skill** | One packaged agent procedure: `taskchef-bootstrap`, `taskchef-dashboard`, `taskchef-delegate`, `taskchef-executor`, or `taskchef-copilot`. |
33
33
 
34
34
  ## Components and ownership
35
35
 
@@ -43,6 +43,9 @@ is dated research, not contract.
43
43
  - `taskchef-copilot` MUST own conversational outcome explanation, attention,
44
44
  and next-action recommendations. It MUST use cached normalized briefs by
45
45
  default and MUST NOT poll or persist inferred state.
46
+ - `taskchef-dashboard` MUST own manual dashboard ensure and recovery. It MUST
47
+ NOT dispatch work or inspect task outcomes, and browser navigation failure
48
+ MUST NOT suppress the returned dashboard URL.
46
49
  - The MCP server MUST expose `ensure_dashboard`, four primary lifecycle tools,
47
50
  and the deprecated `report_result` compatibility alias specified below.
48
51
  - The CLI MAY administer and inspect the workspace, but MUST NOT provide a
@@ -56,7 +59,8 @@ is dated research, not contract.
56
59
  TaskChef MUST manage only `AGENTS.md`, `taskchef.json`, and `tasks.jsonl`
57
60
  inside the dispatcher workspace. It MUST preserve unrelated paths.
58
61
 
59
- `taskchef.json` MUST have schema version 2 and exactly:
62
+ `taskchef.json` MUST have schema version 2, the following required fields, and
63
+ an optional exact `dashboard` object:
60
64
 
61
65
  ```json
62
66
  {
@@ -71,6 +75,11 @@ Names and paths MUST be unique. Git projects MUST be exact Git roots.
71
75
  Repository URLs MUST canonicalize to `https://github.com/<owner>/<repository>`
72
76
  and be case-insensitively deduplicated.
73
77
 
78
+ When present, `dashboard` MUST contain exactly boolean `autostart`. Its absence
79
+ is backward-compatible and means `true`; new workspaces SHOULD write
80
+ `{"autostart": true}`. `false` disables MCP-lifecycle autostart but MUST NOT
81
+ disable the explicit `ensure_dashboard` tool.
82
+
74
83
  `tasks.jsonl` MUST contain zero or more newline-terminated schema-4 through
75
84
  schema-9 records, one per line. Schemas 4 through 8 are supported
76
85
  migration/read formats; every new record and state mutation MUST write schema 9. Other schemas
@@ -112,6 +121,14 @@ fields MUST NOT change after recording.
112
121
 
113
122
  ## Required lifecycle
114
123
 
124
+ When the canonical TaskChef MCP server finishes connecting, it MUST invoke the
125
+ same serialized dashboard ensure path once by default. It MUST read the
126
+ canonical configuration and skip this only for explicit
127
+ `dashboard.autostart: false`. Initialization failures, invalid workspace state,
128
+ port conflicts, and dashboard errors MUST NOT prevent tool registration or MCP
129
+ availability. They MUST emit only a bounded non-sensitive diagnostic through
130
+ the MCP process logging channel. MCP initialization MUST NOT open a browser.
131
+
115
132
  At the start of every dispatcher turn, the dispatcher SHOULD call
116
133
  `ensure_dashboard` best-effort. Failure MUST NOT block direct TaskChef answers,
117
134
  reporting, or delegation. Every dispatcher final response MUST end with the
@@ -233,6 +250,11 @@ failure MUST leave no owned listener. The MCP server MUST close its owned
233
250
  dashboard when its transport or process shuts down; it MUST NOT close a reused
234
251
  external foreground server.
235
252
 
253
+ The packaged `$taskchef-dashboard` skill MUST call this tool, report `started`
254
+ or `reused`, and return the canonical clickable URL. It MAY use an available
255
+ in-app browser when permitted; absent or blocked browser navigation MUST fall
256
+ back to the link without failing. It MUST NOT inspect task outcomes or dispatch.
257
+
236
258
  **Annotations:** `readOnlyHint: false`, `destructiveHint: false`,
237
259
  `openWorldHint: false`.
238
260
 
@@ -473,3 +495,10 @@ system services, cron jobs, hooks, privileged components, or elevated/system
473
495
  permissions for dashboard availability. Availability is best-effort while the
474
496
  owning Codex/plugin MCP process is alive and is not guaranteed while Codex is
475
497
  closed.
498
+
499
+ Installing or replacing plugin files MUST NOT be described as activating the
500
+ new MCP code. Release verification MUST install the plugin, activate or reload
501
+ the new MCP process, ensure the dashboard, and verify the expected TaskChef
502
+ version, dashboard protocol `serverVersion`, canonical workspace, and canonical
503
+ URL. Exact-compatible listener reuse remains valid; installation MUST NOT be
504
+ claimed to reload Codex automatically.
package/docs/workflows.md CHANGED
@@ -13,6 +13,7 @@ research.
13
13
  | `skills/taskchef-delegate/SKILL.md` | Split, route, record-before-create, create, return. |
14
14
  | `skills/taskchef-executor/SKILL.md` | Own, self-link, execute, and report every executor turn. |
15
15
  | `skills/taskchef-bootstrap/SKILL.md` | Initialize the workspace and maintain the Codex project index. |
16
+ | `skills/taskchef-dashboard/SKILL.md` | Ensure or recover the canonical dashboard and return its URL. |
16
17
  | `skills/taskchef-copilot/SKILL.md` | Explain normalized cached briefs and coordinate safe next actions. |
17
18
  | `src/mcp.js` | Dashboard ensure, four primary lifecycle tools, one deprecated alias, shutdown ownership, and MCP annotations. |
18
19
  | `src/delegation.js` | UUID marker, concise executor-skill invocation shape, and creation-failure handling. |
@@ -27,9 +28,9 @@ per-user default.
27
28
 
28
29
  ## Dispatcher dashboard lifecycle
29
30
 
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/copilot skills.
31
+ MCP activation makes dashboard maintenance a best-effort lifecycle action. The
32
+ generated managed `AGENTS.md` block also retries it before dispatcher turns and
33
+ keeps response ordering centralized instead of duplicating it across skills.
33
34
 
34
35
  ```mermaid
35
36
  sequenceDiagram
@@ -38,6 +39,8 @@ sequenceDiagram
38
39
  participant M as TaskChef MCP
39
40
  participant H as Loopback health
40
41
  participant S as Dashboard server
42
+ M->>M: Read dashboard.autostart (absent means true)
43
+ M->>M: Best-effort ensure after MCP connect
41
44
  D->>M: ensure_dashboard()
42
45
  M->>M: Serialize concurrent ensure calls
43
46
  M->>H: GET 127.0.0.1:3210/api/health
@@ -63,6 +66,27 @@ started. A compatible foreground `taskchef dashboard` listener may be reused
63
66
  but remains owned by that CLI process. No TaskChef path terminates an unknown
64
67
  listener or installs OS persistence.
65
68
 
69
+ Autostart and explicit ensures share the same manager promise, so concurrent
70
+ initialization and recovery calls produce at most one owned listener. An
71
+ explicit `dashboard.autostart: false` skips only activation-time ensure. Any
72
+ failure is reduced to a fixed stderr diagnostic; tool registration and MCP
73
+ availability continue. Activation never opens a browser.
74
+
75
+ ## Release-install verification
76
+
77
+ The practical release handoff ends in this order:
78
+
79
+ 1. Install the released plugin.
80
+ 2. Activate or reload its new TaskChef MCP process.
81
+ 3. Run `$taskchef-dashboard` or call `ensure_dashboard`.
82
+ 4. Verify the expected TaskChef version, protocol `serverVersion`, canonical
83
+ workspace, and canonical URL returned by the dashboard identity.
84
+
85
+ Replacing plugin files alone cannot execute autostart because old code remains
86
+ in the already-running MCP process. Installation does not necessarily reload
87
+ Codex. An exact-compatible dashboard may be reused; an unknown listener is
88
+ never terminated or replaced.
89
+
66
90
  ## Normal delegation and self-linking
67
91
 
68
92
  The dispatcher uses native Codex project discovery for routing and MCP for
package/index.js CHANGED
@@ -5,6 +5,7 @@ export {
5
5
  canonicalDirectory,
6
6
  canonicalGitRoot,
7
7
  doctorWorkspace,
8
+ dashboardAutostartEnabled,
8
9
  ensureWorkspaceInstructions,
9
10
  filterTasks,
10
11
  importProjects,
@@ -74,4 +75,4 @@ export {
74
75
 
75
76
  export { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./src/version.js";
76
77
 
77
- export { createTaskChefMcpServer } from "./src/mcp.js";
78
+ export { createDashboardAutostart, createTaskChefMcpServer } from "./src/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.17.0",
3
+ "version": "7.18.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -36,6 +36,7 @@
36
36
  "scripts/e2e-benchmark.js",
37
37
  "src",
38
38
  "skills/taskchef-bootstrap",
39
+ "skills/taskchef-dashboard",
39
40
  "skills/taskchef-delegate",
40
41
  "skills/taskchef-executor",
41
42
  "skills/taskchef-copilot"
@@ -160,5 +160,8 @@ Example managed-workspace import entry:
160
160
  }
161
161
  ```
162
162
 
163
- TaskChef accepts configuration schema version 2 only. `githubRepos` is always
164
- an array; unsupported configuration is rejected without being rewritten.
163
+ TaskChef accepts configuration schema version 2 only. Its optional exact
164
+ `dashboard` object contains boolean `autostart`; absence means enabled and
165
+ `false` opts out of MCP-lifecycle dashboard startup without disabling manual
166
+ `$taskchef-dashboard` recovery. `githubRepos` is always an array; unsupported
167
+ configuration is rejected without being rewritten.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: taskchef-dashboard
3
+ description: "Ensure and recover the canonical TaskChef dashboard. Use when the user asks to start, restore, open, or get the link for the TaskChef dashboard. Do not use to dispatch work or inspect task outcomes."
4
+ ---
5
+
6
+ # TaskChef Dashboard
7
+
8
+ Maintain only the canonical TaskChef dashboard.
9
+
10
+ 1. Call the TaskChef `ensure_dashboard` MCP tool with its empty input. Do not
11
+ supply a workspace, host, or port.
12
+ 2. Report whether the returned `action` is `started` or `reused` and provide
13
+ the returned canonical URL as a clickable link.
14
+ 3. If the user asked to open the dashboard and an in-app browser navigation
15
+ capability is available and permits the returned localhost URL, open or
16
+ navigate to it. Browser absence, refusal, or blocked localhost navigation is
17
+ non-fatal: still return the clickable URL and the ensure result.
18
+
19
+ Do not dispatch project work, read task briefs, inspect executor outcomes, poll,
20
+ start a separate foreground CLI server, or modify TaskChef workspace files.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "TaskChef Dashboard"
3
+ short_description: "Start or recover the TaskChef dashboard"
4
+ default_prompt: "Use $taskchef-dashboard to ensure the TaskChef dashboard is available and give me its link."
@@ -19,6 +19,7 @@ import { openTaskFromControl } from "./actions.js";
19
19
  import {
20
20
  githubReferenceAccessibleLabel,
21
21
  githubReferenceDisplayLabels,
22
+ groupRelatedGitHubLinks,
22
23
  referenceSegments,
23
24
  } from "./github-links.js";
24
25
  import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
@@ -116,26 +117,35 @@ function appendLinkedText(container, text, task) {
116
117
  }
117
118
 
118
119
  function relatedGitHubLinks(task, { compact = false } = {}) {
119
- const seen = new Set();
120
- const links = (task.relatedGitHubLinks ?? []).filter((link) => {
121
- if (link.type === "repository" || seen.has(link.label)) return false;
122
- seen.add(link.label);
123
- return true;
124
- });
120
+ const groups = groupRelatedGitHubLinks(task.relatedGitHubLinks);
121
+ const links = groups.flat();
125
122
  const container = document.createElement("nav");
126
123
  container.className = `github-links${compact ? " github-links-compact" : ""}`;
127
124
  container.setAttribute("aria-label", `Related GitHub links for ${task.title}`);
128
125
  const displayLabels = githubReferenceDisplayLabels(links, { related: true });
129
- const children = links.map((link, index) => (
130
- referenceLink(link, { compact, displayLabel: displayLabels[index] })
131
- ));
126
+ let linkIndex = 0;
127
+ const children = groups.map((group) => {
128
+ const line = document.createElement("div");
129
+ line.className = "github-links-group";
130
+ line.replaceChildren(...group.map((link) => {
131
+ const anchor = referenceLink(link, {
132
+ compact,
133
+ displayLabel: displayLabels[linkIndex],
134
+ });
135
+ linkIndex += 1;
136
+ return anchor;
137
+ }));
138
+ return line;
139
+ });
132
140
  if (task.relatedGitHubLinksTruncated) {
133
141
  const more = document.createElement("span");
134
142
  more.className = "github-links-more";
135
143
  more.textContent = "+ more";
136
144
  more.title = "Additional related GitHub references are omitted from this bounded dashboard view.";
137
145
  more.setAttribute("aria-label", more.title);
138
- children.push(more);
146
+ const finalGroup = children.at(-1);
147
+ if (finalGroup) finalGroup.append(more);
148
+ else children.push(more);
139
149
  }
140
150
  container.hidden = children.length === 0;
141
151
  container.replaceChildren(...children);
@@ -52,6 +52,27 @@ export function githubReferenceDisplayLabels(links, { related = false } = {}) {
52
52
  });
53
53
  }
54
54
 
55
+ function compareReferenceNumbers(left, right) {
56
+ const leftNumber = String(left.number ?? "");
57
+ const rightNumber = String(right.number ?? "");
58
+ return leftNumber.length - rightNumber.length || leftNumber.localeCompare(rightNumber);
59
+ }
60
+
61
+ export function groupRelatedGitHubLinks(links) {
62
+ const groups = new Map();
63
+ const seen = new Set();
64
+ for (const link of links ?? []) {
65
+ if (link.type === "repository" || seen.has(link.label)) continue;
66
+ seen.add(link.label);
67
+ const repository = repositoryIdentity(link);
68
+ if (!repository) continue;
69
+ const group = groups.get(repository) ?? [];
70
+ group.push(link);
71
+ groups.set(repository, group);
72
+ }
73
+ return [...groups.values()].map((group) => [...group].sort(compareReferenceNumbers));
74
+ }
75
+
55
76
  export function githubReferenceAccessibleLabel(link) {
56
77
  const repository = repositoryIdentity(link);
57
78
  if (!repository) return link.label ?? link.text;
@@ -97,9 +97,10 @@ select { min-width: 180px; min-height: 36px; padding: 7px 32px 7px 10px; border:
97
97
  .task-title, .task-summary { overflow-wrap: anywhere; }
98
98
  .task-summary { display: grid; max-width: 82ch; margin-bottom: 12px; gap: 2px; font-size: 0.9rem; line-height: 1.45; }
99
99
  .task-summary strong { margin-top: 4px; color: var(--muted); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; }
100
- .github-links { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 18px; }
100
+ .github-links { display: grid; gap: 7px; margin: 0 0 18px; }
101
101
  .github-links[hidden] { display: none; }
102
102
  .github-links-compact { margin: -2px 0 12px; gap: 5px; }
103
+ .github-links-group { display: flex; min-width: 0; flex-wrap: wrap; gap: inherit; }
103
104
  .github-link { color: var(--accent); font-weight: 700; text-decoration-thickness: 1px; text-underline-offset: 3px; overflow-wrap: anywhere; }
104
105
  .github-link-compact { padding: 2px 7px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-muted); font-size: 0.72rem; line-height: 1.35; text-decoration: none; }
105
106
  .github-link:hover { text-decoration-thickness: 2px; }
package/src/mcp.js CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  recordTask,
7
7
  reportTaskState,
8
8
  reportTaskResult,
9
+ dashboardAutostartEnabled,
10
+ readConfig,
9
11
  } from "./workspace.js";
10
12
  import { parseTaskChefMarker } from "./delegation.js";
11
13
  import { createDashboardManager } from "./dashboard-manager.js";
@@ -99,6 +101,39 @@ function toolResult(key, value, message) {
99
101
  };
100
102
  }
101
103
 
104
+ function dashboardAutostartDiagnostic(error) {
105
+ const message = typeof error?.message === "string" ? error.message : "";
106
+ if (error?.code === "EADDRINUSE" || /port conflict|already in use/i.test(message)) {
107
+ return "TaskChef dashboard autostart skipped: port 127.0.0.1:3210 is unavailable; the listener was left untouched.";
108
+ }
109
+ if (/configuration|workspace|task log/i.test(message)) {
110
+ return "TaskChef dashboard autostart skipped: the canonical workspace is not ready.";
111
+ }
112
+ return "TaskChef dashboard autostart skipped: the dashboard could not be started.";
113
+ }
114
+
115
+ export function createDashboardAutostart({
116
+ workspace,
117
+ dashboardManager,
118
+ readConfiguration = readConfig,
119
+ log = (message) => process.stderr.write(`${message}\n`),
120
+ } = {}) {
121
+ let startPromise = null;
122
+ return async () => {
123
+ startPromise ??= (async () => {
124
+ try {
125
+ const config = await readConfiguration(workspace, { checkPaths: false });
126
+ if (!dashboardAutostartEnabled(config)) return { action: "disabled" };
127
+ return await dashboardManager.ensure();
128
+ } catch (error) {
129
+ try { log(dashboardAutostartDiagnostic(error)); } catch {}
130
+ return { action: "failed" };
131
+ }
132
+ })();
133
+ return startPromise;
134
+ };
135
+ }
136
+
102
137
  export function createTaskChefMcpServer({
103
138
  workspace = resolveWorkspacePath().workspace,
104
139
  prepare = prepareDispatch,
@@ -107,6 +142,8 @@ export function createTaskChefMcpServer({
107
142
  reportState = reportTaskState,
108
143
  link = linkTask,
109
144
  dashboardManager = createDashboardManager({ workspace }),
145
+ readConfiguration = readConfig,
146
+ logDashboardDiagnostic,
110
147
  } = {}) {
111
148
  const server = new McpServer(
112
149
  { name: "taskchef", version: TASKCHEF_VERSION },
@@ -280,5 +317,17 @@ export function createTaskChefMcpServer({
280
317
  },
281
318
  );
282
319
 
320
+ const originalConnect = server.connect.bind(server);
321
+ const autostartDashboard = createDashboardAutostart({
322
+ workspace,
323
+ dashboardManager,
324
+ readConfiguration,
325
+ ...(logDashboardDiagnostic ? { log: logDashboardDiagnostic } : {}),
326
+ });
327
+ server.connect = async (...args) => {
328
+ await originalConnect(...args);
329
+ void autostartDashboard();
330
+ };
331
+
283
332
  return server;
284
333
  }
package/src/workspace.js CHANGED
@@ -43,7 +43,8 @@ const PREVIOUS_TASK_SCHEMA_VERSION = 8;
43
43
  const FIRST_TURN_HISTORY_TASK_SCHEMA_VERSION = 7;
44
44
  const LEGACY_RESULTS_TASK_SCHEMA_VERSION = 6;
45
45
  const FIRST_SELF_LINKING_TASK_SCHEMA_VERSION = 4;
46
- const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
46
+ const CONFIG_FIELDS = new Set(["schemaVersion", "projects", "dashboard"]);
47
+ const DASHBOARD_CONFIG_FIELDS = new Set(["autostart"]);
47
48
  const PROJECT_FIELDS = new Set([
48
49
  "name",
49
50
  "path",
@@ -533,16 +534,36 @@ async function inspectProject(input, index = 0) {
533
534
  }
534
535
 
535
536
  export async function validateConfig(config, { checkPaths = true } = {}) {
536
- requireExactFields(config, CONFIG_FIELDS, "taskchef.json");
537
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
538
+ throw new Error("taskchef.json must be an object");
539
+ }
540
+ const unexpected = Object.keys(config).find((key) => !CONFIG_FIELDS.has(key));
541
+ if (unexpected) throw new Error(`taskchef.json has unsupported field: ${unexpected}`);
542
+ for (const required of ["schemaVersion", "projects"]) {
543
+ if (!(required in config)) throw new Error(`taskchef.json is missing field: ${required}`);
544
+ }
537
545
  if (config.schemaVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
538
546
  throw new Error("unsupported configuration schemaVersion");
539
547
  }
548
+ let dashboard;
549
+ if ("dashboard" in config) {
550
+ requireExactFields(config.dashboard, DASHBOARD_CONFIG_FIELDS, "taskchef.json.dashboard");
551
+ if (typeof config.dashboard.autostart !== "boolean") {
552
+ throw new Error("taskchef.json.dashboard.autostart must be a boolean");
553
+ }
554
+ dashboard = { autostart: config.dashboard.autostart };
555
+ }
540
556
  return {
541
557
  schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
542
558
  projects: await normalizeProjects(config.projects, { checkPaths }),
559
+ ...(dashboard ? { dashboard } : {}),
543
560
  };
544
561
  }
545
562
 
563
+ export function dashboardAutostartEnabled(config) {
564
+ return config?.dashboard?.autostart !== false;
565
+ }
566
+
546
567
  async function ensureDispatchFile(workspaceRoot) {
547
568
  const filePath = path.join(workspaceRoot, DISPATCH_FILE_NAME);
548
569
  const exists = await managedRegularFileExists(filePath);
@@ -569,7 +590,11 @@ export async function initializeWorkspace(workspaceRoot) {
569
590
  const configExists = await managedRegularFileExists(configPath);
570
591
  const config = configExists
571
592
  ? await readConfig(root, { checkPaths: false })
572
- : { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION, projects: [] };
593
+ : {
594
+ schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
595
+ projects: [],
596
+ dashboard: { autostart: true },
597
+ };
573
598
  const dispatchPath = path.join(root, DISPATCH_FILE_NAME);
574
599
  if (configExists && (await managedRegularFileExists(dispatchPath))) {
575
600
  await readDispatchesUnlocked(root);
@@ -638,7 +663,7 @@ export async function addProject(workspaceRoot, input) {
638
663
  const project = await inspectProject(input);
639
664
  assertWorkspaceOutsideProject(root, project.path);
640
665
  const updated = await validateConfig({
641
- schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
666
+ ...config,
642
667
  projects: [...config.projects, project],
643
668
  });
644
669
  await writeJsonAtomic(path.join(root, "taskchef.json"), updated);
@@ -676,7 +701,7 @@ export async function importProjects(workspaceRoot, inputs, { replace = false }
676
701
  else projects[index] = project;
677
702
  }
678
703
  const config = await validateConfig({
679
- schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
704
+ ...current,
680
705
  projects,
681
706
  });
682
707
  await writeJsonAtomic(path.join(root, "taskchef.json"), config);
@@ -700,7 +725,7 @@ export async function removeProject(workspaceRoot, name) {
700
725
  const [project] = config.projects.slice(index, index + 1);
701
726
  const projects = config.projects.filter((_, projectIndex) => projectIndex !== index);
702
727
  await writeJsonAtomic(path.join(root, "taskchef.json"), {
703
- schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
728
+ ...config,
704
729
  projects,
705
730
  });
706
731
  return { project };