taskchef 7.12.2 → 7.14.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 +3 -3
- package/README.md +28 -8
- package/assets/taskchef-dispatcher-instructions.md +14 -6
- package/docs/firstmate-taskchef-comparison.md +8 -7
- package/docs/spec.md +38 -12
- package/docs/workflows.md +3 -3
- package/index.js +1 -0
- package/package.json +2 -2
- package/skills/taskchef-copilot/SKILL.md +73 -0
- package/skills/taskchef-copilot/agents/openai.yaml +4 -0
- package/src/cli.js +40 -0
- package/src/dashboard/app.js +70 -3
- package/src/dashboard/github-links.js +197 -0
- package/src/dashboard/index.html +1 -0
- package/src/dashboard/styles.css +12 -1
- package/src/dashboard.js +11 -2
- package/src/workspace.js +142 -0
- package/skills/taskchef-report/SKILL.md +0 -91
- package/skills/taskchef-report/agents/openai.yaml +0 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskchef",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.14.0",
|
|
4
4
|
"description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Favo Yang",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"interface": {
|
|
21
21
|
"displayName": "TaskChef",
|
|
22
22
|
"shortDescription": "Dispatch work to the right Codex project task.",
|
|
23
|
-
"longDescription": "Route work to visible Codex project tasks,
|
|
23
|
+
"longDescription": "Route work to visible Codex project tasks, monitor them in the dashboard, and get concise cached-first coordination briefs.",
|
|
24
24
|
"developerName": "Favo Yang",
|
|
25
25
|
"category": "Productivity",
|
|
26
26
|
"capabilities": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"$taskchef-bootstrap Set up TaskChef in this folder.",
|
|
33
33
|
"$taskchef-delegate Dispatch this request to the right project.",
|
|
34
34
|
"$taskchef-executor Execute this delegated TaskChef assignment.",
|
|
35
|
-
"$taskchef-
|
|
35
|
+
"$taskchef-copilot Explain TaskChef outcomes and recommend the next action."
|
|
36
36
|
],
|
|
37
37
|
"brandColor": "#D97706",
|
|
38
38
|
"composerIcon": "./assets/taskchef.svg",
|
package/README.md
CHANGED
|
@@ -136,27 +136,47 @@ inline executor protocol still parses, self-links, and may use the deprecated
|
|
|
136
136
|
`report_result` alias. The v7 inline-paragraph named exports remain as deprecated
|
|
137
137
|
historical snapshots, but new delegations use the executor skill and `report_state`.
|
|
138
138
|
|
|
139
|
-
## View and
|
|
139
|
+
## View tasks and ask copilot
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
The dashboard is the primary monitoring and browsing UI. Ask copilot when you
|
|
142
|
+
want a concise explanation or recommendation:
|
|
142
143
|
|
|
143
144
|
```text
|
|
144
|
-
|
|
145
|
+
$taskchef-copilot What finished, what needs attention, and what should I do next?
|
|
145
146
|
```
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
metadata
|
|
149
|
-
|
|
148
|
+
Copilot starts from TaskChef's normalized cached brief. It uses live Codex
|
|
149
|
+
metadata only when you explicitly request fresh/live verification or a focused
|
|
150
|
+
task presents a meaningful contradiction, and it never polls. It can identify
|
|
151
|
+
the exact executor, explain or draft a same-assignment follow-up, and—with your
|
|
152
|
+
explicit authorization—continue that existing task. It never automatically
|
|
153
|
+
retries failures, interrupts working tasks, or redelegates an executor. New
|
|
154
|
+
independent work still belongs to `$taskchef-delegate`. In the dispatcher, an
|
|
155
|
+
explicit instruction to answer, resume, or continue a named existing task
|
|
156
|
+
routes to copilot; it re-reads that exact task before sending.
|
|
157
|
+
|
|
158
|
+
The former `$taskchef-report` skill is not packaged as an alias because a
|
|
159
|
+
second discoverable skill would preserve ambiguous behavior. Explicit
|
|
160
|
+
historical invocations are understood as requests for `$taskchef-copilot` and
|
|
161
|
+
receive a brief rename notice.
|
|
162
|
+
|
|
163
|
+
File-backed inspection is also available:
|
|
150
164
|
|
|
151
165
|
```sh
|
|
166
|
+
taskchef task brief
|
|
167
|
+
taskchef task brief c0f010ff
|
|
168
|
+
taskchef task brief --project payments
|
|
152
169
|
taskchef task list
|
|
153
170
|
taskchef task list --project payments
|
|
154
171
|
taskchef task show c0f010ff
|
|
155
172
|
taskchef task summary
|
|
156
173
|
```
|
|
157
174
|
|
|
158
|
-
Add `--json` for structured output. `task
|
|
159
|
-
|
|
175
|
+
Add `--json` for structured output. `task brief` returns the stable schema-1
|
|
176
|
+
cached coordination model and omits terminal tasks older than seven days from
|
|
177
|
+
overviews unless `--all` is supplied. Focused task and project briefs retain
|
|
178
|
+
their full selected scope. Task IDs accept a full UUID or an unambiguous
|
|
179
|
+
eight-character prefix.
|
|
160
180
|
|
|
161
181
|
## Dashboard
|
|
162
182
|
|
|
@@ -9,18 +9,26 @@ This folder is the canonical per-user TaskChef dispatcher workspace.
|
|
|
9
9
|
- Use `$taskchef-bootstrap` when initializing or refreshing this workspace,
|
|
10
10
|
changing or listing its configured projects, running TaskChef doctor,
|
|
11
11
|
or repairing its managed instructions.
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
- When the user asks to answer, follow up, resume, or continue the same
|
|
13
|
+
recorded assignment in its existing executor task, use `$taskchef-copilot`
|
|
14
|
+
before the general delegation rule. A direct imperative that names the exact
|
|
15
|
+
existing task is explicit send authorization, but copilot must immediately
|
|
16
|
+
re-read that exact task and apply its state and identity guardrails.
|
|
17
|
+
- For every other actionable request that asks for an independent new outcome,
|
|
18
|
+
use `$taskchef-delegate` automatically, even when the user does not explicitly
|
|
19
|
+
say "delegate" or mention TaskChef.
|
|
15
20
|
- GitHub issue and pull-request URLs may identify any repository advertised by
|
|
16
21
|
a configured project, including child repositories of managed workspaces.
|
|
17
22
|
- Do not perform delegated work directly in the dispatcher thread.
|
|
18
23
|
- Return immediately after dispatch, as required by `$taskchef-delegate`.
|
|
19
24
|
- Answer directly only when the user explicitly asks about TaskChef itself or
|
|
20
25
|
explicitly says not to delegate.
|
|
21
|
-
- Use `$taskchef-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
- Use `$taskchef-copilot` when the user asks what delegated work finished,
|
|
27
|
+
what needs attention, why it failed or needs input, or what should happen
|
|
28
|
+
next, including same-assignment continuation in an exact existing executor.
|
|
29
|
+
Copilot uses the cached TaskChef brief by default; the dashboard is the
|
|
30
|
+
primary monitoring and browsing UI. Live verification is explicit or
|
|
31
|
+
contradiction-driven, focused, and never polled.
|
|
24
32
|
- Explicit invocations of TaskChef skills from other Codex projects use this
|
|
25
33
|
same workspace and task history through TaskChef's global resolution rules.
|
|
26
34
|
- Every final response in this dispatcher workspace must end with this exact
|
|
@@ -170,10 +170,10 @@ semantic `failed`.
|
|
|
170
170
|
`needs_input` is reserved for a real semantic decision, not a native approval
|
|
171
171
|
prompt.
|
|
172
172
|
|
|
173
|
-
**Fact:** The
|
|
174
|
-
Codex metadata
|
|
175
|
-
|
|
176
|
-
state.
|
|
173
|
+
**Fact:** The copilot skill explains normalized cached briefs by default and
|
|
174
|
+
uses live Codex metadata only for an explicit fresh/live request or a focused
|
|
175
|
+
meaningful contradiction. It recommends safe next actions, never polls, and
|
|
176
|
+
never writes inferred state.
|
|
177
177
|
|
|
178
178
|
### FirstMate
|
|
179
179
|
|
|
@@ -198,8 +198,9 @@ This navigation check does not prove schema 4 or 5 self-link provenance. The
|
|
|
198
198
|
dashboard does not refresh native task state, submit replies, or show
|
|
199
199
|
transcripts or token usage.
|
|
200
200
|
|
|
201
|
-
**Fact:** The user can also ask for a
|
|
202
|
-
|
|
201
|
+
**Fact:** The user can also ask TaskChef copilot for a concise cached brief or
|
|
202
|
+
focused live verification, or inspect the data CLI. Executor work remains
|
|
203
|
+
visible in normal Codex desktop tasks.
|
|
203
204
|
|
|
204
205
|
### FirstMate
|
|
205
206
|
|
|
@@ -218,7 +219,7 @@ harness adapter.
|
|
|
218
219
|
| --- | --- | --- |
|
|
219
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. |
|
|
220
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. |
|
|
221
|
-
| Skills | Four plugin skills with narrow bootstrap, delegate, executor, and
|
|
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
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. |
|
|
223
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. |
|
|
224
225
|
|
package/docs/spec.md
CHANGED
|
@@ -28,7 +28,7 @@ is dated research, not contract.
|
|
|
28
28
|
| **Last semantic result** | The final result-history entry, exposed through the derived `lastResult` compatibility alias. |
|
|
29
29
|
| **Current turn ID** | The canonical Codex UUIDv7 returned by an exact native read of the linked executor for the turn being reported. |
|
|
30
30
|
| **Dashboard** | The loopback, read-only UI derived from validated workspace snapshots and bounded native actions. |
|
|
31
|
-
| **Skill** | One packaged agent procedure: `taskchef-bootstrap`, `taskchef-delegate`, `taskchef-executor`, or `taskchef-
|
|
31
|
+
| **Skill** | One packaged agent procedure: `taskchef-bootstrap`, `taskchef-delegate`, `taskchef-executor`, or `taskchef-copilot`. |
|
|
32
32
|
|
|
33
33
|
## Components and ownership
|
|
34
34
|
|
|
@@ -39,8 +39,9 @@ is dated research, not contract.
|
|
|
39
39
|
- `taskchef-executor` MUST own executor assignment ownership, self-linking,
|
|
40
40
|
exact thread/turn identity, per-turn state reporting, failure behavior,
|
|
41
41
|
privacy, and idempotency. It MUST NOT dispatch the owned assignment again.
|
|
42
|
-
- `taskchef-
|
|
43
|
-
|
|
42
|
+
- `taskchef-copilot` MUST own conversational outcome explanation, attention,
|
|
43
|
+
and next-action recommendations. It MUST use cached normalized briefs by
|
|
44
|
+
default and MUST NOT poll or persist inferred state.
|
|
44
45
|
- The MCP server MUST expose `ensure_dashboard`, four primary lifecycle tools,
|
|
45
46
|
and the deprecated `report_result` compatibility alias specified below.
|
|
46
47
|
- The CLI MAY administer and inspect the workspace, but MUST NOT provide a
|
|
@@ -320,15 +321,40 @@ low-level opaque direct records. It does not accept `working`. New executor
|
|
|
320
321
|
instructions MUST use `report_state`. Successful mutation upgrades schema 4/5/6/7
|
|
321
322
|
to schema 8; unsupported schemas remain rejected.
|
|
322
323
|
|
|
323
|
-
##
|
|
324
|
-
|
|
325
|
-
A semantic result is cached evidence, not permanent live truth.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
324
|
+
## Copilot and dashboard
|
|
325
|
+
|
|
326
|
+
A semantic result is cached evidence, not permanent live truth. The dashboard
|
|
327
|
+
MUST remain the primary monitoring and browsing UI. Copilot MUST start from the
|
|
328
|
+
schema-1 normalized cached brief and explain what finished, what needs
|
|
329
|
+
attention, why, and the recommended next action. It MUST NOT need to interpret
|
|
330
|
+
historical task schema versions. A working task's current summary MUST remain
|
|
331
|
+
null; any prior semantic result MUST be exposed separately as a clearly
|
|
332
|
+
historical `lastOutcome`. A link-pending task with no exact thread identity MUST
|
|
333
|
+
recommend passive waiting or inspection, never retry or continuation.
|
|
334
|
+
|
|
335
|
+
Copilot MAY take one bounded native metadata snapshot only when the user
|
|
336
|
+
explicitly requests fresh/live verification or a focused task presents a
|
|
337
|
+
meaningful contradiction. Active or approval-waiting native state overrides
|
|
338
|
+
cache. An inactive task does not prove completion. Copilot MAY read the exact
|
|
339
|
+
selected task once for an explicit focused live-verification request or to
|
|
340
|
+
resolve a focused contradiction. It MUST NOT poll, wait, perform exhaustive
|
|
341
|
+
live audits, or classify assistant prose.
|
|
342
|
+
|
|
343
|
+
Copilot MAY identify the exact existing executor, explain or draft a
|
|
344
|
+
same-assignment follow-up, and continue that executor only with explicit user
|
|
345
|
+
authorization. It MUST NOT automatically retry failures, interrupt working
|
|
346
|
+
tasks, or redelegate an existing executor. Independent new work MUST route
|
|
347
|
+
through delegation. Managed dispatcher routing MUST give same-assignment
|
|
348
|
+
answer, follow-up, resume, and continue requests precedence over the blanket
|
|
349
|
+
new-work delegation rule. A direct imperative naming the exact existing task
|
|
350
|
+
MAY constitute send authorization, but copilot MUST re-read that exact task
|
|
351
|
+
immediately before sending.
|
|
352
|
+
|
|
353
|
+
The published plugin MUST NOT package `taskchef-report` as a discoverable alias.
|
|
354
|
+
Its historical explicit name is a documented rename hint handled by copilot,
|
|
355
|
+
not a second workflow.
|
|
356
|
+
|
|
357
|
+
Task lists, summaries, and broad briefs MUST use the final result by default.
|
|
332
358
|
The dashboard MUST bind only to loopback, validate the current workspace
|
|
333
359
|
snapshot, and avoid sessions or shared client state. `GET /api/health` MUST
|
|
334
360
|
return only the bounded service identity, health schema, exact TaskChef and
|
package/docs/workflows.md
CHANGED
|
@@ -13,11 +13,11 @@ 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 current workspace and configure projects. |
|
|
16
|
-
| `skills/taskchef-
|
|
16
|
+
| `skills/taskchef-copilot/SKILL.md` | Explain normalized cached briefs and coordinate safe next actions. |
|
|
17
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
|
-
| `src/cli.js` | Administration, inspection, diagnostics, and dashboard startup. |
|
|
20
|
+
| `src/cli.js` | Administration, normalized cached briefs, inspection, diagnostics, and dashboard startup. |
|
|
21
21
|
| `src/dashboard.js` | Versioned health identity, validated compact snapshots, SSE fan-out, on-demand details, and bounded open actions. |
|
|
22
22
|
| `src/dashboard-manager.js` | Concurrent singleton ensure, exact listener reuse, conflicts, and owned shutdown. |
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ per-user default.
|
|
|
29
29
|
|
|
30
30
|
The generated managed `AGENTS.md` block makes dashboard maintenance a
|
|
31
31
|
best-effort prelude to every dispatcher turn and keeps response ordering
|
|
32
|
-
centralized instead of duplicating it across delegate/
|
|
32
|
+
centralized instead of duplicating it across delegate/copilot skills.
|
|
33
33
|
|
|
34
34
|
```mermaid
|
|
35
35
|
sequenceDiagram
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskchef",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.14.0",
|
|
4
4
|
"description": "A non-blocking interactive dispatcher for visible Codex tasks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Favo Yang",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"skills/taskchef-bootstrap",
|
|
39
39
|
"skills/taskchef-delegate",
|
|
40
40
|
"skills/taskchef-executor",
|
|
41
|
-
"skills/taskchef-
|
|
41
|
+
"skills/taskchef-copilot"
|
|
42
42
|
],
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=18"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: taskchef-copilot
|
|
3
|
+
description: "Explain and coordinate TaskChef executor outcomes from cached task briefs. Use when the user asks what finished, what needs attention, why a delegated task failed or needs input, what should happen next, asks to answer, follow up, resume, or continue the same recorded assignment in its existing task, or explicitly invokes the historical $taskchef-report name. The dashboard remains the primary monitoring UI. Use live Codex metadata only for an explicit fresh/live request or a meaningful focused contradiction, and never poll."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TaskChef Copilot
|
|
7
|
+
|
|
8
|
+
Act as TaskChef's conversational coordination assistant. Give a concise brief
|
|
9
|
+
about finished work, attention, cause, and the safest next action. The dashboard
|
|
10
|
+
is the primary place to browse and monitor the task history.
|
|
11
|
+
|
|
12
|
+
Resolve this skill directory with `realpath`. The TaskChef plugin root is two
|
|
13
|
+
parents above it. Invoke `<plugin-root>/bin/taskchef.js` for deterministic
|
|
14
|
+
TaskChef data reads.
|
|
15
|
+
|
|
16
|
+
## Cached brief first
|
|
17
|
+
|
|
18
|
+
1. Select the smallest scope that answers the request:
|
|
19
|
+
- exact task: `task brief <task-id> --json`;
|
|
20
|
+
- configured project: `task brief --project <name-or-path> --json`;
|
|
21
|
+
- overview: `task brief --json`.
|
|
22
|
+
Use `task list --json` once only when a title or description must first be
|
|
23
|
+
matched. Ask when multiple tasks plausibly match.
|
|
24
|
+
2. Treat the returned schema-1 brief as the stable model. Do not interpret raw
|
|
25
|
+
task-log schema versions or read `tasks.jsonl` directly. The brief already
|
|
26
|
+
normalizes state, current summary, explicitly historical `lastOutcome`,
|
|
27
|
+
attention, next action, interrupted-turn count, link-pending identity, and
|
|
28
|
+
the default seven-day overview window. Never present `lastOutcome` as the
|
|
29
|
+
result of a newer working request.
|
|
30
|
+
3. Lead with the useful answer: what finished, what needs attention, why, and
|
|
31
|
+
what action should happen next. Keep overviews short and mention the omitted
|
|
32
|
+
old-terminal count when nonzero. Link or identify the exact existing
|
|
33
|
+
executor task when its durable thread ID is available.
|
|
34
|
+
|
|
35
|
+
## Live verification is exceptional
|
|
36
|
+
|
|
37
|
+
Use native Codex metadata only when the user explicitly asks for a fresh,
|
|
38
|
+
current, or live verification, or when a focused task has a meaningful
|
|
39
|
+
contradiction such as the user observing activity that conflicts with the
|
|
40
|
+
cached brief.
|
|
41
|
+
|
|
42
|
+
- Take one bounded metadata snapshot and match only the exact stored thread ID.
|
|
43
|
+
- For one focused task, read that exact existing task once when the user
|
|
44
|
+
explicitly requests live verification or when needed to resolve a meaningful
|
|
45
|
+
contradiction. Do not read transcripts or classify assistant prose.
|
|
46
|
+
- Active or native-approval state may override the cached presentation. An
|
|
47
|
+
inactive task does not prove semantic completion. If verification fails,
|
|
48
|
+
present the cached brief with a freshness warning.
|
|
49
|
+
- Never poll, wait, repeatedly refresh, or perform a broad exhaustive live
|
|
50
|
+
audit.
|
|
51
|
+
|
|
52
|
+
## Coordination and continuation
|
|
53
|
+
|
|
54
|
+
- Explain or draft a focused follow-up for the exact existing executor task.
|
|
55
|
+
- Continue that task only after the user explicitly authorizes sending the
|
|
56
|
+
follow-up. A direct imperative naming the exact existing task is explicit
|
|
57
|
+
send authorization. Re-read the exact target immediately before sending and
|
|
58
|
+
preserve its durable identity.
|
|
59
|
+
- Never automatically retry a failure, interrupt a working task, or redelegate
|
|
60
|
+
an existing executor as a new task.
|
|
61
|
+
- A missing executor link calls for passive waiting or identity inspection,
|
|
62
|
+
not a retry, continuation, or reason to create another executor.
|
|
63
|
+
- Keep same-assignment follow-up in the existing executor task. Independent new
|
|
64
|
+
work with its own outcome belongs to `$taskchef-delegate`.
|
|
65
|
+
- If scope is mixed, separate the proposed existing-task follow-up from the new
|
|
66
|
+
work and ask for authorization before either mutation.
|
|
67
|
+
|
|
68
|
+
## Historical invocation
|
|
69
|
+
|
|
70
|
+
`taskchef-report` was renamed rather than retained as a packaged alias because
|
|
71
|
+
a second discoverable skill would preserve ambiguous reporting behavior. If a
|
|
72
|
+
user explicitly writes `$taskchef-report`, briefly note the rename and fulfill
|
|
73
|
+
the request through this cached-first copilot workflow.
|
package/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { resolveWorkspacePath } from "./workspace-path.js";
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
addProject,
|
|
10
|
+
buildCopilotBrief,
|
|
10
11
|
buildTaskSummary,
|
|
11
12
|
doctorWorkspace,
|
|
12
13
|
filterTasks,
|
|
@@ -414,6 +415,43 @@ async function taskSummary(args) {
|
|
|
414
415
|
return 0;
|
|
415
416
|
}
|
|
416
417
|
|
|
418
|
+
async function taskBrief(args) {
|
|
419
|
+
const hasTaskId = Boolean(args[2] && !args[2].startsWith("--"));
|
|
420
|
+
validateCommandArgs(args, hasTaskId ? 3 : 2, {
|
|
421
|
+
values: ["--workspace", "--project"],
|
|
422
|
+
switches: ["--all", "--json"],
|
|
423
|
+
});
|
|
424
|
+
if (hasTaskId && args.includes("--project")) {
|
|
425
|
+
throw new Error("task brief accepts either a task ID or --project, not both");
|
|
426
|
+
}
|
|
427
|
+
const taskId = hasTaskId
|
|
428
|
+
? (await readTaskForShow(workspaceRoot(args), args[2])).id
|
|
429
|
+
: null;
|
|
430
|
+
const brief = await buildCopilotBrief(workspaceRoot(args), {
|
|
431
|
+
taskId,
|
|
432
|
+
project: option(args, "--project", null),
|
|
433
|
+
includeOldTerminal: args.includes("--all"),
|
|
434
|
+
});
|
|
435
|
+
print(brief, args, (value) => {
|
|
436
|
+
const rows = value.tasks.map((task) => [
|
|
437
|
+
task.title,
|
|
438
|
+
task.project.name,
|
|
439
|
+
task.state,
|
|
440
|
+
task.attention?.kind ?? "-",
|
|
441
|
+
task.nextAction.kind,
|
|
442
|
+
displayId(task.id, false),
|
|
443
|
+
]);
|
|
444
|
+
return [
|
|
445
|
+
`Cached TaskChef brief (${value.scope})`,
|
|
446
|
+
table(["TITLE", "PROJECT", "STATE", "ATTENTION", "NEXT ACTION", "ID"], rows),
|
|
447
|
+
value.omittedTerminalCount > 0
|
|
448
|
+
? `Omitted old terminal tasks: ${value.omittedTerminalCount}`
|
|
449
|
+
: null,
|
|
450
|
+
].filter(Boolean).join("\n");
|
|
451
|
+
});
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
417
455
|
function dashboardPort(args) {
|
|
418
456
|
const value = option(args, "--port", "3210");
|
|
419
457
|
if (!/^\d+$/.test(value)) throw new Error("--port must be an integer from 0 to 65535");
|
|
@@ -481,6 +519,7 @@ Usage:
|
|
|
481
519
|
taskchef project remove <name> [--json] [--workspace <path>]
|
|
482
520
|
taskchef dispatch prepare [--json] [--workspace <path>]
|
|
483
521
|
taskchef task record [--json] [--workspace <path>]
|
|
522
|
+
taskchef task brief [<task-id-or-8-character-prefix> | --project <name-or-path>] [--all] [--json] [--workspace <path>]
|
|
484
523
|
taskchef task show <task-id-or-8-character-prefix> [--json] [--workspace <path>]
|
|
485
524
|
taskchef task list [--project <name-or-path>] [--ascending] [--full-id] [--json] [--workspace <path>]
|
|
486
525
|
taskchef task summary [--json] [--workspace <path>]
|
|
@@ -515,6 +554,7 @@ export async function runCli(args) {
|
|
|
515
554
|
if (args[0] === "project" && args[1] === "remove") return projectRemove(args);
|
|
516
555
|
if (args[0] === "dispatch" && args[1] === "prepare") return dispatchPrepare(args);
|
|
517
556
|
if (args[0] === "task" && args[1] === "record") return taskRecord(args);
|
|
557
|
+
if (args[0] === "task" && args[1] === "brief") return taskBrief(args);
|
|
518
558
|
if (args[0] === "task" && args[1] === "show" && args[2]) return taskShow(args);
|
|
519
559
|
if (args[0] === "task" && args[1] === "list") return taskList(args);
|
|
520
560
|
if (args[0] === "task" && args[1] === "summary") return taskSummary(args);
|
package/src/dashboard/app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,197 @@
|
|
|
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_])/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]) && /[\w/]/.test(preceding)) continue;
|
|
32
|
+
|
|
33
|
+
let owner = match[1] ?? match[5] ?? null;
|
|
34
|
+
let repository = match[2] ?? match[6] ?? null;
|
|
35
|
+
const number = match[4] ?? match[7] ?? match[8];
|
|
36
|
+
const pathType = match[3]?.toLowerCase();
|
|
37
|
+
const type = pathType === "pull" ? "pull" : pathType === "issues" ? "issue" : "generic";
|
|
38
|
+
const explicit = Boolean(owner && repository);
|
|
39
|
+
if (explicit) {
|
|
40
|
+
if (!validRepositoryName(repository)) continue;
|
|
41
|
+
const normalized = canonicalRepository(owner, repository);
|
|
42
|
+
[owner, repository] = normalized.split("/");
|
|
43
|
+
}
|
|
44
|
+
references.push({
|
|
45
|
+
end: start + match[0].length,
|
|
46
|
+
explicit,
|
|
47
|
+
number,
|
|
48
|
+
owner,
|
|
49
|
+
repository,
|
|
50
|
+
start,
|
|
51
|
+
text: match[0],
|
|
52
|
+
type,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return references;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function uniqueExplicitRepository(references) {
|
|
59
|
+
const repositories = new Set(
|
|
60
|
+
references.filter(({ explicit }) => explicit).map(({ owner, repository }) => (
|
|
61
|
+
`${owner}/${repository}`
|
|
62
|
+
)),
|
|
63
|
+
);
|
|
64
|
+
return repositories.size === 1 ? [...repositories][0] : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function uniqueProjectRepository(projectRepositories) {
|
|
68
|
+
const repositories = new Set(
|
|
69
|
+
(projectRepositories ?? []).map(normalizeGitHubRepository).filter(Boolean),
|
|
70
|
+
);
|
|
71
|
+
return repositories.size === 1 ? [...repositories][0] : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolvedReference(reference, repositoryContext) {
|
|
75
|
+
const repository = reference.explicit
|
|
76
|
+
? `${reference.owner}/${reference.repository}`
|
|
77
|
+
: repositoryContext;
|
|
78
|
+
if (!repository) return null;
|
|
79
|
+
const [owner, repositoryName] = repository.split("/");
|
|
80
|
+
const path = reference.type === "pull" ? "pull" : "issues";
|
|
81
|
+
return {
|
|
82
|
+
kind: "link",
|
|
83
|
+
number: reference.number,
|
|
84
|
+
owner,
|
|
85
|
+
repository: repositoryName,
|
|
86
|
+
text: reference.text,
|
|
87
|
+
type: reference.type,
|
|
88
|
+
url: `https://github.com/${owner}/${repositoryName}/${path}/${reference.number}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function githubReferenceSegments(text, {
|
|
93
|
+
projectRepositories = [],
|
|
94
|
+
taskRepository = null,
|
|
95
|
+
} = {}) {
|
|
96
|
+
const value = String(text ?? "");
|
|
97
|
+
const references = rawReferences(value);
|
|
98
|
+
const sourceRepository = uniqueExplicitRepository(references);
|
|
99
|
+
const repositoryContext = sourceRepository
|
|
100
|
+
?? normalizeGitHubRepository(taskRepository)
|
|
101
|
+
?? uniqueProjectRepository(projectRepositories);
|
|
102
|
+
const segments = [];
|
|
103
|
+
let offset = 0;
|
|
104
|
+
for (const reference of references) {
|
|
105
|
+
if (reference.start > offset) {
|
|
106
|
+
segments.push({ kind: "text", text: value.slice(offset, reference.start) });
|
|
107
|
+
}
|
|
108
|
+
const resolved = resolvedReference(reference, repositoryContext);
|
|
109
|
+
if (resolved) segments.push(resolved);
|
|
110
|
+
else {
|
|
111
|
+
segments.push({
|
|
112
|
+
kind: "ambiguous",
|
|
113
|
+
reason: `GitHub reference ${reference.text} is not linked because the repository is ambiguous.`,
|
|
114
|
+
text: reference.text,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
offset = reference.end;
|
|
118
|
+
}
|
|
119
|
+
if (offset < value.length) segments.push({ kind: "text", text: value.slice(offset) });
|
|
120
|
+
return segments;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function taskTexts(task) {
|
|
124
|
+
const texts = [task.instruction];
|
|
125
|
+
for (const turn of task.turns ?? []) {
|
|
126
|
+
texts.push(turn.requestSummary, turn.result?.summary);
|
|
127
|
+
}
|
|
128
|
+
if (!(task.turns?.length > 0)) {
|
|
129
|
+
for (const result of task.results ?? []) texts.push(result.summary);
|
|
130
|
+
texts.push(task.summary);
|
|
131
|
+
}
|
|
132
|
+
return texts.filter((text) => typeof text === "string" && text.length > 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function relatedLinkLabel(link, includeRepository, includeOwner) {
|
|
136
|
+
const reference = link.type === "pull"
|
|
137
|
+
? `PR #${link.number}`
|
|
138
|
+
: link.type === "issue"
|
|
139
|
+
? `Issue #${link.number}`
|
|
140
|
+
: `#${link.number}`;
|
|
141
|
+
if (!includeRepository) return reference;
|
|
142
|
+
const repository = includeOwner ? `${link.owner}/${link.repository}` : link.repository;
|
|
143
|
+
return `${repository} ${reference}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function taskGitHubProjection(task) {
|
|
147
|
+
const texts = taskTexts(task);
|
|
148
|
+
const explicitRepositories = new Set();
|
|
149
|
+
for (const text of texts) {
|
|
150
|
+
for (const reference of rawReferences(text)) {
|
|
151
|
+
if (reference.explicit) {
|
|
152
|
+
explicitRepositories.add(`${reference.owner}/${reference.repository}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const taskRepository = explicitRepositories.size === 1 ? [...explicitRepositories][0] : null;
|
|
157
|
+
const links = [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
let truncated = false;
|
|
160
|
+
sourceLoop:
|
|
161
|
+
for (const text of texts) {
|
|
162
|
+
for (const segment of githubReferenceSegments(text, {
|
|
163
|
+
projectRepositories: task.project?.githubRepos,
|
|
164
|
+
taskRepository,
|
|
165
|
+
})) {
|
|
166
|
+
if (segment.kind !== "link") continue;
|
|
167
|
+
const key = segment.url.toLowerCase();
|
|
168
|
+
if (seen.has(key)) continue;
|
|
169
|
+
if (links.length === MAX_RELATED_GITHUB_LINKS) {
|
|
170
|
+
truncated = true;
|
|
171
|
+
break sourceLoop;
|
|
172
|
+
}
|
|
173
|
+
seen.add(key);
|
|
174
|
+
links.push(segment);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const repositories = new Set(links.map(({ owner, repository }) => `${owner}/${repository}`));
|
|
178
|
+
const repositoryNames = new Map();
|
|
179
|
+
for (const link of links) {
|
|
180
|
+
const owners = repositoryNames.get(link.repository) ?? new Set();
|
|
181
|
+
owners.add(link.owner);
|
|
182
|
+
repositoryNames.set(link.repository, owners);
|
|
183
|
+
}
|
|
184
|
+
const includeRepository = repositories.size > 1;
|
|
185
|
+
return {
|
|
186
|
+
relatedGitHubLinks: links.map((link) => ({
|
|
187
|
+
label: relatedLinkLabel(link, includeRepository, repositoryNames.get(link.repository).size > 1),
|
|
188
|
+
number: link.number,
|
|
189
|
+
owner: link.owner,
|
|
190
|
+
repository: link.repository,
|
|
191
|
+
type: link.type,
|
|
192
|
+
url: link.url,
|
|
193
|
+
})),
|
|
194
|
+
relatedGitHubLinksTruncated: truncated,
|
|
195
|
+
relatedGitHubRepository: taskRepository,
|
|
196
|
+
};
|
|
197
|
+
}
|
package/src/dashboard/index.html
CHANGED
|
@@ -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>
|
package/src/dashboard/styles.css
CHANGED
|
@@ -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 } =
|
|
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/workspace.js
CHANGED
|
@@ -1581,6 +1581,148 @@ export async function buildTaskSummary(workspaceRoot) {
|
|
|
1581
1581
|
};
|
|
1582
1582
|
}
|
|
1583
1583
|
|
|
1584
|
+
const COPILOT_RECENT_TERMINAL_DAYS = 7;
|
|
1585
|
+
|
|
1586
|
+
function sortTasksByNewestUpdate(tasks) {
|
|
1587
|
+
return tasks
|
|
1588
|
+
.map((task, index) => ({ task, index }))
|
|
1589
|
+
.sort((left, right) => (
|
|
1590
|
+
Date.parse(right.task.updatedAt) - Date.parse(left.task.updatedAt)
|
|
1591
|
+
|| left.index - right.index
|
|
1592
|
+
))
|
|
1593
|
+
.map(({ task }) => task);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function taskBriefEntry(task) {
|
|
1597
|
+
const linkPending = task.threadId === null && task.status === "working";
|
|
1598
|
+
const creationFailure = task.threadId === null && task.status === "failed";
|
|
1599
|
+
const interruptedTurnCount = task.turns.filter(
|
|
1600
|
+
(turn) => turn.result?.status === "interrupted",
|
|
1601
|
+
).length;
|
|
1602
|
+
let attention = null;
|
|
1603
|
+
let nextAction;
|
|
1604
|
+
if (linkPending) {
|
|
1605
|
+
attention = {
|
|
1606
|
+
kind: "link_pending",
|
|
1607
|
+
reason: "The executor has not linked its durable Codex task identity yet.",
|
|
1608
|
+
};
|
|
1609
|
+
nextAction = {
|
|
1610
|
+
kind: "wait",
|
|
1611
|
+
instruction: "Wait for the existing executor to self-link; recover its exact identity before proposing any continuation.",
|
|
1612
|
+
requiresExplicitAuthorization: false,
|
|
1613
|
+
};
|
|
1614
|
+
} else if (task.status === "needs_input") {
|
|
1615
|
+
attention = {
|
|
1616
|
+
kind: "needs_input",
|
|
1617
|
+
reason: task.summary,
|
|
1618
|
+
};
|
|
1619
|
+
nextAction = {
|
|
1620
|
+
kind: "continue_existing_task",
|
|
1621
|
+
instruction: "Provide the missing decision or fact in the existing executor task.",
|
|
1622
|
+
requiresExplicitAuthorization: true,
|
|
1623
|
+
};
|
|
1624
|
+
} else if (task.status === "failed") {
|
|
1625
|
+
attention = {
|
|
1626
|
+
kind: creationFailure ? "creation_failed" : "failed",
|
|
1627
|
+
reason: task.summary,
|
|
1628
|
+
};
|
|
1629
|
+
nextAction = creationFailure ? {
|
|
1630
|
+
kind: "inspect_creation_failure",
|
|
1631
|
+
instruction: "Review the creation failure before deciding whether to dispatch new work.",
|
|
1632
|
+
requiresExplicitAuthorization: true,
|
|
1633
|
+
} : {
|
|
1634
|
+
kind: "continue_existing_task",
|
|
1635
|
+
instruction: "Review the failure and authorize a focused follow-up in the existing executor task if appropriate.",
|
|
1636
|
+
requiresExplicitAuthorization: true,
|
|
1637
|
+
};
|
|
1638
|
+
} else if (task.status === "working") {
|
|
1639
|
+
nextAction = {
|
|
1640
|
+
kind: "wait",
|
|
1641
|
+
instruction: "No action is recommended while the existing executor is working.",
|
|
1642
|
+
requiresExplicitAuthorization: false,
|
|
1643
|
+
};
|
|
1644
|
+
} else {
|
|
1645
|
+
nextAction = {
|
|
1646
|
+
kind: "none",
|
|
1647
|
+
instruction: "No follow-up is required unless the user has a new request.",
|
|
1648
|
+
requiresExplicitAuthorization: false,
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
return {
|
|
1652
|
+
id: task.id,
|
|
1653
|
+
title: task.title,
|
|
1654
|
+
project: {
|
|
1655
|
+
name: task.project.name,
|
|
1656
|
+
path: task.project.path,
|
|
1657
|
+
},
|
|
1658
|
+
threadId: task.threadId,
|
|
1659
|
+
state: task.status,
|
|
1660
|
+
summary: task.summary,
|
|
1661
|
+
lastOutcome: task.lastResult === null ? null : {
|
|
1662
|
+
status: task.lastResult.status,
|
|
1663
|
+
summary: task.lastResult.summary,
|
|
1664
|
+
turnId: task.lastResult.turnId,
|
|
1665
|
+
updatedAt: task.lastResult.updatedAt,
|
|
1666
|
+
},
|
|
1667
|
+
requestSummary: task.latestTurn?.requestSummary ?? null,
|
|
1668
|
+
updatedAt: task.updatedAt,
|
|
1669
|
+
attention,
|
|
1670
|
+
nextAction: {
|
|
1671
|
+
...nextAction,
|
|
1672
|
+
taskId: task.id,
|
|
1673
|
+
threadId: task.threadId,
|
|
1674
|
+
},
|
|
1675
|
+
liveCheck: {
|
|
1676
|
+
recommended: false,
|
|
1677
|
+
reason: null,
|
|
1678
|
+
},
|
|
1679
|
+
interruptedTurnCount,
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
export async function buildCopilotBrief(workspaceRoot, {
|
|
1684
|
+
project = null,
|
|
1685
|
+
taskId = null,
|
|
1686
|
+
includeOldTerminal = false,
|
|
1687
|
+
now = () => new Date().toISOString(),
|
|
1688
|
+
} = {}) {
|
|
1689
|
+
if (project !== null && taskId !== null) {
|
|
1690
|
+
throw new Error("copilot brief accepts either project or taskId, not both");
|
|
1691
|
+
}
|
|
1692
|
+
let tasks = project === null
|
|
1693
|
+
? await listTasks(workspaceRoot)
|
|
1694
|
+
: await filterTasks(workspaceRoot, { project });
|
|
1695
|
+
if (taskId !== null) {
|
|
1696
|
+
const id = requireSafeId(taskId, "taskId");
|
|
1697
|
+
tasks = tasks.filter((task) => task.id === id);
|
|
1698
|
+
if (tasks.length === 0) throw new Error(`task not found: ${id}`);
|
|
1699
|
+
}
|
|
1700
|
+
const generatedAt = requireTimestamp(now(), "copilot brief timestamp");
|
|
1701
|
+
const recentCutoff = Date.parse(generatedAt)
|
|
1702
|
+
- COPILOT_RECENT_TERMINAL_DAYS * 24 * 60 * 60 * 1_000;
|
|
1703
|
+
const focused = taskId !== null || project !== null;
|
|
1704
|
+
const selected = [];
|
|
1705
|
+
let omittedTerminalCount = 0;
|
|
1706
|
+
for (const task of sortTasksByNewestUpdate(tasks)) {
|
|
1707
|
+
const terminal = task.status === "completed" || task.status === "failed";
|
|
1708
|
+
const oldTerminal = terminal && Date.parse(task.updatedAt) < recentCutoff;
|
|
1709
|
+
if (!focused && !includeOldTerminal && oldTerminal) {
|
|
1710
|
+
omittedTerminalCount += 1;
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
selected.push(taskBriefEntry(task));
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
schemaVersion: 1,
|
|
1717
|
+
mode: "cached",
|
|
1718
|
+
scope: taskId !== null ? "task" : project !== null ? "project" : "overview",
|
|
1719
|
+
generatedAt,
|
|
1720
|
+
taskCount: selected.length,
|
|
1721
|
+
omittedTerminalCount,
|
|
1722
|
+
tasks: selected,
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1584
1726
|
export async function doctorWorkspace(workspaceRoot) {
|
|
1585
1727
|
const root = path.resolve(workspaceRoot);
|
|
1586
1728
|
const checks = [];
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: taskchef-report
|
|
3
|
-
description: "Report useful current state for Codex tasks recorded by TaskChef. Use only when the user asks for delegated-task status, outcomes, or a report. Prefer cached semantic results, filter old terminal tasks from overviews, use one cheap live metadata snapshot to override active or approval-waiting tasks, and reserve detailed reads for anomalies. Never poll or wait."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# TaskChef Report
|
|
7
|
-
|
|
8
|
-
Read the canonical per-user TaskChef task snapshots and report useful current
|
|
9
|
-
state once. A stored result is cached semantic evidence, not permanent truth.
|
|
10
|
-
|
|
11
|
-
Resolve this skill directory with `realpath`. The TaskChef plugin root is two
|
|
12
|
-
parents above the skill directory. Invoke `<plugin-root>/bin/taskchef.js` for
|
|
13
|
-
all deterministic task-log operations.
|
|
14
|
-
|
|
15
|
-
## Report
|
|
16
|
-
|
|
17
|
-
1. Run `<plugin-root>/bin/taskchef.js workspace path --json`. The CLI resolves
|
|
18
|
-
`--workspace`, then an absolute (or `~/`-prefixed) `TASKCHEF_WORKSPACE`, then
|
|
19
|
-
`~/.agents/taskchef`; never
|
|
20
|
-
infer the history from the current project. Select only the tasks the user
|
|
21
|
-
asked about:
|
|
22
|
-
- For an exact task ID, run
|
|
23
|
-
`<plugin-root>/bin/taskchef.js task show <task-id> --json`.
|
|
24
|
-
- For a project, run
|
|
25
|
-
`<plugin-root>/bin/taskchef.js task list --project <name-or-path> --json`.
|
|
26
|
-
- For a title or other description, run
|
|
27
|
-
`<plugin-root>/bin/taskchef.js task list --json`
|
|
28
|
-
once, then select matching entries. Ask the user if the match is ambiguous.
|
|
29
|
-
- Use the full list only when the user asks for an overview of the task history.
|
|
30
|
-
2. For an overview, select only attention-worthy candidates before detailed
|
|
31
|
-
reads:
|
|
32
|
-
- always include `working`, `needs_input`, and entries with a null
|
|
33
|
-
`threadId`;
|
|
34
|
-
- include `completed` and `failed` entries updated during the last seven
|
|
35
|
-
days;
|
|
36
|
-
- omit older terminal entries by default and report the omitted count;
|
|
37
|
-
- include any omitted task whose Codex metadata appears as active or awaiting
|
|
38
|
-
native approval in the recent-thread snapshot.
|
|
39
|
-
Explicit task, title, or project requests override this age filter.
|
|
40
|
-
3. Take one recent `list_threads` metadata snapshot for the whole report. This
|
|
41
|
-
is a cheap contradiction check across many tasks, not one call per task.
|
|
42
|
-
Match only exact durable `threadId` values. A metadata status that is active
|
|
43
|
-
or awaiting native approval immediately overrides a cached result without a
|
|
44
|
-
detailed read. Native approval is live Codex state, not a `needs_input`
|
|
45
|
-
callback. An inactive status never proves semantic completion; it only
|
|
46
|
-
permits a trustworthy cached MCP result to stand.
|
|
47
|
-
4. In schema 8, treat `turns` as the ordered request/result timeline. Each turn
|
|
48
|
-
pairs a bounded `requestSummary` with one semantic result, a TaskChef-generated
|
|
49
|
-
`interrupted` outcome, or null while work is in progress. Describe an
|
|
50
|
-
interrupted historical turn plainly as interrupted/abandoned, never failed.
|
|
51
|
-
`latestTurn` is the compact current pair.
|
|
52
|
-
`results` and `lastResult` remain derived compatibility projections; do not
|
|
53
|
-
expect interrupted outcomes in either projection and do not pair the latest
|
|
54
|
-
request with an earlier result. Treat a failed result with
|
|
55
|
-
null thread and turn IDs as a fresh executor-creation failure. Schema 4/5/6
|
|
56
|
-
snapshots normalize their semantic results into legacy turns with a null
|
|
57
|
-
request summary without rewriting their log line. Schema 7 timelines remain
|
|
58
|
-
readable but cannot contain the schema-8 interrupted outcome.
|
|
59
|
-
No live read is possible or needed for that creation failure. When identity is certain and
|
|
60
|
-
metadata says the thread is inactive, trust the latest semantic result by
|
|
61
|
-
default in a broad overview unless a newer working state makes it historical.
|
|
62
|
-
Do not read every idle terminal task in an overview merely because native
|
|
63
|
-
`updatedAt` is later: callbacks normally run before Codex finalizes the same
|
|
64
|
-
turn, and overview performance matters more than investigating every rare
|
|
65
|
-
missed callback.
|
|
66
|
-
|
|
67
|
-
For a focused task, title, or project report, perform at most one detailed
|
|
68
|
-
read for each selected inactive task when matched metadata `updatedAt` is
|
|
69
|
-
later than `lastResult.updatedAt`, by any amount. Read once as well
|
|
70
|
-
when there is no semantic callback, identity or metadata is uncertain or
|
|
71
|
-
contradictory, or the user explicitly requests a fully live result. If
|
|
72
|
-
focused metadata is not newer, trust the cache. Absence from the bounded
|
|
73
|
-
recent snapshot is not by itself a reason to read every cached terminal
|
|
74
|
-
overview entry. Batch immediate native reads with no more than eight targets
|
|
75
|
-
per call. When a detailed read occurs, compare the latest structured turn ID
|
|
76
|
-
and native turn state with `lastResult.turnId`: a newer turn without a callback
|
|
77
|
-
makes the cache stale, while an interrupted or cancelled callback turn
|
|
78
|
-
cannot prove completion. Never classify assistant prose.
|
|
79
|
-
5. Report each task as one of: working, needs input, awaiting native approval,
|
|
80
|
-
completed, failed, unresolved, or unknown. Show the cached summary when it
|
|
81
|
-
remains fresh. If a newer turn exists without a callback, describe the live
|
|
82
|
-
state and label the preserved result historical or stale rather than overwriting it.
|
|
83
|
-
6. Never edit `tasks.jsonl` directly during reporting. A null identity is
|
|
84
|
-
executor link-pending and must be retried by that executor. Never persist inferred status,
|
|
85
|
-
transcripts, prose classifications, or hidden reasoning. Do not poll or wait.
|
|
86
|
-
|
|
87
|
-
If the task history is empty, say that TaskChef has not recorded any tasks. If
|
|
88
|
-
a task has no durable thread ID and is not a stored creation failure, identify
|
|
89
|
-
it by task ID and say that its marker remains available for recovery. If live
|
|
90
|
-
metadata or a targeted read fails, use the cached result with an explicit
|
|
91
|
-
freshness warning and continue.
|