sillyspec 3.26.8 → 3.26.9
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/.claude/skills/sillyspec-archive/SKILL.md +1 -2
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -2
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
- package/.claude/skills/sillyspec-explore/SKILL.md +0 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +1 -2
- package/.claude/skills/sillyspec-scan/SKILL.md +2 -2
- package/.claude/skills/sillyspec-status/SKILL.md +0 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -2
- package/README.md +5 -9
- package/package.json +2 -2
- package/packages/dashboard/server/index.js +14 -8
- package/src/change-risk-profile.js +5 -207
- package/src/config-schema.js +6 -1
- package/src/constants.js +97 -106
- package/src/db-engine.js +1 -1
- package/src/docs-check.js +34 -5
- package/src/git-helper.js +2 -2
- package/src/hooks/worktree-guard.js +1 -1
- package/src/index.js +96 -13
- package/src/init.js +14 -3
- package/src/machine-interface.js +7 -4
- package/src/progress/stage-machine.js +3 -0
- package/src/quicklog.js +182 -1
- package/src/run/command.js +101 -3
- package/src/run/complete.js +9 -1
- package/src/run/gates.js +67 -1
- package/src/run/prompt.js +721 -713
- package/src/run/quick-audit.js +146 -137
- package/src/run/shared.js +1197 -1109
- package/src/run/stage.js +17 -1
- package/src/scan-diff.js +388 -0
- package/src/scan-staleness.js +18 -13
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-review.js +1 -1
- package/src/stages/brainstorm.js +2 -2
- package/src/stages/execute.js +991 -985
- package/src/stages/plan.js +22 -7
- package/src/stages/quick.js +1 -1
- package/src/stages/scan.js +2 -2
- package/src/stages/verify.js +10 -11
- package/src/sync.js +12 -0
- package/src/task-review.js +12 -4
- package/src/workflow.js +8 -185
- package/src/worktree-apply.js +122 -0
- package/src/worktree.js +2 -2
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- package/templates/prompts/verify-probes.md +2 -2
- package/templates/skills/sillyspec-onboard/SKILL.md +0 -92
package/src/stages/plan.js
CHANGED
|
@@ -113,6 +113,9 @@ needs_human_review: true | false
|
|
|
113
113
|
|
|
114
114
|
然后列出已加载的文件清单(含 decisions.md 当前版本/未决项状态、模块文档 + 模块依赖关系摘要)。
|
|
115
115
|
|
|
116
|
+
### 落盘锚点(必须执行)
|
|
117
|
+
把 plan_level 写进 plan.md frontmatter(\`plan_level: <none|light|full>\`)——上一步分类结果在本步落盘为持久锚点,后续步骤(及 context 压缩后的恢复)一律读 plan.md frontmatter,不依赖对话记忆传递。
|
|
118
|
+
|
|
116
119
|
分类完成后,继续进入下一步。`,
|
|
117
120
|
outputHint: '复杂度分类结果 + 文件清单',
|
|
118
121
|
optional: false
|
|
@@ -125,13 +128,13 @@ needs_human_review: true | false
|
|
|
125
128
|
const stepGeneratePlan = {
|
|
126
129
|
id: 'generate_plan',
|
|
127
130
|
name: '生成分级计划',
|
|
128
|
-
prompt:
|
|
131
|
+
prompt: `根据 plan.md frontmatter 的 plan_level 结果,按对应级别生成计划。
|
|
129
132
|
|
|
130
133
|
### 操作
|
|
131
|
-
1.
|
|
134
|
+
1. 读取 plan.md frontmatter 的 \`plan_level:\` 字段(上一步已落盘为持久锚点;文件不存在或无该字段时回退读上一步输出的分类结果)
|
|
132
135
|
2. 读取 tasks.md 和 design.md 了解需求范围
|
|
133
136
|
3. 按 plan_level 选择对应模板输出
|
|
134
|
-
4. 保存 plan.md(审查在下一步"审查计划"
|
|
137
|
+
4. 保存 plan.md(审查在下一步"审查计划"独立进行,不在本步自审——避免生成与自审同一次输出);frontmatter 保留 plan_level 字段不删
|
|
135
138
|
|
|
136
139
|
---
|
|
137
140
|
|
|
@@ -283,7 +286,7 @@ full 计划的约束:
|
|
|
283
286
|
### 通用操作(所有级别)
|
|
284
287
|
1. 读取 tasks.md 获取任务列表
|
|
285
288
|
2. 读取 design.md 获取文件变更清单
|
|
286
|
-
3.
|
|
289
|
+
3. 读取 plan.md frontmatter 的 \`plan_level:\` 字段(持久锚点;缺失时回退上一步输出)
|
|
287
290
|
4. 按对应级别模板生成内容
|
|
288
291
|
5. 保存到变更目录下的 plan.md(路径格式:\`{SPEC_ROOT}/changes/<change-name>/plan.md\`,其中 <change-name> 是变更目录名,直接使用,不加子目录。正确路径示例:\`{SPEC_ROOT}/changes/2026-05-28-agent-log-streaming/plan.md\`)
|
|
289
292
|
**plan_level 为 none 时生成最小 plan.md(占位),不生成完整蓝图。**
|
|
@@ -346,15 +349,25 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
346
349
|
### 生成 module-impact.md 首版(scale≠small 时)
|
|
347
350
|
计划审查通过后,顺带生成 module-impact.md 首版(本次变更的模块影响分析,供 execute/verify 阶段更新、archive 阶段终审)。scale=small 不生成(small 走 quick,module-impact 对 quick 无用)。
|
|
348
351
|
|
|
349
|
-
输入(此时 TaskCard/allowed_paths
|
|
352
|
+
输入(此时 TaskCard/allowed_paths 尚未生成——在下一步「生成 TaskCard」才产出,故用以下两项作输入,粒度与 archive 现状一致):
|
|
350
353
|
- design.md 的「文件变更清单」(本次计划改哪些源码文件)
|
|
351
354
|
- plan.md 的任务列表(每个 task 改的范围)
|
|
352
355
|
|
|
353
|
-
|
|
356
|
+
步骤(module-impact 的生成口径与 archive 终审一致):
|
|
354
357
|
1. 读 {SPEC_ROOT}/docs/<project>/modules/_module-map.yaml(模块→文件路径映射)。**不存在 → 降级**:生成只含 unmapped 部分的 module-impact.md + 提示「建议运行 scan 生成模块映射」,不阻断
|
|
355
358
|
2. 对照 design 文件变更清单 + plan 任务列表,逐文件匹配所属模块(命中的归 mapped,未命中的归 unmapped)
|
|
356
359
|
3. 生成模块影响矩阵(模块 × 影响类型[新增/修改/删除/依赖变更] × 说明),落盘 {SPEC_ROOT}/changes/<change>/module-impact.md
|
|
357
360
|
4. 首行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
|
|
361
|
+
5. **必须含「更新结果」表骨架**(本表是 verify/archive 死信门控的收口目标——CLI 硬校验表内无 pending/待办行,漏写此表则 agent 只能从 gate 报错反推格式):
|
|
362
|
+
\`\`\`markdown
|
|
363
|
+
## 更新结果
|
|
364
|
+
|
|
365
|
+
| 目标 | 操作 | 状态 |
|
|
366
|
+
|------|------|------|
|
|
367
|
+
| \`modules/<id>.md\` | 更新<模块名>模块卡(本次变更涉及) | pending |
|
|
368
|
+
| \`_module-map.yaml\` | 无变化(未增删模块) | skipped |
|
|
369
|
+
\`\`\`
|
|
370
|
+
规则:每个受影响模块文档一行,状态列初始化 pending;无变化的行直接写 skipped。execute/verify 完成文档同步后把对应行回填 done;确定不同步的行改 skipped 并在操作列写明原因。**末列是状态列**(done/skipped/pending),不要在表尾追加其他列。
|
|
358
371
|
|
|
359
372
|
execute/verify 阶段会按实际代码变更更新此文档;archive 阶段会最终确认它。
|
|
360
373
|
|
|
@@ -470,7 +483,9 @@ ${subagentPrompts}
|
|
|
470
483
|
|
|
471
484
|
## 验收(生成后自查,不另开步骤)
|
|
472
485
|
- 每个 task-N.md 文件存在且非空
|
|
473
|
-
- frontmatter
|
|
486
|
+
- frontmatter 字段分两组对照(与 plan-postcheck / taskcard-rules.md 同源,三处清单以此为准):
|
|
487
|
+
- **硬校验 9 字段**(缺失 plan-postcheck 直接报错阻断):id、title、title_zh、allowed_paths、goal、implementation、acceptance、verify、constraints
|
|
488
|
+
- **规范约定 5 字段**(应填但缺失只影响规范性,不阻断):author、created_at、priority、depends_on、blocks
|
|
474
489
|
- 每个 task 总长度 20~40 行
|
|
475
490
|
- **一致性自查**:
|
|
476
491
|
- allowed_paths 有无冲突
|
package/src/stages/quick.js
CHANGED
|
@@ -135,7 +135,7 @@ CLI 落盘的条目已是结构化产物(非简版骨架):标题从「需
|
|
|
135
135
|
- backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
|
|
136
136
|
- backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
|
|
137
137
|
|
|
138
|
-
一条 quick = 一条独立 ql 条目;核对只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/(
|
|
138
|
+
一条 quick = 一条独立 ql 条目;核对只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/quicklog/(git 跟踪,--done 后需提交),核对不影响 --done 已通过的边界审计。`,
|
|
139
139
|
outputHint: '暂存和记录确认',
|
|
140
140
|
optional: false
|
|
141
141
|
}
|
package/src/stages/scan.js
CHANGED
|
@@ -138,7 +138,7 @@ export const definition = {
|
|
|
138
138
|
e. 等待该项目所有子代理完成后,验证文件是否生成且非空
|
|
139
139
|
f. 该项目完成后,继续下一个项目
|
|
140
140
|
3. 所有项目完成后,运行以下命令检查产物:
|
|
141
|
-
\`
|
|
141
|
+
\`sillyspec workflow check scan-docs --project <project>\`(CLI 子命令,任意项目根可用)
|
|
142
142
|
对每个项目分别执行(将 \`<project>\` 替换为实际项目名)
|
|
143
143
|
4. 如果检查报告有失败项,按报告中的角色和文件重试失败的部分
|
|
144
144
|
|
|
@@ -165,7 +165,7 @@ export const definition = {
|
|
|
165
165
|
- 断点续扫步骤列出的缺失文档列表
|
|
166
166
|
- 环境探测结果摘要(如有 _env-detect.md,直接贴入)
|
|
167
167
|
- **⚠️ 必须强调:子代理必须用 write 工具将文件写入磁盘**
|
|
168
|
-
- **文件标题用中文**(sillyhub 平台解析识别用):frontmatter 必须在文件最前(第 1 行 --- 起),frontmatter 结束的 --- 之后空一行,再写 # 中文名(English)作为标题。严禁把 # 标题放到 frontmatter 之前(否则 frontmatter
|
|
168
|
+
- **文件标题用中文**(sillyhub 平台解析识别用):frontmatter 必须在文件最前(第 1 行 --- 起),frontmatter 结束的 --- 之后空一行,再写 # 中文名(English)作为标题。严禁把 # 标题放到 frontmatter 之前(否则 frontmatter 不在头部、「检查产物完整性」步的 frontmatter 检查会报缺 author/created_at)。各文档标准标题格式:
|
|
169
169
|
- STRUCTURE.md = # 目录结构(Structure)
|
|
170
170
|
- CONVENTIONS.md = # 代码约定(Conventions)
|
|
171
171
|
- ARCHITECTURE.md = # 架构(Architecture)
|
package/src/stages/verify.js
CHANGED
|
@@ -75,7 +75,7 @@ export const definition = {
|
|
|
75
75
|
### Execute Evidence 传递检查
|
|
76
76
|
7. 检查 verify-required-evidence.json 是否存在(由 execute 阶段 Task Review Gate 写入)
|
|
77
77
|
- 路径:变更目录下的 verify-required-evidence.json
|
|
78
|
-
- 文件 schema:\`{ items: [{ task, verdict, evidence: string[] }] }\`——顶层是 \`items\`,每项的 \`evidence\`
|
|
78
|
+
- 文件 schema:\`{ items: [{ task, verdict, evidence: string[] }] }\`——顶层是 \`items\`,每项的 \`evidence\` 是字符串数组(不是 \`requiredEvidence\` 键)
|
|
79
79
|
- 如果存在 → 逐项读取 \`items\`,对每个 cannot_verify 任务逐条核对其 \`evidence\` 数组是否已满足
|
|
80
80
|
- 每条 evidence 必须在 verify-result.md 中给出明确结论(satisfied / missing / partial)
|
|
81
81
|
- 如果有任何 evidence 为 missing → verify 结论不能为 PASS
|
|
@@ -149,9 +149,8 @@ export const definition = {
|
|
|
149
149
|
### 操作
|
|
150
150
|
1. 检查变更目录下 tasks/ 是否存在
|
|
151
151
|
2. 如果存在:
|
|
152
|
-
- 逐个读取 tasks/task-NN.md
|
|
153
|
-
-
|
|
154
|
-
- 未勾选的项列为不通过
|
|
152
|
+
- 逐个读取 tasks/task-NN.md,对照 frontmatter 的 \`acceptance:\` 列表逐条核验(TaskCard 协议的验收标准在 frontmatter YAML,正文无 checkbox)
|
|
153
|
+
- 每条 acceptance 对照实际实现/测试结果判定满足与否,未满足的项列为不通过
|
|
155
154
|
3. 如果不存在:跳过此步骤
|
|
156
155
|
|
|
157
156
|
### 输出
|
|
@@ -235,13 +234,13 @@ PASS / PASS WITH NOTES / FAIL
|
|
|
235
234
|
(自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,以声明为准并在此注明「显式声明 = <等级>」+ 理由)
|
|
236
235
|
|
|
237
236
|
## Runtime Evidence(integration-critical / deployment-critical 必填;自报告,CLI 仅校验字面存在、不独立核验,须真实执行过)
|
|
238
|
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
237
|
+
按本次变更实际触碰的运行时组件填写(模板是通用形态——长驻进程 / 服务端点 / 生命周期状态机的证据结构;未涉及的行写「不涉及」并注明原因,勿堆砌无关关键词):
|
|
238
|
+
- 长驻进程/服务 启动命令:
|
|
239
|
+
- 服务地址(本变更触碰的端点):
|
|
240
|
+
- 触发核心路径的请求/命令(附关键响应):
|
|
241
|
+
- 进程日志关键片段(证明走了新路径、无降级/回退分支):
|
|
242
|
+
- 生命周期终态断言(本变更涉及的状态机:初始态 → 运行态 → 终态,逐个确认到达):
|
|
243
|
+
- 失败模式排除(针对本变更声明的风险,逐条说明为何未触发):
|
|
245
244
|
|
|
246
245
|
## 代码审查
|
|
247
246
|
(问题列表 + 总体评价)
|
package/src/sync.js
CHANGED
|
@@ -452,6 +452,18 @@ export class SyncManager {
|
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
console.log(`[sync] 已同步变更: ${changeName}`);
|
|
455
|
+
|
|
456
|
+
// 2026-08-16-auto-sync-from-repo:进度上行成功后顺带推四件套文档(best-effort)。
|
|
457
|
+
// 根因:run 流程此前只推进度不推文档(本文件头注释),本地直跑 sillyspec 的产出
|
|
458
|
+
// 文档永远不自动到平台(变更中心"进度到了文档没到")。生产者直连,不依赖 daemon
|
|
459
|
+
// 缓存链路。失败仅 debugLog——文档同步失败不得影响进度上行的返回值与流程;
|
|
460
|
+
// 四件套全缺失时 syncDocuments 内部已有跳过(syncedCount===0 提前返回,不调端点)。
|
|
461
|
+
try {
|
|
462
|
+
await this.syncDocuments(changeName);
|
|
463
|
+
} catch (err) {
|
|
464
|
+
debugLog(`[sync] 文档同步失败(不影响进度): ${changeName}: ${err.message}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
455
467
|
return { synced: 1, errors: [] };
|
|
456
468
|
}
|
|
457
469
|
|
package/src/task-review.js
CHANGED
|
@@ -30,9 +30,9 @@ import { resolveRuntimeRoot } from './run/shared.js'
|
|
|
30
30
|
// 跨仓 task 写 review.json 可用 v2 带 repo 字段,单仓/旧 change 仍用 v1。后续可拆分 stage-review 专用
|
|
31
31
|
// 常量,本期遵循 allowed_paths 约束不扩散到 stage-review.js。
|
|
32
32
|
export const REVIEW_SCHEMA_VERSION = 1
|
|
33
|
-
|
|
33
|
+
const REVIEW_SCHEMA_VERSIONS_ACCEPTED = [1, 2]
|
|
34
34
|
// repo 缺省值(review.repo / task 卡 repo 缺省时统一按 'main' 处理)。design §7.2 / §7.4。
|
|
35
|
-
|
|
35
|
+
const DEFAULT_REPO_KEY = 'main'
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* 规范化 review.repo / task 卡 repo 字段为有效 repoKey。
|
|
@@ -53,7 +53,7 @@ export const VALID_VERDICTS = ['pass', 'fail', 'cannot_verify']
|
|
|
53
53
|
* @param {string} planContent - plan.md 文件内容
|
|
54
54
|
* @returns {string[]} task id 列表,如 ['task-01', 'task-02']
|
|
55
55
|
*/
|
|
56
|
-
|
|
56
|
+
function parseTaskIdsFromPlan(planContent) {
|
|
57
57
|
if (!planContent) return []
|
|
58
58
|
const ids = new Set()
|
|
59
59
|
const re = /^\s*[-*]\s*\[[ x]\]\s*task-(\d+)/gim
|
|
@@ -792,7 +792,15 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
792
792
|
} catch {}
|
|
793
793
|
if (!executeRunId) {
|
|
794
794
|
executeRunId = generateExecuteRunId()
|
|
795
|
-
|
|
795
|
+
// D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:marker 在则目录在)。
|
|
796
|
+
// 保留 fail-open 契约(调用方 catch 降级)——只去静默:失败 console.error 留痕,不 throw
|
|
797
|
+
//(草稿兜底缺了也只是退回 gate 报缺 review.json,不比修复前差)。
|
|
798
|
+
try {
|
|
799
|
+
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
800
|
+
writeFileSync(runIdFile, executeRunId + '\n')
|
|
801
|
+
} catch (e) {
|
|
802
|
+
console.error(`[sillyspec] execute run marker/目录写入失败(降级继续,草稿落盘仍尝试): ${e.message}`)
|
|
803
|
+
}
|
|
796
804
|
}
|
|
797
805
|
|
|
798
806
|
// base..head diff 文件集(worktree-aware;null=git 不可用,[]=无 commit diff)
|
package/src/workflow.js
CHANGED
|
@@ -48,7 +48,7 @@ export function loadWorkflow(cwd, name, validate = true) {
|
|
|
48
48
|
* @param {object} wf - workflow 定义
|
|
49
49
|
* @returns {string[]} 错误列表,空数组表示通过
|
|
50
50
|
*/
|
|
51
|
-
|
|
51
|
+
function validateWorkflow(wf) {
|
|
52
52
|
const errors = []
|
|
53
53
|
const roles = wf.roles || []
|
|
54
54
|
const roleIds = new Set(roles.map(r => r.id))
|
|
@@ -256,8 +256,13 @@ export function runPostCheck(wf, cwd, projectName, placeholders = {}, specBase)
|
|
|
256
256
|
let json = JSON.stringify(resolved)
|
|
257
257
|
for (const [key, value] of Object.entries(allPlaceholders)) {
|
|
258
258
|
// 支持 {key} 和 <key> 两种占位符语法
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
// 字符串值先做 JSON 转义再嵌入:Windows 路径等含反斜杠/引号的值裸替换进 JSON 字面量会
|
|
260
|
+
// 破坏 parse(实测报「Bad escaped character」→ scan 深度扫描质量门静默 fail-open)。
|
|
261
|
+
// JSON.stringify(v).slice(1,-1) 产出合法的 JSON 字符串内容(反斜杠→\\、引号→\");
|
|
262
|
+
// 非字符串值保持 String(v) 原样(数字占位符不被引号包裹,语义不变)。
|
|
263
|
+
const embedValue = (v) => (typeof v === 'string' ? JSON.stringify(v).slice(1, -1) : String(v))
|
|
264
|
+
json = json.replace(new RegExp(`\{${key}\}`, 'g'), embedValue(value))
|
|
265
|
+
json = json.replace(new RegExp(`<${key}>`, 'g'), embedValue(value))
|
|
261
266
|
}
|
|
262
267
|
resolved = JSON.parse(json)
|
|
263
268
|
|
|
@@ -537,188 +542,6 @@ export function formatCheckReport(result) {
|
|
|
537
542
|
return lines.join('\n')
|
|
538
543
|
}
|
|
539
544
|
|
|
540
|
-
// ─── 兼容适配层 ───
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* 兼容旧接口:generateRetryPrompt
|
|
544
|
-
* @deprecated 直接用 runPostCheck 返回的 retry_prompts
|
|
545
|
-
*/
|
|
546
|
-
export function generateRetryPrompt(wf, checkResult, projectName) {
|
|
547
|
-
const resolved = replaceProjectPlaceholder(wf, projectName)
|
|
548
|
-
const lines = []
|
|
549
|
-
lines.push('上一次 workflow 执行存在失败项,请重试。\n')
|
|
550
|
-
|
|
551
|
-
const roles = resolved.roles || []
|
|
552
|
-
const roleResults = checkResult.roles || []
|
|
553
|
-
for (const r of roleResults) {
|
|
554
|
-
if (r.status === 'pass') continue
|
|
555
|
-
const role = roles.find(rl => rl.id === r.id)
|
|
556
|
-
if (!role) continue
|
|
557
|
-
|
|
558
|
-
lines.push(`### 失败角色:${r.name} (${r.id})`)
|
|
559
|
-
lines.push(`失败原因:`)
|
|
560
|
-
const roleFailures = (checkResult.failures || []).filter(f => f.role_id === r.id)
|
|
561
|
-
for (const f of roleFailures) {
|
|
562
|
-
lines.push(`- ${f.message}`)
|
|
563
|
-
}
|
|
564
|
-
lines.push('')
|
|
565
|
-
|
|
566
|
-
for (const output of (role.outputs || [])) {
|
|
567
|
-
lines.push(`目标文件:\`${output.path}\``)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (role.constraints && role.constraints.length > 0) {
|
|
571
|
-
lines.push('约束:')
|
|
572
|
-
for (const c of role.constraints) {
|
|
573
|
-
lines.push(`- ${c}`)
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
lines.push('')
|
|
578
|
-
lines.push('⚠️ 你必须确保文件写入指定路径。不要只报告完成,请用 write 工具实际写入。')
|
|
579
|
-
lines.push('')
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return lines.join('\n')
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// ─── Role Prompt 生成(Level 2)───
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* 根据 workflow role 定义生成子代理 prompt
|
|
589
|
-
* @param {object} wf - workflow 定义
|
|
590
|
-
* @param {string} roleId - 角色ID
|
|
591
|
-
* @param {string} projectName - 项目名
|
|
592
|
-
* @param {object} context - 额外上下文(envSummary, missingDocs 等)
|
|
593
|
-
* @returns {string|null} 生成的 prompt,或 null(角色不存在)
|
|
594
|
-
*/
|
|
595
|
-
export function generateRolePrompt(wf, roleId, projectName, context = {}) {
|
|
596
|
-
const resolved = replaceProjectPlaceholder(wf, projectName)
|
|
597
|
-
const role = (resolved.roles || []).find(r => r.id === roleId)
|
|
598
|
-
if (!role) return null
|
|
599
|
-
|
|
600
|
-
const lines = []
|
|
601
|
-
lines.push(`## 子代理任务:${role.name} (${roleId})`)
|
|
602
|
-
lines.push('')
|
|
603
|
-
lines.push(`项目:${projectName}`)
|
|
604
|
-
|
|
605
|
-
// 任务描述
|
|
606
|
-
if (role.task) {
|
|
607
|
-
lines.push(`任务:${role.task}`)
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// 依赖角色的输出(depends_on + from_role)
|
|
611
|
-
const deps = role.depends_on || []
|
|
612
|
-
if (deps.length > 0) {
|
|
613
|
-
lines.push('')
|
|
614
|
-
lines.push('前置依赖(已完成角色的输出):')
|
|
615
|
-
const inputs = role.inputs || {}
|
|
616
|
-
for (const depId of deps) {
|
|
617
|
-
const depRole = (resolved.roles || []).find(r => r.id === depId)
|
|
618
|
-
if (!depRole) continue
|
|
619
|
-
if (inputs.from_role === depId) {
|
|
620
|
-
lines.push(`- ${depRole.name}(${depId}):${inputs.output_description || ''}`)
|
|
621
|
-
if (inputs.output) {
|
|
622
|
-
const depOutput = (depRole.outputs || []).find(o => {
|
|
623
|
-
const outputName = o.name || o.path?.split('/').pop()?.replace(/\.md$/, '') || ''
|
|
624
|
-
return outputName === inputs.output
|
|
625
|
-
})
|
|
626
|
-
if (depOutput) {
|
|
627
|
-
lines.push(` 输出文件:${depOutput.path}`)
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
} else {
|
|
631
|
-
lines.push(`- ${depRole.name}(${depId})`)
|
|
632
|
-
for (const o of (depRole.outputs || [])) {
|
|
633
|
-
lines.push(` 输出:${o.path}`)
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// 输入提示
|
|
640
|
-
const inputs = role.inputs || {}
|
|
641
|
-
const inputPaths = inputs.paths || []
|
|
642
|
-
const inputHints = inputs.hints || {}
|
|
643
|
-
if (inputPaths.length > 0) {
|
|
644
|
-
lines.push('')
|
|
645
|
-
lines.push('搜索范围:')
|
|
646
|
-
for (const p of inputPaths) {
|
|
647
|
-
lines.push(`- ${p}`)
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
if (inputHints.grep_patterns && inputHints.grep_patterns.length > 0) {
|
|
651
|
-
lines.push('')
|
|
652
|
-
lines.push('搜索关键词:')
|
|
653
|
-
lines.push(`- ${inputHints.grep_patterns.join(', ')}`)
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// 额外上下文
|
|
657
|
-
if (context.envSummary) {
|
|
658
|
-
lines.push('')
|
|
659
|
-
lines.push('环境探测结果:')
|
|
660
|
-
lines.push(context.envSummary)
|
|
661
|
-
}
|
|
662
|
-
if (context.missingDocs) {
|
|
663
|
-
lines.push('')
|
|
664
|
-
lines.push('缺失文档列表:')
|
|
665
|
-
lines.push(context.missingDocs)
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// 输出目标
|
|
669
|
-
const outputs = role.outputs || []
|
|
670
|
-
lines.push('')
|
|
671
|
-
lines.push('目标文件:')
|
|
672
|
-
for (const o of outputs) {
|
|
673
|
-
lines.push(`- \`${o.path}\``)
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// 约束
|
|
677
|
-
if (role.constraints && role.constraints.length > 0) {
|
|
678
|
-
lines.push('')
|
|
679
|
-
lines.push('约束:')
|
|
680
|
-
for (const c of role.constraints) {
|
|
681
|
-
lines.push(`- ${c}`)
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// 检查要求(告诉子代理需要满足什么)
|
|
686
|
-
for (const o of outputs) {
|
|
687
|
-
const checks = o.checks || []
|
|
688
|
-
for (const check of checks) {
|
|
689
|
-
if (check.type === 'contains_sections' && check.sections) {
|
|
690
|
-
lines.push('')
|
|
691
|
-
lines.push(`必须包含章节:${check.sections.map(s => `"## ${s}"`).join(', ')}`)
|
|
692
|
-
}
|
|
693
|
-
if (check.type === 'min_lines') {
|
|
694
|
-
lines.push(`文件长度要求:至少 ${check.min} 行`)
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
lines.push('')
|
|
700
|
-
lines.push('⚠️ 必须用 write 工具将文件写入磁盘!写完后用 read 工具确认文件存在!')
|
|
701
|
-
|
|
702
|
-
return lines.join('\n')
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* 为 workflow 的所有角色生成 role prompts
|
|
707
|
-
* @param {object} wf - workflow 定义
|
|
708
|
-
* @param {string} projectName - 项目名
|
|
709
|
-
* @param {object} context - 额外上下文
|
|
710
|
-
* @returns {Array<{ roleId: string, roleName: string, prompt: string }>}
|
|
711
|
-
*/
|
|
712
|
-
export function generateAllRolePrompts(wf, projectName, context = {}) {
|
|
713
|
-
const resolved = replaceProjectPlaceholder(wf, projectName)
|
|
714
|
-
const roles = resolved.roles || []
|
|
715
|
-
return roles.map(role => ({
|
|
716
|
-
roleId: role.id,
|
|
717
|
-
roleName: role.name || role.id,
|
|
718
|
-
prompt: generateRolePrompt(resolved, role.id, projectName, context)
|
|
719
|
-
}))
|
|
720
|
-
}
|
|
721
|
-
|
|
722
545
|
// ─── Workflow Run 归档 ───
|
|
723
546
|
|
|
724
547
|
|
package/src/worktree-apply.js
CHANGED
|
@@ -697,6 +697,124 @@ function rollbackApply(projectRoot, trackedFiles, newFiles) {
|
|
|
697
697
|
return { conflicts, error };
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
+
/**
|
|
701
|
+
* merge 前预对齐 baseline 并行文件到 main 版(D-002@v1 / task-02)。
|
|
702
|
+
*
|
|
703
|
+
* 背景:baseline checkpoint 把 execute 启动时主仓 dirty 的并行会话文件快照进了 worktree 分支
|
|
704
|
+
* (worktree.js _createBaselineCheckpoint)。apply --merge 时主仓常已把这批文件推进(并行会话
|
|
705
|
+
* 落了 commit),分支侧旧快照与 main 新版在 merge-base 两侧同改同一文件 → merge 冲突。预对齐在
|
|
706
|
+
* merge 前把这批文件 checkout 成 main 版并提交到分支,使三方合并对该文件 ours == theirs,
|
|
707
|
+
* 消除冲突主因。
|
|
708
|
+
*
|
|
709
|
+
* 过滤集(四条件全满足才对齐;任一不满足跳过该文件):
|
|
710
|
+
* a. baseline checkpoint 文件集(已提交口径):git diff --name-only <baseHash>..<baselineCommit>
|
|
711
|
+
* ——不用 result.changedFiles(工作区口径且已过滤基础设施文件)
|
|
712
|
+
* b. ∩ main 已推进集:git diff --name-only <merge-base>..<main HEAD>
|
|
713
|
+
* (merge-base = 分支与 main HEAD 的最近公共祖先)
|
|
714
|
+
* c. ∖ 分支已变更集:git diff --name-only <baselineCommit>..<branch> 含该文件(本变更交付,不动)
|
|
715
|
+
* d. ∖ worktree 工作区 dirty 集:git status --porcelain -- <file> 非空(未提交改动会被 checkout 覆盖)
|
|
716
|
+
* 另排除「main HEAD 与分支 tip 内容已一致」的文件(对齐无意义,且防 nothing-to-commit 空提交)。
|
|
717
|
+
*
|
|
718
|
+
* 执行 cwd:checkout/commit 在 worktreePath(改的是 worktree 分支的 index+工作区);mainHead /
|
|
719
|
+
* merge-base 锚点在 projectRoot 读取。merge 本身仍在 projectRoot(applyByMerge 原逻辑不动)。
|
|
720
|
+
*
|
|
721
|
+
* 降级(不阻断):不适用场景(无 baseline checkpoint / 非 worktree 模式(in-place 的 worktreePath
|
|
722
|
+
* 就是主工作区,在其上 commit 等于绕过验收污染 main;native-worktree 是外部隔离环境)/ worktree
|
|
723
|
+
* 目录丢失 / worktree HEAD ≠ 分支 tip(checkout+commit 会落错分支,merge 的却是 meta.branch))
|
|
724
|
+
* 静默跳过;任一 git 步骤失败 → best-effort 回滚已 checkout 文件 + console.warn + result.warnings
|
|
725
|
+
* → 调用方走原 merge 路径(降级不阻断,D-002@v1 约束③)。
|
|
726
|
+
*
|
|
727
|
+
* @param {object} p
|
|
728
|
+
* @param {object} p.meta - worktree meta(baseHash / baselineCommit / worktreePath / mode)
|
|
729
|
+
* @param {string} p.branch - 将被 merge 的分支名(meta.branch)
|
|
730
|
+
* @param {string} p.projectRoot - 主仓库根(merge 的执行侧)
|
|
731
|
+
* @param {object} p.result - applyWorktree 的 result(预对齐信息写入 result.warnings,可追溯,约束④)
|
|
732
|
+
* @returns {{ aligned: string[], commit: string|null }} aligned=本次对齐的文件(空数组=未对齐)
|
|
733
|
+
*/
|
|
734
|
+
function preAlignBaselineToMain({ meta, branch, projectRoot, result }) {
|
|
735
|
+
const pushWarning = (msg) => {
|
|
736
|
+
result.warnings = result.warnings || [];
|
|
737
|
+
result.warnings.push(msg);
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// 不适用场景(非失败,静默跳过)
|
|
741
|
+
if (!meta || !meta.baseHash || !meta.baselineCommit) return { aligned: [], commit: null };
|
|
742
|
+
if (meta.mode && meta.mode !== 'worktree') return { aligned: [], commit: null };
|
|
743
|
+
const worktreePath = meta.worktreePath;
|
|
744
|
+
if (!worktreePath || !existsSync(worktreePath)) return { aligned: [], commit: null };
|
|
745
|
+
const wtHead = gitQuiet(worktreePath, ['rev-parse', 'HEAD']);
|
|
746
|
+
const branchTip = gitQuiet(projectRoot, ['rev-parse', branch]);
|
|
747
|
+
if (!wtHead || !branchTip || wtHead !== branchTip) return { aligned: [], commit: null };
|
|
748
|
+
|
|
749
|
+
const { baseHash, baselineCommit } = meta;
|
|
750
|
+
const done = []; // 已 checkout 的文件(失败回滚用)
|
|
751
|
+
try {
|
|
752
|
+
// (a) baseline checkpoint 文件集(已提交口径;--no-renames:rename 退化为 D+A,两侧路径都进集)
|
|
753
|
+
const baselineSet = new Set(
|
|
754
|
+
(git(worktreePath, ['diff', '--no-renames', '--name-only', baseHash, baselineCommit]) || '')
|
|
755
|
+
.split('\n').filter(Boolean)
|
|
756
|
+
);
|
|
757
|
+
if (baselineSet.size === 0) return { aligned: [], commit: null };
|
|
758
|
+
|
|
759
|
+
// (b) main 已推进集(merge-base 后 main HEAD 的树差异;锚点拿不到 → 交原 merge,不算失败)
|
|
760
|
+
const mainHead = git(projectRoot, ['rev-parse', 'HEAD']);
|
|
761
|
+
const mergeBase = git(projectRoot, ['merge-base', branch, 'HEAD']);
|
|
762
|
+
if (!mainHead || !mergeBase) return { aligned: [], commit: null };
|
|
763
|
+
const mainAdvanced = new Set(
|
|
764
|
+
(git(projectRoot, ['diff', '--no-renames', '--name-only', mergeBase, mainHead]) || '')
|
|
765
|
+
.split('\n').filter(Boolean)
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// (c) 分支已变更集(baselineCommit → 分支 tip 的已提交交付,不动)
|
|
769
|
+
const branchChanged = new Set(
|
|
770
|
+
(git(worktreePath, ['diff', '--no-renames', '--name-only', baselineCommit, branchTip]) || '')
|
|
771
|
+
.split('\n').filter(Boolean)
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
// (d) worktree 工作区 dirty + 两侧 HEAD 同内容排除 → 最终对齐集
|
|
775
|
+
const candidates = [];
|
|
776
|
+
for (const f of baselineSet) {
|
|
777
|
+
if (!mainAdvanced.has(f) || branchChanged.has(f)) continue;
|
|
778
|
+
// dirty(checkout 会覆盖 worktree 未提交改动)→ 跳过该文件
|
|
779
|
+
if ((gitQuiet(worktreePath, ['status', '--porcelain', '--', f]) || '') !== '') continue;
|
|
780
|
+
// main HEAD 与分支 tip 内容已一致 → 对齐无意义 + 防空提交
|
|
781
|
+
if ((gitQuiet(worktreePath, ['diff', '--name-only', mainHead, branchTip, '--', f]) || '') === '') continue;
|
|
782
|
+
candidates.push(f);
|
|
783
|
+
}
|
|
784
|
+
if (candidates.length === 0) return { aligned: [], commit: null };
|
|
785
|
+
|
|
786
|
+
// 对齐:逐文件 checkout main 版(worktree 内执行,同时更新 worktree 分支的 index+工作区)
|
|
787
|
+
for (const f of candidates) {
|
|
788
|
+
git(worktreePath, ['checkout', mainHead, '--', f]);
|
|
789
|
+
done.push(f);
|
|
790
|
+
}
|
|
791
|
+
// 显式 pathspec 提交:只提交对齐文件,不扫入 worktree 内其他 staged 改动(坑 git-commit-sweeps-prestaged)。
|
|
792
|
+
// --no-verify:对齐 commit 与 baseline checkpoint 同性质(锚点非交付物,worktree.js _createBaselineCheckpoint
|
|
793
|
+
// 同款处理)——项目 pre-commit hook(如 husky lint+全量测试)会把预对齐拖成分钟级甚至失败降级。
|
|
794
|
+
git(worktreePath, [
|
|
795
|
+
'commit', '--no-verify', '-m',
|
|
796
|
+
`sillyspec: align baseline files to main (pre-merge, ${done.length} files)`,
|
|
797
|
+
'--', ...done,
|
|
798
|
+
]);
|
|
799
|
+
const shortHash = gitQuiet(worktreePath, ['rev-parse', '--short', 'HEAD']);
|
|
800
|
+
pushWarning(
|
|
801
|
+
`merge 前预对齐 ${done.length} 个 baseline 并行文件到 main 版` +
|
|
802
|
+
`${shortHash ? `(align commit ${shortHash})` : ''}:${done.join(', ')}`
|
|
803
|
+
);
|
|
804
|
+
return { aligned: done, commit: shortHash };
|
|
805
|
+
} catch (e) {
|
|
806
|
+
// 降级:任一步失败 → best-effort 回滚已 checkout 文件(checkout <branchTip> -- f 还原 index+工作区)
|
|
807
|
+
for (const f of done) {
|
|
808
|
+
gitQuiet(worktreePath, ['checkout', branchTip, '--', f]);
|
|
809
|
+
}
|
|
810
|
+
const msg = `baseline 预对齐失败,已跳过(降级走原 merge 路径):${(e.message || '').split('\n')[0]}` +
|
|
811
|
+
(done.length > 0 ? `;已回滚 ${done.length} 个已 checkout 文件` : '');
|
|
812
|
+
console.warn(`⚠️ ${msg}`);
|
|
813
|
+
pushWarning(msg);
|
|
814
|
+
return { aligned: [], commit: null };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
700
818
|
/**
|
|
701
819
|
* baseline 漂移时的 merge 降级路径(D-001)。
|
|
702
820
|
*
|
|
@@ -705,6 +823,7 @@ function rollbackApply(projectRoot, trackedFiles, newFiles) {
|
|
|
705
823
|
* git merge 比 patch 鲁棒,能处理 baseline 漂移 + 潜在冲突。会引入合并提交
|
|
706
824
|
* (D-002:与 worktree.md:84「patch 而非 merge 保持线性历史」架构决策的张力——
|
|
707
825
|
* 仅作 --merge 显式 opt-in,不改变默认 patch 行为)。
|
|
826
|
+
* merge 前先经 preAlignBaselineToMain 预对齐 baseline 并行文件到 main 版(D-002@v1 / task-02,失败降级走原 merge)。
|
|
708
827
|
*
|
|
709
828
|
* merge 冲突时不自动解决:git merge --abort 回滚到合并前状态 + 报冲突文件列表。
|
|
710
829
|
*
|
|
@@ -720,6 +839,9 @@ function applyByMerge(result, changeName, projectRoot, wm) {
|
|
|
720
839
|
// 用 meta.branch(native-worktree 模式分支名可能不是 sillyspec/<change>),不硬编码。
|
|
721
840
|
const branch = meta.branch || `sillyspec/${changeName}`;
|
|
722
841
|
|
|
842
|
+
// --- merge 前预对齐(D-002@v1 / task-02):baseline 并行旧文件对齐 main 版,消除 merge 冲突主因 ---
|
|
843
|
+
preAlignBaselineToMain({ meta, branch, projectRoot, result });
|
|
844
|
+
|
|
723
845
|
try {
|
|
724
846
|
git(projectRoot, ['merge', '--no-ff', branch], { timeout: 30000 });
|
|
725
847
|
} catch (e) {
|
package/src/worktree.js
CHANGED
|
@@ -58,7 +58,7 @@ export function detectIsolation(cwd = process.cwd()) {
|
|
|
58
58
|
* @param {string} cwd - 项目根目录
|
|
59
59
|
* @returns {{ ignored: boolean, path: string }}
|
|
60
60
|
*/
|
|
61
|
-
|
|
61
|
+
function checkWorktreeDirIgnored(cwd = process.cwd()) {
|
|
62
62
|
const relPath = WORKTREES_REL;
|
|
63
63
|
try {
|
|
64
64
|
git(cwd, ['check-ignore', '-q', relPath]);
|
|
@@ -195,7 +195,7 @@ function validateChangeName(changeName) {
|
|
|
195
195
|
* @param {string} cwd
|
|
196
196
|
* @returns {{ supported: boolean, version: string|null, reason?: string }}
|
|
197
197
|
*/
|
|
198
|
-
|
|
198
|
+
function isGitWorktreeSupported(cwd = process.cwd()) {
|
|
199
199
|
try {
|
|
200
200
|
const raw = git(cwd, ['--version']);
|
|
201
201
|
const match = raw.match(/git version (\d+)\.(\d+)/);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
|
|
13
13
|
7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
|
|
14
14
|
8. **实证核验再 `--done`**:`quick --done` 前,触及 `src`/`test` 的改动先 `npm test` + `npm run lint`;纯 doc/配置改动可跳过(lint 不扫 docs/,空转无信息),以落盘文件与测试结果为准,不信口头"已完成"。
|
|
15
|
-
9.
|
|
15
|
+
9. **中途停下不靠额外命令存进度**——进度已由上一次 `--done` 自动落盘;恢复时用 `sillyspec progress show` 查看进度,再用 `sillyspec run <stage>` 续跑,不直接 commit 半成品。
|
|
16
16
|
10. **实现完成后对照文档验收**(design.md / 模块文档),并检查是否影响已有测试。
|
|
17
17
|
11. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|
|
18
18
|
12. **hook 拦截提交时禁止跳过**(`.husky/pre-push`),修复问题后再提交。
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
TaskCard 格式规则(必须严格遵守):
|
|
2
2
|
- 总长度 20~40 行,不要写成长文档
|
|
3
3
|
- frontmatter 只含必要字段,不加 estimated_hours
|
|
4
|
+
- title 用英文短语(kebab 风格动词短语,如 "Add readonly auxiliary constant"),title_zh 用对应中文标题——两字段是同一任务的双语表示,非不同信息;归档产物与 sillyhub 平台解析各取所需(title_zh 为中文展示首选)
|
|
4
5
|
- goal: 一句话,用 > 多行字符串
|
|
5
6
|
- implementation: 列表,每条一个具体步骤
|
|
6
7
|
- acceptance: 列表,每条可独立验证(不是表格)
|
|
@@ -46,11 +46,11 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
|
|
|
46
46
|
3. Diff 对账:
|
|
47
47
|
- 前端调用路径在 backend 端点清单中找不到 → **❌ Missing backend endpoint**(advisory:CLI 复核后仅 warn,不硬阻断归档;但 contract gap 是真实集成缺陷,应诚实标 FAIL 并回 execute 补端点)
|
|
48
48
|
- backend 端点在前端无调用 → ⚠️ Unused backend endpoint(warning,不阻断)
|
|
49
|
-
4.
|
|
49
|
+
4. 输出对账结果表格(示例用通用占位路径,实际填本仓真实文件):
|
|
50
50
|
|
|
51
51
|
| 状态 | 前端调用 | 后端端点 | 文件 |
|
|
52
52
|
|---|---|---|---|
|
|
53
|
-
| ❌ missing | GET /api
|
|
53
|
+
| ❌ missing | GET /api/<域>/<资源>/<param> | — | <前端调用所在文件,如 src/lib/<模块>.ts> |
|
|
54
54
|
|
|
55
55
|
如果发现 Missing backend endpoint,在验证报告中标记为 ❌ contract gap(CLI 仅 advisory 提示、不硬阻断归档;但缺口真实存在——诚实判 FAIL 并回 execute 补齐端点,勿因 CLI 不拦而放行)。
|
|
56
56
|
|