clearai-dsh 0.4.0 → 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/CHANGELOG.md +40 -0
- package/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/bin/clearai.mjs +29 -231
- package/cordis.patch.yml +3 -3
- package/lib/client.js +4 -3
- package/lib/domain-language.js +71 -9
- package/lib/fold.js +109 -14
- package/lib/knowledge-view.js +85 -26
- package/package.json +2 -2
- package/presets/clearai/agent.cordis.yml +5 -2
- package/presets/clearai/clearai.patch.yml +5 -2
- package/presets/clearai/plugins/clearai-kernel.js +578 -60
- package/presets/clearai/plugins/ontology.js +21 -7
- package/presets/clearai/plugins/prompts.js +18 -16
- package/presets/clearai/skills/clearai-loop/SKILL.md +12 -4
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* P5 什么都不删 → 状态是可重放的投影;refine/void/superseded 的旧值都在
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { tmpdir } from 'node:os'
|
|
23
24
|
|
|
24
25
|
import { createHash } from 'node:crypto'
|
|
25
26
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
@@ -83,6 +84,15 @@ const canonicalVerdict = (value) => {
|
|
|
83
84
|
}
|
|
84
85
|
/** 自判等级上限:L0–L2 可自判,L3 以上拒绝。 */
|
|
85
86
|
const SELF_JUDGE_MAX_INDEX = 2
|
|
87
|
+
/**
|
|
88
|
+
* **给模型选的只有三档**:L2 自判、L3 独立评估、L4 人放行。L0 / L1 / L2 的细分从没改变过谁来判
|
|
89
|
+
* (模型一律自标 L1/L2),所以合并成「自判」:旧账照旧读(界面一律说「自判」),新写的 L0 / L1 当 L2 收。
|
|
90
|
+
*/
|
|
91
|
+
const MODEL_LEVELS = ['L2', 'L3', 'L4']
|
|
92
|
+
const normalizeLevel = (level) => {
|
|
93
|
+
const index = levelIndexOf(level)
|
|
94
|
+
return index < 0 ? level : index <= SELF_JUDGE_MAX_INDEX ? 'L2' : LEVELS[index]
|
|
95
|
+
}
|
|
86
96
|
/** `MAX_PLAN_STEPS`。 */
|
|
87
97
|
const MAX_PLAN_STEPS = 25
|
|
88
98
|
/**
|
|
@@ -206,6 +216,7 @@ export const CONFIG_KEYS = [
|
|
|
206
216
|
'auditProvider',
|
|
207
217
|
'auditTimeoutMs',
|
|
208
218
|
'auditToolFilter',
|
|
219
|
+
'auditRerun',
|
|
209
220
|
'runtimeCard',
|
|
210
221
|
'contributions',
|
|
211
222
|
]
|
|
@@ -216,7 +227,7 @@ export const CONFIG_KEYS = [
|
|
|
216
227
|
*/
|
|
217
228
|
export const MECHANISM_TOOLS = {
|
|
218
229
|
goal: ['Frame', 'Conclude'],
|
|
219
|
-
plan: ['CreatePlan', 'AdvancePlan', 'RevisePlan', 'ClosePlan'],
|
|
230
|
+
plan: ['CreatePlan', 'AdvancePlan', 'RevisePlan', 'ClosePlan', 'Anomaly'],
|
|
220
231
|
/**
|
|
221
232
|
* 领域本体没有工具:模型用原生文件工具直接写 `clear/ontology/{concepts,relations,entities}/**.json`,
|
|
222
233
|
* 写入时由 `tools/pre-execute` 查单个文件,读取时折法从文件折出图(见 `syncWorkspace`)。
|
|
@@ -334,6 +345,7 @@ export function apply(ctx, config = {}) {
|
|
|
334
345
|
// (见 resolveToolFace):`read_image` 只在挂了 `attachments` 的部署里存在,名单里写了它、
|
|
335
346
|
// 部署里没有,`tools.restrict` 会**直接抛**(未知工具名)→ 评估整条路 fail-closed。
|
|
336
347
|
auditToolFilter: config.auditToolFilter ?? ['read', 'glob', 'grep', 'read_image'],
|
|
348
|
+
auditRerun: config.auditRerun === true,
|
|
337
349
|
runtimeCard: config.runtimeCard !== false,
|
|
338
350
|
/** 贡献表。缺省 = 全开;要裁剪就从这里裁,而不是去改装配代码。 */
|
|
339
351
|
contributions: config.contributions ?? {},
|
|
@@ -383,6 +395,30 @@ export function apply(ctx, config = {}) {
|
|
|
383
395
|
return list
|
|
384
396
|
}
|
|
385
397
|
|
|
398
|
+
/**
|
|
399
|
+
* **未解释项**(认识论里的「反常」):和预期或本体对不上的观测。登记走独立落账通道——
|
|
400
|
+
* 它是观测那一刻的发现,后面的交付失败、工具被 abort 都不该把它丢掉。
|
|
401
|
+
*/
|
|
402
|
+
function openAnomalies(sessionId, items, { step = null, by = 'model' } = {}) {
|
|
403
|
+
const opened = []
|
|
404
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
405
|
+
const what = String(item?.what ?? '').trim()
|
|
406
|
+
if (what === '') continue
|
|
407
|
+
const anchor = String(item?.anchor ?? '').trim()
|
|
408
|
+
opened.push(landFact(sessionId, { t: 'anomaly/opened', id: `u-${Math.random().toString(36).slice(2, 8)}`, what: what.slice(0, 400), ...(anchor === '' ? {} : { anchor: anchor.slice(0, 80) }), step, by }))
|
|
409
|
+
}
|
|
410
|
+
return opened
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** 交付成立之后的那一句:这一步写过预期就提醒拿结果去对;登记了未解释就说它们挂在哪。 */
|
|
414
|
+
function expectationNote(step, opened, found = []) {
|
|
415
|
+
const lines = []
|
|
416
|
+
if (found.length > 0) lines.push(tr(`评估者看到 ${found.length} 处没登记的异常,已记成未解释项挂在卡上:${found.map((item) => clip(item.what, 80)).join(';')}。先弄清它们,再往下走。`, `The evaluator saw ${found.length} anomaly(ies) you did not record; they are now unexplained items on the card: ${found.map((item) => clip(item.what, 80)).join('; ')}. Sort them out before moving on.`))
|
|
417
|
+
if (opened.length > 0) lines.push(tr(`登记了 ${opened.length} 条未解释(${opened.map((item) => item.id).join('、')}):它们挂在卡上,直到被解释、写明理由排除、或交给人(Anomaly)。`, `${opened.length} unexplained item(s) recorded (${opened.map((item) => item.id).join(', ')}): they stay on the card until explained, ruled out with a reason, or handed to a person (Anomaly).`))
|
|
418
|
+
else if (typeof step?.expect === 'string' && step.expect !== '') lines.push(tr(`这一步的预期是「${clip(step.expect, 80)}」。结果和它对不上的话,用 Anomaly 登记,不要解释过去。`, `This step expected "${clip(step.expect, 80)}". If the result does not match, record it with Anomaly; do not explain it away.`))
|
|
419
|
+
return lines.length === 0 ? '' : `\n${lines.join('\n')}`
|
|
420
|
+
}
|
|
421
|
+
|
|
386
422
|
function hasState(state) {
|
|
387
423
|
return state.goal !== null || state.plans.length > 0 || state.evidence.length > 0 || state.materials.length > 0
|
|
388
424
|
}
|
|
@@ -445,6 +481,8 @@ export function apply(ctx, config = {}) {
|
|
|
445
481
|
* 而当时只认 id,错误信息又不列出有效 id——它于是逐字猜哪里差了一个标点,白烧了三轮上下文。
|
|
446
482
|
* 「机制把模型逼进猜谜」是机制的问题,不是模型的问题。
|
|
447
483
|
*/
|
|
484
|
+
|
|
485
|
+
|
|
448
486
|
function matchHypothesis(hypotheses, wanted) {
|
|
449
487
|
const raw = String(wanted ?? '').trim()
|
|
450
488
|
if (raw === '') return null
|
|
@@ -479,7 +517,7 @@ export function apply(ctx, config = {}) {
|
|
|
479
517
|
if (found === null) return { ok: false, wanted: String(raw) }
|
|
480
518
|
if (!ids.includes(found.id)) ids.push(found.id)
|
|
481
519
|
}
|
|
482
|
-
return { ok: true, tests: { hypotheses: ids, level: tests.level } }
|
|
520
|
+
return { ok: true, tests: { hypotheses: ids, level: normalizeLevel(tests.level) } }
|
|
483
521
|
}
|
|
484
522
|
|
|
485
523
|
/** 一个已登记步骤检验的判断 id(兼容旧账本的单条写法)。 */
|
|
@@ -734,7 +772,7 @@ export function apply(ctx, config = {}) {
|
|
|
734
772
|
'你是独立评估者:拿着判定标准与产物,做冷静、可复核的评估,产出一张结构化评估卡。你没有参与探索,不背任何方案的立场——这正是你可信的原因。',
|
|
735
773
|
'',
|
|
736
774
|
'**三层信道(按幻觉风险):**',
|
|
737
|
-
'1. 硬信号(零幻觉,最可信)
|
|
775
|
+
'1. 硬信号(零幻觉,最可信):产物自带的客观数字、文件是否存在及字段是否齐、以及执行记录。直接读取已落盘的产物,照抄进评估卡;任务书给了工作区副本时,可以在副本里复跑产物自带的脚本,复跑读数同样是硬信号。不要在原工作区写文件或执行命令,不要润色或估计。',
|
|
738
776
|
'2. 领域信号(逐条核对):逐条对照判据给结论(通过/不通过/不适用 + 一句证据),不发挥、不新增标准。',
|
|
739
777
|
'3. 软判断(有幻觉风险,须隔离标注):报告完整性、方案合理性这类主观项可以给,但必须显式标注「主观评估」。',
|
|
740
778
|
'',
|
|
@@ -745,6 +783,7 @@ export function apply(ctx, config = {}) {
|
|
|
745
783
|
' · **交付成立吗**(`holds`):yes = 判据逐条满足、观测真实;no = 有判据不满足,或观测与记录对不上;unclear = 凭现有材料判不了。',
|
|
746
784
|
' · **每条判断的结果**(`results`,这一步检验几条就给几格):对照它的推翻条件读——support = 没碰到推翻条件,refute = 碰到了,inconclusive = 这次观测区分不了。',
|
|
747
785
|
'- 交付成立与判断被推翻可以同时为真:推翻是有价值的结果,它不让交付失败。不要为了让步骤通过而写 support。',
|
|
786
|
+
'- 你还要查**还有什么没解释**(`anomalies`):原始数据里有没有做的人没登记的异常——参考读数对不上、重复点漂移、不同装置或不同批次之间的差、和常识或本体不符的量级?数字复跑对得上不等于数据可信。每条做的人已排除的未解释项,理由站不站得住?站不住的也写进来。没有就给空数组。',
|
|
748
787
|
'',
|
|
749
788
|
'**回包的形状就是你的动作空间(字段长度由 schema 校验,超了会被拒):**',
|
|
750
789
|
'- `basis` 是**一句话结论**,≤1200 字。**不要在这里写论证**——论证放 `refs`:逐条 `{path, line}` 指到你实际读过的文件与行,让第三方照着就能复核。',
|
|
@@ -756,7 +795,7 @@ export function apply(ctx, config = {}) {
|
|
|
756
795
|
'You are an independent evaluator: with the criteria and the outputs in hand, make a calm, checkable evaluation and produce a structured evaluation card. You took no part in the work and hold no stake in any approach; that is why you can be trusted.',
|
|
757
796
|
'',
|
|
758
797
|
'**Three channels (by hallucination risk):**',
|
|
759
|
-
'1. Hard signals (no hallucination, most trusted): objective numbers in the outputs, whether files exist and have their fields, and the execution record. Read the outputs on disk and copy them into the card;
|
|
798
|
+
'1. Hard signals (no hallucination, most trusted): objective numbers in the outputs, whether files exist and have their fields, and the execution record. Read the outputs on disk and copy them into the card; when the brief gives a copy of the workspace, you may re-run the outputs\' own scripts there, and re-run readings are hard signals too. Do not write files or run commands in the original workspace, and do not polish or estimate.',
|
|
760
799
|
'2. Domain signals (check one by one): give a finding per criterion (pass / fail / not applicable + one line of evidence); do not improvise or add criteria.',
|
|
761
800
|
'3. Soft judgments (hallucination risk, keep them separate): subjective items such as report completeness or soundness may be given, but must be labelled "subjective".',
|
|
762
801
|
'',
|
|
@@ -767,6 +806,7 @@ export function apply(ctx, config = {}) {
|
|
|
767
806
|
' · **Does the delivery hold** (`holds`): yes = every criterion met and the observations are real; no = some criterion unmet, or observations do not match the record; unclear = cannot be decided from the material.',
|
|
768
807
|
' · **The result for each judgment** (`results`, one entry per judgment this step tests): read it against its refutation condition; support = the condition was not hit, refute = it was hit, inconclusive = this observation cannot tell.',
|
|
769
808
|
'- A delivery can hold while a judgment is refuted: refutation is a valuable result and does not fail the delivery. Do not write support to let a step pass.',
|
|
809
|
+
'- You also check **what is still unexplained** (`anomalies`): does the raw data hold anomalies the worker did not record (a reference reading that disagrees, drift between repeated points, a gap between devices or batches, a magnitude that contradicts common knowledge or the ontology)? Re-run numbers matching does not mean the data can be trusted. For each unexplained item the worker ruled out, does the reason hold? If not, list it here too. If there are none, give an empty array.',
|
|
770
810
|
'',
|
|
771
811
|
'**The shape of your reply is your action space (field lengths are checked by the schema; overruns are rejected):**',
|
|
772
812
|
'- `basis` is a **one-sentence conclusion**, ≤1200 characters. **Do not argue here**; arguments go in `refs`: one `{path, line}` per file and line you actually read, so a third party can check.',
|
|
@@ -817,6 +857,33 @@ export function apply(ctx, config = {}) {
|
|
|
817
857
|
additionalProperties: false,
|
|
818
858
|
},
|
|
819
859
|
},
|
|
860
|
+
anomalies: {
|
|
861
|
+
type: 'array',
|
|
862
|
+
description: tr('还没解释的异常:做的人没登记的,或排除理由站不住的。每条一句,带 path:line。没有就给空数组。', 'Anomalies still unexplained: ones the worker did not record, or whose ruling-out does not hold. One sentence each, with path:line. Empty array if none.'),
|
|
863
|
+
items: {
|
|
864
|
+
type: 'object',
|
|
865
|
+
properties: {
|
|
866
|
+
what: { type: 'string', maxLength: 400, description: tr('哪里不符(带 path:line)', 'what does not fit (with path:line)') },
|
|
867
|
+
matters: { type: 'string', enum: ['yes', 'no', 'unclear'], description: tr('它会不会动摇这次的结论', 'whether it could shake this conclusion') },
|
|
868
|
+
},
|
|
869
|
+
required: ['what'],
|
|
870
|
+
additionalProperties: false,
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
lessons: {
|
|
874
|
+
type: 'array',
|
|
875
|
+
description: tr('任务书列了待核的经验(L1、L2……)时,每条各一格;没列就给空数组。', 'When the brief lists lessons to check (L1, L2, ...), one entry each; otherwise an empty array.'),
|
|
876
|
+
items: {
|
|
877
|
+
type: 'object',
|
|
878
|
+
properties: {
|
|
879
|
+
lesson: { type: 'string', maxLength: 20, description: tr('经验编号,如 L1', 'the lesson number, e.g. L1') },
|
|
880
|
+
verdict: { type: 'string', enum: ['support', 'refute', 'inconclusive'] },
|
|
881
|
+
basis: { type: 'string', maxLength: 400, description: tr('一句话:记录里哪处支持或推翻它', 'One sentence: where in the record it is supported or refuted') },
|
|
882
|
+
},
|
|
883
|
+
required: ['lesson', 'verdict'],
|
|
884
|
+
additionalProperties: false,
|
|
885
|
+
},
|
|
886
|
+
},
|
|
820
887
|
refs: {
|
|
821
888
|
type: 'array',
|
|
822
889
|
description: tr('逐条证据引用:文件与行号。裁决要能被第三方照着复核。', 'Evidence references, one per file and line, so a third party can check the verdict.'),
|
|
@@ -844,7 +911,23 @@ export function apply(ctx, config = {}) {
|
|
|
844
911
|
additionalProperties: false,
|
|
845
912
|
})
|
|
846
913
|
|
|
847
|
-
|
|
914
|
+
/**
|
|
915
|
+
* 评估者要看的未解释项:还开着的、做的人排除了的(核它的理由)、交给人的,以及这次交付刚登记的。
|
|
916
|
+
* 这是评估者从「报告和数据一致吗」扩到「还有什么没解释」的那份材料。
|
|
917
|
+
*/
|
|
918
|
+
function anomalyBrief(state, gate) {
|
|
919
|
+
const known = [...(state?.anomalies ?? [])]
|
|
920
|
+
for (const item of Array.isArray(gate?.anomalies) ? gate.anomalies : []) if (!known.some((entry) => entry.id === item.id)) known.push({ ...item, status: 'open' })
|
|
921
|
+
if (known.length === 0) return ['', tr('# 未解释项\n- (做的人没登记任何未解释项:原始数据里有没有它漏掉的,由你查)', '# Unexplained items\n- (the worker recorded none: whether the raw data holds any it missed is for you to check)')]
|
|
922
|
+
const word = { open: tr('开着', 'open'), explained: tr('已解释', 'explained'), ruled_out: tr('已排除', 'ruled out'), escalated: tr('交给人', 'handed to a person') }
|
|
923
|
+
return [
|
|
924
|
+
'',
|
|
925
|
+
tr('# 未解释项(做的人登记的;已排除的要核理由站不站得住)', '# Unexplained items (recorded by the worker; for ruled-out ones, check whether the reason holds)'),
|
|
926
|
+
...known.map((item) => `- ${item.id} · ${word[item.status] ?? item.status} · ${item.what}${item.reason ? tr(` —— 理由:${item.reason}`, ` — reason: ${item.reason}`) : ''}`),
|
|
927
|
+
]
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function evaluatorPrompt(state, step, gate, sessionId, rerunDir = null) {
|
|
848
931
|
const goal = state.goal
|
|
849
932
|
const tested = testedBy(step)
|
|
850
933
|
.map((id) => state.hypotheses.find((item) => item.id === id))
|
|
@@ -884,6 +967,7 @@ export function apply(ctx, config = {}) {
|
|
|
884
967
|
...(gate.confirmed.length === 0 ? ['- (none)'] : gate.confirmed.map((item) => `- ${item.ref} — ${item.bytes} bytes — ${item.digest ?? 'digest unavailable'}`)),
|
|
885
968
|
],
|
|
886
969
|
),
|
|
970
|
+
...anomalyBrief(state, gate),
|
|
887
971
|
...(Array.isArray(gate.extra) && gate.extra.length > 0 ? ['', ...gate.extra] : []),
|
|
888
972
|
'',
|
|
889
973
|
...tr(
|
|
@@ -892,14 +976,18 @@ export function apply(ctx, config = {}) {
|
|
|
892
976
|
'',
|
|
893
977
|
'请只读上述坐标与执行记录,给出两项裁决:拿**已登记的判定标准**对照观测,判交付成立吗(`holds`);再拿每条判断的**推翻条件**对照观测,读出它的结果(`results`)。',
|
|
894
978
|
'准入只核验了「坐标存在且非空」——齐备不等于这一步做完了;判据里的断言(数值、口径、一致性)必须由你逐条核对。',
|
|
895
|
-
|
|
979
|
+
rerunDir === null
|
|
980
|
+
? '你不得修改任何文件,不得执行写入命令,不得重做方案。'
|
|
981
|
+
: `原工作区只读:不要在里面写文件或运行命令。要核对数字就复跑:工作区副本在 ${rerunDir}(不含 clear/),cd 进去运行产物自带的脚本,对照它打印的结果。复跑的读数是硬信号;不要重做方案。`,
|
|
896
982
|
],
|
|
897
983
|
[
|
|
898
984
|
`Working directory: ${sessionCwdLabel(sessionId)}`,
|
|
899
985
|
'',
|
|
900
986
|
'Read only the outputs above and the execution record, and give two verdicts: check the observations against the **registered criteria** to decide whether the delivery holds (`holds`); then read each judgment against its **refutation condition** to give its result (`results`).',
|
|
901
987
|
'Admission only checked that the outputs exist and are non-empty; that is not the same as the step being done. You must check the claims in the criteria (numbers, definitions, consistency) one by one.',
|
|
902
|
-
|
|
988
|
+
rerunDir === null
|
|
989
|
+
? 'You may not modify any file, run write commands or redo the work.'
|
|
990
|
+
: `The original workspace is read-only: do not write files or run commands in it. To check numbers, re-run: a copy of the workspace is at ${rerunDir} (without clear/); cd into it and run the scripts the outputs came from, and compare what they print. Re-run readings are hard signals; do not redo the work.`,
|
|
903
991
|
],
|
|
904
992
|
),
|
|
905
993
|
].join('\n')
|
|
@@ -952,10 +1040,27 @@ export function apply(ctx, config = {}) {
|
|
|
952
1040
|
const line = Number.isInteger(item.line) ? item.line : null
|
|
953
1041
|
refs.push({ path: path.slice(0, 300), line })
|
|
954
1042
|
}
|
|
1043
|
+
const anomalies = []
|
|
1044
|
+
for (const item of Array.isArray(value?.anomalies) ? value.anomalies : []) {
|
|
1045
|
+
const what = typeof item === 'string' ? item.trim() : String(item?.what ?? '').trim()
|
|
1046
|
+
if (what === '') continue
|
|
1047
|
+
const matters = ['yes', 'no', 'unclear'].includes(item?.matters) ? item.matters : 'unclear'
|
|
1048
|
+
anomalies.push({ what: what.slice(0, 400), matters })
|
|
1049
|
+
}
|
|
1050
|
+
const lessons = []
|
|
1051
|
+
for (const item of Array.isArray(value?.lessons) ? value.lessons : []) {
|
|
1052
|
+
if (item === null || typeof item !== 'object') continue
|
|
1053
|
+
const lesson = String(item.lesson ?? '').trim().toUpperCase()
|
|
1054
|
+
const verdict = String(item.verdict ?? '').toLowerCase()
|
|
1055
|
+
if (!/^L\d+$/.test(lesson) || !['support', 'refute', 'inconclusive'].includes(verdict)) continue
|
|
1056
|
+
lessons.push({ lesson, verdict, basis: typeof item.basis === 'string' && item.basis.trim() !== '' ? item.basis.trim().slice(0, 400) : null })
|
|
1057
|
+
}
|
|
955
1058
|
const basis = typeof value?.basis === 'string' && value.basis.trim() !== '' ? value.basis.trim() : tr('评估者未给出依据', 'the evaluator gave no basis')
|
|
956
1059
|
return {
|
|
957
1060
|
holds,
|
|
958
1061
|
results,
|
|
1062
|
+
anomalies,
|
|
1063
|
+
lessons,
|
|
959
1064
|
// 截断是**兜底**:schema 已声明 maxLength,越界的产出本不该到这里;真到了也不能让账本吃下五千字。
|
|
960
1065
|
// 截断标记**算在预算内**:申报多少就必须是多少。
|
|
961
1066
|
basis: basis.length > 1200 ? tr(`${basis.slice(0, 1170)}…(裁到 1200 字;完整论证应由 refs 指认)`, `${basis.slice(0, 1170)}… (cut to 1200 characters; the full argument belongs in refs)`) : basis,
|
|
@@ -1208,7 +1313,7 @@ export function apply(ctx, config = {}) {
|
|
|
1208
1313
|
if (recovered !== null && recovered.ok === true) {
|
|
1209
1314
|
const card = { schema_version: 'clearai.audit.v2', kind: audit.kind ?? 'evidence_audit', step_id: audit.step, auditor_run_id: String(audit.child), holds: recovered.verdict.holds, results: recovered.verdict.results, shortfalls: recovered.verdict.shortfalls, card: recovered.verdict.basis, created_at: Date.now() }
|
|
1210
1315
|
const cardPath = writeAuditCard(sessionId, audit.step, card)
|
|
1211
|
-
mutations.push({ t: 'audit/settled', id: audit.id, step: audit.step, verdict: recovered.verdict.holds, holds: recovered.verdict.holds, results: recovered.verdict.results, basis: recovered.verdict.basis, shortfalls: recovered.verdict.shortfalls, card_path: cardPath, digest: audit.digest ?? null })
|
|
1316
|
+
mutations.push({ t: 'audit/settled', id: audit.id, step: audit.step, verdict: recovered.verdict.holds, holds: recovered.verdict.holds, results: recovered.verdict.results, anomalies: recovered.verdict.anomalies, basis: recovered.verdict.basis, shortfalls: recovered.verdict.shortfalls, card_path: cardPath, digest: audit.digest ?? null })
|
|
1212
1317
|
lines.push(tr(`${audit.step}:裁决从子会话日志取回(交付成立:${recovered.verdict.holds})`, `${audit.step}: verdict recovered from the child session log (holds: ${recovered.verdict.holds})`))
|
|
1213
1318
|
continue
|
|
1214
1319
|
}
|
|
@@ -1280,6 +1385,8 @@ export function apply(ctx, config = {}) {
|
|
|
1280
1385
|
[String(item?.id ?? ''), String(item?.status ?? ''), String(item?.claim ?? '').replace(/\s+/g, ' '), JSON.stringify(item?.assertions ?? null)].join(':'),
|
|
1281
1386
|
)
|
|
1282
1387
|
const materials = (Array.isArray(state?.materials) ? state.materials : []).map((item) => `${String(item?.id ?? '')}:${String(item?.digest ?? '')}`)
|
|
1388
|
+
/** 做的人登记与消解的未解释项是材料;评估者自己报的是回声,不算。 */
|
|
1389
|
+
const anomalies = [...(Array.isArray(state?.anomalies) ? state.anomalies : []), ...(Array.isArray(gate?.anomalies) ? gate.anomalies : [])].filter((item) => item?.by !== 'evaluator').map((item) => `${String(item?.id ?? '')}:${String(item?.status ?? 'open')}`)
|
|
1283
1390
|
const facts = (Array.isArray(state?.facts) ? state.facts : []).map((item) => `${String(item?.id ?? '')}:${String(item?.level ?? '')}:${JSON.stringify(item?.assertions ?? null)}`)
|
|
1284
1391
|
/**
|
|
1285
1392
|
* 步的**判据**与产物一起算材料:判据一变,"这一步算不算做完"就是另一个问题
|
|
@@ -1303,7 +1410,7 @@ export function apply(ctx, config = {}) {
|
|
|
1303
1410
|
return !ref.startsWith('evidence:') && !ref.startsWith('hypothesis:')
|
|
1304
1411
|
})
|
|
1305
1412
|
.map((item) => `${String(item?.ref ?? '')}:${String(item?.bytes ?? '')}:${String(item?.digest ?? '')}`)
|
|
1306
|
-
const root = createHash('sha256').update(JSON.stringify([kind, step.id, goal, steps, materials, hypotheses, facts, material, artifacts])).digest('hex')
|
|
1413
|
+
const root = createHash('sha256').update(JSON.stringify([kind, step.id, goal, steps, materials, hypotheses, facts, material, artifacts, ...(anomalies.length > 0 ? [anomalies] : [])])).digest('hex')
|
|
1307
1414
|
return root.slice(0, 16)
|
|
1308
1415
|
}
|
|
1309
1416
|
|
|
@@ -1399,16 +1506,18 @@ export function apply(ctx, config = {}) {
|
|
|
1399
1506
|
*/
|
|
1400
1507
|
const pendingId = `audit:pending:${kind}:${step.id}:${digest}`
|
|
1401
1508
|
landFact(sessionId, { t: 'audit/dispatched', id: pendingId, step: step.id, plan: plan?.id ?? 'goal', kind, digest, capability: null, evaluator_session: null, status: 'dispatching' })
|
|
1509
|
+
const rerunDir = CFG.auditRerun ? rerunCopy(sessionId, auditKey) : null
|
|
1402
1510
|
const dispatched = await dispatchSubRun({
|
|
1403
1511
|
label: `${kind === 'goal_audit' ? tr('目标评估者', 'Goal evaluator') : tr('评估者', 'Evaluator')} · ${step.id}`,
|
|
1404
1512
|
persona: evaluatorDiscipline(),
|
|
1405
|
-
prompt: evaluatorPrompt(stateOf(sessionId), step, gate, sessionId),
|
|
1513
|
+
prompt: evaluatorPrompt(stateOf(sessionId), step, gate, sessionId, rerunDir),
|
|
1406
1514
|
outputSchema: verdictSchema(),
|
|
1407
|
-
toolFilter: { allow: resolveToolFace(agent, CFG.auditToolFilter) },
|
|
1515
|
+
toolFilter: { allow: resolveToolFace(agent, rerunDir === null ? CFG.auditToolFilter : [...CFG.auditToolFilter, 'bash']) },
|
|
1408
1516
|
parent: agent,
|
|
1409
1517
|
signal,
|
|
1410
1518
|
})
|
|
1411
1519
|
if (dispatched.ok !== true) {
|
|
1520
|
+
dropRerunCopy(rerunDir)
|
|
1412
1521
|
/**
|
|
1413
1522
|
* 派不出去:把那条"正在派"如实结掉。结算与派发**同 id**,且上面那条 pending
|
|
1414
1523
|
* 走的是独立落账通道、在 `withPendingFacts` 的合并结果里排在前面,所以折法先建记录、
|
|
@@ -1418,7 +1527,7 @@ export function apply(ctx, config = {}) {
|
|
|
1418
1527
|
return { holds: 'unknown', results: [], basis: tr(`独立评估者无法派遣(${dispatched.reason})`, `The independent evaluator could not be dispatched (${dispatched.reason})`), shortfalls: ['audit_dispatch_failed'], cardPath: null, mutations }
|
|
1419
1528
|
}
|
|
1420
1529
|
mutations.push({ t: 'audit/dispatched', id: pendingId, step: step.id, plan: plan?.id ?? 'goal', kind, evaluator_session: String(dispatched.run.id), capability: dispatched.capability, digest, status: 'dispatched' })
|
|
1421
|
-
entry = { sessionId, run: dispatched.run, capability: dispatched.capability, auditKey: pendingId, step: step.id, plan: plan?.id ?? 'goal', kind, settled: undefined }
|
|
1530
|
+
entry = { sessionId, run: dispatched.run, capability: dispatched.capability, auditKey: pendingId, step: step.id, plan: plan?.id ?? 'goal', kind, rerunDir, settled: undefined }
|
|
1422
1531
|
pendingAudits.set(key, entry)
|
|
1423
1532
|
entry.settled = dispatched.run.result.then(
|
|
1424
1533
|
(value) => ({ ok: true, value }),
|
|
@@ -1438,6 +1547,7 @@ export function apply(ctx, config = {}) {
|
|
|
1438
1547
|
}
|
|
1439
1548
|
// 已落定:这次派遣的生命周期到此为止。下一次交付是**新的一次评估**(新证据),必须重新派遣。
|
|
1440
1549
|
pendingAudits.delete(key)
|
|
1550
|
+
dropRerunCopy(entry.rerunDir ?? null)
|
|
1441
1551
|
if (outcome.ok !== true) {
|
|
1442
1552
|
return settleUnknown(tr(`评估者失败:${String(outcome.error?.message ?? outcome.error)}`, `The evaluator failed: ${String(outcome.error?.message ?? outcome.error)}`), ['audit_failed'])
|
|
1443
1553
|
}
|
|
@@ -1464,15 +1574,46 @@ export function apply(ctx, config = {}) {
|
|
|
1464
1574
|
} catch {
|
|
1465
1575
|
/* dispose 失败不影响裁决事实 */
|
|
1466
1576
|
}
|
|
1467
|
-
const card = { schema_version: 'clearai.audit.v2', kind, step_id: step.id, auditor_run_id: String(entry.run.id), holds: verdict.holds, results: verdict.results, shortfalls: verdict.shortfalls, card: verdict.basis, created_at: Date.now() }
|
|
1577
|
+
const card = { schema_version: 'clearai.audit.v2', kind, step_id: step.id, auditor_run_id: String(entry.run.id), holds: verdict.holds, results: verdict.results, anomalies: verdict.anomalies, ...(verdict.lessons.length > 0 ? { lessons: verdict.lessons } : {}), shortfalls: verdict.shortfalls, card: verdict.basis, created_at: Date.now() }
|
|
1468
1578
|
const cardPath = writeAuditCard(sessionId, step.id, card)
|
|
1469
1579
|
if (cardPath === null) {
|
|
1470
1580
|
return settleUnknown(tr('评估卡落盘失败:裁决降级', 'Could not save the evaluation card: verdict downgraded'), ['card_persist_failed'])
|
|
1471
1581
|
}
|
|
1472
|
-
mutations.push({ t: 'audit/settled', id: entry.auditKey, step: step.id, verdict: verdict.holds, holds: verdict.holds, results: verdict.results, basis: verdict.basis, shortfalls: verdict.shortfalls, card_path: cardPath, digest })
|
|
1582
|
+
mutations.push({ t: 'audit/settled', id: entry.auditKey, step: step.id, verdict: verdict.holds, holds: verdict.holds, results: verdict.results, anomalies: verdict.anomalies, basis: verdict.basis, shortfalls: verdict.shortfalls, card_path: cardPath, digest })
|
|
1473
1583
|
return { ...verdict, cardPath, digest, mutations }
|
|
1474
1584
|
}
|
|
1475
1585
|
|
|
1586
|
+
/**
|
|
1587
|
+
* **复跑用的工作区副本**:评估者可以在这里跑脚本核数字,原工作区一个字节不碰。
|
|
1588
|
+
*
|
|
1589
|
+
* 为什么要它:L3 的定义是「新产生、可重跑」,而只能读文件的评估者从不重跑,只核对文件彼此是否一致——
|
|
1590
|
+
* 一份自洽但算错的结果照样过。副本不带 `clear/`(账本与评估卡不是复跑的材料)。复制失败就不给副本,
|
|
1591
|
+
* 评估照旧只读,不因此失败。
|
|
1592
|
+
*/
|
|
1593
|
+
function rerunCopy(sessionId, auditKey) {
|
|
1594
|
+
const cwd = sessionCwd(sessionId)
|
|
1595
|
+
if (cwd === null) return null
|
|
1596
|
+
const target = join(tmpdir(), `clearai-rerun-${auditKey}`)
|
|
1597
|
+
const skip = new Set([join(cwd, 'clear'), join(cwd, '.git'), join(cwd, 'node_modules')])
|
|
1598
|
+
try {
|
|
1599
|
+
cpSync(cwd, target, { recursive: true, filter: (source) => !skip.has(source) })
|
|
1600
|
+
return target
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
ctx.logger?.warn?.(`clearai kernel: 复跑副本没建成 ${String(error?.message ?? error).slice(0, 160)}`)
|
|
1603
|
+
dropRerunCopy(target)
|
|
1604
|
+
return null
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function dropRerunCopy(dir) {
|
|
1609
|
+
if (dir === null) return
|
|
1610
|
+
try {
|
|
1611
|
+
rmSync(dir, { recursive: true, force: true })
|
|
1612
|
+
} catch {
|
|
1613
|
+
/* 删不掉留在临时目录里,不影响裁决 */
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1476
1617
|
/** 评估卡落盘(系统的面)。写不进 → 返回 null,调用方 fail-closed。 */
|
|
1477
1618
|
function writeAuditCard(sessionId, stepId, card) {
|
|
1478
1619
|
const file = sessionFile(sessionId, 'clear', 'evidence', 'audits', String(stepId), `${String(card.auditor_run_id)}.json`)
|
|
@@ -1727,6 +1868,7 @@ export function apply(ctx, config = {}) {
|
|
|
1727
1868
|
const CHOICES = {
|
|
1728
1869
|
releaseYes: ['放行这次交付', 'Release this delivery'],
|
|
1729
1870
|
releaseNo: ['先不放行', 'Not now'],
|
|
1871
|
+
releaseCommand: ['放行这条命令', 'Release this command'],
|
|
1730
1872
|
stallContinue: ['让它按缺口再改', 'Let it fix the gaps'],
|
|
1731
1873
|
stallVoid: ['作废这一步', 'Void this step'],
|
|
1732
1874
|
factRetract: ['撤回这条事实', 'Retract this fact'],
|
|
@@ -1775,10 +1917,22 @@ export function apply(ctx, config = {}) {
|
|
|
1775
1917
|
* 没人能答 ⇒ 事实标着「被推翻、待复核」,原生 goal 置为阻塞。
|
|
1776
1918
|
*/
|
|
1777
1919
|
async function reviewRefutedFacts(exec, sessionId, state, refutedHypotheses, basis, mutations) {
|
|
1778
|
-
|
|
1779
|
-
|
|
1920
|
+
/**
|
|
1921
|
+
* 两条路找到被推翻的事实:本目标里升格的(判断 id 对得上),以及被推翻的判断声明了
|
|
1922
|
+
* `retests` 的那条——它可能是别的会话留下的,判断 id 对不上,只有事实 id 跨会话不变。
|
|
1923
|
+
*/
|
|
1924
|
+
const retested = state.hypotheses.filter((item) => refutedHypotheses.includes(item.id) && typeof item.retests === 'string').map((item) => item.retests)
|
|
1925
|
+
const pending = []
|
|
1926
|
+
for (const fact of state.facts ?? []) if (refutedHypotheses.includes(fact.hypothesis) || retested.includes(fact.id)) pending.push(fact)
|
|
1927
|
+
for (const id of retested) {
|
|
1928
|
+
if (pending.some((fact) => fact.id === id)) continue
|
|
1929
|
+
const fact = findFact(sessionId, state, id)
|
|
1930
|
+
if (fact !== null) pending.push(fact)
|
|
1931
|
+
}
|
|
1932
|
+
const open = pending.filter((fact) => fact.review === undefined || fact.review === null)
|
|
1933
|
+
if (open.length === 0) return ''
|
|
1780
1934
|
const notes = []
|
|
1781
|
-
for (const fact of
|
|
1935
|
+
for (const fact of open) {
|
|
1782
1936
|
const asked = await askHuman(exec, {
|
|
1783
1937
|
id: `fact-${fact.id}`,
|
|
1784
1938
|
header: tr('已确立的结论被推翻了', 'An established conclusion was refuted'),
|
|
@@ -1900,6 +2054,9 @@ export function apply(ctx, config = {}) {
|
|
|
1900
2054
|
// 读盘按 (mtime, size) 记进程内缓存:没变的文件不重读、不重算摘要。
|
|
1901
2055
|
|
|
1902
2056
|
const FACTS_REL = ['clear', 'knowledge', 'facts']
|
|
2057
|
+
const LESSONS_REL = ['clear', 'knowledge', 'lessons']
|
|
2058
|
+
/** 经验的四类(与 `domain-language.js` 的 `LESSON_KINDS` 同值):坑、要先核的、会骗人的捷径、先验。 */
|
|
2059
|
+
const LESSON_KINDS = ['trap', 'check', 'shortcut', 'prior']
|
|
1903
2060
|
const ONTOLOGY_REL = ['clear', 'ontology']
|
|
1904
2061
|
const ONTOLOGY_BRANCH_DIRS = ['concepts', 'relations', 'entities']
|
|
1905
2062
|
/** 读面有界(与 `domain-language.js` 的 `WORKSPACE_LIMITS` 同值):超出的如实报成读不成,不静默截断。 */
|
|
@@ -1927,6 +2084,7 @@ export function apply(ctx, config = {}) {
|
|
|
1927
2084
|
}
|
|
1928
2085
|
}
|
|
1929
2086
|
walk(FACTS_REL, false)
|
|
2087
|
+
walk(LESSONS_REL, false)
|
|
1930
2088
|
for (const branch of ONTOLOGY_BRANCH_DIRS) walk([...ONTOLOGY_REL, branch], true)
|
|
1931
2089
|
return out
|
|
1932
2090
|
}
|
|
@@ -2215,7 +2373,7 @@ export function apply(ctx, config = {}) {
|
|
|
2215
2373
|
defineTool({
|
|
2216
2374
|
name: 'Frame',
|
|
2217
2375
|
description:
|
|
2218
|
-
'立约或修订:往当前原生 goal 上挂一份「怎样算回答了」的判据(done_criteria)与候选判断(每条一句话主张 + 一句「什么结果会推翻它」)。没有原生 goal 时会建一个,目标那一句话(headline)就是它的说法;续跑、暂停由原生 goal 管。修订必须带 reason,版本 +1
|
|
2376
|
+
'立约或修订:往当前原生 goal 上挂一份「怎样算回答了」的判据(done_criteria)与候选判断(每条一句话主张 + 一句「什么结果会推翻它」)。没有原生 goal 时会建一个,目标那一句话(headline)就是它的说法;续跑、暂停由原生 goal 管。修订必须带 reason,版本 +1,旧值全部留痕;改判据文本要带一份独立裁决。不可逆的动作写进 irreversible(匹配的命令执行前要人放行)。同一时间只开一个目标;它跨计划存在,一张 Plan 只承载它的一个阶段。',
|
|
2219
2377
|
parameters: {
|
|
2220
2378
|
type: 'object',
|
|
2221
2379
|
properties: {
|
|
@@ -2233,17 +2391,35 @@ export function apply(ctx, config = {}) {
|
|
|
2233
2391
|
type: 'string',
|
|
2234
2392
|
description: '改判据文本时要带的独立裁决 auditKey:改「怎样算完成」不能被顺手做掉',
|
|
2235
2393
|
},
|
|
2236
|
-
promote_at_level: { type: 'string', enum:
|
|
2394
|
+
promote_at_level: { type: 'string', enum: MODEL_LEVELS, description: '升格门槛(默认 L3)' },
|
|
2395
|
+
irreversible: {
|
|
2396
|
+
type: 'array',
|
|
2397
|
+
description: '不可逆的动作:做了就收不回、或结果不可重复的那几件(跑中试、下单、发出去的消息……)。每件写一句说明和它的命令特征(命令里一定会出现的一段原文)。匹配的命令执行前要人放行。修订目标时不传 = 不变',
|
|
2398
|
+
items: {
|
|
2399
|
+
type: 'object',
|
|
2400
|
+
properties: {
|
|
2401
|
+
action: { type: 'string', description: '一句话:这是什么动作' },
|
|
2402
|
+
command: { type: 'string', description: '命令特征:这类命令里一定出现的一段原文(如 "reactor.mjs state.json pilot")' },
|
|
2403
|
+
},
|
|
2404
|
+
required: ['action', 'command'],
|
|
2405
|
+
additionalProperties: false,
|
|
2406
|
+
},
|
|
2407
|
+
},
|
|
2237
2408
|
hypotheses: {
|
|
2238
2409
|
type: 'array',
|
|
2239
2410
|
description:
|
|
2240
|
-
'候选假设:每条一句话主张 +
|
|
2411
|
+
'候选假设:每条一句话主张 + 一句推翻条件,一条只说一件事(「A,且 B」拆成两条);可带**类型化断言**(可选,提供即严校:谓词与概念必须已登记、宾语形态要合值域、同一事实里不许自相矛盾)。不写断言照旧成立——断言是加法,不是门槛。修订目标时不传这一项 = 判断不变;传了就是这一版的完整清单,没列出的会记成「已替换」。',
|
|
2241
2412
|
items: {
|
|
2242
2413
|
type: 'object',
|
|
2243
2414
|
properties: {
|
|
2244
2415
|
name: { type: 'string', description: '短名,十二个汉字或二十来个字母以内,你自己起(如「python3 能跑」)。之后说起这条判断、在别的工具里引用它,都用这个名字' },
|
|
2245
2416
|
claim: { type: 'string' },
|
|
2246
2417
|
refute_when: { type: 'string' },
|
|
2418
|
+
retests: {
|
|
2419
|
+
type: 'string',
|
|
2420
|
+
description:
|
|
2421
|
+
'这条判断在复检哪条已写进长期知识的事实:写那条事实的 id(clear/knowledge/facts/<id>.json 的文件名)。claim 照抄那条事实原来的说法、拿新数据再验一遍,不要写成「它被推翻了」:这条判断被推翻,系统才会当场问人撤回还是维持那条事实——别的会话留下的事实也一样',
|
|
2422
|
+
},
|
|
2247
2423
|
assertions: {
|
|
2248
2424
|
type: 'array',
|
|
2249
2425
|
description: '断言:主词–谓词–宾语(引用领域词汇里的 id)',
|
|
@@ -2300,7 +2476,7 @@ export function apply(ctx, config = {}) {
|
|
|
2300
2476
|
: carried
|
|
2301
2477
|
? state.hypotheses
|
|
2302
2478
|
.filter((item) => item.goal === state.goal.id && item.status !== 'superseded')
|
|
2303
|
-
.map((item) => ({ claim: item.claim, refute_when: item.refute_when, ...(item.name ? { name: item.name } : {}), ...(Array.isArray(item.assertions) ? { assertions: item.assertions } : {}) }))
|
|
2479
|
+
.map((item) => ({ claim: item.claim, refute_when: item.refute_when, ...(item.name ? { name: item.name } : {}), ...(item.retests ? { retests: item.retests } : {}), ...(Array.isArray(item.assertions) ? { assertions: item.assertions } : {}) }))
|
|
2304
2480
|
: []
|
|
2305
2481
|
const names = hypotheses.map((hypothesis) => (typeof hypothesis?.name === 'string' ? hypothesis.name.trim() : '')).filter((name) => name !== '')
|
|
2306
2482
|
if (new Set(names).size !== names.length) return fail('hypothesis_name_duplicate', tr('两条判断用了同一个短名:短名是用来区分判断的,换一个。', 'Two judgments share a short name; short names tell judgments apart, so pick another.'))
|
|
@@ -2308,6 +2484,9 @@ export function apply(ctx, config = {}) {
|
|
|
2308
2484
|
if (typeof hypothesis?.claim !== 'string' || hypothesis.claim.trim() === '') return fail('hypothesis_claim_required', tr('每条假设要有一句话主张。', 'Each judgment needs a one-sentence claim.'))
|
|
2309
2485
|
if (typeof hypothesis?.refute_when !== 'string' || hypothesis.refute_when.trim() === '') return fail('hypothesis_refute_required', tr('每条假设必须写清「什么结果会推翻它」——没有推翻条件的假设无法被检验。', 'Each judgment must say what result would refute it; a judgment with no refutation condition cannot be tested.'))
|
|
2310
2486
|
if (hypothesis?.name !== undefined && (typeof hypothesis.name !== 'string' || textWidth(hypothesis.name.trim()) > (HANDLE_LIMIT + 4) * 2)) return fail('hypothesis_name_too_long', tr(`判断的短名要短:${HANDLE_LIMIT} 个汉字(或约 ${HANDLE_LIMIT * 2} 个字母)以内,能让人一眼认出是哪条就够。`, `Keep the short name short: within ${HANDLE_LIMIT} CJK characters (about ${HANDLE_LIMIT * 2} letters), just enough to recognize the judgment at a glance.`))
|
|
2487
|
+
if (hypothesis?.retests !== undefined && hypothesis.retests !== null && findFact(sessionId, state, hypothesis.retests) === null) {
|
|
2488
|
+
return fail('retests_unknown', tr(`retests 指向的事实「${String(hypothesis.retests).slice(0, 40)}」不在 clear/knowledge/facts/ 里:写那条事实文件名里的 id(如 f-ab12cd),或者去掉 retests。`, `The fact retests points to ("${String(hypothesis.retests).slice(0, 40)}") is not in clear/knowledge/facts/: use the id from that fact's file name (such as f-ab12cd), or drop retests.`))
|
|
2489
|
+
}
|
|
2311
2490
|
/**
|
|
2312
2491
|
* **宽松+校验**:不写断言放行(断言是加法),写了就在**落账之前**严校——
|
|
2313
2492
|
* 引用不存在的谓词 / 概念、宾语形态不合值域、同一事实自相矛盾,一律当场拒。
|
|
@@ -2401,7 +2580,16 @@ export function apply(ctx, config = {}) {
|
|
|
2401
2580
|
)
|
|
2402
2581
|
}
|
|
2403
2582
|
}
|
|
2404
|
-
const promoteAtLevel = LEVELS.includes(args.promote_at_level) ? args.promote_at_level : 'L3'
|
|
2583
|
+
const promoteAtLevel = LEVELS.includes(args.promote_at_level) ? normalizeLevel(args.promote_at_level) : 'L3'
|
|
2584
|
+
/** 不可逆动作:修订时没传就沿用上一版(与判断同一条纪律:「不提供」不等于「清空」)。 */
|
|
2585
|
+
const irreversible = Array.isArray(args.irreversible)
|
|
2586
|
+
? args.irreversible.map((item) => ({ action: String(item?.action ?? '').trim(), command: String(item?.command ?? '').trim() })).filter((item) => item.action !== '' && item.command.length >= 3)
|
|
2587
|
+
: isRevision
|
|
2588
|
+
? (state.goal.irreversible ?? [])
|
|
2589
|
+
: []
|
|
2590
|
+
if (Array.isArray(args.irreversible) && irreversible.length !== args.irreversible.length) {
|
|
2591
|
+
return fail('irreversible_invalid', tr('每件不可逆动作都要有一句说明(action)和命令特征(command,至少 3 个字符,是这类命令里一定出现的一段原文)。', 'Each irreversible action needs a one-line description (action) and a command signature (command, at least 3 characters of text that always appears in such a command).'))
|
|
2592
|
+
}
|
|
2405
2593
|
/**
|
|
2406
2594
|
* **修订不许给同一句话发新身份。**
|
|
2407
2595
|
*
|
|
@@ -2430,6 +2618,8 @@ export function apply(ctx, config = {}) {
|
|
|
2430
2618
|
...(given !== '' ? { name: given } : kept !== '' ? { name: kept } : {}),
|
|
2431
2619
|
claim,
|
|
2432
2620
|
refute_when: hypothesis.refute_when.trim(),
|
|
2621
|
+
/** 复检哪条已有事实(跨会话的身份是事实 id;判断 id 只活在本目标里)。 */
|
|
2622
|
+
...(typeof hypothesis.retests === 'string' && hypothesis.retests.trim() !== '' ? { retests: hypothesis.retests.trim() } : {}),
|
|
2433
2623
|
/** 断言随假设落账;没写就是 null(加法,不是门槛)。 */
|
|
2434
2624
|
assertions: Array.isArray(hypothesis.assertions) ? hypothesis.assertions : null,
|
|
2435
2625
|
version: index + 1,
|
|
@@ -2445,6 +2635,7 @@ export function apply(ctx, config = {}) {
|
|
|
2445
2635
|
criteria: criteriaList,
|
|
2446
2636
|
criteria_note: criteriaNote,
|
|
2447
2637
|
promote_at_level: promoteAtLevel,
|
|
2638
|
+
irreversible,
|
|
2448
2639
|
revision,
|
|
2449
2640
|
reason: isRevision ? String(args.reason).trim() : null,
|
|
2450
2641
|
hypotheses: nextHypotheses,
|
|
@@ -2468,15 +2659,63 @@ export function apply(ctx, config = {}) {
|
|
|
2468
2659
|
|
|
2469
2660
|
// ── Conclude ──────────────────────────────────────────────────────────
|
|
2470
2661
|
|
|
2662
|
+
/** 结案时提议的经验:去掉空的、限 5 条,字段裁到能放进一张卡。 */
|
|
2663
|
+
function proposedLessonsOf(value) {
|
|
2664
|
+
const out = []
|
|
2665
|
+
for (const item of Array.isArray(value) ? value : []) {
|
|
2666
|
+
const text = typeof item?.text === 'string' ? item.text.trim() : ''
|
|
2667
|
+
if (text === '' || out.length >= 5) continue
|
|
2668
|
+
out.push({
|
|
2669
|
+
text: text.slice(0, 300),
|
|
2670
|
+
kind: LESSON_KINDS.includes(item.kind) ? item.kind : 'trap',
|
|
2671
|
+
about: Array.isArray(item.about) ? item.about.map((entry) => String(entry).trim()).filter((entry) => entry !== '').slice(0, 6) : [],
|
|
2672
|
+
evidence: typeof item.evidence === 'string' && item.evidence.trim() !== '' ? item.evidence.trim().slice(0, 300) : null,
|
|
2673
|
+
boundary: typeof item.boundary === 'string' && item.boundary.trim() !== '' ? item.boundary.trim().slice(0, 300) : null,
|
|
2674
|
+
})
|
|
2675
|
+
}
|
|
2676
|
+
return out
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/** 交给结案评估者的那一段:待核的经验,编号 L1……,判法写在一起。 */
|
|
2680
|
+
function lessonBrief(lessons) {
|
|
2681
|
+
if (lessons.length === 0) return []
|
|
2682
|
+
const kindWord = { trap: tr('坑', 'trap'), check: tr('要核的', 'check'), shortcut: tr('会骗人的捷径', 'misleading shortcut'), prior: tr('先验', 'prior') }
|
|
2683
|
+
return [
|
|
2684
|
+
tr('# 待核的经验(做的人提议:下次在同类现场要怎么做)', '# Lessons to check (proposed by the worker: what to do differently next time in a similar setting)'),
|
|
2685
|
+
...lessons.map((item, index) => `- L${index + 1} · ${kindWord[item.kind]} · ${item.text}${item.evidence ? tr(`(凭据:${item.evidence})`, ` (evidence: ${item.evidence})`) : ''}${item.boundary ? tr(`(不适用:${item.boundary})`, ` (does not apply: ${item.boundary})`) : ''}`),
|
|
2686
|
+
tr(
|
|
2687
|
+
'每条在 `lessons` 里各给一格:support = 记录里确实发生过它说的事,而且照它做下次会更好;refute = 记录与它矛盾,或照它做会误导;inconclusive = 凭记录判不了,或它只是这次结论的复述。经验不影响 `holds`。',
|
|
2688
|
+
'Give one `lessons` entry each: support = the record shows what it says happened, and following it would do better next time; refute = the record contradicts it, or following it would mislead; inconclusive = the record cannot decide, or it only restates this run\'s conclusion. Lessons do not affect `holds`.',
|
|
2689
|
+
),
|
|
2690
|
+
]
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2471
2693
|
defineTool({
|
|
2472
2694
|
name: 'Conclude',
|
|
2473
2695
|
description:
|
|
2474
|
-
'结案:交目标验收——这是完成目标的唯一路径(原生「完成目标」会被拒)。系统**无条件**派独立评估者,拿目标判据与转写忠实度逐条核对;判据达成才结案为 achieved:原生 goal
|
|
2696
|
+
'结案:交目标验收——这是完成目标的唯一路径(原生「完成目标」会被拒)。系统**无条件**派独立评估者,拿目标判据与转写忠实度逐条核对;判据达成才结案为 achieved:原生 goal 置为完成;评估者同时逐条判还活着的判断,判为支持的(或中途已达门槛的)升格为事实;各步收下的产物声明为交付卡片;随结案提议的经验(`lessons`)由评估者逐条核,支持的写进 clear/knowledge/lessons/。否则目标保持开放并回注缺口。**顺序**:achieved 之前必须先把计划收尾(`ClosePlan`)。放弃(`abandoned`)不受此限:原生 goal 置为阻塞,写明原因,由人决定结束。',
|
|
2475
2697
|
parameters: {
|
|
2476
2698
|
type: 'object',
|
|
2477
2699
|
properties: {
|
|
2478
2700
|
outcome: { type: 'string', enum: ['achieved', 'abandoned'], description: 'achieved=判据已达成;abandoned=如实说清阻塞后放弃' },
|
|
2479
2701
|
note: { type: 'string', description: '结案说明' },
|
|
2702
|
+
lessons: {
|
|
2703
|
+
type: 'array',
|
|
2704
|
+
maxItems: 5,
|
|
2705
|
+
description: '经验(可选,只在 achieved 时核):这次摸出的、下次在同类现场改变做法的东西,不是这次结论的复述。评估者逐条对照记录核,判为支持的写进 clear/knowledge/lessons/,之后的会话在立题和定计划时会看到',
|
|
2706
|
+
items: {
|
|
2707
|
+
type: 'object',
|
|
2708
|
+
properties: {
|
|
2709
|
+
text: { type: 'string', description: '一句话,下次怎么做(「这台装置的温度读数先拿参考温度核一次」)' },
|
|
2710
|
+
kind: { type: 'string', enum: LESSON_KINDS, description: 'trap=会踩的坑;check=要先核的读数或假设;shortcut=省事但会骗人的做法;prior=对这类体系的先验(例如「两个因素常常耦合,先做斜向扫描」)' },
|
|
2711
|
+
about: { type: 'array', items: { type: 'string' }, description: '涉及的装置、量或概念(短名,用来下次匹配)' },
|
|
2712
|
+
evidence: { type: 'string', description: '这次记录里哪处表明了它(步骤、文件)' },
|
|
2713
|
+
boundary: { type: 'string', description: '在什么条件下不适用' },
|
|
2714
|
+
},
|
|
2715
|
+
required: ['text', 'kind'],
|
|
2716
|
+
additionalProperties: false,
|
|
2717
|
+
},
|
|
2718
|
+
},
|
|
2480
2719
|
},
|
|
2481
2720
|
required: ['outcome'],
|
|
2482
2721
|
additionalProperties: false,
|
|
@@ -2543,15 +2782,22 @@ export function apply(ctx, config = {}) {
|
|
|
2543
2782
|
* 第四阶段删掉的两道门:「跳级没写理由」(`ExplainLevelSkip` 整套删除)与
|
|
2544
2783
|
* 「将升格的命题没有断言形态」(降为缺口)。依据见 `docs/less-is-more-plan.zh-CN.md` 第四阶段。
|
|
2545
2784
|
*/
|
|
2785
|
+
/**
|
|
2786
|
+
* **结案评估顺带判每条还活着的判断**(这一目标的、没被推翻过、还没成事实的)。
|
|
2787
|
+
*
|
|
2788
|
+
* 为什么:升格原来只看模型自己给步骤标的等级,而 L0–L2 允许自判——真跑里模型一律标 L1/L2,
|
|
2789
|
+
* 于是 9 次运行 0 条事实,跨会话复用与复检全部空转。结案这次独立评估是唯一必然发生的独立判断,
|
|
2790
|
+
* 让它逐条读判断:它判「支持」的,与中途到了门槛的一样升格。不加新机制、不多派一次评估者。
|
|
2791
|
+
*/
|
|
2792
|
+
const candidates = derived.hypotheses.filter(
|
|
2793
|
+
(hypothesis) =>
|
|
2794
|
+
(typeof hypothesis.goal !== 'string' || hypothesis.goal === goal.id) &&
|
|
2795
|
+
(hypothesis.status === 'alive' || hypothesis.status === 'proposed') &&
|
|
2796
|
+
(hypothesis.refutations ?? 0) === 0 &&
|
|
2797
|
+
!(state.facts ?? []).some((fact) => fact.hypothesis === hypothesis.id),
|
|
2798
|
+
)
|
|
2546
2799
|
if (CFG.requireLandedEntities && derived.knowledge.mode === 'knowledge') {
|
|
2547
|
-
const
|
|
2548
|
-
const promotable = derived.hypotheses.filter(
|
|
2549
|
-
(hypothesis) =>
|
|
2550
|
-
(hypothesis.status === 'alive' || hypothesis.status === 'proposed') &&
|
|
2551
|
-
(hypothesis.refutations ?? 0) === 0 &&
|
|
2552
|
-
levelIndexOf(hypothesis.supportedLevel) >= threshold,
|
|
2553
|
-
)
|
|
2554
|
-
const offGraph = promotable.filter((hypothesis) => (hypothesis.unlanded ?? []).length > 0)
|
|
2800
|
+
const offGraph = candidates.filter((hypothesis) => (hypothesis.unlanded ?? []).length > 0)
|
|
2555
2801
|
if (offGraph.length > 0) {
|
|
2556
2802
|
return fail(
|
|
2557
2803
|
'entities_unlanded',
|
|
@@ -2563,12 +2809,21 @@ export function apply(ctx, config = {}) {
|
|
|
2563
2809
|
}
|
|
2564
2810
|
}
|
|
2565
2811
|
const unfinished = plan === null ? [] : plan.steps.filter((step) => step.status === 'open')
|
|
2566
|
-
const syntheticStep = {
|
|
2812
|
+
const syntheticStep = {
|
|
2813
|
+
id: `goal:${goal.id}`,
|
|
2814
|
+
ordinal: 0,
|
|
2815
|
+
do: tr(`核验目标 ${goal.id} 的判据与转写忠实度`, `Check goal ${goal.id} against its criteria and transcription fidelity`),
|
|
2816
|
+
done_criteria: goal.done_criteria,
|
|
2817
|
+
artifacts: [],
|
|
2818
|
+
tests: candidates.length === 0 ? null : { hypotheses: candidates.map((hypothesis) => hypothesis.id), level: goal.promote_at_level },
|
|
2819
|
+
}
|
|
2820
|
+
const proposedLessons = proposedLessonsOf(args.lessons)
|
|
2567
2821
|
const gate = {
|
|
2568
2822
|
confirmed: [
|
|
2569
2823
|
...state.evidence.map((item) => ({ ref: `evidence:${item.id}`, bytes: 0, digest: `verdict=${item.verdict}` })),
|
|
2570
2824
|
...derived.hypotheses.map((item) => ({ ref: `hypothesis:${item.id}`, bytes: 0, digest: `${item.status}/支持到${item.supportedLevel ?? '—'}` })),
|
|
2571
2825
|
],
|
|
2826
|
+
extra: lessonBrief(proposedLessons),
|
|
2572
2827
|
}
|
|
2573
2828
|
const audit = await runEvaluator(sessionId, exec.agent, plan, syntheticStep, gate, 'goal_audit', exec.signal)
|
|
2574
2829
|
mutations.push(...audit.mutations)
|
|
@@ -2636,8 +2891,45 @@ export function apply(ctx, config = {}) {
|
|
|
2636
2891
|
* 三样全零 = **没人碰过它**。不强制证实/证伪——但「没看过」不能被写成「没问题」,
|
|
2637
2892
|
* 所以这里把它记进结案那条变更里,卡片与面板都说得出来。
|
|
2638
2893
|
*/
|
|
2894
|
+
/** 结案评估对每条判断的结果,各记一份独立证据(等级记成目标的升格门槛:判它的是独立评估者)。 */
|
|
2895
|
+
const goalOrigin = buildEvidenceOrigins({
|
|
2896
|
+
cwd: sessionCwd(sessionId),
|
|
2897
|
+
accepted: [],
|
|
2898
|
+
confirmed: [],
|
|
2899
|
+
cardPath: audit.cardPath ?? null,
|
|
2900
|
+
evaluatorSession: audit.mutations.find((mutation) => mutation.t === 'audit/dispatched')?.evaluator_session ?? null,
|
|
2901
|
+
basis: audit.basis,
|
|
2902
|
+
})
|
|
2903
|
+
const judged = new Map()
|
|
2904
|
+
for (const result of audit.results ?? []) {
|
|
2905
|
+
const hypothesis = candidates.find((item) => item.id === result.hypothesis || item.name === result.hypothesis)
|
|
2906
|
+
if (hypothesis === undefined || judged.has(hypothesis.id)) continue
|
|
2907
|
+
const evidenceId = `e-${Math.random().toString(36).slice(2, 8)}`
|
|
2908
|
+
judged.set(hypothesis.id, { verdict: result.verdict, evidence: evidenceId })
|
|
2909
|
+
mutations.push({
|
|
2910
|
+
t: 'evidence/recorded',
|
|
2911
|
+
id: evidenceId,
|
|
2912
|
+
step: syntheticStep.id,
|
|
2913
|
+
plan: plan?.id ?? 'goal',
|
|
2914
|
+
hypothesis: hypothesis.id,
|
|
2915
|
+
verdict: result.verdict,
|
|
2916
|
+
level: goal.promote_at_level,
|
|
2917
|
+
evaluator: 'independent',
|
|
2918
|
+
basis: result.basis ?? audit.basis,
|
|
2919
|
+
refs: goalOrigin.paths,
|
|
2920
|
+
origins: goalOrigin.origins,
|
|
2921
|
+
anchor: 'auditor',
|
|
2922
|
+
basis_reviewable: true,
|
|
2923
|
+
})
|
|
2924
|
+
}
|
|
2925
|
+
const refutedAtClose = [...judged].filter(([, item]) => item.verdict === 'refute').map(([id]) => id)
|
|
2926
|
+
const factReview =
|
|
2927
|
+
refutedAtClose.length === 0
|
|
2928
|
+
? ''
|
|
2929
|
+
: await reviewRefutedFacts(exec, sessionId, state, refutedAtClose, (audit.results ?? []).find((item) => item.verdict === 'refute')?.basis ?? audit.basis, mutations)
|
|
2639
2930
|
const untouched = derived.hypotheses.filter(
|
|
2640
|
-
(hypothesis) =>
|
|
2931
|
+
(hypothesis) =>
|
|
2932
|
+
!judged.has(hypothesis.id) && (hypothesis.supportedLevel === null || hypothesis.supportedLevel === undefined) && (hypothesis.refutations ?? 0) === 0 && (hypothesis.inconclusive ?? 0) === 0,
|
|
2641
2933
|
)
|
|
2642
2934
|
mutations.push({ t: 'goal/closed', id: goal.id, status: 'achieved', verdict: 'support', note: args.note ?? null, unjudged: untouched.map((hypothesis) => hypothesis.id) })
|
|
2643
2935
|
const continuationNote = completeNativeGoal(exec.agent)
|
|
@@ -2666,7 +2958,11 @@ export function apply(ctx, config = {}) {
|
|
|
2666
2958
|
if ((state.facts ?? []).some((fact) => fact.hypothesis === hypothesis.id)) continue
|
|
2667
2959
|
if (hypothesis.status !== 'alive' && hypothesis.status !== 'proposed') continue
|
|
2668
2960
|
if (hypothesis.refutations > 0) continue
|
|
2669
|
-
|
|
2961
|
+
const atClose = judged.get(hypothesis.id)
|
|
2962
|
+
if (atClose?.verdict === 'refute') continue
|
|
2963
|
+
const reached = levelIndexOf(hypothesis.supportedLevel) >= threshold
|
|
2964
|
+
if (!reached && atClose?.verdict !== 'support') continue
|
|
2965
|
+
const factLevel = reached ? hypothesis.supportedLevel : goal.promote_at_level
|
|
2670
2966
|
const assertions = Array.isArray(hypothesis.assertions) ? hypothesis.assertions : null
|
|
2671
2967
|
/**
|
|
2672
2968
|
* **第三道校验(升格时)**:断言涉及的谓词、类型、主体此刻都要在本体文件里成立。
|
|
@@ -2688,6 +2984,7 @@ export function apply(ctx, config = {}) {
|
|
|
2688
2984
|
const evidence = evidenceFor(state, hypothesis.id)
|
|
2689
2985
|
.filter((item) => item.verdict === 'support')
|
|
2690
2986
|
.map((item) => item.id)
|
|
2987
|
+
if (atClose?.verdict === 'support') evidence.push(atClose.evidence)
|
|
2691
2988
|
/**
|
|
2692
2989
|
* 升格这一刻它用到的词条的含义指纹:之后谁改了定义,这条事实就知道要复核。
|
|
2693
2990
|
* 「用到」= 断言引用的,加上主张与边界原文里按词面提到的(模型很少写断言,见 fingerprintDefinitions)。
|
|
@@ -2700,7 +2997,7 @@ export function apply(ctx, config = {}) {
|
|
|
2700
2997
|
} catch {
|
|
2701
2998
|
definitions = null
|
|
2702
2999
|
}
|
|
2703
|
-
const record = { id: factId, goal: goal.id, hypothesis: hypothesis.id, text: hypothesis.claim, scope: hypothesis.refute_when ?? null, level:
|
|
3000
|
+
const record = { id: factId, goal: goal.id, hypothesis: hypothesis.id, text: hypothesis.claim, scope: hypothesis.refute_when ?? null, level: factLevel ?? null, evidence, assertions, definitions }
|
|
2704
3001
|
const path = persistFact(sessionId, record)
|
|
2705
3002
|
/**
|
|
2706
3003
|
* 事实带上**边界**(`scope` = 这条假设的推翻条件):没有边界的事实,下一轮没人敢用;
|
|
@@ -2718,7 +3015,7 @@ export function apply(ctx, config = {}) {
|
|
|
2718
3015
|
hypothesis: hypothesis.id,
|
|
2719
3016
|
text: hypothesis.claim,
|
|
2720
3017
|
scope: hypothesis.refute_when ?? null,
|
|
2721
|
-
level:
|
|
3018
|
+
level: factLevel ?? null,
|
|
2722
3019
|
evidence,
|
|
2723
3020
|
assertions,
|
|
2724
3021
|
definitions,
|
|
@@ -2726,6 +3023,21 @@ export function apply(ctx, config = {}) {
|
|
|
2726
3023
|
})
|
|
2727
3024
|
promoted.push({ id: factId, claim: hypothesis.claim })
|
|
2728
3025
|
}
|
|
3026
|
+
/** 评估者判为支持的经验写成文件;别的如实说出来,不写。 */
|
|
3027
|
+
const lessonVerdicts = new Map((audit.lessons ?? []).map((item) => [item.lesson, item]))
|
|
3028
|
+
const keptLessons = []
|
|
3029
|
+
const droppedLessons = []
|
|
3030
|
+
proposedLessons.forEach((lesson, index) => {
|
|
3031
|
+
const verdict = lessonVerdicts.get(`L${index + 1}`) ?? null
|
|
3032
|
+
if (verdict?.verdict !== 'support') {
|
|
3033
|
+
droppedLessons.push(`${quote(lesson.text)}${tr(':', ': ')}${verdict === null ? tr('评估者没判', 'the evaluator gave no verdict') : `${verdict.verdict === 'refute' ? tr('被推翻', 'refuted') : tr('说不清', 'inconclusive')}${verdict.basis ? `(${verdict.basis})` : ''}`}`)
|
|
3034
|
+
return
|
|
3035
|
+
}
|
|
3036
|
+
const record = { id: `l-${Math.random().toString(36).slice(2, 8)}`, goal: goal.id, ...lesson, basis: verdict.basis }
|
|
3037
|
+
const path = persistLesson(sessionId, record)
|
|
3038
|
+
mutations.push({ t: 'lesson/recorded', id: record.id, goal: goal.id, text: lesson.text, kind: lesson.kind, about: lesson.about, evidence: lesson.evidence, boundary: lesson.boundary, basis: verdict.basis, path })
|
|
3039
|
+
keptLessons.push(record)
|
|
3040
|
+
})
|
|
2729
3041
|
return done({
|
|
2730
3042
|
ok: true,
|
|
2731
3043
|
code: 'goal_achieved',
|
|
@@ -2740,6 +3052,9 @@ export function apply(ctx, config = {}) {
|
|
|
2740
3052
|
? tr(`\n结案时有 ${untouched.length} 条判断**一次都没检验过**:${untouched.map((hypothesis) => `「${handleOf(hypothesis)}」`).join('、')}——没检验不是「没问题」,是「没看过」;它们留在账上,随时可以补一次检验。`, `\nAt conclusion, ${untouched.length} judgment(s) were **never tested**: ${untouched.map((hypothesis) => `"${handleOf(hypothesis)}"`).join(', ')}. Untested does not mean fine; it means not looked at. They stay in the ledger and can be tested any time.`)
|
|
2741
3053
|
: '') +
|
|
2742
3054
|
tr('\n被推翻与被替换的判断都留在记录里。', '\nRefuted and replaced judgments stay in the record.') +
|
|
3055
|
+
(keptLessons.length > 0 ? tr(`\n写下的经验:${keptLessons.map((item) => `${item.text}(clear/knowledge/lessons/${item.id}.json)`).join(' / ')}。之后的会话立题和定计划时会看到。`, `\nLessons written: ${keptLessons.map((item) => `${item.text} (clear/knowledge/lessons/${item.id}.json)`).join(' / ')}. Later sessions see them when framing and planning.`) : '') +
|
|
3056
|
+
(droppedLessons.length > 0 ? tr(`\n没写下的经验:\n${droppedLessons.map((item) => `- ${item}`).join('\n')}`, `\nLessons not written:\n${droppedLessons.map((item) => `- ${item}`).join('\n')}`) : '') +
|
|
3057
|
+
factReview +
|
|
2743
3058
|
/** 「怎么做」也要攒下来:这次摸出的可复用做法写成原生技能,下次宿主会列出来。 */
|
|
2744
3059
|
tr('\n这次如果摸出了以后还会用的做法(怎么查、怎么算、怎么验),写成原生技能:.agents/skills/<名字>/SKILL.md(description 写清什么时候用)。', '\nIf this run found a method worth reusing (how to look up, compute or verify), write it as a native skill: .agents/skills/<name>/SKILL.md (with a description that says when to use it).') +
|
|
2745
3060
|
continuationNote +
|
|
@@ -2768,6 +3083,25 @@ export function apply(ctx, config = {}) {
|
|
|
2768
3083
|
}
|
|
2769
3084
|
}
|
|
2770
3085
|
|
|
3086
|
+
/**
|
|
3087
|
+
* **一条经验一个文件**(`clear/knowledge/lessons/<经验 id>.json`),只有系统写。
|
|
3088
|
+
*
|
|
3089
|
+
* 经验与事实分开放:事实改变信念(「T 与 cat 沿斜脊耦合」),经验改变下一次怎么做
|
|
3090
|
+
* (「这台装置的温度要拿参考读数核」)。两者都要过独立评估才写,写了下个会话就读得到。
|
|
3091
|
+
* 人要撤回一条,在文件上把 `status` 改成 `retracted`。
|
|
3092
|
+
*/
|
|
3093
|
+
function persistLesson(sessionId, record) {
|
|
3094
|
+
const file = sessionFile(sessionId, 'clear', 'knowledge', 'lessons', `${record.id}.json`)
|
|
3095
|
+
if (file === null) return null
|
|
3096
|
+
try {
|
|
3097
|
+
writeTextFile(file, `${JSON.stringify({ ...record, session: sessionId, status: 'active', at: Date.now() }, null, 2)}\n`)
|
|
3098
|
+
return file
|
|
3099
|
+
} catch (error) {
|
|
3100
|
+
ctx.logger?.warn?.(`clearai kernel: 经验落盘失败 ${String(error?.message ?? error)}`)
|
|
3101
|
+
return null
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
2771
3105
|
// ── 计划六件 ───────────────────────────────────────────────────────────
|
|
2772
3106
|
|
|
2773
3107
|
const STEP_SCHEMA = {
|
|
@@ -2786,11 +3120,15 @@ export function apply(ctx, config = {}) {
|
|
|
2786
3120
|
items: { type: 'string' },
|
|
2787
3121
|
description: '这一步检验哪几条判断:填判断的短名(也认主张原文)。一次观测同时判几条竞争的判断时(比较那一步),把它们都列上',
|
|
2788
3122
|
},
|
|
2789
|
-
level: { type: 'string', enum:
|
|
3123
|
+
level: { type: 'string', enum: MODEL_LEVELS, description: 'L2 = 自判(你写结果与依据);L3 = 独立评估(系统派评估者判);L4 = 人放行(不可重复或来自外部的证据,先过人)' },
|
|
2790
3124
|
},
|
|
2791
3125
|
required: ['hypotheses', 'level'],
|
|
2792
3126
|
additionalProperties: false,
|
|
2793
3127
|
},
|
|
3128
|
+
expect: {
|
|
3129
|
+
type: 'string',
|
|
3130
|
+
description: '可选,动手前写:这一步预计看到什么(尽量给数或方向),以及预期从哪来(本体里的哪条关系 / 哪条经验 / 哪条判断,或直说是直觉)。写下来,落空才看得见',
|
|
3131
|
+
},
|
|
2794
3132
|
},
|
|
2795
3133
|
required: ['id', 'do', 'done_criteria'],
|
|
2796
3134
|
additionalProperties: false,
|
|
@@ -2800,7 +3138,7 @@ export function apply(ctx, config = {}) {
|
|
|
2800
3138
|
defineTool({
|
|
2801
3139
|
name: 'CreatePlan',
|
|
2802
3140
|
description:
|
|
2803
|
-
'立约:把复杂任务立成一份计划。每步一句话说清做什么(do)、以何物为证(artifacts)、以及判定标准(done_criteria,在结果出现之前写下)。检验判断的步骤用 tests:{hypotheses, level} 声明验哪几条、什么等级(比较竞争路线的那一步,把竞争的几条都列上)。最多 25 步。约立起便锁定:局部挫折改当前步,不要推倒重来。',
|
|
3141
|
+
'立约:把复杂任务立成一份计划。每步一句话说清做什么(do)、以何物为证(artifacts)、以及判定标准(done_criteria,在结果出现之前写下);可带预期(expect:预计看到什么、从哪来)。检验判断的步骤用 tests:{hypotheses, level} 声明验哪几条、什么等级(比较竞争路线的那一步,把竞争的几条都列上)。最多 25 步。约立起便锁定:局部挫折改当前步,不要推倒重来。',
|
|
2804
3142
|
parameters: {
|
|
2805
3143
|
type: 'object',
|
|
2806
3144
|
properties: {
|
|
@@ -2836,7 +3174,7 @@ export function apply(ctx, config = {}) {
|
|
|
2836
3174
|
goal: goal?.id ?? null,
|
|
2837
3175
|
phase_id: goal?.id ?? null,
|
|
2838
3176
|
brief,
|
|
2839
|
-
steps: args.steps.map((step) => ({ id: step.id, do: step.do, artifacts: step.artifacts ?? [], done_criteria: step.done_criteria, tests: resolvedTests.get(step.id) ?? null })),
|
|
3177
|
+
steps: args.steps.map((step) => ({ id: step.id, do: step.do, artifacts: step.artifacts ?? [], done_criteria: step.done_criteria, tests: resolvedTests.get(step.id) ?? null, ...(typeof step.expect === 'string' && step.expect.trim() !== '' ? { expect: step.expect.trim() } : {}) })),
|
|
2840
3178
|
})
|
|
2841
3179
|
return done({
|
|
2842
3180
|
ok: true,
|
|
@@ -2855,13 +3193,14 @@ export function apply(ctx, config = {}) {
|
|
|
2855
3193
|
defineTool({
|
|
2856
3194
|
name: 'RevisePlan',
|
|
2857
3195
|
description:
|
|
2858
|
-
'改当前计划,不动进度(返回 progress_changed=false)
|
|
3196
|
+
'改当前计划,不动进度(返回 progress_changed=false),四种动作:`action="expect"` 给还没做的一步写下预期(给 `step_id` 与 `expect`:动手前写,落空才看得见);`action="add"` 补一步(漏了活就补上,给 `step`);`action="refine"` 精化一步的判定标准(给 `step_id` 与新的 `done_criteria`,旧判据留在日志里);`action="void"` 带因作废一步(给 `step_id` 与 `reason`:发现某步本不该存在就作废并说明缘由——作废留痕光明正大,为凑完成而造证是大忌;已交付的步不能作废)。',
|
|
2859
3197
|
parameters: {
|
|
2860
3198
|
type: 'object',
|
|
2861
3199
|
properties: {
|
|
2862
|
-
action: { type: 'string', enum: ['add', 'refine', 'void'], description: 'add=补一步;refine=改判据;void=带因作废' },
|
|
3200
|
+
action: { type: 'string', enum: ['expect', 'add', 'refine', 'void'], description: 'expect=写预期;add=补一步;refine=改判据;void=带因作废' },
|
|
2863
3201
|
step: { ...STEP_SCHEMA, description: 'action=add:新步' },
|
|
2864
|
-
step_id: { type: 'string', description: 'action=refine / void:哪一步' },
|
|
3202
|
+
step_id: { type: 'string', description: 'action=expect / refine / void:哪一步' },
|
|
3203
|
+
expect: { type: 'string', description: 'action=expect:预计看到什么,以及预期从哪来(关系 / 经验 / 判断,或直觉)' },
|
|
2865
3204
|
done_criteria: { type: 'string', description: 'action=refine:新的判定标准' },
|
|
2866
3205
|
reason: { type: 'string', description: '为什么改(action=void 必填)' },
|
|
2867
3206
|
},
|
|
@@ -2884,14 +3223,21 @@ export function apply(ctx, config = {}) {
|
|
|
2884
3223
|
if (plan.steps.some((step) => step.id === args.step.id)) return fail('duplicate_step', tr(`步骤 id 已存在:${args.step.id}`, `Step id already exists: ${args.step.id}`))
|
|
2885
3224
|
const resolved = resolveTests(state.hypotheses, args.step.tests)
|
|
2886
3225
|
if (resolved.ok !== true) return fail('unknown_hypothesis', tr(`步骤 ${args.step.id} 声明的判断「${resolved.wanted}」对不上任何一条已登记的判断。`, `Step ${args.step.id} names the judgment "${resolved.wanted}", which matches no registered judgment. `) + hypothesisMenu(state.hypotheses))
|
|
2887
|
-
const amended = { id: args.step.id, do: args.step.do, artifacts: args.step.artifacts ?? [], done_criteria: args.step.done_criteria, tests: resolved.tests }
|
|
3226
|
+
const amended = { id: args.step.id, do: args.step.do, artifacts: args.step.artifacts ?? [], done_criteria: args.step.done_criteria, tests: resolved.tests, ...(typeof args.step.expect === 'string' && args.step.expect.trim() !== '' ? { expect: args.step.expect.trim() } : {}) }
|
|
2888
3227
|
mutations.push({ t: 'plan/amended', plan: plan.id, step: amended })
|
|
2889
3228
|
if (plan.blocked !== undefined) mutations.push({ t: 'block/cleared', plan: plan.id, step: plan.blocked.step })
|
|
2890
3229
|
return done({ ok: true, code: 'plan_amended', progress_changed: false, message: tr(`已补一步 ${args.step.id}(进度不变)。`, `Added step ${args.step.id} (progress unchanged).`) })
|
|
2891
3230
|
}
|
|
2892
|
-
if (
|
|
3231
|
+
if (!['expect', 'refine', 'void'].includes(args.action)) return fail('unknown_action', tr('action 只能是 expect / add / refine / void。', 'action must be expect, add, refine or void.'))
|
|
2893
3232
|
const step = plan.steps.find((item) => item.id === args.step_id)
|
|
2894
3233
|
if (step === undefined) return fail('unknown_step', tr(`没有这一步:${args.step_id}`, `No such step: ${args.step_id}`))
|
|
3234
|
+
if (args.action === 'expect') {
|
|
3235
|
+
if (step.status !== 'open') return fail('step_settled', tr(`步骤 ${step.id} 已落定(${step.status}):预期要在动手前写。`, `Step ${step.id} is settled (${step.status}); expectations are written before acting.`))
|
|
3236
|
+
const expect = String(args.expect ?? '').trim()
|
|
3237
|
+
if (expect.length < 4) return fail('expect_required', tr('预期不能为空:写预计看到什么,以及它从哪来。', 'The expectation cannot be empty: say what you expect to see and where that comes from.'))
|
|
3238
|
+
mutations.push({ t: 'step/expected', plan: plan.id, step: step.id, expect })
|
|
3239
|
+
return done({ ok: true, code: 'step_expected', progress_changed: false, message: tr(`步骤 ${step.id} 的预期已记下。交付时拿结果对它:对不上的地方写进 AdvancePlan 的 anomalies。`, `Expectation recorded for step ${step.id}. When you deliver, compare the result with it: whatever does not match goes in AdvancePlan's anomalies.`) })
|
|
3240
|
+
}
|
|
2895
3241
|
if (args.action === 'refine') {
|
|
2896
3242
|
if (step.status !== 'open') return fail('step_settled', tr(`步骤 ${step.id} 已落定(${step.status}),判据不再可改。`, `Step ${step.id} is settled (${step.status}); its criteria can no longer change.`))
|
|
2897
3243
|
const criteria = String(args.done_criteria ?? '').trim()
|
|
@@ -2959,7 +3305,7 @@ export function apply(ctx, config = {}) {
|
|
|
2959
3305
|
defineTool({
|
|
2960
3306
|
name: 'AdvancePlan',
|
|
2961
3307
|
description:
|
|
2962
|
-
'交付一步(唯一完成动词)
|
|
3308
|
+
'交付一步(唯一完成动词):把观测交上来。系统先做观测准入——声明的产物存在、非空、结构合法;准入只看收不收,不做裁决。交付成立这一步就完成——它检验的判断被支持、被推翻还是说不清,**都算完成**,结果单独记成证据。结果和预期对不上的地方写进 anomalies。L2(自判)由你给 basis(交付凭什么成立,必须能被复查)与 results(这一步检验的每条判断各一格);L3 以上两项都由系统派独立评估者判,你写 results 会被拒绝。没有物证,就还没有完成——没有手动标记这回事。',
|
|
2963
3309
|
parameters: {
|
|
2964
3310
|
type: 'object',
|
|
2965
3311
|
properties: {
|
|
@@ -2969,10 +3315,15 @@ export function apply(ctx, config = {}) {
|
|
|
2969
3315
|
description: '观测:这一步拿到的原始结果(相对 workspace 的路径 + 一句说明)',
|
|
2970
3316
|
items: { type: 'object', properties: { ref: { type: 'string' }, note: { type: 'string' } }, required: ['ref'], additionalProperties: false },
|
|
2971
3317
|
},
|
|
2972
|
-
|
|
3318
|
+
anomalies: {
|
|
3319
|
+
type: 'array',
|
|
3320
|
+
description: '未解释:这一步的结果里和预期或本体对不上的地方、说不通的读数(一条一句,写哪里不符)。它们挂在卡上,直到被解释、写明理由排除、或交给人(用 Anomaly 工具)。不要把它们解释过去',
|
|
3321
|
+
items: { type: 'object', properties: { what: { type: 'string', description: '哪里不符:预期什么、实际什么' }, anchor: { type: 'string', description: '可选:在哪个实体或装置上(实体 id 或名字)' } }, required: ['what'], additionalProperties: false },
|
|
3322
|
+
},
|
|
3323
|
+
basis: { type: 'string', description: '交付凭什么成立:引用了哪个产物里的哪个事实(必须可复查)。仅自判(L2)由你写' },
|
|
2973
3324
|
results: {
|
|
2974
3325
|
type: 'array',
|
|
2975
|
-
description: '
|
|
3326
|
+
description: '仅自判(L2):这一步检验的每条判断各一格,对照它的推翻条件读结果:没碰到推翻条件是「支持」,碰到了是「推翻」,这次观测区分不了是「不确定」。推翻和不确定都不妨碍这一步完成',
|
|
2976
3327
|
items: {
|
|
2977
3328
|
type: 'object',
|
|
2978
3329
|
properties: {
|
|
@@ -3042,8 +3393,16 @@ export function apply(ctx, config = {}) {
|
|
|
3042
3393
|
accepted.push({ id: materialId, ref })
|
|
3043
3394
|
}
|
|
3044
3395
|
|
|
3396
|
+
/**
|
|
3397
|
+
* 未解释项在准入**之前**就落账(走独立通道):它是观测时的发现,交付成没成都不该丢。
|
|
3398
|
+
* 这一次登记的也随交付交给评估者(见 `evaluatorPrompt`)。
|
|
3399
|
+
*/
|
|
3400
|
+
const opened = openAnomalies(sessionId, args.anomalies, { step: step.id, by: 'model' })
|
|
3401
|
+
mutations.push(...opened)
|
|
3402
|
+
|
|
3045
3403
|
// ② 准入
|
|
3046
3404
|
const gate = admission(cwd, step)
|
|
3405
|
+
gate.anomalies = opened
|
|
3047
3406
|
mutations.push({
|
|
3048
3407
|
t: 'admission/checked',
|
|
3049
3408
|
step: step.id,
|
|
@@ -3127,12 +3486,21 @@ export function apply(ctx, config = {}) {
|
|
|
3127
3486
|
let auditSessionId = null
|
|
3128
3487
|
/** 复用说明(模型与人都看得到的那一句);不复用时为空串。 */
|
|
3129
3488
|
let reuseNote = ''
|
|
3489
|
+
/** 评估者报的、做的人没登记的异常(折法把它们记成未解释项)。 */
|
|
3490
|
+
let foundByEvaluator = []
|
|
3130
3491
|
if (levelIndex > SELF_JUDGE_MAX_INDEX) {
|
|
3131
3492
|
if (Array.isArray(args.results) && args.results.length > 0) {
|
|
3132
3493
|
return fail('verdict_not_accepted', tr(`${level} 的两项裁决只能由机器或独立评估者写:做的人不判自己。去掉 results 重新交付,系统会派评估者。`, `At ${level}, both verdicts can only be written by a machine check or an independent evaluator; the worker does not judge itself. Deliver again without results and the system will dispatch an evaluator.`))
|
|
3133
3494
|
}
|
|
3495
|
+
/** 评估者能跑命令之后,收下的产物在评估前后必须一字不差:改过就不认这份裁决。 */
|
|
3496
|
+
const digestsOf = () => gate.confirmed.map((item) => (readableCwd ? sha256File(isAbsolute(item.ref) ? item.ref : resolvePath(cwd, item.ref)) : null)).join('|')
|
|
3497
|
+
const beforeAudit = CFG.auditRerun ? digestsOf() : ''
|
|
3134
3498
|
const audit = await runEvaluator(sessionId, exec.agent, plan, step, gate, 'evidence_audit', exec.signal)
|
|
3135
3499
|
mutations.push(...audit.mutations)
|
|
3500
|
+
foundByEvaluator = audit.reused === true ? [] : Array.isArray(audit.anomalies) ? audit.anomalies : []
|
|
3501
|
+
if (CFG.auditRerun && audit.reused !== true && ['yes', 'no', 'unclear'].includes(audit.holds) && digestsOf() !== beforeAudit) {
|
|
3502
|
+
return fail('evaluator_touched_outputs', tr('评估期间,收下的产物被改动过(评估者应当只在副本里复跑):这份裁决不认。重新交付会再派一次评估。', 'The accepted outputs changed during evaluation (the evaluator should re-run only in the copy), so this verdict is not accepted. Delivering again dispatches a new evaluation.'), { mutations })
|
|
3503
|
+
}
|
|
3136
3504
|
if (audit.holds === 'pending') return fail('audit_pending', tr(`独立评估者仍在跑:${plainIds(sessionId, audit.basis)}。先观察当前事实,再谈重试——不要重复派遣。`, `The independent evaluator is still running: ${plainIds(sessionId, audit.basis)}. Look at the current facts before retrying; do not dispatch again.`), { mutations })
|
|
3137
3505
|
if (audit.holds === 'unknown') {
|
|
3138
3506
|
const count = countBlock('audit_unavailable', audit.basis)
|
|
@@ -3287,6 +3655,7 @@ export function apply(ctx, config = {}) {
|
|
|
3287
3655
|
message:
|
|
3288
3656
|
tr(`第 ${step.ordinal} 步(${step.id})已交付(${evaluator === 'independent' ? '独立评估者判的' : '你自己判的,依据已记下'})。收下的观测:${gate.confirmed.map((item) => item.ref).join(', ') || '(无)'}。`, `Step ${step.ordinal} (${step.id}) delivered (${evaluator === 'independent' ? 'judged by the independent evaluator' : 'judged by you; the basis is recorded'}). Observations accepted: ${gate.confirmed.map((item) => item.ref).join(', ') || '(none)'}.`) +
|
|
3289
3657
|
outcome +
|
|
3658
|
+
expectationNote(step, opened, foundByEvaluator) +
|
|
3290
3659
|
(refuted ? tr('\n推翻也是有价值的结果:它和支持一样记进证据,判断的状态由证据算。', '\nA refutation is a valuable result too: it is recorded as evidence just like support, and the judgment status is computed from the evidence.') : '') +
|
|
3291
3660
|
factReview +
|
|
3292
3661
|
reuseNote,
|
|
@@ -3294,6 +3663,66 @@ export function apply(ctx, config = {}) {
|
|
|
3294
3663
|
},
|
|
3295
3664
|
})
|
|
3296
3665
|
|
|
3666
|
+
/**
|
|
3667
|
+
* ═══ 未解释项:登记与消解 ═══
|
|
3668
|
+
*
|
|
3669
|
+
* 失分最多的不是「没核」,而是**看见了异常却把它解释过去**(温度计偏 8 °C 被判成「两次读数存疑」)。
|
|
3670
|
+
* 所以未解释项是一等状态:登记了就挂在卡上,只有三个去处——被一条判断或关系解释、写明理由排除、交给人。
|
|
3671
|
+
* 它不阻塞结案,但结案时随交付交给评估者,由评估者判它动不动摇结论。
|
|
3672
|
+
*/
|
|
3673
|
+
const ANOMALY_OUTCOMES = ['explained', 'ruled_out', 'escalated']
|
|
3674
|
+
defineTool({
|
|
3675
|
+
name: 'Anomaly',
|
|
3676
|
+
description:
|
|
3677
|
+
'未解释项(反常):和预期或本体对不上的观测、说不通的读数。`action="open"` 登记一条(给 `what`,可选 `anchor`);`action="resolve"` 给它一个去处(给 `id`、`outcome` 与 `reason`):explained = 被一条判断、关系或新查到的原因解释了(`by` 写是哪条);ruled_out = 写明理由排除(比如证实是录入错误);escalated = 交给人。不要为了让卡干净而排除:评估者会核排除的理由。',
|
|
3678
|
+
parameters: {
|
|
3679
|
+
type: 'object',
|
|
3680
|
+
properties: {
|
|
3681
|
+
action: { type: 'string', enum: ['open', 'resolve'] },
|
|
3682
|
+
what: { type: 'string', description: 'action=open:哪里不符——预期什么、实际什么' },
|
|
3683
|
+
anchor: { type: 'string', description: 'action=open,可选:在哪个实体或装置上' },
|
|
3684
|
+
id: { type: 'string', description: 'action=resolve:哪一条(u-… 的 id)' },
|
|
3685
|
+
outcome: { type: 'string', enum: ANOMALY_OUTCOMES, description: 'explained=已解释;ruled_out=排除;escalated=交给人' },
|
|
3686
|
+
reason: { type: 'string', description: 'action=resolve:凭什么(解释是什么 / 为什么能排除 / 要人定什么)' },
|
|
3687
|
+
by: { type: 'string', description: 'action=resolve,可选:解释它的那条判断、关系或事实' },
|
|
3688
|
+
},
|
|
3689
|
+
required: ['action'],
|
|
3690
|
+
additionalProperties: false,
|
|
3691
|
+
},
|
|
3692
|
+
output: CARD_OUTPUT,
|
|
3693
|
+
async execute(args, exec) {
|
|
3694
|
+
const call = open(exec)
|
|
3695
|
+
if (call.ok !== true) return call.response
|
|
3696
|
+
const { hostService, sessionId, state, mutations } = call
|
|
3697
|
+
const done = finish(hostService, sessionId, mutations)
|
|
3698
|
+
if (args.action === 'open') {
|
|
3699
|
+
const opened = openAnomalies(sessionId, [{ what: args.what, anchor: args.anchor }], { step: firstOpenStep(activePlanOf(state))?.id ?? null, by: 'model' })
|
|
3700
|
+
if (opened.length === 0) return fail('what_required', tr('要写哪里不符:预期什么、实际什么。', 'Say what does not fit: what was expected and what was seen.'))
|
|
3701
|
+
mutations.push(...opened)
|
|
3702
|
+
return done({ ok: true, code: 'anomaly_opened', message: tr(`已登记未解释 ${opened[0].id}。它挂在卡上,直到被解释、排除或交给人。`, `Unexplained item ${opened[0].id} recorded. It stays on the card until explained, ruled out or handed to a person.`) })
|
|
3703
|
+
}
|
|
3704
|
+
if (args.action !== 'resolve') return fail('unknown_action', tr('action 只能是 open / resolve。', 'action must be open or resolve.'))
|
|
3705
|
+
const wanted = String(args.id ?? '').trim()
|
|
3706
|
+
const known = (state.anomalies ?? []).filter((item) => item.status === 'open')
|
|
3707
|
+
const target = known.find((item) => item.id === wanted) ?? (wanted.length >= 3 ? known.filter((item) => item.id.startsWith(wanted)) : []).at(0)
|
|
3708
|
+
if (target === undefined) return fail('unknown_anomaly', tr(`没有这条开着的未解释:${wanted || '(没给 id)'}。开着的:${known.map((item) => `${item.id}(${clip(item.what, 30)})`).join('、') || '(没有)'}`, `No open unexplained item ${wanted || '(no id given)'}. Open ones: ${known.map((item) => `${item.id} (${clip(item.what, 30)})`).join(', ') || '(none)'}`))
|
|
3709
|
+
if (!ANOMALY_OUTCOMES.includes(args.outcome)) return fail('outcome_required', tr('outcome 只能是 explained / ruled_out / escalated。', 'outcome must be explained, ruled_out or escalated.'))
|
|
3710
|
+
const reason = String(args.reason ?? '').trim()
|
|
3711
|
+
if (reason.length < 4) return fail('reason_required', tr('要写凭什么:解释是什么、为什么能排除、或要人定什么。', 'Say on what grounds: the explanation, why it can be ruled out, or what a person must decide.'))
|
|
3712
|
+
const by = String(args.by ?? '').trim()
|
|
3713
|
+
mutations.push({ t: 'anomaly/resolved', id: target.id, outcome: args.outcome, reason: reason.slice(0, 600), ...(by === '' ? {} : { by: by.slice(0, 120) }) })
|
|
3714
|
+
const word = { explained: tr('已解释', 'explained'), ruled_out: tr('已排除', 'ruled out'), escalated: tr('交给人', 'handed to a person') }[args.outcome]
|
|
3715
|
+
return done({
|
|
3716
|
+
ok: true,
|
|
3717
|
+
code: 'anomaly_resolved',
|
|
3718
|
+
message:
|
|
3719
|
+
tr(`${target.id}:${word}。`, `${target.id}: ${word}.`) +
|
|
3720
|
+
(args.outcome === 'explained' ? tr('这条解释如果改变了判断或计划,现在就改(Frame / RevisePlan);它说的若是一条一般规律,写进本体的关系。', ' If this explanation changes a judgment or the plan, change it now (Frame / RevisePlan); if it states a general regularity, write it into the ontology as a relation.') : '') +
|
|
3721
|
+
(args.outcome === 'escalated' ? tr('它会随交付交给评估者与人。', ' It goes with the delivery to the evaluator and the person.') : ''),
|
|
3722
|
+
})
|
|
3723
|
+
},
|
|
3724
|
+
})
|
|
3725
|
+
|
|
3297
3726
|
/**
|
|
3298
3727
|
* ═══ 当档(人在场 / 无人值守):人的事实,内核负责下发 ═══
|
|
3299
3728
|
*
|
|
@@ -3352,6 +3781,26 @@ export function apply(ctx, config = {}) {
|
|
|
3352
3781
|
* 为什么在文件里而不是只留在账本:别的会话与模型读的都是 `clear/knowledge/facts/`,
|
|
3353
3782
|
* 撤回过的若还原样躺在那里,下一轮会照旧引用一条已经作废的事实。
|
|
3354
3783
|
*/
|
|
3784
|
+
/**
|
|
3785
|
+
* 按事实 id 找一条事实:先看本会话的账,再看工作区里的事实文件(别的会话留下的)。
|
|
3786
|
+
* 找不到返回 null。读文件而不是只读同步进来的快照:Frame 那一拍的同步可能还没跑。
|
|
3787
|
+
*/
|
|
3788
|
+
function findFact(sessionId, state, id) {
|
|
3789
|
+
const key = String(id ?? '').trim()
|
|
3790
|
+
if (!/^[A-Za-z0-9_-]+$/.test(key)) return null
|
|
3791
|
+
const own = (state.facts ?? []).find((fact) => fact.id === key)
|
|
3792
|
+
if (own !== undefined) return own
|
|
3793
|
+
const cwd = sessionCwd(sessionId)
|
|
3794
|
+
if (cwd === null) return null
|
|
3795
|
+
try {
|
|
3796
|
+
const data = JSON.parse(readFileSync(join(cwd, 'clear', 'knowledge', 'facts', `${key}.json`), 'utf8'))
|
|
3797
|
+
if (String(data?.id ?? '') !== key || typeof data?.text !== 'string') return null
|
|
3798
|
+
return { id: key, text: data.text, hypothesis: data.hypothesis ?? null, review: data.review ?? null, foreign: true }
|
|
3799
|
+
} catch {
|
|
3800
|
+
return null
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3355
3804
|
function markFactReviewed(cwd, fact, review) {
|
|
3356
3805
|
if (cwd === null || cwd === undefined || !/^[A-Za-z0-9_-]+$/.test(String(fact?.id ?? ''))) return null
|
|
3357
3806
|
const file = join(cwd, 'clear', 'knowledge', 'facts', `${fact.id}.json`)
|
|
@@ -3414,7 +3863,7 @@ export function apply(ctx, config = {}) {
|
|
|
3414
3863
|
* `ontology` 里**模型可写**的是三支本体目录(`concepts` / `relations` / `entities`);
|
|
3415
3864
|
* 字段定义 `SCHEMA.json` 与过程本体那份货架仍归系统。
|
|
3416
3865
|
*/
|
|
3417
|
-
const PROTECTED_REL = [['clear', 'evidence'], ['clear', 'knowledge', 'facts'], ['clear', 'goals'], ['clear', 'ontology']]
|
|
3866
|
+
const PROTECTED_REL = [['clear', 'evidence'], ['clear', 'knowledge', 'facts'], ['clear', 'knowledge', 'lessons'], ['clear', 'goals'], ['clear', 'ontology']]
|
|
3418
3867
|
const OPEN_REL = ONTOLOGY_BRANCH_DIRS.map((branch) => ['clear', 'ontology', branch])
|
|
3419
3868
|
|
|
3420
3869
|
/** 工作区内的相对路径(正斜杠);在工作区外或取不到工作区返回 null。 */
|
|
@@ -3510,6 +3959,43 @@ export function apply(ctx, config = {}) {
|
|
|
3510
3959
|
}
|
|
3511
3960
|
|
|
3512
3961
|
|
|
3962
|
+
/**
|
|
3963
|
+
* **不可逆动作的人放行(L4),拦在命令上**。
|
|
3964
|
+
*
|
|
3965
|
+
* 从前那道门挂在 L4 步骤的交付上,而不可逆的动作往往就是一条命令:模型直接跑了它,
|
|
3966
|
+
* 门形同虚设。所以 Frame 里声明的不可逆动作(命令特征)在**执行之前**就问人:放行才跑,
|
|
3967
|
+
* 没放行或没人能答就拒。放行落一条 `human/released`;当前步是 L4 的,这次放行也算它的放行。
|
|
3968
|
+
* 返回 null = 放行(或与不可逆动作无关)。
|
|
3969
|
+
*/
|
|
3970
|
+
async function releaseIrreversible(exec, sessionId, state, command) {
|
|
3971
|
+
if (!CFG.l4RequiresHumanRelease || state?.goal === null || state?.goal === undefined || state.goal.status !== 'open') return null
|
|
3972
|
+
const action = (Array.isArray(state.goal.irreversible) ? state.goal.irreversible : []).find((item) => typeof item?.command === 'string' && item.command !== '' && command.includes(item.command))
|
|
3973
|
+
if (action === undefined) return null
|
|
3974
|
+
const plan = activePlanOf(state)
|
|
3975
|
+
const step = firstOpenStep(plan)
|
|
3976
|
+
const asked = await askHuman(exec, {
|
|
3977
|
+
id: `release-cmd-${String(exec.callId ?? Date.now().toString(36))}`,
|
|
3978
|
+
header: tr('不可逆动作要人放行', 'Irreversible action needs release'),
|
|
3979
|
+
question: tr(`这条命令是声明过的不可逆动作「${clip(action.action, 60)}」。放行吗?`, `This command is a declared irreversible action ("${clip(action.action, 60)}"). Release it?`),
|
|
3980
|
+
detail: clip(command, 300),
|
|
3981
|
+
options: [
|
|
3982
|
+
{ label: choiceLabel('releaseCommand'), description: tr('让它执行。', 'Let it run.') },
|
|
3983
|
+
{ label: choiceLabel('releaseNo'), description: tr('不执行,模型停下等你。', 'Do not run it; the model stops and waits for you.') },
|
|
3984
|
+
],
|
|
3985
|
+
})
|
|
3986
|
+
if (asked.ok === true && chose(asked, 'releaseCommand')) {
|
|
3987
|
+
landFact(sessionId, { t: 'human/released', id: `r-${Math.random().toString(36).slice(2, 8)}`, plan: plan?.id ?? null, step: step !== null && levelIndexOf(step.tests?.level) === 4 ? step.id : null, action: action.action, call: String(exec.callId ?? ''), via: 'ask', note: asked.note })
|
|
3988
|
+
return null
|
|
3989
|
+
}
|
|
3990
|
+
const why = asked.ok !== true ? unanswered(asked) : tr(`人选了「${asked.choice ?? '没选'}」${asked.note === null ? '' : `:${asked.note}`}`, `the person chose "${asked.choice ?? 'nothing'}"${asked.note === null ? '' : `: ${asked.note}`}`)
|
|
3991
|
+
return {
|
|
3992
|
+
kind: 'deny',
|
|
3993
|
+
reason:
|
|
3994
|
+
tr(`「${action.action}」是不可逆动作,这次没有放行(${why})。不要换个写法绕过去:等人放行,或改计划。`, `"${action.action}" is irreversible and was not released this time (${why}). Do not rewrite the command to get around this: wait for a person, or change the plan.`) +
|
|
3995
|
+
(asked.ok === true ? '' : blockNativeGoal(exec.agent, BLOCK_CODES.needsHuman, tr(`不可逆动作「${action.action}」等人放行。`, `The irreversible action "${action.action}" waits for a person to release it.`))),
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3513
3999
|
ctx.on('tools/pre-execute', (exec, next) => (exec.agent === undefined ? next() : inLanguage(languageOf(String(exec.agent.id)), () => guardTool(exec, next))))
|
|
3514
4000
|
async function guardTool(exec, next) {
|
|
3515
4001
|
{
|
|
@@ -3549,7 +4035,7 @@ export function apply(ctx, config = {}) {
|
|
|
3549
4035
|
if (suspect !== null) {
|
|
3550
4036
|
return {
|
|
3551
4037
|
kind: 'deny',
|
|
3552
|
-
reason: tr(`${suspect} 由系统所有,做的人不能写(clear/evidence、clear/knowledge/facts、clear/goals、clear/ontology 的 SCHEMA.json 与货架)
|
|
4038
|
+
reason: tr(`${suspect} 由系统所有,做的人不能写(clear/evidence、clear/knowledge/facts、clear/knowledge/lessons、clear/goals、clear/ontology 的 SCHEMA.json 与货架)。事实与经验只由独立评估之后的结案写;本体请写在 clear/ontology/concepts、relations、entities 下。`, `${suspect} is owned by the system and the worker cannot write it (clear/evidence, clear/knowledge/facts, clear/knowledge/lessons, clear/goals, and SCHEMA.json and the shelf in clear/ontology). Facts and lessons are written only when a goal concludes after independent evaluation; write the ontology under clear/ontology/concepts, relations and entities.`),
|
|
3553
4039
|
}
|
|
3554
4040
|
}
|
|
3555
4041
|
if (writing && hostService !== undefined) {
|
|
@@ -3564,6 +4050,10 @@ export function apply(ctx, config = {}) {
|
|
|
3564
4050
|
if (test(args.command)) return { kind: 'deny', reason: tr(`危险的 bash 命令(${reason[0]}):改写命令避开这种写法(dangerous bash command)`, `dangerous bash command (${reason[1]}); rewrite the command to avoid this pattern`) }
|
|
3565
4051
|
}
|
|
3566
4052
|
}
|
|
4053
|
+
if (owned && exec.name === 'bash' && typeof args.command === 'string') {
|
|
4054
|
+
const denied = await releaseIrreversible(exec, sessionId, state, args.command)
|
|
4055
|
+
if (denied !== null) return denied
|
|
4056
|
+
}
|
|
3567
4057
|
}
|
|
3568
4058
|
return next()
|
|
3569
4059
|
}
|
|
@@ -3811,12 +4301,14 @@ export function apply(ctx, config = {}) {
|
|
|
3811
4301
|
artifacts: 'What counts as proof: concrete output file paths relative to the workspace (a directory is not proof)',
|
|
3812
4302
|
done_criteria: 'Criterion: written before the result exists, and checkable',
|
|
3813
4303
|
'tests.hypotheses': 'Which judgments this step tests: their short names (claim text also works). When one observation decides several competing judgments (a comparison step), list them all',
|
|
4304
|
+
'tests.level': 'L2 = self-judged (you write the results and basis); L3 = independent evaluation (the system dispatches an evaluator); L4 = released by a person (unrepeatable or external evidence, a person first)',
|
|
4305
|
+
expect: 'Optional, written before acting: what you expect this step to show (a number or a direction if you can), and where the expectation comes from (which ontology relation, lesson or judgment, or plainly intuition). Written down, a miss becomes visible',
|
|
3814
4306
|
}
|
|
3815
4307
|
const prefixed = (prefix, table) => Object.fromEntries(Object.entries(table).map(([key, value]) => [`${prefix}${key}`, value]))
|
|
3816
4308
|
const TOOL_TEXT_EN = {
|
|
3817
4309
|
Frame: {
|
|
3818
4310
|
description:
|
|
3819
|
-
'Set or revise the contract: attach to the current native goal the criteria for "what counts as answered" (done_criteria) and candidate judgments (each a one-sentence claim plus "what result would refute it"). If there is no native goal, one is created, named by the goal headline; continuing and pausing are handled by the native goal. A revision needs a reason; the version goes up by one and every old value stays in the log; changing the criteria text needs an independent verdict. Only one goal is open at a time; it spans plans, and each plan carries one phase of it.',
|
|
4311
|
+
'Set or revise the contract: attach to the current native goal the criteria for "what counts as answered" (done_criteria) and candidate judgments (each a one-sentence claim plus "what result would refute it"). If there is no native goal, one is created, named by the goal headline; continuing and pausing are handled by the native goal. A revision needs a reason; the version goes up by one and every old value stays in the log; changing the criteria text needs an independent verdict. Irreversible actions go in irreversible (a matching command needs release by a person before it runs). Only one goal is open at a time; it spans plans, and each plan carries one phase of it.',
|
|
3820
4312
|
params: {
|
|
3821
4313
|
headline: 'One-sentence goal (≤120 CJK characters or about 240 letters): the line repeated on the card, the panel and continuation notes. Required the first time',
|
|
3822
4314
|
claim: 'Goal: the question the project answers (may be long; where identity and criteria attach)',
|
|
@@ -3826,24 +4318,34 @@ export function apply(ctx, config = {}) {
|
|
|
3826
4318
|
criteria_note: 'Background for the criteria (not part of the verdict; only explains why)',
|
|
3827
4319
|
criteria_verdict: 'Independent verdict auditKey required when changing the criteria text: "what counts as done" cannot be changed in passing',
|
|
3828
4320
|
promote_at_level: 'Promotion threshold (default L3)',
|
|
4321
|
+
irreversible: 'Irreversible actions: the ones that cannot be taken back or whose results cannot be repeated (a pilot run, an order, a message sent out). For each, one line and its command signature (text that always appears in such a command). A matching command needs release by a person before it runs. Omitting this when revising keeps the previous list',
|
|
4322
|
+
'irreversible.items.action': 'One line: what this action is',
|
|
4323
|
+
'irreversible.items.command': 'Command signature: text that always appears in such a command (e.g. "reactor.mjs state.json pilot")',
|
|
3829
4324
|
hypotheses:
|
|
3830
|
-
'Candidate judgments: each a one-sentence claim plus a refutation condition; may carry **typed assertions** (optional, strictly checked when given: predicates and concepts must exist, object forms must fit the range, no contradiction within one fact). Leaving out assertions is fine; they add, they do not gate. When revising the goal, omitting this keeps the judgments; passing it gives this version\'s full list, and any judgment not listed is recorded as replaced.',
|
|
4325
|
+
'Candidate judgments: each a one-sentence claim plus a refutation condition, one thing per judgment (split "A, and B" into two); may carry **typed assertions** (optional, strictly checked when given: predicates and concepts must exist, object forms must fit the range, no contradiction within one fact). Leaving out assertions is fine; they add, they do not gate. When revising the goal, omitting this keeps the judgments; passing it gives this version\'s full list, and any judgment not listed is recorded as replaced.',
|
|
3831
4326
|
'hypotheses.items.name': 'Short name, within about 12 CJK characters or 20-odd letters, chosen by you (e.g. "python3 runs"). Use this name whenever you mention or reference the judgment in other tools',
|
|
4327
|
+
'hypotheses.items.retests': 'Which fact already in long-term knowledge this judgment re-tests: the fact id (the file name in clear/knowledge/facts/<id>.json). Copy that fact\'s original statement as the claim and test it again on new data; do not write "it is refuted" as the claim. When this judgment is refuted, the system asks a person on the spot whether to retract or keep that fact, including facts left by other sessions',
|
|
3832
4328
|
'hypotheses.items.assertions': 'Assertions: subject, predicate, object (ids from the domain vocabulary)',
|
|
3833
4329
|
reason: 'Required when revising the goal: one sentence on why (not needed the first time)',
|
|
3834
4330
|
},
|
|
3835
4331
|
},
|
|
3836
4332
|
Conclude: {
|
|
3837
4333
|
description:
|
|
3838
|
-
'Conclude: hand the goal in for acceptance. This is the only way to complete a goal (the native "complete goal" is refused). The system **always** dispatches an independent evaluator to check the goal criteria and transcription fidelity item by item; only when the criteria are met does the goal conclude as achieved: the native goal is marked complete,
|
|
4334
|
+
'Conclude: hand the goal in for acceptance. This is the only way to complete a goal (the native "complete goal" is refused). The system **always** dispatches an independent evaluator to check the goal criteria and transcription fidelity item by item; only when the criteria are met does the goal conclude as achieved: the native goal is marked complete; the evaluator also judges each live judgment, and those it supports (or that already reached the threshold) are promoted to facts; and the outputs accepted by each step are declared as a deliverables card; lessons proposed with the conclusion (`lessons`) are checked one by one, and supported ones are written to clear/knowledge/lessons/. Otherwise the goal stays open and the gaps come back. **Order**: before achieved, close the plan first (`ClosePlan`). Abandoning (`abandoned`) is not limited by this: the native goal is marked blocked with the reason, and a person decides whether to end it.',
|
|
3839
4335
|
params: {
|
|
3840
4336
|
outcome: 'achieved = criteria met; abandoned = give up after stating the blocker honestly',
|
|
3841
4337
|
note: 'Conclusion note',
|
|
4338
|
+
lessons: 'Lessons (optional, checked only on achieved): what this run found that changes how to work next time in a similar setting, not a restatement of this run\'s conclusion. The evaluator checks each against the record; supported ones are written to clear/knowledge/lessons/, and later sessions see them when framing and planning',
|
|
4339
|
+
'lessons.items.text': 'One sentence on what to do next time ("check this rig\'s temperature against the reference reading first")',
|
|
4340
|
+
'lessons.items.kind': 'trap = a pitfall; check = a reading or assumption to check first; shortcut = a convenient method that misleads; prior = a prior about this kind of system (e.g. "two factors are often coupled; scan diagonally first")',
|
|
4341
|
+
'lessons.items.about': 'The equipment, quantities or concepts involved (short names, used to match next time)',
|
|
4342
|
+
'lessons.items.evidence': 'Where in this run\'s record it showed (step, file)',
|
|
4343
|
+
'lessons.items.boundary': 'When it does not apply',
|
|
3842
4344
|
},
|
|
3843
4345
|
},
|
|
3844
4346
|
CreatePlan: {
|
|
3845
4347
|
description:
|
|
3846
|
-
'Make a plan for a complex task. Each step says in one sentence what it does (do), what counts as proof (artifacts), and its criterion (done_criteria, written before the result exists). Steps that test judgments declare tests: {hypotheses, level}: which judgments, at what level (for a step that compares competing routes, list all the competing judgments). At most 25 steps. Once made, the plan is locked: on a local setback change the current step, do not start over.',
|
|
4348
|
+
'Make a plan for a complex task. Each step says in one sentence what it does (do), what counts as proof (artifacts), and its criterion (done_criteria, written before the result exists); it may carry an expectation (expect: what you expect to see and where that comes from). Steps that test judgments declare tests: {hypotheses, level}: which judgments, at what level (for a step that compares competing routes, list all the competing judgments). At most 25 steps. Once made, the plan is locked: on a local setback change the current step, do not start over.',
|
|
3847
4349
|
params: {
|
|
3848
4350
|
brief: `Plan description for people (Markdown, suggested ≥${MIN_BRIEF_CHARS} characters, at least two ## sections)`,
|
|
3849
4351
|
...prefixed('steps.items.', STEP_TEXT_EN),
|
|
@@ -3851,12 +4353,13 @@ export function apply(ctx, config = {}) {
|
|
|
3851
4353
|
},
|
|
3852
4354
|
RevisePlan: {
|
|
3853
4355
|
description:
|
|
3854
|
-
'Change the current plan without moving progress (returns progress_changed=false).
|
|
4356
|
+
'Change the current plan without moving progress (returns progress_changed=false). Four actions: `action="expect"` writes the expectation for a step not yet done (give `step_id` and `expect`: write it before acting, so a miss is visible); `action="add"` adds a step (add missing work; give `step`); `action="refine"` refines a step\'s criterion (give `step_id` and the new `done_criteria`; the old one stays in the log); `action="void"` voids a step with a reason (give `step_id` and `reason`: if a step should not exist, void it and say why. Voiding on the record is honest; fabricating evidence to finish is not. A delivered step cannot be voided).',
|
|
3855
4357
|
params: {
|
|
3856
|
-
action: 'add = add a step; refine = change the criterion; void = void with a reason',
|
|
4358
|
+
action: 'expect = write the expectation; add = add a step; refine = change the criterion; void = void with a reason',
|
|
4359
|
+
expect: 'action=expect: what you expect to see, and where it comes from (relation / lesson / judgment, or intuition)',
|
|
3857
4360
|
step: 'action=add: the new step',
|
|
3858
4361
|
...prefixed('step.', STEP_TEXT_EN),
|
|
3859
|
-
step_id: 'action=refine / void: which step',
|
|
4362
|
+
step_id: 'action=expect / refine / void: which step',
|
|
3860
4363
|
done_criteria: 'action=refine: the new criterion',
|
|
3861
4364
|
reason: 'Why (required for action=void)',
|
|
3862
4365
|
},
|
|
@@ -3867,18 +4370,33 @@ export function apply(ctx, config = {}) {
|
|
|
3867
4370
|
},
|
|
3868
4371
|
AdvancePlan: {
|
|
3869
4372
|
description:
|
|
3870
|
-
'Deliver a step (the only completion verb): hand in the observations. The system first runs intake: declared outputs exist, are non-empty and well-formed; intake only accepts or rejects, it does not judge. If the delivery holds, the step is complete, whether the judgments it tests were supported, refuted or left unclear; **all count as complete**, and the results are recorded separately as evidence. At
|
|
4373
|
+
'Deliver a step (the only completion verb): hand in the observations. The system first runs intake: declared outputs exist, are non-empty and well-formed; intake only accepts or rejects, it does not judge. If the delivery holds, the step is complete, whether the judgments it tests were supported, refuted or left unclear; **all count as complete**, and the results are recorded separately as evidence. Whatever does not match the expectation goes in anomalies. At L2 (self-judged) you give the basis (why the delivery holds, checkable) and results (one entry per judgment the step tests); at L3 and above both are judged by an independent evaluator the system dispatches, and results you write are refused. Without proof it is not complete; there is no manual marking.',
|
|
3871
4374
|
params: {
|
|
3872
4375
|
step_id: 'Which step to deliver (only the first unsettled step; this confirms against slips, it does not select)',
|
|
3873
4376
|
observations: 'Observations: the raw results of this step (path relative to the workspace plus one sentence)',
|
|
3874
|
-
|
|
4377
|
+
anomalies: 'Unexplained: where this step\'s result does not fit the expectation or the ontology, or a reading that makes no sense (one sentence each, saying what does not fit). They stay on the card until explained, ruled out with a reason, or handed to a person (with the Anomaly tool). Do not explain them away',
|
|
4378
|
+
'anomalies.items.what': 'What does not fit: what was expected, what was seen',
|
|
4379
|
+
'anomalies.items.anchor': 'Optional: on which entity or device (entity id or name)',
|
|
4380
|
+
basis: 'Why the delivery holds: which fact in which output (must be checkable). Written by you only at L2 (self-judged)',
|
|
3875
4381
|
results:
|
|
3876
|
-
'
|
|
4382
|
+
'L2 (self-judged) only: one entry per judgment this step tests, read against its refutation condition: not met is support, met is refute, and if this observation cannot tell, inconclusive. Refute and inconclusive do not stop the step from completing',
|
|
3877
4383
|
'results.items.hypothesis': 'Short name of the judgment (claim text also works)',
|
|
3878
4384
|
'results.items.basis': 'One sentence: what this observation shows against the refutation condition (defaults to the basis above)',
|
|
3879
4385
|
},
|
|
3880
4386
|
},
|
|
3881
4387
|
}
|
|
4388
|
+
TOOL_TEXT_EN.Anomaly = {
|
|
4389
|
+
description:
|
|
4390
|
+
'Unexplained items (anomalies): observations that do not fit the expectation or the ontology, readings that make no sense. `action="open"` records one (give `what`, optionally `anchor`); `action="resolve"` gives it a destination (give `id`, `outcome` and `reason`): explained = a judgment, relation or newly found cause explains it (say which in `by`); ruled_out = ruled out with a stated reason (for example, shown to be a transcription error); escalated = handed to a person. Do not rule things out to keep the card tidy: the evaluator checks the reasons.',
|
|
4391
|
+
params: {
|
|
4392
|
+
what: 'action=open: what does not fit: what was expected, what was seen',
|
|
4393
|
+
anchor: 'action=open, optional: on which entity or device',
|
|
4394
|
+
id: 'action=resolve: which one (the u-… id)',
|
|
4395
|
+
outcome: 'explained / ruled_out / escalated (to a person)',
|
|
4396
|
+
reason: 'action=resolve: on what grounds (the explanation, why it can be ruled out, what a person must decide)',
|
|
4397
|
+
by: 'action=resolve, optional: the judgment, relation or fact that explains it',
|
|
4398
|
+
},
|
|
4399
|
+
}
|
|
3882
4400
|
/** 按路径把英文说明写进一份参数 schema 的副本。 */
|
|
3883
4401
|
function englishTool(tool) {
|
|
3884
4402
|
const text = TOOL_TEXT_EN[tool.name]
|