@bolloon/bolloon-agent 0.4.32 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/chain/explorer.js +52 -0
- package/dist/agents/chain/index.js +2 -0
- package/dist/agents/chain/onchain-trade.js +15 -1
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/network-pulse.js +525 -27
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-onchain-runner.js +19 -1
- package/dist/agents/task/task-runner.js +36 -6
- package/dist/agents/task-board.js +702 -0
- package/dist/agents/task-group.js +524 -0
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/tasks.js +977 -8
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/protocol-envelope.js +4 -0
- package/dist/cli/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- package/dist/electron-build/agents/chain/explorer.js +59 -0
- package/dist/electron-build/agents/chain/explorer.js.map +1 -0
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +492 -34
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/local-signer.js +101 -0
- package/dist/electron-build/agents/local-signer.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +531 -28
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +35 -5
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/task-board.js +761 -0
- package/dist/electron-build/agents/task-board.js.map +1 -0
- package/dist/electron-build/agents/task-contract.js +382 -0
- package/dist/electron-build/agents/task-contract.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* skill-candidate.ts — P1b: Skill 改进候选的评估与晋升草案 (2026-09-25)
|
|
4
|
+
*
|
|
5
|
+
* 归属: `docs/wiki/goal-continuation-flywheel.md` §13「P1b Memory/Skill」独占文件。
|
|
6
|
+
* 本文件**只**实现 §14 冻结签名里的 `assessCandidate` / `draftPromotion`, 不碰任何现有调用方,
|
|
7
|
+
* 也不 import 其它阶段的实现 (§13: 阶段之间只通过 `types.ts` 的类型耦合)。
|
|
8
|
+
*
|
|
9
|
+
* 本文件是**纯函数** (零 I/O, 不 import `fs`) —— 时间由调用方注入, 注册表由参数传入。
|
|
10
|
+
*
|
|
11
|
+
* 两道门 (缺一不可, 且互相独立 —— 调用方清空 `junkReasons` 也绕不过第二道):
|
|
12
|
+
*
|
|
13
|
+
* ① `assessCandidate` — 把「防 Skill 垃圾」(SkillJunkReason) 真的算出来, 并给出与现有 Skill 的关系:
|
|
14
|
+
* `single_success` ← occurrences < 2 (只成功一次 / 偶然成功)
|
|
15
|
+
* `no_io_schema` ← inputSchema 或 outputSchema 为空
|
|
16
|
+
* `no_failure_boundary` ← failureCases 为空 (没有失效边界)
|
|
17
|
+
* `temp_path_dependency` ← evidenceRefs / sourceRunIds 里出现临时路径
|
|
18
|
+
* `unverifiable_result` ← 无证据 / 无 guarantees / 无 doesNotGuarantee / 无 contentHash
|
|
19
|
+
* `one_line_experience` ← 只有一句经验: 无 schema + 无失败边界 + 无 guarantees
|
|
20
|
+
* `duplicate_of_existing` ← 与已有同名 Skill 的 contentHash 完全相同
|
|
21
|
+
* `promotable = junkReasons 为空 && boundaryClear === true` (批准与否是后面 `approve` 步的事)。
|
|
22
|
+
*
|
|
23
|
+
* ② `draftPromotion` — 正式变更记录 (`SkillPromotionRecord`) 的构建:
|
|
24
|
+
* - 必备项缺一即**抛错** (函数签名无法表达失败, 所以不能静默返回一条假记录):
|
|
25
|
+
* 没有 `contentHash` / 没有 `changeReason` / 没有 `approvedBy` / `now` 不是合法 ISO。
|
|
26
|
+
* - **防御性重算**: 不看调用方传进来的 `junkReasons`, 自己重算一遍 (命中即抛错);
|
|
27
|
+
* 这样"单次偶然成功不得自动晋升"(P5 强负例 8) 不靠调用方自觉。
|
|
28
|
+
* - `snapshotScope` 恒为 `'next_run_only'` (自动更新**不覆盖**正在执行的 Skill snapshot)。
|
|
29
|
+
* - 版本: `null → 1.0.0`; `x.y.z → x.(y+1).0`; 非 semver → `1.0.0`。
|
|
30
|
+
*
|
|
31
|
+
* 阴性对照 (怎么知道这些门不是空转): 把 `single_success` 判定删掉 → 「单次成功不得晋升」立刻判红;
|
|
32
|
+
* 把 `draftPromotion` 的 `contentHash` 检查删掉 → 「无哈希不许写正式变更记录」立刻判红。
|
|
33
|
+
* 见 `src/test/goal-flywheel-skill-candidate.test.ts`。
|
|
34
|
+
*/
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SKILL_TRIAL_RULES = exports.CANDIDATE_REQUIRED_LIST_FIELDS = exports.CANDIDATE_REQUIRED_TEXT_FIELDS = exports.FORBIDDEN_TRIAL_SURFACES = exports.SKILL_CONTRACT_SIGNALS = exports.SKILL_SCHEMA_PLACEHOLDERS = exports.SKILL_SCHEMA_MIN_LENGTH = exports.SKILL_CHANNEL_FORBIDDEN_ACTORS = exports.SKILL_CHANNEL_ACTORS = exports.SKILL_CHANNEL_STAGE_STATUSES = exports.SKILL_CHANNEL_STAGE_TEXT = exports.SKILL_CHANNEL_STAGES = exports.TEMP_PATH_HINTS = void 0;
|
|
37
|
+
exports.looksLikeTempPath = looksLikeTempPath;
|
|
38
|
+
exports.deriveJunkReasons = deriveJunkReasons;
|
|
39
|
+
exports.assessCandidate = assessCandidate;
|
|
40
|
+
exports.bumpSkillVersion = bumpSkillVersion;
|
|
41
|
+
exports.draftPromotion = draftPromotion;
|
|
42
|
+
exports.validateSkillContract = validateSkillContract;
|
|
43
|
+
exports.scanForbiddenTrialSurfaces = scanForbiddenTrialSurfaces;
|
|
44
|
+
exports.structureMissingFields = structureMissingFields;
|
|
45
|
+
exports.openSkillTrial = openSkillTrial;
|
|
46
|
+
exports.settleSkillTrial = settleSkillTrial;
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// 对外常量 / 小工具 (纯)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
/** 出现这些片段 = 依赖临时路径 (跨机器重放必挂) */
|
|
51
|
+
exports.TEMP_PATH_HINTS = [
|
|
52
|
+
'/tmp/',
|
|
53
|
+
'/var/tmp/',
|
|
54
|
+
'/var/folders/',
|
|
55
|
+
'/private/tmp/',
|
|
56
|
+
'\\temp\\',
|
|
57
|
+
'tmpdir',
|
|
58
|
+
];
|
|
59
|
+
/** 该引用是否指向临时路径 */
|
|
60
|
+
function looksLikeTempPath(ref) {
|
|
61
|
+
const t = typeof ref === 'string' ? ref.toLowerCase() : '';
|
|
62
|
+
if (t.trim().length === 0)
|
|
63
|
+
return false;
|
|
64
|
+
return exports.TEMP_PATH_HINTS.some((h) => t.includes(h)) || t.startsWith('/tmp');
|
|
65
|
+
}
|
|
66
|
+
const NON_EMPTY = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
67
|
+
const len = (v) => (Array.isArray(v) ? v.length : 0);
|
|
68
|
+
function uniq(items) {
|
|
69
|
+
return [...new Set(items)];
|
|
70
|
+
}
|
|
71
|
+
function isValidIso(v) {
|
|
72
|
+
if (typeof v !== 'string' || v.trim().length === 0)
|
|
73
|
+
return false;
|
|
74
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(v))
|
|
75
|
+
return false;
|
|
76
|
+
return Number.isFinite(Date.parse(v));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 「防 Skill 垃圾」判定。`existing` 只用于 `duplicate_of_existing` —— 因此
|
|
80
|
+
* `draftPromotion` 传空数组复算时**刻意**查不出重复 (重复要靠注册表, 这一条由 `assessCandidate` 负责)。
|
|
81
|
+
*/
|
|
82
|
+
function deriveJunkReasons(c, existing) {
|
|
83
|
+
const out = [];
|
|
84
|
+
const push = (r) => {
|
|
85
|
+
if (!out.includes(r))
|
|
86
|
+
out.push(r);
|
|
87
|
+
};
|
|
88
|
+
// 只成功一次 → 偶然成功, 不许转正 (P5 强负例 8)
|
|
89
|
+
if (!Number.isFinite(c.occurrences) || c.occurrences < 2)
|
|
90
|
+
push('single_success');
|
|
91
|
+
// 无明确输入输出
|
|
92
|
+
if (!NON_EMPTY(c.inputSchema) || !NON_EMPTY(c.outputSchema))
|
|
93
|
+
push('no_io_schema');
|
|
94
|
+
// 无失败边界
|
|
95
|
+
if (len(c.failureCases) === 0)
|
|
96
|
+
push('no_failure_boundary');
|
|
97
|
+
// 依赖临时路径
|
|
98
|
+
const refs = [...(Array.isArray(c.evidenceRefs) ? c.evidenceRefs : []), ...(Array.isArray(c.sourceRunIds) ? c.sourceRunIds : [])];
|
|
99
|
+
if (refs.some((r) => looksLikeTempPath(r)))
|
|
100
|
+
push('temp_path_dependency');
|
|
101
|
+
// 结果不可验证: 没证据 / 没草案哈希 / 只声明保证却不声明边界
|
|
102
|
+
if (len(c.evidenceRefs) === 0)
|
|
103
|
+
push('unverifiable_result');
|
|
104
|
+
if (!NON_EMPTY(c.contentHash))
|
|
105
|
+
push('unverifiable_result');
|
|
106
|
+
if (len(c.guarantees) === 0)
|
|
107
|
+
push('unverifiable_result');
|
|
108
|
+
if (len(c.doesNotGuarantee) === 0)
|
|
109
|
+
push('unverifiable_result');
|
|
110
|
+
// 只是一句经验: 除了名字/目的以外什么都没有
|
|
111
|
+
if (len(c.failureCases) === 0 && !NON_EMPTY(c.inputSchema) && !NON_EMPTY(c.outputSchema) && len(c.guarantees) === 0) {
|
|
112
|
+
push('one_line_experience');
|
|
113
|
+
}
|
|
114
|
+
// 与已有 Skill 完全重复 (同名 + 同 contentHash)
|
|
115
|
+
const dup = existing.some((e) => e.name === c.name && NON_EMPTY(c.contentHash) && e.contentHash === c.contentHash);
|
|
116
|
+
if (dup)
|
|
117
|
+
push('duplicate_of_existing');
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// §14 冻结签名 ①: assessCandidate
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
/**
|
|
124
|
+
* 评估一个 Skill 改进候选: 命中哪些垃圾理由 / 能不能晋升 / 与现有 Skill 是什么关系。
|
|
125
|
+
*
|
|
126
|
+
* `promotable` 的含义是「过了垃圾门 + 边界清晰」, **不等于已批准** ——
|
|
127
|
+
* `approve` 与 `write_content_hash_and_change_reason` 是流程里后面的步骤。
|
|
128
|
+
*/
|
|
129
|
+
function assessCandidate(c, existing) {
|
|
130
|
+
const registry = Array.isArray(existing) ? existing : [];
|
|
131
|
+
const junkReasons = deriveJunkReasons(c, registry);
|
|
132
|
+
const same = registry.find((e) => e.name === c.name);
|
|
133
|
+
let comparison;
|
|
134
|
+
if (!same)
|
|
135
|
+
comparison = 'new';
|
|
136
|
+
else if (NON_EMPTY(c.contentHash) && same.contentHash === c.contentHash)
|
|
137
|
+
comparison = 'duplicate';
|
|
138
|
+
else
|
|
139
|
+
comparison = 'version_bump';
|
|
140
|
+
const promotable = junkReasons.length === 0 && c.boundaryClear === true;
|
|
141
|
+
return { junkReasons, promotable, comparison };
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// §14 冻结签名 ②: draftPromotion
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
/** 版本推进: `null → 1.0.0`; `x.y.z → x.(y+1).0`; 非 semver → `1.0.0` */
|
|
147
|
+
function bumpSkillVersion(fromVersion) {
|
|
148
|
+
if (!NON_EMPTY(fromVersion))
|
|
149
|
+
return '1.0.0';
|
|
150
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(fromVersion.trim());
|
|
151
|
+
if (!m)
|
|
152
|
+
return '1.0.0';
|
|
153
|
+
return `${Number(m[1])}.${Number(m[2]) + 1}.0`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 构造正式 Skill 变更记录 (批准后才写; 带 contentHash 与变更原因)。
|
|
157
|
+
*
|
|
158
|
+
* 签名 (§14) 无法表达失败, 所以任何"不该晋升"的输入一律**抛错**, 不返回假记录:
|
|
159
|
+
* - `approvedBy` / `changeReason` / `now` / 候选的 `contentHash` 缺一即抛
|
|
160
|
+
* - 候选命中垃圾理由 (含调用方没写进 `junkReasons` 的那些, 本函数自行重算) 即抛
|
|
161
|
+
* - `boundaryClear !== true` 即抛 (边界不清的候选不许成为正式 Skill)
|
|
162
|
+
*/
|
|
163
|
+
function draftPromotion(c, existingVersion, changeReason, approvedBy, now) {
|
|
164
|
+
if (!NON_EMPTY(approvedBy)) {
|
|
165
|
+
throw new Error('draftPromotion: approvedBy 必填 (没有批准人不许写正式变更记录)');
|
|
166
|
+
}
|
|
167
|
+
if (!NON_EMPTY(changeReason)) {
|
|
168
|
+
throw new Error('draftPromotion: changeReason 必填 (正式变更必须带变更原因)');
|
|
169
|
+
}
|
|
170
|
+
if (!isValidIso(now)) {
|
|
171
|
+
throw new Error(`draftPromotion: now 必须是合法 ISO 时间戳 (收到 ${JSON.stringify(now)})`);
|
|
172
|
+
}
|
|
173
|
+
if (!NON_EMPTY(c.contentHash)) {
|
|
174
|
+
throw new Error('draftPromotion: contentHash 必填 (没有草案哈希不许写正式变更记录)');
|
|
175
|
+
}
|
|
176
|
+
// 防御性重算: 不信调用方传进来的 junkReasons (duplicate 需注册表, 这里查不到)
|
|
177
|
+
const junk = uniq([...(Array.isArray(c.junkReasons) ? c.junkReasons : []), ...deriveJunkReasons(c, [])]);
|
|
178
|
+
if (junk.length > 0) {
|
|
179
|
+
throw new Error(`draftPromotion: 候选命中垃圾理由, 不得晋升: ${junk.join(', ')}`);
|
|
180
|
+
}
|
|
181
|
+
if (c.boundaryClear !== true) {
|
|
182
|
+
throw new Error('draftPromotion: boundaryClear 必须为 true (边界不清的候选不得晋升)');
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
skillName: c.name,
|
|
186
|
+
fromVersion: NON_EMPTY(existingVersion) ? existingVersion : null,
|
|
187
|
+
toVersion: bumpSkillVersion(existingVersion),
|
|
188
|
+
contentHash: c.contentHash,
|
|
189
|
+
changeReason,
|
|
190
|
+
sourceRunIds: [...(c.sourceRunIds ?? [])],
|
|
191
|
+
evidenceRefs: [...(c.evidenceRefs ?? [])],
|
|
192
|
+
failureCases: [...(c.failureCases ?? [])],
|
|
193
|
+
inputSchema: c.inputSchema,
|
|
194
|
+
outputSchema: c.outputSchema,
|
|
195
|
+
guarantees: [...(c.guarantees ?? [])],
|
|
196
|
+
doesNotGuarantee: [...(c.doesNotGuarantee ?? [])],
|
|
197
|
+
approval: {
|
|
198
|
+
state: 'approved',
|
|
199
|
+
approvedBy,
|
|
200
|
+
approvedAt: now,
|
|
201
|
+
changeReason,
|
|
202
|
+
},
|
|
203
|
+
/** 自动更新不覆盖正在执行的 Skill snapshot (类型级陈述) */
|
|
204
|
+
snapshotScope: 'next_run_only',
|
|
205
|
+
promotedAt: now,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// P1b ② Skill 升级通道 (M2 接线, 2026-09-26)
|
|
210
|
+
// ============================================================================
|
|
211
|
+
//
|
|
212
|
+
// 一条 Skill 从「评审里的一句话」变成「正式 Skill」必须走六个阶段, 一个都不许跳
|
|
213
|
+
// (阶段的固定顺序由 `SKILL_CHANNEL_STAGES` 钉住):
|
|
214
|
+
//
|
|
215
|
+
// ① `structure` 结构化 —— 冻结契约的必备字段齐全, 且不命中垃圾理由
|
|
216
|
+
// ② `schema_validation` schema —— 输入输出契约是**能用的契约** (不是占位符/残破文本)
|
|
217
|
+
// ③ `dedup` 去重 —— 与已有 Skill 不重复; 同一批候选之间也不许重复
|
|
218
|
+
// ④ `permission` 安全权限 —— 请求者身份合法 + 草案不带危险面
|
|
219
|
+
// (写正式 skills 目录 / 凭证面 / 提权动作)
|
|
220
|
+
// ⑤ `trial_snapshot` 快照试用 —— `snapshotScope='next_run_only'`: 只影响**下一次** Run,
|
|
221
|
+
// 绝不覆盖正在执行的那份 snapshot
|
|
222
|
+
// ⑥ `reuse_confirmation` 复用确认 —— **下一条 Run 真的成功复用它 (带证据) 之后**才提升;
|
|
223
|
+
// 同一条 Run 自己说自己成功不算; 复用失败/无证据 → 不提升
|
|
224
|
+
//
|
|
225
|
+
// 为什么准入与提升必须是两个出口 (`openSkillTrial` / `settleSkillTrial`):
|
|
226
|
+
// 一旦合成一处, "谁负责确认它真的被复用过"就没有归属 —— 于是"评审说好就转正"会以
|
|
227
|
+
// "反正评估过了"的样子混进来。`openSkillTrial` 只**准入** (它返回的 `promotion` 恒为 `null`),
|
|
228
|
+
// 提升的唯一出口是 `settleSkillTrial`, 且它不信任调用方传进来的字段: 自己复核内容哈希,
|
|
229
|
+
// 自己重算垃圾门 (与 `draftPromotion` 的防御性重算同一手法)。
|
|
230
|
+
//
|
|
231
|
+
// 与其他模块的关系: 本段**零 I/O**、不读钟 (时间一律 `now` 注入), 注册表/候选/复用事实全部
|
|
232
|
+
// 由参数传入 —— 因此同一个判据既能被收尾接线层在"候选刚被提取出来"时调用 (准入),
|
|
233
|
+
// 也能在**下一条 Run 的成功点**上被调用 (结算)。
|
|
234
|
+
/** 六个阶段的**固定顺序** (少一个 = 通道被跳过) */
|
|
235
|
+
exports.SKILL_CHANNEL_STAGES = [
|
|
236
|
+
'structure',
|
|
237
|
+
'schema_validation',
|
|
238
|
+
'dedup',
|
|
239
|
+
'permission',
|
|
240
|
+
'trial_snapshot',
|
|
241
|
+
'reuse_confirmation',
|
|
242
|
+
];
|
|
243
|
+
exports.SKILL_CHANNEL_STAGE_TEXT = {
|
|
244
|
+
structure: '结构化: 冻结契约必备字段齐全, 且不命中垃圾理由',
|
|
245
|
+
schema_validation: 'schema 校验: 输入输出契约可用 (不是占位符/残破文本)',
|
|
246
|
+
dedup: '去重: 不与已有 Skill 重复, 同批候选之间也不许重复',
|
|
247
|
+
permission: '安全权限: 请求者身份合法 + 草案不带危险面 (正式 skills 目录/凭证/提权)',
|
|
248
|
+
trial_snapshot: 'next_run_only 快照试用: 只影响下一次 Run, 不覆盖正在执行的 snapshot',
|
|
249
|
+
reuse_confirmation: '复用确认: 下一条 Run 真的成功复用它 (带证据) 之后才提升',
|
|
250
|
+
};
|
|
251
|
+
/** 阶段裁决只有四种 —— `pending` / `not_reached` 也是**如实**的结论, 不许当成 pass */
|
|
252
|
+
exports.SKILL_CHANNEL_STAGE_STATUSES = ['pass', 'refused', 'pending', 'not_reached'];
|
|
253
|
+
/** 谁能请求"开一次试用" */
|
|
254
|
+
exports.SKILL_CHANNEL_ACTORS = ['human', 'supervisor', 'runner', 'closure'];
|
|
255
|
+
/**
|
|
256
|
+
* 认识但**不许**开试用的主体。
|
|
257
|
+
*
|
|
258
|
+
* `child_agent`: 子 Agent 不能给自己的产出开试用 —— 试用是通向正式 Skill 的第一步, 而子 Agent
|
|
259
|
+
* 越界清单 (`CHILD_PROHIBITIONS.mutate_parent_goal_state` / `mark_unverified_as_complete`) 的精神
|
|
260
|
+
* 正是"不许自己给自己盖章"。子 Agent 可以**提**候选, 由父/宿主开试用。
|
|
261
|
+
*/
|
|
262
|
+
exports.SKILL_CHANNEL_FORBIDDEN_ACTORS = ['child_agent'];
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// ② schema 校验 (纯)
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
exports.SKILL_SCHEMA_MIN_LENGTH = 3;
|
|
267
|
+
/** 这些"契约"等于没写 (占位的形状太多, 所以列成清单, 不靠长度猜) */
|
|
268
|
+
exports.SKILL_SCHEMA_PLACEHOLDERS = [
|
|
269
|
+
'tbd', 'todo', 'n/a', 'na', 'none', 'null', 'undefined', 'unknown', 'any', 'x', '-', '--',
|
|
270
|
+
'...', '???', '待定', '随便', '无', '未定', '略',
|
|
271
|
+
];
|
|
272
|
+
exports.SKILL_CONTRACT_SIGNALS = ['json_object', 'object_literal', 'named_contract', 'described_contract'];
|
|
273
|
+
const IDENT_TOKEN = /[A-Za-z_][A-Za-z0-9_./-]*/;
|
|
274
|
+
function countChar(s, ch) {
|
|
275
|
+
let n = 0;
|
|
276
|
+
for (const c of s)
|
|
277
|
+
if (c === ch)
|
|
278
|
+
n += 1;
|
|
279
|
+
return n;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 校验一份输入/输出契约够不够"能用"。
|
|
283
|
+
*
|
|
284
|
+
* 它比 `deriveJunkReasons` 的 `no_io_schema` (只看非空) 严一档, 但刻意**不**要求是合法 JSON:
|
|
285
|
+
* 本仓真实的契约既有 JSON Schema 文本, 也有对象字面量形状的简写 (`{ criteria: string[] }`)
|
|
286
|
+
* 与命名契约 (`bolloon-probe-input/1`) —— 把合法形状误杀, 门就会被人用"放宽判据"解决掉。
|
|
287
|
+
* 所以只拦三类**确实没用**的东西:
|
|
288
|
+
* 空 / 占位符 (TBD 这种) · 括号残缺 (`{ criteria: string[]` 用不了) · 没有任何可指认的字段名。
|
|
289
|
+
*/
|
|
290
|
+
function validateSkillContract(text, which) {
|
|
291
|
+
const where = `${which}_schema`;
|
|
292
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
293
|
+
return { ok: false, signal: null, reason: `${where}_empty: 契约是空的` };
|
|
294
|
+
}
|
|
295
|
+
const t = text.trim();
|
|
296
|
+
if (t.length < exports.SKILL_SCHEMA_MIN_LENGTH) {
|
|
297
|
+
return { ok: false, signal: null, reason: `${where}_too_short: 比 ${exports.SKILL_SCHEMA_MIN_LENGTH} 个字符还短, 说不清契约` };
|
|
298
|
+
}
|
|
299
|
+
if (exports.SKILL_SCHEMA_PLACEHOLDERS.includes(t.toLowerCase())) {
|
|
300
|
+
return { ok: false, signal: null, reason: `${where}_placeholder: "${t}" 是占位符, 不是契约` };
|
|
301
|
+
}
|
|
302
|
+
if (countChar(t, '{') !== countChar(t, '}') ||
|
|
303
|
+
countChar(t, '(') !== countChar(t, ')') ||
|
|
304
|
+
countChar(t, '[') !== countChar(t, ']')) {
|
|
305
|
+
return { ok: false, signal: null, reason: `${where}_unbalanced: "${t}" 括号不配对 (残缺的契约用不了)` };
|
|
306
|
+
}
|
|
307
|
+
if (!IDENT_TOKEN.test(t)) {
|
|
308
|
+
return { ok: false, signal: null, reason: `${where}_no_named_field: "${t}" 里没有任何可指认的字段名/类型名` };
|
|
309
|
+
}
|
|
310
|
+
const first = t[0];
|
|
311
|
+
if (first === '{' || first === '[') {
|
|
312
|
+
try {
|
|
313
|
+
const parsed = JSON.parse(t);
|
|
314
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
315
|
+
return { ok: true, signal: 'json_object', reason: null };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// 不是 JSON —— 不一定不是契约 (对象字面量简写是本仓常见的写法)
|
|
320
|
+
}
|
|
321
|
+
return { ok: true, signal: 'object_literal', reason: null };
|
|
322
|
+
}
|
|
323
|
+
if (/^[A-Za-z_][A-Za-z0-9_./-]*$/.test(t))
|
|
324
|
+
return { ok: true, signal: 'named_contract', reason: null };
|
|
325
|
+
return { ok: true, signal: 'described_contract', reason: null };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 试用**不许带**的三类面。清单刻意很短 —— 每加一条都可能误杀合法草案, 而误杀会让人用
|
|
329
|
+
* "放宽判据"来解 (那时真信号也被一起埋掉)。所以只放三类没有争议的:
|
|
330
|
+
* ① 草案直接指向正式 Skill 目录 → 那就是绕过"验证+快照+可回退"通道;
|
|
331
|
+
* ② 草案依赖凭证面 → 凭证永不进 Skill (连描述都不该进产物);
|
|
332
|
+
* ③ 草案带提权动作 → 试用期不许带提权面。
|
|
333
|
+
*/
|
|
334
|
+
exports.FORBIDDEN_TRIAL_SURFACES = [
|
|
335
|
+
{
|
|
336
|
+
id: 'writes_formal_skill_dir',
|
|
337
|
+
pattern: /\.bolloon\/skills\b/,
|
|
338
|
+
why: '草案指向正式 Skill 目录 → 绕过 验证+快照+可回退 通道',
|
|
339
|
+
scope: 'any',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: 'credential_path',
|
|
343
|
+
pattern: /(llm-config\.json|credentials?\.json|id_rsa|\.ssh\/|keychain|\.env\b)/i,
|
|
344
|
+
why: '草案依赖凭证面 → 凭证永不进 Skill',
|
|
345
|
+
scope: 'contract',
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: 'privilege_escalation',
|
|
349
|
+
pattern: /(\bsudo\b|chmod\s+777|\bsetuid\b|\brunas\b)/i,
|
|
350
|
+
why: '草案带提权动作 → 试用不许带提权面',
|
|
351
|
+
scope: 'contract',
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
/** 扫一份候选草案有没有踩危险面; 返回空数组 = 没踩 */
|
|
355
|
+
function scanForbiddenTrialSurfaces(c) {
|
|
356
|
+
const contractFields = [
|
|
357
|
+
['name', String(c?.name ?? '')],
|
|
358
|
+
['purpose', String(c?.purpose ?? '')],
|
|
359
|
+
['inputSchema', String(c?.inputSchema ?? '')],
|
|
360
|
+
['outputSchema', String(c?.outputSchema ?? '')],
|
|
361
|
+
['guarantees', (c?.guarantees ?? []).join(' | ')],
|
|
362
|
+
['doesNotGuarantee', (c?.doesNotGuarantee ?? []).join(' | ')],
|
|
363
|
+
['failureCases', (c?.failureCases ?? []).join(' | ')],
|
|
364
|
+
];
|
|
365
|
+
const anyFields = [
|
|
366
|
+
...contractFields,
|
|
367
|
+
['evidenceRefs', (c?.evidenceRefs ?? []).join(' | ')],
|
|
368
|
+
['sourceRunIds', (c?.sourceRunIds ?? []).join(' | ')],
|
|
369
|
+
];
|
|
370
|
+
const out = [];
|
|
371
|
+
for (const surface of exports.FORBIDDEN_TRIAL_SURFACES) {
|
|
372
|
+
for (const [where, text] of surface.scope === 'any' ? anyFields : contractFields) {
|
|
373
|
+
if (!text)
|
|
374
|
+
continue;
|
|
375
|
+
if (surface.pattern.test(text)) {
|
|
376
|
+
out.push({ id: surface.id, where, why: surface.why });
|
|
377
|
+
break; // 同一类面只报一次 (原因清单要读得完)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// ① 结构化的必备字段 (冻结契约里**不许为空**的那些)
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
/** 冻结契约里必须非空的**文本**字段 (缺一个 = 候选不结构化, 不许进通道) */
|
|
387
|
+
exports.CANDIDATE_REQUIRED_TEXT_FIELDS = ['name', 'purpose', 'inputSchema', 'outputSchema', 'contentHash'];
|
|
388
|
+
/** 必须非空的**清单**字段 */
|
|
389
|
+
exports.CANDIDATE_REQUIRED_LIST_FIELDS = ['sourceRunIds', 'evidenceRefs', 'failureCases', 'guarantees', 'doesNotGuarantee'];
|
|
390
|
+
/** 返回空数组 = 结构化通过; 否则返回缺/不合格的字段名 (结构化原因, 不是 boolean) */
|
|
391
|
+
function structureMissingFields(c) {
|
|
392
|
+
if (!c || typeof c !== 'object')
|
|
393
|
+
return ['(candidate)'];
|
|
394
|
+
const missing = [];
|
|
395
|
+
for (const f of exports.CANDIDATE_REQUIRED_TEXT_FIELDS) {
|
|
396
|
+
const v = c[f];
|
|
397
|
+
if (typeof v !== 'string' || v.trim().length === 0)
|
|
398
|
+
missing.push(f);
|
|
399
|
+
}
|
|
400
|
+
for (const f of exports.CANDIDATE_REQUIRED_LIST_FIELDS) {
|
|
401
|
+
const v = c[f];
|
|
402
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
403
|
+
missing.push(f);
|
|
404
|
+
}
|
|
405
|
+
if (!Number.isFinite(c.occurrences) || Number(c.occurrences) < 2)
|
|
406
|
+
missing.push('occurrences>=2');
|
|
407
|
+
if (c.boundaryClear !== true)
|
|
408
|
+
missing.push('boundaryClear=true');
|
|
409
|
+
return missing;
|
|
410
|
+
}
|
|
411
|
+
function emptyStageVerdicts() {
|
|
412
|
+
const out = {};
|
|
413
|
+
for (const s of exports.SKILL_CHANNEL_STAGES)
|
|
414
|
+
out[s] = { stage: s, status: 'not_reached', reason: 'not_reached' };
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* 通道 ①–⑤: 把一份候选**准入到试用位**。返回的 `promotion` 恒为 `null` —— 准入不是晋升。
|
|
419
|
+
*
|
|
420
|
+
* 六个阶段**逐条**给出裁决 (被拒之后的下游阶段是 `not_reached`, 不是"省略") ——
|
|
421
|
+
* 因为"哪一步拒的、后面还有哪些步没走"本身就是给人看的事实。
|
|
422
|
+
*/
|
|
423
|
+
function openSkillTrial(input) {
|
|
424
|
+
const c = (input?.candidate ?? null);
|
|
425
|
+
const now = input?.now;
|
|
426
|
+
const registry = Array.isArray(input?.existing) ? input.existing : [];
|
|
427
|
+
const siblings = Array.isArray(input?.siblings) ? input.siblings : [];
|
|
428
|
+
const verdicts = emptyStageVerdicts();
|
|
429
|
+
let refusal = null;
|
|
430
|
+
let trial = null;
|
|
431
|
+
const pass = (stage, reason) => {
|
|
432
|
+
verdicts[stage] = { stage, status: 'pass', reason };
|
|
433
|
+
};
|
|
434
|
+
const refuse = (stage, reason) => {
|
|
435
|
+
verdicts[stage] = { stage, status: 'refused', reason };
|
|
436
|
+
refusal = { stage, reason };
|
|
437
|
+
};
|
|
438
|
+
const finish = () => {
|
|
439
|
+
const why = refusal ? `被 ${refusal.stage} 阶段拦住` : '';
|
|
440
|
+
for (const s of exports.SKILL_CHANNEL_STAGES) {
|
|
441
|
+
if (verdicts[s].status === 'not_reached')
|
|
442
|
+
verdicts[s].reason = `not_reached: ${why}`;
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
ok: refusal === null,
|
|
446
|
+
stages: exports.SKILL_CHANNEL_STAGES.map((s) => verdicts[s]),
|
|
447
|
+
refusal,
|
|
448
|
+
trial,
|
|
449
|
+
promotion: null,
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
// 时间必须注入且合法 (本模块不读真实钟)
|
|
453
|
+
if (!isValidIso(now)) {
|
|
454
|
+
verdicts.trial_snapshot = {
|
|
455
|
+
stage: 'trial_snapshot',
|
|
456
|
+
status: 'refused',
|
|
457
|
+
reason: 'invalid_now: 试用时间必须是合法 ISO 时间戳 (时间由调用方注入, 不许读真实钟)',
|
|
458
|
+
};
|
|
459
|
+
refusal = { stage: 'trial_snapshot', reason: 'invalid_now: 时间由调用方注入, 不许读真实钟' };
|
|
460
|
+
return finish();
|
|
461
|
+
}
|
|
462
|
+
if (!c) {
|
|
463
|
+
refuse('structure', 'structure_incomplete: 候选缺失 (拿不到候选就没有"结构化"可言)');
|
|
464
|
+
return finish();
|
|
465
|
+
}
|
|
466
|
+
// ── ① 结构化 ─────────────────────────────────────────────────────────────
|
|
467
|
+
const missing = structureMissingFields(c);
|
|
468
|
+
if (missing.length > 0) {
|
|
469
|
+
refuse('structure', `structure_incomplete: 必备字段为空/不合格: ${missing.join(', ')}`);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
const junk = deriveJunkReasons(c, registry);
|
|
473
|
+
if (junk.length > 0) {
|
|
474
|
+
refuse('structure', `structure_junk: 命中垃圾理由: ${junk.join(', ')} (偶然成功/无 IO 契约/无失败边界/结果不可验证都不得进通道)`);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
pass('structure', `冻结契约必备字段齐全 (occurrences=${c.occurrences}, boundaryClear=${c.boundaryClear === true})`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// ── ② schema 校验 ────────────────────────────────────────────────────────
|
|
481
|
+
if (!refusal) {
|
|
482
|
+
const inV = validateSkillContract(c.inputSchema, 'input');
|
|
483
|
+
const outV = validateSkillContract(c.outputSchema, 'output');
|
|
484
|
+
if (!inV.ok)
|
|
485
|
+
refuse('schema_validation', inV.reason ?? 'input_schema_invalid');
|
|
486
|
+
else if (!outV.ok)
|
|
487
|
+
refuse('schema_validation', outV.reason ?? 'output_schema_invalid');
|
|
488
|
+
else
|
|
489
|
+
pass('schema_validation', `契约可用: input=${inV.signal}, output=${outV.signal}`);
|
|
490
|
+
}
|
|
491
|
+
// ── ③ 去重 ───────────────────────────────────────────────────────────────
|
|
492
|
+
if (!refusal) {
|
|
493
|
+
const sameName = registry.find((e) => e.name === c.name);
|
|
494
|
+
const batchDup = siblings.find((s) => String(s?.name ?? '') === c.name);
|
|
495
|
+
if (sameName && NON_EMPTY(c.contentHash) && sameName.contentHash === c.contentHash) {
|
|
496
|
+
refuse('dedup', `duplicate_of_existing: 与已有 Skill ${c.name} 内容完全相同 (contentHash=${c.contentHash}) → 不需要新版本`);
|
|
497
|
+
}
|
|
498
|
+
else if (batchDup) {
|
|
499
|
+
refuse('dedup', `duplicate_in_batch: 同一批里已有同名候选 ${c.name} → 一次收尾只开一次试用`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const comparison = assessCandidate(c, registry).comparison;
|
|
503
|
+
pass('dedup', sameName ? `与已有 Skill ${c.name} 同名但内容不同 → ${comparison}` : '新 Skill (注册表里没有同名项)');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// ── ④ 安全权限 ───────────────────────────────────────────────────────────
|
|
507
|
+
if (!refusal) {
|
|
508
|
+
const actor = String(input?.requestedBy ?? '');
|
|
509
|
+
if (exports.SKILL_CHANNEL_FORBIDDEN_ACTORS.includes(actor)) {
|
|
510
|
+
refuse('permission', 'child_cannot_open_trial: 子 Agent 不能给自己的产出开试用 (提候选可以, 开试用由父/宿主做)');
|
|
511
|
+
}
|
|
512
|
+
else if (!exports.SKILL_CHANNEL_ACTORS.includes(actor)) {
|
|
513
|
+
refuse('permission', `unknown_actor: "${actor}" 不是已知请求者 (只认 ${exports.SKILL_CHANNEL_ACTORS.join('/')})`);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
const hits = scanForbiddenTrialSurfaces(c);
|
|
517
|
+
if (hits.length > 0) {
|
|
518
|
+
refuse('permission', `forbidden_surface: ${hits.map((h) => `${h.id}@${h.where}`).join(', ')}; ${hits[0].why}`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
pass('permission', `请求者=${actor}; 草案未踩危险面 (正式 skills 目录/凭证/提权)`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// ── ⑤ next_run_only 快照试用 ─────────────────────────────────────────────
|
|
526
|
+
if (!refusal) {
|
|
527
|
+
const hash = String(c.contentHash ?? '').trim();
|
|
528
|
+
const proposedByRunId = String(c.proposedByRunId ?? '').trim();
|
|
529
|
+
if (!proposedByRunId) {
|
|
530
|
+
refuse('trial_snapshot', 'trial_without_run_id: 候选说不清是哪条 Run 提的 → 试用无法被兑现');
|
|
531
|
+
}
|
|
532
|
+
else if (!hash) {
|
|
533
|
+
refuse('trial_snapshot', 'trial_without_content_hash: 没有草案哈希 → 无法证明复用/提升的是同一份内容');
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
const runningSnapshotHash = typeof input?.runningSnapshotHash === 'string' ? input.runningSnapshotHash : null;
|
|
537
|
+
const same = registry.find((e) => e.name === c.name)?.version ?? null;
|
|
538
|
+
trial = {
|
|
539
|
+
trialId: `trial:${c.candidateId}`,
|
|
540
|
+
skillName: c.name,
|
|
541
|
+
candidateId: c.candidateId,
|
|
542
|
+
contentHash: hash,
|
|
543
|
+
fromVersion: same,
|
|
544
|
+
toVersion: bumpSkillVersion(same),
|
|
545
|
+
snapshotScope: 'next_run_only',
|
|
546
|
+
appliesToRunningRun: false,
|
|
547
|
+
runningSnapshotHash,
|
|
548
|
+
status: 'trialing',
|
|
549
|
+
startedAt: now,
|
|
550
|
+
startedBy: input.requestedBy,
|
|
551
|
+
startedByRunId: proposedByRunId,
|
|
552
|
+
trialRunId: null,
|
|
553
|
+
reuseEvidenceRefs: [],
|
|
554
|
+
reason: '试用已开: 只影响下一次 Run (scope=next_run_only, appliesToRunningRun=false); 在**下一条 Run** 成功复用并带证据之前不得提升',
|
|
555
|
+
};
|
|
556
|
+
pass('trial_snapshot', trial.reason);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// ── ⑥ 复用确认 (准入时**只能**是 pending: 兑现要等下一条 Run) ─────────────
|
|
560
|
+
if (!refusal && trial) {
|
|
561
|
+
verdicts.reuse_confirmation = {
|
|
562
|
+
stage: 'reuse_confirmation',
|
|
563
|
+
status: 'pending',
|
|
564
|
+
reason: 'pending_next_run_reuse: 提升的唯一出口是 settleSkillTrial (下一条 Run 成功复用并带证据; 同一条 Run 自己说自己成功不算)',
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return finish();
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* 通道 ⑥: **下一条 Run 成功复用之后**才结算。
|
|
571
|
+
*
|
|
572
|
+
* 只有一种输入会产出 `promotion`: 试用在 `trialing` + 复用的是**另一条** Run + 复用成功 +
|
|
573
|
+
* 带可核验证据 + 待提升的候选与试用记录是同一份内容 + 提升带批准人与变更原因。
|
|
574
|
+
* 其余一律**不提升**, 并如实给出结构化原因 (复用失败 → `rolled_back`; 无证据 → 留在 `trialing`)。
|
|
575
|
+
*/
|
|
576
|
+
function settleSkillTrial(input) {
|
|
577
|
+
const trial = input?.trial;
|
|
578
|
+
if (!trial || typeof trial !== 'object' || typeof trial.status !== 'string') {
|
|
579
|
+
throw new Error('settleSkillTrial: trial 必填 (没有试用记录就没有"结算"这回事)');
|
|
580
|
+
}
|
|
581
|
+
const c = (input?.candidate ?? null);
|
|
582
|
+
const reuse = input?.reuse;
|
|
583
|
+
const keep = (status, reason) => ({
|
|
584
|
+
ok: false,
|
|
585
|
+
stage: 'reuse_confirmation',
|
|
586
|
+
status,
|
|
587
|
+
trial: { ...trial, status, reason },
|
|
588
|
+
promotion: null,
|
|
589
|
+
reason,
|
|
590
|
+
});
|
|
591
|
+
if (trial.status !== 'trialing') {
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
stage: 'reuse_confirmation',
|
|
595
|
+
status: trial.status,
|
|
596
|
+
trial,
|
|
597
|
+
promotion: null,
|
|
598
|
+
reason: `not_trialing: 试用状态已是 ${trial.status} → 不许重复结算`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const reuseRunId = String(reuse?.runId ?? '');
|
|
602
|
+
if (!reuseRunId || reuseRunId === trial.startedByRunId) {
|
|
603
|
+
return keep('trialing', `same_run_cannot_promote: 试用的"下次复用"必须是**另一条** Run (试用由 ${trial.startedByRunId} 开, 收到 ${reuseRunId || '(空)'})`);
|
|
604
|
+
}
|
|
605
|
+
if (!reuse.succeeded) {
|
|
606
|
+
return keep('rolled_back', `trial_reuse_failed: 下一条 Run (${reuseRunId}) 复用失败 → 回退, 不提升`);
|
|
607
|
+
}
|
|
608
|
+
const evidence = (reuse.evidenceRefs ?? []).filter((x) => NON_EMPTY(x));
|
|
609
|
+
if (evidence.length === 0) {
|
|
610
|
+
return keep('trialing', 'reuse_without_evidence: 复用没有可核验证据 → 无证据不算成功 (与"无证据不许判目标完成"同一纪律), 留在试用位');
|
|
611
|
+
}
|
|
612
|
+
const hash = String(c?.contentHash ?? '').trim();
|
|
613
|
+
if (!hash || hash !== trial.contentHash) {
|
|
614
|
+
return keep('trialing', `candidate_content_hash_mismatch: 待提升的候选 (${hash || '(空)'}) 与试用记录 (${trial.contentHash}) 不是同一份内容 → 不许换内容冒充`);
|
|
615
|
+
}
|
|
616
|
+
let promotion;
|
|
617
|
+
try {
|
|
618
|
+
promotion = draftPromotion(c, trial.fromVersion, reuse.changeReason, reuse.approvedBy, reuse.now);
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
622
|
+
return keep('trialing', `promotion_refused: ${msg}`);
|
|
623
|
+
}
|
|
624
|
+
const settled = {
|
|
625
|
+
...trial,
|
|
626
|
+
status: 'promoted',
|
|
627
|
+
trialRunId: reuseRunId,
|
|
628
|
+
reuseEvidenceRefs: evidence,
|
|
629
|
+
reason: `下一条 Run (${reuseRunId}) 成功复用并带 ${evidence.length} 条证据 → 提升为 ${promotion.toVersion} (变更原因: ${reuse.changeReason}; 批准人: ${reuse.approvedBy})`,
|
|
630
|
+
};
|
|
631
|
+
return { ok: true, stage: 'reuse_confirmation', status: 'promoted', trial: settled, promotion, reason: settled.reason };
|
|
632
|
+
}
|
|
633
|
+
/** 通道的纪律 (给接线层/门引用, 不在这里重写第二遍) */
|
|
634
|
+
exports.SKILL_TRIAL_RULES = [
|
|
635
|
+
'准入不是晋升: openSkillTrial 的 promotion 恒为 null, 提升的唯一出口是 settleSkillTrial',
|
|
636
|
+
'试用的"下次复用"必须是另一条 Run: 同一条 Run 自己说自己成功不算',
|
|
637
|
+
'复用必须带可核验证据: 无证据不算成功, 留在试用位',
|
|
638
|
+
'复用失败 → 回退 (rolled_back), 不提升',
|
|
639
|
+
'提升必须带批准人与变更原因 (正式变更的必备项)',
|
|
640
|
+
'试用范围恒为 next_run_only: 不覆盖正在执行的 Skill snapshot',
|
|
641
|
+
];
|
|
642
|
+
//# sourceMappingURL=skill-candidate.js.map
|