@peterxiaoyang/superspec 0.1.14 → 0.1.16-alpha
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 +37 -36
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +301 -0
- package/dist/format.d.ts +29 -0
- package/dist/format.js +141 -0
- package/dist/install.d.ts +18 -0
- package/dist/install.js +161 -0
- package/dist/next.d.ts +3 -0
- package/dist/next.js +161 -0
- package/dist/openspec.d.ts +9 -0
- package/dist/openspec.js +41 -0
- package/dist/record.d.ts +20 -0
- package/dist/record.js +266 -0
- package/dist/skill_loop.d.ts +23 -0
- package/dist/skill_loop.js +58 -0
- package/dist/store.d.ts +31 -0
- package/dist/store.js +213 -0
- package/dist/sync.d.ts +3 -0
- package/dist/sync.js +135 -0
- package/dist/task.d.ts +7 -0
- package/dist/task.js +44 -0
- package/dist/transition.d.ts +34 -0
- package/dist/transition.js +400 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.js +3 -0
- package/package.json +8 -53
- package/templates/workflow/agents/architect.toml +13 -0
- package/{adapters/codex → templates/workflow}/agents/code-reviewer.toml +2 -2
- package/templates/workflow/agents/critic.toml +13 -0
- package/{adapters/codex → templates/workflow}/agents/executor.toml +2 -2
- package/{adapters/codex → templates/workflow}/agents/explore.toml +3 -3
- package/templates/workflow/agents/final-audit.toml +13 -0
- package/templates/workflow/agents/proposal-auditor.toml +13 -0
- package/{adapters/codex → templates/workflow}/agents/test-engineer.toml +4 -4
- package/{adapters/codex → templates/workflow}/agents/test-runner.toml +2 -2
- package/{adapters/codex → templates/workflow}/agents/verifier.toml +2 -2
- package/templates/workflow/prompts/architect.md +16 -0
- package/templates/workflow/prompts/critic.md +16 -0
- package/templates/workflow/prompts/explore.md +1 -1
- package/templates/workflow/prompts/final-audit.md +32 -0
- package/templates/workflow/prompts/proposal-auditor.md +34 -0
- package/templates/workflow/prompts/test-engineer.md +16 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +39 -121
- package/templates/workflow/skills/superspec-archive/SKILL.md +17 -49
- package/templates/workflow/skills/superspec-explore/SKILL.md +34 -70
- package/templates/workflow/skills/superspec-propose/SKILL.md +57 -60
- package/templates/workflow/skills/superspec-review/SKILL.md +18 -85
- package/adapters/codex/agents/architect.toml +0 -13
- package/adapters/codex/agents/critic.toml +0 -13
- package/adapters/codex/install-map.json +0 -116
- package/bin/launch.js +0 -37
- package/bin/superspec-check.js +0 -4
- package/bin/superspec-hook.js +0 -4
- package/bin/superspec-init.js +0 -4
- package/bin/superspec.js +0 -4
- package/dist/src/apply_worker_chain.d.ts +0 -60
- package/dist/src/apply_worker_chain.js +0 -1283
- package/dist/src/apply_worker_chain_lifecycle.d.ts +0 -21
- package/dist/src/apply_worker_chain_lifecycle.js +0 -409
- package/dist/src/archive.d.ts +0 -23
- package/dist/src/archive.js +0 -430
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +0 -33
- package/dist/src/cli_args.d.ts +0 -32
- package/dist/src/cli_args.js +0 -454
- package/dist/src/core.d.ts +0 -20
- package/dist/src/core.js +0 -394
- package/dist/src/disclosure.d.ts +0 -35
- package/dist/src/disclosure.js +0 -671
- package/dist/src/doctor.d.ts +0 -44
- package/dist/src/doctor.js +0 -230
- package/dist/src/evidence.d.ts +0 -29
- package/dist/src/evidence.js +0 -1107
- package/dist/src/gates.d.ts +0 -19
- package/dist/src/gates.js +0 -1631
- package/dist/src/git.d.ts +0 -8
- package/dist/src/git.js +0 -112
- package/dist/src/hooks/adapter.d.ts +0 -5
- package/dist/src/hooks/adapter.js +0 -347
- package/dist/src/hooks/guard_api.d.ts +0 -12
- package/dist/src/hooks/guard_api.js +0 -797
- package/dist/src/hooks/health.d.ts +0 -4
- package/dist/src/hooks/health.js +0 -86
- package/dist/src/hooks/policy_event.d.ts +0 -41
- package/dist/src/hooks/policy_event.js +0 -2634
- package/dist/src/hooks/types.d.ts +0 -72
- package/dist/src/hooks/types.js +0 -1
- package/dist/src/hooks/validation.d.ts +0 -3
- package/dist/src/hooks/validation.js +0 -70
- package/dist/src/i18n.d.ts +0 -21
- package/dist/src/i18n.js +0 -720
- package/dist/src/init_cli.d.ts +0 -28
- package/dist/src/init_cli.js +0 -404
- package/dist/src/install_engine.d.ts +0 -71
- package/dist/src/install_engine.js +0 -538
- package/dist/src/invariants.d.ts +0 -16
- package/dist/src/invariants.js +0 -363
- package/dist/src/openspec.d.ts +0 -37
- package/dist/src/openspec.js +0 -274
- package/dist/src/packet_render.d.ts +0 -5
- package/dist/src/packet_render.js +0 -1767
- package/dist/src/packet_schema.d.ts +0 -59
- package/dist/src/packet_schema.js +0 -1
- package/dist/src/paths.d.ts +0 -22
- package/dist/src/paths.js +0 -203
- package/dist/src/project_init.d.ts +0 -33
- package/dist/src/project_init.js +0 -191
- package/dist/src/self_update.d.ts +0 -14
- package/dist/src/self_update.js +0 -56
- package/dist/src/state.d.ts +0 -37
- package/dist/src/state.js +0 -464
- package/dist/src/tasks.d.ts +0 -38
- package/dist/src/tasks.js +0 -373
- package/dist/src/util.d.ts +0 -163
- package/dist/src/util.js +0 -792
- package/dist/superspec.d.ts +0 -6
- package/dist/superspec.js +0 -97
- package/dist/superspec_guard.d.ts +0 -4
- package/dist/superspec_guard.js +0 -19
- package/dist/superspec_hook.d.ts +0 -4
- package/dist/superspec_hook.js +0 -35
- package/dist/superspec_init.d.ts +0 -2
- package/dist/superspec_init.js +0 -17
- package/schemas/hook-event.schema.json +0 -27
- package/schemas/install-manifest.schema.json +0 -97
- package/templates/hooks/codex-hooks.json +0 -35
- package/templates/sidecar/archive-preservation.json +0 -11
- package/templates/sidecar/business-invariants.md +0 -38
- package/templates/sidecar/config.yaml +0 -13
- package/templates/sidecar/discovery.md +0 -33
- package/templates/sidecar/test-contract.md +0 -27
package/dist/install.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const WORKFLOW_SKILLS = [
|
|
4
|
+
"superspec-explore",
|
|
5
|
+
"superspec-propose",
|
|
6
|
+
"superspec-apply",
|
|
7
|
+
"superspec-review",
|
|
8
|
+
"superspec-archive",
|
|
9
|
+
];
|
|
10
|
+
export const WORKFLOW_PROMPTS = [
|
|
11
|
+
"architect.md",
|
|
12
|
+
"code-reviewer.md",
|
|
13
|
+
"critic.md",
|
|
14
|
+
"executor.md",
|
|
15
|
+
"explore.md",
|
|
16
|
+
"final-audit.md",
|
|
17
|
+
"proposal-auditor.md",
|
|
18
|
+
"test-engineer.md",
|
|
19
|
+
"test-runner.md",
|
|
20
|
+
"verifier.md",
|
|
21
|
+
];
|
|
22
|
+
export const WORKFLOW_AGENTS = [
|
|
23
|
+
"architect.toml",
|
|
24
|
+
"code-reviewer.toml",
|
|
25
|
+
"critic.toml",
|
|
26
|
+
"executor.toml",
|
|
27
|
+
"explore.toml",
|
|
28
|
+
"final-audit.toml",
|
|
29
|
+
"proposal-auditor.toml",
|
|
30
|
+
"test-engineer.toml",
|
|
31
|
+
"test-runner.toml",
|
|
32
|
+
"verifier.toml",
|
|
33
|
+
];
|
|
34
|
+
function defaultTemplateRoot() {
|
|
35
|
+
return join(import.meta.dirname, "..", "templates", "workflow");
|
|
36
|
+
}
|
|
37
|
+
function legacyStateFound(projectRoot) {
|
|
38
|
+
const changesRoot = join(projectRoot, "openspec", "changes");
|
|
39
|
+
if (!existsSync(changesRoot))
|
|
40
|
+
return false;
|
|
41
|
+
const oldStateFiles = ["superspec-state.json", "superspec-state.lock", "ledger.jsonl"];
|
|
42
|
+
return oldStateFiles.some(file => readdirSync(changesRoot).some(change => existsSync(join(changesRoot, change, ".superspec", file))));
|
|
43
|
+
}
|
|
44
|
+
function assertWorkflowTemplates(templateRoot) {
|
|
45
|
+
const missing = [];
|
|
46
|
+
for (const skill of WORKFLOW_SKILLS) {
|
|
47
|
+
const file = join(templateRoot, "skills", skill, "SKILL.md");
|
|
48
|
+
if (!existsSync(file))
|
|
49
|
+
missing.push(file);
|
|
50
|
+
}
|
|
51
|
+
for (const prompt of WORKFLOW_PROMPTS) {
|
|
52
|
+
const file = join(templateRoot, "prompts", prompt);
|
|
53
|
+
if (!existsSync(file))
|
|
54
|
+
missing.push(file);
|
|
55
|
+
}
|
|
56
|
+
for (const agent of WORKFLOW_AGENTS) {
|
|
57
|
+
const file = join(templateRoot, "agents", agent);
|
|
58
|
+
if (!existsSync(file))
|
|
59
|
+
missing.push(file);
|
|
60
|
+
}
|
|
61
|
+
if (missing.length > 0) {
|
|
62
|
+
throw new Error(`workflow templates missing: ${missing.join(", ")}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function copySkills(templateRoot, projectRoot) {
|
|
66
|
+
const skillsDest = join(projectRoot, ".codex", "skills");
|
|
67
|
+
const installedSkills = [];
|
|
68
|
+
for (const skill of WORKFLOW_SKILLS) {
|
|
69
|
+
const src = join(templateRoot, "skills", skill, "SKILL.md");
|
|
70
|
+
mkdirSync(join(skillsDest, skill), { recursive: true });
|
|
71
|
+
copyFileSync(src, join(skillsDest, skill, "SKILL.md"));
|
|
72
|
+
installedSkills.push(skill);
|
|
73
|
+
}
|
|
74
|
+
return installedSkills;
|
|
75
|
+
}
|
|
76
|
+
function copyPrompts(templateRoot, projectRoot) {
|
|
77
|
+
const promptsDest = join(projectRoot, ".codex", "prompts");
|
|
78
|
+
const installedPrompts = [];
|
|
79
|
+
for (const prompt of WORKFLOW_PROMPTS) {
|
|
80
|
+
const src = join(templateRoot, "prompts", prompt);
|
|
81
|
+
mkdirSync(promptsDest, { recursive: true });
|
|
82
|
+
copyFileSync(src, join(promptsDest, prompt));
|
|
83
|
+
installedPrompts.push(prompt);
|
|
84
|
+
}
|
|
85
|
+
return installedPrompts;
|
|
86
|
+
}
|
|
87
|
+
function copyAgents(templateRoot, projectRoot) {
|
|
88
|
+
const agentsDest = join(projectRoot, ".codex", "agents");
|
|
89
|
+
const installedAgents = [];
|
|
90
|
+
for (const agent of WORKFLOW_AGENTS) {
|
|
91
|
+
const src = join(templateRoot, "agents", agent);
|
|
92
|
+
mkdirSync(agentsDest, { recursive: true });
|
|
93
|
+
copyFileSync(src, join(agentsDest, agent));
|
|
94
|
+
installedAgents.push(agent);
|
|
95
|
+
}
|
|
96
|
+
return installedAgents;
|
|
97
|
+
}
|
|
98
|
+
function insertMissingTableEntries(content, table, entries) {
|
|
99
|
+
const tablePattern = new RegExp(`^\\[${table}\\]\\s*$`, "m");
|
|
100
|
+
const tableMatch = tablePattern.exec(content);
|
|
101
|
+
const lines = Object.entries(entries).map(([key, value]) => `${key} = ${value}`);
|
|
102
|
+
if (!tableMatch) {
|
|
103
|
+
const separator = content.trim().length === 0 ? "" : "\n\n";
|
|
104
|
+
return `${content.trimEnd()}${separator}[${table}]\n${lines.join("\n")}\n`;
|
|
105
|
+
}
|
|
106
|
+
const tableStart = tableMatch.index;
|
|
107
|
+
const headerEnd = tableStart + tableMatch[0].length;
|
|
108
|
+
const nextTable = /^\[[^\]]+\]\s*$/m.exec(content.slice(headerEnd));
|
|
109
|
+
const tableEnd = nextTable ? headerEnd + nextTable.index : content.length;
|
|
110
|
+
const tableBody = content.slice(headerEnd, tableEnd);
|
|
111
|
+
const missing = lines.filter(line => {
|
|
112
|
+
const key = line.split(" = ", 1)[0];
|
|
113
|
+
return !new RegExp(`^\\s*${key}\\s*=`, "m").test(tableBody);
|
|
114
|
+
});
|
|
115
|
+
if (missing.length === 0)
|
|
116
|
+
return content;
|
|
117
|
+
return `${content.slice(0, headerEnd)}\n${missing.join("\n")}${content.slice(headerEnd)}`;
|
|
118
|
+
}
|
|
119
|
+
function ensureCodexConfig(projectRoot) {
|
|
120
|
+
const codexDir = join(projectRoot, ".codex");
|
|
121
|
+
const configPath = join(codexDir, "config.toml");
|
|
122
|
+
mkdirSync(codexDir, { recursive: true });
|
|
123
|
+
const current = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
124
|
+
let next = insertMissingTableEntries(current, "features", {
|
|
125
|
+
multi_agent: "true",
|
|
126
|
+
child_agents_md: "true",
|
|
127
|
+
});
|
|
128
|
+
next = insertMissingTableEntries(next, "agents", {
|
|
129
|
+
max_threads: "12",
|
|
130
|
+
max_depth: "1",
|
|
131
|
+
});
|
|
132
|
+
if (next !== current)
|
|
133
|
+
writeFileSync(configPath, next);
|
|
134
|
+
return ".codex/config.toml";
|
|
135
|
+
}
|
|
136
|
+
export function installProject(projectRoot, options = {}) {
|
|
137
|
+
if (legacyStateFound(projectRoot)) {
|
|
138
|
+
throw new Error("检测到老版 SuperSpec (0.x) 的状态文件。\n" +
|
|
139
|
+
"SuperSpec 0.1.16-alpha 是全新引擎,不兼容 0.x 的状态格式。\n" +
|
|
140
|
+
"请先用老版(0.1.x)完成或归档现有 change,再安装 0.1.16-alpha。\n" +
|
|
141
|
+
"或在全新项目目录中安装。");
|
|
142
|
+
}
|
|
143
|
+
const templateRoot = options.templateRoot ?? defaultTemplateRoot();
|
|
144
|
+
assertWorkflowTemplates(templateRoot);
|
|
145
|
+
const engineDir = join(projectRoot, ".superspec");
|
|
146
|
+
mkdirSync(join(engineDir, "changes"), { recursive: true });
|
|
147
|
+
const gitignorePath = join(engineDir, ".gitignore");
|
|
148
|
+
if (!existsSync(gitignorePath))
|
|
149
|
+
writeFileSync(gitignorePath, "changes/\n*.log\n*.tmp\n");
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
message: "SuperSpec 0.1.16-alpha 已安装",
|
|
153
|
+
installed: {
|
|
154
|
+
engine_dir: ".superspec/",
|
|
155
|
+
skills: copySkills(templateRoot, projectRoot),
|
|
156
|
+
prompts: copyPrompts(templateRoot, projectRoot),
|
|
157
|
+
agents: copyAgents(templateRoot, projectRoot),
|
|
158
|
+
config: ensureCodexConfig(projectRoot),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
package/dist/next.d.ts
ADDED
package/dist/next.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// SuperSpec 流程引擎 — next:返回可执行路径
|
|
2
|
+
import { rebuildSnapshot } from "./sync.js";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { validateDiscovery, countDiscoveryOpenQuestions } from "./format.js";
|
|
6
|
+
function packetCommand(change, jobId) {
|
|
7
|
+
return `superspec jobs packet --change "${change}" --job "${jobId}"`;
|
|
8
|
+
}
|
|
9
|
+
function transitionCommand(change, name, extra = "") {
|
|
10
|
+
return `superspec transition ${name} --change "${change}"${extra ? " " + extra : ""}`;
|
|
11
|
+
}
|
|
12
|
+
/** next 命令:读 snapshot,返回唯一可执行路径 */
|
|
13
|
+
export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
14
|
+
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
15
|
+
switch (snapshot.state) {
|
|
16
|
+
case "init":
|
|
17
|
+
return {
|
|
18
|
+
state: "init",
|
|
19
|
+
path: "next_command",
|
|
20
|
+
next_command: transitionCommand(change, "explore"),
|
|
21
|
+
reason: "初始化完成,开始探索",
|
|
22
|
+
missing_inputs: [],
|
|
23
|
+
};
|
|
24
|
+
case "explore": {
|
|
25
|
+
// Phase 2:检查 discovery.md
|
|
26
|
+
const discoveryCheck = validateDiscovery(changeRoot);
|
|
27
|
+
if (!discoveryCheck.ok) {
|
|
28
|
+
const ask = {
|
|
29
|
+
question: discoveryCheck.message + ",请处理后继续",
|
|
30
|
+
allowed_answers: ["已处理"],
|
|
31
|
+
scope: "explore_discovery",
|
|
32
|
+
};
|
|
33
|
+
return { state: "explore", path: "ask_user", ask_user: ask, reason: discoveryCheck.message };
|
|
34
|
+
}
|
|
35
|
+
// 检查"待确认问题"段(用 format.ts 的段感知解析)
|
|
36
|
+
const content = readFileSync(join(changeRoot, ".superspec", "artifacts", "discovery.md"), "utf8");
|
|
37
|
+
const openQs = countDiscoveryOpenQuestions(content);
|
|
38
|
+
if (openQs > 0) {
|
|
39
|
+
const ask = {
|
|
40
|
+
question: `discovery.md 有 ${openQs} 个未解决的待确认问题,请逐个确认`,
|
|
41
|
+
allowed_answers: ["所有问题已确认"],
|
|
42
|
+
scope: "explore_open_questions",
|
|
43
|
+
};
|
|
44
|
+
return { state: "explore", path: "ask_user", ask_user: ask, reason: `有 ${openQs} 个未确认问题` };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
state: "explore",
|
|
48
|
+
path: "next_command",
|
|
49
|
+
next_command: transitionCommand(change, "explore", `--risk ${defaultRisk}`),
|
|
50
|
+
reason: "探索完成,推进到计划阶段",
|
|
51
|
+
missing_inputs: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
case "propose": {
|
|
55
|
+
if (snapshot.open_jobs.length > 0) {
|
|
56
|
+
const jobs = snapshot.open_jobs.map(j => ({
|
|
57
|
+
job_id: j.job_id,
|
|
58
|
+
role: j.role,
|
|
59
|
+
packet_command: packetCommand(change, j.job_id),
|
|
60
|
+
}));
|
|
61
|
+
return {
|
|
62
|
+
state: "propose",
|
|
63
|
+
path: "required_job",
|
|
64
|
+
required_jobs: jobs,
|
|
65
|
+
reason: `有 ${jobs.length} 个待完成工作项`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const riskFlag = `--risk ${defaultRisk}`;
|
|
69
|
+
return {
|
|
70
|
+
state: "propose",
|
|
71
|
+
path: "next_command",
|
|
72
|
+
next_command: transitionCommand(change, "propose-ready", riskFlag),
|
|
73
|
+
reason: "计划文档就绪,提交 propose-ready",
|
|
74
|
+
missing_inputs: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "propose_ready":
|
|
78
|
+
return {
|
|
79
|
+
state: "propose_ready",
|
|
80
|
+
path: "next_command",
|
|
81
|
+
next_command: transitionCommand(change, "start-apply"),
|
|
82
|
+
reason: "计划就绪,开始执行",
|
|
83
|
+
missing_inputs: [],
|
|
84
|
+
};
|
|
85
|
+
case "apply": {
|
|
86
|
+
// 有 open job → 做
|
|
87
|
+
if (snapshot.open_jobs.length > 0) {
|
|
88
|
+
return {
|
|
89
|
+
state: "apply",
|
|
90
|
+
path: "required_job",
|
|
91
|
+
required_jobs: snapshot.open_jobs.map(j => ({ job_id: j.job_id, role: j.role, packet_command: packetCommand(change, j.job_id) })),
|
|
92
|
+
reason: `有 ${snapshot.open_jobs.length} 个待完成工作项`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// 检查是否所有任务已完成
|
|
96
|
+
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
97
|
+
const allDone = !tasksContent.split("\n").some(l => l.includes("- [ ]"));
|
|
98
|
+
if (allDone) {
|
|
99
|
+
return {
|
|
100
|
+
state: "apply",
|
|
101
|
+
path: "next_command",
|
|
102
|
+
next_command: transitionCommand(change, "review-ready"),
|
|
103
|
+
reason: "所有任务完成,进入审查",
|
|
104
|
+
missing_inputs: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
state: "apply",
|
|
109
|
+
path: "next_command",
|
|
110
|
+
next_command: transitionCommand(change, "task-start", "--task TASK-XXX"),
|
|
111
|
+
reason: "执行中:查看 tasks.md 找下一个未完成任务",
|
|
112
|
+
missing_inputs: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
case "apply_done": {
|
|
116
|
+
if (snapshot.open_jobs.length > 0) {
|
|
117
|
+
return {
|
|
118
|
+
state: "apply_done",
|
|
119
|
+
path: "required_job",
|
|
120
|
+
required_jobs: snapshot.open_jobs.map(j => ({ job_id: j.job_id, role: j.role, packet_command: packetCommand(change, j.job_id) })),
|
|
121
|
+
reason: `有 ${snapshot.open_jobs.length} 个待完成工作项`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
state: "apply_done",
|
|
126
|
+
path: "next_command",
|
|
127
|
+
next_command: transitionCommand(change, "review-ready"),
|
|
128
|
+
reason: "所有任务完成,进入审查",
|
|
129
|
+
missing_inputs: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
case "review":
|
|
133
|
+
return {
|
|
134
|
+
state: "review",
|
|
135
|
+
path: "next_command",
|
|
136
|
+
next_command: transitionCommand(change, "accept"),
|
|
137
|
+
reason: "审查完成,提交接受",
|
|
138
|
+
missing_inputs: [],
|
|
139
|
+
};
|
|
140
|
+
case "accepted":
|
|
141
|
+
return {
|
|
142
|
+
state: "accepted",
|
|
143
|
+
path: "next_command",
|
|
144
|
+
next_command: transitionCommand(change, "archive"),
|
|
145
|
+
reason: "审查通过,提交归档",
|
|
146
|
+
missing_inputs: [],
|
|
147
|
+
};
|
|
148
|
+
case "archive":
|
|
149
|
+
return {
|
|
150
|
+
state: "archive",
|
|
151
|
+
path: "done",
|
|
152
|
+
reason: "已归档,流程完成。",
|
|
153
|
+
};
|
|
154
|
+
default:
|
|
155
|
+
return {
|
|
156
|
+
state: snapshot.state,
|
|
157
|
+
path: "done",
|
|
158
|
+
reason: `状态 ${snapshot.state} 超出 Phase 1 范围`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface OpenSpecProbe {
|
|
2
|
+
available: boolean;
|
|
3
|
+
version: string | null;
|
|
4
|
+
changeExists: boolean;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function probeOpenSpec(projectRoot: string, change?: string): OpenSpecProbe;
|
|
8
|
+
export declare function openspecStatus(projectRoot: string, change: string): string;
|
|
9
|
+
export declare function changeRoot(projectRoot: string, change: string): string;
|
package/dist/openspec.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SuperSpec 流程引擎 — OpenSpec 探测
|
|
2
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
/** B2 修复:校验 change 字符集,防 shell 注入 */
|
|
7
|
+
function validateChange(change) {
|
|
8
|
+
if (!/^[A-Za-z0-9._-]+$/.test(change)) {
|
|
9
|
+
throw new Error(`change 名称含非法字符:${change}(只允许字母数字._-)`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function probeOpenSpec(projectRoot, change) {
|
|
13
|
+
try {
|
|
14
|
+
const version = execSync("openspec --version 2>/dev/null", { encoding: "utf8" }).trim();
|
|
15
|
+
let changeExists = false;
|
|
16
|
+
if (change) {
|
|
17
|
+
validateChange(change);
|
|
18
|
+
changeExists = existsSync(join(projectRoot, "openspec", "changes", change));
|
|
19
|
+
}
|
|
20
|
+
return { available: true, version, changeExists };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { available: false, version: null, changeExists: false, error: "openspec CLI 不可用" };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function openspecStatus(projectRoot, change) {
|
|
27
|
+
validateChange(change);
|
|
28
|
+
try {
|
|
29
|
+
// B2 修复:用 execFileSync 数组传参,彻底避免 shell 解析
|
|
30
|
+
const result = execFileSync("openspec", ["status", "--change", change, "--json"], {
|
|
31
|
+
encoding: "utf8", cwd: projectRoot, stdio: ["pipe", "pipe", "ignore"],
|
|
32
|
+
});
|
|
33
|
+
return "sha256:" + createHash("sha256").update(result).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "sha256:unknown";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function changeRoot(projectRoot, change) {
|
|
40
|
+
return join(projectRoot, "openspec", "changes", change);
|
|
41
|
+
}
|
package/dist/record.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RecordResult, Job } from "./types.ts";
|
|
2
|
+
/** record job-submit:登记工作项结果 */
|
|
3
|
+
export declare function recordJobSubmit(projectRoot: string, change: string, changeRoot: string, jobId: string, reportFile: string): RecordResult;
|
|
4
|
+
/** record user-decision:登记用户决策 */
|
|
5
|
+
export declare function recordUserDecision(projectRoot: string, change: string, inputFile: string): RecordResult;
|
|
6
|
+
/** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
|
|
7
|
+
export declare function jobsList(projectRoot: string, change: string): {
|
|
8
|
+
open: Job[];
|
|
9
|
+
accepted: Job[];
|
|
10
|
+
rejected: {
|
|
11
|
+
job_id: string;
|
|
12
|
+
role: string;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
/** jobs packet:返回工作项执行说明 */
|
|
16
|
+
export declare function jobsPacket(projectRoot: string, change: string, jobId: string): {
|
|
17
|
+
found: boolean;
|
|
18
|
+
packet?: Record<string, unknown>;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
package/dist/record.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// SuperSpec 流程引擎 — record:工作项结果登记
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, } from "./store.js";
|
|
5
|
+
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
6
|
+
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
7
|
+
function recommendedAgentForRole(role) {
|
|
8
|
+
switch (role) {
|
|
9
|
+
case "proposal-auditor": return "proposal-auditor";
|
|
10
|
+
case "critic": return "critic";
|
|
11
|
+
case "architect": return "architect";
|
|
12
|
+
case "test-engineer": return "test-engineer";
|
|
13
|
+
case "final-audit": return "final-audit";
|
|
14
|
+
case "executor": return "executor";
|
|
15
|
+
case "test-run": return "test-runner";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function roleDescription(role) {
|
|
19
|
+
switch (role) {
|
|
20
|
+
case "proposal-auditor":
|
|
21
|
+
return "审查 proposal/tasks/design/discovery/business-invariants/test-contract 是否足够进入实现计划门";
|
|
22
|
+
case "critic":
|
|
23
|
+
return "从反方角度审查需求澄清或计划材料中的隐藏假设、范围漂移、验收漏洞和证据缺口";
|
|
24
|
+
case "architect":
|
|
25
|
+
return "审查架构边界、接口契约、长期维护风险和设计取舍";
|
|
26
|
+
case "test-engineer":
|
|
27
|
+
return "审查测试契约、覆盖策略、RED/GREEN 可信度和验收场景映射";
|
|
28
|
+
case "final-audit":
|
|
29
|
+
return "最终审查 proposal、实现状态、任务完成、测试契约和 SuperSpec 证据一致性";
|
|
30
|
+
case "executor":
|
|
31
|
+
return "执行受限实现工作项";
|
|
32
|
+
case "test-run":
|
|
33
|
+
return "执行受限测试工作项";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** 从 events 中查找 job(H4 修复:job 只在 transition_commit 的 new_jobs payload 里) */
|
|
37
|
+
function findJob(events, jobId) {
|
|
38
|
+
for (const ev of events) {
|
|
39
|
+
if (ev.event_type === "transition_commit") {
|
|
40
|
+
const newJobs = ev.payload.new_jobs ?? [];
|
|
41
|
+
const job = newJobs.find(j => j.job_id === jobId);
|
|
42
|
+
if (job)
|
|
43
|
+
return job;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** 检查 job 是否已终态 */
|
|
49
|
+
function jobTerminalState(events, jobId) {
|
|
50
|
+
for (const ev of events) {
|
|
51
|
+
if (ev.event_type === "job_accepted" && ev.payload.job_id === jobId)
|
|
52
|
+
return "accepted";
|
|
53
|
+
if (ev.event_type === "job_rejected" && ev.payload.job_id === jobId)
|
|
54
|
+
return "rejected";
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/** record job-submit:登记工作项结果 */
|
|
59
|
+
export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFile) {
|
|
60
|
+
return withLock(projectRoot, change, () => {
|
|
61
|
+
ensureChangeLayout(projectRoot, change);
|
|
62
|
+
const events = readEvents(projectRoot, change);
|
|
63
|
+
// 查找 job
|
|
64
|
+
const job = findJob(events, jobId);
|
|
65
|
+
if (!job) {
|
|
66
|
+
return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
|
|
67
|
+
}
|
|
68
|
+
// 检查终态
|
|
69
|
+
const terminal = jobTerminalState(events, jobId);
|
|
70
|
+
if (terminal) {
|
|
71
|
+
// 幂等检查:同 report_digest → 返回旧结果
|
|
72
|
+
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
73
|
+
const existing = events.find(e => (e.event_type === "job_accepted" || e.event_type === "job_rejected")
|
|
74
|
+
&& e.payload.job_id === jobId
|
|
75
|
+
&& e.payload.report_digest === reportDigest);
|
|
76
|
+
if (existing) {
|
|
77
|
+
return {
|
|
78
|
+
event_type: existing.event_type,
|
|
79
|
+
accepted: existing.event_type === "job_accepted",
|
|
80
|
+
message: "幂等返回:同 report 已提交",
|
|
81
|
+
job_state: existing.event_type === "job_accepted" ? "accepted" : "rejected",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// 终态 job + 不同 report → 拒绝
|
|
85
|
+
return {
|
|
86
|
+
event_type: "job_rejected",
|
|
87
|
+
accepted: false,
|
|
88
|
+
message: `工作项 ${jobId} 已终态(${terminal}),不接受新报告。需要新工作项请重跑 transition。`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// 读报告
|
|
92
|
+
if (!existsSync(reportFile)) {
|
|
93
|
+
return { event_type: "job_rejected", accepted: false, message: `报告文件不存在:${reportFile}` };
|
|
94
|
+
}
|
|
95
|
+
const reportContent = readFileSync(reportFile, "utf8");
|
|
96
|
+
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
97
|
+
// acceptance checks
|
|
98
|
+
const checks = [];
|
|
99
|
+
// 0. 报告格式和角色匹配(最小 JSON contract)
|
|
100
|
+
try {
|
|
101
|
+
const report = JSON.parse(reportContent);
|
|
102
|
+
if (!report || typeof report !== "object" || Array.isArray(report)) {
|
|
103
|
+
checks.push("报告必须是 JSON object");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const obj = report;
|
|
107
|
+
for (const field of REVIEW_REPORT_REQUIRED_FIELDS) {
|
|
108
|
+
if (!(field in obj))
|
|
109
|
+
checks.push(`报告缺少必填字段 ${field}`);
|
|
110
|
+
}
|
|
111
|
+
if (obj.role !== job.role) {
|
|
112
|
+
checks.push(`报告角色 ${String(obj.role)} 与工作项角色 ${job.role} 不匹配`);
|
|
113
|
+
}
|
|
114
|
+
if (obj.verdict !== "pass" && obj.verdict !== "fail") {
|
|
115
|
+
checks.push("报告 verdict 必须是 pass 或 fail");
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(obj.findings)) {
|
|
118
|
+
checks.push("报告 findings 必须是数组");
|
|
119
|
+
}
|
|
120
|
+
if (obj.verdict === "fail") {
|
|
121
|
+
checks.push("报告 verdict=fail,工作项未通过");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
checks.push("报告必须是有效 JSON");
|
|
127
|
+
}
|
|
128
|
+
// 1. boundFiles 仍匹配当前文档(missing 也算不匹配)
|
|
129
|
+
for (const bf of job.boundFiles) {
|
|
130
|
+
const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
|
|
131
|
+
if (currentSha !== bf.sha) {
|
|
132
|
+
checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// 2. 报告格式基本校验(非空 JSON 或文本)
|
|
136
|
+
if (!reportContent.trim()) {
|
|
137
|
+
checks.push("报告内容为空");
|
|
138
|
+
}
|
|
139
|
+
if (checks.length > 0) {
|
|
140
|
+
// 拒绝
|
|
141
|
+
const rejectEvent = makeEvent(change, "job_rejected", {
|
|
142
|
+
job_id: jobId,
|
|
143
|
+
role: job.role,
|
|
144
|
+
report_digest: reportDigest,
|
|
145
|
+
reason: checks.join("; "),
|
|
146
|
+
});
|
|
147
|
+
appendEvent(projectRoot, change, rejectEvent);
|
|
148
|
+
return {
|
|
149
|
+
event_type: "job_rejected",
|
|
150
|
+
accepted: false,
|
|
151
|
+
message: `工作项 ${jobId} 被拒绝:${checks.join("; ")}`,
|
|
152
|
+
job_state: "rejected",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// 接受
|
|
156
|
+
const acceptEvent = makeEvent(change, "job_accepted", {
|
|
157
|
+
job_id: jobId,
|
|
158
|
+
role: job.role,
|
|
159
|
+
report_digest: reportDigest,
|
|
160
|
+
accepted_at: new Date().toISOString(),
|
|
161
|
+
});
|
|
162
|
+
appendEvent(projectRoot, change, acceptEvent);
|
|
163
|
+
return {
|
|
164
|
+
event_type: "job_accepted",
|
|
165
|
+
accepted: true,
|
|
166
|
+
message: `工作项 ${jobId}(${job.role})已接受`,
|
|
167
|
+
job_state: "accepted",
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** record user-decision:登记用户决策 */
|
|
172
|
+
export function recordUserDecision(projectRoot, change, inputFile) {
|
|
173
|
+
return withLock(projectRoot, change, () => {
|
|
174
|
+
ensureChangeLayout(projectRoot, change);
|
|
175
|
+
if (!existsSync(inputFile)) {
|
|
176
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "file_not_found", path: inputFile }));
|
|
177
|
+
return { event_type: "user_decision_recorded", accepted: false, message: `决策文件不存在:${inputFile}` };
|
|
178
|
+
}
|
|
179
|
+
const content = readFileSync(inputFile, "utf8");
|
|
180
|
+
let decision;
|
|
181
|
+
try {
|
|
182
|
+
decision = JSON.parse(content);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "invalid_json" }));
|
|
186
|
+
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件不是有效 JSON" };
|
|
187
|
+
}
|
|
188
|
+
if (!decision.scope || !decision.answer) {
|
|
189
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "missing_scope_or_answer" }));
|
|
190
|
+
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少 scope 或 answer" };
|
|
191
|
+
}
|
|
192
|
+
const event = makeEvent(change, "user_decision_recorded", {
|
|
193
|
+
scope: decision.scope,
|
|
194
|
+
question: decision.question ?? "",
|
|
195
|
+
answer: decision.answer,
|
|
196
|
+
input_digest: sha256File(inputFile) ?? "sha256:unknown",
|
|
197
|
+
});
|
|
198
|
+
appendEvent(projectRoot, change, event);
|
|
199
|
+
return {
|
|
200
|
+
event_type: "user_decision_recorded",
|
|
201
|
+
accepted: true,
|
|
202
|
+
message: `用户决策已登记:scope=${decision.scope}`,
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
|
|
207
|
+
export function jobsList(projectRoot, change) {
|
|
208
|
+
const events = readEvents(projectRoot, change);
|
|
209
|
+
const open = [];
|
|
210
|
+
const accepted = [];
|
|
211
|
+
const rejected = [];
|
|
212
|
+
for (const ev of events) {
|
|
213
|
+
if (ev.event_type === "transition_commit") {
|
|
214
|
+
const newJobs = ev.payload.new_jobs ?? [];
|
|
215
|
+
for (const job of newJobs) {
|
|
216
|
+
if (!open.some(j => j.job_id === job.job_id) && !accepted.some(j => j.job_id === job.job_id) && !rejected.some(r => r.job_id === job.job_id)) {
|
|
217
|
+
open.push(job);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (ev.event_type === "job_accepted") {
|
|
222
|
+
const { job_id } = ev.payload;
|
|
223
|
+
const idx = open.findIndex(j => j.job_id === job_id);
|
|
224
|
+
if (idx >= 0) {
|
|
225
|
+
const job = open.splice(idx, 1)[0];
|
|
226
|
+
job.state = "accepted";
|
|
227
|
+
accepted.push(job);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (ev.event_type === "job_rejected") {
|
|
231
|
+
const { job_id, role } = ev.payload;
|
|
232
|
+
const idx = open.findIndex(j => j.job_id === job_id);
|
|
233
|
+
if (idx >= 0)
|
|
234
|
+
open.splice(idx, 1)[0];
|
|
235
|
+
rejected.push({ job_id, role });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { open, accepted, rejected };
|
|
239
|
+
}
|
|
240
|
+
/** jobs packet:返回工作项执行说明 */
|
|
241
|
+
export function jobsPacket(projectRoot, change, jobId) {
|
|
242
|
+
const events = readEvents(projectRoot, change);
|
|
243
|
+
const job = findJob(events, jobId);
|
|
244
|
+
if (!job) {
|
|
245
|
+
return { found: false, message: `工作项 ${jobId} 不存在` };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
found: true,
|
|
249
|
+
packet: {
|
|
250
|
+
job_id: job.job_id,
|
|
251
|
+
role: job.role,
|
|
252
|
+
recommended_agent: recommendedAgentForRole(job.role),
|
|
253
|
+
boundFiles: job.boundFiles,
|
|
254
|
+
packet_digest: job.packet_digest,
|
|
255
|
+
required_output_kind: "job_report_json",
|
|
256
|
+
output_contract_fields: [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
257
|
+
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
258
|
+
output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
|
|
259
|
+
`产出 JSON 报告文件并通过 superspec record job-submit 登记。` +
|
|
260
|
+
`最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]}`,
|
|
261
|
+
stop_conditions: ["审查完成后提交报告,不要修改文档"],
|
|
262
|
+
created_from_transition: job.created_from_transition,
|
|
263
|
+
},
|
|
264
|
+
message: `工作项 ${jobId} 的执行说明`,
|
|
265
|
+
};
|
|
266
|
+
}
|