@olierjs/fugue-cli 0.0.0-stage → 0.1.2

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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +99 -2
  3. package/THIRD_PARTY_NOTICES +467 -0
  4. package/bin/fugue-cli.mjs +23 -0
  5. package/dist/darwin-arm64/fugue-cli +0 -0
  6. package/dist/darwin-x64/fugue-cli +0 -0
  7. package/dist/linux-arm64/fugue-cli +0 -0
  8. package/dist/linux-x64/fugue-cli +0 -0
  9. package/dist/win32-arm64/fugue-cli.exe +0 -0
  10. package/dist/win32-x64/fugue-cli.exe +0 -0
  11. package/package.json +27 -4
  12. package/scripts/postinstall.mjs +13 -0
  13. package/scripts/skills.mjs +104 -0
  14. package/skills/fugue-show-me/SKILL.md +16 -0
  15. package/skills/fugue-stack/SKILL.md +10 -0
  16. package/skills/plan-with-remote/SKILL.md +60 -0
  17. package/skills/plan-with-remote/references/configuration.md +43 -0
  18. package/skills/plan-with-remote/scripts/client.mjs +61 -0
  19. package/skills/plan-with-remote/scripts/config.mjs +53 -0
  20. package/skills/plan-with-remote/scripts/planning.mjs +279 -0
  21. package/skills/plan-with-remote/scripts/protocol.mjs +147 -0
  22. package/skills/plan-with-remote/scripts/service.mjs +146 -0
  23. package/skills/plan-with-remote/scripts/storage.mjs +74 -0
  24. package/skills/show-me/SKILL.md +128 -0
  25. package/skills/show-me/agents/openai.yaml +2 -0
  26. package/skills/statechart/SKILL.md +36 -0
  27. package/skills/statechart/scripts/bug-states.mjs +81 -0
  28. package/skills/statechart/scripts/chart-edges.mjs +68 -0
  29. package/skills/statechart/scripts/chart-parts.mjs +21 -0
  30. package/skills/statechart/scripts/chart-to-tla.mjs +205 -0
  31. package/skills/statechart/scripts/compare-charts.mjs +74 -0
  32. package/skills/statechart/scripts/lib.mjs +970 -0
  33. package/skills/statechart/scripts/map-view.mjs +92 -0
  34. package/skills/statechart/scripts/property-loop.mjs +60 -0
  35. package/skills/statechart/scripts/read-check.mjs +79 -0
  36. package/skills/statechart/scripts/regions-chart.mjs +307 -0
  37. package/skills/statechart/scripts/render.mjs +531 -0
  38. package/skills/statechart/scripts/rule-book.mjs +84 -0
  39. package/skills/statechart/scripts/second-replay.mjs +122 -0
  40. package/skills/statechart/scripts/snapshot.mjs +74 -0
  41. package/skills/statechart/scripts/spec-text.mjs +126 -0
  42. package/skills/statechart/scripts/statechart-help.mjs +350 -0
  43. package/skills/statechart/scripts/statechart.mjs +903 -0
  44. package/skills/statechart/scripts/test/band.mmd +15 -0
  45. package/skills/statechart/scripts/test/regions.mmd +62 -0
  46. package/skills/statechart/scripts/test/statechart.test.mjs +6854 -0
  47. package/skills/statechart/scripts/test/sync.mmd +19 -0
  48. package/skills/statechart/scripts/test/upload.mmd +16 -0
  49. package/skills/statechart/scripts/tlc-to-chart-args.mjs +52 -0
  50. package/skills/statechart/scripts/tlc-to-chart.mjs +802 -0
  51. package/skills/statechart/scripts/tlc-tools.mjs +69 -0
  52. package/skills/statechart/scripts/ways-in.mjs +69 -0
  53. package/upstream-skills.json +18 -0
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: fugue-show-me
3
+ description: 使用 show-me 和 statechart 可视化解释当前实现、代码变更、运行结果或状态变化;当用户需要图解、结构示意或缺陷路径时使用。
4
+ ---
5
+
6
+ # 可视化解释
7
+
8
+ 先阅读并遵循[上游 show-me](../show-me/SKILL.md),由它指导表达形式的选择。
9
+
10
+ 需要分析状态转换、并发、重试、取消、恢复或缺陷路径,并检查行为规则、对比修复时,阅读并遵循[上游 statechart](../statechart/SKILL.md)的建模、模型检查、出图与交付要求。用 `show-me` 解释整体结构,用 `statechart` 展示状态变化和缺陷路径,按当前问题需要组合。
11
+
12
+ 原版脚本安装的 TLC 使用 ASCII 模型标识符和字符串值,中文状态与步骤标签写在映射文件中,再从模型生成中文图示。
13
+
14
+ 说明使用用户的语言,依据当前源码、差异或已有材料,只展示本次问题需要的内容。
15
+
16
+ 将上游的 `Bash(open ...)` 对应为当前宿主可用的终端或文件预览工具,按实际操作系统打开 HTML 或图片。交付本地文件时提供绝对路径链接;仅在实际打开成功后说明已打开。
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: fugue-stack
3
+ description: 开始或继续个人需求交付时,通过 fugue-cli 获取当前工作指南,选择并衔接规划、编码、代码审查与验证或实现解释;不用于纯闲聊和普通知识问答。
4
+ ---
5
+
6
+ # Fugue Stack
7
+
8
+ 每次开始或继续任务时,运行 `fugue-cli guide`,读取当前安装版本的总览,并遵循其中与本次用户目标对应的流程。
9
+
10
+ 工作路由和具体要求由本次命令输出提供。按需读取其中指向的技能与材料,保留已有规划会话和产物,在本次授权范围内完成工作。
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: plan-with-remote
3
+ description: 当用户需要通过远端服务制定开发方案、生成执行和验收文档、查看规划进展、补充需求或审阅修改方案时使用。
4
+ ---
5
+
6
+ # 远端规划
7
+
8
+ 将用户需求交给规划服务,向用户呈现方案、待回答的问题和最终文档。规划交付不表示软件已经实现或验收。脚本由 Node 内置能力组成,不需要从 Reconcile 源码仓库构建或安装 npm 依赖。
9
+
10
+ ## 开始
11
+
12
+ 使用 Node 22.19.0 或更高版本及 Git。在目标 Git 仓库中调用:
13
+
14
+ ```sh
15
+ node <skill-root>/scripts/client.mjs --help
16
+ ```
17
+
18
+ 服务地址由 `RECONCILE_BASE_URL` 或 `--config` 指定;首次配置、切换服务或调整等待时间时阅读[配置说明](references/configuration.md)。仓库必须已在服务中登记;匹配失败时报告缺项。
19
+
20
+ 先收集用户目标、范围、约束和需求来源。保留产品事实的原意;先读取来源正文,不把链接当成正文,也不把已有建议当成已批准决定。创建 `request.json`:
21
+
22
+ ```json
23
+ {
24
+ "title": "修复切换对象后的状态显示",
25
+ "description": "切换到新对象后,旧请求的结果不能覆盖新对象的状态。保留正常加载行为。",
26
+ "sources": [{ "label": "用户补充", "content": "切换后应立即显示新对象的加载状态。" }]
27
+ }
28
+ ```
29
+
30
+ `sources` 可省略。两种规划方式都生成执行文档与验收文档,并等待最终批准:
31
+
32
+ - `lightweight`:适合范围明确的局部改动,直接形成最终文档。
33
+ - `standard`:先审阅方案方向,用户认可后继续生成文档并进行独立评审。
34
+
35
+ 按用户目标明确选择;不清楚是否需要先审方向时向用户确认。
36
+
37
+ ```sh
38
+ node <skill-root>/scripts/client.mjs plan --input request.json --mode lightweight
39
+ ```
40
+
41
+ ## 处理结果
42
+
43
+ 脚本输出状态、文件路径、问题和下一步命令。保存返回的 `session` 和 `review` 路径,原样传回脚本;这些文件由脚本维护,无需打开、编辑或拼装其内容。
44
+
45
+ | 返回状态 | Agent 的下一步 |
46
+ | --- | --- |
47
+ | `running` | 使用 `read --session <路径>` 继续查看,不重复创建任务。 |
48
+ | `review_solution` | 阅读并呈现方案;用户认可后用 `accept --review <路径>`,有修改用 `feedback`。 |
49
+ | `review_documents` | 阅读执行文档、验收文档及返回的问题文件;用户明确批准后用 `approve --review <路径>`。 |
50
+ | `questions` | 向用户提出返回的问题,用 `answer --review <路径> --question <编号> --text <用户回答>` 提交。确认类问题在用户同意后使用 `--confirm`。 |
51
+ | `changes_needed` | 呈现待解决问题,按用户修改要求使用 `feedback`;不能批准。 |
52
+ | `submission_unconfirmed` | 用 `retry --session <路径>` 重试原请求,不另建任务。 |
53
+ | `failed` | 报告失败;需要重试时用 `retry --session <路径>`。 |
54
+ | `approved` / `stopped` | 交付返回的文档,或报告规划已停止。 |
55
+
56
+ 反馈用 `feedback --review <路径> --scope <范围> --change <修改要求> --preserve <保留要求>`。范围选需求 `requirements`、方案 `solution` 或验收设计 `verification`;方案总览还支持仅改表达的 `wording`。具体可用范围见 `--help`。
57
+
58
+ 决定来源通过配置或 `--actor` 提供。认可、批准、停止和确认重新规划必须有用户明确授权。已获得且仍适用于本次文档的授权不重复询问。版本变化时重新呈现,不将旧决定用于新文档。
59
+
60
+ 阅读并解释返回的正式文档。计划中的实现、测试或效果保持为计划,不写成已完成事实。文档可以提交审阅,不代表用户已批准。
@@ -0,0 +1,43 @@
1
+ # 配置
2
+
3
+ 运行时无需安装 npm 依赖。需要 Node >=22.19.0、Git、可访问的规划服务,以及已登记到该服务的目标仓库。
4
+
5
+ 配置优先级:命令行参数 > 环境变量 > `--config` 指定的 JSON 文件 > 默认值。也可用 `RECONCILE_CLIENT_CONFIG` 指定配置文件路径。不会自动读取或执行 `.env` 文件。
6
+
7
+ | JSON 字段 | 环境变量 | 命令行 | 默认值 |
8
+ | --- | --- | --- | --- |
9
+ | `baseUrl` | `RECONCILE_BASE_URL` | `--base-url` | 必须配置,无内置 IP |
10
+ | `remote` | `RECONCILE_REMOTE` | `plan --remote` | `origin` |
11
+ | `agent` | `RECONCILE_AGENT` | `plan --agent` | 服务中配置的项目默认客户端 |
12
+ | `actor` | `RECONCILE_ACTOR` | 决策、反馈、回答命令的 `--actor` | 无;需明确提供 |
13
+ | `waitMs` | `RECONCILE_WAIT_MS` | `--wait-ms` | `60000` |
14
+ | `requestTimeoutMs` | `RECONCILE_REQUEST_TIMEOUT_MS` | `--request-timeout-ms` | `30000` |
15
+
16
+ 例如将配置存入仓库外的 `reconcile.config.json`,把示例域名换成实际服务地址:
17
+
18
+ ```json
19
+ {
20
+ "baseUrl": "https://planning.example.com",
21
+ "remote": "origin",
22
+ "actor": "user:your-name",
23
+ "waitMs": 60000,
24
+ "requestTimeoutMs": 30000
25
+ }
26
+ ```
27
+
28
+ `actor` 只声明决定来源,不代表登录或身份认证,不要编造真实人员身份。`agent` 仅在明确需要覆盖项目默认值时配置,可选 `traex` 或 `grok-build`,创建后不可切换。
29
+
30
+ ```sh
31
+ node <skill-root>/scripts/client.mjs plan \
32
+ --config /absolute/path/reconcile.config.json \
33
+ --input request.json --mode standard \
34
+ --output-dir /absolute/path/planning-output
35
+ ```
36
+
37
+ 默认输出目录为目标仓库的 `.plan-with-remote/<任务编号>/`。可用 `--output-dir` 指定空目录;不要把需求材料、输出或配置提交到公开仓库。后续命令直接使用返回的绝对路径。
38
+
39
+ 任务会记住创建时的服务地址;继续任务不需要重复配置地址。显式配置了不同地址时命令会拒绝执行,不能把同一个任务切换到其他服务。
40
+
41
+ `waitMs` 限制本轮等待,结束后返回当前进展;`read` 可以继续等待,远端任务不会因此停止。每次 HTTP 请求另外受 `requestTimeoutMs` 限制。网络结果不明确时保留任务文件,用 `retry` 重试同一个请求。
42
+
43
+ 只有 `plan --dry-run` 提供本地输入预览,不访问服务、不创建任务;它不证明服务可达或仓库已登记。其他命令拒绝 `--dry-run`。
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { realpathSync } from 'node:fs';
5
+ import { configuration } from './config.mjs';
6
+ import { start, useSession } from './planning.mjs';
7
+
8
+ const commandOptions = {
9
+ plan: ['input', 'mode', 'output-dir', 'remote', 'agent', 'dry-run'],
10
+ read: ['session'], retry: ['session'],
11
+ accept: ['review', 'actor'], approve: ['review', 'actor'], stop: ['review', 'actor'],
12
+ answer: ['review', 'question', 'text', 'confirm', 'actor'],
13
+ feedback: ['review', 'scope', 'change', 'preserve', 'actor'],
14
+ };
15
+ const globalOptions = ['config', 'base-url', 'wait-ms', 'request-timeout-ms', 'help'];
16
+ const booleanOptions = new Set(['help', 'dry-run', 'confirm']);
17
+ const help = `用法:node <skill-root>/scripts/client.mjs <命令> [参数]
18
+
19
+ plan --input request.json --mode lightweight|standard [--output-dir 目录] [--dry-run]
20
+ read --session <返回的任务文件>
21
+ retry --session <任务文件> 重试原提交,或重新规划失败任务
22
+ accept --review <返回的审阅凭据> 用户认可方案,继续生成最终文档
23
+ approve --review <审阅凭据> 用户批准最终文档
24
+ stop --review <审阅凭据> 用户停止规划
25
+ answer --review <审阅凭据> --question <编号> --text <用户回答>
26
+ 确认类问题使用 --confirm,代替 --text
27
+ feedback --review <审阅凭据> --scope <范围> --change <修改要求> --preserve <保留要求>
28
+ 方案范围:requirements|solution|wording;最终文档:requirements|solution|verification
29
+
30
+ 决定、反馈和回答需要 --actor <决定来源>,也可通过配置提供。
31
+ plan 支持 --remote <Git remote>、--agent traex|grok-build;通常使用项目默认客户端。
32
+ 通用配置:--config <JSON 文件>、--base-url <URL>、--wait-ms <毫秒>、--request-timeout-ms <毫秒>。
33
+ 配置优先级:命令行 > 环境变量 > 配置文件 > 内置默认值。配置说明见 references/configuration.md。
34
+ 只有 plan 支持 --dry-run,且不会访问网络。其他命令拒绝此参数。
35
+ 输出为 JSON:状态、文件路径、待回答问题和下一步命令。无需打开任务文件或审阅凭据。
36
+ `;
37
+
38
+ export async function main(argv = process.argv.slice(2), env = process.env) {
39
+ const [command, ...args] = argv;
40
+ if (!command || command === '--help' || command === '-h') return { help };
41
+ if (!Object.hasOwn(commandOptions, command)) throw new Error('未知命令,请使用 --help。');
42
+ const options = Object.fromEntries([...globalOptions, ...commandOptions[command]].map(key => [key, { type: booleanOptions.has(key) ? 'boolean' : 'string' }]));
43
+ const { values } = parseArgs({ args, options, strict: true, allowPositionals: false });
44
+ if (values.help) return { help };
45
+ const config = await configuration(values, env);
46
+ return command === 'plan' ? start(values, config) : useSession(command, values, config);
47
+ }
48
+
49
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
50
+ const [major, minor] = process.versions.node.split('.').map(Number);
51
+ if (major < 22 || (major === 22 && minor < 19)) {
52
+ console.error('请使用 Node 22.19.0 或更高版本。');
53
+ process.exitCode = 1;
54
+ } else {
55
+ main().then(result => console.log(result.help ?? JSON.stringify(result, null, 2))).catch(error => {
56
+ const message = error.message?.match(/[\u3400-\u9fff]/) ? error.message : '命令未完成。请检查参数或配置;已提交的任务请保留任务文件后继续。';
57
+ console.error(JSON.stringify({ status: 'error', message, ...(error.session ? { session: error.session } : {}), ...(error.diagnostic ? { diagnostic: error.diagnostic } : {}), ...(!error.session ? { detail: error.message } : {}) }, null, 2));
58
+ process.exitCode = 1;
59
+ });
60
+ }
61
+ }
@@ -0,0 +1,53 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+
4
+ const settings = {
5
+ baseUrl: ['base-url', 'RECONCILE_BASE_URL'],
6
+ remote: ['remote', 'RECONCILE_REMOTE', 'origin'],
7
+ agent: ['agent', 'RECONCILE_AGENT'],
8
+ actor: ['actor', 'RECONCILE_ACTOR'],
9
+ waitMs: ['wait-ms', 'RECONCILE_WAIT_MS', 60000],
10
+ requestTimeoutMs: ['request-timeout-ms', 'RECONCILE_REQUEST_TIMEOUT_MS', 30000],
11
+ };
12
+
13
+ export async function configuration(flags, env = process.env) {
14
+ const path = flags.config ?? env.RECONCILE_CLIENT_CONFIG;
15
+ const file = path ? JSON.parse(await readFile(resolve(path), 'utf8')) : {};
16
+ if (!file || typeof file !== 'object' || Array.isArray(file)) throw new Error('配置文件必须是 JSON 对象。');
17
+ for (const key of Object.keys(file)) {
18
+ if (!(key in settings)) throw new Error(`未知配置:${key}`);
19
+ }
20
+ const config = {};
21
+ for (const [key, [flag, variable, fallback]] of Object.entries(settings)) {
22
+ config[key] = flags[flag] ?? env[variable] ?? file[key] ?? fallback;
23
+ }
24
+ if (config.baseUrl !== undefined) {
25
+ const url = new URL(config.baseUrl);
26
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
27
+ throw new Error('服务地址必须是没有凭证、查询参数或片段的 HTTP(S) 地址。');
28
+ }
29
+ config.baseUrl = url.href.replace(/\/+$/, '');
30
+ }
31
+ for (const key of ['waitMs', 'requestTimeoutMs']) {
32
+ config[key] = Number(config[key]);
33
+ if (!Number.isSafeInteger(config[key]) || config[key] <= 0) throw new Error(`${key} 必须是正整数毫秒。`);
34
+ }
35
+ for (const key of ['remote', 'actor', 'agent']) {
36
+ if (config[key] !== undefined && (typeof config[key] !== 'string' || !config[key].trim())) {
37
+ throw new Error(`${key} 不能为空。`);
38
+ }
39
+ }
40
+ if (config.agent !== undefined && !['traex', 'grok-build'].includes(config.agent)) {
41
+ throw new Error('agent 只能是 traex 或 grok-build。');
42
+ }
43
+ return config;
44
+ }
45
+
46
+ export function serviceAddress(config, session) {
47
+ if (session && config.baseUrl && config.baseUrl !== session.baseUrl) {
48
+ throw new Error('当前配置指向另一服务。继续原任务时请使用创建任务的服务地址。');
49
+ }
50
+ const url = session?.baseUrl ?? config.baseUrl;
51
+ if (!url) throw new Error('请设置 RECONCILE_BASE_URL,或通过 --config / --base-url 配置服务地址。');
52
+ return url;
53
+ }
@@ -0,0 +1,279 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { serviceAddress } from './config.mjs';
5
+ import { createSession, loadSession, saveSession, lockSession, writeReview, loadReview, digest } from './storage.mjs';
6
+ import { transport, resolveProject, readInput, requestPackage, validateDocuments, assertFresh } from './service.mjs';
7
+ import { runPlanningFromAcceptedOperation, waitForProjectPreparation, continuePlanningAfterHumanInput, buildAcceptSolutionReviewCommand, buildFeedbackSolutionReviewCommand, parseSolutionReviewOperationAccepted, parseDeliveryPackage } from './protocol.mjs';
8
+
9
+ function required(value, label) {
10
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`请提供 ${label}。`);
11
+ return value;
12
+ }
13
+
14
+ export async function start(flags, config, cwd = process.cwd()) {
15
+ if (!['lightweight', 'standard'].includes(flags.mode)) throw new Error('请用 --mode lightweight 或 standard 明确选择规划方式。');
16
+ const input = await readInput(required(flags.input, '--input'));
17
+ if (flags['dry-run']) return { status: 'preview', mode: flags.mode, input, message: '仅校验本地输入,没有发送网络请求或创建任务。' };
18
+ const baseUrl = serviceAddress(config);
19
+ const api = transport(baseUrl, config.requestTimeoutMs);
20
+ const projectId = await resolveProject(api, config.remote, cwd);
21
+ const requirementId = randomUUID();
22
+ const command = { type: 'submit_requirement', input: requestPackage(input, requirementId, projectId, flags.mode, config.agent) };
23
+ const state = { version: 1, requirementId, baseUrl, projectId, mode: flags.mode, phase: 'preparing', pending: { command } };
24
+ const path = await createSession(flags['output-dir'] ?? resolve(cwd, '.plan-with-remote', requirementId), state);
25
+ return withSession(path, config, async context => {
26
+ await submitPending(context);
27
+ return refresh(context);
28
+ });
29
+ }
30
+
31
+ export async function useSession(action, flags, config) {
32
+ if (['read', 'retry'].includes(action)) {
33
+ return withSession(required(flags.session, '--session'), config, async context => {
34
+ const { state } = context;
35
+ if (state.pending) {
36
+ if (action === 'read') return { status: 'submission_unconfirmed', session: context.path, message: '上次提交结果尚未确认。使用 retry 重试原请求;不要另建任务。', next: ['retry'] };
37
+ await submitPending(context);
38
+ } else if (action === 'retry' && state.outcome?.code === 'preparation_failed') {
39
+ const resource = `/v1/reconcile/requirements/${encodeURIComponent(state.requirementId)}/context`;
40
+ const current = (await context.api.request(resource)).data;
41
+ if (current?.project?.projectId !== state.projectId || current.preparation?.runId !== state.operationId || current.preparation?.requirementId !== state.requirementId || current.preparation.status !== 'failed' || !Number.isSafeInteger(current.snapshotRevision) || current.snapshotRevision < 1) {
42
+ throw new Error('项目准备状态已变化,请先读取进展。');
43
+ }
44
+ state.pending = { resource: `${resource}/retry`, command: { expectedSnapshotRevision: current.snapshotRevision } };
45
+ await saveSession(context.path, state);
46
+ await submitPending(context);
47
+ } else if (action === 'retry' && state.outcome?.status === 'failed' && state.outcome.code !== 'planning_wait_timeout') {
48
+ const report = (await context.api.post({ type: 'inspect_requirement', requirementId: state.requirementId })).data.report;
49
+ if (report?.requirementId !== state.requirementId || !Number.isSafeInteger(report.snapshotRevision) || report.snapshotRevision < 1) throw new Error('无法确认失败任务的当前版本。');
50
+ state.pending = { command: { type: 'request_replan', requirementId: state.requirementId, commandId: randomUUID(), reason: '用户请求重试失败的规划', expectedSnapshotRevision: report.snapshotRevision } };
51
+ await saveSession(context.path, state);
52
+ await submitPending(context);
53
+ }
54
+ return refresh(context);
55
+ });
56
+ }
57
+ const review = await loadReview(required(flags.review, '--review'));
58
+ return withSession(review.session, config, async context => {
59
+ if (context.state.pending) throw new Error('上次提交结果尚未确认,请先用 read / retry 处理原任务。');
60
+ const actor = required(config.actor, '--actor 或 RECONCILE_ACTOR(用户决定的来源)');
61
+ const command = decisionCommand(action, flags, review.outcome, context.state, actor);
62
+ await assertFresh(context.api, context.state, review.outcome);
63
+ context.state.pending = { command, outcome: review.outcome };
64
+ await saveSession(context.path, context.state);
65
+ await submitPending(context);
66
+ return refresh(context);
67
+ });
68
+ }
69
+
70
+ export function decisionCommand(action, flags, outcome, state, actor) {
71
+ const common = { requirementId: state.requirementId, actor, expectedSnapshotRevision: outcome.snapshotRevision, commandId: randomUUID() };
72
+ if (action === 'accept') {
73
+ if (outcome.status !== 'awaiting_solution_review') throw new Error('当前文档不需要方案认可。');
74
+ return buildAcceptSolutionReviewCommand({ ...common, reviewRevision: outcome.solutionReview.reviewRevision, sourceBinding: outcome.solutionReview.sourceBinding });
75
+ }
76
+ if (action === 'approve') {
77
+ if (outcome.status !== 'ready_for_approval' || outcome.gate.decision !== 'pass') throw new Error('当前规划不能批准,请先处理待解决的问题。');
78
+ return { type: 'execution_doc_decision', requirementId: state.requirementId, actor, expectedSnapshotRevision: outcome.snapshotRevision, decision: 'approve' };
79
+ }
80
+ if (action === 'stop') return { type: 'execution_doc_decision', requirementId: state.requirementId, actor, expectedSnapshotRevision: outcome.snapshotRevision, decision: 'stop' };
81
+ if (action === 'answer') {
82
+ if (outcome.status !== 'needs_human_input') throw new Error('当前没有等待回答的问题。');
83
+ const index = Number(flags.question);
84
+ if (!Number.isSafeInteger(index) || index < 1 || index > outcome.questions.length) throw new Error('请使用本次返回的问题编号。');
85
+ const question = outcome.questions[index - 1];
86
+ if (question.kind === 'stage') {
87
+ if (flags.confirm) throw new Error('此问题需要文字回答,不能使用 --confirm。');
88
+ return { ...common, type: 'submit_stage_clarification', questionKey: question.questionKey, questionRef: question.questionRef, blockerRef: question.blockerRef, answer: required(flags.text, '--text') };
89
+ }
90
+ if (question.action === 'confirm_replan') {
91
+ if (!flags.confirm || flags.text) throw new Error('请在用户同意重新规划后使用 --confirm。');
92
+ } else {
93
+ if (flags.confirm) throw new Error('此问题需要文字回答,不能使用 --confirm。');
94
+ required(flags.text, '--text');
95
+ }
96
+ return { ...common, type: 'resolve_owner_resolution', pending_resolution_identity: question.pendingResolutionIdentity, action: question.action, ...(question.action === 'clarify' ? { answer: flags.text } : {}) };
97
+ }
98
+ if (action === 'feedback') {
99
+ const change = required(flags.change, '--change(要修改的内容)');
100
+ const preserve = required(flags.preserve, '--preserve(必须保留的内容)');
101
+ const scope = flags.scope;
102
+ if (outcome.status === 'awaiting_solution_review') {
103
+ const scopes = { requirements: 'requirement', solution: 'technical_design', wording: 'presentation_only' };
104
+ if (!scopes[scope]) throw new Error('方案反馈请选择 --scope requirements、solution 或 wording。');
105
+ return buildFeedbackSolutionReviewCommand({ ...common, reviewRevision: outcome.solutionReview.reviewRevision, sourceBinding: outcome.solutionReview.sourceBinding, scope: scopes[scope], feedback: `需要修改:\n${change}\n\n必须保留:\n${preserve}` });
106
+ }
107
+ if (!['ready_for_approval', 'awaiting_revision'].includes(outcome.status)) throw new Error('请先读取可修改的规划文档。');
108
+ const stages = { requirements: 'prd_understanding', solution: 'technical_design', verification: 'verification_plan' };
109
+ if (!stages[scope]) throw new Error('文档反馈请选择 --scope requirements、solution 或 verification。');
110
+ return { ...common, type: 'execution_doc_decision', decision: 'regenerate', rerunFromStage: stages[scope], revisionConstraint: { mustChange: change, mustPreserve: preserve } };
111
+ }
112
+ throw new Error('未知操作。');
113
+ }
114
+
115
+ async function withSession(path, config, work) {
116
+ path = resolve(path);
117
+ return lockSession(path, async () => {
118
+ const state = await loadSession(path);
119
+ const api = transport(serviceAddress(config, state), config.requestTimeoutMs);
120
+ const context = { path, state, api, config };
121
+ try { return await work(context); }
122
+ catch (error) {
123
+ const diagnostic = resolve(dirname(path), `error-${randomUUID()}.json`);
124
+ await writeFile(diagnostic, JSON.stringify({ message: error.message, status: error.status, detail: error.detail }, null, 2), { mode: 0o600 });
125
+ error.session = path;
126
+ error.diagnostic = diagnostic;
127
+ throw error;
128
+ }
129
+ });
130
+ }
131
+
132
+ async function submitPending(context) {
133
+ const { state, api, path } = context;
134
+ const pending = state.pending;
135
+ if (!pending) return;
136
+ if (!pending.response) {
137
+ try { pending.response = pending.resource ? await api.request(pending.resource, pending.command) : await api.post(pending.command); }
138
+ catch (error) {
139
+ // A definite conflict did not accept this decision. Allow a new read;
140
+ // ambiguous network outcomes keep the exact pending command for retry.
141
+ if (error.status === 409 && pending.command.type !== 'submit_requirement') {
142
+ delete state.pending;
143
+ await saveSession(path, state);
144
+ error.message = '任务已变化,服务拒绝了旧决定。请使用 read 重新读取,再请用户审阅。';
145
+ }
146
+ throw error;
147
+ }
148
+ // Persist acceptance before polling, so a disconnected client can resume exactly.
149
+ await saveSession(path, state);
150
+ }
151
+ const data = pending.response.data;
152
+ const command = pending.command;
153
+ if (pending.resource) {
154
+ if (data.requirementId !== state.requirementId || data.projectId !== state.projectId || typeof data.runId !== 'string' || !data.runId) throw new Error('服务返回的准备任务身份不匹配。');
155
+ state.operationId = data.runId;
156
+ state.accepted = { data: { requirementId: state.requirementId, operationId: data.runId, polling: { nextPollAfterMs: state.accepted.data.polling.nextPollAfterMs } } };
157
+ state.phase = 'preparing';
158
+ delete state.outcome;
159
+ } else if (data.operationId) {
160
+ if (data.requirementId !== state.requirementId || typeof data.operationId !== 'string') throw new Error('服务返回的任务身份不匹配。');
161
+ if (command.type === 'accept_solution_review' || command.type === 'feedback_solution_review') {
162
+ parseSolutionReviewOperationAccepted(pending.response, command);
163
+ }
164
+ state.operationId = data.operationId;
165
+ state.phase = command.type === 'submit_requirement' ? 'preparing' : 'planning';
166
+ if (command.type === 'submit_requirement') state.accepted = pending.response;
167
+ delete state.outcome;
168
+ } else if (command.type === 'submit_stage_clarification' && data.type === 'submit_stage_clarification') {
169
+ state.outcome = await continuePlanningAfterHumanInput({ requirementId: state.requirementId, command, timeoutMs: context.config.waitMs }, {
170
+ post: async () => pending.response, wait: async () => {}, now: Date.now,
171
+ });
172
+ state.phase = 'awaiting_input';
173
+ } else if (command.type === 'execution_doc_decision' && ['approve', 'stop'].includes(command.decision)) {
174
+ const expected = command.decision === 'approve' ? 'approved' : 'stopped';
175
+ if (data.type !== 'execution_doc_decision' || data.result?.outcome !== expected) {
176
+ delete state.pending;
177
+ await saveSession(path, state);
178
+ throw new Error('服务未接受该决定,请重新读取当前规划后处理。');
179
+ }
180
+ state.phase = expected;
181
+ state.approvedReview = pending.outcome;
182
+ } else {
183
+ throw new Error('服务未返回可确认的结果。请保留任务文件,检查诊断后重试原请求。');
184
+ }
185
+ delete state.pending;
186
+ await saveSession(path, state);
187
+ }
188
+
189
+ async function refresh(context) {
190
+ const { state, config, api, path } = context;
191
+ if (state.phase === 'stopped') return { status: 'stopped', session: path, message: '规划已停止。', next: [] };
192
+ if (state.phase === 'approved') return delivery(context);
193
+ const startedAt = Date.now();
194
+ if (state.phase === 'preparing') {
195
+ try {
196
+ await waitForProjectPreparation({ requirementId: state.requirementId, projectId: state.projectId, timeoutMs: config.waitMs, startedAt }, state.accepted, {
197
+ read: () => api.request(`/v1/reconcile/requirements/${encodeURIComponent(state.requirementId)}/context`),
198
+ wait: milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)), now: Date.now,
199
+ });
200
+ } catch (error) {
201
+ if (error.message === 'project preparation timed out') return running(path);
202
+ if (error.message.startsWith('project preparation failed:')) {
203
+ const report = (await api.post({ type: 'inspect_requirement', requirementId: state.requirementId })).data.report;
204
+ if (report?.requirementId !== state.requirementId) throw error;
205
+ state.outcome = { status: 'failed', requirementId: state.requirementId, viewVersion: report.snapshotRevision, code: 'preparation_failed', message: error.message, retryable: true };
206
+ await saveSession(path, state);
207
+ return present(context, state.outcome);
208
+ }
209
+ throw error;
210
+ }
211
+ state.phase = 'planning';
212
+ await saveSession(path, state);
213
+ }
214
+ const remaining = config.waitMs - (Date.now() - startedAt);
215
+ if (remaining <= 0) return running(path);
216
+ const outcome = await runPlanningFromAcceptedOperation({ requirementId: state.requirementId, operationId: state.operationId, expectedPlanningMode: state.mode, timeoutMs: remaining }, {
217
+ post: async command => {
218
+ const response = await api.post(command);
219
+ if (command.type === 'get_requirement_flow_view' && response.data.requirementId !== state.requirementId) throw new Error('返回的规划进度属于其他任务。');
220
+ return response;
221
+ },
222
+ wait: milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)), now: Date.now,
223
+ });
224
+ state.outcome = outcome;
225
+ await saveSession(path, state);
226
+ return present(context, outcome);
227
+ }
228
+
229
+ function running(path) { return { status: 'running', session: path, message: '远端仍在处理。使用 read 继续查看,不要重复提交。', next: ['read'] }; }
230
+
231
+ async function present(context, outcome) {
232
+ const { path, state } = context;
233
+ if (outcome.status === 'failed') {
234
+ if (outcome.code === 'planning_wait_timeout') return running(path);
235
+ return { status: 'failed', session: path, message: '远端规划失败。可使用 retry 请求重试,或检查诊断文件。', diagnostic: path, next: ['retry'] };
236
+ }
237
+ let documents = {};
238
+ let status;
239
+ let message;
240
+ let next;
241
+ if (outcome.status === 'awaiting_solution_review') {
242
+ documents = { 'solution.md': outcome.solutionReview.overview.markdown };
243
+ status = 'review_solution'; message = '请阅读方案。用户认可后可以继续生成最终文档,也可以先反馈修改。'; next = ['accept', 'feedback', 'stop'];
244
+ } else if (['ready_for_approval', 'awaiting_revision'].includes(outcome.status)) {
245
+ documents = validateDocuments(outcome, state.requirementId);
246
+ if (outcome.status === 'ready_for_approval' && (outcome.gate.status !== 'decided' || outcome.gate.decision !== 'pass')) throw new Error('规划文档尚未通过检查,不能提交批准。');
247
+ status = outcome.status === 'ready_for_approval' ? 'review_documents' : 'changes_needed';
248
+ message = status === 'review_documents' ? '请审阅执行文档和验收文档。仅在用户明确批准后提交批准决定。' : '当前规划仍有待解决的问题,暂时不能批准;请阅读文档并提交修改要求。';
249
+ next = status === 'review_documents' ? ['approve', 'feedback', 'stop'] : ['feedback', 'stop'];
250
+ const issues = collectIssues(outcome.flow);
251
+ if (issues.length) documents['issues.md'] = '# 待处理问题\n\n' + issues.map(text => `- ${text}`).join('\n') + '\n';
252
+ } else if (outcome.status === 'needs_human_input') {
253
+ status = 'questions'; message = '请向用户确认以下问题,再使用本次审阅凭据回答。'; next = ['answer', 'stop'];
254
+ } else { throw new Error('服务返回了当前客户端不支持的完成状态。'); }
255
+ const result = await writeReview(path, state, outcome, documents);
256
+ return { status, message, session: path, ...result,
257
+ ...(status === 'questions' ? { questions: outcome.questions.map((question, index) => ({ id: String(index + 1), question: question.prompt, response: question.action === 'confirm_replan' ? 'confirm' : 'text' })) } : {}), next };
258
+ }
259
+
260
+ function collectIssues(value) {
261
+ const issues = new Set();
262
+ function visit(item) {
263
+ if (!item || typeof item !== 'object') return;
264
+ if (typeof item.problem === 'string') issues.add(item.problem);
265
+ for (const child of Object.values(item)) visit(child);
266
+ }
267
+ visit(value);
268
+ return [...issues];
269
+ }
270
+
271
+ async function delivery({ api, state, path }) {
272
+ const pkg = parseDeliveryPackage((await api.post({ type: 'get_delivery_package', requirementId: state.requirementId })).data.deliveryPackage);
273
+ if (pkg.status !== 'ready' || pkg.planningDelivery.requirementId !== state.requirementId) throw new Error('已批准文档暂时不可读取,请使用 read 重试。');
274
+ if (pkg.planningCandidateRevision !== state.approvedReview.candidateRevision || digest(pkg.executionDocumentRef) !== digest(state.approvedReview.executionDocumentRef) || digest(pkg.acceptanceDocumentRef) !== digest(state.approvedReview.acceptanceDocumentRef)) throw new Error('交付文档与已审阅版本不一致。');
275
+ const documents = validateDocuments(state.approvedReview, state.requirementId);
276
+ if (documents['execution.md'] !== pkg.executionDocument || documents['acceptance.md'] !== pkg.acceptanceDocument) throw new Error('交付正文与已审阅正文不一致。');
277
+ const files = await writeReview(path, state, state.approvedReview, documents);
278
+ return { status: 'approved', session: path, documents: files.documents, message: '规划文档已批准并交付;不表示软件已经实现、部署或验收。', next: [] };
279
+ }