agent-trellis 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +12 -1
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +87 -2
- package/dist/commands/evolve.d.ts +43 -0
- package/dist/commands/evolve.js +479 -0
- package/dist/commands/mcpGateway.d.ts +28 -0
- package/dist/commands/mcpGateway.js +40 -6
- package/dist/evolve/agentView.d.ts +32 -0
- package/dist/evolve/agentView.js +34 -0
- package/dist/evolve/apply.d.ts +71 -0
- package/dist/evolve/apply.js +162 -0
- package/dist/evolve/archive.d.ts +23 -0
- package/dist/evolve/archive.js +58 -0
- package/dist/evolve/erase.d.ts +17 -0
- package/dist/evolve/erase.js +65 -0
- package/dist/evolve/extension.d.ts +37 -0
- package/dist/evolve/extension.js +63 -0
- package/dist/evolve/gate.d.ts +95 -0
- package/dist/evolve/gate.js +295 -0
- package/dist/evolve/importHistory.d.ts +91 -0
- package/dist/evolve/importHistory.js +239 -0
- package/dist/evolve/jobMain.d.ts +22 -0
- package/dist/evolve/jobMain.js +52 -0
- package/dist/evolve/jobRun.d.ts +94 -0
- package/dist/evolve/jobRun.js +273 -0
- package/dist/evolve/jobShell.d.ts +46 -0
- package/dist/evolve/jobShell.js +75 -0
- package/dist/evolve/l2.d.ts +93 -0
- package/dist/evolve/l2.js +81 -0
- package/dist/evolve/lesson.d.ts +57 -0
- package/dist/evolve/lesson.js +97 -0
- package/dist/evolve/pending.d.ts +69 -0
- package/dist/evolve/pending.js +116 -0
- package/dist/evolve/probeWorkspace.d.ts +57 -0
- package/dist/evolve/probeWorkspace.js +69 -0
- package/dist/evolve/propose.d.ts +73 -0
- package/dist/evolve/propose.js +136 -0
- package/dist/evolve/recorder.d.ts +68 -0
- package/dist/evolve/recorder.js +169 -0
- package/dist/evolve/report.d.ts +26 -0
- package/dist/evolve/report.js +37 -0
- package/dist/evolve/reportView.d.ts +96 -0
- package/dist/evolve/reportView.js +311 -0
- package/dist/evolve/risk.d.ts +106 -0
- package/dist/evolve/risk.js +224 -0
- package/dist/evolve/sources.d.ts +19 -0
- package/dist/evolve/sources.js +29 -0
- package/dist/evolve/stateDir.d.ts +21 -0
- package/dist/evolve/stateDir.js +56 -0
- package/dist/evolve/surface.d.ts +34 -0
- package/dist/evolve/surface.js +72 -0
- package/dist/evolve/trajectory.d.ts +25 -0
- package/dist/evolve/trajectory.js +88 -0
- package/dist/evolve/treeToCanonical.d.ts +28 -0
- package/dist/evolve/treeToCanonical.js +79 -0
- package/dist/evolve/trellisSubstrate.d.ts +65 -0
- package/dist/evolve/trellisSubstrate.js +154 -0
- package/dist/evolve/trigger.d.ts +177 -0
- package/dist/evolve/trigger.js +400 -0
- package/dist/evolve/types.d.ts +126 -0
- package/dist/evolve/types.js +15 -0
- package/dist/evolve/validate.d.ts +139 -0
- package/dist/evolve/validate.js +315 -0
- package/dist/host/checks.d.ts +57 -0
- package/dist/host/checks.js +126 -0
- package/dist/host/delivery.d.ts +42 -0
- package/dist/host/delivery.js +59 -0
- package/dist/host/edits.d.ts +87 -0
- package/dist/host/edits.js +102 -0
- package/dist/host/hostEvents.d.ts +74 -0
- package/dist/host/hostEvents.js +45 -0
- package/dist/host/index.d.ts +22 -0
- package/dist/host/index.js +17 -0
- package/dist/host/levels.d.ts +106 -0
- package/dist/host/levels.js +206 -0
- package/dist/host/staging.d.ts +56 -0
- package/dist/host/staging.js +106 -0
- package/dist/host/surface.d.ts +36 -0
- package/dist/host/surface.js +20 -0
- package/dist/lib/backup.js +12 -3
- package/dist/lib/fsAtomic.d.ts +7 -0
- package/dist/lib/fsAtomic.js +14 -0
- package/dist/lib/hostEventBus.d.ts +47 -0
- package/dist/lib/hostEventBus.js +94 -0
- package/dist/lib/hostExtensions.d.ts +75 -0
- package/dist/lib/hostExtensions.js +196 -0
- package/dist/lib/mcpRuntime.d.ts +12 -1
- package/dist/lib/mcpRuntime.js +34 -2
- package/dist/lib/mcpToolRegistry.d.ts +18 -2
- package/dist/lib/mcpToolRegistry.js +29 -9
- package/dist/pi-bridge/bundle.js +16 -6
- package/docs/magpie-gateway-research.md +422 -0
- package/docs/roadmap.md +22 -12
- package/docs/runtime-infra.md +41 -22
- package/docs/self-evolution.md +138 -44
- package/package.json +7 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 匿名前后对比记录协议(trellis-evolve 任务 13.1)。
|
|
3
|
+
*
|
|
4
|
+
* spec"匿名前后对比记录协议(L2)":内容类对比的比较记录以类型化档案
|
|
5
|
+
* 格式持久化——baseline 与候选各自的运行标识、冻结的交付物引用、按
|
|
6
|
+
* 任务设种子的匿名标识映射、选择器结果与失败收口的 verdict;base 保留在
|
|
7
|
+
* 比较中;记录格式表达两种收口:选中 baseline ⟹ 不采纳,选择器失败 ⟹
|
|
8
|
+
* invalid。本模块只定义记录格式与边界;选择器执行(运行比较与裁决)不
|
|
9
|
+
* 在本期范围。
|
|
10
|
+
*
|
|
11
|
+
* 匿名性:呈现给选择器的匿名标识(anonId)由任务种子 + 运行标识派生,
|
|
12
|
+
* 本身不含角色词;选择器视图(`presentL2Arms`)只给 anonId 与冻结交付
|
|
13
|
+
* 物引用,不带 role/runId。角色映射只存在于档案(审计用)。
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { appendArchiveRecord, readArchiveRecords } from "./archive.js";
|
|
17
|
+
/** 运行档案 kind:一条 L2 比较记录。 */
|
|
18
|
+
export const L2_COMPARISON_ARCHIVE_KIND = "l2-comparison";
|
|
19
|
+
/**
|
|
20
|
+
* 匿名标识:按任务种子 + 运行标识派生的稳定短哈希。构造上不可能
|
|
21
|
+
* 含角色词——呈现面与档案里的值同源,泄露面为零。
|
|
22
|
+
*/
|
|
23
|
+
export function anonArmId(taskSeeds, runId) {
|
|
24
|
+
const seedKey = taskSeeds
|
|
25
|
+
.map((entry) => `${entry.task}=${entry.seed}`)
|
|
26
|
+
.join("&");
|
|
27
|
+
const digest = createHash("sha256").update(`${seedKey}#${runId}`).digest("hex");
|
|
28
|
+
return `arm-${digest.slice(0, 16)}`;
|
|
29
|
+
}
|
|
30
|
+
function verdictFor(arms, selector) {
|
|
31
|
+
if (selector.status === "failed") {
|
|
32
|
+
return { kind: "invalid", reason: selector.reason };
|
|
33
|
+
}
|
|
34
|
+
const picked = arms.find((arm) => arm.anonId === selector.anonId);
|
|
35
|
+
if (picked === undefined) {
|
|
36
|
+
return { kind: "invalid", reason: `selector picked unknown arm ${selector.anonId}` };
|
|
37
|
+
}
|
|
38
|
+
// 任一臂未交付:无 artifacts 的臂视为未交付(design:不采纳收口)。
|
|
39
|
+
if (arms.some((arm) => arm.artifacts.length === 0)) {
|
|
40
|
+
return { kind: "invalid", reason: "arm-undelivered" };
|
|
41
|
+
}
|
|
42
|
+
if (picked.role === "baseline") {
|
|
43
|
+
return { kind: "not-adopted-baseline", anonId: picked.anonId };
|
|
44
|
+
}
|
|
45
|
+
return { kind: "adopt-candidate", anonId: picked.anonId };
|
|
46
|
+
}
|
|
47
|
+
/** 由臂与选择器结果构建完整记录(派生匿名映射 + 失败收口 verdict)。 */
|
|
48
|
+
export function buildL2Comparison(input) {
|
|
49
|
+
const arms = input.arms.map((arm) => ({
|
|
50
|
+
anonId: anonArmId(input.taskSeeds, arm.runId),
|
|
51
|
+
runId: arm.runId,
|
|
52
|
+
role: arm.role,
|
|
53
|
+
artifacts: arm.artifacts,
|
|
54
|
+
}));
|
|
55
|
+
return {
|
|
56
|
+
comparisonId: input.comparisonId,
|
|
57
|
+
createdAt: input.createdAt,
|
|
58
|
+
taskSeeds: input.taskSeeds,
|
|
59
|
+
arms,
|
|
60
|
+
selector: input.selector,
|
|
61
|
+
verdict: verdictFor(arms, input.selector),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** 追加一条比较记录到运行档案。 */
|
|
65
|
+
export function appendL2Comparison(homeDir, record, now = () => new Date()) {
|
|
66
|
+
return appendArchiveRecord(homeDir, { kind: L2_COMPARISON_ARCHIVE_KIND, payload: record }, now);
|
|
67
|
+
}
|
|
68
|
+
/** 读回全部比较记录(档案读取,零副作用)。 */
|
|
69
|
+
export function readL2Comparisons(homeDir) {
|
|
70
|
+
const records = [];
|
|
71
|
+
for (const entry of readArchiveRecords(homeDir)) {
|
|
72
|
+
if (entry.kind !== L2_COMPARISON_ARCHIVE_KIND)
|
|
73
|
+
continue;
|
|
74
|
+
records.push(entry.payload);
|
|
75
|
+
}
|
|
76
|
+
return records;
|
|
77
|
+
}
|
|
78
|
+
/** 选择器视图:只给匿名标识与冻结交付物引用——不带 role/runId。 */
|
|
79
|
+
export function presentL2Arms(record) {
|
|
80
|
+
return record.arms.map((arm) => ({ anonId: arm.anonId, artifacts: arm.artifacts }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lesson 记录(trellis-evolve 任务 12.6)。
|
|
3
|
+
*
|
|
4
|
+
* 被采纳的编辑产出一条 lesson(cue / 正文 / 证据链接 / 可选 confidence /
|
|
5
|
+
* 层作用域,类型见 types.ts)。spec"lesson 记录与上下文呈现":
|
|
6
|
+
*
|
|
7
|
+
* - canonical 记忆已启用 → 经现有记忆管道写入(缺省内置写入器把
|
|
8
|
+
* lesson 落成 canonical `memories/lesson-<id>.md`;调用方可注入
|
|
9
|
+
* 自有管道);
|
|
10
|
+
* - 未启用 → 只留运行档案,运行不失败:返回 skipped 而非 failed;
|
|
11
|
+
* - 后续提案只呈现匹配上下文的 lesson:全局 lesson 处处可见,
|
|
12
|
+
* workspace lesson 只给该 workspace;对其他上下文仅作为升格审批
|
|
13
|
+
* 的跨项目佐证(单独出口,不混入提案上下文)。
|
|
14
|
+
*
|
|
15
|
+
* "记忆已启用"是调用方判定后的显式参数(共享 Memory MCP 的 on/off
|
|
16
|
+
* 属托管配置面,消费者不读 mcp 配置 —— 10.3 边界)。档案恒写:lesson
|
|
17
|
+
* 的审计源是运行档案,记忆管道是投递产物。
|
|
18
|
+
*/
|
|
19
|
+
import type { Lesson } from "./types.js";
|
|
20
|
+
/** 运行档案 kind:一条 lesson。payload 即 Lesson 的 JSON 形态。 */
|
|
21
|
+
export declare const LESSON_ARCHIVE_KIND = "lesson";
|
|
22
|
+
export type LessonMemoryStatus = "written" | "skipped" | "failed";
|
|
23
|
+
export interface RecordLessonOptions {
|
|
24
|
+
readonly homeDir: string;
|
|
25
|
+
readonly lesson: Lesson;
|
|
26
|
+
/** canonical 记忆是否已启用(调用方按托管配置判定)。 */
|
|
27
|
+
readonly memoryEnabled: boolean;
|
|
28
|
+
/** 记忆管道写入缝;缺省内置写入器落 `memories/lesson-<id>.md`。
|
|
29
|
+
* 抛错按 failed 收口(仍留档案),不拖垮运行。 */
|
|
30
|
+
readonly writeMemory?: (lesson: Lesson) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface RecordLessonResult {
|
|
33
|
+
readonly memoryStatus: LessonMemoryStatus;
|
|
34
|
+
/** 内置写入器落盘的路径(注入自有管道时为 undefined)。 */
|
|
35
|
+
readonly memoryPath?: string;
|
|
36
|
+
/** 写入失败时的错误信息(仅 failed)。 */
|
|
37
|
+
readonly error?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 记录一条 lesson:恒写运行档案;记忆已启用才走记忆管道(未启用
|
|
41
|
+
* skipped;管道抛错 failed 但运行不中断)。
|
|
42
|
+
*/
|
|
43
|
+
export declare function recordLesson(options: RecordLessonOptions): RecordLessonResult;
|
|
44
|
+
/** 全部 lesson(档案读取;档案是 lesson 的审计源)。 */
|
|
45
|
+
export declare function readAllLessons(homeDir: string): Lesson[];
|
|
46
|
+
/**
|
|
47
|
+
* 提案上下文用:只呈现匹配上下文的 lesson——全局处处可见;workspace
|
|
48
|
+
* lesson 只给同一 workspace root;其他 workspace 的 lesson 不进提案
|
|
49
|
+
* 上下文(spec 场景:仓库 X 的 lesson 不原样进入 Y)。
|
|
50
|
+
*/
|
|
51
|
+
export declare function lessonsForContext(homeDir: string, workspaceRoot?: string): Lesson[];
|
|
52
|
+
/**
|
|
53
|
+
* 升格审批的跨项目佐证:当前上下文之外的 workspace lesson(全局
|
|
54
|
+
* lesson 不在此列——它们本就处处可见)。仅供升格审批参考,绝不混入
|
|
55
|
+
* 提案生成上下文。
|
|
56
|
+
*/
|
|
57
|
+
export declare function lessonsAsCorroboration(homeDir: string, workspaceRoot?: string): Lesson[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lesson 记录(trellis-evolve 任务 12.6)。
|
|
3
|
+
*
|
|
4
|
+
* 被采纳的编辑产出一条 lesson(cue / 正文 / 证据链接 / 可选 confidence /
|
|
5
|
+
* 层作用域,类型见 types.ts)。spec"lesson 记录与上下文呈现":
|
|
6
|
+
*
|
|
7
|
+
* - canonical 记忆已启用 → 经现有记忆管道写入(缺省内置写入器把
|
|
8
|
+
* lesson 落成 canonical `memories/lesson-<id>.md`;调用方可注入
|
|
9
|
+
* 自有管道);
|
|
10
|
+
* - 未启用 → 只留运行档案,运行不失败:返回 skipped 而非 failed;
|
|
11
|
+
* - 后续提案只呈现匹配上下文的 lesson:全局 lesson 处处可见,
|
|
12
|
+
* workspace lesson 只给该 workspace;对其他上下文仅作为升格审批
|
|
13
|
+
* 的跨项目佐证(单独出口,不混入提案上下文)。
|
|
14
|
+
*
|
|
15
|
+
* "记忆已启用"是调用方判定后的显式参数(共享 Memory MCP 的 on/off
|
|
16
|
+
* 属托管配置面,消费者不读 mcp 配置 —— 10.3 边界)。档案恒写:lesson
|
|
17
|
+
* 的审计源是运行档案,记忆管道是投递产物。
|
|
18
|
+
*/
|
|
19
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { appendArchiveRecord, readArchiveRecords } from "./archive.js";
|
|
22
|
+
/** 运行档案 kind:一条 lesson。payload 即 Lesson 的 JSON 形态。 */
|
|
23
|
+
export const LESSON_ARCHIVE_KIND = "lesson";
|
|
24
|
+
function defaultWriteMemory(homeDir, lesson) {
|
|
25
|
+
const dir = join(homeDir, ".trellis", "memories");
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
// lesson.id 已带 `lesson-` 前缀(产出方约定),文件名直接用 id。
|
|
28
|
+
const path = join(dir, `${lesson.id}.md`);
|
|
29
|
+
const evidenceLines = lesson.evidence.map((link) => {
|
|
30
|
+
const ref = link.kind === "run" ? link.runId : link.kind === "doctor-finding" ? link.findingId : link.signalId;
|
|
31
|
+
return `- ${link.kind}:${ref}`;
|
|
32
|
+
});
|
|
33
|
+
const confidenceLine = lesson.confidence !== undefined ? `confidence: ${lesson.confidence}\n` : "";
|
|
34
|
+
const layerLine = lesson.layer.kind === "global"
|
|
35
|
+
? "layer: global"
|
|
36
|
+
: `layer: workspace(${lesson.layer.root})`;
|
|
37
|
+
writeFileSync(path, `# ${lesson.cue}\n\n${lesson.body}\n\n${layerLine}\n${confidenceLine}\nevidence:\n${evidenceLines.join("\n")}\n`, "utf-8");
|
|
38
|
+
return path;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 记录一条 lesson:恒写运行档案;记忆已启用才走记忆管道(未启用
|
|
42
|
+
* skipped;管道抛错 failed 但运行不中断)。
|
|
43
|
+
*/
|
|
44
|
+
export function recordLesson(options) {
|
|
45
|
+
const { homeDir, lesson, memoryEnabled } = options;
|
|
46
|
+
appendArchiveRecord(homeDir, { kind: LESSON_ARCHIVE_KIND, payload: lesson });
|
|
47
|
+
if (!memoryEnabled) {
|
|
48
|
+
return { memoryStatus: "skipped" };
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
if (options.writeMemory !== undefined) {
|
|
52
|
+
options.writeMemory(lesson);
|
|
53
|
+
return { memoryStatus: "written" };
|
|
54
|
+
}
|
|
55
|
+
const memoryPath = defaultWriteMemory(homeDir, lesson);
|
|
56
|
+
return { memoryStatus: "written", memoryPath };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return { memoryStatus: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function lessonLayerMatches(lesson, workspaceRoot) {
|
|
63
|
+
const layer = lesson.layer;
|
|
64
|
+
if (layer.kind === "global")
|
|
65
|
+
return true;
|
|
66
|
+
return workspaceRoot !== undefined && layer.root === workspaceRoot;
|
|
67
|
+
}
|
|
68
|
+
/** 全部 lesson(档案读取;档案是 lesson 的审计源)。 */
|
|
69
|
+
export function readAllLessons(homeDir) {
|
|
70
|
+
const lessons = [];
|
|
71
|
+
for (const entry of readArchiveRecords(homeDir)) {
|
|
72
|
+
if (entry.kind !== LESSON_ARCHIVE_KIND)
|
|
73
|
+
continue;
|
|
74
|
+
lessons.push(entry.payload);
|
|
75
|
+
}
|
|
76
|
+
return lessons;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 提案上下文用:只呈现匹配上下文的 lesson——全局处处可见;workspace
|
|
80
|
+
* lesson 只给同一 workspace root;其他 workspace 的 lesson 不进提案
|
|
81
|
+
* 上下文(spec 场景:仓库 X 的 lesson 不原样进入 Y)。
|
|
82
|
+
*/
|
|
83
|
+
export function lessonsForContext(homeDir, workspaceRoot) {
|
|
84
|
+
return readAllLessons(homeDir).filter((lesson) => lessonLayerMatches(lesson, workspaceRoot));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 升格审批的跨项目佐证:当前上下文之外的 workspace lesson(全局
|
|
88
|
+
* lesson 不在此列——它们本就处处可见)。仅供升格审批参考,绝不混入
|
|
89
|
+
* 提案生成上下文。
|
|
90
|
+
*/
|
|
91
|
+
export function lessonsAsCorroboration(homeDir, workspaceRoot) {
|
|
92
|
+
return readAllLessons(homeDir).filter((lesson) => {
|
|
93
|
+
if (lesson.layer.kind === "global")
|
|
94
|
+
return false;
|
|
95
|
+
return workspaceRoot === undefined || lesson.layer.root !== workspaceRoot;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pending 提案存储与状态机(trellis-evolve 任务 12.4)。
|
|
3
|
+
*
|
|
4
|
+
* 存储在状态目录 `~/.trellis/evolve/pending/`(10.4 的规划子目录):
|
|
5
|
+
* 一提案一 JSON 文件,状态 `pending → approved | rejected` 只经管理面
|
|
6
|
+
* 命令变更(批准仅限交互式终端,见 `src/commands/evolve.ts`)。agent
|
|
7
|
+
* 侧的只读视图在 `agentView.ts` —— 暴露面物理分离,批准/应用能力不在
|
|
8
|
+
* agent 可达模块里(spec"批准只走带外通道")。
|
|
9
|
+
*
|
|
10
|
+
* 决策写运行档案(`proposal-decision`),与提案/verdict 同一条只追加
|
|
11
|
+
* 审计链;存储文件本身是可变状态(提案状态迁移),不是档案。
|
|
12
|
+
*/
|
|
13
|
+
import type { Proposal, RiskClass } from "./types.js";
|
|
14
|
+
/** 提案状态:待批 → 已批准(待应用)/ 已拒绝。 */
|
|
15
|
+
export type ProposalStatus = "pending" | "approved" | "rejected";
|
|
16
|
+
/** 档案 kind:管理面批准/拒绝决策。 */
|
|
17
|
+
export declare const PROPOSAL_DECISION_ARCHIVE_KIND = "proposal-decision";
|
|
18
|
+
/** pending 存储中的一条提案记录(提案 + 分类结果 + 状态)。 */
|
|
19
|
+
export interface PendingRecord {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly status: ProposalStatus;
|
|
22
|
+
readonly riskClass: RiskClass;
|
|
23
|
+
/** 12.3 分类器命中的规则 id(审计用;不采信 proposal 自报)。 */
|
|
24
|
+
readonly riskRuleId: string;
|
|
25
|
+
readonly proposal: Proposal;
|
|
26
|
+
readonly createdAt: number;
|
|
27
|
+
/** 决策时间(毫秒);未决策为 null。 */
|
|
28
|
+
readonly decidedAt: number | null;
|
|
29
|
+
}
|
|
30
|
+
/** pending 存储目录(只在状态目录下落盘)。 */
|
|
31
|
+
export declare function pendingRecordsDir(homeDir: string): string;
|
|
32
|
+
/** 提案 id → 文件名(路径安全字符;保留可读前缀)。 */
|
|
33
|
+
export declare function safeProposalFileName(id: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* 入队一条提案(状态 pending)。分类结果由调用方经 12.3 分类器算出后
|
|
36
|
+
* 传入 —— 存储层不重算,只留审计痕迹。
|
|
37
|
+
*/
|
|
38
|
+
export declare function enqueueProposal(options: {
|
|
39
|
+
readonly homeDir: string;
|
|
40
|
+
readonly proposal: Proposal;
|
|
41
|
+
readonly riskClass: RiskClass;
|
|
42
|
+
readonly riskRuleId: string;
|
|
43
|
+
readonly now?: () => number;
|
|
44
|
+
}): PendingRecord;
|
|
45
|
+
/** 读回全部提案记录(按 id 排序;目录不存在返回 [],读取零副作用)。 */
|
|
46
|
+
export declare function readPendingRecords(homeDir: string): PendingRecord[];
|
|
47
|
+
/** 按状态过滤。 */
|
|
48
|
+
export declare function readProposalsByStatus(homeDir: string, status: ProposalStatus): PendingRecord[];
|
|
49
|
+
/** 单条读取;不存在返回 null。 */
|
|
50
|
+
export declare function readPendingRecord(homeDir: string, id: string): PendingRecord | null;
|
|
51
|
+
export type ProposalDecision = "approved" | "rejected";
|
|
52
|
+
export type DecideProposalResult = {
|
|
53
|
+
readonly ok: true;
|
|
54
|
+
readonly record: PendingRecord;
|
|
55
|
+
} | {
|
|
56
|
+
readonly ok: false;
|
|
57
|
+
readonly error: "not-found" | "already-decided";
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* 管理面决策(仅 `src/commands/evolve.ts` 的交互式批准路径调用;agent
|
|
61
|
+
* 可达面没有本函数)。写状态迁移 + 档案 `proposal-decision`;重复决策
|
|
62
|
+
* 拒绝(状态机单向)。
|
|
63
|
+
*/
|
|
64
|
+
export declare function decideProposal(options: {
|
|
65
|
+
readonly homeDir: string;
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly decision: ProposalDecision;
|
|
68
|
+
readonly now?: () => number;
|
|
69
|
+
}): DecideProposalResult;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pending 提案存储与状态机(trellis-evolve 任务 12.4)。
|
|
3
|
+
*
|
|
4
|
+
* 存储在状态目录 `~/.trellis/evolve/pending/`(10.4 的规划子目录):
|
|
5
|
+
* 一提案一 JSON 文件,状态 `pending → approved | rejected` 只经管理面
|
|
6
|
+
* 命令变更(批准仅限交互式终端,见 `src/commands/evolve.ts`)。agent
|
|
7
|
+
* 侧的只读视图在 `agentView.ts` —— 暴露面物理分离,批准/应用能力不在
|
|
8
|
+
* agent 可达模块里(spec"批准只走带外通道")。
|
|
9
|
+
*
|
|
10
|
+
* 决策写运行档案(`proposal-decision`),与提案/verdict 同一条只追加
|
|
11
|
+
* 审计链;存储文件本身是可变状态(提案状态迁移),不是档案。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { appendArchiveRecord } from "./archive.js";
|
|
16
|
+
import { evolveStateDir } from "./stateDir.js";
|
|
17
|
+
/**
|
|
18
|
+
* 原子写 pending 记录(tmp + rename,新建 0600):pending 是可变磁盘
|
|
19
|
+
* 状态,半写的 JSON 会让 `readPendingRecords` 整体抛错(status/pending/
|
|
20
|
+
* 作业全受影响)。本目录只依赖 host 门面与 node 内置(边界 10.3),
|
|
21
|
+
* 助手就地实现,不引 src/lib。
|
|
22
|
+
*/
|
|
23
|
+
function writeRecordAtomic(path, record) {
|
|
24
|
+
const temporary = join(dirname(path), `.atomic-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`);
|
|
25
|
+
writeFileSync(temporary, JSON.stringify(record, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
26
|
+
renameSync(temporary, path);
|
|
27
|
+
}
|
|
28
|
+
/** 档案 kind:管理面批准/拒绝决策。 */
|
|
29
|
+
export const PROPOSAL_DECISION_ARCHIVE_KIND = "proposal-decision";
|
|
30
|
+
/** pending 存储目录(只在状态目录下落盘)。 */
|
|
31
|
+
export function pendingRecordsDir(homeDir) {
|
|
32
|
+
return join(evolveStateDir(homeDir), "pending");
|
|
33
|
+
}
|
|
34
|
+
/** 提案 id → 文件名(路径安全字符;保留可读前缀)。 */
|
|
35
|
+
export function safeProposalFileName(id) {
|
|
36
|
+
const safe = id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
37
|
+
return safe === "" || safe === "." || safe === ".." ? "proposal" : safe;
|
|
38
|
+
}
|
|
39
|
+
function pendingFilePath(homeDir, id) {
|
|
40
|
+
return join(pendingRecordsDir(homeDir), `${safeProposalFileName(id)}.json`);
|
|
41
|
+
}
|
|
42
|
+
function readRecordFile(path) {
|
|
43
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
44
|
+
if (typeof parsed.id !== "string" || typeof parsed.status !== "string") {
|
|
45
|
+
throw new Error(`pending: ${path} 形态不符(缺 id/status)`);
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 入队一条提案(状态 pending)。分类结果由调用方经 12.3 分类器算出后
|
|
51
|
+
* 传入 —— 存储层不重算,只留审计痕迹。
|
|
52
|
+
*/
|
|
53
|
+
export function enqueueProposal(options) {
|
|
54
|
+
const now = (options.now ?? Date.now)();
|
|
55
|
+
const record = {
|
|
56
|
+
id: options.proposal.id,
|
|
57
|
+
status: "pending",
|
|
58
|
+
riskClass: options.riskClass,
|
|
59
|
+
riskRuleId: options.riskRuleId,
|
|
60
|
+
proposal: options.proposal,
|
|
61
|
+
createdAt: now,
|
|
62
|
+
decidedAt: null,
|
|
63
|
+
};
|
|
64
|
+
mkdirSync(pendingRecordsDir(options.homeDir), { recursive: true, mode: 0o700 });
|
|
65
|
+
writeRecordAtomic(pendingFilePath(options.homeDir, record.id), record);
|
|
66
|
+
return record;
|
|
67
|
+
}
|
|
68
|
+
/** 读回全部提案记录(按 id 排序;目录不存在返回 [],读取零副作用)。 */
|
|
69
|
+
export function readPendingRecords(homeDir) {
|
|
70
|
+
const dir = pendingRecordsDir(homeDir);
|
|
71
|
+
if (!existsSync(dir))
|
|
72
|
+
return [];
|
|
73
|
+
const records = [];
|
|
74
|
+
for (const name of readdirSync(dir).sort()) {
|
|
75
|
+
if (!name.endsWith(".json"))
|
|
76
|
+
continue;
|
|
77
|
+
records.push(readRecordFile(join(dir, name)));
|
|
78
|
+
}
|
|
79
|
+
return records.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
80
|
+
}
|
|
81
|
+
/** 按状态过滤。 */
|
|
82
|
+
export function readProposalsByStatus(homeDir, status) {
|
|
83
|
+
return readPendingRecords(homeDir).filter((record) => record.status === status);
|
|
84
|
+
}
|
|
85
|
+
/** 单条读取;不存在返回 null。 */
|
|
86
|
+
export function readPendingRecord(homeDir, id) {
|
|
87
|
+
const path = pendingFilePath(homeDir, id);
|
|
88
|
+
if (!existsSync(path))
|
|
89
|
+
return null;
|
|
90
|
+
return readRecordFile(path);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 管理面决策(仅 `src/commands/evolve.ts` 的交互式批准路径调用;agent
|
|
94
|
+
* 可达面没有本函数)。写状态迁移 + 档案 `proposal-decision`;重复决策
|
|
95
|
+
* 拒绝(状态机单向)。
|
|
96
|
+
*/
|
|
97
|
+
export function decideProposal(options) {
|
|
98
|
+
const existing = readPendingRecord(options.homeDir, options.id);
|
|
99
|
+
if (existing === null)
|
|
100
|
+
return { ok: false, error: "not-found" };
|
|
101
|
+
if (existing.status !== "pending")
|
|
102
|
+
return { ok: false, error: "already-decided" };
|
|
103
|
+
const now = (options.now ?? Date.now)();
|
|
104
|
+
const record = { ...existing, status: options.decision, decidedAt: now };
|
|
105
|
+
writeRecordAtomic(pendingFilePath(options.homeDir, record.id), record);
|
|
106
|
+
appendArchiveRecord(options.homeDir, {
|
|
107
|
+
kind: PROPOSAL_DECISION_ARCHIVE_KIND,
|
|
108
|
+
payload: {
|
|
109
|
+
proposalId: record.id,
|
|
110
|
+
decision: options.decision,
|
|
111
|
+
riskClass: record.riskClass,
|
|
112
|
+
riskRuleId: record.riskRuleId,
|
|
113
|
+
},
|
|
114
|
+
}, () => new Date(now));
|
|
115
|
+
return { ok: true, record };
|
|
116
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* probe 工作区隔离 helper(trellis-evolve 任务 13.3)。
|
|
3
|
+
*
|
|
4
|
+
* 探针会话必须跑在隔离环境里:mkdtemp 工作区(探针自己写文件)、staging
|
|
5
|
+
* 渲染配置(agent 配置面经 host 门面 `createStaging` 渲染进 scratch home,
|
|
6
|
+
* 真实 home 与 agent 原生配置逐字节不变)、重定向 agent 配置 HOME(spawn
|
|
7
|
+
* env 的 HOME/XDG 指向 scratch home,探针不读用户真实配置)。probe 启动
|
|
8
|
+
* 规格经 `assertNoBypassFlags` 断言不含任何审批/沙盒绕过标志(与 7.2 引擎
|
|
9
|
+
* 侧 spawn 规格同一禁令:D12 不使用 bypass 标志)。
|
|
10
|
+
*
|
|
11
|
+
* 本模块只交付隔离与断言;选择器执行与探针任务集不在本期范围。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 审批/沙盒绕过标志词面:命中即拒绝。与引擎侧 7.2 断言
|
|
15
|
+
* (proposer spawn 规格)同一禁令——harness2 的
|
|
16
|
+
* `--dangerously-bypass-approvals-and-sandbox` 不采纳。
|
|
17
|
+
*/
|
|
18
|
+
export declare const PROBE_FORBIDDEN_FLAG: RegExp;
|
|
19
|
+
/** probe 启动规格:argv + 重定向 agent 配置 HOME 的 env。 */
|
|
20
|
+
export interface ProbeSpawnSpec {
|
|
21
|
+
readonly argv: readonly string[];
|
|
22
|
+
readonly env: Readonly<Record<string, string>>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 断言 argv 不含任何审批/沙盒绕过标志(含则抛错并点名)。
|
|
26
|
+
* 命令参数以 argv 数组传递,永不拼 shell 字符串——断言也因此只看词面。
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertNoBypassFlags(argv: readonly string[]): void;
|
|
29
|
+
/** 重定向 agent 配置 HOME 的 spawn env(配置面只从隔离 home 读)。 */
|
|
30
|
+
export declare function probeEnv(homeDir: string): Record<string, string>;
|
|
31
|
+
/** 构造 probe 启动规格:先断言无绕过标志,再绑定重定向 env。 */
|
|
32
|
+
export declare function probeSpawnSpec(argv: readonly string[], homeDir: string): ProbeSpawnSpec;
|
|
33
|
+
export interface CreateProbeWorkspaceOptions {
|
|
34
|
+
/** 源(真实)home——只读,配置面渲染进隔离;绝不写回。 */
|
|
35
|
+
homeDir: string;
|
|
36
|
+
/** 可选:把真实 workspace 树也渲染进隔离(默认不复制,探针工作区空白)。 */
|
|
37
|
+
workspaceRoot?: string | null;
|
|
38
|
+
/** 隔离树的落点;缺省 OS 临时目录。 */
|
|
39
|
+
tmpRoot?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ProbeWorkspace {
|
|
42
|
+
/** 全部隔离内容的根(mkdtemp);dispose() 整树删除,可重复调用。 */
|
|
43
|
+
readonly root: string;
|
|
44
|
+
/** 重定向后的 agent 配置 HOME(staging 渲染配置的 scratch home)。 */
|
|
45
|
+
readonly homeDir: string;
|
|
46
|
+
/** 探针会话的工作区(空白 mkdtemp 目录)。 */
|
|
47
|
+
readonly workspaceDir: string;
|
|
48
|
+
/** probe 启动 env(HOME/XDG 重定向到隔离 home)。 */
|
|
49
|
+
readonly env: Readonly<Record<string, string>>;
|
|
50
|
+
dispose(): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 三件套一次到位:mkdtemp 工作区 + staging 渲染配置 + 重定向 HOME env。
|
|
54
|
+
* staging 复制面被 NATIVE_CHECK_SURFACE 约束(凭据形态永不进隔离树,
|
|
55
|
+
* 与 morphix-env 边界一致)。
|
|
56
|
+
*/
|
|
57
|
+
export declare function createProbeWorkspace(options: CreateProbeWorkspaceOptions): ProbeWorkspace;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* probe 工作区隔离 helper(trellis-evolve 任务 13.3)。
|
|
3
|
+
*
|
|
4
|
+
* 探针会话必须跑在隔离环境里:mkdtemp 工作区(探针自己写文件)、staging
|
|
5
|
+
* 渲染配置(agent 配置面经 host 门面 `createStaging` 渲染进 scratch home,
|
|
6
|
+
* 真实 home 与 agent 原生配置逐字节不变)、重定向 agent 配置 HOME(spawn
|
|
7
|
+
* env 的 HOME/XDG 指向 scratch home,探针不读用户真实配置)。probe 启动
|
|
8
|
+
* 规格经 `assertNoBypassFlags` 断言不含任何审批/沙盒绕过标志(与 7.2 引擎
|
|
9
|
+
* 侧 spawn 规格同一禁令:D12 不使用 bypass 标志)。
|
|
10
|
+
*
|
|
11
|
+
* 本模块只交付隔离与断言;选择器执行与探针任务集不在本期范围。
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createStaging } from "../host/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* 审批/沙盒绕过标志词面:命中即拒绝。与引擎侧 7.2 断言
|
|
18
|
+
* (proposer spawn 规格)同一禁令——harness2 的
|
|
19
|
+
* `--dangerously-bypass-approvals-and-sandbox` 不采纳。
|
|
20
|
+
*/
|
|
21
|
+
export const PROBE_FORBIDDEN_FLAG = /dangerous|bypass|skip-permissions|yolo/i;
|
|
22
|
+
/**
|
|
23
|
+
* 断言 argv 不含任何审批/沙盒绕过标志(含则抛错并点名)。
|
|
24
|
+
* 命令参数以 argv 数组传递,永不拼 shell 字符串——断言也因此只看词面。
|
|
25
|
+
*/
|
|
26
|
+
export function assertNoBypassFlags(argv) {
|
|
27
|
+
for (const arg of argv) {
|
|
28
|
+
if (PROBE_FORBIDDEN_FLAG.test(arg)) {
|
|
29
|
+
throw new Error(`probe 规格含审批/沙盒绕过标志: ${arg}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** 重定向 agent 配置 HOME 的 spawn env(配置面只从隔离 home 读)。 */
|
|
34
|
+
export function probeEnv(homeDir) {
|
|
35
|
+
return {
|
|
36
|
+
HOME: homeDir,
|
|
37
|
+
XDG_CONFIG_HOME: join(homeDir, ".config"),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** 构造 probe 启动规格:先断言无绕过标志,再绑定重定向 env。 */
|
|
41
|
+
export function probeSpawnSpec(argv, homeDir) {
|
|
42
|
+
assertNoBypassFlags(argv);
|
|
43
|
+
return { argv, env: probeEnv(homeDir) };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 三件套一次到位:mkdtemp 工作区 + staging 渲染配置 + 重定向 HOME env。
|
|
47
|
+
* staging 复制面被 NATIVE_CHECK_SURFACE 约束(凭据形态永不进隔离树,
|
|
48
|
+
* 与 morphix-env 边界一致)。
|
|
49
|
+
*/
|
|
50
|
+
export function createProbeWorkspace(options) {
|
|
51
|
+
const staging = createStaging({
|
|
52
|
+
homeDir: options.homeDir,
|
|
53
|
+
workspaceRoot: options.workspaceRoot ?? null,
|
|
54
|
+
tmpRoot: options.tmpRoot,
|
|
55
|
+
});
|
|
56
|
+
const workspaceDir = mkdtempSync(join(staging.root, "probe-workspace-"));
|
|
57
|
+
mkdirSync(join(workspaceDir, ".config"), { recursive: true });
|
|
58
|
+
return {
|
|
59
|
+
root: staging.root,
|
|
60
|
+
homeDir: staging.homeDir,
|
|
61
|
+
workspaceDir,
|
|
62
|
+
env: probeEnv(staging.homeDir),
|
|
63
|
+
dispose() {
|
|
64
|
+
// staging.dispose 删整树(workspaceDir 在其内),安全幂等。
|
|
65
|
+
staging.dispose();
|
|
66
|
+
rmSync(staging.root, { recursive: true, force: true });
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 提案通道选择与 rule-based fallback(trellis-evolve 任务 12.2)。
|
|
3
|
+
*
|
|
4
|
+
* 正常通道是 editor(agent-bridge `runDelegatedCall` 委派或 spawn 本机
|
|
5
|
+
* CLI);委派/spawn 双不可用时,doctor finding 形态的信号走 rule-based
|
|
6
|
+
* 提案 —— 对有显而易见机械修复的 finding kind 直接构造提案,不调模型。
|
|
7
|
+
* 本期唯一定义的机械修复:`workspace-toml-invalid` 把解析失败行注释掉
|
|
8
|
+
* (TOML 有注释语法且 doctor 消息带行号;JSON 无注释语法、无行号的
|
|
9
|
+
* 情形返回 null,不硬造)。
|
|
10
|
+
*
|
|
11
|
+
* rule-based 产出的提案是草稿:仍要过 12.2 机械校验与 12.1 的 L1 前闸,
|
|
12
|
+
* 是否真的修好由 verdict 差集说话,不在这里担保。`riskClass` 只是占位
|
|
13
|
+
* ("B"),12.3 按 kind+target 重算,不采信自报。
|
|
14
|
+
*/
|
|
15
|
+
import type { CheckEntry } from "../host/index.js";
|
|
16
|
+
import type { Proposal, Signal } from "./types.js";
|
|
17
|
+
/** 提案通道可用性:委派(agent-bridge)与 spawn(本机 CLI)各一票。 */
|
|
18
|
+
export interface ProposalChannelAvailability {
|
|
19
|
+
readonly delegate: boolean;
|
|
20
|
+
readonly spawn: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 通道选择:任一通道可用走 editor;双不可用才降级 rule-based。
|
|
24
|
+
* rule-based 不是"更好的编辑",是无模型可用时对机械可修 finding 的兜底。
|
|
25
|
+
*/
|
|
26
|
+
export declare function selectProposalChannel(availability: ProposalChannelAvailability): "editor" | "rule-based";
|
|
27
|
+
/** doctor finding 形态信号:stableId 以 `doctor:` 开头(= CheckEntry.id)。 */
|
|
28
|
+
export declare function isDoctorFindingSignal(signal: Pick<Signal, "stableId">): boolean;
|
|
29
|
+
export interface TomlFindingLocator {
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly line?: number;
|
|
32
|
+
readonly column?: number;
|
|
33
|
+
readonly reason: string;
|
|
34
|
+
}
|
|
35
|
+
/** 解析 workspace-toml-invalid 的消息 → 路径/行/列/原因;形态不符返回 null。 */
|
|
36
|
+
export declare function parseTomlFindingMessage(message: string): TomlFindingLocator | null;
|
|
37
|
+
export interface RuleBasedOptions {
|
|
38
|
+
/** 缺省 `rule-<findingId>`;调用方要确定性 id 时注入。 */
|
|
39
|
+
readonly id?: string;
|
|
40
|
+
/** 时间源(毫秒);缺省 Date.now。 */
|
|
41
|
+
readonly now?: () => number;
|
|
42
|
+
/** 读原文注入缝(测试可免写盘);缺省读真实文件。 */
|
|
43
|
+
readonly readFile?: (path: string) => string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 对单条检查条目构造 rule-based 提案;没有显而易见机械修复时返回 null。
|
|
47
|
+
* `workspace-toml-invalid`:失败行行首加 `# ` 注释(行已注释/空行时没
|
|
48
|
+
* 有可注释的目标,返回 null 不硬造)。
|
|
49
|
+
*/
|
|
50
|
+
export declare function ruleBasedProposal(entry: CheckEntry, options?: RuleBasedOptions): Proposal | null;
|
|
51
|
+
export interface RuleBasedProposeOptions {
|
|
52
|
+
readonly signals: readonly Signal[];
|
|
53
|
+
readonly findings: readonly CheckEntry[];
|
|
54
|
+
readonly availability: ProposalChannelAvailability;
|
|
55
|
+
readonly now?: () => number;
|
|
56
|
+
readonly readFile?: (path: string) => string;
|
|
57
|
+
}
|
|
58
|
+
export interface RuleBasedProposeResult {
|
|
59
|
+
readonly channel: "editor" | "rule-based";
|
|
60
|
+
/** 产出的草稿提案(仍待机械校验与 L1 前闸)。 */
|
|
61
|
+
readonly proposals: readonly Proposal[];
|
|
62
|
+
/** 无法机械修复的 doctor finding(带原因,便于档案/日志)。 */
|
|
63
|
+
readonly skipped: readonly {
|
|
64
|
+
readonly findingId: string;
|
|
65
|
+
readonly reason: string;
|
|
66
|
+
}[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 信号 → rule-based 提案:只在 rule-based 通道消费 doctor finding 形态
|
|
70
|
+
* 信号(按 stableId 对齐 findings);editor 通道由 editor 产出提案,本
|
|
71
|
+
* 函数不代劳(channel 透传,proposals 为空)。
|
|
72
|
+
*/
|
|
73
|
+
export declare function proposeFromSignals(options: RuleBasedProposeOptions): RuleBasedProposeResult;
|