@ifrankwang/openspec-agents 0.118.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/README.md +91 -0
- package/assets/agents/openspec-architect.md +61 -0
- package/assets/agents/openspec-developer.md +21 -0
- package/assets/agents/openspec-main.md +35 -0
- package/assets/agents/openspec-reviewer-architecture.md +60 -0
- package/assets/agents/openspec-reviewer-maintainability.md +59 -0
- package/assets/agents/openspec-reviewer-performance.md +49 -0
- package/assets/agents/openspec-reviewer-security.md +51 -0
- package/assets/agents/openspec-reviewer-style.md +50 -0
- package/assets/agents/openspec-reviewer-task.md +40 -0
- package/assets/agents/openspec-reviewer-tool.md +30 -0
- package/assets/dashboard/index.html +295 -0
- package/assets/skills/api-conventions/SKILL.md +60 -0
- package/assets/skills/api-test/SKILL.md +175 -0
- package/assets/skills/architecture-evolution/SKILL.md +60 -0
- package/assets/skills/db-conventions/SKILL.md +62 -0
- package/assets/skills/ddd-architecture/SKILL.md +261 -0
- package/assets/skills/environment-cleanup/SKILL.md +44 -0
- package/assets/skills/java-api-conventions/SKILL.md +30 -0
- package/assets/skills/java-code-maintainability/SKILL.md +62 -0
- package/assets/skills/java-code-performance/SKILL.md +49 -0
- package/assets/skills/java-code-style/SKILL.md +54 -0
- package/assets/skills/java-db-conventions/SKILL.md +44 -0
- package/assets/skills/java-ddd-architecture/SKILL.md +533 -0
- package/assets/skills/java-dev-practices/SKILL.md +110 -0
- package/assets/skills/java-quality-gate/SKILL.md +455 -0
- package/assets/skills/java-quality-gate/reference/pmd-rules.md +16 -0
- package/assets/skills/java-quality-tool-improve/SKILL.md +142 -0
- package/assets/skills/java-security/SKILL.md +59 -0
- package/assets/skills/java-technical-debt/SKILL.md +36 -0
- package/assets/skills/orchestrator/SKILL.md +65 -0
- package/assets/skills/quality-gate/SKILL.md +84 -0
- package/assets/skills/security-baseline/SKILL.md +101 -0
- package/assets/skills/technical-debt/SKILL.md +78 -0
- package/assets/workflows/task.yaml +177 -0
- package/package.json +79 -0
- package/src/adapters/agent-md.ts +37 -0
- package/src/adapters/claude-code/index.ts +35 -0
- package/src/adapters/codex/index.ts +161 -0
- package/src/adapters/mcp-common/cli.ts +64 -0
- package/src/adapters/mcp-common/index.ts +137 -0
- package/src/adapters/mcp-common/json-schema.ts +53 -0
- package/src/adapters/mcp-common/server.ts +157 -0
- package/src/adapters/opencode/agents.ts +80 -0
- package/src/adapters/opencode/dashboard.ts +64 -0
- package/src/adapters/opencode/index.ts +34 -0
- package/src/adapters/opencode/json-schema.ts +60 -0
- package/src/adapters/opencode/skills.ts +19 -0
- package/src/adapters/opencode/tools.ts +78 -0
- package/src/adapters/plugin-common/index.ts +173 -0
- package/src/adapters/zcode/index.ts +32 -0
- package/src/core/constants.ts +46 -0
- package/src/core/dashboard.ts +101 -0
- package/src/core/derive.ts +146 -0
- package/src/core/exemptions.ts +168 -0
- package/src/core/git.ts +373 -0
- package/src/core/index.ts +76 -0
- package/src/core/namespace.ts +12 -0
- package/src/core/paths.ts +23 -0
- package/src/core/provider.ts +74 -0
- package/src/core/state.ts +357 -0
- package/src/core/task-children.ts +59 -0
- package/src/core/tasks-md.ts +89 -0
- package/src/core/tools/gate.ts +165 -0
- package/src/core/tools/lifecycle.ts +645 -0
- package/src/core/tools/schemas.ts +363 -0
- package/src/core/tools/submit.ts +863 -0
- package/src/core/tools/types.ts +129 -0
- package/src/core/types.ts +135 -0
- package/src/core/views.ts +195 -0
- package/src/core/workflow/collector.ts +150 -0
- package/src/core/workflow/engine.ts +576 -0
- package/src/core/workflow/index.ts +56 -0
- package/src/core/workflow/loader.ts +277 -0
- package/src/core/workflow/poller.ts +159 -0
- package/src/core/workflow/reset.ts +93 -0
- package/src/core/workflow/status.ts +1010 -0
- package/src/core/workflow/submit.ts +331 -0
- package/src/core/workflow/types.ts +100 -0
- package/src/core/workflow/writeback.ts +48 -0
- package/src/dashboard/page.ts +19 -0
- package/src/index.ts +2 -0
- package/src/skills/resolve.ts +95 -0
- package/src/skills/scan.ts +29 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { BuildPhaseTarget, ReviewLayer, ReviewVerifyStep } from "../types.ts"
|
|
2
|
+
|
|
3
|
+
export interface ToolContext {
|
|
4
|
+
worktree: string
|
|
5
|
+
agent: string
|
|
6
|
+
/** 编排视角角色判定:true 表示调用者承担编排者职责(各 agent 主代理),替代 agent 名硬编码 openspec-orchestrator。 */
|
|
7
|
+
orchestrator?: boolean
|
|
8
|
+
/** 调用者 agent 身份是否显式声明:MCP 形态下为是否携带 `_agent` 参数,OpenCode 直载形态恒为 true。
|
|
9
|
+
* false 且落入编排视角时,opx_status 视图渲染补传 `_agent` 的身份提示(MCP 子代理首查死锁兜底)。 */
|
|
10
|
+
identityDeclared?: boolean
|
|
11
|
+
remote?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolResult {
|
|
15
|
+
content: string
|
|
16
|
+
isError?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface InitParams {
|
|
20
|
+
change_id: string
|
|
21
|
+
task_group_id: string
|
|
22
|
+
base_branch?: string
|
|
23
|
+
recovery?: {
|
|
24
|
+
phase: BuildPhaseTarget
|
|
25
|
+
review_layer?: ReviewLayer
|
|
26
|
+
reopenIssues?: boolean
|
|
27
|
+
/** 重置指定 verify step 的审查 tags 为 pending(仅 phase=review 时有效,与 review_layer 互斥),
|
|
28
|
+
* 恢复后 currentStep 落在第一个未全部通过的 verify step,可能早于被重置的 step。 */
|
|
29
|
+
reset_steps?: ReviewVerifyStep[]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SetWorktreeParams {
|
|
34
|
+
change_id: string
|
|
35
|
+
worktree_path?: string
|
|
36
|
+
branch_name?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UnattendedParams {
|
|
40
|
+
change_id: string
|
|
41
|
+
enabled: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface StatusParams {
|
|
45
|
+
change_id: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AgentSubmitParams {
|
|
49
|
+
change_id: string
|
|
50
|
+
step_id: string
|
|
51
|
+
verdict: "passed" | "failed"
|
|
52
|
+
fixed_issue_ids?: string[]
|
|
53
|
+
exempt_issue_ids?: string[]
|
|
54
|
+
exempt_adjudications?: Array<{
|
|
55
|
+
issue_id: string
|
|
56
|
+
action: "dismissed" | "rejected"
|
|
57
|
+
}>
|
|
58
|
+
recheck_adjudications?: Array<{
|
|
59
|
+
issue_id: string
|
|
60
|
+
verdict: "passed" | "rejected"
|
|
61
|
+
reject_reason?: string
|
|
62
|
+
}>
|
|
63
|
+
checkpoint_decision?: "continue" | "giveup"
|
|
64
|
+
/** giveup 决策配套:对当前 step 质量门必做清单(must_do)未覆盖项逐项提供的结构化降级理由
|
|
65
|
+
* (giveup 未覆盖项缺理由则拒绝 giveup)。adjudication 取值 user_response / unattended_auto / env_unavailable。 */
|
|
66
|
+
checkpoint_skip_reasons?: Array<{
|
|
67
|
+
item: string
|
|
68
|
+
category: string
|
|
69
|
+
adjudication: string
|
|
70
|
+
note?: string
|
|
71
|
+
}>
|
|
72
|
+
new_children?: Array<{
|
|
73
|
+
id: string
|
|
74
|
+
title: string
|
|
75
|
+
description: string
|
|
76
|
+
severity?: string
|
|
77
|
+
dimension?: string
|
|
78
|
+
file?: string
|
|
79
|
+
line?: number
|
|
80
|
+
suggestion?: string
|
|
81
|
+
rule?: string
|
|
82
|
+
root_cause_guess?: string
|
|
83
|
+
}>
|
|
84
|
+
execution_boundary?: {
|
|
85
|
+
allowed_directories: string[]
|
|
86
|
+
allowed_packages: string[]
|
|
87
|
+
notes: string
|
|
88
|
+
}
|
|
89
|
+
blockers?: Array<{
|
|
90
|
+
source_role: string
|
|
91
|
+
task_id?: string
|
|
92
|
+
category: string
|
|
93
|
+
description: string
|
|
94
|
+
evidence: string
|
|
95
|
+
attempted_actions: string
|
|
96
|
+
options?: string[]
|
|
97
|
+
}>
|
|
98
|
+
blocker_updates?: Array<{
|
|
99
|
+
blocker_id: string
|
|
100
|
+
user_response: string
|
|
101
|
+
}>
|
|
102
|
+
blocker?: {
|
|
103
|
+
source_role: string
|
|
104
|
+
task_id?: string
|
|
105
|
+
category: string
|
|
106
|
+
description: string
|
|
107
|
+
evidence: string
|
|
108
|
+
attempted_actions: string
|
|
109
|
+
options?: string[]
|
|
110
|
+
}
|
|
111
|
+
self_check_results?: string
|
|
112
|
+
completed_task_ids?: string[]
|
|
113
|
+
test_results?: string
|
|
114
|
+
validation_steps?: Array<{
|
|
115
|
+
step: string
|
|
116
|
+
completed: boolean
|
|
117
|
+
evidence?: string
|
|
118
|
+
skip_reason?: string
|
|
119
|
+
}>
|
|
120
|
+
boundary_expansion?: {
|
|
121
|
+
allowed_directories?: string[]
|
|
122
|
+
allowed_packages?: string[]
|
|
123
|
+
}
|
|
124
|
+
verified_tasks?: string[]
|
|
125
|
+
failed_tasks?: Array<{
|
|
126
|
+
task_id: string
|
|
127
|
+
reason: string
|
|
128
|
+
}>
|
|
129
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { WorkItem } from "./workflow/types.ts"
|
|
2
|
+
|
|
3
|
+
export const CODE_DIMENSIONS = ["style", "architecture", "performance", "security", "maintainability"] as const
|
|
4
|
+
export const REVIEW_DIMENSIONS = [...CODE_DIMENSIONS] as const
|
|
5
|
+
export type ReviewDimension = typeof REVIEW_DIMENSIONS[number]
|
|
6
|
+
export type Dimension = ReviewDimension
|
|
7
|
+
|
|
8
|
+
export const TASK_STATUSES = ["open", "submitted", "rejected", "verified"] as const
|
|
9
|
+
export type TaskStatus = typeof TASK_STATUSES[number]
|
|
10
|
+
export const ISSUE_STATUSES = ["open", "submitted", "rejected", "verified", "exemption_requested", "exempted"] as const
|
|
11
|
+
export type IssueStatus = typeof ISSUE_STATUSES[number]
|
|
12
|
+
|
|
13
|
+
export type Phase = "task_analysis" | "dev_impl" | "review" | "completed"
|
|
14
|
+
|
|
15
|
+
/** 恢复(recovery)可恢复的阶段合法值,与 InitParams.recovery.phase 值域一致。 */
|
|
16
|
+
export const BUILD_PHASE_TARGETS = ["task_analysis", "dev_impl", "review"] as const
|
|
17
|
+
export type BuildPhaseTarget = typeof BUILD_PHASE_TARGETS[number]
|
|
18
|
+
|
|
19
|
+
/** review 内子层合法值(review_layer / issue 报源层共用同一三元组)。 */
|
|
20
|
+
export const REVIEW_LAYERS = ["tool", "task", "quality"] as const
|
|
21
|
+
export type ReviewLayer = typeof REVIEW_LAYERS[number]
|
|
22
|
+
|
|
23
|
+
/** review 三个验证 step 合法值(recovery.reset_steps 值域)。 */
|
|
24
|
+
export const REVIEW_VERIFY_STEPS = ["verify_tool", "verify_task", "verify_quality"] as const
|
|
25
|
+
export type ReviewVerifyStep = typeof REVIEW_VERIFY_STEPS[number]
|
|
26
|
+
export type OrchestrateStatus = "not_started" | "in_progress" | "completed"
|
|
27
|
+
export type DimensionVerdict = "pending" | "passed" | "failed"
|
|
28
|
+
export type QualityLayerProgress = Record<ReviewDimension, DimensionVerdict>
|
|
29
|
+
|
|
30
|
+
export interface ExecutionBoundary {
|
|
31
|
+
allowed_directories: string[]
|
|
32
|
+
allowed_packages: string[]
|
|
33
|
+
notes: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TaskItem {
|
|
37
|
+
id: string
|
|
38
|
+
specTrace: string
|
|
39
|
+
title: string
|
|
40
|
+
status: TaskStatus
|
|
41
|
+
taskNumber: string
|
|
42
|
+
rejectReason: string | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface IssueItem {
|
|
46
|
+
id: string
|
|
47
|
+
dimension: Dimension
|
|
48
|
+
sourcePhase: ReviewLayer
|
|
49
|
+
severity: string
|
|
50
|
+
file: string
|
|
51
|
+
line: number
|
|
52
|
+
description: string
|
|
53
|
+
suggestion: string
|
|
54
|
+
status: IssueStatus
|
|
55
|
+
refixCount: number
|
|
56
|
+
rootCauseGuess: string | null
|
|
57
|
+
exemptReason: string | null
|
|
58
|
+
rejectReason: string | null
|
|
59
|
+
rule?: string // 工具规则名(如 PMD Rule / SonarQube rule),非工具来源可为空
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type BlockerStatus = "awaiting_user" | "resolved"
|
|
63
|
+
|
|
64
|
+
export interface BlockerItem {
|
|
65
|
+
id: string
|
|
66
|
+
sourceRole: string
|
|
67
|
+
taskId: string | null
|
|
68
|
+
category: string
|
|
69
|
+
description: string
|
|
70
|
+
evidence: string
|
|
71
|
+
attemptedActions: string
|
|
72
|
+
options: string[]
|
|
73
|
+
status: BlockerStatus
|
|
74
|
+
userResponse: string | null
|
|
75
|
+
architectConclusion: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ValidationStep {
|
|
79
|
+
step: string
|
|
80
|
+
completed: boolean
|
|
81
|
+
evidence: string
|
|
82
|
+
skip_reason: string | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ReviewLayerData {
|
|
86
|
+
completed: boolean
|
|
87
|
+
testResults?: string
|
|
88
|
+
validationSteps?: ValidationStep[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ReviewPhaseData {
|
|
92
|
+
retryCount: number
|
|
93
|
+
lastResolvedRetryCount: number
|
|
94
|
+
tool: ReviewLayerData
|
|
95
|
+
task: ReviewLayerData
|
|
96
|
+
quality: { progress: QualityLayerProgress }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SimplePhaseData {
|
|
100
|
+
completed: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface Phases {
|
|
104
|
+
architect_review: SimplePhaseData
|
|
105
|
+
review: ReviewPhaseData
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface TaskGroupState {
|
|
109
|
+
id: string
|
|
110
|
+
name: string
|
|
111
|
+
taskCount: number
|
|
112
|
+
worktreePath: string | null
|
|
113
|
+
branchName: string | null
|
|
114
|
+
baseRef: string | null
|
|
115
|
+
executionBoundary: ExecutionBoundary | null
|
|
116
|
+
relevantSpecs: string[]
|
|
117
|
+
devSelfCheckResults?: string
|
|
118
|
+
status: Phase
|
|
119
|
+
phases: Phases
|
|
120
|
+
tasks: TaskItem[]
|
|
121
|
+
issues: IssueItem[]
|
|
122
|
+
blockers: BlockerItem[]
|
|
123
|
+
agentSummaries?: Record<string, string> // agent 名 → 最近一次提交摘要(单值覆盖)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface OrchestrateState {
|
|
127
|
+
changeId: string
|
|
128
|
+
isolationNamespace: string
|
|
129
|
+
taskGroupId: string
|
|
130
|
+
baseBranch: string
|
|
131
|
+
workItems: WorkItem[] // 工作流引擎原生持久化(单轨事实源)
|
|
132
|
+
createdAt: string
|
|
133
|
+
updatedAt: string
|
|
134
|
+
unattended?: boolean
|
|
135
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { TaskGroupState, OrchestrateState, TaskItem } from "./types.ts"
|
|
2
|
+
import { derivePortFromNamespace } from "./namespace.ts"
|
|
3
|
+
import { readFileSync } from "node:fs"
|
|
4
|
+
import { findSkillPath } from "../skills/scan.ts"
|
|
5
|
+
import { scanSkillTags, resolveSkillsForCapabilities, getEfficiencySkills } from "../skills/resolve.ts"
|
|
6
|
+
|
|
7
|
+
export function renderSkillSuggestions(agent: string, caps: string[]): string[] {
|
|
8
|
+
if (caps.length === 0) return []
|
|
9
|
+
const index = scanSkillTags()
|
|
10
|
+
const { skillNames, generic, techStackOnly } = resolveSkillsForCapabilities(caps, index)
|
|
11
|
+
|
|
12
|
+
const lines: string[] = []
|
|
13
|
+
if (skillNames.length === 0) return lines
|
|
14
|
+
|
|
15
|
+
lines.push("## Skill 加载清单", "")
|
|
16
|
+
lines.push("必须加载(逐项用 Skill tool 加载,未找到时跳过):", "")
|
|
17
|
+
|
|
18
|
+
for (const name of generic) {
|
|
19
|
+
lines.push(`- \`${name}\``)
|
|
20
|
+
}
|
|
21
|
+
lines.push("")
|
|
22
|
+
if (techStackOnly.length > 0) {
|
|
23
|
+
lines.push("仅当技术栈匹配时加载:", "")
|
|
24
|
+
const byStack = new Map<string, string[]>()
|
|
25
|
+
for (const name of techStackOnly) {
|
|
26
|
+
const tags = index.skillTags.get(name) || []
|
|
27
|
+
const stackTag = tags.find((t) => t.startsWith("tech-stack-"))
|
|
28
|
+
if (!stackTag) continue
|
|
29
|
+
const arr = byStack.get(stackTag) || []
|
|
30
|
+
arr.push(name)
|
|
31
|
+
byStack.set(stackTag, arr)
|
|
32
|
+
}
|
|
33
|
+
for (const names of byStack.values()) {
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
lines.push(`- \`${name}\``)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
lines.push("")
|
|
39
|
+
}
|
|
40
|
+
lines.push("(其他 available_skills 中未列举的相关 skill,按需加载)")
|
|
41
|
+
lines.push("")
|
|
42
|
+
|
|
43
|
+
const hinted: string[] = []
|
|
44
|
+
for (const name of skillNames) {
|
|
45
|
+
const mdPath = findSkillPath(name)
|
|
46
|
+
if (!mdPath) continue
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(mdPath, "utf-8")
|
|
49
|
+
if (!raw.includes("boundary_hints:")) continue
|
|
50
|
+
const dm = raw.match(/directories:\s*\[([^\]]*)\]/)
|
|
51
|
+
if (dm) for (const d of dm[1].split(",").map((s: string) => s.trim().replace(/["']/g, ""))) {
|
|
52
|
+
hinted.push(`- \`${name}\` → \`${d}\``)
|
|
53
|
+
}
|
|
54
|
+
} catch { /* skip */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (hinted.length > 0) {
|
|
58
|
+
lines.push("## 路径提示(skill 声明的 boundary_hints)", "")
|
|
59
|
+
lines.push("以下目录不受执行边界限制:", "")
|
|
60
|
+
lines.push(...hinted)
|
|
61
|
+
lines.push("")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return lines
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderEfficiencySteps(startNum: number): { lines: string[], nextNum: number } {
|
|
68
|
+
const lines: string[] = []
|
|
69
|
+
let n = startNum
|
|
70
|
+
if (getEfficiencySkills().length > 0) {
|
|
71
|
+
lines.push(`${n++}. 按已加载的效率 skill 中的工具可用性检测步骤确认代码探索工具就绪(含索引初始化)`)
|
|
72
|
+
lines.push(`${n++}. 初始化仅是前置——本 session 后续所有代码探索操作须持续遵循已加载效率 skill 中的工具选择规则,不可将初始化视为终点而恢复默认工具习惯`)
|
|
73
|
+
lines.push(`${n++}. 提交前自查:本 step 涉及的代码探索(符号定位/调用关系/影响评估)是否遵循已加载效率 skill 的工具选择规则,未遵循须补做`)
|
|
74
|
+
}
|
|
75
|
+
return { lines, nextNum: n }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** worktree 就绪判定:taskGroup 投影的 worktreePath 非空即就绪(与 submit 硬门禁的 metadata.worktree_path 同源)。 */
|
|
79
|
+
export function isWorktreeReady(tg: TaskGroupState): boolean {
|
|
80
|
+
return Boolean(tg.worktreePath)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 统一 worktree 未就绪提示文案(renderWorktreeSection / orchestrator 分派视图 / 子代理拒绝执行视图共用)。 */
|
|
84
|
+
export function renderWorktreeNotReady(): string[] {
|
|
85
|
+
return ["- (worktree 未就绪 — 请编排者先调用 opx_orch_set_worktree)"]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 状态异常(phase ↔ step 归属错位)诊断行:subagent 拒绝视图与 orchestrator 分派视图共用。 */
|
|
89
|
+
export function renderStateMismatchDiagnostic(
|
|
90
|
+
phase: string,
|
|
91
|
+
currentStep: string | null,
|
|
92
|
+
expectedPhase: string | null,
|
|
93
|
+
): string[] {
|
|
94
|
+
return [
|
|
95
|
+
`- **当前阶段 (phase)**: \`${phase}\``,
|
|
96
|
+
`- **当前 step**: \`${currentStep ?? "(无)"}\``,
|
|
97
|
+
`- **step 预期归属阶段**: \`${expectedPhase ?? "(未知)"}\``,
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function renderWorktreeSection(
|
|
102
|
+
state: OrchestrateState,
|
|
103
|
+
tg: TaskGroupState,
|
|
104
|
+
opts?: { showNamespace?: boolean; showPort?: boolean },
|
|
105
|
+
): string[] {
|
|
106
|
+
const lines: string[] = []
|
|
107
|
+
lines.push("## Worktree", "")
|
|
108
|
+
if (tg.worktreePath) {
|
|
109
|
+
lines.push(`- **路径**: \`${tg.worktreePath}\``)
|
|
110
|
+
lines.push(`- **分支**: \`${tg.branchName || "(none)"}\``)
|
|
111
|
+
if (tg.baseRef) lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\` 查询本 change 全部已提交变更文件`)
|
|
112
|
+
lines.push("- **⚠️ 约束**: 所有读写和 git 操作均在此目录下进行;严禁直接修改主仓库/主分支路径下的文件(如 `<repo>/openspec/...`)")
|
|
113
|
+
lines.push("- **路径解析**: 推荐阅读文档均为相对 worktree 路径的引用,一律以 worktree 路径为基准解析,禁止从主仓库根目录解析")
|
|
114
|
+
} else {
|
|
115
|
+
lines.push(...renderWorktreeNotReady())
|
|
116
|
+
}
|
|
117
|
+
if (opts?.showNamespace) {
|
|
118
|
+
lines.push(`- **隔离标识**: \`${state.isolationNamespace}\``)
|
|
119
|
+
if (opts?.showPort) {
|
|
120
|
+
lines.push(` - 建议端口: ${derivePortFromNamespace(state.isolationNamespace)}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
lines.push("")
|
|
124
|
+
return lines
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function formatFilePath(file: string, line: number, maxLen = 60): string {
|
|
128
|
+
const suffix = line > 0 ? `:${line}` : ""
|
|
129
|
+
const full = `${file}${suffix}`
|
|
130
|
+
if (full.length <= maxLen) return full
|
|
131
|
+
const parts = file.split("/")
|
|
132
|
+
if (parts.length <= 1) return full.slice(0, maxLen - 3) + "..."
|
|
133
|
+
const base = parts[parts.length - 1]
|
|
134
|
+
const parent = parts[parts.length - 2]
|
|
135
|
+
const tryTwo = `.../${parent}/${base}${suffix}`
|
|
136
|
+
if (tryTwo.length <= maxLen) return tryTwo
|
|
137
|
+
const lastSeg = `${base}${suffix}`
|
|
138
|
+
if (lastSeg.length <= maxLen) return lastSeg
|
|
139
|
+
return lastSeg.slice(0, maxLen - 3) + "..."
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function renderTaskItem(t: TaskItem): string {
|
|
143
|
+
const trace = t.specTrace ? ` [spec:${t.specTrace}]` : ""
|
|
144
|
+
return `- Task id=${t.id} | ${t.title}${trace}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function formatSeverity(severity: string): string {
|
|
148
|
+
switch (severity) {
|
|
149
|
+
case "Critical": return `**${severity}**`
|
|
150
|
+
case "High": return `**${severity}**`
|
|
151
|
+
case "Medium": return `*${severity}*`
|
|
152
|
+
case "Low": return severity
|
|
153
|
+
case "Info": return `\`${severity}\``
|
|
154
|
+
default: return severity
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 上轮会话摘要渲染。参数为 agentSummaries 记录(workItems 单轨下直接读 item.metadata["agent_summaries"])。
|
|
159
|
+
* 按调用者角色隔离(409c411 意图):只渲染当前 agent 自己的摘要,不跨 agent 传递。 */
|
|
160
|
+
export function renderAgentSummaries(agentSummaries: Record<string, string> | undefined, agentName: string): string[] {
|
|
161
|
+
if (!agentSummaries || !agentSummaries[agentName]) return []
|
|
162
|
+
const lines: string[] = ["## 上轮会话摘要", ""]
|
|
163
|
+
lines.push(`- **${agentName}**:${agentSummaries[agentName]}`)
|
|
164
|
+
lines.push("")
|
|
165
|
+
return lines
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── 占位符插值层 ───
|
|
169
|
+
|
|
170
|
+
/** 插值白名单:仅这些 key 可被 {{key}} 占位符引用,其余一律保留原文(防配置注入任意动态值)。 */
|
|
171
|
+
const INTERPOLATION_ALLOWED_KEYS = new Set([
|
|
172
|
+
"worktree_path",
|
|
173
|
+
"change_id",
|
|
174
|
+
"step_id",
|
|
175
|
+
"phase",
|
|
176
|
+
"agent",
|
|
177
|
+
"allowed_directories",
|
|
178
|
+
"allowed_packages",
|
|
179
|
+
"notes",
|
|
180
|
+
"base_branch",
|
|
181
|
+
])
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 占位符插值:将 text 中的 `{{key}}` 替换为 ctx[key]。
|
|
185
|
+
* - key 不在白名单:保留原文(原样返回占位符)
|
|
186
|
+
* - ctx 缺值:保留原文(降级,不抛错、不阻断渲染)
|
|
187
|
+
*/
|
|
188
|
+
export function interpolateText(text: string, ctx: Record<string, string>): string {
|
|
189
|
+
return text.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (raw, key: string) => {
|
|
190
|
+
if (!INTERPOLATION_ALLOWED_KEYS.has(key)) return raw
|
|
191
|
+
const value = ctx[key]
|
|
192
|
+
return value === undefined ? raw : value
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import type { WorkItem } from "./types.ts"
|
|
4
|
+
import { createInitialWorkItem } from "./engine.ts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 收集器统一接入接口(spec workflow-collectors)。
|
|
8
|
+
* 生命周期:pull 拉取外部原始项 → transform 产出初始 WorkItem(phase=todo、
|
|
9
|
+
* suspended=false、children 为空、tags 为空)→ writeback 将处理结果回写外部源。
|
|
10
|
+
*/
|
|
11
|
+
export interface CollectorAdapter {
|
|
12
|
+
readonly name: string
|
|
13
|
+
pollIntervalMs: number
|
|
14
|
+
pull(): Promise<unknown[]>
|
|
15
|
+
transform(raw: unknown[]): WorkItem[]
|
|
16
|
+
writeback(item: WorkItem, payload: unknown): Promise<{ success: boolean; error?: string }>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OpenSpecCollectorOptions {
|
|
20
|
+
openspecDir: string
|
|
21
|
+
pollIntervalMs?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** OpenSpec change 原始扫描项:change 目录名 + proposal.md 全文。 */
|
|
25
|
+
export interface OpenSpecChangeRef {
|
|
26
|
+
changeName: string
|
|
27
|
+
proposalText: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_POLL_INTERVAL_MS = 30_000
|
|
31
|
+
const SOURCE = "openspec"
|
|
32
|
+
const CHANGE_LABEL = "openspec-change"
|
|
33
|
+
|
|
34
|
+
/** 提取 proposal.md 一级标题作为 title;无一级标题时回退到 change 目录名。 */
|
|
35
|
+
function extractTitle(proposalText: string, fallback: string): string {
|
|
36
|
+
const heading = /^#\s+(.+)$/m.exec(proposalText)
|
|
37
|
+
const title = heading?.[1]?.trim()
|
|
38
|
+
return title ? title : fallback
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 提取 proposal.md 的 Why 段正文作为摘要;无 Why 段时返回空串。 */
|
|
42
|
+
function extractSummary(proposalText: string): string {
|
|
43
|
+
const lines = proposalText.split(/\r?\n/)
|
|
44
|
+
const whyIndex = lines.findIndex((line) => /^##\s+Why\s*$/.test(line.trim()))
|
|
45
|
+
if (whyIndex === -1) return ""
|
|
46
|
+
const body: string[] = []
|
|
47
|
+
for (let i = whyIndex + 1; i < lines.length; i++) {
|
|
48
|
+
if (/^##\s+/.test(lines[i])) break
|
|
49
|
+
const trimmed = lines[i].trim()
|
|
50
|
+
if (trimmed !== "") body.push(trimmed)
|
|
51
|
+
}
|
|
52
|
+
return body.join(" ")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* OpenSpec 收集器:pull 扫描 {openspecDir}/changes 下含 proposal.md 的 change 子目录
|
|
57
|
+
* 返回原始 ref,transform 将其转为 task WorkItem。每次 pull 重新扫描磁盘,幂等且不抛错。
|
|
58
|
+
*/
|
|
59
|
+
export class OpenSpecCollector implements CollectorAdapter {
|
|
60
|
+
readonly name = SOURCE
|
|
61
|
+
readonly openspecDir: string
|
|
62
|
+
readonly pollIntervalMs: number
|
|
63
|
+
|
|
64
|
+
constructor(options: OpenSpecCollectorOptions) {
|
|
65
|
+
this.openspecDir = options.openspecDir
|
|
66
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async pull(): Promise<OpenSpecChangeRef[]> {
|
|
70
|
+
const changesDir = join(this.openspecDir, "changes")
|
|
71
|
+
let entries
|
|
72
|
+
try {
|
|
73
|
+
entries = readdirSync(changesDir, { withFileTypes: true })
|
|
74
|
+
} catch {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const refs: OpenSpecChangeRef[] = []
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (!entry.isDirectory()) continue
|
|
81
|
+
try {
|
|
82
|
+
const proposalText = readFileSync(join(changesDir, entry.name, "proposal.md"), "utf8")
|
|
83
|
+
refs.push({ changeName: entry.name, proposalText })
|
|
84
|
+
} catch {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return refs
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
transform(raw: unknown[]): WorkItem[] {
|
|
92
|
+
return raw.map((ref) => {
|
|
93
|
+
const { changeName, proposalText } = ref as OpenSpecChangeRef
|
|
94
|
+
return createInitialWorkItem({
|
|
95
|
+
id: `${SOURCE}:${changeName}`,
|
|
96
|
+
source: SOURCE,
|
|
97
|
+
externalId: changeName,
|
|
98
|
+
type: "task",
|
|
99
|
+
title: extractTitle(proposalText, changeName),
|
|
100
|
+
description: extractSummary(proposalText) || changeName,
|
|
101
|
+
labels: [CHANGE_LABEL],
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async writeback(_item: WorkItem, _payload: unknown): Promise<{ success: boolean; error?: string }> {
|
|
107
|
+
return { success: true }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* ADO 收集器占位实现(spec workflow-collectors:占位拉取不报错)。
|
|
113
|
+
* pull/transform 返回空,为后续接入真实 ADO 预留接口,引擎调度不受影响。
|
|
114
|
+
*/
|
|
115
|
+
export class AdoCollector implements CollectorAdapter {
|
|
116
|
+
readonly name = "ado"
|
|
117
|
+
readonly pollIntervalMs = DEFAULT_POLL_INTERVAL_MS
|
|
118
|
+
|
|
119
|
+
async pull(): Promise<unknown[]> {
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
transform(_raw: unknown[]): WorkItem[] {
|
|
124
|
+
return []
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async writeback(_item: WorkItem, _payload: unknown): Promise<{ success: boolean; error?: string }> {
|
|
128
|
+
return { success: true }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── 自定义 adapter 注册表(静态注册,spec 设计决策取静态而非目录扫描)───
|
|
133
|
+
|
|
134
|
+
const registry: CollectorAdapter[] = []
|
|
135
|
+
|
|
136
|
+
/** 注册自定义收集器;同名 adapter 重复注册幂等跳过。 */
|
|
137
|
+
export function registerCollector(adapter: CollectorAdapter): void {
|
|
138
|
+
if (registry.some((a) => a.name === adapter.name)) return
|
|
139
|
+
registry.push(adapter)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 返回已注册收集器副本(含内置 openspec/ado 占位)。 */
|
|
143
|
+
export function getCollectors(): CollectorAdapter[] {
|
|
144
|
+
return [...registry]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 清空注册表(测试隔离用,非编排业务接口)。 */
|
|
148
|
+
export function __resetCollectors(): void {
|
|
149
|
+
registry.length = 0
|
|
150
|
+
}
|