deveco_hmigbot 0.21.5

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 (101) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +50 -0
  3. package/agents/hmigbot-worker.md +61 -0
  4. package/agents/hmigbot.md +22 -0
  5. package/agents/workflow-subagent.md +55 -0
  6. package/commands/hmigbot.md +17 -0
  7. package/dist/index.js +1 -0
  8. package/manifest.json +19 -0
  9. package/package.json +29 -0
  10. package/skills/migrate-core/FILES.md +26 -0
  11. package/skills/migrate-core/SKILL.md +484 -0
  12. package/skills/migrate-core/references/README.md +64 -0
  13. package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
  14. package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
  15. package/skills/migrate-core/references/flow/conventions-template.md +244 -0
  16. package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
  17. package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
  18. package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
  19. package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
  20. package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
  21. package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
  22. package/skills/migrate-core/references/host-capabilities.md +24 -0
  23. package/skills/migrate-core/references/topics/app-identity.md +214 -0
  24. package/skills/migrate-core/references/topics/env-doctor.md +245 -0
  25. package/skills/migrate-core/references/topics/i18n/README.md +458 -0
  26. package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
  27. package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
  28. package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
  29. package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
  30. package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
  31. package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
  32. package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
  33. package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
  34. package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
  35. package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
  36. package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
  37. package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
  38. package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
  39. package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
  40. package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
  41. package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
  42. package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
  43. package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
  44. package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
  45. package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
  46. package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
  47. package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
  48. package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
  49. package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
  50. package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
  51. package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
  52. package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
  53. package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
  54. package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
  55. package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
  56. package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
  57. package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
  58. package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
  59. package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
  60. package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
  61. package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
  62. package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
  63. package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
  64. package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
  65. package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
  66. package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
  67. package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
  68. package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
  69. package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
  70. package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
  71. package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
  72. package/skills/migrate-core/scripts/closure/idioms.json +105 -0
  73. package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
  74. package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
  75. package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
  76. package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
  77. package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
  78. package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
  79. package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
  80. package/skills/migrate-core/scripts/front.mjs +107 -0
  81. package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
  82. package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
  83. package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
  84. package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
  85. package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
  86. package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
  87. package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
  88. package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
  89. package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
  90. package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
  91. package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
  92. package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
  93. package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
  94. package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
  95. package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
  96. package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
  97. package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
  98. package/skills/migrate-core/templates/acceptance.js +365 -0
  99. package/skills/migrate-core/templates/explore.js +86 -0
  100. package/skills/migrate-core/templates/implement.js +211 -0
  101. package/skills/migrate-core/templates/mig_slices.js +491 -0
@@ -0,0 +1,365 @@
1
+ export const meta = {
2
+ name: 'acceptance',
3
+ description: '单元行为验收闭环:构建绿灯 → 逐单元验收(只读)→ 失败裁决 → 修复(有改码权)→ 复验;高风险单元对抗否证。不收敛即显式失败。',
4
+ phases: [
5
+ { title: 'Build', detail: '构建绿灯(退出码裁决,最多 N 轮修复)' },
6
+ { title: 'Verify', detail: '逐单元只读验收断言' },
7
+ { title: 'Fix', detail: '确认项修复(限本单元 targetPath)' },
8
+ { title: 'Adversary', detail: '高风险单元 2/3 对抗否证' },
9
+ ],
10
+ };
11
+
12
+ /*
13
+ * 验收模板 —— 修的是 verify_fix 的结构性缺陷:验出问题必须有人修、修完必须复验、
14
+ * 不收敛不许静默结束。设计对应《hmigbot 交付标准》验收环节。
15
+ *
16
+ * args = {
17
+ * goal: 一句话目标
18
+ * build: "$ cd <DST> && hvigorw assembleHap --no-daemon" // 必须 $ 命令形态
19
+ * planFile: plan.md 路径(含 "| u-xxx |" 单元表;启动时校验 units 覆盖它)
20
+ * conventionsFile: 规范文件路径(修复者必读)
21
+ * units: [{ id, targetPath, expectation, featureIds?, sliceIds? (验收单元id≠台账切片id时的映射,缺省=[id]), risk? ('auth'|'pay'|'push'|'share'|'crypto'|…) }]
22
+ * waived: [{ id, reason }] // 允许缺席的计划单元(必须给理由)
23
+ * only: [ids] // 只验这些(复跑)
24
+ * priorFile: "<spec>/accept-prior.json" // 跨 run 记忆:未过单元判词自动落盘/接力(限幅复验),不靠主会话转录
25
+ * force: false // true=已 accepted 单元也重验(默认跳过已 accepted 且无待复验判词的单元)
26
+ * maxRounds: 2 // 每单元 验→修→复验 轮数上限
27
+ * maxBuildRounds: 3
28
+ * batchSize: 4 // 已废弃(兼容忽略):验收改全量下发,并发由引擎 DW_CONCURRENCY 滑动窗控制
29
+ * models: { verify?, fix?, adversary?, fallback? } // 省略即继承会话;单模型部署可全空
30
+ * skills: [ ... ] // 传给修复者的技能名
31
+ * }
32
+ *
33
+ * 纪律(机制内置,不靠自觉):
34
+ * - 启动校验:units 必须覆盖 planFile 单元表(waived 除外),否则拒跑 —— "4 个对账顶 15 个单元"在此被拦死。
35
+ * - build 必须是 `$ ` 命令:退出码即裁决;文字条目一律拒绝。
36
+ * - verify agent 只读(提示词禁改码 + cache:false 防旧结论回放);修复只处理"验收确认失败"项。
37
+ * - 修复者不自评:复验永远由新的 verify agent 出裁决。
38
+ * - 可用性策略:一批内 null(死亡/超时)达半数(含恰好一半)→ 降批并发为 2、该批起换
39
+ * fallback 模型(只影响本次 runBatch,不污染全局)、退避 20s 重发;同一单元连续死亡
40
+ * 3 次记"无裁决失败"出队——绝不无限重发。
41
+ * - 末轮验收失败不再派修:修了也无人复验,"已修未验"不许记通过——直接记 failed 等 resume。
42
+ * - 对抗否证法定票数:存活票 <2 先重发一次,仍不足按未过处理(高风险门不弃权)。
43
+ * - 出口:任何单元不收敛 → throw(run 报 FAILED,携带 resumeFrom 提示),绝不把 PARTIAL 当完成。
44
+ */
45
+
46
+ const A = args || {};
47
+ const fail = (msg) => { throw new Error(`acceptance args: ${msg}`); };
48
+
49
+ // ---------- 0) 启动硬校验(任何 agent 起跑之前) ----------
50
+ if (!A.build || typeof A.build !== 'string') fail('缺 build');
51
+ if (!A.build.trim().startsWith('$ ')) fail('build 必须是 "$ <shell 命令>" 形态(退出码即裁决);文字条目不接受');
52
+ if (!Array.isArray(A.units) || A.units.length === 0) fail('units 为空');
53
+ for (const u of A.units) {
54
+ if (!u.id || !u.targetPath) fail(`单元缺 id/targetPath: ${JSON.stringify(u).slice(0, 80)}`);
55
+ if (!u.expectation || String(u.expectation).trim().length < 20) fail(`单元 ${u.id} 的 expectation 过短——验收标准必须原子可断言、可枚举必穷举、带源锚点`);
56
+ }
57
+ const ids = A.units.map(u => u.id);
58
+ if (new Set(ids).size !== ids.length) fail('units id 重复');
59
+
60
+ if (A.planFile) {
61
+ const planText = await readSection(A.planFile);
62
+ // 单元 id 形态可配(默认:小写词-连字符复合,如 u-models / page-item-list / backend-timer);
63
+ // 只认表格行**首列**(行锚定),避免描述列里的连字符词被误当计划单元
64
+ const idPat = new RegExp(`^\\s*\\|\\s*(${A.planIdPattern || '[a-z][a-z0-9]*(?:-[a-z0-9]+)+'})\\s*\\|`, 'gim');
65
+ let planIds = [...new Set([...planText.matchAll(idPat)].map(m => m[1]))];
66
+ // planFile 不是表格(SKILL 指向 instructions.md 这类 SECTION 工单)时首列正则抓不到任何 id,覆盖门会空过——
67
+ // 回退用台账切片表当计划(台账就是计划的真源;无台账才真空跑)
68
+ if (!planIds.length && A.ledgerCmd) {
69
+ const r = await check(`${A.ledgerCmd} list --type slices`);
70
+ planIds = [...new Set(String(r.ok ? r.output || '' : '').split('\n').filter(l => l.split('\t').length >= 3).map(l => l.split('\t')[0].trim()).filter(Boolean))];
71
+ log(planIds.length ? `planFile 无单元表,改用台账切片表作计划(${planIds.length} 片)` : `⚠ planFile 无单元表且台账无切片:覆盖门空过`);
72
+ }
73
+ const waived = new Map((A.waived || []).map(w => [w.id, w.reason]));
74
+ const missing = planIds.filter(pid => !ids.includes(pid) && !waived.has(pid));
75
+ if (missing.length) fail(`验收清单未覆盖 plan 单元表:缺 ${missing.join(', ')}(如确需缺席,用 waived:[{id,reason}] 显式豁免)`);
76
+ for (const [wid, reason] of waived) if (!reason || reason.length < 8) fail(`豁免 ${wid} 必须给出实质理由`);
77
+ log(`启动校验通过:plan 单元 ${planIds.length},受验 ${ids.length},豁免 ${(A.waived || []).length}`);
78
+ }
79
+
80
+ const only = Array.isArray(A.only) && A.only.length ? A.only : null;
81
+ if (only) for (const o of only) if (!ids.includes(o)) fail(`only 中的 ${o} 不在 units`);
82
+ let units = only ? A.units.filter(u => only.includes(u.id)) : A.units;
83
+ const maxRounds = Math.min(Math.max(A.maxRounds || 2, 1), 4);
84
+
85
+ // ---------- 0b) 跨 run 记忆(机制,不靠主会话转录) ----------
86
+ // 实证:一次迁移跑了 6 轮 accept、另一次 2 轮,每轮都从零重验全部单元,上轮判词一条没带。
87
+ // priorFile:本 run 结束把未过单元的判词写进去,下次 run 启动自动接力成 priorIssues → 限幅复验;
88
+ // 已 accepted(台账)且无待复验判词的单元直接跳过(force:true 强制全验)。
89
+ const PRIOR = A.priorFile ? String(A.priorFile).trim() : null;
90
+ // 十六进制走 shell:判词含引号/换行/中文,任何平台的命令行都不用转义(Windows cmd 8K 上限则分片追加)
91
+ const hex = (s) => encodeURIComponent(s).replace(/%([0-9A-F]{2})|(.)/g, (m, h, c) => h ? h.toLowerCase() : c.charCodeAt(0).toString(16).padStart(2, '0'));
92
+ async function readPrior() {
93
+ if (!PRIOR) return {};
94
+ const r = await check(`node -e "const f=process.argv[1];const fs=require('fs');process.stdout.write(fs.existsSync(f)?fs.readFileSync(f,'utf8'):'{}')" "${PRIOR}"`);
95
+ try { const j = r.ok ? JSON.parse(String(r.output || '{}')) : {}; return j && typeof j === 'object' ? j : {}; } catch { return {}; }
96
+ }
97
+ async function writePrior(obj) {
98
+ if (!PRIOR) return;
99
+ const h = hex(JSON.stringify(obj));
100
+ const CH = 6000;
101
+ for (let i = 0; i < h.length || i === 0; i += CH) {
102
+ const part = h.slice(i, i + CH) || '';
103
+ await check(`node -e "require('fs').${i === 0 ? 'writeFileSync' : 'appendFileSync'}(process.argv[1],process.argv[2]||'')" "${PRIOR}.hex" ${part}`);
104
+ }
105
+ const w = await check(`node -e "const fs=require('fs');const p=process.argv[1];fs.writeFileSync(p,Buffer.from(fs.readFileSync(p+'.hex','utf8'),'hex').toString('utf8'));fs.unlinkSync(p+'.hex')" "${PRIOR}"`);
106
+ if (!w.ok) log(`⚠ priorFile 写入失败:${(w.output || '').slice(0, 200)}`);
107
+ }
108
+ if (PRIOR) {
109
+ const prior = await readPrior();
110
+ let carried = 0;
111
+ for (const u of units) {
112
+ if (Array.isArray(u.priorIssues) && u.priorIssues.length) continue;
113
+ if (Array.isArray(prior[u.id]) && prior[u.id].length) { u.priorIssues = prior[u.id].slice(0, 20); carried++; }
114
+ }
115
+ if (carried) log(`上轮判词接力:${carried} 个单元带 priorIssues 进限幅复验(${PRIOR})`);
116
+ }
117
+ const skippedAccepted = [];
118
+ if (A.ledgerCmd && !A.force) {
119
+ // list 的数据行是 4 列 tab 分隔(id\tkind\tstatus\ttitle),汇总行走 stderr 但 check() 会把两路合并——只认 ≥3 列的行,否则 "[ledger] slices 2 条" 会被当成 id
120
+ const listIds = (out) => new Set(String(out || '').split('\n').filter(l => l.split('\t').length >= 3).map(l => l.split('\t')[0].trim()).filter(Boolean));
121
+ const idsOf = async (q) => { const r = await check(`${A.ledgerCmd} list ${q}`); return listIds(r.ok ? r.output : ''); };
122
+ const acc = await idsOf('--type slices --status accepted');
123
+ if (acc.size) {
124
+ // 跳过条件:片已 accepted **且**其断言已过账(passed/waived)。只看片状态会把地基波"片 accepted 但断言从未过账"的
125
+ // 单元永久跳过,断言留在 untested(实证一轮 52 条)——断言未过账的单元照常验收过账。
126
+ const passed = await idsOf('--type features --status passed');
127
+ const waived = await idsOf('--type features --status waived');
128
+ const keep = [], pending = [];
129
+ for (const u of units) {
130
+ const sids = Array.isArray(u.sliceIds) && u.sliceIds.length ? u.sliceIds : [u.id];
131
+ const done = sids.every(s => acc.has(s));
132
+ const fids = Array.isArray(u.featureIds) ? u.featureIds : [];
133
+ const booked = fids.every(f => passed.has(f) || waived.has(f));
134
+ if (done && booked && !(Array.isArray(u.priorIssues) && u.priorIssues.length)) skippedAccepted.push(u.id);
135
+ else { keep.push(u); if (done && !booked) pending.push(u.id); }
136
+ }
137
+ if (skippedAccepted.length) { log(`已 accepted 且断言已过账的单元跳过重验 ${skippedAccepted.length} 个:${skippedAccepted.join(', ')}`); units = keep; }
138
+ if (pending.length) log(`已 accepted 但断言未过账的单元照常验收 ${pending.length} 个:${pending.join(', ')}`);
139
+ }
140
+ }
141
+ if (!units.length) log('无待验单元(全部已 accepted 或被 only 排除)——直接收口');
142
+ const maxBuildRounds = Math.min(Math.max(A.maxBuildRounds || 3, 1), 6);
143
+ const M = A.models || {};
144
+ const opt = (model, extra) => model ? { model, ...extra } : { ...extra };
145
+ // 末端验收即"判断性验证唯一一遍",过账在此:slice→accepted、featureIds→passed(on_device 跳过)。
146
+ const ledgerWarns = [];
147
+ async function ledger(op) {
148
+ if (!A.ledgerCmd) return;
149
+ let r = await check(`${A.ledgerCmd} ${op}`);
150
+ // 宿主重解压内置技能时脚本文件会短暂消失(另一版本 deveco 在同机启动即触发,实证 71 秒窗口):等 5 s 重试两次,不把丢账当结果
151
+ for (let i = 0; i < 2 && !r.ok && /Cannot find module/.test(r.output || ''); i++) { await check(`node -e "setTimeout(()=>{},5000)"`); r = await check(`${A.ledgerCmd} ${op}`); }
152
+ if (!r.ok) { const w = `台账写入失败:${op.slice(0, 140)} → ${(r.output || '').trim().slice(0, 200)}`; ledgerWarns.push(w); log(`⚠ ${w}`); }
153
+ }
154
+
155
+ const VERIFY_SCHEMA = {
156
+ type: 'object', required: ['pass', 'evidence'],
157
+ properties: {
158
+ pass: { type: 'boolean' },
159
+ evidence: { type: 'string' },
160
+ issues: { type: 'array', items: { type: 'object', required: ['claim', 'anchor'], properties: { claim: { type: 'string' }, anchor: { type: 'string' }, severity: { type: 'string' } } } },
161
+ },
162
+ };
163
+ const FIX_SCHEMA = {
164
+ type: 'object', required: ['done', 'summary', 'files'],
165
+ properties: { done: { type: 'boolean' }, summary: { type: 'string' }, files: { type: 'array', items: { type: 'string' } }, blocked: { type: 'string' } },
166
+ };
167
+ const VOTE_SCHEMA = { type: 'object', required: ['refuted', 'reason'], properties: { refuted: { type: 'boolean' }, reason: { type: 'string' } } };
168
+
169
+ // ---------- 1) Build 绿灯 ----------
170
+ phase('Build');
171
+ let built = false;
172
+ for (let r = 1; r <= maxBuildRounds; r++) {
173
+ const res = await check(A.build.trim().slice(2));
174
+ if (res.ok) { built = true; log(`构建绿灯(第 ${r} 轮)`); break; }
175
+ log(`构建失败(第 ${r} 轮),派修复代理`);
176
+ if (r === maxBuildRounds) break;
177
+ await agent(
178
+ `构建失败。修复以下构建错误,只改必要文件,禁止降级/删功能/注释掉代码来止血。\n` +
179
+ `必读规范:${A.conventionsFile || '(未提供)'}\n构建输出(截断):\n${(res.output || '').slice(-6000)}`,
180
+ opt(M.fix, { label: `fix-build-r${r}`, phase: 'Build', cache: false, skills: A.skills || ['arkts-error-fixes', 'arkts-runtime-fix'] })
181
+ );
182
+ }
183
+ if (!built) throw new Error(`acceptance FAILED: 构建 ${maxBuildRounds} 轮未收敛。修复后用 resumeFrom 续跑。`);
184
+
185
+ // ---------- 2) 逐单元 验 → 裁 → 修 → 复验 ----------
186
+ phase('Verify');
187
+ const results = [];
188
+ async function verifyUnit(u, round, model) {
189
+ // 限幅复验:本 run 上一轮或上次 run(priorFile 接力)留下的判词都算——复验只裁清单、不重开全面发现
190
+ const reverify = Array.isArray(u.priorIssues) && u.priorIssues.length;
191
+ return agent(
192
+ `你是验收裁判,【只读,禁止修改任何文件】。逐条断言验收该单元是否达成:\n` +
193
+ `单元 ${u.id},产物目录:${u.targetPath}\n完成标准(逐条核对,每条给证据或反证):\n${u.expectation}\n` +
194
+ (reverify
195
+ ? `【复验轮·限幅】只裁决下列上轮问题是否已修复(逐条给证据);清单外新发现如实列入 issues 标 severity:"new",但不据此翻转本单元裁决(属首轮漏检,单独上报收尾):\n- ${u.priorIssues.slice(0, 20).join('\n- ')}\n`
196
+ : `【首轮·穷举】必须一次列全全部不达标项再下结论——挤牙膏式逐轮冒新问题 = 裁判失职。\n`) +
197
+ `判定基线:台账已登记且已裁决的降级(status open)/债务对应偏差不计失败,只核映射说明与实现一致;标"待裁决"(pending)或"已退回"(rejected)的降级不算已登记,对应缺口照常判失败${A.ledgerCmd ? `(查看:\`${A.ledgerCmd} render\`,只读)` : ''};` +
198
+ `kind 为 on_device 的断言本环节验不了:列入 issues 标 severity:"deferred"、不计入 pass 裁决(4b 真机验)。\n` +
199
+ `纪律:结论必须附证据(文件:行号,路径写相对工程根或绝对路径 / 实际执行的命令与输出);无证据的"通过"按失败处理;` +
200
+ `"验过没问题"与"没验到"必须分开表述,验不到的条目如实说明并计入 issues。`,
201
+ opt(model, { label: `verify:${u.id}${round > 1 ? `-r${round}` : ''}`, phase: 'Verify', cache: false, schema: VERIFY_SCHEMA })
202
+ );
203
+ }
204
+ async function runBatch(us, round) {
205
+ // 全量一次下发:引擎并发窗(DW_CONCURRENCY)是滑动信号量,自动补位——
206
+ // 分批 await 是人为批间栅栏,最慢单元拖住整批(实测 31 单元 ×8 批被拖 ~2 倍时长)。
207
+ let vModel = M.verify; // 换发只作用于本 runBatch,不改写全局 M
208
+ const out = [];
209
+ const deaths = new Map(); // 单元连续死亡计数——防无限重发
210
+ let wave = [...us];
211
+ while (wave.length) {
212
+ // 结构化失败时引擎回退原文字符串:无 pass 字段,若放行会既不过也不败、单元静默蒸发——按死亡重发
213
+ const rs = await parallel(wave.map(u => () => verifyUnit(u, round, vModel).then(v => ({ u, v: (v !== null && typeof v !== 'object') ? (log(`⚠ ${u.id} 裁决非结构化文本,按死亡重发`), null) : v }))));
214
+ // 死亡判定看 v==null:引擎契约里 agent 死亡是 agent() 返回 null(不是 thunk 抛异常),
215
+ // 所以 parallel 项是 {u, v:null} 而非 null——按 x===null 判死等于从不判死,死亡单元会静默蒸发。
216
+ const dead = rs.filter(x => !x || x.v == null).length;
217
+ const retry = [];
218
+ rs.forEach((x, i) => {
219
+ if (x && x.v != null) { out.push(x); return; }
220
+ const n = (deaths.get(wave[i].id) || 0) + 1;
221
+ deaths.set(wave[i].id, n);
222
+ if (n >= 3) {
223
+ log(`✗ ${wave[i].id} 验收 agent 连续死亡 ${n} 次,记无裁决出队(不盲修、不再重发)`);
224
+ out.push({ u: wave[i], v: null, dead: true });
225
+ } else retry.push(wave[i]);
226
+ });
227
+ if (dead && dead * 2 >= wave.length) { // 波内死亡达半数 = 网关性问题,换 fallback+退避(配额类错误引擎已直接熔断整流)
228
+ if (M.fallback) vModel = M.fallback;
229
+ log(`可用性策略:本波 ${wave.length} 个中 ${dead} 个无输出死亡 → ${M.fallback ? '换 fallback 模型、' : ''}退避 20s 重发`);
230
+ await check('node -e "setTimeout(()=>{},20000)"'); // 跨平台等待(Windows 无 sleep)
231
+ } else if (dead > 0) {
232
+ log(`${dead} 个验收代理死亡,重新排队`);
233
+ }
234
+ wave = retry;
235
+ }
236
+ return out;
237
+ }
238
+
239
+ let pending = [...units];
240
+ const lastIssues = new Map(); // 单元 → 最近一轮判词(写 priorFile 用)
241
+ const issueLines = (v) => (v && Array.isArray(v.issues) ? v.issues : []).map(i => `${i.claim}${i.anchor ? `(${i.anchor})` : ''}`).slice(0, 20);
242
+ for (let round = 1; round <= maxRounds && pending.length; round++) {
243
+ const verdicts = await runBatch(pending, round);
244
+ for (const x of verdicts.filter(x => x.dead)) {
245
+ results.push({ id: x.u.id, status: 'failed', round, evidence: '验收 agent 连续死亡无裁决(网络/网关问题;resumeFrom + only 复验,不派盲修)' });
246
+ }
247
+ const failed = verdicts.filter(x => x.v && x.v.pass === false);
248
+ for (const x of failed) lastIssues.set(x.u.id, issueLines(x.v).length ? issueLines(x.v) : [String(x.v.evidence || '').slice(0, 300)]);
249
+ for (const x of verdicts.filter(x => x.v && x.v.pass === true)) {
250
+ results.push({ id: x.u.id, status: 'passed', round, evidence: x.v.evidence });
251
+ lastIssues.delete(x.u.id);
252
+ }
253
+ if (!failed.length) { pending = []; break; }
254
+ if (round === maxRounds) {
255
+ // 末轮不派修:修了也无人复验,"已修未验"不许记通过——账面必须诚实
256
+ for (const x of failed) results.push({ id: x.u.id, status: 'failed', round, evidence: '达到轮数上限仍未通过' });
257
+ pending = [];
258
+ break;
259
+ }
260
+ phase('Fix');
261
+ log(`第 ${round} 轮:${failed.length} 个单元验收失败,进入修复`);
262
+ const fixes = await parallel(failed.map(x => () => agent(
263
+ `验收裁判确认该单元未达标,请修复。你有改码权,但【只准改 ${x.u.targetPath} 及其下文件】;` +
264
+ `需要动共享/壳文件的,不要动手,在返回值 blocked 里写清楚需要谁改什么。\n` +
265
+ `必读规范:${A.conventionsFile || '(未提供)'}\n单元 ${x.u.id} 的完成标准:\n${x.u.expectation}\n` +
266
+ `裁判判词与证据:\n${JSON.stringify(x.v.issues && x.v.issues.length ? x.v.issues : x.v.evidence).slice(0, 4000)}\n` +
267
+ `纪律:修真因不糊表面;禁止为过验收而降级功能;改完自查能编译;你的修复是否达标由下一轮裁判裁决,不要自评"已修复"。`,
268
+ opt(M.fix, { label: `fix:${x.u.id}-r${round}`, phase: 'Fix', cache: false, schema: FIX_SCHEMA, skills: A.skills || ['arkts-error-fixes', 'arkts-runtime-fix'] })
269
+ ).then(fx => ({ u: x.u, fx }))));
270
+ const blocked = fixes.filter(f => f && f.fx && f.fx.blocked);
271
+ for (const b of blocked) log(`⚠ ${b.u.id} 修复受阻(越权项):${b.fx.blocked}`);
272
+ // 修复后回构建确认没把楼修塌
273
+ const rb = await check(A.build.trim().slice(2));
274
+ if (!rb.ok) {
275
+ log('修复后构建变红,派一次构建修复');
276
+ await agent(`修复引入了构建错误,恢复绿灯(禁止回退掉刚才的行为修复本身)。构建输出:\n${(rb.output || '').slice(-6000)}`,
277
+ opt(M.fix, { label: `fix-build-after-r${round}`, phase: 'Fix', cache: false }));
278
+ const rb2 = await check(A.build.trim().slice(2));
279
+ if (!rb2.ok) throw new Error('acceptance FAILED: 修复后构建无法恢复绿灯');
280
+ }
281
+ // 复验限幅:把上轮判词带给下一轮裁判——复验只裁清单、不重开全面发现(防逐轮冒新问题的 churn)
282
+ pending = failed.map(x => ({ ...x.u, priorIssues: issueLines(x.v) }));
283
+ }
284
+
285
+ // ---------- 3) 高风险单元对抗否证(2/3 多数才算过) ----------
286
+ // 风险靠派生不靠猜:对抗否证贵(每单元 3 票),只压**平台实际有差异**的片。risky 由调用方(模型)
287
+ // 从台账按鸿蒙平台实际派生进 `u.risk`——碰 degradations(无鸿蒙等价)/ 开放 debts(平台驱动 parity 决策)/
288
+ // 平台模型差异断言(backend 的后台执行/线程并发/生命周期,即结果契约≠机制处)的片才 risky。
289
+ // 不用硬编码关键词正则(普通 app 过度匹配、把对抗铺满全片、拖长尾巴收益低,且不反映平台真实差异)。
290
+ const risky = units.filter(u => u.risk && results.find(r => r.id === u.id && r.status === 'passed'));
291
+ if (risky.length) {
292
+ phase('Adversary');
293
+ log(`高风险单元 ${risky.length} 个,进入 2/3 对抗否证(跨单元并行——末端集中判断,串行会被断连整体拖死)`);
294
+ // 各单元的对抗独立,跨单元并行:一个单元的票被断连拖着不阻塞其他单元(轻编排实证:
295
+ // 末端集中判断把断连暴露也集中到末端,串行对抗遇断连簇拖了 1h)。单元内 3 票仍并行。
296
+ const adjudicate = async (u) => {
297
+ const castVotes = (suffix) => parallel([1, 2, 3].map(i => () => agent(
298
+ `你是否证者#${i}。该单元已通过常规验收,你的唯一任务是【推翻它】:找出它与完成标准不符的实证。\n` +
299
+ `单元 ${u.id},目录 ${u.targetPath}\n完成标准:\n${u.expectation}\n` +
300
+ `拿得出实证才算 refuted=true;拿不准按 refuted=false 但写明疑点。只读,禁改码。`,
301
+ opt(M.adversary || M.verify, { label: `adversary:${u.id}#${i}${suffix}`, phase: 'Adversary', cache: false, schema: VOTE_SCHEMA })
302
+ )));
303
+ let votes = (await castVotes('')).filter(Boolean);
304
+ if (votes.length < 2) {
305
+ log(`⚠ ${u.id} 否证存活票不足(${votes.length}/3),重发一次`);
306
+ votes = (await castVotes('-retry')).filter(Boolean);
307
+ }
308
+ const r = results.find(r => r.id === u.id);
309
+ const refuters = votes.filter(v => v.refuted);
310
+ if (votes.length < 2) {
311
+ r.status = 'failed';
312
+ r.evidence = '对抗否证两度组不齐法定票数(agent 死亡)——resumeFrom 复验';
313
+ log(`✗ ${u.id} 否证法定票数不足,判未过(高风险门不弃权)`);
314
+ } else if (refuters.length >= 2) {
315
+ r.status = 'refuted';
316
+ r.evidence = `对抗否证 ${refuters.length}/${votes.length} 推翻:` + refuters.map(v => v.reason).join(' | ').slice(0, 800);
317
+ log(`✗ ${u.id} 被对抗否证推翻`);
318
+ } else {
319
+ log(`✓ ${u.id} 通过对抗否证(${refuters.length}/${votes.length} 推翻票,存活 ${votes.length}/3)`);
320
+ }
321
+ };
322
+ await parallel(risky.map(u => () => adjudicate(u)));
323
+ }
324
+
325
+ // ---------- 4) 末端一次过账(判断性验证结论落台账;单一写手在此) ----------
326
+ const passed = results.filter(r => r.status === 'passed');
327
+ const bad = results.filter(r => r.status !== 'passed');
328
+ if (A.ledgerCmd) {
329
+ phase('Post');
330
+ for (const r of passed) {
331
+ const u = units.find(x => x.id === r.id);
332
+ // 验收单元 id ≠ 台账切片 id 时用 sliceIds 映射(缺省=id)——否则过账必失败只能靠 ledgerWarns 响亮(实证:主会话被迫手工补账)
333
+ const sids = u && Array.isArray(u.sliceIds) && u.sliceIds.length ? u.sliceIds : [r.id];
334
+ for (const sid of sids) await ledger(`status --type slices --id ${sid} --to accepted --evidence "末端验收通过(判断性一遍)"`);
335
+ if (u && Array.isArray(u.featureIds) && u.featureIds.length) {
336
+ // 判词里的 文件:行(只取 shell 安全字符,最多 6 处)随过账写进台账证据——台账按"证据可定位"计数,散文证据不算验过
337
+ const where = [...new Set([...String(r.evidence || '').matchAll(/((?:[A-Za-z]:)?[A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+):(\d+)/g)].map(m => `${m[1]}:${m[2]}`))].slice(0, 6).join(' ');
338
+ await ledger(`status --type features --ids ${u.featureIds.join(',')} --to passed --skip-kind on_device --evidence "末端验收 ${r.id} 通过(静态断言;on_device 待 4b)${where ? ';证据 ' + where : ''}"`);
339
+ }
340
+ }
341
+ if (ledgerWarns.length) log(`⚠ 台账写入失败 ${ledgerWarns.length} 次——报告前须核对补账`);
342
+ }
343
+ // 跨 run 记忆落盘:未过单元的最近判词(refuted 带推翻理由);通过的单元从记忆里清掉
344
+ if (PRIOR) {
345
+ const mem = {};
346
+ for (const r of bad) mem[r.id] = r.status === 'refuted' ? [String(r.evidence).slice(0, 400)] : (lastIssues.get(r.id) || [String(r.evidence).slice(0, 300)]);
347
+ await writePrior(mem);
348
+ if (bad.length) log(`未过单元判词已写 ${PRIOR}(${bad.length} 个)——下次 run 自动接力限幅复验`);
349
+ }
350
+ log(`验收收口:${passed.length}/${units.length} 通过${skippedAccepted.length ? `,已 accepted 跳过 ${skippedAccepted.length}` : ''}${(A.waived || []).length ? `,豁免 ${A.waived.length}` : ''}${bad.length ? `,未过 ${bad.map(b => b.id).join(',')}` : ''}`);
351
+ const summary = {
352
+ goal: A.goal || '',
353
+ built: true,
354
+ unitsTotal: units.length,
355
+ skippedAccepted,
356
+ passed: passed.map(r => ({ id: r.id, evidence: String(r.evidence).slice(0, 400) })),
357
+ failed: bad.map(r => ({ id: r.id, status: r.status, evidence: String(r.evidence).slice(0, 400), issues: lastIssues.get(r.id) || [] })),
358
+ waived: A.waived || [],
359
+ ...(ledgerWarns.length ? { ledgerWarns } : {}),
360
+ };
361
+ if (bad.length) {
362
+ throw new Error(`acceptance FAILED: ${bad.length} 个单元未收敛(${bad.map(b => b.id).join(', ')})。` +
363
+ `修复后用 only:[${bad.map(b => `"${b.id}"`).join(',')}] + resumeFrom 只复验失败项。详情见 run 报告。`);
364
+ }
365
+ return summary;
@@ -0,0 +1,86 @@
1
+ export const meta = { name: 'explore', description: 'Parallel per-area exploration writing one doc per area plus a compact map' }
2
+
3
+ const fail = (msg) => { throw new Error('workflow args: ' + msg) }
4
+ const got = (v) => v === undefined ? 'missing' : v === null ? 'null' : Array.isArray(v) ? 'an array of ' + v.length : typeof v === 'string' ? 'the string ' + JSON.stringify(v.length > 80 ? v.slice(0, 80) + '…' : v) : typeof v
5
+ const isStr = (v) => typeof v === 'string' && v.trim().length > 0
6
+ const asArr = (v) => {
7
+ if (Array.isArray(v)) return v
8
+ if (typeof v === 'string') {
9
+ const s = v.trim()
10
+ if (s[0] === '[' || s[0] === '{') { try { const p = JSON.parse(s); if (Array.isArray(p)) return p; if (p && typeof p === 'object') return [p] } catch (e) {} }
11
+ return s ? [s] : []
12
+ }
13
+ return v && typeof v === 'object' ? [v] : []
14
+ }
15
+ const skillStep = (...vs) => {
16
+ const sk = []
17
+ for (const v of vs) for (const s of asArr(v)) if (isStr(s) && !sk.includes(s)) sk.push(s)
18
+ return sk.length ? 'Before starting, use the skill tool to load and follow these skills: ' + sk.join(', ') + '.' : ''
19
+ }
20
+ const norm = (r, strictPass) => {
21
+ if (r === null || r === undefined) return { ok: false, summary: '(agent failed)' }
22
+ if (typeof r === 'string') return strictPass ? { ok: false, summary: r.slice(0, 2000) } : { ok: true, unverified: true, summary: r.slice(0, 2000) }
23
+ if (typeof r === 'object') {
24
+ const okFlag = r.ok === undefined ? r.pass : r.ok
25
+ return { ...r, ok: strictPass ? okFlag === true : okFlag !== false, summary: typeof r.summary === 'string' ? r.summary : '' }
26
+ }
27
+ return { ok: !strictPass, summary: String(r) }
28
+ }
29
+ // '$ ' 前缀 = 字面 shell 命令:引擎 check() 直跑,退出码即结论(比模型转述更硬的证据,
30
+ // 还省一个子会话);无前缀 = 交给检查代理执行并评判(携带判定标准的文字检查走这条)。
31
+ const isCmd = (v) => isStr(v) && v.trim().indexOf('$ ') === 0
32
+ const cmdOf = (v) => v.trim().slice(2)
33
+ const CHECK_ERRORS_SCHEMA = { type: 'object', required: ['errors'], properties: { errors: { type: 'array', items: { type: 'object', required: ['message'], properties: { file: { type: 'string' }, message: { type: 'string' } } } } } }
34
+
35
+ if (!args || typeof args !== 'object') fail("explore requires args: { goal, docDir, areas: [{ name, ... }] } — got " + got(args) + '. Pass args as a real JSON object (not a quoted string); for large args, Write a JSON file and pass argsFile.')
36
+ if (!isStr(args.goal)) fail('explore requires args.goal (what this exploration is for) — got ' + got(args.goal))
37
+ if (!isStr(args.docDir)) fail('explore requires args.docDir (ABSOLUTE directory for the per-area docs) — got ' + got(args.docDir))
38
+ let areas = asArr(args.areas).filter((a) => a && isStr(a.name))
39
+ if (!areas.length) fail('explore requires args.areas: [{ name, focus?, sources?, doc?, detail? }] (at least one) — got ' + got(args.areas))
40
+ if (asArr(args.only).length) {
41
+ const only = new Set(args.only)
42
+ areas = areas.filter((a) => only.has(a.name))
43
+ if (!areas.length) fail('args.only matched no area names')
44
+ log('re-running only: ' + areas.map((a) => a.name).join(', '))
45
+ }
46
+ if (areas.length > 50) fail('explore: more than 50 areas — merge related areas')
47
+
48
+ const seenDocs = new Set()
49
+ const docs = areas.map((a, i) => {
50
+ let d = isStr(a.doc) ? a.doc : (a.name.toLowerCase().replace(/[^\p{L}\p{N}._-]+/gu, '_').replace(/^_+|_+$/g, '') || 'area_' + (i + 1)) + '.md'
51
+ while (seenDocs.has(d)) d = d.replace(/\.md$/, '') + '_' + (i + 1) + '.md'
52
+ seenDocs.add(d)
53
+ return d
54
+ })
55
+ const areaPrompt = (a, i) => {
56
+ const parts = []
57
+ parts.push('You are ONE of ' + areas.length + ' parallel explorers. Goal of the overall task: ' + args.goal)
58
+ if (isStr(args.context)) parts.push('Shared context / constraints:\n' + args.context)
59
+ if (isStr(args.instructionsFile)) parts.push('First READ the shared instructions file at ' + args.instructionsFile + ' (Read tool) and follow its common rules.')
60
+ const skills = skillStep(args.skills, a.skills)
61
+ if (skills) parts.push(skills)
62
+ parts.push('Your area: ' + a.name + (isStr(a.focus) ? ' — focus: ' + a.focus : ''))
63
+ if (isStr(a.detail)) parts.push(a.detail)
64
+ if (asArr(a.sources).length) parts.push('Start from these sources (absolute paths):\n' + asArr(a.sources).join('\n'))
65
+ parts.push('Dig into the real files (read/grep/glob/bash). Report FACTS with absolute paths: what exists, key files/APIs/data models/behaviors, and anything a later design or implementation must know. Never invent.')
66
+ parts.push('WRITE your full findings to ' + args.docDir + '/' + docs[i] + ' (create the directory if needed). The doc is the deliverable — be thorough there.')
67
+ parts.push('Then RETURN only a COMPACT summary (max ~40 lines): what the area is, the load-bearing specifics, and the top risks. Do not paste the doc back.')
68
+ return parts.join('\n\n')
69
+ }
70
+
71
+ phase('Explore (' + areas.length + ' areas)')
72
+ // 死亡重试(超时/停滞/传输错)由引擎原地完成:同一 agent id,进度分母与 resume 哈希都不变。
73
+ const results = await parallel(areas.map((a, i) => () => agent(areaPrompt(a, i), { label: isStr(a.label) ? a.label : 'explore ' + a.name })))
74
+ const entries = areas.map((a, i) => ({ area: a.name, doc: args.docDir + '/' + docs[i], ...norm(results[i], false) }))
75
+ const failed = entries.filter((e) => !e.ok).map((e) => e.area)
76
+ // 失败早报:Map 之前就把失败区亮出来,不等 run 结束。
77
+ if (failed.length) log('failed areas (re-run explore with only these): ' + failed.join(', '))
78
+
79
+ let risks = null
80
+ if (args.synthesize !== false) {
81
+ phase('Map')
82
+ const brief = entries.map((e) => '## ' + e.area + '\n' + e.summary).join('\n\n')
83
+ const gap = failed.length ? 'NOTE: exploration FAILED for these areas, so their summaries below are missing or partial — treat coverage as incomplete: ' + failed.join(', ') + '\n\n' : ''
84
+ risks = await agent('These compact per-area summaries come from a parallel exploration for: ' + args.goal + '\n\n' + gap + brief + '\n\nReturn ONLY a compact cross-cutting view (max 300 words): the top cross-area risks, dependencies between areas, and anything one area assumed that another contradicts. No area-by-area recap.', { label: 'cross-area risks' })
85
+ }
86
+ return { areas: entries.map((e) => ({ area: e.area, doc: e.doc, ok: e.ok, summary: e.summary })), failedAreas: failed, risks }