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.
Files changed (96) hide show
  1. package/dist/cli.js +12 -1
  2. package/dist/commands/doctor.d.ts +8 -0
  3. package/dist/commands/doctor.js +87 -2
  4. package/dist/commands/evolve.d.ts +43 -0
  5. package/dist/commands/evolve.js +479 -0
  6. package/dist/commands/mcpGateway.d.ts +28 -0
  7. package/dist/commands/mcpGateway.js +40 -6
  8. package/dist/evolve/agentView.d.ts +32 -0
  9. package/dist/evolve/agentView.js +34 -0
  10. package/dist/evolve/apply.d.ts +71 -0
  11. package/dist/evolve/apply.js +162 -0
  12. package/dist/evolve/archive.d.ts +23 -0
  13. package/dist/evolve/archive.js +58 -0
  14. package/dist/evolve/erase.d.ts +17 -0
  15. package/dist/evolve/erase.js +65 -0
  16. package/dist/evolve/extension.d.ts +37 -0
  17. package/dist/evolve/extension.js +63 -0
  18. package/dist/evolve/gate.d.ts +95 -0
  19. package/dist/evolve/gate.js +295 -0
  20. package/dist/evolve/importHistory.d.ts +91 -0
  21. package/dist/evolve/importHistory.js +239 -0
  22. package/dist/evolve/jobMain.d.ts +22 -0
  23. package/dist/evolve/jobMain.js +52 -0
  24. package/dist/evolve/jobRun.d.ts +94 -0
  25. package/dist/evolve/jobRun.js +273 -0
  26. package/dist/evolve/jobShell.d.ts +46 -0
  27. package/dist/evolve/jobShell.js +75 -0
  28. package/dist/evolve/l2.d.ts +93 -0
  29. package/dist/evolve/l2.js +81 -0
  30. package/dist/evolve/lesson.d.ts +57 -0
  31. package/dist/evolve/lesson.js +97 -0
  32. package/dist/evolve/pending.d.ts +69 -0
  33. package/dist/evolve/pending.js +116 -0
  34. package/dist/evolve/probeWorkspace.d.ts +57 -0
  35. package/dist/evolve/probeWorkspace.js +69 -0
  36. package/dist/evolve/propose.d.ts +73 -0
  37. package/dist/evolve/propose.js +136 -0
  38. package/dist/evolve/recorder.d.ts +68 -0
  39. package/dist/evolve/recorder.js +169 -0
  40. package/dist/evolve/report.d.ts +26 -0
  41. package/dist/evolve/report.js +37 -0
  42. package/dist/evolve/reportView.d.ts +96 -0
  43. package/dist/evolve/reportView.js +311 -0
  44. package/dist/evolve/risk.d.ts +106 -0
  45. package/dist/evolve/risk.js +224 -0
  46. package/dist/evolve/sources.d.ts +19 -0
  47. package/dist/evolve/sources.js +29 -0
  48. package/dist/evolve/stateDir.d.ts +21 -0
  49. package/dist/evolve/stateDir.js +56 -0
  50. package/dist/evolve/surface.d.ts +34 -0
  51. package/dist/evolve/surface.js +72 -0
  52. package/dist/evolve/trajectory.d.ts +25 -0
  53. package/dist/evolve/trajectory.js +88 -0
  54. package/dist/evolve/treeToCanonical.d.ts +28 -0
  55. package/dist/evolve/treeToCanonical.js +79 -0
  56. package/dist/evolve/trellisSubstrate.d.ts +65 -0
  57. package/dist/evolve/trellisSubstrate.js +154 -0
  58. package/dist/evolve/trigger.d.ts +177 -0
  59. package/dist/evolve/trigger.js +400 -0
  60. package/dist/evolve/types.d.ts +126 -0
  61. package/dist/evolve/types.js +15 -0
  62. package/dist/evolve/validate.d.ts +139 -0
  63. package/dist/evolve/validate.js +315 -0
  64. package/dist/host/checks.d.ts +57 -0
  65. package/dist/host/checks.js +126 -0
  66. package/dist/host/delivery.d.ts +42 -0
  67. package/dist/host/delivery.js +59 -0
  68. package/dist/host/edits.d.ts +87 -0
  69. package/dist/host/edits.js +102 -0
  70. package/dist/host/hostEvents.d.ts +74 -0
  71. package/dist/host/hostEvents.js +45 -0
  72. package/dist/host/index.d.ts +22 -0
  73. package/dist/host/index.js +17 -0
  74. package/dist/host/levels.d.ts +106 -0
  75. package/dist/host/levels.js +206 -0
  76. package/dist/host/staging.d.ts +56 -0
  77. package/dist/host/staging.js +106 -0
  78. package/dist/host/surface.d.ts +36 -0
  79. package/dist/host/surface.js +20 -0
  80. package/dist/lib/backup.js +12 -3
  81. package/dist/lib/fsAtomic.d.ts +7 -0
  82. package/dist/lib/fsAtomic.js +14 -0
  83. package/dist/lib/hostEventBus.d.ts +47 -0
  84. package/dist/lib/hostEventBus.js +94 -0
  85. package/dist/lib/hostExtensions.d.ts +75 -0
  86. package/dist/lib/hostExtensions.js +196 -0
  87. package/dist/lib/mcpRuntime.d.ts +12 -1
  88. package/dist/lib/mcpRuntime.js +34 -2
  89. package/dist/lib/mcpToolRegistry.d.ts +18 -2
  90. package/dist/lib/mcpToolRegistry.js +29 -9
  91. package/dist/pi-bridge/bundle.js +16 -6
  92. package/docs/magpie-gateway-research.md +422 -0
  93. package/docs/roadmap.md +22 -12
  94. package/docs/runtime-infra.md +41 -22
  95. package/docs/self-evolution.md +138 -44
  96. package/package.json +7 -2
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 一次性 detached 作业入口(trellis-evolve 任务 11.5)。
3
+ *
4
+ * 契约:argv = [--hold-ms N] [--trigger event|manual|chain] <homeDir>。
5
+ * `--hold-ms`(持有锁 N 毫秒后再收尾,供集成测试观察"作业运行中"的锁
6
+ * 状态;生产不传,作业即时完成)。`--trigger` 记录触发来源入 job-run
7
+ * 档案(缺省 event;chain 为补跑链起,run-backfill D3)。锁由作业自身
8
+ * 持有(拉起方 spawn 前只做活锁检查,本入口再权威抢一次以防并发双拉起
9
+ * 竞态,抢不到立即退出、不做任何工作、不排队)。壳语义(抢锁/释放、
10
+ * job-run 落档含 trigger 字段、错误收口)在 `jobShell.js`——`trellis
11
+ * evolve run` 前台手动触发走同一壳(D1 不另写执行路径)。作业体是
12
+ * rule-based 治理流水的一次运行:`runEvolveJob`(信号 → rule-based
13
+ * 提案 → 机械校验 → L1 前闸 → 风险/采纳门 → 应用 → lesson,14.1 的
14
+ * fixture E2E 同链)。引擎内核(packages/evolve-engine 的
15
+ * schedule/recursion/proposer/selection/judge)golden 锚定但生产未经
16
+ * 此消费,待 L2/探针任务集接入。级别 off/observe 时作业体零副作用早退。
17
+ *
18
+ * 注意:本文件是入口脚本,被 import 时顶层即执行;测试只应 spawn 它,
19
+ * 不应 import(需要锁原语请从 `trigger.js` 取,作业壳从 `jobShell.js`
20
+ * 取,作业体从 `jobRun.js` 取)。
21
+ */
22
+ import { runJobShell } from "./jobShell.js";
23
+ const TRIGGER_KINDS = ["event", "manual", "chain"];
24
+ const argv = process.argv.slice(2);
25
+ let holdMs = 0;
26
+ const holdIdx = argv.indexOf("--hold-ms");
27
+ if (holdIdx !== -1) {
28
+ holdMs = Number(argv[holdIdx + 1] ?? 0);
29
+ argv.splice(holdIdx, 2);
30
+ }
31
+ let trigger = "event";
32
+ const triggerIdx = argv.indexOf("--trigger");
33
+ if (triggerIdx !== -1) {
34
+ const raw = argv[triggerIdx + 1];
35
+ if (raw === undefined || !TRIGGER_KINDS.includes(raw)) {
36
+ console.error(`jobMain: --trigger 必须是 ${TRIGGER_KINDS.join(" | ")}`);
37
+ process.exit(2);
38
+ }
39
+ trigger = raw;
40
+ argv.splice(triggerIdx, 2);
41
+ }
42
+ const homeDir = argv[argv.length - 1];
43
+ if (homeDir === undefined || homeDir === "") {
44
+ console.error("jobMain: 用法 node jobMain.js [--hold-ms N] [--trigger event|manual|chain] <homeDir>");
45
+ process.exit(2);
46
+ }
47
+ const result = await runJobShell(homeDir, { trigger, holdMs });
48
+ if (result.status === "error") {
49
+ console.error(`jobMain: 作业体失败:${result.message}`);
50
+ process.exitCode = 1;
51
+ }
52
+ // lock-busy:静默退出 0(竞态窗口已有作业在跑,不排队)。
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 一次性作业的作业体(trellis-evolve 任务 11.5;14.1 fixture E2E 的流水
3
+ * 在此落地为生产入口)。作业体是下述 rule-based 治理流水;引擎内核
4
+ * (packages/evolve-engine 的 schedule/recursion/proposer/selection/judge)
5
+ * golden 锚定但生产未经此消费,待 L2/探针任务集接入(review-fixes 纠偏
6
+ * 旧注"作业即运行移植引擎"的名不副实)。
7
+ *
8
+ * 流水(顺序即依赖,与 fixtureE2e 同链):
9
+ * 0. 级别闸:没有任何 propose/auto-A 设置 → 零副作用返回(触发器只在
10
+ * propose+ 拉起;这里是直接 spawn 的防御)。
11
+ * 1. 可选源探测(11.3):agent-bridge 缺席记 unavailable,不失败。
12
+ * 2. 信号合并(自上次作业的信号合并时刻以来;首次为全部,11.5"未
13
+ * 消费"语义)。
14
+ * 3. 检查发现:全局 + 信号层归属出现过/级别策略声明过的 workspace,
15
+ * 各自 `runChecks`(L1 的 doctor oracle)。观察面即发现面——只有
16
+ * 进过信号/策略的 workspace 会被扫描(与"信号是观察记录"一致)。
17
+ * 4. 提案草稿:rule-based 通道——`proposeFromSignals` 的信号对齐路径
18
+ * ∪ 当前发现直接 `ruleBasedProposal`(同一内核、提案 id 同源
19
+ * `rule-<findingId>` 天然去重;已入 pending 的跳过)。editor/spawn
20
+ * 通道(design D7)在 `runDelegatedCall` 就绪前不在作业里拉真实
21
+ * CLI 会话,见 `ProposalChannelAvailability` 注释。
22
+ * 5. 逐草稿治理:域分派(配置类才进通道)→ 机械校验(无 editor 不
23
+ * 重试)→ L1 前闸(verdict 差集,唯一 verdict)→ 风险分类(不采信
24
+ * 自报)→ 采纳门(A 仅 auto-A 且闸通过自动采纳并通知;B 永远待批)。
25
+ * 草稿的提案/采纳级别按其目标层解析(workspace > default;agent
26
+ * 粒度只闸观察与触发,不进提案产出——提案的来源 agent 不可考,
27
+ * 发现是 doctor 检查面而非 agent 行为)。
28
+ * 6. 应用阶段(12.5):全部已批准未应用的编辑在同一备份会话写入;
29
+ * 按 delivery 分类对原生渲染 agent 触发同步(host 门面 `triggerRenderSync`)。
30
+ * 7. lesson(12.6):本次应用的每条提案蒸馏一条 lesson;记忆启用走
31
+ * 记忆管道,否则只留档案(skipped 不是 failed)。
32
+ *
33
+ * 每步都只经 `src/host/*` 门面与本目录模块(边界 10.3);无模型调用、
34
+ * 不 spawn 真实 agent CLI。摘要返回给 jobMain 并入 job-run 档案。
35
+ */
36
+ import type { CheckEntry } from "../host/index.js";
37
+ import { type EvolveLevelPolicy } from "../host/index.js";
38
+ import { type AgentEffectiveness, type ApplyAgent } from "./apply.js";
39
+ import { type EvaluateL1Options, type L1GateResult } from "./gate.js";
40
+ import { type OptionalSourceProbe } from "./sources.js";
41
+ import type { Proposal, Signal } from "./types.js";
42
+ /** 草稿来源的输入面(proposeFn 注入缝的契约)。 */
43
+ export interface ProposeInput {
44
+ readonly homeDir: string;
45
+ readonly signals: readonly Signal[];
46
+ readonly findings: readonly CheckEntry[];
47
+ readonly now: () => number;
48
+ }
49
+ export interface RunEvolveJobOptions {
50
+ /** 时间源(毫秒);缺省 Date.now(测试注入固定时钟)。 */
51
+ readonly now?: () => number;
52
+ /** 草稿来源注入缝(测试直达治理层);缺省 rule-based 通道组装。 */
53
+ readonly proposeFn?: (input: ProposeInput) => readonly Proposal[];
54
+ /** L1 前闸注入缝(与 `EvaluateL1Options` 自带缝同风格);缺省真实闸。 */
55
+ readonly evaluateL1Fn?: (options: EvaluateL1Options) => Promise<L1GateResult>;
56
+ /** 应用阶段的 agent 分类(测试传空避开真实同步);缺省按 delivery 分类。 */
57
+ readonly applyAgents?: readonly ApplyAgent[];
58
+ /** 原生渲染同步触发缝;缺省 host `triggerRenderSync`。 */
59
+ readonly triggerSync?: () => Promise<{
60
+ exitCode: number;
61
+ }>;
62
+ }
63
+ export interface EvolveJobSummary {
64
+ readonly signalsConsidered: number;
65
+ readonly findingsConsidered: number;
66
+ readonly draftCount: number;
67
+ readonly enqueuedIds: string[];
68
+ readonly autoAdoptedIds: string[];
69
+ readonly discarded: readonly {
70
+ readonly id: string;
71
+ readonly code: string;
72
+ readonly reason: string;
73
+ }[];
74
+ readonly skippedNotes: readonly string[];
75
+ readonly applied: {
76
+ readonly runId: string | null;
77
+ readonly appliedProposalIds: readonly string[];
78
+ readonly syncExitCode: number | null;
79
+ /** 同步触发失败文本(收口为效果文本);无为 null。 */
80
+ readonly syncError: string | null;
81
+ /** 逐 agent 生效时点(review-fixes ❌3:随 job-run 档案入档)。 */
82
+ readonly effects: readonly AgentEffectiveness[];
83
+ };
84
+ readonly lessonIds: string[];
85
+ readonly notices: string[];
86
+ readonly optionalSources: readonly OptionalSourceProbe[];
87
+ /** 信号合并时刻(步骤 2 的 now());job-run 档案据此推进消费水位线,
88
+ * 作业运行期间到达的信号下次仍被消费。级别闸早退为 null(未合并)。 */
89
+ readonly signalsMergedAtMs: number | null;
90
+ }
91
+ /** 作业级闸:任一粒度出现 propose/auto-A 才值得运行(observe 只采集)。
92
+ * `trellis evolve run` 的早退判定与作业体同源(1.3)。 */
93
+ export declare function anyProposalLevel(policy: EvolveLevelPolicy): boolean;
94
+ export declare function runEvolveJob(homeDir: string, options?: RunEvolveJobOptions): Promise<EvolveJobSummary>;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * 一次性作业的作业体(trellis-evolve 任务 11.5;14.1 fixture E2E 的流水
3
+ * 在此落地为生产入口)。作业体是下述 rule-based 治理流水;引擎内核
4
+ * (packages/evolve-engine 的 schedule/recursion/proposer/selection/judge)
5
+ * golden 锚定但生产未经此消费,待 L2/探针任务集接入(review-fixes 纠偏
6
+ * 旧注"作业即运行移植引擎"的名不副实)。
7
+ *
8
+ * 流水(顺序即依赖,与 fixtureE2e 同链):
9
+ * 0. 级别闸:没有任何 propose/auto-A 设置 → 零副作用返回(触发器只在
10
+ * propose+ 拉起;这里是直接 spawn 的防御)。
11
+ * 1. 可选源探测(11.3):agent-bridge 缺席记 unavailable,不失败。
12
+ * 2. 信号合并(自上次作业的信号合并时刻以来;首次为全部,11.5"未
13
+ * 消费"语义)。
14
+ * 3. 检查发现:全局 + 信号层归属出现过/级别策略声明过的 workspace,
15
+ * 各自 `runChecks`(L1 的 doctor oracle)。观察面即发现面——只有
16
+ * 进过信号/策略的 workspace 会被扫描(与"信号是观察记录"一致)。
17
+ * 4. 提案草稿:rule-based 通道——`proposeFromSignals` 的信号对齐路径
18
+ * ∪ 当前发现直接 `ruleBasedProposal`(同一内核、提案 id 同源
19
+ * `rule-<findingId>` 天然去重;已入 pending 的跳过)。editor/spawn
20
+ * 通道(design D7)在 `runDelegatedCall` 就绪前不在作业里拉真实
21
+ * CLI 会话,见 `ProposalChannelAvailability` 注释。
22
+ * 5. 逐草稿治理:域分派(配置类才进通道)→ 机械校验(无 editor 不
23
+ * 重试)→ L1 前闸(verdict 差集,唯一 verdict)→ 风险分类(不采信
24
+ * 自报)→ 采纳门(A 仅 auto-A 且闸通过自动采纳并通知;B 永远待批)。
25
+ * 草稿的提案/采纳级别按其目标层解析(workspace > default;agent
26
+ * 粒度只闸观察与触发,不进提案产出——提案的来源 agent 不可考,
27
+ * 发现是 doctor 检查面而非 agent 行为)。
28
+ * 6. 应用阶段(12.5):全部已批准未应用的编辑在同一备份会话写入;
29
+ * 按 delivery 分类对原生渲染 agent 触发同步(host 门面 `triggerRenderSync`)。
30
+ * 7. lesson(12.6):本次应用的每条提案蒸馏一条 lesson;记忆启用走
31
+ * 记忆管道,否则只留档案(skipped 不是 failed)。
32
+ *
33
+ * 每步都只经 `src/host/*` 门面与本目录模块(边界 10.3);无模型调用、
34
+ * 不 spawn 真实 agent CLI。摘要返回给 jobMain 并入 job-run 档案。
35
+ */
36
+ import { classifyManagedObservation, isMemoryDeliveryEnabled, loadEvolveLevelPolicy, resolveEvolveLevel, runChecks, triggerRenderSync, } from "../host/index.js";
37
+ import { appendArchiveRecord, readArchiveRecords } from "./archive.js";
38
+ import { applyApprovedProposals } from "./apply.js";
39
+ import { dispatchProposalDomain, evaluateL1Proposal } from "./gate.js";
40
+ import { recordLesson } from "./lesson.js";
41
+ import { decideProposal, enqueueProposal, readPendingRecord, readPendingRecords, safeProposalFileName, } from "./pending.js";
42
+ import { proposeFromSignals, ruleBasedProposal } from "./propose.js";
43
+ import { readAllSignals } from "./recorder.js";
44
+ import { classifyProposalRisk, evaluateAdoptionGate, retargetNotificationRollback } from "./risk.js";
45
+ import { probeAgentBridgeSource } from "./sources.js";
46
+ import { PROPOSAL_ARCHIVE_KIND, lastSignalWatermarkMs, } from "./trigger.js";
47
+ import { validateProposalWithRetry } from "./validate.js";
48
+ /** 作业级闸:任一粒度出现 propose/auto-A 才值得运行(observe 只采集)。
49
+ * `trellis evolve run` 的早退判定与作业体同源(1.3)。 */
50
+ export function anyProposalLevel(policy) {
51
+ const levels = [
52
+ policy.defaultLevel,
53
+ ...Object.values(policy.agents),
54
+ ...Object.values(policy.workspaces),
55
+ ];
56
+ return levels.some((level) => level === "propose" || level === "auto-A");
57
+ }
58
+ /** 草稿目标层的生效级别(workspace > agent > default > off)。 */
59
+ function draftLevel(policy, layer) {
60
+ return resolveEvolveLevel(policy, layer.kind === "workspace" ? { workspaceRoot: layer.root } : {});
61
+ }
62
+ /** 缺省草稿组装:信号对齐 ∪ 当前发现直提,id 同源去重。 */
63
+ function defaultPropose(input) {
64
+ // editor/spawn 通道就绪前,生产走 rule-based 兜底(design D7:委派不可
65
+ // 用时 doctor finding 形态信号走 rule-based)。ruleBasedProposal 是
66
+ // proposeFromSignals 的同一内核;这里补上"无对齐信号但发现仍在"的条目
67
+ // ——发现由作业的 runChecks 观察(9.4①:doctor/sync 检查即原生渲染
68
+ // agent 的信号来源),生产信号面里事件信号 stableId 恒为 null。
69
+ const loop = proposeFromSignals({
70
+ signals: input.signals,
71
+ findings: input.findings,
72
+ availability: { delegate: false, spawn: false },
73
+ now: input.now,
74
+ });
75
+ const drafts = [...loop.proposals];
76
+ const covered = new Set(drafts.map((draft) => draft.id));
77
+ for (const entry of input.findings) {
78
+ const draft = ruleBasedProposal(entry, { now: input.now });
79
+ if (draft === null || covered.has(draft.id))
80
+ continue;
81
+ covered.add(draft.id);
82
+ drafts.push(draft);
83
+ }
84
+ return drafts;
85
+ }
86
+ /** 上一作业的信号水位线(优先"信号合并时刻",旧档案回退 endedAtMs);
87
+ * 从未跑过为 null。 */
88
+ function lastSignalWatermark(homeDir) {
89
+ return lastSignalWatermarkMs(readArchiveRecords(homeDir));
90
+ }
91
+ export async function runEvolveJob(homeDir, options = {}) {
92
+ const now = options.now ?? Date.now;
93
+ const empty = {
94
+ signalsConsidered: 0,
95
+ findingsConsidered: 0,
96
+ draftCount: 0,
97
+ enqueuedIds: [],
98
+ autoAdoptedIds: [],
99
+ discarded: [],
100
+ skippedNotes: [],
101
+ applied: { runId: null, appliedProposalIds: [], syncExitCode: null, syncError: null, effects: [] },
102
+ lessonIds: [],
103
+ notices: [],
104
+ optionalSources: [],
105
+ signalsMergedAtMs: null,
106
+ };
107
+ // ── 0. 级别闸 ─────────────────────────────────────────────────────────
108
+ const policy = loadEvolveLevelPolicy(homeDir);
109
+ if (!anyProposalLevel(policy))
110
+ return empty;
111
+ // ── 1. 可选源探测(缺席合法,记状态不失败)────────────────────────────
112
+ const optionalSources = [probeAgentBridgeSource(homeDir)];
113
+ // ── 2. 信号合并(自上次作业的信号水位线以来)────────────────────────
114
+ const signalsMergedAtMs = now();
115
+ const signals = readAllSignals(homeDir, lastSignalWatermark(homeDir));
116
+ // ── 3. 检查发现(全局 + 观察面内的 workspace)────────────────────────
117
+ const workspaceRoots = new Set();
118
+ for (const signal of signals) {
119
+ if (signal.layer.kind === "workspace")
120
+ workspaceRoots.add(signal.layer.root);
121
+ }
122
+ for (const root of Object.keys(policy.workspaces))
123
+ workspaceRoots.add(root);
124
+ const findingsById = new Map();
125
+ const runOne = async (runOptions) => {
126
+ const result = await runChecks(runOptions);
127
+ for (const entry of result.entries)
128
+ findingsById.set(entry.id, entry);
129
+ };
130
+ await runOne({ homeDir });
131
+ for (const root of workspaceRoots)
132
+ await runOne({ homeDir, workspaceRoot: root });
133
+ const findings = [...findingsById.values()];
134
+ // ── 4. 提案草稿 ───────────────────────────────────────────────────────
135
+ const proposeFn = options.proposeFn ?? defaultPropose;
136
+ const drafts = proposeFn({ homeDir, signals, findings, now });
137
+ // ── 5. 逐草稿治理 ─────────────────────────────────────────────────────
138
+ const enqueuedIds = [];
139
+ const autoAdoptedIds = [];
140
+ const discarded = [];
141
+ const skippedNotes = [];
142
+ const notices = [];
143
+ // 自动采纳通知先收集、应用后定稿:决策时备份会话尚不存在,回滚行只能
144
+ // 是泛化占位,拿到真实 runId 再重写(review-fixes ❌5)。
145
+ const adoptedNotifications = [];
146
+ const known = new Set(readPendingRecords(homeDir).map((record) => record.id));
147
+ for (const draft of drafts) {
148
+ if (known.has(draft.id)) {
149
+ skippedNotes.push(`${draft.id}:已有提案记录,不重复产出`);
150
+ continue;
151
+ }
152
+ const level = draftLevel(policy, draft.layer);
153
+ if (level !== "propose" && level !== "auto-A") {
154
+ skippedNotes.push(`${draft.id}:目标层级别 ${level},不产出提案`);
155
+ continue;
156
+ }
157
+ const domain = dispatchProposalDomain(draft.kind);
158
+ if (!domain.entersProposalChannel) {
159
+ skippedNotes.push(`${draft.id}:${domain.note}`);
160
+ continue;
161
+ }
162
+ const validated = await validateProposalWithRetry({ homeDir, proposal: draft, now });
163
+ if (validated.status === "discarded") {
164
+ discarded.push({ id: draft.id, code: validated.code, reason: validated.reason });
165
+ continue;
166
+ }
167
+ const proposal = validated.proposal;
168
+ const evaluateL1 = options.evaluateL1Fn ?? evaluateL1Proposal;
169
+ const gate = await evaluateL1({
170
+ homeDir,
171
+ workspaceRoot: proposal.layer.kind === "workspace" ? proposal.layer.root : null,
172
+ proposal,
173
+ now,
174
+ });
175
+ const classification = classifyProposalRisk(proposal, {
176
+ homeDir,
177
+ ...(validated.validation.needsExplicitApproval !== undefined
178
+ ? { needsExplicitApproval: validated.validation.needsExplicitApproval }
179
+ : {}),
180
+ });
181
+ const adoption = evaluateAdoptionGate({
182
+ level,
183
+ classification,
184
+ probePassed: gate.decision === "acceptable",
185
+ proposal,
186
+ });
187
+ if (gate.decision !== "acceptable") {
188
+ skippedNotes.push(`${proposal.id}:L1 前闸不可采纳(${gate.reason})`);
189
+ continue;
190
+ }
191
+ enqueueProposal({
192
+ homeDir,
193
+ proposal,
194
+ riskClass: classification.riskClass,
195
+ riskRuleId: classification.ruleId,
196
+ now,
197
+ });
198
+ appendArchiveRecord(homeDir, {
199
+ kind: PROPOSAL_ARCHIVE_KIND,
200
+ payload: { proposalId: proposal.id, riskClass: classification.riskClass, riskRuleId: classification.ruleId },
201
+ }, () => new Date(now()));
202
+ known.add(proposal.id);
203
+ if (adoption.action === "auto-adopt") {
204
+ decideProposal({ homeDir, id: proposal.id, decision: "approved", now });
205
+ autoAdoptedIds.push(proposal.id);
206
+ adoptedNotifications.push(adoption.notification);
207
+ }
208
+ else {
209
+ enqueuedIds.push(proposal.id);
210
+ }
211
+ }
212
+ // ── 6. 应用阶段(全部已批准未应用,同一备份会话)──────────────────────
213
+ const agents = options.applyAgents ?? classifyManagedObservation(homeDir).map((entry) => ({
214
+ agent: entry.agent,
215
+ observation: entry.observation,
216
+ }));
217
+ const triggerSync = options.triggerSync ?? (async () => await triggerRenderSync({ homeDir }));
218
+ const applied = await applyApprovedProposals({
219
+ homeDir,
220
+ agents,
221
+ triggerSync,
222
+ now: () => new Date(now()),
223
+ });
224
+ for (const id of applied.skippedInvalidIds) {
225
+ skippedNotes.push(`${id}:应用前重校验不通过(pending 磁盘状态漂移?),跳过`);
226
+ }
227
+ // 自动采纳通知定稿:回滚行重写为真实备份会话标识(应用失败/未开会话时
228
+ // 保留泛化占位——没有真实 runId 可指)。
229
+ for (const notification of adoptedNotifications) {
230
+ notices.push(applied.runId === null
231
+ ? notification.message
232
+ : retargetNotificationRollback(notification, `trellis evolve rollback ${applied.runId}`).message);
233
+ }
234
+ // ── 7. lesson(本次应用的每条提案蒸馏一条)───────────────────────────
235
+ const memoryEnabled = isMemoryDeliveryEnabled(homeDir);
236
+ const lessonIds = [];
237
+ for (const id of applied.appliedProposalIds) {
238
+ const record = readPendingRecord(homeDir, id);
239
+ if (record === null)
240
+ continue;
241
+ const lesson = {
242
+ id: `lesson-${safeProposalFileName(id)}`,
243
+ createdAt: now(),
244
+ cue: record.proposal.summary,
245
+ body: `应用阶段已写入备份会话 ${applied.runId ?? "?"}(风险 ${record.riskClass});` +
246
+ `编辑:${record.proposal.edits.map((edit) => `${edit.op} ${edit.path}`).join("; ")}(kind ${record.proposal.kind})。`,
247
+ evidence: record.proposal.evidence,
248
+ layer: record.proposal.layer,
249
+ };
250
+ recordLesson({ homeDir, lesson, memoryEnabled });
251
+ lessonIds.push(lesson.id);
252
+ }
253
+ return {
254
+ signalsConsidered: signals.length,
255
+ findingsConsidered: findings.length,
256
+ draftCount: drafts.length,
257
+ enqueuedIds,
258
+ autoAdoptedIds,
259
+ discarded,
260
+ skippedNotes,
261
+ applied: {
262
+ runId: applied.runId,
263
+ appliedProposalIds: applied.appliedProposalIds,
264
+ syncExitCode: applied.syncExitCode,
265
+ syncError: applied.syncError,
266
+ effects: applied.effects,
267
+ },
268
+ lessonIds,
269
+ notices,
270
+ optionalSources,
271
+ signalsMergedAtMs,
272
+ };
273
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 作业壳(trellis-evolve-run-backfill 任务 1.1)。
3
+ *
4
+ * jobMain(detached 入口)与 `trellis evolve run`(前台手动触发)共享的
5
+ * 一次作业运行外壳:抢锁 → 跑作业体 → 落 job-run 档案 → 释锁,错误收口
6
+ * (失败也落档案——不落档案会让水位线与冷却都不推进,同一确定性错误在
7
+ * 每次会话结束被立即重拉成崩溃循环)。job-run payload 带 `trigger` 触发
8
+ * 来源(`event` / `manual` / `chain`);旧档案无该字段时消费端视作 `event`。
9
+ *
10
+ * 本模块只做壳:作业体在 `jobRun.js`(rule-based 治理流水),锁与档案
11
+ * 原语在 `trigger.js`。jobMain 仍是唯一 detached 入口(链起用),CLI
12
+ * 前台调用本壳即手动触发(D1:不另写执行路径)。
13
+ */
14
+ import { type EvolveJobSummary } from "./jobRun.js";
15
+ import { type JobTriggerKind, type TryLaunchJobOptions } from "./trigger.js";
16
+ /** 触发来源(单一来源在 trigger.js;此处 re-export 保持壳类型面完整)。 */
17
+ export type { JobTriggerKind } from "./trigger.js";
18
+ export interface RunJobShellOptions {
19
+ /** 触发来源,入 job-run 档案。 */
20
+ readonly trigger: JobTriggerKind;
21
+ /** 持锁 N 毫秒后再跑作业体(jobMain `--hold-ms` 测试缝;生产不传)。 */
22
+ readonly holdMs?: number;
23
+ /** 时间源(毫秒);缺省 Date.now(测试注入固定时钟)。 */
24
+ readonly now?: () => number;
25
+ /** 补跑链起的作业命令/ spawn 注入缝(测试);缺省真实 jobMain 与 spawn。 */
26
+ readonly chainJobCommand?: TryLaunchJobOptions["jobCommand"];
27
+ readonly chainSpawnFn?: TryLaunchJobOptions["spawnFn"];
28
+ }
29
+ export type RunJobShellResult =
30
+ /** 抢锁失败:已有作业在跑(竞态窗口),不做任何工作、不排队。 */
31
+ {
32
+ readonly status: "lock-busy";
33
+ } | {
34
+ readonly status: "ok";
35
+ readonly summary: EvolveJobSummary;
36
+ }
37
+ /** 作业体抛错:job-run 档案已落 error 记录(推进冷却)。 */
38
+ | {
39
+ readonly status: "error";
40
+ readonly message: string;
41
+ };
42
+ /**
43
+ * 跑一次作业(壳语义):锁由本壳持有,收尾无论成败都释锁并落 job-run。
44
+ * lock-busy 时不落任何档案(一次根本没发生的运行不在档案说谎)。
45
+ */
46
+ export declare function runJobShell(homeDir: string, options: RunJobShellOptions): Promise<RunJobShellResult>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 作业壳(trellis-evolve-run-backfill 任务 1.1)。
3
+ *
4
+ * jobMain(detached 入口)与 `trellis evolve run`(前台手动触发)共享的
5
+ * 一次作业运行外壳:抢锁 → 跑作业体 → 落 job-run 档案 → 释锁,错误收口
6
+ * (失败也落档案——不落档案会让水位线与冷却都不推进,同一确定性错误在
7
+ * 每次会话结束被立即重拉成崩溃循环)。job-run payload 带 `trigger` 触发
8
+ * 来源(`event` / `manual` / `chain`);旧档案无该字段时消费端视作 `event`。
9
+ *
10
+ * 本模块只做壳:作业体在 `jobRun.js`(rule-based 治理流水),锁与档案
11
+ * 原语在 `trigger.js`。jobMain 仍是唯一 detached 入口(链起用),CLI
12
+ * 前台调用本壳即手动触发(D1:不另写执行路径)。
13
+ */
14
+ import { loadEvolveLevelPolicy } from "../host/index.js";
15
+ import { appendArchiveRecord } from "./archive.js";
16
+ import { runEvolveJob } from "./jobRun.js";
17
+ import { acquireJobLock, chainAfterJob, consumeRunRequest, releaseJobLock, JOB_RUN_KIND, } from "./trigger.js";
18
+ /**
19
+ * 跑一次作业(壳语义):锁由本壳持有,收尾无论成败都释锁并落 job-run。
20
+ * lock-busy 时不落任何档案(一次根本没发生的运行不在档案说谎)。
21
+ */
22
+ export async function runJobShell(homeDir, options) {
23
+ const now = options.now ?? Date.now;
24
+ const pid = process.pid;
25
+ if (!acquireJobLock(homeDir, pid, now())) {
26
+ return { status: "lock-busy" };
27
+ }
28
+ // 任何作业启动即清补跑标记(D3:该请求已被本次运行满足)。
29
+ consumeRunRequest(homeDir);
30
+ const startedAtMs = now();
31
+ try {
32
+ if (options.holdMs !== undefined && options.holdMs > 0) {
33
+ await new Promise((resolve) => setTimeout(resolve, options.holdMs));
34
+ }
35
+ const summary = await runEvolveJob(homeDir, { now });
36
+ appendArchiveRecord(homeDir, {
37
+ kind: JOB_RUN_KIND,
38
+ payload: {
39
+ pid,
40
+ startedAtMs,
41
+ endedAtMs: now(),
42
+ trigger: options.trigger,
43
+ signalsMergedAtMs: summary.signalsMergedAtMs,
44
+ summary,
45
+ },
46
+ });
47
+ return { status: "ok", summary };
48
+ }
49
+ catch (err) {
50
+ const message = err instanceof Error ? err.message : String(err);
51
+ try {
52
+ appendArchiveRecord(homeDir, {
53
+ kind: JOB_RUN_KIND,
54
+ payload: { pid, startedAtMs, endedAtMs: now(), trigger: options.trigger, error: message },
55
+ });
56
+ }
57
+ catch {
58
+ // 档案写入自身失败(如磁盘满)时不再连锁,锁仍由 finally 释放。
59
+ }
60
+ return { status: "error", message };
61
+ }
62
+ finally {
63
+ releaseJobLock(homeDir, pid);
64
+ // 收尾(释锁后)消费补跑标记并链起至多一次补跑(D3):运行期间到达的
65
+ // 请求塌缩为一个标记,在这里被消费;无标记不链。
66
+ const policy = loadEvolveLevelPolicy(homeDir);
67
+ chainAfterJob({
68
+ homeDir,
69
+ now,
70
+ ...(policy.trigger !== undefined ? { limits: policy.trigger } : {}),
71
+ ...(options.chainJobCommand !== undefined ? { jobCommand: options.chainJobCommand } : {}),
72
+ ...(options.chainSpawnFn !== undefined ? { spawnFn: options.chainSpawnFn } : {}),
73
+ });
74
+ }
75
+ }
@@ -0,0 +1,93 @@
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 type { ArchiveEntry } from "./archive.js";
16
+ /** 运行档案 kind:一条 L2 比较记录。 */
17
+ export declare const L2_COMPARISON_ARCHIVE_KIND = "l2-comparison";
18
+ export type L2ArmRole = "baseline" | "candidate";
19
+ /** 冻结的交付物引用:标签 + 内容摘要键(切片任何变化即键变)。 */
20
+ export interface L2FrozenArtifact {
21
+ readonly label: string;
22
+ readonly digest: string;
23
+ }
24
+ export interface L2Arm {
25
+ /** 种子派生的匿名标识(呈现面);不含角色词。 */
26
+ readonly anonId: string;
27
+ /** 该臂自己的运行标识。 */
28
+ readonly runId: string;
29
+ readonly role: L2ArmRole;
30
+ readonly artifacts: readonly L2FrozenArtifact[];
31
+ }
32
+ /** 按任务设种子:每条探针任务一个种子,匿名标识由此派生。 */
33
+ export interface L2TaskSeed {
34
+ readonly task: string;
35
+ readonly seed: string;
36
+ }
37
+ export type L2SelectorResult = {
38
+ readonly status: "selected";
39
+ readonly anonId: string;
40
+ } | {
41
+ readonly status: "failed";
42
+ readonly reason: string;
43
+ };
44
+ /** 失败收口的 verdict:
45
+ * - selected baseline → not-adopted(不采纳);
46
+ * - 选择器失败 / 任一臂未交付 → invalid(同样不采纳,收口形态不同)。 */
47
+ export type L2Verdict = {
48
+ readonly kind: "adopt-candidate";
49
+ readonly anonId: string;
50
+ } | {
51
+ readonly kind: "not-adopted-baseline";
52
+ readonly anonId: string;
53
+ } | {
54
+ readonly kind: "invalid";
55
+ readonly reason: string;
56
+ };
57
+ export interface L2ComparisonRecord {
58
+ readonly comparisonId: string;
59
+ readonly createdAt: number;
60
+ readonly taskSeeds: readonly L2TaskSeed[];
61
+ /** baseline 与候选全在(base 留池,13.2 的偏差在引擎侧,记录层照存)。 */
62
+ readonly arms: readonly L2Arm[];
63
+ readonly selector: L2SelectorResult;
64
+ readonly verdict: L2Verdict;
65
+ }
66
+ /**
67
+ * 匿名标识:按任务种子 + 运行标识派生的稳定短哈希。构造上不可能
68
+ * 含角色词——呈现面与档案里的值同源,泄露面为零。
69
+ */
70
+ export declare function anonArmId(taskSeeds: readonly L2TaskSeed[], runId: string): string;
71
+ export interface BuildL2ComparisonInput {
72
+ readonly comparisonId: string;
73
+ readonly createdAt: number;
74
+ readonly taskSeeds: readonly L2TaskSeed[];
75
+ /** 每臂运行标识 + 角色 + 冻结交付物;anonId 由本函数派生。 */
76
+ readonly arms: readonly {
77
+ readonly runId: string;
78
+ readonly role: L2ArmRole;
79
+ readonly artifacts: readonly L2FrozenArtifact[];
80
+ }[];
81
+ readonly selector: L2SelectorResult;
82
+ }
83
+ /** 由臂与选择器结果构建完整记录(派生匿名映射 + 失败收口 verdict)。 */
84
+ export declare function buildL2Comparison(input: BuildL2ComparisonInput): L2ComparisonRecord;
85
+ /** 追加一条比较记录到运行档案。 */
86
+ export declare function appendL2Comparison(homeDir: string, record: L2ComparisonRecord, now?: () => Date): ArchiveEntry;
87
+ /** 读回全部比较记录(档案读取,零副作用)。 */
88
+ export declare function readL2Comparisons(homeDir: string): L2ComparisonRecord[];
89
+ /** 选择器视图:只给匿名标识与冻结交付物引用——不带 role/runId。 */
90
+ export declare function presentL2Arms(record: L2ComparisonRecord): {
91
+ readonly anonId: string;
92
+ readonly artifacts: readonly L2FrozenArtifact[];
93
+ }[];