@ifrankwang/openspec-agents 0.146.1 → 0.147.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,64 +0,0 @@
1
- import { createServer } from "node:http"
2
- import type { Server } from "node:http"
3
- import { readDashboardState } from "../../core/dashboard.ts"
4
- import { getDashboardPage } from "../../dashboard/page.ts"
5
-
6
- const BASE_PORT = 4519
7
- const MAX_ATTEMPTS = 20
8
-
9
- const servers = new Map<string, Server>()
10
-
11
- export async function startDashboard(worktree: string): Promise<void> {
12
- if (servers.has(worktree)) return
13
-
14
- const pageHtml = getDashboardPage()
15
-
16
- // 端口占用自动递增:EADDRINUSE 走 server error 事件(异步),经 once("error") + await listen
17
- // 判定重试;其余启动期错误仅记录不中断。修复前 try/catch 同步捕获永不触发,换端口失效。
18
- for (let port = BASE_PORT; port < BASE_PORT + MAX_ATTEMPTS; port++) {
19
- try {
20
- const server = createServer(async (req, res) => {
21
- try {
22
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`)
23
- if (url.pathname === "/api/state") {
24
- try {
25
- const data = await readDashboardState(worktree)
26
- res.writeHead(200, { "content-type": "application/json;charset=utf-8" })
27
- res.end(JSON.stringify(data ?? { active: false }))
28
- } catch (err) {
29
- res.writeHead(500, { "content-type": "application/json;charset=utf-8" })
30
- res.end(JSON.stringify({ active: false, error: String(err) }))
31
- }
32
- return
33
- }
34
- res.writeHead(200, { "content-type": "text/html;charset=utf-8" })
35
- res.end(pageHtml)
36
- } catch (err) {
37
- console.error("[dashboard]", (err as Error).message)
38
- res.writeHead(500, { "content-type": "text/plain;charset=utf-8" })
39
- res.end("internal error")
40
- }
41
- })
42
- await new Promise<void>((resolve, reject) => {
43
- server.once("error", (err: NodeJS.ErrnoException) => {
44
- if (err.code === "EADDRINUSE") {
45
- reject(err)
46
- } else {
47
- console.error("[dashboard]", err.message)
48
- resolve()
49
- }
50
- })
51
- server.listen(port, "127.0.0.1", () => resolve())
52
- })
53
- servers.set(worktree, server)
54
- console.log(`[dashboard] 编排进度看板 http://127.0.0.1:${port}`)
55
- return
56
- } catch {
57
- continue
58
- }
59
- }
60
-
61
- console.error(
62
- `[dashboard] 无法启动编排进度看板:端口 ${BASE_PORT}-${BASE_PORT + MAX_ATTEMPTS - 1} 均被占用`
63
- )
64
- }
@@ -1,101 +0,0 @@
1
- import type { OrchestrateState } from "./types.ts"
2
- import { readStateByWorktree, readStateByChangeId, getStateDir } from "./state.ts"
3
- import { readdirSync, existsSync } from "node:fs"
4
- import { readFile } from "node:fs/promises"
5
- import path from "path"
6
- import type { WorkItem, WorkItemType, WorkItemPhase, Verdict } from "./workflow/types.ts"
7
-
8
- /** 看板卡片投影:WorkItem → 前端可渲染结构。 */
9
- export interface WorkItemCard {
10
- id: string
11
- type: WorkItemType
12
- title: string
13
- description: string
14
- phase: WorkItemPhase
15
- suspended: boolean
16
- suspendReason?: string
17
- currentStep: string | null
18
- labels: string[]
19
- source: string
20
- severity?: string
21
- agentVerdicts: Array<{ stepId: string; agentKey: string; verdict: Verdict }>
22
- children: Array<{ id: string; type: WorkItemType; title: string; phase: WorkItemPhase; severity?: string }>
23
- }
24
-
25
- /** taskGroup → 看板投影(旧三面板视图已随单轨化移除)。 */
26
-
27
- /**
28
- * WorkItem → 看板卡片。
29
- * tags 键为 `${stepId}:${agentKey}`,拆解为可渲染的 agent 裁决数组;
30
- * metadata 仅暴露无下划线前缀的业务字段(suspend_reason),内部字段(_ 前缀)不泄露。
31
- */
32
- function toWorkItemCard(item: WorkItem): WorkItemCard {
33
- const agentVerdicts = Object.entries(item.tags).map(([key, verdict]) => {
34
- const sep = key.indexOf(":")
35
- return {
36
- stepId: sep === -1 ? key : key.slice(0, sep),
37
- agentKey: sep === -1 ? key : key.slice(sep + 1),
38
- verdict,
39
- }
40
- })
41
- const suspendReason =
42
- typeof item.metadata["suspend_reason"] === "string" ? (item.metadata["suspend_reason"] as string) : undefined
43
- return {
44
- id: item.id,
45
- type: item.type,
46
- title: item.title,
47
- description: item.description,
48
- phase: item.phase,
49
- suspended: item.suspended,
50
- suspendReason,
51
- currentStep: item.currentStep,
52
- labels: item.labels,
53
- source: item.source,
54
- severity: item.severity,
55
- agentVerdicts,
56
- children: item.children.map((child) => ({
57
- id: child.id,
58
- type: child.type,
59
- title: child.title,
60
- phase: child.phase,
61
- severity: child.severity,
62
- })),
63
- }
64
- }
65
-
66
- /** OrchestrateState → 看板投影(workItems 单轨 + workItemCards 5 列视图)。 */
67
- function projectState(state: OrchestrateState) {
68
- return {
69
- active: true,
70
- changeId: state.changeId,
71
- currentTaskGroupId: state.taskGroupId,
72
- baseBranch: state.baseBranch,
73
- createdAt: state.createdAt,
74
- updatedAt: state.updatedAt,
75
- workItems: state.workItems,
76
- workItemCards: state.workItems.map(toWorkItemCard),
77
- }
78
- }
79
-
80
- export async function readDashboardState(worktree: string, changeId?: string) {
81
- if (changeId) {
82
- const state = await readStateByWorktree(worktree, changeId)
83
- if (!state) return null
84
- return projectState(state)
85
- }
86
-
87
- const stateDir = getStateDir(worktree)
88
- if (!existsSync(stateDir)) return null
89
- const files = readdirSync(stateDir).filter(f => f.endsWith(".json"))
90
- const results: ReturnType<typeof projectState>[] = []
91
- for (const f of files) {
92
- try {
93
- const raw = await readFile(path.join(stateDir, f), "utf-8")
94
- const state = JSON.parse(raw) as OrchestrateState
95
- if (!state.changeId) continue
96
- results.push(projectState(state))
97
- } catch { /* skip unreadable */ }
98
- }
99
- if (results.length === 1) return results[0]
100
- return results.length > 0 ? results : null
101
- }
@@ -1,19 +0,0 @@
1
- import { fileURLToPath } from "node:url"
2
- import { readFileSync, existsSync } from "node:fs"
3
- import { join } from "node:path"
4
-
5
- let _html: string | null = null
6
-
7
- /** 页面资源定位:bundle 形态(ZCode 插件包)资源随 bundle 同目录放置;源码/包形态位于 ../../assets/dashboard/。 */
8
- function resolveDashboardHtml(): string {
9
- const __dirname = fileURLToPath(new URL(".", import.meta.url))
10
- const bundled = join(__dirname, "dashboard", "index.html")
11
- if (existsSync(bundled)) return bundled
12
- return join(__dirname, "../../assets/dashboard/index.html")
13
- }
14
-
15
- export function getDashboardPage(): string {
16
- if (_html) return _html
17
- _html = readFileSync(resolveDashboardHtml(), "utf-8")
18
- return _html
19
- }