taskchef 7.13.0 → 7.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.13.0",
3
+ "version": "7.14.1",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/docs/spec.md CHANGED
@@ -138,6 +138,13 @@ the final link, preserving the delegate skill's immediate-return contract.
138
138
  TaskChef MUST atomically close it as `interrupted` before appending the new
139
139
  working turn; the executor MUST NOT report semantic `failed` for recovery.
140
140
 
141
+ Request and result summaries are the durable source for dashboard related-link
142
+ projection; TaskChef does not scan full Codex transcripts. When known, an
143
+ executor MUST preserve the selected repository as a canonical GitHub repository
144
+ URL and issues or pull requests as canonical URLs. A result spanning both a
145
+ managed child repository and its workspace/root repository MUST include both
146
+ pull-request URLs. Executors MUST NOT guess unresolved repository identity.
147
+
141
148
  If native creation fails after recording, the dispatcher MUST call
142
149
  `report_state` with `failed`, null thread/turn IDs, and a bounded summary.
143
150
  A link failure MUST remain visible and retryable; the executor MUST report it
@@ -287,8 +294,8 @@ marker, or ineligible state fails.
287
294
  | `threadId` | Matching non-empty ID for a linked task; null only for creation failure. |
288
295
  | `turnId` | Current canonical Codex UUIDv7 for a linked MCP journey; null only for creation failure. Maximum 256 characters at the MCP boundary. |
289
296
  | `status` | `working`, `needs_input`, `completed`, or `failed`. |
290
- | `summary` | Omitted or null for `working`; required non-empty string of at most 2,000 characters otherwise. |
291
- | `requestSummary` | Concise current request of at most 1,000 characters for `working`; optional for backward compatibility and omitted for semantic states. |
297
+ | `summary` | Omitted or null for `working`; required non-empty string of at most 2,000 characters otherwise. Known GitHub issues and pull requests use canonical URLs. |
298
+ | `requestSummary` | Concise current request of at most 1,000 characters for `working`; optional for backward compatibility and omitted for semantic states. A known selected GitHub repository uses its canonical URL. |
292
299
 
293
300
  **Structured output:** `{ task: Task }`.
294
301
 
package/docs/workflows.md CHANGED
@@ -120,6 +120,13 @@ The executor obtains the turn identity from an exact native read of its own
120
120
  linked task. `report_state` records live turn state as a paired request/result
121
121
  timeline.
122
122
 
123
+ That timeline is also the durable GitHub-link source. A working request names a
124
+ known selected repository with its canonical GitHub URL. A terminal result
125
+ keeps full issue and pull-request URLs, including separate child-repository and
126
+ workspace/root pull requests. The dashboard does not recover omitted links by
127
+ scanning the native Codex transcript, and the executor does not guess an
128
+ ambiguous repository.
129
+
123
130
  ```mermaid
124
131
  sequenceDiagram
125
132
  autonumber
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.13.0",
3
+ "version": "7.14.1",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -35,6 +35,9 @@ Complete this lifecycle setup before substantive assignment work:
35
35
  4. Call TaskChef `report_state` with the marked task ID, self-linked thread ID,
36
36
  current turn ID, `status: working`, an omitted or null result summary, and a
37
37
  concise `requestSummary` describing this turn's assignment or follow-up.
38
+ When the turn targets a known GitHub repository, include its canonical
39
+ `https://github.com/<owner>/<repository>` URL so multi-repository projects
40
+ retain the selected repository instead of leaving TaskChef to guess.
38
41
 
39
42
  If the preceding TaskChef turn is still unfinished because its terminal report
40
43
  was lost, this newer valid working report atomically records that predecessor
@@ -71,6 +74,30 @@ and unnecessary personal data. Identical lifecycle retries are safe; never
71
74
  replace a same-turn report with different content or let an older turn
72
75
  overwrite newer state.
73
76
 
77
+ ### Preserve repository and delivery links
78
+
79
+ Treat lifecycle summaries as the durable TaskChef timeline; the dashboard does
80
+ not scan the full Codex transcript later. Preserve known GitHub context in that
81
+ timeline with canonical links:
82
+
83
+ - Use `https://github.com/<owner>/<repository>` when identifying the repository
84
+ selected for a turn. Derive it from established project or Git remote
85
+ evidence; never guess from a similarly named directory.
86
+ - Use the full canonical issue or pull-request URL whenever one is known. Do
87
+ not shorten it to bare `#123` or `PR #123` in a multi-repository project.
88
+ - Include every delivered pull request in the result summary. If work produces
89
+ both a child-repository pull request and a workspace/root pull request,
90
+ preserve both URLs and state which repository each belongs to.
91
+ - Keep references specific to the text being reported. Do not copy unrelated
92
+ links from earlier turns merely to make them visible.
93
+ - Never invent an issue, pull request, or repository link. If repository
94
+ identity is genuinely unresolved, describe that ambiguity without a link.
95
+
96
+ For example, a working summary may say `Update dashboard projection in
97
+ https://github.com/favoyang/taskchef`. A terminal summary may say `Shipped the
98
+ child change in https://github.com/favoyang/taskchef/pull/78 and the workspace
99
+ plan in https://github.com/favoyang/skills-workspace/pull/62`.
100
+
74
101
  ## Compatibility
75
102
 
76
103
  Existing delegated tasks may include the former inline ownership, linking, and
@@ -16,6 +16,7 @@ import {
16
16
  turnPresentation,
17
17
  } from "./state.js";
18
18
  import { openTaskFromControl } from "./actions.js";
19
+ import { githubReferenceSegments } from "./github-links.js";
19
20
  import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
20
21
 
21
22
  const state = {
@@ -43,6 +44,7 @@ const elements = {
43
44
  dialogInstruction: document.querySelector("#dialog-instruction"),
44
45
  dialogMetadata: document.querySelector("#dialog-metadata"),
45
46
  dialogProject: document.querySelector("#dialog-project"),
47
+ dialogRelatedLinks: document.querySelector("#dialog-related-links"),
46
48
  dialogResults: document.querySelector("#dialog-results"),
47
49
  dialogTitle: document.querySelector("#dialog-title"),
48
50
  dismissDashboardMessage: document.querySelector("#dismiss-dashboard-message"),
@@ -59,6 +61,58 @@ const elements = {
59
61
  };
60
62
  let notificationDescriptionSerial = 0;
61
63
 
64
+ function githubLink(link, { compact = false } = {}) {
65
+ const anchor = document.createElement("a");
66
+ anchor.className = compact ? "github-link github-link-compact" : "github-link";
67
+ anchor.href = link.url;
68
+ anchor.target = "_blank";
69
+ anchor.rel = "noopener noreferrer";
70
+ anchor.textContent = link.label ?? link.text;
71
+ anchor.setAttribute(
72
+ "aria-label",
73
+ `${link.label ?? link.text} on GitHub (opens in a new tab)`,
74
+ );
75
+ anchor.addEventListener("click", (event) => event.stopPropagation());
76
+ return anchor;
77
+ }
78
+
79
+ function appendGitHubText(container, text, task) {
80
+ const children = githubReferenceSegments(text, {
81
+ projectRepositories: task.project?.githubRepos,
82
+ taskRepository: task.relatedGitHubRepository,
83
+ }).map((segment) => {
84
+ if (segment.kind === "text") return document.createTextNode(segment.text);
85
+ if (segment.kind === "link") return githubLink(segment);
86
+ const ambiguous = document.createElement("span");
87
+ ambiguous.className = "github-reference-ambiguous";
88
+ ambiguous.textContent = segment.text;
89
+ ambiguous.tabIndex = 0;
90
+ ambiguous.title = segment.reason;
91
+ ambiguous.setAttribute("aria-label", segment.reason);
92
+ return ambiguous;
93
+ });
94
+ container.replaceChildren(...children);
95
+ }
96
+
97
+ function relatedGitHubLinks(task, { compact = false } = {}) {
98
+ const links = task.relatedGitHubLinks ?? [];
99
+ const container = document.createElement("nav");
100
+ container.className = `github-links${compact ? " github-links-compact" : ""}`;
101
+ container.setAttribute("aria-label", `Related GitHub links for ${task.title}`);
102
+ const children = links.map((link) => githubLink(link, { compact }));
103
+ if (task.relatedGitHubLinksTruncated) {
104
+ const more = document.createElement("span");
105
+ more.className = "github-links-more";
106
+ more.textContent = "+ more";
107
+ more.title = "Additional related GitHub references are omitted from this bounded dashboard view.";
108
+ more.setAttribute("aria-label", more.title);
109
+ children.push(more);
110
+ }
111
+ container.hidden = children.length === 0;
112
+ container.replaceChildren(...children);
113
+ return container;
114
+ }
115
+
62
116
  function timestampControl(value, { accessibleName, key, prefix = "" }) {
63
117
  if (!parsedTimestamp(value)) {
64
118
  const missing = document.createElement("span");
@@ -253,12 +307,16 @@ function turnTimeline(task) {
253
307
  requestLabel.textContent = "Request";
254
308
  const request = document.createElement("p");
255
309
  request.className = "preserve-lines";
256
- request.textContent = turn.requestSummary ?? "Request not recorded by this TaskChef version.";
310
+ appendGitHubText(
311
+ request,
312
+ turn.requestSummary ?? "Request not recorded by this TaskChef version.",
313
+ task,
314
+ );
257
315
  const resultLabel = document.createElement("h4");
258
316
  resultLabel.textContent = "Result";
259
317
  const result = document.createElement("p");
260
318
  result.className = "preserve-lines";
261
- result.textContent = presentation.summary;
319
+ appendGitHubText(result, presentation.summary, task);
262
320
  const turnMetadata = document.createElement("p");
263
321
  turnMetadata.className = "result-history-turn";
264
322
  turnMetadata.textContent = turn.turnId
@@ -281,6 +339,14 @@ function renderDialog(task) {
281
339
  state.selectedTask = detailedTask;
282
340
  elements.dialogProject.textContent = task.project.name;
283
341
  elements.dialogTitle.textContent = task.title;
342
+ elements.dialogRelatedLinks.replaceChildren(...relatedGitHubLinks(detailedTask).children);
343
+ elements.dialogRelatedLinks.setAttribute(
344
+ "aria-label",
345
+ `Related GitHub links for ${task.title}`,
346
+ );
347
+ elements.dialogRelatedLinks.hidden = (
348
+ (task.relatedGitHubLinks?.length ?? 0) === 0 && !task.relatedGitHubLinksTruncated
349
+ );
284
350
  elements.dialogResults.replaceChildren(...turnTimeline(detailedTask));
285
351
  elements.dialogInstruction.textContent = task.instruction;
286
352
  elements.copyThreadId.disabled = !task.threadId;
@@ -365,6 +431,7 @@ function taskCard(task) {
365
431
  result.className = "preserve-lines";
366
432
  result.textContent = latest.resultSummary;
367
433
  summary.replaceChildren(requestLabel, request, resultLabel, result);
434
+ const relatedLinks = relatedGitHubLinks(task, { compact: true });
368
435
  const time = timestampControl(
369
436
  task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt,
370
437
  {
@@ -383,7 +450,7 @@ function taskCard(task) {
383
450
  showMessage,
384
451
  }));
385
452
  footer.append(time, openTask);
386
- article.append(heading, project, summary, footer);
453
+ article.append(heading, project, summary, relatedLinks, footer);
387
454
  return article;
388
455
  }
389
456
 
@@ -0,0 +1,212 @@
1
+ const GITHUB_REPOSITORY = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+$/;
2
+ const REFERENCE_PATTERN = /https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)\/(issues|pull)\/([1-9]\d*)(?![A-Za-z0-9_])|([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)#([1-9]\d*)(?![A-Za-z0-9_])|#([1-9]\d*)(?![A-Za-z0-9_])|https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9_-])?)\/?(?=$|[\s),.;:!?'"\]}>])/gi;
3
+ export const MAX_RELATED_GITHUB_LINKS = 20;
4
+
5
+ function canonicalRepository(owner, repository) {
6
+ return `${owner.toLowerCase()}/${repository.toLowerCase()}`;
7
+ }
8
+
9
+ function validRepositoryName(repository) {
10
+ return repository !== "." && repository !== "..";
11
+ }
12
+
13
+ export function normalizeGitHubRepository(value) {
14
+ if (typeof value !== "string") return null;
15
+ let candidate = value.trim();
16
+ const url = candidate.match(/^https?:\/\/(?:www\.)?github\.com\/([^/?#]+)\/([^/?#]+?)(?:\.git)?\/?$/i);
17
+ if (url) candidate = `${url[1]}/${url[2]}`;
18
+ if (!GITHUB_REPOSITORY.test(candidate)) return null;
19
+ const [owner, repository] = candidate.split("/");
20
+ if (!validRepositoryName(repository)) return null;
21
+ return canonicalRepository(owner, repository);
22
+ }
23
+
24
+ function rawReferences(text) {
25
+ const value = String(text ?? "");
26
+ const references = [];
27
+ for (const match of value.matchAll(REFERENCE_PATTERN)) {
28
+ const start = match.index;
29
+ const preceding = start > 0 ? value[start - 1] : "";
30
+ if (match[8] && /[\w&#/]/.test(preceding)) continue;
31
+ if ((match[5] || match[1] || match[9]) && /[\w/]/.test(preceding)) continue;
32
+
33
+ let owner = match[1] ?? match[5] ?? match[9] ?? null;
34
+ let repository = match[2] ?? match[6] ?? match[10] ?? null;
35
+ if (match[9]) repository = repository.replace(/\.git$/i, "");
36
+ const number = match[4] ?? match[7] ?? match[8] ?? null;
37
+ const pathType = match[3]?.toLowerCase();
38
+ const type = match[9]
39
+ ? "repository"
40
+ : pathType === "pull" ? "pull" : pathType === "issues" ? "issue" : "generic";
41
+ const explicit = Boolean(owner && repository);
42
+ if (explicit) {
43
+ if (!validRepositoryName(repository)) continue;
44
+ const normalized = canonicalRepository(owner, repository);
45
+ [owner, repository] = normalized.split("/");
46
+ }
47
+ references.push({
48
+ end: start + match[0].length,
49
+ explicit,
50
+ number,
51
+ owner,
52
+ repository,
53
+ start,
54
+ text: match[0],
55
+ type,
56
+ });
57
+ }
58
+ return references;
59
+ }
60
+
61
+ function uniqueExplicitRepository(references) {
62
+ const repositories = new Set(
63
+ references.filter(({ explicit }) => explicit).map(({ owner, repository }) => (
64
+ `${owner}/${repository}`
65
+ )),
66
+ );
67
+ return repositories.size === 1 ? [...repositories][0] : null;
68
+ }
69
+
70
+ function uniqueProjectRepository(projectRepositories) {
71
+ const repositories = new Set(
72
+ (projectRepositories ?? []).map(normalizeGitHubRepository).filter(Boolean),
73
+ );
74
+ return repositories.size === 1 ? [...repositories][0] : null;
75
+ }
76
+
77
+ function resolvedReference(reference, repositoryContext) {
78
+ const repository = reference.explicit
79
+ ? `${reference.owner}/${reference.repository}`
80
+ : repositoryContext;
81
+ if (!repository) return null;
82
+ const [owner, repositoryName] = repository.split("/");
83
+ if (reference.type === "repository") {
84
+ return {
85
+ kind: "link",
86
+ number: null,
87
+ owner,
88
+ repository: repositoryName,
89
+ text: reference.text,
90
+ type: reference.type,
91
+ url: `https://github.com/${owner}/${repositoryName}`,
92
+ };
93
+ }
94
+ const path = reference.type === "pull" ? "pull" : "issues";
95
+ return {
96
+ kind: "link",
97
+ number: reference.number,
98
+ owner,
99
+ repository: repositoryName,
100
+ text: reference.text,
101
+ type: reference.type,
102
+ url: `https://github.com/${owner}/${repositoryName}/${path}/${reference.number}`,
103
+ };
104
+ }
105
+
106
+ export function githubReferenceSegments(text, {
107
+ projectRepositories = [],
108
+ taskRepository = null,
109
+ } = {}) {
110
+ const value = String(text ?? "");
111
+ const references = rawReferences(value);
112
+ const sourceRepository = uniqueExplicitRepository(references);
113
+ const repositoryContext = sourceRepository
114
+ ?? normalizeGitHubRepository(taskRepository)
115
+ ?? uniqueProjectRepository(projectRepositories);
116
+ const segments = [];
117
+ let offset = 0;
118
+ for (const reference of references) {
119
+ if (reference.start > offset) {
120
+ segments.push({ kind: "text", text: value.slice(offset, reference.start) });
121
+ }
122
+ const resolved = resolvedReference(reference, repositoryContext);
123
+ if (resolved) segments.push(resolved);
124
+ else {
125
+ segments.push({
126
+ kind: "ambiguous",
127
+ reason: `GitHub reference ${reference.text} is not linked because the repository is ambiguous.`,
128
+ text: reference.text,
129
+ });
130
+ }
131
+ offset = reference.end;
132
+ }
133
+ if (offset < value.length) segments.push({ kind: "text", text: value.slice(offset) });
134
+ return segments;
135
+ }
136
+
137
+ function taskTexts(task) {
138
+ const texts = [task.instruction];
139
+ for (const turn of task.turns ?? []) {
140
+ texts.push(turn.requestSummary, turn.result?.summary);
141
+ }
142
+ if (!(task.turns?.length > 0)) {
143
+ for (const result of task.results ?? []) texts.push(result.summary);
144
+ texts.push(task.summary);
145
+ }
146
+ return texts.filter((text) => typeof text === "string" && text.length > 0);
147
+ }
148
+
149
+ function relatedLinkLabel(link, includeRepository, includeOwner) {
150
+ if (link.type === "repository") return `${link.owner}/${link.repository}`;
151
+ const reference = link.type === "pull"
152
+ ? `PR #${link.number}`
153
+ : link.type === "issue"
154
+ ? `Issue #${link.number}`
155
+ : `#${link.number}`;
156
+ if (!includeRepository) return reference;
157
+ const repository = includeOwner ? `${link.owner}/${link.repository}` : link.repository;
158
+ return `${repository} ${reference}`;
159
+ }
160
+
161
+ export function taskGitHubProjection(task) {
162
+ const texts = taskTexts(task);
163
+ const explicitRepositories = new Set();
164
+ for (const text of texts) {
165
+ for (const reference of rawReferences(text)) {
166
+ if (reference.explicit) {
167
+ explicitRepositories.add(`${reference.owner}/${reference.repository}`);
168
+ }
169
+ }
170
+ }
171
+ const taskRepository = explicitRepositories.size === 1 ? [...explicitRepositories][0] : null;
172
+ const links = [];
173
+ const seen = new Set();
174
+ let truncated = false;
175
+ sourceLoop:
176
+ for (const text of texts) {
177
+ for (const segment of githubReferenceSegments(text, {
178
+ projectRepositories: task.project?.githubRepos,
179
+ taskRepository,
180
+ })) {
181
+ if (segment.kind !== "link") continue;
182
+ const key = segment.url.toLowerCase();
183
+ if (seen.has(key)) continue;
184
+ if (links.length === MAX_RELATED_GITHUB_LINKS) {
185
+ truncated = true;
186
+ break sourceLoop;
187
+ }
188
+ seen.add(key);
189
+ links.push(segment);
190
+ }
191
+ }
192
+ const repositories = new Set(links.map(({ owner, repository }) => `${owner}/${repository}`));
193
+ const repositoryNames = new Map();
194
+ for (const link of links) {
195
+ const owners = repositoryNames.get(link.repository) ?? new Set();
196
+ owners.add(link.owner);
197
+ repositoryNames.set(link.repository, owners);
198
+ }
199
+ const includeRepository = repositories.size > 1;
200
+ return {
201
+ relatedGitHubLinks: links.map((link) => ({
202
+ label: relatedLinkLabel(link, includeRepository, repositoryNames.get(link.repository).size > 1),
203
+ number: link.number,
204
+ owner: link.owner,
205
+ repository: link.repository,
206
+ type: link.type,
207
+ url: link.url,
208
+ })),
209
+ relatedGitHubLinksTruncated: truncated,
210
+ relatedGitHubRepository: taskRepository,
211
+ };
212
+ }
@@ -114,6 +114,7 @@
114
114
  </button>
115
115
  <button id="copy-thread-id" class="secondary-button" type="button">Copy thread ID</button>
116
116
  </div>
117
+ <nav id="dialog-related-links" class="github-links" aria-label="Related GitHub links" hidden></nav>
117
118
  <section>
118
119
  <h3>Activity timeline</h3>
119
120
  <div id="dialog-results" class="result-history"></div>
@@ -97,6 +97,15 @@ 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; }
101
+ .github-links[hidden] { display: none; }
102
+ .github-links-compact { margin: -2px 0 12px; gap: 5px; }
103
+ .github-link { color: var(--accent); font-weight: 700; text-decoration-thickness: 1px; text-underline-offset: 3px; overflow-wrap: anywhere; }
104
+ .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
+ .github-link:hover { text-decoration-thickness: 2px; }
106
+ .github-link-compact:hover { border-color: var(--accent); background: var(--accent-soft); }
107
+ .github-links-more { align-self: center; color: var(--muted); font-size: 0.75rem; font-weight: 700; }
108
+ .github-reference-ambiguous { border-bottom: 2px dotted var(--warning); background: color-mix(in srgb, var(--warning) 12%, transparent); cursor: help; }
100
109
  time, .timestamp-missing { font-size: 0.75rem; }
101
110
  .timestamp-toggle { padding: 2px 0; border: 0; background: none; cursor: pointer; text-align: left; }
102
111
  .timestamp-toggle:hover time { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
@@ -136,7 +145,7 @@ time, .timestamp-missing { font-size: 0.75rem; }
136
145
 
137
146
  dialog { width: min(760px, calc(100vw - 32px)); max-height: min(82vh, 900px); padding: 28px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); color: var(--text); box-shadow: var(--shadow); }
138
147
  dialog::backdrop { background: rgb(18 23 21 / 50%); backdrop-filter: blur(2px); }
139
- .dialog-header { display: flex; justify-content: space-between; gap: 20px; }
148
+ .dialog-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
140
149
  .dialog-header h2 { margin-bottom: 12px; font-size: 1.6rem; }
141
150
  .dialog-actions { display: flex; gap: 8px; margin-bottom: 26px; }
142
151
  .primary-button, .secondary-button { padding: 8px 12px; border-radius: 7px; font-weight: 700; cursor: pointer; }
@@ -173,6 +182,8 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
173
182
  .task-heading { align-items: flex-start; }
174
183
  .task-title, .timestamp-toggle { display: flex; align-items: center; min-height: 36px; }
175
184
  .task-footer { align-items: flex-start; flex-direction: column; }
185
+ .github-links { max-width: 100%; }
186
+ .github-link-compact { padding-block: 5px; }
176
187
  .task-open { align-self: flex-start; }
177
188
  select { min-height: 40px; }
178
189
  .status-filter-option span { min-height: 38px; }
package/src/dashboard.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  readConfig,
18
18
  } from "./workspace.js";
19
19
  import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
20
+ import { taskGitHubProjection } from "./dashboard/github-links.js";
20
21
 
21
22
  const TASKS_FILE_NAME = "tasks.jsonl";
22
23
  const STATIC_ROOT = fileURLToPath(new URL("./dashboard/", import.meta.url));
@@ -43,6 +44,7 @@ const STATIC_FILES = new Map([
43
44
  ["/", [path.join(STATIC_ROOT, "index.html"), "text/html; charset=utf-8"]],
44
45
  ["/actions.js", [path.join(STATIC_ROOT, "actions.js"), "text/javascript; charset=utf-8"]],
45
46
  ["/app.js", [path.join(STATIC_ROOT, "app.js"), "text/javascript; charset=utf-8"]],
47
+ ["/github-links.js", [path.join(STATIC_ROOT, "github-links.js"), "text/javascript; charset=utf-8"]],
46
48
  ["/time.js", [path.join(STATIC_ROOT, "time.js"), "text/javascript; charset=utf-8"]],
47
49
  ["/state.js", [path.join(STATIC_ROOT, "state.js"), "text/javascript; charset=utf-8"]],
48
50
  ["/styles.css", [path.join(STATIC_ROOT, "styles.css"), "text/css; charset=utf-8"]],
@@ -171,10 +173,17 @@ function assertDashboardTaskBounds(tasks, maximumTasks) {
171
173
  }
172
174
 
173
175
  function taskListProjection(task) {
174
- const { turns: _turns, results: _results, ...projection } = task;
176
+ const { turns: _turns, results: _results, ...projection } = {
177
+ ...task,
178
+ ...taskGitHubProjection(task),
179
+ };
175
180
  return projection;
176
181
  }
177
182
 
183
+ function taskDetailProjection(task) {
184
+ return { ...task, ...taskGitHubProjection(task) };
185
+ }
186
+
178
187
  export class DashboardMonitor extends EventEmitter {
179
188
  constructor(workspace, {
180
189
  debounceMs = 75,
@@ -548,7 +557,7 @@ export async function createDashboardServer({
548
557
  response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
549
558
  response.end();
550
559
  } else {
551
- sendJson(response, 200, { schemaVersion: 1, task });
560
+ sendJson(response, 200, { schemaVersion: 1, task: taskDetailProjection(task) });
552
561
  }
553
562
  return;
554
563
  }
package/src/mcp.js CHANGED
@@ -225,7 +225,7 @@ export function createTaskChefMcpServer({
225
225
  {
226
226
  title: "Report TaskChef state",
227
227
  description:
228
- "Report this self-linked executor turn's lifecycle state. Use working before substantive work in a newly linked or follow-up turn, with summary omitted or null and a concise requestSummary. Before ending the same turn, report needs_input, completed, or failed with a concise semantic summary and requestSummary omitted. Exact retries are idempotent; stale or mismatched turns are rejected.",
228
+ "Report this self-linked executor turn's lifecycle state. Use working before substantive work in a newly linked or follow-up turn, with summary omitted or null and a concise requestSummary. Preserve known repository context and delivered issues or pull requests as canonical GitHub URLs, including both child-repository and workspace pull requests when applicable. Before ending the same turn, report needs_input, completed, or failed with a concise semantic summary and requestSummary omitted. Exact retries are idempotent; stale or mismatched turns are rejected.",
229
229
  inputSchema: {
230
230
  taskId: z.string().min(1),
231
231
  threadId: z.string().min(1).nullable(),