@sema-agent/client-core 0.5.0 → 0.7.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/adapt.d.ts +17 -3
- package/dist/adapt.js +555 -36
- package/dist/clientSlice.d.ts +169 -0
- package/dist/clientSlice.js +62 -0
- package/dist/diff/patch.d.ts +29 -0
- package/dist/diff/patch.js +45 -0
- package/dist/engineSessionParam.d.ts +7 -0
- package/dist/engineSessionParam.js +35 -0
- package/dist/engineWireSdk.d.ts +25 -1
- package/dist/engineWireSdk.js +30 -4
- package/dist/finalVerifyWire.d.ts +74 -0
- package/dist/finalVerifyWire.js +63 -0
- package/dist/headlessPermissionModeWire.d.ts +55 -0
- package/dist/headlessPermissionModeWire.js +111 -0
- package/dist/headlessReconnectWire.d.ts +96 -0
- package/dist/headlessReconnectWire.js +141 -0
- package/dist/host.d.ts +90 -0
- package/dist/host.js +84 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +48 -3
- package/dist/interactiveToolsWire.d.ts +48 -0
- package/dist/interactiveToolsWire.js +86 -0
- package/dist/limitsWire.d.ts +89 -0
- package/dist/limitsWire.js +225 -0
- package/dist/notifications.d.ts +7 -0
- package/dist/notifications.js +32 -0
- package/dist/request/taskRequest.d.ts +135 -0
- package/dist/request/taskRequest.js +176 -0
- package/dist/sandboxWire.d.ts +75 -0
- package/dist/sandboxWire.js +138 -0
- package/dist/scenarioWire.d.ts +62 -0
- package/dist/scenarioWire.js +115 -0
- package/dist/scratchpadWireCaps.d.ts +16 -0
- package/dist/scratchpadWireCaps.js +65 -0
- package/dist/seam.d.ts +114 -11
- package/dist/seam.js +31 -0
- package/dist/toolResult.d.ts +118 -0
- package/dist/toolResult.js +774 -0
- package/package.json +4 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clientSlice.ts — **verb 门面接口定型**(B4,设计稿 §8-1 裁决:「要,但只定接口不定实现」)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要有这一层
|
|
5
|
+
*
|
|
6
|
+
* §6.1.4 解包 CC 桌面端量到 **854 条 IPC 通道**(`ns_$_Service_$_method`),其中会话相关的
|
|
7
|
+
* `LocalSessions` 一族就有 212 个 verb。那一层客观存在而且很大 —— **不定接口,每端会各长一套**,
|
|
8
|
+
* 于是 HITL 权威登记、通知直结这些「必须只有一个落点」的东西会被重写三遍。
|
|
9
|
+
*
|
|
10
|
+
* ## 但它**只是接口**
|
|
11
|
+
*
|
|
12
|
+
* 🔴 本文件不导出任何 IPC / HTTP / SDK 实现,一行运行时代码都没有(编译产物只有类型)。
|
|
13
|
+
* 依据公理一:CC 的皮肤形状不进 wire ⇒ **IPC 的形状同样不该进 wire**。谁来实现:
|
|
14
|
+
* · TUI = 直接把 `@sema-agent/sdk` 的 AgentClient 方法接上;
|
|
15
|
+
* · web = BFF 转发(逐请求注入 principal);
|
|
16
|
+
* · 桌面 = 主进程 IPC(CC 同款 `ns_$_Service_$_method` 形)。
|
|
17
|
+
*
|
|
18
|
+
* ## 判据:什么进这个接口
|
|
19
|
+
*
|
|
20
|
+
* **会话运行时自己会调的 verb 才进**。纯 UI 触发、端各自决定的动作(开面板、切主题、选文件)
|
|
21
|
+
* 不进 —— 那些是端的事。今天这张表按 cli 现役会话链路归纳,`additive` 演进(只增不改),
|
|
22
|
+
* 与 `CHROME_ARMS` 一样做成可断言的数据出口。
|
|
23
|
+
*/
|
|
24
|
+
/** 一个 verb 的登记项(覆盖率断言用;与 `CHROME_ARMS` 同形)。 */
|
|
25
|
+
export interface ClientVerbSpec {
|
|
26
|
+
/** `ClientSliceLike` 上的方法名。 */
|
|
27
|
+
verb: string;
|
|
28
|
+
/** 缺它会让**已发生的行为丢失**(而不是少个装饰)⇒ required。 */
|
|
29
|
+
required: boolean;
|
|
30
|
+
duty: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 会话运行时需要宿主提供的 verb 集合(**结构型**:实现方不必 implements,形状对上即可)。
|
|
34
|
+
*
|
|
35
|
+
* 全部返回 Promise —— 三端里有两端是跨进程/跨网络的,同步形会逼 web/桌面造假。
|
|
36
|
+
*/
|
|
37
|
+
export interface ClientSliceLike {
|
|
38
|
+
/** 发起一个 turn,拿到 wire 帧流(喂给 `adapt()`)。 */
|
|
39
|
+
streamTask(req: unknown, opts?: {
|
|
40
|
+
signal?: {
|
|
41
|
+
readonly aborted: boolean;
|
|
42
|
+
};
|
|
43
|
+
}): AsyncIterable<unknown>;
|
|
44
|
+
/** 取消在跑的 run(Esc / `-p` 的信号兜底)。 */
|
|
45
|
+
cancelRun(runId: string): Promise<void>;
|
|
46
|
+
/** 工具审批决断。`boundCallId` / `boundInputHash` **必须原样回显**,一个字节不改。 */
|
|
47
|
+
respondApproval(input: {
|
|
48
|
+
approvalId: string;
|
|
49
|
+
decision: 'allow' | 'deny';
|
|
50
|
+
boundCallId: string;
|
|
51
|
+
boundInputHash: string;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
/** AskUserQuestion 应答。 */
|
|
54
|
+
respondQuestion(input: {
|
|
55
|
+
questionId: string;
|
|
56
|
+
answer: unknown;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
/** plan-review 决断(`done{status:"needs_review"}` + gate `plan_review` 的 resume 腿)。 */
|
|
59
|
+
respondPlanReview(input: {
|
|
60
|
+
taskId: string;
|
|
61
|
+
approved: boolean;
|
|
62
|
+
feedback?: string;
|
|
63
|
+
}): Promise<void>;
|
|
64
|
+
/** 后台子代的实时转录尾巴(查看态)。 */
|
|
65
|
+
tailSubagent(taskId: string): AsyncIterable<unknown>;
|
|
66
|
+
/** 给在跑的子代插话(steer)。 */
|
|
67
|
+
steerSubagent(input: {
|
|
68
|
+
taskId: string;
|
|
69
|
+
text: string;
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
/** 唤醒已停机的保留子代(design/144 `retainSubagentSessions` 的兑现面)。 */
|
|
72
|
+
wakeSubagent(input: {
|
|
73
|
+
taskId: string;
|
|
74
|
+
message: string;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
/** fleet 行帧流(常开;后台子代唯一在场的 lane)。 */
|
|
77
|
+
streamFleet(): AsyncIterable<unknown>;
|
|
78
|
+
/** 列会话(resume 选单)。 */
|
|
79
|
+
listSessions(input?: {
|
|
80
|
+
limit?: number;
|
|
81
|
+
}): Promise<unknown>;
|
|
82
|
+
/** 读一个会话的转录(桌面与 TUI 共享 `~/.claude/projects/**` 的先例,§8-11)。 */
|
|
83
|
+
readSession(sessionId: string): Promise<unknown>;
|
|
84
|
+
/** 引擎 caps(壳侧所有 `engineCapTrue` 门的数据源)。 */
|
|
85
|
+
capabilities(): Promise<unknown>;
|
|
86
|
+
/** 健康探测(🔴 语义差见 `probeHealth` 记账:SDK 要求 `status==='ok'`,壳现实现只要 `res.ok`)。 */
|
|
87
|
+
probeHealth(): Promise<{
|
|
88
|
+
ok: boolean;
|
|
89
|
+
version?: string;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* verb 覆盖率断言出口(§8-1 的另一半)。
|
|
94
|
+
*
|
|
95
|
+
* 宿主自检遍历本表,对 `required:true` 的逐条断言"我实现了"。不实现 ≠ 报错,是**那个面在该端是哑的**
|
|
96
|
+
* —— 与 `CHROME_ARMS` 同一套纪律:把义务做成数据,而不是散落在注释里。
|
|
97
|
+
*/
|
|
98
|
+
export declare const CLIENT_VERBS: readonly [{
|
|
99
|
+
readonly verb: "streamTask";
|
|
100
|
+
readonly required: true;
|
|
101
|
+
readonly duty: "发起 turn 并回 wire 帧流(会话运行时的入口)";
|
|
102
|
+
}, {
|
|
103
|
+
readonly verb: "cancelRun";
|
|
104
|
+
readonly required: true;
|
|
105
|
+
readonly duty: "取消在跑的 run(不实现 = Esc 只停渲染,引擎照跑)";
|
|
106
|
+
}, {
|
|
107
|
+
readonly verb: "respondApproval";
|
|
108
|
+
readonly required: true;
|
|
109
|
+
readonly duty: "工具审批;两元组 verbatim 回显(安全不变量)";
|
|
110
|
+
}, {
|
|
111
|
+
readonly verb: "respondQuestion";
|
|
112
|
+
readonly required: true;
|
|
113
|
+
readonly duty: "AskUserQuestion 应答(不实现 = 问了没法答,turn 挂死)";
|
|
114
|
+
}, {
|
|
115
|
+
readonly verb: "respondPlanReview";
|
|
116
|
+
readonly required: false;
|
|
117
|
+
readonly duty: "plan-review 决断(fail-soft:不实现只是少一张审批卡)";
|
|
118
|
+
}, {
|
|
119
|
+
readonly verb: "tailSubagent";
|
|
120
|
+
readonly required: false;
|
|
121
|
+
readonly duty: "后台子代查看态实时转录";
|
|
122
|
+
}, {
|
|
123
|
+
readonly verb: "steerSubagent";
|
|
124
|
+
readonly required: false;
|
|
125
|
+
readonly duty: "给在跑的子代插话";
|
|
126
|
+
}, {
|
|
127
|
+
readonly verb: "wakeSubagent";
|
|
128
|
+
readonly required: false;
|
|
129
|
+
readonly duty: "唤醒保留子代(SendMessage 语义)";
|
|
130
|
+
}, {
|
|
131
|
+
readonly verb: "streamFleet";
|
|
132
|
+
readonly required: false;
|
|
133
|
+
readonly duty: "fleet 行帧流(后台子代面板行的唯一来源)";
|
|
134
|
+
}, {
|
|
135
|
+
readonly verb: "listSessions";
|
|
136
|
+
readonly required: false;
|
|
137
|
+
readonly duty: "resume 选单";
|
|
138
|
+
}, {
|
|
139
|
+
readonly verb: "readSession";
|
|
140
|
+
readonly required: false;
|
|
141
|
+
readonly duty: "读历史转录";
|
|
142
|
+
}, {
|
|
143
|
+
readonly verb: "capabilities";
|
|
144
|
+
readonly required: true;
|
|
145
|
+
readonly duty: "引擎 caps(所有能力门的数据源;不实现 = 门全判假)";
|
|
146
|
+
}, {
|
|
147
|
+
readonly verb: "probeHealth";
|
|
148
|
+
readonly required: false;
|
|
149
|
+
readonly duty: "健康探测";
|
|
150
|
+
}];
|
|
151
|
+
type _MissingFromTable = Exclude<keyof ClientSliceLike, (typeof CLIENT_VERBS)[number]['verb']>;
|
|
152
|
+
type _ExtraInTable = Exclude<(typeof CLIENT_VERBS)[number]['verb'], keyof ClientSliceLike>;
|
|
153
|
+
type _MustBeNever<T extends never> = T;
|
|
154
|
+
export type _VerbTableClosure = [_MustBeNever<_MissingFromTable>, _MustBeNever<_ExtraInTable>];
|
|
155
|
+
/**
|
|
156
|
+
* 🔴 `probeHealth` 的**行为差记账**(B4,设计稿 §3 B4 第四件明确要求先保旧行为)。
|
|
157
|
+
*
|
|
158
|
+
* 壳里两处手拼 `/health`(`S/limitsWire.ts:225`、`S/detachWire.ts:82`)判的是 **`res.ok`**
|
|
159
|
+
* (HTTP 2xx 即算活);SDK 的 `probeHealth` 额外要求 **`body.status === 'ok'`**。
|
|
160
|
+
* 差异面:引擎起来了但自报 degraded(2xx + `status!=='ok'`)时 —— 手拼判**活**,SDK 判**死**。
|
|
161
|
+
*
|
|
162
|
+
* B4 的处置:**先保旧行为**(不换,两处仍判 `res.ok`),差异记在这里候裁。
|
|
163
|
+
* 换的话是行为改动,要按宪法三问单独立项:
|
|
164
|
+
* · 谁需要:degraded 引擎上 `--max-*` 版本门与 detach 头版本门本就该保守拒发 ⇒ SDK 语义更对;
|
|
165
|
+
* · 谁受害:自报 degraded 但功能其实在线的引擎,`-p` 会少发 detach 头(断连即杀 run)⇒ 真回归;
|
|
166
|
+
* · 补偿:换之前先量一次"degraded 但可用"在真部署里出不出现。
|
|
167
|
+
*/
|
|
168
|
+
export declare const PROBE_HEALTH_DIVERGENCE: string;
|
|
169
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clientSlice.ts — **verb 门面接口定型**(B4,设计稿 §8-1 裁决:「要,但只定接口不定实现」)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要有这一层
|
|
5
|
+
*
|
|
6
|
+
* §6.1.4 解包 CC 桌面端量到 **854 条 IPC 通道**(`ns_$_Service_$_method`),其中会话相关的
|
|
7
|
+
* `LocalSessions` 一族就有 212 个 verb。那一层客观存在而且很大 —— **不定接口,每端会各长一套**,
|
|
8
|
+
* 于是 HITL 权威登记、通知直结这些「必须只有一个落点」的东西会被重写三遍。
|
|
9
|
+
*
|
|
10
|
+
* ## 但它**只是接口**
|
|
11
|
+
*
|
|
12
|
+
* 🔴 本文件不导出任何 IPC / HTTP / SDK 实现,一行运行时代码都没有(编译产物只有类型)。
|
|
13
|
+
* 依据公理一:CC 的皮肤形状不进 wire ⇒ **IPC 的形状同样不该进 wire**。谁来实现:
|
|
14
|
+
* · TUI = 直接把 `@sema-agent/sdk` 的 AgentClient 方法接上;
|
|
15
|
+
* · web = BFF 转发(逐请求注入 principal);
|
|
16
|
+
* · 桌面 = 主进程 IPC(CC 同款 `ns_$_Service_$_method` 形)。
|
|
17
|
+
*
|
|
18
|
+
* ## 判据:什么进这个接口
|
|
19
|
+
*
|
|
20
|
+
* **会话运行时自己会调的 verb 才进**。纯 UI 触发、端各自决定的动作(开面板、切主题、选文件)
|
|
21
|
+
* 不进 —— 那些是端的事。今天这张表按 cli 现役会话链路归纳,`additive` 演进(只增不改),
|
|
22
|
+
* 与 `CHROME_ARMS` 一样做成可断言的数据出口。
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* verb 覆盖率断言出口(§8-1 的另一半)。
|
|
26
|
+
*
|
|
27
|
+
* 宿主自检遍历本表,对 `required:true` 的逐条断言"我实现了"。不实现 ≠ 报错,是**那个面在该端是哑的**
|
|
28
|
+
* —— 与 `CHROME_ARMS` 同一套纪律:把义务做成数据,而不是散落在注释里。
|
|
29
|
+
*/
|
|
30
|
+
// 🔴 **不要**给这个 const 加 `: readonly ClientVerbSpec[]` 注解 —— 注解会把 `verb` 拓宽成 `string`,
|
|
31
|
+
// 下面那道编译期双向闭合门就变成恒真的空转(B4 首写时正是这么写的,加注解后 `Exclude` 恒为 `string`
|
|
32
|
+
// ⇒ 门当场红且删表项也红,红得毫无判别力)。用 `as const satisfies` 既约束形状又保住字面量类型。
|
|
33
|
+
export const CLIENT_VERBS = [
|
|
34
|
+
{ verb: 'streamTask', required: true, duty: '发起 turn 并回 wire 帧流(会话运行时的入口)' },
|
|
35
|
+
{ verb: 'cancelRun', required: true, duty: '取消在跑的 run(不实现 = Esc 只停渲染,引擎照跑)' },
|
|
36
|
+
{ verb: 'respondApproval', required: true, duty: '工具审批;两元组 verbatim 回显(安全不变量)' },
|
|
37
|
+
{ verb: 'respondQuestion', required: true, duty: 'AskUserQuestion 应答(不实现 = 问了没法答,turn 挂死)' },
|
|
38
|
+
{ verb: 'respondPlanReview', required: false, duty: 'plan-review 决断(fail-soft:不实现只是少一张审批卡)' },
|
|
39
|
+
{ verb: 'tailSubagent', required: false, duty: '后台子代查看态实时转录' },
|
|
40
|
+
{ verb: 'steerSubagent', required: false, duty: '给在跑的子代插话' },
|
|
41
|
+
{ verb: 'wakeSubagent', required: false, duty: '唤醒保留子代(SendMessage 语义)' },
|
|
42
|
+
{ verb: 'streamFleet', required: false, duty: 'fleet 行帧流(后台子代面板行的唯一来源)' },
|
|
43
|
+
{ verb: 'listSessions', required: false, duty: 'resume 选单' },
|
|
44
|
+
{ verb: 'readSession', required: false, duty: '读历史转录' },
|
|
45
|
+
{ verb: 'capabilities', required: true, duty: '引擎 caps(所有能力门的数据源;不实现 = 门全判假)' },
|
|
46
|
+
{ verb: 'probeHealth', required: false, duty: '健康探测' },
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* 🔴 `probeHealth` 的**行为差记账**(B4,设计稿 §3 B4 第四件明确要求先保旧行为)。
|
|
50
|
+
*
|
|
51
|
+
* 壳里两处手拼 `/health`(`S/limitsWire.ts:225`、`S/detachWire.ts:82`)判的是 **`res.ok`**
|
|
52
|
+
* (HTTP 2xx 即算活);SDK 的 `probeHealth` 额外要求 **`body.status === 'ok'`**。
|
|
53
|
+
* 差异面:引擎起来了但自报 degraded(2xx + `status!=='ok'`)时 —— 手拼判**活**,SDK 判**死**。
|
|
54
|
+
*
|
|
55
|
+
* B4 的处置:**先保旧行为**(不换,两处仍判 `res.ok`),差异记在这里候裁。
|
|
56
|
+
* 换的话是行为改动,要按宪法三问单独立项:
|
|
57
|
+
* · 谁需要:degraded 引擎上 `--max-*` 版本门与 detach 头版本门本就该保守拒发 ⇒ SDK 语义更对;
|
|
58
|
+
* · 谁受害:自报 degraded 但功能其实在线的引擎,`-p` 会少发 detach 头(断连即杀 run)⇒ 真回归;
|
|
59
|
+
* · 补偿:换之前先量一次"degraded 但可用"在真部署里出不出现。
|
|
60
|
+
*/
|
|
61
|
+
export const PROBE_HEALTH_DIVERGENCE = 'probeHealth: SDK 要求 status==="ok",壳侧 limitsWire:225 / detachWire:82 手拼只要 res.ok。' +
|
|
62
|
+
'B4 保旧行为(res.ok),差异候裁 —— 换成 SDK 语义会让"自报 degraded 但可用"的引擎少发 detach 头。';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/diff/patch.ts — T20:**diff hunks 在客户端算**(core 已裁定归客户端,零上游依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须进本包:core 明确「引擎发 originalFile(客户端永远重建不出来的那一半),hunks 由客户端
|
|
5
|
+
* 算」—— 那么每个端(TUI/web/桌面)各算一遍就是三份实现三种 hunk;进包之后是**一份**。
|
|
6
|
+
* 搬自 cli `src/utils/diff.ts:81 getPatchFromContents` + `src/utils/file.ts:137
|
|
7
|
+
* convertLeadingTabsToSpaces`,**逐字**(含 & / $ 转义 token 与 CONTEXT_LINES/TIMEOUT 常量),
|
|
8
|
+
* 只丢掉宿主侧的 analytics / LOC 计数副作用(那两条在 cli 里也不在本函数内)。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 唯一的运行时依赖 `diff`(jsdiff 9,零传递依赖、纯 JS、浏览器可用)—— 本包第一个 runtime dep。
|
|
11
|
+
* 判据:它不是「顺手拉进来的 hub」,而是 T20 这条能力**本身**;不引它就只能把能力退回各端。
|
|
12
|
+
* portability 门的 ALLOWED_PACKAGES 因此显式登记 `diff` 一条(登记 = 可审计,不是放行一切)。
|
|
13
|
+
*/
|
|
14
|
+
import { type StructuredPatchHunk } from 'diff';
|
|
15
|
+
export declare const CONTEXT_LINES = 3;
|
|
16
|
+
export declare const DIFF_TIMEOUT_MS = 5000;
|
|
17
|
+
/**
|
|
18
|
+
* 渲染前把**行首** tab 折成两空格 —— CC 自己的 Edit 卡在 structuredPatch 之前做同一件事
|
|
19
|
+
* (FileEditTool/utils.ts),不做的话 tab 缩进文件两边算出来的 hunk 对不上。
|
|
20
|
+
*/
|
|
21
|
+
export declare function convertLeadingTabsToSpaces(content: string): string;
|
|
22
|
+
export declare function getPatchFromContents({ filePath, oldContent, newContent, ignoreWhitespace, singleHunk, }: {
|
|
23
|
+
filePath: string;
|
|
24
|
+
oldContent: string;
|
|
25
|
+
newContent: string;
|
|
26
|
+
ignoreWhitespace?: boolean;
|
|
27
|
+
singleHunk?: boolean;
|
|
28
|
+
}): StructuredPatchHunk[];
|
|
29
|
+
export type { StructuredPatchHunk };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/diff/patch.ts — T20:**diff hunks 在客户端算**(core 已裁定归客户端,零上游依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须进本包:core 明确「引擎发 originalFile(客户端永远重建不出来的那一半),hunks 由客户端
|
|
5
|
+
* 算」—— 那么每个端(TUI/web/桌面)各算一遍就是三份实现三种 hunk;进包之后是**一份**。
|
|
6
|
+
* 搬自 cli `src/utils/diff.ts:81 getPatchFromContents` + `src/utils/file.ts:137
|
|
7
|
+
* convertLeadingTabsToSpaces`,**逐字**(含 & / $ 转义 token 与 CONTEXT_LINES/TIMEOUT 常量),
|
|
8
|
+
* 只丢掉宿主侧的 analytics / LOC 计数副作用(那两条在 cli 里也不在本函数内)。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 唯一的运行时依赖 `diff`(jsdiff 9,零传递依赖、纯 JS、浏览器可用)—— 本包第一个 runtime dep。
|
|
11
|
+
* 判据:它不是「顺手拉进来的 hub」,而是 T20 这条能力**本身**;不引它就只能把能力退回各端。
|
|
12
|
+
* portability 门的 ALLOWED_PACKAGES 因此显式登记 `diff` 一条(登记 = 可审计,不是放行一切)。
|
|
13
|
+
*/
|
|
14
|
+
import { structuredPatch } from 'diff';
|
|
15
|
+
export const CONTEXT_LINES = 3;
|
|
16
|
+
export const DIFF_TIMEOUT_MS = 5_000;
|
|
17
|
+
// cli 原注:`&` 会让 diff 库犯迷糊,先换成 token、算完再换回来。`$` 同理(替换模式转义位)。
|
|
18
|
+
const AMPERSAND_TOKEN = '<<:AMPERSAND_TOKEN:>>';
|
|
19
|
+
const DOLLAR_TOKEN = '<<:DOLLAR_TOKEN:>>';
|
|
20
|
+
function escapeForDiff(s) {
|
|
21
|
+
return s.replaceAll('&', AMPERSAND_TOKEN).replaceAll('$', DOLLAR_TOKEN);
|
|
22
|
+
}
|
|
23
|
+
function unescapeFromDiff(s) {
|
|
24
|
+
return s.replaceAll(AMPERSAND_TOKEN, '&').replaceAll(DOLLAR_TOKEN, '$');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 渲染前把**行首** tab 折成两空格 —— CC 自己的 Edit 卡在 structuredPatch 之前做同一件事
|
|
28
|
+
* (FileEditTool/utils.ts),不做的话 tab 缩进文件两边算出来的 hunk 对不上。
|
|
29
|
+
*/
|
|
30
|
+
export function convertLeadingTabsToSpaces(content) {
|
|
31
|
+
// 无 tab 的常见情形直接短路(/gm 正则会逐行扫)。
|
|
32
|
+
if (!content.includes('\t'))
|
|
33
|
+
return content;
|
|
34
|
+
return content.replace(/^\t+/gm, _ => ' '.repeat(_.length));
|
|
35
|
+
}
|
|
36
|
+
export function getPatchFromContents({ filePath, oldContent, newContent, ignoreWhitespace = false, singleHunk = false, }) {
|
|
37
|
+
const result = structuredPatch(filePath, filePath, escapeForDiff(oldContent), escapeForDiff(newContent), undefined, undefined, {
|
|
38
|
+
ignoreWhitespace,
|
|
39
|
+
context: singleHunk ? 100_000 : CONTEXT_LINES,
|
|
40
|
+
timeout: DIFF_TIMEOUT_MS,
|
|
41
|
+
});
|
|
42
|
+
if (!result)
|
|
43
|
+
return [];
|
|
44
|
+
return result.hunks.map(_ => ({ ..._, lines: _.lines.map(unescapeFromDiff) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** 当前会话的 session 参数值;undefined = 两源都缺席(动态 import 未落地的头几毫秒)——
|
|
2
|
+
* 调用方此时不带参(legacy 面软拍放行;强制面 404 自回落,下次重试自然带上)。 */
|
|
3
|
+
export declare function engineSessionParam(): string | undefined;
|
|
4
|
+
/** spread 便捷形:`{ ...engineSessionParamSpread() }` → `{ session: '<sid>' }` 或 `{}`。 */
|
|
5
|
+
export declare function engineSessionParamSpread(): {
|
|
6
|
+
session?: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engineSessionParam — durable run 面 `?session=` 参数的统一派生([1501]§三,server 1.246 起
|
|
3
|
+
* legacy 八面软拍纪律:带了且不匹配=404,没带=warn-once 放行,二拍将把「没带」也 404;新面
|
|
4
|
+
* A/B(taskOutput/taskStop)与 subagentOutput 则出生即强制)。
|
|
5
|
+
*
|
|
6
|
+
* 取值语义 = run 绑定的会话(本壳所有 durable 动词的目标 run 都是本会话自有 run,见扩散面复审
|
|
7
|
+
* 镜头二 W-A:寻址 id 逐跳自有,T13 不变量保证会话 id turn 内不漂移)。优先级与
|
|
8
|
+
* fleetClient.effectiveScopeSessionId() / engineSubagentOutput 同源:引擎捕获值
|
|
9
|
+
* (getLiveSessionId,首轮 done 回填的权威值)优先,未捕获时退壳 boot-time getSessionId()
|
|
10
|
+
* (SB-5:同命名空间,引擎首轮认养壳自报 id)。动态 import 姿势同两处先例,避免把
|
|
11
|
+
* bootstrap/state 重图拖进静态依赖。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 [1501] 空串陷阱:`?session=`(空值)按「带了且不匹配」处理=404 —— 本 helper 恒不返回
|
|
14
|
+
* 空串:要么真值要么 undefined(调用方 spread 姿势 `...(s ? { session: s } : {})`)。
|
|
15
|
+
*/
|
|
16
|
+
import { getLiveSessionId } from './liveSessionStore.js';
|
|
17
|
+
let shellSessionIdFn;
|
|
18
|
+
void import('../bootstrap/state.js')
|
|
19
|
+
.then(m => {
|
|
20
|
+
shellSessionIdFn = m.getSessionId;
|
|
21
|
+
})
|
|
22
|
+
.catch(() => {
|
|
23
|
+
// 极端环境缺模块:退化为只用 getLiveSessionId(),与 fleetClient/engineSubagentOutput 同款容错
|
|
24
|
+
});
|
|
25
|
+
/** 当前会话的 session 参数值;undefined = 两源都缺席(动态 import 未落地的头几毫秒)——
|
|
26
|
+
* 调用方此时不带参(legacy 面软拍放行;强制面 404 自回落,下次重试自然带上)。 */
|
|
27
|
+
export function engineSessionParam() {
|
|
28
|
+
const sid = getLiveSessionId() ?? shellSessionIdFn?.();
|
|
29
|
+
return typeof sid === 'string' && sid.length > 0 ? sid : undefined;
|
|
30
|
+
}
|
|
31
|
+
/** spread 便捷形:`{ ...engineSessionParamSpread() }` → `{ session: '<sid>' }` 或 `{}`。 */
|
|
32
|
+
export function engineSessionParamSpread() {
|
|
33
|
+
const s = engineSessionParam();
|
|
34
|
+
return s ? { session: s } : {};
|
|
35
|
+
}
|
package/dist/engineWireSdk.d.ts
CHANGED
|
@@ -27,7 +27,31 @@ export type WireAuthToken = string | {
|
|
|
27
27
|
};
|
|
28
28
|
/** loopback 判定(engineTarget.isLoopbackEngineUrl 逐字镜像;解析失败=非 loopback)。 */
|
|
29
29
|
export declare function isLoopbackWireUrl(url: string): boolean;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* §8-2 定谳的**唯一**凭证解析口(B4 收编)—— 11 处构造点的公共分母。
|
|
32
|
+
*
|
|
33
|
+
* 背景(设计稿 §8-2 + B4 实测):壳里今天是 **2 份三态解析**(`engineTarget.resolveLiveAuthToken`
|
|
34
|
+
* 产品半场 + 本包 `wireAuthTokenFor` 纯半场,后者头注自述是前者的「逐字镜像」⇒ 两份必然漂移)
|
|
35
|
+
* 外加 **11 处 `new AgentClient` 直造**,其中 7 处根本不走任何解析口,直接手写
|
|
36
|
+
* `token ?? { mode:'loopback-unauthed' }` —— 那一形**丢了 'anon' 那一态**:非 loopback 且无 token
|
|
37
|
+
* 时它给的是 loopback-unauthed(出站零 Authorization 头),而三态解析给 'anon'。对 fail-closed
|
|
38
|
+
* worker 两者都是 401,但对 **owner-gated 路由**不是同一回事,所以这是真分歧不是等价写法。
|
|
39
|
+
*
|
|
40
|
+
* 🔴 **env 缺省留在端上**:`resolveLiveAuthToken` 的第二参缺省是 `process.env.SEMA_LIVE_TOKEN`,
|
|
41
|
+
* 那一半**绝不进包**(本包零 `process.env` —— portability 门盯着)。壳侧 shim 保留缺省参即可:
|
|
42
|
+
* `resolveLiveAuthToken(u, t = process.env.SEMA_LIVE_TOKEN) => resolveWireAuth(u, t)`。
|
|
43
|
+
*
|
|
44
|
+
* 三态(#118):token 有 ⇒ 原样 Bearer;无 token ∧ loopback ⇒ `{mode:'loopback-unauthed'}`
|
|
45
|
+
* (出站零 Authorization 头,绝不伪造);无 token ∧ 非 loopback ⇒ `'anon'`(fail-closed 换诚实 401)。
|
|
46
|
+
* `baseUrl` 允许 undefined(= `resolveLiveAuthToken` 的签名超集,本地自 spawn 态)⇒ 'anon'。
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveWireAuth(baseUrl: string | undefined, token?: string): WireAuthToken;
|
|
49
|
+
/**
|
|
50
|
+
* #118 三态凭证解析(旧名,B2 引入)。
|
|
51
|
+
* B4 起是 `resolveWireAuth` 的**别名**(baseUrl 必填的窄签名);两者行为逐字相同,保留是为了
|
|
52
|
+
* 壳侧既有 import 零改动 —— 不设代际让位([clean-cut-no-legacy-compat] 只约束**行为面**硬切,
|
|
53
|
+
* 同一函数的两个名字不是两套行为)。新代码一律用 `resolveWireAuth`。
|
|
54
|
+
*/
|
|
31
55
|
export declare function wireAuthTokenFor(baseUrl: string, token?: string): WireAuthToken;
|
|
32
56
|
export type EngineWireClientConfig = {
|
|
33
57
|
baseUrl: string;
|
package/dist/engineWireSdk.js
CHANGED
|
@@ -32,14 +32,40 @@ export function isLoopbackWireUrl(url) {
|
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* §8-2 定谳的**唯一**凭证解析口(B4 收编)—— 11 处构造点的公共分母。
|
|
37
|
+
*
|
|
38
|
+
* 背景(设计稿 §8-2 + B4 实测):壳里今天是 **2 份三态解析**(`engineTarget.resolveLiveAuthToken`
|
|
39
|
+
* 产品半场 + 本包 `wireAuthTokenFor` 纯半场,后者头注自述是前者的「逐字镜像」⇒ 两份必然漂移)
|
|
40
|
+
* 外加 **11 处 `new AgentClient` 直造**,其中 7 处根本不走任何解析口,直接手写
|
|
41
|
+
* `token ?? { mode:'loopback-unauthed' }` —— 那一形**丢了 'anon' 那一态**:非 loopback 且无 token
|
|
42
|
+
* 时它给的是 loopback-unauthed(出站零 Authorization 头),而三态解析给 'anon'。对 fail-closed
|
|
43
|
+
* worker 两者都是 401,但对 **owner-gated 路由**不是同一回事,所以这是真分歧不是等价写法。
|
|
44
|
+
*
|
|
45
|
+
* 🔴 **env 缺省留在端上**:`resolveLiveAuthToken` 的第二参缺省是 `process.env.SEMA_LIVE_TOKEN`,
|
|
46
|
+
* 那一半**绝不进包**(本包零 `process.env` —— portability 门盯着)。壳侧 shim 保留缺省参即可:
|
|
47
|
+
* `resolveLiveAuthToken(u, t = process.env.SEMA_LIVE_TOKEN) => resolveWireAuth(u, t)`。
|
|
48
|
+
*
|
|
49
|
+
* 三态(#118):token 有 ⇒ 原样 Bearer;无 token ∧ loopback ⇒ `{mode:'loopback-unauthed'}`
|
|
50
|
+
* (出站零 Authorization 头,绝不伪造);无 token ∧ 非 loopback ⇒ `'anon'`(fail-closed 换诚实 401)。
|
|
51
|
+
* `baseUrl` 允许 undefined(= `resolveLiveAuthToken` 的签名超集,本地自 spawn 态)⇒ 'anon'。
|
|
52
|
+
*/
|
|
53
|
+
export function resolveWireAuth(baseUrl, token) {
|
|
37
54
|
if (token)
|
|
38
55
|
return token;
|
|
39
|
-
if (isLoopbackWireUrl(baseUrl))
|
|
56
|
+
if (baseUrl !== undefined && isLoopbackWireUrl(baseUrl))
|
|
40
57
|
return { mode: 'loopback-unauthed' };
|
|
41
58
|
return 'anon';
|
|
42
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* #118 三态凭证解析(旧名,B2 引入)。
|
|
62
|
+
* B4 起是 `resolveWireAuth` 的**别名**(baseUrl 必填的窄签名);两者行为逐字相同,保留是为了
|
|
63
|
+
* 壳侧既有 import 零改动 —— 不设代际让位([clean-cut-no-legacy-compat] 只约束**行为面**硬切,
|
|
64
|
+
* 同一函数的两个名字不是两套行为)。新代码一律用 `resolveWireAuth`。
|
|
65
|
+
*/
|
|
66
|
+
export function wireAuthTokenFor(baseUrl, token) {
|
|
67
|
+
return resolveWireAuth(baseUrl, token);
|
|
68
|
+
}
|
|
43
69
|
/**
|
|
44
70
|
* 构造一个引擎 wire 用 AgentClient。null = 构造失败(SDK fail-closed 拒绝——不该发生:
|
|
45
71
|
* wireAuthTokenFor 恒给合法形;防御性 fail-soft,调用方按各自离线/探测失败路径处理)。
|
|
@@ -48,7 +74,7 @@ export function makeEngineWireClient(cfg) {
|
|
|
48
74
|
try {
|
|
49
75
|
return new AgentClient({
|
|
50
76
|
baseUrl: cfg.baseUrl,
|
|
51
|
-
authToken:
|
|
77
|
+
authToken: resolveWireAuth(cfg.baseUrl, cfg.token),
|
|
52
78
|
principal: cfg.principal ?? 'anon:shell-live',
|
|
53
79
|
...(cfg.timeoutMs !== undefined ? { timeoutMs: cfg.timeoutMs } : {}),
|
|
54
80
|
maxRetries: cfg.maxRetries ?? 0,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/sema/finalVerifyWire.ts — headless `-p` finalVerification wire (TB2.0 反馈批 A · P1-1, 2026-07-15):
|
|
3
|
+
* stamp the ENGINE-EXISTING `TaskRequest.finalVerification` field on the headless `-p` path, DEFAULT ON.
|
|
4
|
+
*
|
|
5
|
+
* WHY: TB2.0 无人值守回归里 14/19 题 agent 自认完成判 0(写完不验/即兴自测即收尾)。引擎的
|
|
6
|
+
* finalVerification stop-gate(core runtask.js:1429-1493 —— 最多追加 2 条 '[final verification]'
|
|
7
|
+
* system-reminder,wroteThisRun && 非 structured-output && 未贴 maxTurns 才触发)+ server wire
|
|
8
|
+
* (http/server.js:3711 校验 / main.js:1383 转发)早已在线,壳侧从未 stamp——本模块补上这最后一段 wire。
|
|
9
|
+
*
|
|
10
|
+
* PRECEDENCE(off 优先,任何一条命中即不 stamp;否则默认 true):
|
|
11
|
+
* a) `--no-final-verify` flag → off(一键回上游 2.1.207 严格对齐态:上游
|
|
12
|
+
* `-p` 全文 0 处 final verification,默认开属超集;上游对应扩展点是 Stop hook)。
|
|
13
|
+
* b) `SEMA_HEADLESS_FINAL_VERIFY` env('false'/'0') → off。settings.json `env` 块 → 1a-envseed →
|
|
14
|
+
* process.env,其它 wire 配置(SEMA_HEADLESS_SCENARIO 等)同一车道。
|
|
15
|
+
* c) 用户配置了 Stop hook(settings hooks.Stop,经 hooksForWire 投影)→ off【对抗轮硬性 amendment】:
|
|
16
|
+
* 引擎 finalVerification 分支在 Stop hook 查询之前 return(runtask.js:1430-1443),默认开会把
|
|
17
|
+
* 用户 Stop hook 延迟至多 2 turn——对上游迁移来的 hook 用户是真行为变化,故有 Stop hook 即让位。
|
|
18
|
+
* d) 都没有 → true(TB 默认态吃满终验闸杠杆)。
|
|
19
|
+
*
|
|
20
|
+
* LIVE-GATE:与 scenarioWire 同款——调用点(seamQueryEngine.ask)以 SEMA_LIVE_BASEURL 为门,
|
|
21
|
+
* mock/pty fixture 车道永不 stamp 该字段(离线投影字节等价)。
|
|
22
|
+
*
|
|
23
|
+
* 语义澄清(分诊报告):finalVerification 是同会话 stop-gate 追加,不是独立上下文 verifier;
|
|
24
|
+
* 纯读任务(wroteThisRun gate)与 structured-output 任务被 core 自身豁免,回归面天然收窄。
|
|
25
|
+
*/
|
|
26
|
+
import type { SemaSettings } from '@sema-agent/sdk';
|
|
27
|
+
import { type EnvLike } from './hostEnv.js';
|
|
28
|
+
/**
|
|
29
|
+
* B5:`hooksWireCaps` 的宿主耦合半场(settings 三层合并 + trust 门)还没搬,但这个**纯类型**
|
|
30
|
+
* 先落本包 —— 它就是 SDK 的 `settings.hooks`,与 hooksWireCaps 无耦合。
|
|
31
|
+
* 🔴 hooksWireCaps 后续进包时 **import 本条**,别再定义第二份(两份类型 = 漂移温床)。
|
|
32
|
+
*/
|
|
33
|
+
export type WireHooksConfig = NonNullable<SemaSettings['hooks']>;
|
|
34
|
+
/** Settings-lane env knob(settings.json `env` 块 → 1a-envseed → process.env)。'false'/'0' = off。 */
|
|
35
|
+
export declare const HEADLESS_FINAL_VERIFY_ENV = "SEMA_HEADLESS_FINAL_VERIFY";
|
|
36
|
+
/**
|
|
37
|
+
* Parse `--no-final-verify` out of an argv slice(everything before a bare `--` — positionals are
|
|
38
|
+
* never flags,scenarioWire 同款纪律)。Boolean flag,无值形态,重复无害。
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseNoFinalVerifyArgv(argv: string[]): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The settings-lane off-switch:`SEMA_HEADLESS_FINAL_VERIFY` = 'false' or '0'(case-insensitive)
|
|
43
|
+
* disables the stamp。其它任何值(含空串/'true'/typo)= 默认态 on——fail-soft:一个 settings typo
|
|
44
|
+
* 只会落回默认开,绝不 brick 任何 run(scenarioWire 的 settings-lane 纪律)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function headlessFinalVerifyDisabledByEnv(env?: EnvLike): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* 用户 Stop hook 在场判定:hooksForWire() 投影(policy→user→project→local 合并、trust/managed 门后)
|
|
49
|
+
* 里 `Stop` 事件带 ≥1 个 matcher 组即认定。判定输入用 wire 投影而非裸 settings——被 trust/
|
|
50
|
+
* disableAllHooks 门掉的 Stop hook 引擎侧同样不会 fire,不构成让位理由。
|
|
51
|
+
*/
|
|
52
|
+
export declare function hasUserStopHook(wireHooks: WireHooksConfig | undefined): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the finalVerification stamp for a headless `-p` submit:默认 true;
|
|
55
|
+
* `--no-final-verify` flag / SEMA_HEADLESS_FINAL_VERIFY=false|0 / 用户 Stop hook 在场 → false。
|
|
56
|
+
* 调用点必须 live-gate(SEMA_LIVE_BASEURL)——mock/offline 形状不变。
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveHeadlessFinalVerify(argv: string[], env?: EnvLike, wireHooks?: WireHooksConfig): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* #106 裁 B(clay 2026-07-26):让位维持 + 诚实告知。detail 形暴露 off 原因供调用点分诊 ——
|
|
61
|
+
* flag/env 关闭是用户显式意愿(不聒噪);`stop-hook` 让位是用户**没显式要求**的关闭
|
|
62
|
+
* (Stop hook 可能来自 /goal 自注册或 settings hook),必须告知(修前静默,用户不知道自己
|
|
63
|
+
* 失去了终验)。方向注记:finalVerification 后续默认关(CC Stop hook 机制同,不叠超集)——
|
|
64
|
+
* 见记忆 final-verification-direction;届时本函数的默认臂翻转,detail 形不动。
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveHeadlessFinalVerifyDetail(argv: string[], env?: EnvLike, wireHooks?: WireHooksConfig): {
|
|
67
|
+
on: boolean;
|
|
68
|
+
offReason?: 'flag' | 'env' | 'stop-hook';
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* stop-hook 让位的告知文案(单一措辞源;stderr 一行)。措辞纪律与 hook_notice 同族:
|
|
72
|
+
* 「让位/接管」向,绝不写「守卫失效」向 —— 后者会让用户以为链路坏了。
|
|
73
|
+
*/
|
|
74
|
+
export declare const FINAL_VERIFY_YIELD_NOTICE = "final verification is off for this run \u2014 a Stop hook is active and takes over end-of-turn gating (it would otherwise be delayed by up to 2 turns)";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { hostEnv } from './hostEnv.js';
|
|
2
|
+
/** Settings-lane env knob(settings.json `env` 块 → 1a-envseed → process.env)。'false'/'0' = off。 */
|
|
3
|
+
export const HEADLESS_FINAL_VERIFY_ENV = 'SEMA_HEADLESS_FINAL_VERIFY';
|
|
4
|
+
/**
|
|
5
|
+
* Parse `--no-final-verify` out of an argv slice(everything before a bare `--` — positionals are
|
|
6
|
+
* never flags,scenarioWire 同款纪律)。Boolean flag,无值形态,重复无害。
|
|
7
|
+
*/
|
|
8
|
+
export function parseNoFinalVerifyArgv(argv) {
|
|
9
|
+
for (const a of argv) {
|
|
10
|
+
if (a === '--')
|
|
11
|
+
break; // positionals — never flags
|
|
12
|
+
if (a === '--no-final-verify')
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The settings-lane off-switch:`SEMA_HEADLESS_FINAL_VERIFY` = 'false' or '0'(case-insensitive)
|
|
19
|
+
* disables the stamp。其它任何值(含空串/'true'/typo)= 默认态 on——fail-soft:一个 settings typo
|
|
20
|
+
* 只会落回默认开,绝不 brick 任何 run(scenarioWire 的 settings-lane 纪律)。
|
|
21
|
+
*/
|
|
22
|
+
export function headlessFinalVerifyDisabledByEnv(env = hostEnv()) {
|
|
23
|
+
const v = env[HEADLESS_FINAL_VERIFY_ENV]?.trim().toLowerCase();
|
|
24
|
+
return v === 'false' || v === '0';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 用户 Stop hook 在场判定:hooksForWire() 投影(policy→user→project→local 合并、trust/managed 门后)
|
|
28
|
+
* 里 `Stop` 事件带 ≥1 个 matcher 组即认定。判定输入用 wire 投影而非裸 settings——被 trust/
|
|
29
|
+
* disableAllHooks 门掉的 Stop hook 引擎侧同样不会 fire,不构成让位理由。
|
|
30
|
+
*/
|
|
31
|
+
export function hasUserStopHook(wireHooks) {
|
|
32
|
+
const groups = wireHooks?.['Stop'];
|
|
33
|
+
return Array.isArray(groups) && groups.length > 0;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the finalVerification stamp for a headless `-p` submit:默认 true;
|
|
37
|
+
* `--no-final-verify` flag / SEMA_HEADLESS_FINAL_VERIFY=false|0 / 用户 Stop hook 在场 → false。
|
|
38
|
+
* 调用点必须 live-gate(SEMA_LIVE_BASEURL)——mock/offline 形状不变。
|
|
39
|
+
*/
|
|
40
|
+
export function resolveHeadlessFinalVerify(argv, env = hostEnv(), wireHooks) {
|
|
41
|
+
return resolveHeadlessFinalVerifyDetail(argv, env, wireHooks).on;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* #106 裁 B(clay 2026-07-26):让位维持 + 诚实告知。detail 形暴露 off 原因供调用点分诊 ——
|
|
45
|
+
* flag/env 关闭是用户显式意愿(不聒噪);`stop-hook` 让位是用户**没显式要求**的关闭
|
|
46
|
+
* (Stop hook 可能来自 /goal 自注册或 settings hook),必须告知(修前静默,用户不知道自己
|
|
47
|
+
* 失去了终验)。方向注记:finalVerification 后续默认关(CC Stop hook 机制同,不叠超集)——
|
|
48
|
+
* 见记忆 final-verification-direction;届时本函数的默认臂翻转,detail 形不动。
|
|
49
|
+
*/
|
|
50
|
+
export function resolveHeadlessFinalVerifyDetail(argv, env = hostEnv(), wireHooks) {
|
|
51
|
+
if (parseNoFinalVerifyArgv(argv))
|
|
52
|
+
return { on: false, offReason: 'flag' };
|
|
53
|
+
if (headlessFinalVerifyDisabledByEnv(env))
|
|
54
|
+
return { on: false, offReason: 'env' };
|
|
55
|
+
if (hasUserStopHook(wireHooks))
|
|
56
|
+
return { on: false, offReason: 'stop-hook' };
|
|
57
|
+
return { on: true };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* stop-hook 让位的告知文案(单一措辞源;stderr 一行)。措辞纪律与 hook_notice 同族:
|
|
61
|
+
* 「让位/接管」向,绝不写「守卫失效」向 —— 后者会让用户以为链路坏了。
|
|
62
|
+
*/
|
|
63
|
+
export const FINAL_VERIFY_YIELD_NOTICE = 'final verification is off for this run — a Stop hook is active and takes over end-of-turn gating (it would otherwise be delayed by up to 2 turns)';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/sema/headlessPermissionModeWire.ts — SEMA_HEADLESS_PERMISSION_MODE 部署旋钮
|
|
3
|
+
* (clay 拍板 2026-07-16:TB/bench 场景 headless `-p` 统一垫 bypassPermissions,对齐 CC bench 实跑的
|
|
4
|
+
* --dangerously-skip-permissions 姿势;落地形态 = 环境变量部署旋钮,产品缺省语义不动)。
|
|
5
|
+
*
|
|
6
|
+
* gap-check 现状(2026-07-16,db3c72f):
|
|
7
|
+
* · `--permission-mode <mode>` CLI flag 存在(main.tsx:1042,choices = PERMISSION_MODES + 'manual'),
|
|
8
|
+
* `--dangerously-skip-permissions` 亦在;但两者在 headless `-p` 车道【都不进 TaskRequest】——
|
|
9
|
+
* seamQueryEngine.ask 自建请求从不 stamp permissionMode(交互 REPL 走 seamQuery →
|
|
10
|
+
* permissionWireCaps.permissionModeField,print/headless 从不 stamp = server absent-mode)。
|
|
11
|
+
* · 即:今天 `-p` 的权限面全靠 server 对 absent-mode 的缺省解释([830]① server 1.191 五模式解释层,
|
|
12
|
+
* absent 不挂门)。core [885] 残余建议:显式垫,防未来缺省变化让 TB 静默变盘。
|
|
13
|
+
*
|
|
14
|
+
* 语义(件2 拍板逐字):
|
|
15
|
+
* 优先级:CLI flag 显式值 > SEMA_HEADLESS_PERMISSION_MODE env > 现状 absent-mode(env 是缺省态的
|
|
16
|
+
* 垫层,flag 恒赢——flag 在场时 env 完全不看,即使 flag 值不在 wire 词表内)。
|
|
17
|
+
* 值域 = 壳既有 wire permissionMode 词表(permissionWireCaps.PermissionMode:default / plan /
|
|
18
|
+
* acceptEdits / bypassPermissions / auto——分类器批3 [907] server 五模式表,server ≥1.209 认
|
|
19
|
+
* "auto")。非法 env 值 ⇒ 一次性 warn + 回落现状,绝不 fail 启动。
|
|
20
|
+
* stamp 规则:
|
|
21
|
+
* · flag 来源:非 default 的 wire 词直接 stamp(auto 自分类器批3 起入表);`default` 不 stamp
|
|
22
|
+
* (= 今天 flag 的实际效果,零行为变化);wire 词表外的 flag 值(dontAsk/manual…是壳内部
|
|
23
|
+
* 模式,server 不认)不 stamp。
|
|
24
|
+
* · env 来源:五词全部显式 stamp(含 `default`——env 是"显式垫层"旋钮,设了就是要显式语义,
|
|
25
|
+
* 不再依赖 server absent-mode 缺省解释;server 1.191 对显式 "default" 走 manual ask 门解释)。
|
|
26
|
+
* 车道纪律:本模块只被 seamQueryEngine.ask(headless `-p`)import;交互 REPL(seamQuery)一根毛
|
|
27
|
+
* 不动——单测有 source-level lane proof(detachWire 同款)。
|
|
28
|
+
*/
|
|
29
|
+
import { type PermissionMode, type PermissionModeField } from './permissionWireCaps.js';
|
|
30
|
+
import { type EnvLike } from './hostEnv.js';
|
|
31
|
+
/** 部署旋钮 env 键名(settings.json env 块 → 1a-envseed → process.env 同车道)。 */
|
|
32
|
+
export declare const HEADLESS_PERMISSION_MODE_ENV = "SEMA_HEADLESS_PERMISSION_MODE";
|
|
33
|
+
/** wire 词表(permissionWireCaps.PermissionMode 的运行时镜像;大小写敏感——与 CC flag choices 一致)。
|
|
34
|
+
* 'auto'(分类器批3,[907]):server ≥1.209 五模式表认;老 server coerce 未知词 → "default" 只紧不松。 */
|
|
35
|
+
export declare const HEADLESS_PERMISSION_WIRE_MODES: readonly PermissionMode[];
|
|
36
|
+
export type HeadlessPermissionResolution = {
|
|
37
|
+
/** 直接展开进 TaskRequest 的字段({} = 不 stamp,现状 absent-mode)。 */
|
|
38
|
+
fields: PermissionModeField;
|
|
39
|
+
source: 'flag' | 'env' | 'absent';
|
|
40
|
+
/** 非法 env 值的告警文案(调用层一次性打 stderr)。 */
|
|
41
|
+
warning?: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* argv 里的 `--permission-mode <v>` / `--permission-mode=<v>`(后者/后出现者赢,commander 同语义)与
|
|
45
|
+
* `--dangerously-skip-permissions`(= bypassPermissions;`--permission-mode` 在场时让位)。
|
|
46
|
+
* 返回 undefined = flag 车道未显式表态。
|
|
47
|
+
*/
|
|
48
|
+
export declare function permissionModeFromArgv(argv: readonly string[]): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* 解析 headless `-p` 的 permissionMode stamp。纯函数除告警 latch(同值只警一次)。
|
|
51
|
+
* 调用方(seamQueryEngine.ask)live-gated:mock/pty 车道永不经过这里。
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveHeadlessPermissionMode(argv: readonly string[], env?: EnvLike): HeadlessPermissionResolution;
|
|
54
|
+
/** 测试钩子:重置一次性告警 latch。 */
|
|
55
|
+
export declare function _resetHeadlessPermissionModeWireForTest(): void;
|