agent-trellis 0.19.0 → 0.21.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.
Files changed (127) hide show
  1. package/dist/adapters/claude-code.js +2 -2
  2. package/dist/adapters/codex.js +3 -3
  3. package/dist/adapters/jsonMcp.d.ts +1 -1
  4. package/dist/adapters/jsonMcp.js +2 -2
  5. package/dist/adapters/kimi-code.js +2 -2
  6. package/dist/adapters/kiro.js +11 -9
  7. package/dist/adapters/mcpPlan.d.ts +1 -1
  8. package/dist/adapters/mcpPlan.js +35 -12
  9. package/dist/adapters/pi.js +5 -5
  10. package/dist/adapters/zcode.d.ts +1 -1
  11. package/dist/adapters/zcode.js +6 -6
  12. package/dist/cli.js +12 -1
  13. package/dist/commands/doctor.d.ts +9 -1
  14. package/dist/commands/doctor.js +92 -7
  15. package/dist/commands/evolve.d.ts +43 -0
  16. package/dist/commands/evolve.js +479 -0
  17. package/dist/commands/mcp.js +1 -1
  18. package/dist/commands/mcpAuth.js +1 -1
  19. package/dist/commands/mcpGateway.d.ts +30 -2
  20. package/dist/commands/mcpGateway.js +44 -10
  21. package/dist/commands/mcpImport.js +2 -1
  22. package/dist/commands/secretsAudit.d.ts +1 -1
  23. package/dist/commands/secretsAudit.js +36 -13
  24. package/dist/evolve/agentView.d.ts +32 -0
  25. package/dist/evolve/agentView.js +34 -0
  26. package/dist/evolve/apply.d.ts +71 -0
  27. package/dist/evolve/apply.js +162 -0
  28. package/dist/evolve/archive.d.ts +23 -0
  29. package/dist/evolve/archive.js +58 -0
  30. package/dist/evolve/erase.d.ts +17 -0
  31. package/dist/evolve/erase.js +65 -0
  32. package/dist/evolve/extension.d.ts +37 -0
  33. package/dist/evolve/extension.js +63 -0
  34. package/dist/evolve/gate.d.ts +95 -0
  35. package/dist/evolve/gate.js +295 -0
  36. package/dist/evolve/importHistory.d.ts +91 -0
  37. package/dist/evolve/importHistory.js +239 -0
  38. package/dist/evolve/jobMain.d.ts +22 -0
  39. package/dist/evolve/jobMain.js +52 -0
  40. package/dist/evolve/jobRun.d.ts +94 -0
  41. package/dist/evolve/jobRun.js +273 -0
  42. package/dist/evolve/jobShell.d.ts +46 -0
  43. package/dist/evolve/jobShell.js +75 -0
  44. package/dist/evolve/l2.d.ts +93 -0
  45. package/dist/evolve/l2.js +81 -0
  46. package/dist/evolve/lesson.d.ts +57 -0
  47. package/dist/evolve/lesson.js +97 -0
  48. package/dist/evolve/pending.d.ts +69 -0
  49. package/dist/evolve/pending.js +116 -0
  50. package/dist/evolve/probeWorkspace.d.ts +57 -0
  51. package/dist/evolve/probeWorkspace.js +69 -0
  52. package/dist/evolve/propose.d.ts +73 -0
  53. package/dist/evolve/propose.js +136 -0
  54. package/dist/evolve/recorder.d.ts +68 -0
  55. package/dist/evolve/recorder.js +169 -0
  56. package/dist/evolve/report.d.ts +26 -0
  57. package/dist/evolve/report.js +37 -0
  58. package/dist/evolve/reportView.d.ts +96 -0
  59. package/dist/evolve/reportView.js +311 -0
  60. package/dist/evolve/risk.d.ts +106 -0
  61. package/dist/evolve/risk.js +224 -0
  62. package/dist/evolve/sources.d.ts +19 -0
  63. package/dist/evolve/sources.js +29 -0
  64. package/dist/evolve/stateDir.d.ts +21 -0
  65. package/dist/evolve/stateDir.js +56 -0
  66. package/dist/evolve/surface.d.ts +34 -0
  67. package/dist/evolve/surface.js +72 -0
  68. package/dist/evolve/trajectory.d.ts +25 -0
  69. package/dist/evolve/trajectory.js +88 -0
  70. package/dist/evolve/treeToCanonical.d.ts +28 -0
  71. package/dist/evolve/treeToCanonical.js +79 -0
  72. package/dist/evolve/trellisSubstrate.d.ts +65 -0
  73. package/dist/evolve/trellisSubstrate.js +154 -0
  74. package/dist/evolve/trigger.d.ts +177 -0
  75. package/dist/evolve/trigger.js +400 -0
  76. package/dist/evolve/types.d.ts +126 -0
  77. package/dist/evolve/types.js +15 -0
  78. package/dist/evolve/validate.d.ts +139 -0
  79. package/dist/evolve/validate.js +315 -0
  80. package/dist/host/checks.d.ts +57 -0
  81. package/dist/host/checks.js +126 -0
  82. package/dist/host/delivery.d.ts +42 -0
  83. package/dist/host/delivery.js +59 -0
  84. package/dist/host/edits.d.ts +87 -0
  85. package/dist/host/edits.js +102 -0
  86. package/dist/host/hostEvents.d.ts +74 -0
  87. package/dist/host/hostEvents.js +45 -0
  88. package/dist/host/index.d.ts +22 -0
  89. package/dist/host/index.js +17 -0
  90. package/dist/host/levels.d.ts +106 -0
  91. package/dist/host/levels.js +206 -0
  92. package/dist/host/staging.d.ts +56 -0
  93. package/dist/host/staging.js +106 -0
  94. package/dist/host/surface.d.ts +36 -0
  95. package/dist/host/surface.js +20 -0
  96. package/dist/lib/backup.js +12 -3
  97. package/dist/lib/envVarNames.js +7 -0
  98. package/dist/lib/fsAtomic.d.ts +7 -0
  99. package/dist/lib/fsAtomic.js +14 -0
  100. package/dist/lib/hostEventBus.d.ts +47 -0
  101. package/dist/lib/hostEventBus.js +94 -0
  102. package/dist/lib/hostExtensions.d.ts +75 -0
  103. package/dist/lib/hostExtensions.js +196 -0
  104. package/dist/lib/mcpConnect.d.ts +1 -1
  105. package/dist/lib/mcpConnect.js +19 -8
  106. package/dist/lib/mcpMigrateRead.js +14 -2
  107. package/dist/lib/mcpRuntime.d.ts +12 -1
  108. package/dist/lib/mcpRuntime.js +34 -2
  109. package/dist/lib/mcpToolRegistry.d.ts +18 -2
  110. package/dist/lib/mcpToolRegistry.js +29 -9
  111. package/dist/lib/secretEnv.d.ts +53 -8
  112. package/dist/lib/secretEnv.js +78 -15
  113. package/dist/lib/tomlSection.js +3 -2
  114. package/dist/lib/workspaceDoctor.d.ts +1 -1
  115. package/dist/lib/workspaceDoctor.js +8 -4
  116. package/dist/lib/workspaceMcp.d.ts +1 -1
  117. package/dist/lib/workspaceMcp.js +5 -5
  118. package/dist/pi-bridge/bundle.js +42236 -2701
  119. package/dist/pi-bridge/index.js +2 -2
  120. package/docs/magpie-gateway-research.md +422 -0
  121. package/docs/research.md +4 -1
  122. package/docs/roadmap.md +22 -12
  123. package/docs/runtime-infra.md +41 -22
  124. package/docs/self-evolution.md +138 -44
  125. package/package.json +8 -2
  126. package/schema/secrets.policy.example.yaml +5 -0
  127. package/schema/servers.example.yaml +5 -1
@@ -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;
@@ -0,0 +1,136 @@
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 { readFileSync } from "node:fs";
16
+ import { basename } from "node:path";
17
+ /**
18
+ * 通道选择:任一通道可用走 editor;双不可用才降级 rule-based。
19
+ * rule-based 不是"更好的编辑",是无模型可用时对机械可修 finding 的兜底。
20
+ */
21
+ export function selectProposalChannel(availability) {
22
+ return availability.delegate || availability.spawn ? "editor" : "rule-based";
23
+ }
24
+ /** doctor finding 形态信号:stableId 以 `doctor:` 开头(= CheckEntry.id)。 */
25
+ export function isDoctorFindingSignal(signal) {
26
+ return signal.stableId !== null && signal.stableId.startsWith("doctor:");
27
+ }
28
+ /** doctor 的 workspace-toml 消息分隔符(U+2014 EM DASH),与
29
+ * `src/commands/doctor.ts` 的拼装逐字对齐。 */
30
+ const MESSAGE_SEP = " — ";
31
+ const FIX_BY_HAND_SUFFIX = " (not rendered by Trellis; fix by hand)";
32
+ /** `path[:line[:col]]` —— 行/列只认右侧的 `:数字` 形态。 */
33
+ const LOCATOR = /^(.*?)(?::(\d+)(?::(\d+))?)?$/;
34
+ /** 解析 workspace-toml-invalid 的消息 → 路径/行/列/原因;形态不符返回 null。 */
35
+ export function parseTomlFindingMessage(message) {
36
+ const sepAt = message.indexOf(MESSAGE_SEP);
37
+ if (sepAt < 0)
38
+ return null;
39
+ const loc = message.slice(0, sepAt);
40
+ const reason = message.slice(sepAt + MESSAGE_SEP.length).replace(FIX_BY_HAND_SUFFIX, "");
41
+ const matched = LOCATOR.exec(loc);
42
+ if (matched === null || matched[1] === undefined || matched[1] === "")
43
+ return null;
44
+ return {
45
+ path: matched[1],
46
+ ...(matched[2] !== undefined ? { line: Number(matched[2]) } : {}),
47
+ ...(matched[3] !== undefined ? { column: Number(matched[3]) } : {}),
48
+ reason,
49
+ };
50
+ }
51
+ /**
52
+ * 对单条检查条目构造 rule-based 提案;没有显而易见机械修复时返回 null。
53
+ * `workspace-toml-invalid`:失败行行首加 `# ` 注释(行已注释/空行时没
54
+ * 有可注释的目标,返回 null 不硬造)。
55
+ */
56
+ export function ruleBasedProposal(entry, options = {}) {
57
+ if (entry.kind !== "workspace-toml-invalid")
58
+ return null;
59
+ if (entry.layer.kind !== "workspace")
60
+ return null;
61
+ const parsed = parseTomlFindingMessage(entry.message);
62
+ if (parsed === null || parsed.line === undefined)
63
+ return null;
64
+ if (!parsed.path.endsWith(".toml"))
65
+ return null;
66
+ const read = options.readFile ?? ((path) => readFileSync(path, "utf-8"));
67
+ let content;
68
+ try {
69
+ content = read(parsed.path);
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ const lines = content.split("\n");
75
+ const index = parsed.line - 1;
76
+ if (parsed.line < 1 || index >= lines.length)
77
+ return null;
78
+ const original = lines[index] ?? "";
79
+ const trimmed = original.trimStart();
80
+ // 空行或已是注释:没有可注释的失败行(EOF 形态的错误常落在空行)。
81
+ if (trimmed === "" || trimmed.startsWith("#"))
82
+ return null;
83
+ const fixed = lines.map((line, i) => (i === index ? `# ${original}` : line)).join("\n");
84
+ const layer = entry.layer;
85
+ const evidence = { kind: "doctor-finding", findingId: entry.id, layer };
86
+ return {
87
+ id: options.id ?? `rule-${entry.id}`,
88
+ createdAt: (options.now ?? Date.now)(),
89
+ kind: "workspace-config-fix",
90
+ target: parsed.path,
91
+ layer,
92
+ evidence: [evidence],
93
+ // 占位:12.3 按 kind+target 重算,不采信自报。
94
+ riskClass: "B",
95
+ summary: `注释 workspace TOML 解析失败行(${basename(parsed.path)}:${parsed.line})`,
96
+ edits: [{ op: "write-file", path: parsed.path, content: fixed }],
97
+ };
98
+ }
99
+ /**
100
+ * 信号 → rule-based 提案:只在 rule-based 通道消费 doctor finding 形态
101
+ * 信号(按 stableId 对齐 findings);editor 通道由 editor 产出提案,本
102
+ * 函数不代劳(channel 透传,proposals 为空)。
103
+ */
104
+ export function proposeFromSignals(options) {
105
+ const channel = selectProposalChannel(options.availability);
106
+ if (channel === "editor") {
107
+ return { channel, proposals: [], skipped: [] };
108
+ }
109
+ const findingsById = new Map(options.findings.map((entry) => [entry.id, entry]));
110
+ const proposals = [];
111
+ const skipped = [];
112
+ const seen = new Set();
113
+ for (const signal of options.signals) {
114
+ if (!isDoctorFindingSignal(signal))
115
+ continue;
116
+ const findingId = signal.stableId;
117
+ if (findingId === null || seen.has(findingId))
118
+ continue;
119
+ seen.add(findingId);
120
+ const entry = findingsById.get(findingId);
121
+ if (entry === undefined) {
122
+ skipped.push({ findingId, reason: "信号没有对应的检查条目" });
123
+ continue;
124
+ }
125
+ const proposal = ruleBasedProposal(entry, {
126
+ ...(options.now !== undefined ? { now: options.now } : {}),
127
+ ...(options.readFile !== undefined ? { readFile: options.readFile } : {}),
128
+ });
129
+ if (proposal === null) {
130
+ skipped.push({ findingId, reason: `kind "${entry.kind}" 没有显而易见的机械修复` });
131
+ continue;
132
+ }
133
+ proposals.push(proposal);
134
+ }
135
+ return { channel, proposals, skipped };
136
+ }