@xpufx/paseo-forges 0.1.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/LICENSE +21 -0
- package/README.md +109 -0
- package/client/active-forge.ts +47 -0
- package/client/board-alert.tsx +236 -0
- package/client/foreign-link.tsx +33 -0
- package/client/hook-queue-panel.tsx +412 -0
- package/client/issues-pill.tsx +1976 -0
- package/client/label-chip.tsx +116 -0
- package/client/linkifier.tsx +280 -0
- package/client/pill-label.ts +183 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/client/webhook-card.tsx +198 -0
- package/docs/specs/forge-workflow-gui.md +820 -0
- package/docs/workflow.md +339 -0
- package/examples/README.md +108 -0
- package/examples/hook-service/README.md +66 -0
- package/examples/hook-service/forge-hook.service +28 -0
- package/examples/hook-service/hook-server.mjs +265 -0
- package/examples/hook-service/hook.env.example +21 -0
- package/examples/labels/README.md +64 -0
- package/examples/labels/label-base.yaml +117 -0
- package/examples/skills/coding-agent/SKILL.md +262 -0
- package/examples/skills/coding-agent-fgjx/SKILL.md +271 -0
- package/examples/skills/orchestrator/SKILL.md +133 -0
- package/examples/skills/orchestrator-fgjx/SKILL.md +139 -0
- package/examples/tools/README.md +68 -0
- package/examples/tools/fgjx +464 -0
- package/package.json +38 -0
- package/paseo-plugin.json +4 -0
- package/server/forge-client.ts +484 -0
- package/server/forge-guard.ts +70 -0
- package/server/git-origin.ts +70 -0
- package/server/hook-queue.ts +127 -0
- package/server/issues.ts +542 -0
- package/server/settings.ts +52 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/hook-queue.ts +140 -0
- package/shared/issues.ts +1750 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- package/shared/version.ts +2 -0
- package/shared/webhook.ts +223 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
# Native Forge Workflow GUI Plugin for Paseo
|
|
2
|
+
|
|
3
|
+
**Status:** specification (implements [Issue #56 (forge.example.com)](https://forge.example.com/your-org/your-repo/issues/56));
|
|
4
|
+
the auth/data-path, label, and install sections below track the shipped
|
|
5
|
+
implementation as of the standalone-release pass for [Issue #121 (forge.example.com)](https://forge.example.com/your-org/your-repo/issues/121)
|
|
6
|
+
**Scope:** `plugins/forges` (`paseo-forges`) server + client, built only on
|
|
7
|
+
`paseo-plugin-helper` primitives — no Paseo host/SDK changes
|
|
8
|
+
**Auth model:** daemon-side token in plugin settings; all forge access goes
|
|
9
|
+
through the embedded TypeScript `fetch` client (`server/forge-client.ts`).
|
|
10
|
+
There is no `fgj`/`fgjx` subprocess dependency and no host dotfile coupling.
|
|
11
|
+
|
|
12
|
+
> Historical note: an earlier revision specified a `fgjx`/`fgj` subprocess
|
|
13
|
+
> data path. That design is superseded — see §3.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Problem statement
|
|
18
|
+
|
|
19
|
+
The operator (`@your-org`) and the Orchestrator must leave the Paseo
|
|
20
|
+
desktop/mobile client and open an external web browser to inspect issues,
|
|
21
|
+
review agent deliverables, advance label state, or cross-reference agent IDs
|
|
22
|
+
and commit hashes. Every context switch breaks the orchestration loop:
|
|
23
|
+
triage happens in the browser, steering happens in chat, and neither side sees
|
|
24
|
+
the other.
|
|
25
|
+
|
|
26
|
+
A native GUI plugin keeps the whole loop inside Paseo: the issue queue, the
|
|
27
|
+
issue detail (with parsed Agent Envelope telemetry), one-click scoped label
|
|
28
|
+
toggles, and quick comments all live in plugin surfaces powered by
|
|
29
|
+
`paseo-plugin-helper`.
|
|
30
|
+
|
|
31
|
+
### What already exists (reuse, do not duplicate)
|
|
32
|
+
|
|
33
|
+
`plugins/forges` (`paseo-forges`) already ships the thin end of this wedge:
|
|
34
|
+
|
|
35
|
+
- Server: `forge.open-issues` contract (`shared/issues.ts`) + `handleOpenIssues`
|
|
36
|
+
(`server/issues.ts`) — resolves owner/repo from the workspace directory's
|
|
37
|
+
git `origin` remote, calls the embedded forge (Gitea-family) fetch client, and never
|
|
38
|
+
throws (failures surface as an `error` field so the pill renders a
|
|
39
|
+
placeholder).
|
|
40
|
+
- Shared: `parseForgeRemote`, `extractForgeIssueUrls` (timeline
|
|
41
|
+
linkifier), `formatIssueCountLabel` (null count renders `"issues --"`
|
|
42
|
+
placeholder, never a false zero).
|
|
43
|
+
- Client: composer pill (`GitPullRequest` icon, 15 s label poll / 30 s query
|
|
44
|
+
poll) + issues modal (`Tabs`, `SearchInput`, `Card`, `EmptyState`, copy the
|
|
45
|
+
`[#N title](url)` markdown ref — push-to-composer is unavailable on the 0.8
|
|
46
|
+
SDK, so copy-and-paste remains the handoff).
|
|
47
|
+
|
|
48
|
+
This spec extends that plugin with four new RPC contracts and three new UI
|
|
49
|
+
surfaces. All shared parsing helpers (`parseForgeRemote`,
|
|
50
|
+
`extractForgeIssueUrls`) are reused as-is.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. Goals / non-goals
|
|
55
|
+
|
|
56
|
+
### Goals
|
|
57
|
+
|
|
58
|
+
1. **Board overview in-client.** Searchable queue of open issues sorted by the
|
|
59
|
+
deterministic priority tuple (§4.3), with quick-filters for `state/*` and
|
|
60
|
+
`priority/*` buckets — no browser required for triage.
|
|
61
|
+
2. **Issue detail in-client.** Full markdown body, comment thread, and parsed
|
|
62
|
+
Agent Envelope cards (agent id, provider/model, branch, commit SHAs,
|
|
63
|
+
`paseo://` session link) in one inspection modal.
|
|
64
|
+
3. **One-click label state toggles.** Scoped `state/`–`priority/`–`attention/`–`spec/`
|
|
65
|
+
selectors that rely on Gitea-family native exclusive auto-eviction (apply the new
|
|
66
|
+
label; the old one in the same scope evicts itself — zero `--remove-label`
|
|
67
|
+
calls).
|
|
68
|
+
4. **Quick comments from the client.** Operator steering posted straight into
|
|
69
|
+
the issue thread without leaving Paseo.
|
|
70
|
+
5. **Token-in-settings auth.** The daemon stores a per-host API token in
|
|
71
|
+
plugin settings; the client never sees it and no host CLI/dotfile is
|
|
72
|
+
required.
|
|
73
|
+
6. **Deep links.** Agent IDs resolve to `paseo://h/<serverId>/agent/<agentId>`
|
|
74
|
+
sessions; commit SHAs resolve to local `git log` / web viewer.
|
|
75
|
+
|
|
76
|
+
### Non-goals
|
|
77
|
+
|
|
78
|
+
- No new Paseo SDK surface (`initClientHelpers` four-field shape unchanged;
|
|
79
|
+
`registerComposerPill` / `registerSidebarSurface` used as documented).
|
|
80
|
+
- No push-to-composer (0.8 SDK exposes no composer-insert API — same
|
|
81
|
+
limitation the current issues modal already documents).
|
|
82
|
+
- No actionable toasts (see the
|
|
83
|
+
[toast-to-approval spec](./toast-to-approval.md) for the split-surface
|
|
84
|
+
approval pattern if a signoff flow is needed later).
|
|
85
|
+
- No plugin-specific logic in `paseo-plugin-helper` — any generally reusable
|
|
86
|
+
parsing (envelope regex, priority-rank comparator) ships in the helper only
|
|
87
|
+
if a second consumer needs it; until then it lives in `plugins/forges`.
|
|
88
|
+
- No implementation in this spec phase — schemas, method names, and layouts
|
|
89
|
+
only.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 3. Authentication & data path
|
|
94
|
+
|
|
95
|
+
### 3.1 Token source (daemon-side plugin settings)
|
|
96
|
+
|
|
97
|
+
All forge access goes through the embedded TypeScript client
|
|
98
|
+
(`server/forge-client.ts`), which speaks the Gitea-family `/api/v1` over
|
|
99
|
+
`fetch` with a 15 s abort timeout. There is no subprocess and no host
|
|
100
|
+
CLI/dotfile dependency: the plugin works on a machine that has never had
|
|
101
|
+
`fgj`/`fgjx` or `~/.config/fgj`.
|
|
102
|
+
|
|
103
|
+
The token lives in daemon-side plugin settings, keyed by host
|
|
104
|
+
(`ForgeSettings.tokensByHost`), and is read by `tokenForHost(host)` in
|
|
105
|
+
`server/settings.ts`:
|
|
106
|
+
|
|
107
|
+
1. Resolve repo coordinates without shelling: read `<directory>/.git/config`
|
|
108
|
+
directly and parse the `origin` URL with `parseForgeRemote` (handles
|
|
109
|
+
`git@host:owner/repo`, `https://host/owner/repo`, `ssh://git@host/...`);
|
|
110
|
+
an explicit per-workspace forge target wins absolutely (issue #109/#137).
|
|
111
|
+
2. Construct `new ForgeClient({ host, token: await tokenForHost(host) })`.
|
|
112
|
+
The token is attached only as an `Authorization: token <t>` header on
|
|
113
|
+
`fetch` calls that run in the daemon; it is never serialized into an RPC
|
|
114
|
+
payload and never reaches the client.
|
|
115
|
+
3. Endpoints: `GET /repos/{owner}/{repo}/issues` (list),
|
|
116
|
+
`GET /repos/{owner}/{repo}/issues/{n}` + `/comments` (detail/labels),
|
|
117
|
+
`PATCH /repos/{owner}/{repo}/issues/{n}` (label writes), and
|
|
118
|
+
`POST /repos/{owner}/{repo}/issues/{n}/comments` (comments). Repo labels
|
|
119
|
+
(`GET`/`POST/DELETE /repos/{owner}/{repo}/labels`) back the optional label
|
|
120
|
+
install (§5.5).
|
|
121
|
+
4. Handlers never throw: failures return a typed `error` field and the client
|
|
122
|
+
renders `EmptyState` + Retry (per `docs/surfaces.md` — data absent with a
|
|
123
|
+
live source renders the empty state, never a crash). Unauthenticated public
|
|
124
|
+
repos stay readable; writes require an accepted, write-scoped token on both
|
|
125
|
+
public and private repos (issues #152, #193). Edit capability is derived
|
|
126
|
+
from the repo response's permission object (`forgeCapabilityFromRepo`:
|
|
127
|
+
Forgejo/GitHub `permissions.push`/`admin`, GitLab `access_level >= 30`),
|
|
128
|
+
with bare token validity as the fallback when a host returns none. The
|
|
129
|
+
minimum Forgejo/Gitea scopes are `read:user`, `read:repository`, and
|
|
130
|
+
`write:issue`; a valid token without write scope surfaces as
|
|
131
|
+
**"token lacks write scope"** (`auth: "lacks-write-scope"`), never as
|
|
132
|
+
edits-enabled.
|
|
133
|
+
|
|
134
|
+
Why embedded fetch over a subprocess: the plugin needs no preinstalled CLI or
|
|
135
|
+
credential store, auth is explicit and daemon-scoped, and the API surface is
|
|
136
|
+
versioned with the plugin instead of tracking a host binary.
|
|
137
|
+
|
|
138
|
+
### 3.2 Caching & polling
|
|
139
|
+
|
|
140
|
+
- There is no daemon-side board cache: reads are served live from the API.
|
|
141
|
+
- Client-side: `useRpcQuery` for reads with a 30 s `refetchInterval` (the
|
|
142
|
+
issues list), plus a manual Refresh; the settings form queries
|
|
143
|
+
`forge.forge-context` independently so the token/host fields render while
|
|
144
|
+
issues load or fail (regression #152). Writes go through `useRpcMutation`
|
|
145
|
+
and refetch the board/detail on success.
|
|
146
|
+
- Handlers stay cheap because each query is one or two API calls; a failed
|
|
147
|
+
list is logged once per host/repo, then demoted to debug to avoid log spam.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 4. Data models
|
|
152
|
+
|
|
153
|
+
All schemas are Zod, defined in `plugins/forges/shared/` (importable by both
|
|
154
|
+
server and client), built with `defineContract` from
|
|
155
|
+
`paseo-plugin-helper/shared`.
|
|
156
|
+
|
|
157
|
+
### 4.1 Scoped label vocabularies (fallback + display data)
|
|
158
|
+
|
|
159
|
+
Gitea-family scoped labels are exclusive: applying one label in a scope evicts the
|
|
160
|
+
previous label in that scope at the DB level. The board is the source of truth
|
|
161
|
+
for which scopes exist (decision #121.2): the client derives live scopes from
|
|
162
|
+
the labels actually present on the returned issues (`liveScopesFromIssues`),
|
|
163
|
+
and the canonical lists below are used only as fallback vocabulary and for
|
|
164
|
+
display aliases. On a write, `handleSetLabel` sends the new label plus an
|
|
165
|
+
explicit removal of any existing label in the same scope, so the result is
|
|
166
|
+
correct even for boards whose scope names differ from ours. Canonical scopes
|
|
167
|
+
and ranks:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
export const StateRank = {
|
|
171
|
+
"state/0-triage": 0,
|
|
172
|
+
"state/1-wip": 1,
|
|
173
|
+
"state/2-review": 2,
|
|
174
|
+
"state/3-verify": 3,
|
|
175
|
+
"state/4-done": 4,
|
|
176
|
+
} as const;
|
|
177
|
+
|
|
178
|
+
export const PriorityRank = {
|
|
179
|
+
"priority/0-SOS": 0,
|
|
180
|
+
"priority/1-high": 1,
|
|
181
|
+
"priority/2-normal": 2,
|
|
182
|
+
"priority/3-low": 3,
|
|
183
|
+
"priority/4-backburner": 4,
|
|
184
|
+
} as const;
|
|
185
|
+
|
|
186
|
+
export const AttentionScope = [
|
|
187
|
+
"attention/0-orchestrator",
|
|
188
|
+
"attention/1-agent",
|
|
189
|
+
"attention/2-user",
|
|
190
|
+
"attention/3-ignore",
|
|
191
|
+
] as const;
|
|
192
|
+
|
|
193
|
+
export const SpecScope = [
|
|
194
|
+
"spec/0-needed",
|
|
195
|
+
"spec/1-checklist",
|
|
196
|
+
"spec/2-approved",
|
|
197
|
+
] as const;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
> [!NOTE]
|
|
201
|
+
> Label-name discrepancy: the issue body §2 cites shorthand names
|
|
202
|
+
> (`state/wip`, `state/ready-for-review`, `state/verify`,
|
|
203
|
+
> `state/confirmed-done`, `attention/0-agent`, `attention/1-user`). The live
|
|
204
|
+
> board (verified via API at spec time) uses the numbered forms above
|
|
205
|
+
> (`state/1-wip`, `state/2-review`, `state/3-verify`, `state/4-done`;
|
|
206
|
+
> `attention/0-orchestrator` … `attention/3-ignore`). This spec norms on the
|
|
207
|
+
> live names; the client renders short display aliases (`WIP`, `Review`,
|
|
208
|
+
> `Verify`, `Done`) so the UI stays compact.
|
|
209
|
+
|
|
210
|
+
### 4.2 Board item
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
export const BoardIssueSchema = z.object({
|
|
214
|
+
number: z.number(),
|
|
215
|
+
title: z.string(),
|
|
216
|
+
state: z.string(),
|
|
217
|
+
labels: z.array(z.string()),
|
|
218
|
+
priorityRank: z.number().int().min(0).max(4),
|
|
219
|
+
stateRank: z.number().int().min(0).max(4).nullable(),
|
|
220
|
+
attention: z.string().nullable().default(null),
|
|
221
|
+
updatedAt: z.string().optional(),
|
|
222
|
+
commentCount: z.number().int().nonnegative().default(0),
|
|
223
|
+
});
|
|
224
|
+
export type BoardIssue = z.infer<typeof BoardIssueSchema>;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- Issues carrying **no** `priority/*` label sort as `priority/2-normal`
|
|
228
|
+
(rank 2); issues with no `state/*` label carry `stateRank: null` and sort
|
|
229
|
+
after ranked states within the same priority band.
|
|
230
|
+
- Closed issues are excluded from the overview (the `state/4-done` filter
|
|
231
|
+
shows open issues labeled done; true closed state is a separate query
|
|
232
|
+
flag — see `includeClosed` in §5.1).
|
|
233
|
+
|
|
234
|
+
### 4.3 Sort tuple
|
|
235
|
+
|
|
236
|
+
The overview is sorted by the deterministic tuple
|
|
237
|
+
`(priorityRank, stateRank ?? 99, updatedAt desc)`:
|
|
238
|
+
|
|
239
|
+
1. `priorityRank` ascending (`0-SOS` preempts everything).
|
|
240
|
+
2. `stateRank` ascending within a band — `3-verify` (needs a human) surfaces
|
|
241
|
+
above `1-wip` (already owned), so operator attention lands where it
|
|
242
|
+
unblocks work. `null` ranks last.
|
|
243
|
+
3. `updatedAt` descending (most recently active first) as the final tiebreak.
|
|
244
|
+
|
|
245
|
+
`size/*` (`0-cheap` … `3-chunk`) and `dep/blocker` are exposed on the card as
|
|
246
|
+
badges but do **not** enter the sort key in v1 (open question §10.3).
|
|
247
|
+
|
|
248
|
+
### 4.4 Agent Envelope (parsed telemetry)
|
|
249
|
+
|
|
250
|
+
Every agent comment ends with the envelope footer stamped by the team's
|
|
251
|
+
issue-comment tool (see the coding-agent example skill):
|
|
252
|
+
|
|
253
|
+
```markdown
|
|
254
|
+
---
|
|
255
|
+
<sub>🤖 **<SessionTitle>** (`<shortId>`) · `<model>` · `<repo>:<branch>` · _<UTC timestamp>_</sub>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Verified live example (`#77`):
|
|
259
|
+
|
|
260
|
+
```markdown
|
|
261
|
+
<sub>🤖 **Update legacy react-native specifier to client** (`d705b95`) · `muse-spark-1.3-contributor` · `paseo:main` · _2026-09-12 12:13 UTC_</sub>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Parsed schema (server-side, pure function, unit-tested):
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
export const AgentEnvelopeSchema = z.object({
|
|
268
|
+
commentId: z.number(),
|
|
269
|
+
sessionTitle: z.string(),
|
|
270
|
+
agentShortId: z.string(),
|
|
271
|
+
model: z.string().nullable().default(null),
|
|
272
|
+
repo: z.string().nullable().default(null),
|
|
273
|
+
branch: z.string().nullable().default(null),
|
|
274
|
+
postedAt: z.string().nullable().default(null),
|
|
275
|
+
commitShas: z.array(z.string().regex(/^[0-9a-f]{7,40}$/)).default([]),
|
|
276
|
+
paseoLinks: z.array(z.string().url().or(z.string().startsWith("paseo://"))).default([]),
|
|
277
|
+
serverId: z.string().nullable().default(null),
|
|
278
|
+
});
|
|
279
|
+
export type AgentEnvelope = z.infer<typeof AgentEnvelopeSchema>;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
- `commitShas`: full 40-char and short 7+ char hex hashes found in the comment
|
|
283
|
+
body (code-fenced blocks included — agents report SHAs in both prose and
|
|
284
|
+
`commit:` lines).
|
|
285
|
+
- `paseoLinks`: `paseo://h/<serverId>/agent/<agentId>` deep links found in
|
|
286
|
+
the body; `serverId` is extracted from the first one when present.
|
|
287
|
+
- Unparseable footers are skipped (the comment still renders as plain
|
|
288
|
+
markdown); envelope parsing never fails the detail RPC.
|
|
289
|
+
|
|
290
|
+
### 4.5 Issue detail
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
export const IssueCommentSchema = z.object({
|
|
294
|
+
id: z.number(),
|
|
295
|
+
author: z.string(),
|
|
296
|
+
createdAt: z.string(),
|
|
297
|
+
updatedAt: z.string(),
|
|
298
|
+
body: z.string(),
|
|
299
|
+
envelope: AgentEnvelopeSchema.nullable().default(null),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
export const IssueDetailSchema = z.object({
|
|
303
|
+
number: z.number(),
|
|
304
|
+
title: z.string(),
|
|
305
|
+
state: z.string(),
|
|
306
|
+
labels: z.array(z.string()),
|
|
307
|
+
body: z.string(),
|
|
308
|
+
author: z.string(),
|
|
309
|
+
createdAt: z.string(),
|
|
310
|
+
updatedAt: z.string(),
|
|
311
|
+
webUrl: z.string().url(),
|
|
312
|
+
comments: z.array(IssueCommentSchema),
|
|
313
|
+
envelopes: z.array(AgentEnvelopeSchema),
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 5. Server RPC interface
|
|
320
|
+
|
|
321
|
+
Four contracts, namespaced `forge.*`, registered on the daemon
|
|
322
|
+
`PluginContext` next to the existing `forge.open-issues` handler (which
|
|
323
|
+
stays untouched for backward compatibility — the pill keeps working during
|
|
324
|
+
migration).
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { z } from "zod";
|
|
328
|
+
import { defineContract } from "paseo-plugin-helper/shared";
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### 5.1 `forge.board-overview` (read)
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
export const boardOverviewContract = defineContract({
|
|
335
|
+
name: "forge.board-overview",
|
|
336
|
+
description: "Open issues sorted by the (priority, state, recency) tuple",
|
|
337
|
+
input: z.object({
|
|
338
|
+
directory: z.string().optional(),
|
|
339
|
+
stateFilter: z.enum(["all", "state/0-triage", "state/1-wip",
|
|
340
|
+
"state/2-review", "state/3-verify", "state/4-done"]).default("all"),
|
|
341
|
+
priorityFilter: z.enum(["all", "priority/0-SOS", "priority/1-high",
|
|
342
|
+
"priority/2-normal", "priority/3-low",
|
|
343
|
+
"priority/4-backburner"]).default("all"),
|
|
344
|
+
query: z.string().max(200).default(""),
|
|
345
|
+
includeClosed: z.boolean().default(false),
|
|
346
|
+
limit: z.number().int().min(1).max(200).default(100),
|
|
347
|
+
}),
|
|
348
|
+
output: z.object({
|
|
349
|
+
repo: z.string().nullable(),
|
|
350
|
+
issues: z.array(BoardIssueSchema),
|
|
351
|
+
verifyCount: z.number().int().nonnegative(),
|
|
352
|
+
wipCount: z.number().int().nonnegative(),
|
|
353
|
+
fetchedAt: z.string().datetime(),
|
|
354
|
+
error: z.string().optional(),
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
- Filtering is server-side (single fetch, cheap in-memory partition) so the
|
|
360
|
+
pill's `verifyCount` and the dashboard always agree — same predicates, same
|
|
361
|
+
data (per `docs/surfaces.md`).
|
|
362
|
+
- `query` matches issue number (`"56"` / `"#56"`), title substring, or label
|
|
363
|
+
substring (case-insensitive) — the same matcher the current modal uses
|
|
364
|
+
client-side, moved server-side so sidebar and modal share it.
|
|
365
|
+
- `verifyCount` = open issues with `state/3-verify`; `wipCount` = open issues
|
|
366
|
+
with `state/1-wip`. Returned on every call so the pill never needs a second
|
|
367
|
+
round-trip.
|
|
368
|
+
|
|
369
|
+
### 5.2 `forge.issue-detail` (read)
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
export const issueDetailContract = defineContract({
|
|
373
|
+
name: "forge.issue-detail",
|
|
374
|
+
description: "Full body, comments, and parsed Agent Envelopes for one issue",
|
|
375
|
+
input: z.object({
|
|
376
|
+
directory: z.string().optional(),
|
|
377
|
+
number: z.number().int().positive(),
|
|
378
|
+
}),
|
|
379
|
+
output: z.object({
|
|
380
|
+
repo: z.string().nullable(),
|
|
381
|
+
issue: IssueDetailSchema.nullable(),
|
|
382
|
+
fetchedAt: z.string().datetime(),
|
|
383
|
+
error: z.string().optional(),
|
|
384
|
+
}),
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- Unknown number → `{ issue: null, error: "Issue #N not found in <repo>" }`
|
|
389
|
+
(typed null, never a throw — the modal renders `EmptyState`).
|
|
390
|
+
- Envelopes are parsed server-side (Node regex) so the client receives
|
|
391
|
+
structured cards with zero parsing logic.
|
|
392
|
+
|
|
393
|
+
### 5.3 `forge.set-label` (write)
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
export const setLabelContract = defineContract({
|
|
397
|
+
name: "forge.set-label",
|
|
398
|
+
description: "Apply one scoped label; an exclusive scope evicts the rest",
|
|
399
|
+
input: z.object({
|
|
400
|
+
directory: z.string().optional(),
|
|
401
|
+
number: z.number().int().positive(),
|
|
402
|
+
label: z.string().min(1),
|
|
403
|
+
}),
|
|
404
|
+
output: z.object({
|
|
405
|
+
number: z.number(),
|
|
406
|
+
labels: z.array(z.string()),
|
|
407
|
+
error: z.string().optional(),
|
|
408
|
+
}),
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
- Implemented by fetching the issue, computing the current label list, and
|
|
413
|
+
`PATCH`ing the issue with the new label added plus any same-scope mate
|
|
414
|
+
removed (the embedded client's `setLabels`). Gitea-family exclusivity alone would
|
|
415
|
+
also evict a scope mate, but the explicit remove keeps the write correct on
|
|
416
|
+
boards with non-canonical scope names.
|
|
417
|
+
- `label` is validated against the live board scopes (fallback: the known
|
|
418
|
+
vocabularies in §4.1) before any API call; unknown scopes return `{ error }`
|
|
419
|
+
without touching the API.
|
|
420
|
+
- On success returns the fresh label list and refetches the board/detail
|
|
421
|
+
client-side.
|
|
422
|
+
- Input accepts `issueNumber` (primary) with `number` as a deprecated alias;
|
|
423
|
+
handlers normalize via `normalizeIssueNumber`.
|
|
424
|
+
|
|
425
|
+
### 5.4 `forge.add-comment` (write)
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
export const addCommentContract = defineContract({
|
|
429
|
+
name: "forge.add-comment",
|
|
430
|
+
description: "Post a quick comment (or steering note) to the issue thread",
|
|
431
|
+
input: z.object({
|
|
432
|
+
directory: z.string().optional(),
|
|
433
|
+
number: z.number().int().positive(),
|
|
434
|
+
body: z.string().min(1).max(10000),
|
|
435
|
+
}),
|
|
436
|
+
output: z.object({
|
|
437
|
+
number: z.number(),
|
|
438
|
+
commentId: z.number().nullable(),
|
|
439
|
+
error: z.string().optional(),
|
|
440
|
+
}),
|
|
441
|
+
});
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
- Implemented with `POST /repos/{owner}/{repo}/issues/{n}/comments` via the
|
|
445
|
+
embedded client (`addComment`). The plugin does **not** append an agent
|
|
446
|
+
envelope — quick comments are operator steering, stamped with the sender's
|
|
447
|
+
identity by the API.
|
|
448
|
+
- Empty/whitespace-only bodies are rejected client-side (button disabled) and
|
|
449
|
+
server-side (`min(1)` after trim).
|
|
450
|
+
- Input accepts `issueNumber` (primary) with `number` as a deprecated alias.
|
|
451
|
+
|
|
452
|
+
### 5.5 `forge.install-labels` (operator-only, not shipped)
|
|
453
|
+
|
|
454
|
+
> Operator-gated, not registered: the optional label-set install is kept in the
|
|
455
|
+
> codebase for our own board but excluded from the release surface (issue #163).
|
|
456
|
+
> `index.server.ts` deliberately omits `handleInstallLabels`, so the RPC is not
|
|
457
|
+
> reachable by end users, and the Settings card does not render. The contract,
|
|
458
|
+
> handler, and planner below remain as the internals.
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
export const installLabelsContract = defineContract({
|
|
462
|
+
name: "forge.install-labels",
|
|
463
|
+
description: "Copy the Paseo label taxonomy onto the configured forge repo after an explicit user choice",
|
|
464
|
+
input: z.object({
|
|
465
|
+
directory: z.string().optional(),
|
|
466
|
+
remoteUrl: z.string().optional(),
|
|
467
|
+
mode: z.enum(["merge", "replace"]),
|
|
468
|
+
}),
|
|
469
|
+
output: z.object({
|
|
470
|
+
host: z.string().nullable(),
|
|
471
|
+
repo: z.string().nullable(),
|
|
472
|
+
mode: z.enum(["merge", "replace"]).nullable(),
|
|
473
|
+
created: z.array(z.string()),
|
|
474
|
+
skipped: z.array(z.string()),
|
|
475
|
+
removed: z.array(z.string()),
|
|
476
|
+
error: z.string().optional(),
|
|
477
|
+
}),
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
- Keeps our taxonomy as installable data (`paseoLabelSet()`): `state/`,
|
|
482
|
+
`priority/`, `attention/`, `spec/`. The install is **never automatic** — the
|
|
483
|
+
pre-gate client required an explicit action plus a keep/replace choice, and
|
|
484
|
+
the server only writes after resolving an explicit forge target.
|
|
485
|
+
- `mode` is required with no default so the choice can never be implicit.
|
|
486
|
+
`merge` only creates missing labels. `replace` additionally deletes the
|
|
487
|
+
target's labels that share a scope with our set but are not in it (e.g. a
|
|
488
|
+
foreign `state/ready-for-review`); labels in other scopes and unscoped
|
|
489
|
+
labels are never touched. See `planLabelSetInstall` in `shared/issues.ts`.
|
|
490
|
+
- Requires a saved daemon-side token; a missing token fails closed
|
|
491
|
+
(`{ error }`) without any write. Results are reported as created/removed/
|
|
492
|
+
skipped name lists.
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## 6. Client UI surfaces
|
|
497
|
+
|
|
498
|
+
All components use only documented helper primitives. Entry point calls
|
|
499
|
+
`initClientHelpers({ Icon, Modal, useRpc, useToast })` once (unchanged).
|
|
500
|
+
|
|
501
|
+
### 6.1 Status count pill (composer trackbar)
|
|
502
|
+
|
|
503
|
+
Extends the existing `forges-issues` pill; no second pill:
|
|
504
|
+
|
|
505
|
+
- Wide label: `"3 verify · 12 open"` (verify-first — the operator's most
|
|
506
|
+
valuable glance). Compact label: `"3v"`.
|
|
507
|
+
- Data: `useRpcQuery(boardOverviewContract, { directory })`, reusing the
|
|
508
|
+
existing 15 s label poll / 30 s query poll cadence.
|
|
509
|
+
- Visibility rule (per `docs/surfaces.md`): `verifyCount > 0` → label as
|
|
510
|
+
above; zero verify but live source → `"12 open"`; source dead/unknown →
|
|
511
|
+
existing `"issues --"` placeholder. Never a `0 verify` chip.
|
|
512
|
+
- Tap opens the Queue Dashboard modal (§6.2) pre-filtered to
|
|
513
|
+
`state/3-verify` when `verifyCount > 0`, else unfiltered.
|
|
514
|
+
|
|
515
|
+
### 6.2 Queue Dashboard (modal + sidebar)
|
|
516
|
+
|
|
517
|
+
Registered **twice** from one shared component tree:
|
|
518
|
+
|
|
519
|
+
- Modal: `registerComposerPill(..., { renderModal: (props) =>
|
|
520
|
+
<ForgeBoardModal {...props} /> })` — replaces the current
|
|
521
|
+
`ForgeIssuesModal` list tab, keeping `SearchInput` + copy-ref behavior.
|
|
522
|
+
- Sidebar: `registerSidebarSurface(plugin, { id: "forge-board",
|
|
523
|
+
title: "Board", icon: "KanbanSquare", Component: ForgeBoardSurface })` —
|
|
524
|
+
full-height surface for sustained triage (desktop split-pane friendly).
|
|
525
|
+
|
|
526
|
+
Layout (shared `<BoardView>` used by both, responsive via `useResponsive()`):
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
┌──────────────────────────────────────────────┐
|
|
530
|
+
│ Card.Header: "Board · owner/repo" │
|
|
531
|
+
│ badge: "3 verify" (warning dot) / hidden │
|
|
532
|
+
├──────────────────────────────────────────────┤
|
|
533
|
+
│ Tabs: [Queue] [Verify n] [WIP n] [Search] │
|
|
534
|
+
│ shortLabels: Queue/Verify/WIP/Search │
|
|
535
|
+
├──────────────────────────────────────────────┤
|
|
536
|
+
│ (Search tab) SearchInput "Filter #number…" │
|
|
537
|
+
│ Priority filter row: [SOS][High][Normal]… │
|
|
538
|
+
│ (ghost Buttons, single-select, All reset) │
|
|
539
|
+
├──────────────────────────────────────────────┤
|
|
540
|
+
│ DataTable (desktop) / card list (compact): │
|
|
541
|
+
│ #N │ Title + label Badges │ 💬n │ updated │
|
|
542
|
+
│ via <Responsive desktop card-list> │
|
|
543
|
+
├──────────────────────────────────────────────┤
|
|
544
|
+
│ ActionBar: [Refresh secondary] [Close ghost] │
|
|
545
|
+
│ (modal only; sidebar omits Close) │
|
|
546
|
+
└──────────────────────────────────────────────┘
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
- Rows: `DataTable` with `keyExtractor={(i) => String(i.number)}` on desktop;
|
|
550
|
+
automatic card-list reflow on compact (built into `DataTable`). Row tap →
|
|
551
|
+
opens Issue Inspection (§6.3) with `{ number }` payload. Copy-ref button
|
|
552
|
+
per row (existing `issueMarkdownRef` behavior preserved).
|
|
553
|
+
- State/priority badges: `Badge` per label (`priority/0-SOS` → `danger`,
|
|
554
|
+
`state/3-verify` → `warning`, `state/1-wip` → `info`, rest `neutral`).
|
|
555
|
+
- Filters are `Tabs` (state scope) + single-select ghost `Button` row
|
|
556
|
+
(priority scope) + `SearchInput` (query) — all three map 1:1 onto the
|
|
557
|
+
`board-overview` input, so sidebar and modal can never disagree.
|
|
558
|
+
- Empty states: `EmptyState` (`"CheckCircle2"` / `"No open issues"`;
|
|
559
|
+
`"Search"` / `"No matches"` with query echo) — same copy as today.
|
|
560
|
+
|
|
561
|
+
### 6.3 Issue Inspection modal
|
|
562
|
+
|
|
563
|
+
`renderModal` payload `{ number }` (from board row tap or pill shortcut).
|
|
564
|
+
`useAutoRefreshQuery(issueDetailContract, { directory, number },
|
|
565
|
+
{ defaultRate: "30s", isOpen })`.
|
|
566
|
+
|
|
567
|
+
```
|
|
568
|
+
┌──────────────────────────────────────────────┐
|
|
569
|
+
│ Card.Header: "#56 Title…" (truncate helper) │
|
|
570
|
+
│ badges: state Badge + priority Badge │
|
|
571
|
+
├──────────────────────────────────────────────┤
|
|
572
|
+
│ Tabs: [Overview] [Comments n] [Envelopes m] │
|
|
573
|
+
│ [Labels] │
|
|
574
|
+
├─ Overview ───────────────────────────────────┤
|
|
575
|
+
│ KeyValueGroup(2): Author │ Updated │ State… │
|
|
576
|
+
│ Markdown body (RN text; fenced blocks via │
|
|
577
|
+
│ CodeBlock w/ copy; issue URLs linkified │
|
|
578
|
+
│ via extractForgeIssueUrls) │
|
|
579
|
+
│ Envelopes preview: latest AgentEnvelopeCard │
|
|
580
|
+
├─ Comments ───────────────────────────────────┤
|
|
581
|
+
│ Comment cards (author, timestamp, body) │
|
|
582
|
+
│ Quick-comment composer: TextInput (multiline)│
|
|
583
|
+
│ + Button "Post" (useRpcMutation add- │
|
|
584
|
+
│ comment; disabled when empty/pending) │
|
|
585
|
+
├─ Envelopes ──────────────────────────────────┤
|
|
586
|
+
│ AgentEnvelopeCard × m: │
|
|
587
|
+
│ Card + Card.Header (session title, agent │
|
|
588
|
+
│ ShortId Badge mono copyable, model │
|
|
589
|
+
│ Badge) │
|
|
590
|
+
│ KeyValue: Branch (copyable mono) │ Posted │
|
|
591
|
+
│ Commit SHAs: one CommandBox per SHA │
|
|
592
|
+
│ (copy button; tap → §7.2) │
|
|
593
|
+
│ paseo:// link Button "Open agent session" │
|
|
594
|
+
│ (ghost w/ ExternalLink icon; tap → §7.1) │
|
|
595
|
+
│ EmptyState "Cpu"/"No agent activity yet" │
|
|
596
|
+
│ when m = 0 │
|
|
597
|
+
├─ Labels ─────────────────────────────────────┤
|
|
598
|
+
│ Scoped toggle groups (one row per scope): │
|
|
599
|
+
│ State: [Triage][WIP][Review][Verify]… │
|
|
600
|
+
│ Priority: [SOS][High][Normal][Low][Parked] │
|
|
601
|
+
│ Attention:[Orches.][Agent][User][Ignore] │
|
|
602
|
+
│ Spec: [Needed][Checklist][Approved] │
|
|
603
|
+
│ Active label per scope: primary Button; rest │
|
|
604
|
+
│ ghost. Tap → useRpcMutation set-label; │
|
|
605
|
+
│ isPending disables the group. │
|
|
606
|
+
│ Signoff row: Button "Post verify request" │
|
|
607
|
+
│ (add-comment w/ canned "Ready for human │
|
|
608
|
+
│ verification" template — operator opt-in) │
|
|
609
|
+
└──────────────────────────────────────────────┘
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
- Markdown rendering: plain React Native `Text` + `CodeBlock` for fenced
|
|
613
|
+
sections + pressable link spans for issue URLs / `paseo://` / SHAs. No new
|
|
614
|
+
markdown dependency (zero native modules).
|
|
615
|
+
- Optimistic label UI: the tapped button shows `loading` until the mutation
|
|
616
|
+
settles, then query invalidation repaints the group from server truth — no
|
|
617
|
+
client-side label prediction (exclusivity edge cases stay server-side).
|
|
618
|
+
- The Labels tab is the Orchestrator Approval Panel's v1: state promotion
|
|
619
|
+
(`1-wip` → `2-review` → `3-verify`) and review-verdict comments cover the
|
|
620
|
+
Agent-vs-Operator handoff the issue asks for, without inventing a new
|
|
621
|
+
approval primitive (defer full signoff flow to the toast-to-approval
|
|
622
|
+
pattern if needed).
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## 7. Deep linking & telemetry
|
|
627
|
+
|
|
628
|
+
### 7.1 Agent session links (`paseo://h/<serverId>/agent/<agentId>`)
|
|
629
|
+
|
|
630
|
+
- Tap handler: `Linking.openURL("paseo://h/<serverId>/agent/<agentId>")`
|
|
631
|
+
(same `Linking` mechanism the current row-tap uses for `https://` URLs).
|
|
632
|
+
- Fallback: on failure, `copyToClipboard(link)` + toast (`"Session link
|
|
633
|
+
copied"`) so the operator can paste it into a connected client.
|
|
634
|
+
- Envelopes without a `paseo://` link (older comments, e.g. `#77`) render
|
|
635
|
+
the agent ShortId as a copyable mono `KeyValue` instead of a dead button —
|
|
636
|
+
surfaces rule: absent data with a live source renders the copyable value,
|
|
637
|
+
never a broken action.
|
|
638
|
+
|
|
639
|
+
### 7.2 Commit SHA links
|
|
640
|
+
|
|
641
|
+
Tap on a SHA `CommandBox`:
|
|
642
|
+
|
|
643
|
+
1. Preferred: open the repo web commit view
|
|
644
|
+
`https://<host>/<owner>/<repo>/commit/<sha>` via `Linking.openURL`
|
|
645
|
+
(coordinates already known from `resolveRepo`).
|
|
646
|
+
2. Long-press (or secondary button): copy the full SHA to clipboard.
|
|
647
|
+
3. v2 (Phase 3): `git -C <directory> show --stat <sha>` via a new
|
|
648
|
+
`forge.commit-stat` read contract — specified but not required for v1.
|
|
649
|
+
|
|
650
|
+
### 7.3 Worktree / branch display
|
|
651
|
+
|
|
652
|
+
Branch names from envelopes render as copyable mono text (`CommandBox`
|
|
653
|
+
single-line variant). No checkout action in v1 — branch teleportation stays
|
|
654
|
+
with the agent harness (open question §10.4).
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## 8. Lifecycle state machine (label transitions)
|
|
659
|
+
|
|
660
|
+
```
|
|
661
|
+
set-label set-label
|
|
662
|
+
0-triage ───────────────────▶ 1-wip ───────────────────▶ 2-review
|
|
663
|
+
│ │ │
|
|
664
|
+
│ attention/1-agent │ agent posts completion │ orchestrator approves /
|
|
665
|
+
│ (claim) │ envelope + sets │ requests changes
|
|
666
|
+
▼ ▼ ▼
|
|
667
|
+
(operator triage) 2-review ◀────────────────── 1-wip (rework)
|
|
668
|
+
│
|
|
669
|
+
│ operator verifies / approves
|
|
670
|
+
▼
|
|
671
|
+
3-verify ── human closes ──▶ (closed)
|
|
672
|
+
│
|
|
673
|
+
│ changes requested
|
|
674
|
+
▼
|
|
675
|
+
1-wip
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Invariants:
|
|
679
|
+
|
|
680
|
+
1. Client sends only `set-label` adds; Gitea-family exclusive scopes guarantee
|
|
681
|
+
single-occupancy per scope — the client never issues removes.
|
|
682
|
+
2. Agents and the Orchestrator never close issues (coding-agent skill §6):
|
|
683
|
+
`state/4-done` is an open label; closing is the human operator's word.
|
|
684
|
+
3. `flag/stop-work` short-circuits everything: when present, the Labels tab
|
|
685
|
+
disables all toggle groups and renders a `danger` banner (circuit breaker
|
|
686
|
+
is board-global, not per-transition).
|
|
687
|
+
4. `attention/3-ignore` suppresses the issue in the default Queue tab
|
|
688
|
+
(server-side exclusion unless `query` matches — deterministic triage
|
|
689
|
+
parity), unless `priority/0-SOS` is also present (SOS outranks ignore).
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## 9. Error handling matrix
|
|
694
|
+
|
|
695
|
+
| Situation | Behavior |
|
|
696
|
+
|---|---|
|
|
697
|
+
| No git remote / unparseable origin | `{ repo: null, error }` → `EmptyState` "No forge repo for this workspace" (existing behavior, kept) |
|
|
698
|
+
| API unreachable / no token for a private repo | `{ error: "Issue list unavailable" }` → `EmptyState` + Retry; pill falls back to `"issues --"` placeholder |
|
|
699
|
+
| Unknown issue number | `{ issue: null, error }` → `EmptyState` "Issue #N not found in repo" |
|
|
700
|
+
| `set-label` with out-of-vocabulary label | Rejected before spawn; `{ error }` surfaced via mutation `onError`; toggle group re-enables |
|
|
701
|
+
| `set-label` race (two operators, same scope) | Last write wins at the host; query invalidation repaints from server truth — no client prediction to unwind |
|
|
702
|
+
| `add-comment` empty body | Button disabled client-side; `min(1)` server-side rejects as typed error |
|
|
703
|
+
| `add-comment` failure (network/auth) | Mutation `onError` → toast; composer text preserved (never cleared on failure) |
|
|
704
|
+
| Envelope footer unparseable | Comment renders as plain markdown; `envelopes` omits it; detail RPC still succeeds |
|
|
705
|
+
| `paseo://` open fails (no handler) | Copy link to clipboard + toast; never a dead tap |
|
|
706
|
+
| Storage write fails (board cache) | Serve last good cache with stale `fetchedAt`; `Card.Header` subtitle shows "updated Xm ago" via `formatDuration` |
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
## 10. Phased roadmap
|
|
711
|
+
|
|
712
|
+
### Phase 0 — This spec (done when merged)
|
|
713
|
+
|
|
714
|
+
Spec file + board review. Labels advance `spec/1-checklist` →
|
|
715
|
+
`spec/2-approved`, `state/1-wip` → `state/2-review` on the tracking issue.
|
|
716
|
+
No code touched.
|
|
717
|
+
|
|
718
|
+
### Phase 1 — Architecture & data path (shipped)
|
|
719
|
+
|
|
720
|
+
- [x] `shared/issues.ts`: label vocabularies + ranks, issue/detail/envelope
|
|
721
|
+
schemas, envelope parser, sort-tuple comparator, contracts (§4–§5).
|
|
722
|
+
- [x] `server/forge-client.ts`: embedded Gitea-family `/api/v1` fetch client
|
|
723
|
+
(list/detail/comments/labels, repo probe, token probe).
|
|
724
|
+
- [x] `server/issues.ts` + `server/settings.ts`: repo resolution (`.git/config`
|
|
725
|
+
parse, explicit-target precedence), host-keyed daemon-side tokens,
|
|
726
|
+
`set-label` / `add-comment` / `install-labels` handlers. No subprocess.
|
|
727
|
+
- [x] Unit tests: envelope parser, sort tuple (SOS-first, verify-before-wip,
|
|
728
|
+
recency tiebreak), scope-vocabulary guard, link classification, label-set
|
|
729
|
+
planning.
|
|
730
|
+
- [x] Keep `forge.open-issues` contract; new contracts register alongside.
|
|
731
|
+
|
|
732
|
+
### Phase 2 — Client UI surfaces
|
|
733
|
+
|
|
734
|
+
- [ ] `<BoardView>` shared tree (`Card`, `Tabs`, `SearchInput`, `DataTable`,
|
|
735
|
+
`Badge`, `EmptyState`, `ActionBar`) + `ForgeBoardModal` (replaces list
|
|
736
|
+
tab content) + `ForgeBoardSurface` via `registerSidebarSurface`.
|
|
737
|
+
- [ ] Pill upgrade: verify-first label (`"3 verify · 12 open"` / `"3v"`),
|
|
738
|
+
tap-through to pre-filtered dashboard.
|
|
739
|
+
- [ ] `<IssueDetailModal>` with four tabs + `<AgentEnvelopeCard>` +
|
|
740
|
+
`<ScopedLabelGroup>` + quick-comment composer (`TextInput` multiline +
|
|
741
|
+
`useRpcMutation`).
|
|
742
|
+
- [ ] Compact-portrait pass (`useResponsive` / `select`: table → cards,
|
|
743
|
+
full labels → `shortLabel`, `touchTargetMin` 44pt on toggles).
|
|
744
|
+
|
|
745
|
+
### Phase 3 — Deep linking & telemetry
|
|
746
|
+
|
|
747
|
+
- [ ] `paseo://` tap-through with clipboard fallback (§7.1).
|
|
748
|
+
- [ ] Commit SHA → web commit view + long-press copy (§7.2).
|
|
749
|
+
- [ ] Optional `forge.commit-stat` read contract (`git show --stat`) if
|
|
750
|
+
operator review needs diff summaries in-client.
|
|
751
|
+
|
|
752
|
+
### Phase 4 — Hardening & parity
|
|
753
|
+
|
|
754
|
+
- [ ] `flag/stop-work` banner + toggle-group disable; `attention/3-ignore`
|
|
755
|
+
exclusion rule (§8.4).
|
|
756
|
+
- [ ] Closed-app honesty: board cache renders instantly with "updated Xm ago"
|
|
757
|
+
subtitle while the 60 s refresh runs (mirrors the toast-to-approval
|
|
758
|
+
durable-queue principle).
|
|
759
|
+
- [ ] Gap suite: every new badge/count renders through shared predicates on
|
|
760
|
+
both pill and dashboard (per `docs/surfaces.md` rules for new metrics).
|
|
761
|
+
|
|
762
|
+
### Open questions (implementation phase)
|
|
763
|
+
|
|
764
|
+
1. Should the envelope parser / sort comparator move into
|
|
765
|
+
`paseo-plugin-helper/shared` for reuse by other plugins (e.g. an
|
|
766
|
+
Orchestrator dashboard)? Recommendation: keep in `plugins/forges` until
|
|
767
|
+
a second consumer exists.
|
|
768
|
+
2. Should `forge.commit-stat` be part of v1? Recommendation: no — web-view
|
|
769
|
+
link + copy covers review; diff-in-client is Phase 3 stretch.
|
|
770
|
+
3. Should `size/*` (effort) and `dep/blocker` enter the sort key?
|
|
771
|
+
Recommendation: badges only in v1; revisit after operator feedback.
|
|
772
|
+
4. Branch teleportation (tap branch → open/attach workspace)? Recommendation:
|
|
773
|
+
explicitly out of scope — stays with the agent harness.
|
|
774
|
+
|
|
775
|
+
---
|
|
776
|
+
|
|
777
|
+
## 11. Testing plan
|
|
778
|
+
|
|
779
|
+
- **Server:** pure shared functions unit-tested directly (`node --test` via
|
|
780
|
+
`esbuild`); the fetch client is exercised through its parsing helpers.
|
|
781
|
+
Assert board sort order, live scope derivation, scope-vocabulary rejection,
|
|
782
|
+
label-set planning (`merge` vs `replace`), detail null-form for unknown
|
|
783
|
+
numbers, and comment/commentId passthrough.
|
|
784
|
+
- **Parser:** envelope footer fixtures (live `#77` footer, multi-SHA body,
|
|
785
|
+
`paseo://` body, malformed footer → skipped, no footer → `null`).
|
|
786
|
+
- **Client:** mock `useRpc` doubles. Assert pill label variants
|
|
787
|
+
(`"3 verify · 12 open"` / `"12 open"` / `"issues --"`), tab filters map to
|
|
788
|
+
contract input, row tap opens detail payload, label tap fires
|
|
789
|
+
`set-label { number, label }` with group disabled while pending, empty
|
|
790
|
+
comment disables Post, failed comment preserves composer text.
|
|
791
|
+
- **Surfaces:** pill and dashboard fed by the same mocked overview payload;
|
|
792
|
+
assert counts agree (gap-suite style, per `docs/surfaces.md`).
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## 12. Acceptance criteria
|
|
797
|
+
|
|
798
|
+
- [ ] Board overview renders the open queue sorted by
|
|
799
|
+
`(priorityRank, stateRank, updatedAt desc)` with state/priority filters
|
|
800
|
+
and `#N`/title/label search — no browser needed for triage.
|
|
801
|
+
- [ ] Issue detail shows body, comment thread, and structured Agent Envelope
|
|
802
|
+
cards (agent id, model, branch, SHAs, `paseo://` link) in-client.
|
|
803
|
+
- [ ] Scoped label toggles advance `state/`–`priority/`–`attention/`–`spec/`
|
|
804
|
+
with single taps; the write adds the label and removes any same-scope
|
|
805
|
+
mate, and the UI repaints from server truth after each mutation.
|
|
806
|
+
- [ ] Quick comments post operator steering into the thread; composer text
|
|
807
|
+
survives failures.
|
|
808
|
+
- [ ] Auth uses a daemon-side token saved per host in plugin settings; the
|
|
809
|
+
token never reaches the client and no `fgj` config or host CLI is
|
|
810
|
+
required.
|
|
811
|
+
- [ ] Pill shows verify-first counts (`"3 verify · 12 open"` / `"3v"`) and
|
|
812
|
+
deep-opens the pre-filtered dashboard; zero-verify hides the verify
|
|
813
|
+
chip (never `0 verify`).
|
|
814
|
+
- [ ] Agent ID taps resolve to `paseo://` sessions (clipboard fallback);
|
|
815
|
+
SHA taps open the web commit view (long-press copies).
|
|
816
|
+
- [ ] Built only from documented helper primitives; `initClientHelpers`
|
|
817
|
+
shape unchanged; zero new SDK imports; client uses no Node
|
|
818
|
+
built-ins.
|
|
819
|
+
- [ ] `forge.open-issues` contract and current pill behavior preserved
|
|
820
|
+
throughout migration.
|