@sema-agent/client-core 0.3.0 → 0.4.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 (56) hide show
  1. package/README.md +23 -1
  2. package/dist/agentsWireCaps.d.ts +112 -0
  3. package/dist/agentsWireCaps.js +319 -0
  4. package/dist/attachmentsWireCaps.d.ts +47 -0
  5. package/dist/attachmentsWireCaps.js +46 -0
  6. package/dist/classifierVerdictWire.d.ts +45 -0
  7. package/dist/classifierVerdictWire.js +76 -0
  8. package/dist/clientContextWireCaps.d.ts +46 -0
  9. package/dist/clientContextWireCaps.js +48 -0
  10. package/dist/cloudConfigWireCaps.d.ts +110 -0
  11. package/dist/cloudConfigWireCaps.js +228 -0
  12. package/dist/controlRouter.d.ts +191 -0
  13. package/dist/controlRouter.js +244 -0
  14. package/dist/effortWire.d.ts +34 -0
  15. package/dist/effortWire.js +38 -0
  16. package/dist/engineWireSdk.d.ts +49 -0
  17. package/dist/engineWireSdk.js +61 -0
  18. package/dist/forkWireCaps.d.ts +25 -0
  19. package/dist/forkWireCaps.js +42 -0
  20. package/dist/hostEnv.d.ts +16 -0
  21. package/dist/hostEnv.js +17 -0
  22. package/dist/imagesWireCaps.d.ts +28 -0
  23. package/dist/imagesWireCaps.js +49 -0
  24. package/dist/index.d.ts +35 -0
  25. package/dist/index.js +41 -0
  26. package/dist/liveModelCatalog.d.ts +62 -0
  27. package/dist/liveModelCatalog.js +79 -0
  28. package/dist/mcpWireCaps.d.ts +41 -0
  29. package/dist/mcpWireCaps.js +51 -0
  30. package/dist/modelBudgetRule.d.ts +44 -0
  31. package/dist/modelBudgetRule.js +73 -0
  32. package/dist/modelWireCaps.d.ts +17 -0
  33. package/dist/modelWireCaps.js +16 -0
  34. package/dist/notifications.d.ts +150 -4
  35. package/dist/notifications.js +472 -0
  36. package/dist/permissionWireCaps.d.ts +37 -0
  37. package/dist/permissionWireCaps.js +46 -0
  38. package/dist/promptProfileWireCaps.d.ts +17 -0
  39. package/dist/promptProfileWireCaps.js +20 -0
  40. package/dist/retainBackgroundWireCaps.d.ts +50 -0
  41. package/dist/retainBackgroundWireCaps.js +58 -0
  42. package/dist/rewindWireCaps.d.ts +36 -0
  43. package/dist/rewindWireCaps.js +41 -0
  44. package/dist/selfOrchestrationWireCaps.d.ts +40 -0
  45. package/dist/selfOrchestrationWireCaps.js +48 -0
  46. package/dist/sessionModelLatch.d.ts +35 -0
  47. package/dist/sessionModelLatch.js +63 -0
  48. package/dist/skillsWireCaps.d.ts +47 -0
  49. package/dist/skillsWireCaps.js +41 -0
  50. package/dist/sseIdleTriage.d.ts +94 -0
  51. package/dist/sseIdleTriage.js +142 -0
  52. package/dist/ultracodeWireCaps.d.ts +90 -0
  53. package/dist/ultracodeWireCaps.js +159 -0
  54. package/dist/webSearchWireCaps.d.ts +54 -0
  55. package/dist/webSearchWireCaps.js +76 -0
  56. package/package.json +1 -1
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/permissionWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/permissionWireCaps.ts — PURE CC permission-mode INTENT projection (R4, revised per the two-axis
6
+ * seamless principle). seamQuery imports it.
7
+ *
8
+ * 🔴 Design (memory seamless-two-axis-wire-principle): the frontend carries the RAW permission-mode INTENT; the
9
+ * SERVICE interprets it (axis-aware, tighten-only). We do NOT pre-interpret `plan → enablePlanMode+handsReadOnly`
10
+ * on the client — that bakes policy into one frontend and would drift between the TUI shell and the web portal.
11
+ * Instead we stamp `permissionMode` and let the service own the interpretation (plan ⇒ present_plan + read-only;
12
+ * acceptEdits/bypassPermissions ⇒ loosening, coerced to default engine-side, enforced client-side). This keeps the
13
+ * shell a thin per-turn intent projector that's identical across TOC-local / TOB-cloud / centralized-session axes.
14
+ *
15
+ * `auto`(分类器批3,黑板 [907] server 五模式表):server ≥1.209 coercePermissionMode 认 "auto"
16
+ * (解释 = default 同款 fs 写 ask 门;存活 ask 由引擎侧 auto-mode 分类器先行裁决——武装 AND 门
17
+ * RuntimeCaps.autoMode ∧ RunnerDeps.autoMode,缺一静默回 ask,fail-closed)。壳只透传意图,分类器
18
+ * 规则/权益都在装配面,非 TaskSpec 字段。版本安全:老 server coerce 未知非空词 → "default"
19
+ * (manual ask 门)= 只紧不松。
20
+ */
21
+ export type PermissionMode = 'default' | 'plan' | 'acceptEdits' | 'bypassPermissions' | 'auto';
22
+ export interface PermissionModeField {
23
+ permissionMode?: PermissionMode;
24
+ }
25
+ /**
26
+ * Project the client's permission mode → the wire intent. Carries any KNOWN non-`default` mode verbatim (the
27
+ * service interprets); `default`/unknown/undefined ⇒ no stamp (the deployment baseline governs). No policy baked
28
+ * here — relevance + tighten-only governance is the service's job.
29
+ *
30
+ * `stampDefault`([830]① server 1.191 五模式解释层):server 侧 absent-mode 不挂门(存量 headless
31
+ * 零回归),manual(=CC default)要挂 fs 写 ask 门必须显式 stamp `permissionMode:"default"`。只有
32
+ * 三选卡真能弹出来的调用方(交互 REPL,leader ToolUseConfirm 队列已注册)才传 true —— print/headless
33
+ * 不 stamp;老 server 对 "default" 走 basePolicy(与 absent 等价),version-safe。
34
+ */
35
+ export declare function permissionModeField(mode: string | undefined, opts?: {
36
+ stampDefault?: boolean;
37
+ }): PermissionModeField;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/permissionWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/permissionWireCaps.ts — PURE CC permission-mode INTENT projection (R4, revised per the two-axis
6
+ * seamless principle). seamQuery imports it.
7
+ *
8
+ * 🔴 Design (memory seamless-two-axis-wire-principle): the frontend carries the RAW permission-mode INTENT; the
9
+ * SERVICE interprets it (axis-aware, tighten-only). We do NOT pre-interpret `plan → enablePlanMode+handsReadOnly`
10
+ * on the client — that bakes policy into one frontend and would drift between the TUI shell and the web portal.
11
+ * Instead we stamp `permissionMode` and let the service own the interpretation (plan ⇒ present_plan + read-only;
12
+ * acceptEdits/bypassPermissions ⇒ loosening, coerced to default engine-side, enforced client-side). This keeps the
13
+ * shell a thin per-turn intent projector that's identical across TOC-local / TOB-cloud / centralized-session axes.
14
+ *
15
+ * `auto`(分类器批3,黑板 [907] server 五模式表):server ≥1.209 coercePermissionMode 认 "auto"
16
+ * (解释 = default 同款 fs 写 ask 门;存活 ask 由引擎侧 auto-mode 分类器先行裁决——武装 AND 门
17
+ * RuntimeCaps.autoMode ∧ RunnerDeps.autoMode,缺一静默回 ask,fail-closed)。壳只透传意图,分类器
18
+ * 规则/权益都在装配面,非 TaskSpec 字段。版本安全:老 server coerce 未知非空词 → "default"
19
+ * (manual ask 门)= 只紧不松。
20
+ */
21
+ const KNOWN = new Set([
22
+ 'default',
23
+ 'plan',
24
+ 'acceptEdits',
25
+ 'bypassPermissions',
26
+ 'auto',
27
+ ]);
28
+ /**
29
+ * Project the client's permission mode → the wire intent. Carries any KNOWN non-`default` mode verbatim (the
30
+ * service interprets); `default`/unknown/undefined ⇒ no stamp (the deployment baseline governs). No policy baked
31
+ * here — relevance + tighten-only governance is the service's job.
32
+ *
33
+ * `stampDefault`([830]① server 1.191 五模式解释层):server 侧 absent-mode 不挂门(存量 headless
34
+ * 零回归),manual(=CC default)要挂 fs 写 ask 门必须显式 stamp `permissionMode:"default"`。只有
35
+ * 三选卡真能弹出来的调用方(交互 REPL,leader ToolUseConfirm 队列已注册)才传 true —— print/headless
36
+ * 不 stamp;老 server 对 "default" 走 basePolicy(与 absent 等价),version-safe。
37
+ */
38
+ export function permissionModeField(mode, opts) {
39
+ if (typeof mode === 'string' && mode !== 'default' && KNOWN.has(mode)) {
40
+ return { permissionMode: mode };
41
+ }
42
+ if (opts?.stampDefault === true && (mode === undefined || mode === 'default')) {
43
+ return { permissionMode: 'default' };
44
+ }
45
+ return {};
46
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/promptProfileWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/promptProfileWireCaps.ts — PURE projection: the shell's prompt-profile pick → the
6
+ * `TaskRequest.promptProfile` axis (core 1.328 [1144]: CC LT(model_id)-同构的呈现轴,
7
+ * "simple"=CC 对 BYOM id 发的短形面(引擎缺省)/"classic"=长形面,按任务/模型可切)。
8
+ *
9
+ * Config source: env `SEMA_PROMPT_PROFILE`(A/B 与手选口;center 下发面走引擎侧 TaskSpec 直通,
10
+ * 不经此位)。STRICT allowlist:仅 "simple"/"classic"(大小写不敏感,trim)stamp;其它/缺省 ⇒
11
+ * `undefined`(不 stamp=引擎缺省 simple,请求形状不变——additive 纪律与 retainBackgroundWireCaps
12
+ * 同款)。CALLER 前置 live 门(mock 形不变)。
13
+ */
14
+ import { type EnvLike } from './hostEnv.js';
15
+ export declare const PROMPT_PROFILE_ENV: "SEMA_PROMPT_PROFILE";
16
+ export type PromptProfile = 'simple' | 'classic';
17
+ export declare function promptProfileFromEnv(env?: EnvLike): PromptProfile | undefined;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/promptProfileWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/promptProfileWireCaps.ts — PURE projection: the shell's prompt-profile pick → the
6
+ * `TaskRequest.promptProfile` axis (core 1.328 [1144]: CC LT(model_id)-同构的呈现轴,
7
+ * "simple"=CC 对 BYOM id 发的短形面(引擎缺省)/"classic"=长形面,按任务/模型可切)。
8
+ *
9
+ * Config source: env `SEMA_PROMPT_PROFILE`(A/B 与手选口;center 下发面走引擎侧 TaskSpec 直通,
10
+ * 不经此位)。STRICT allowlist:仅 "simple"/"classic"(大小写不敏感,trim)stamp;其它/缺省 ⇒
11
+ * `undefined`(不 stamp=引擎缺省 simple,请求形状不变——additive 纪律与 retainBackgroundWireCaps
12
+ * 同款)。CALLER 前置 live 门(mock 形不变)。
13
+ */
14
+ import { hostEnv } from './hostEnv.js';
15
+ export const PROMPT_PROFILE_ENV = 'SEMA_PROMPT_PROFILE';
16
+ export function promptProfileFromEnv(env = hostEnv()) {
17
+ const raw = env[PROMPT_PROFILE_ENV];
18
+ const s = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
19
+ return s === 'simple' || s === 'classic' ? s : undefined;
20
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/retainBackgroundWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/retainBackgroundWireCaps.ts — PURE projection: the shell's "retain background processes past
6
+ * task teardown" opt-in → the TOP-LEVEL `TaskRequest.retainBackgroundProcesses` boolean the SERVICE consumes
7
+ * (spec-fields.js `retainBackgroundProcessesFromBody`: `raw !== true` ⇒ dropped;requirePrincipal 多租户形
8
+ * 服务端另有门)。Mirrors selfOrchestrationWireCaps(the verified top-of-body wire pattern)。
9
+ *
10
+ * 背景([1046] 核谳,TB [991]② reap 族):core 直跑腿(sema-tb)出厂显式
11
+ * `retainBackgroundProcesses:true`(design/128 T1-1——「交付存活服务」任务的 verifier 在进程退出后
12
+ * 才连接),而组合腿产品曾默认关 ⇒ 两腿在 reap 族不是同一场考试。
13
+ *
14
+ * ⚠️ 缺省已翻转为 ON(clay 亲报「后台 shell 命令跑到一半被杀」案,2026-07-26;[1046]④ 的裁量件本次
15
+ * 由真实场景需求兑现)。缺省不发时,引擎 turn finally 的**两道门**都关着:
16
+ * ① `settleKilledForOwner({retainProcesses})` 的 retain 豁免;
17
+ * ② `retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying` —— 为真才整块跳过
18
+ * `bgEnv.disposeBackgroundShells(...)`(该 blanket sweep 不经 task-registry,故被它收掉的任务
19
+ * `stopSource` 为空,终态归因落到 `stoppedBy = "system"` 缺省 = clay 观察到的 `stopped-by: system`)。
20
+ *
21
+ * 三问(宪法 NO-SHIP 检查):
22
+ * 谁需要:真人用户 —— `run_in_background` 起的构建/测试/服务在当轮结束后必须还活着,否则
23
+ * BashOutput/TaskOutput 续读到的是尸体(clay 案:输出文件 0 字节 + status killed)。
24
+ * 谁受害:隔离执行环境(e2b/k8s/ssh 等 `isIsolated(env)`)—— 那里 `ownedEnvDying` 为真,retain 被
25
+ * **无条件覆盖**(core 侧 `!opts.envDying` / `&& !ownedEnvDying`),进程随宿主一起走。这是
26
+ * 正确的:进程宿主没了,留 handle 无意义。故本翻转对隔离腿零语义变化。
27
+ * 次级受害面 = 本机资源泄漏(后台进程活过当轮)—— 但这正是 CC 的既定契约,不是回归。
28
+ * 补偿机制:`SEMA_RETAIN_BACKGROUND=0|false|no|off` 逃生口 ⇒ 不 stamp ⇒ 回引擎缺省 reap;
29
+ * 进程始终可由 KillShell/TaskStop 显式收,`/bashes` 面板可见可停;引擎 hard-wall
30
+ * (defaultBgTimeoutSec 30min / maxBgTimeoutSec 2h)仍是兜底上限。
31
+ *
32
+ * CC 逐字锚(对标纲领 = 100% 达 CC 效果):CC 的 Bash `run_in_background` 契约 = 进程活过当轮,
33
+ * 由 BashOutput 续读、KillShell 显式收 —— **不在 turn 边界回收**。缺省 reap 是对 CC 的偏离,本次钉回。
34
+ *
35
+ * 语义:缺省 ON —— unset / 未识别值 ⇒ `true`(stamp);显式 truthy(1/true/yes/on)⇒ `true`;
36
+ * 只有显式 falsy(0/false/no/off)⇒ `undefined`(不 stamp ⇒ 引擎缺省 reap)。
37
+ * 服务端 `retainBackgroundProcessesFromBody` 是 `raw !== true ⇒ 丢弃` 的严格刻度,所以 opt-out 一律
38
+ * 表达为**不 stamp**,绝不发 `false`(形状诚实)。
39
+ * PURE — callers pass `env`;caller gates to LIVE mode(mock request shape 不变)。
40
+ */
41
+ import { type EnvLike } from './hostEnv.js';
42
+ /** The env key the shell reads(SEMA_ 命名空间,与引擎自己的 spec∨env 通道正交——这里是壳→
43
+ * TaskRequest 的 per-request 意图面,非引擎部署面)。 */
44
+ export declare const RETAIN_BACKGROUND_ENV: "SEMA_RETAIN_BACKGROUND";
45
+ /**
46
+ * ENV source → `true`(缺省 ON = CC `run_in_background` 契约:后台进程活过当轮),
47
+ * 只有显式 falsy 逃生口(`SEMA_RETAIN_BACKGROUND=0|false|no|off`)⇒ `undefined`(不 stamp ⇒ 引擎缺省 reap)。
48
+ * 返回型刻意是 `true | undefined` —— 服务端 `raw !== true ⇒ 丢弃`,opt-out 只能表达为「不 stamp」。
49
+ */
50
+ export declare function retainBackgroundFromEnv(env?: EnvLike): true | undefined;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/retainBackgroundWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/retainBackgroundWireCaps.ts — PURE projection: the shell's "retain background processes past
6
+ * task teardown" opt-in → the TOP-LEVEL `TaskRequest.retainBackgroundProcesses` boolean the SERVICE consumes
7
+ * (spec-fields.js `retainBackgroundProcessesFromBody`: `raw !== true` ⇒ dropped;requirePrincipal 多租户形
8
+ * 服务端另有门)。Mirrors selfOrchestrationWireCaps(the verified top-of-body wire pattern)。
9
+ *
10
+ * 背景([1046] 核谳,TB [991]② reap 族):core 直跑腿(sema-tb)出厂显式
11
+ * `retainBackgroundProcesses:true`(design/128 T1-1——「交付存活服务」任务的 verifier 在进程退出后
12
+ * 才连接),而组合腿产品曾默认关 ⇒ 两腿在 reap 族不是同一场考试。
13
+ *
14
+ * ⚠️ 缺省已翻转为 ON(clay 亲报「后台 shell 命令跑到一半被杀」案,2026-07-26;[1046]④ 的裁量件本次
15
+ * 由真实场景需求兑现)。缺省不发时,引擎 turn finally 的**两道门**都关着:
16
+ * ① `settleKilledForOwner({retainProcesses})` 的 retain 豁免;
17
+ * ② `retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying` —— 为真才整块跳过
18
+ * `bgEnv.disposeBackgroundShells(...)`(该 blanket sweep 不经 task-registry,故被它收掉的任务
19
+ * `stopSource` 为空,终态归因落到 `stoppedBy = "system"` 缺省 = clay 观察到的 `stopped-by: system`)。
20
+ *
21
+ * 三问(宪法 NO-SHIP 检查):
22
+ * 谁需要:真人用户 —— `run_in_background` 起的构建/测试/服务在当轮结束后必须还活着,否则
23
+ * BashOutput/TaskOutput 续读到的是尸体(clay 案:输出文件 0 字节 + status killed)。
24
+ * 谁受害:隔离执行环境(e2b/k8s/ssh 等 `isIsolated(env)`)—— 那里 `ownedEnvDying` 为真,retain 被
25
+ * **无条件覆盖**(core 侧 `!opts.envDying` / `&& !ownedEnvDying`),进程随宿主一起走。这是
26
+ * 正确的:进程宿主没了,留 handle 无意义。故本翻转对隔离腿零语义变化。
27
+ * 次级受害面 = 本机资源泄漏(后台进程活过当轮)—— 但这正是 CC 的既定契约,不是回归。
28
+ * 补偿机制:`SEMA_RETAIN_BACKGROUND=0|false|no|off` 逃生口 ⇒ 不 stamp ⇒ 回引擎缺省 reap;
29
+ * 进程始终可由 KillShell/TaskStop 显式收,`/bashes` 面板可见可停;引擎 hard-wall
30
+ * (defaultBgTimeoutSec 30min / maxBgTimeoutSec 2h)仍是兜底上限。
31
+ *
32
+ * CC 逐字锚(对标纲领 = 100% 达 CC 效果):CC 的 Bash `run_in_background` 契约 = 进程活过当轮,
33
+ * 由 BashOutput 续读、KillShell 显式收 —— **不在 turn 边界回收**。缺省 reap 是对 CC 的偏离,本次钉回。
34
+ *
35
+ * 语义:缺省 ON —— unset / 未识别值 ⇒ `true`(stamp);显式 truthy(1/true/yes/on)⇒ `true`;
36
+ * 只有显式 falsy(0/false/no/off)⇒ `undefined`(不 stamp ⇒ 引擎缺省 reap)。
37
+ * 服务端 `retainBackgroundProcessesFromBody` 是 `raw !== true ⇒ 丢弃` 的严格刻度,所以 opt-out 一律
38
+ * 表达为**不 stamp**,绝不发 `false`(形状诚实)。
39
+ * PURE — callers pass `env`;caller gates to LIVE mode(mock request shape 不变)。
40
+ */
41
+ import { hostEnv } from './hostEnv.js';
42
+ /** The env key the shell reads(SEMA_ 命名空间,与引擎自己的 spec∨env 通道正交——这里是壳→
43
+ * TaskRequest 的 per-request 意图面,非引擎部署面)。 */
44
+ export const RETAIN_BACKGROUND_ENV = 'SEMA_RETAIN_BACKGROUND';
45
+ /** FALSY spellings that opt OUT (case-insensitive, trimmed). Conservative allowlist —— 只有这些
46
+ * 拼写能关掉 retain;其它一切(含未识别值/空串)落缺省 ON,绝不静默 opt-out。 */
47
+ function isFalsy(v) {
48
+ const s = typeof v === 'string' ? v.trim().toLowerCase() : '';
49
+ return s === '0' || s === 'false' || s === 'no' || s === 'off';
50
+ }
51
+ /**
52
+ * ENV source → `true`(缺省 ON = CC `run_in_background` 契约:后台进程活过当轮),
53
+ * 只有显式 falsy 逃生口(`SEMA_RETAIN_BACKGROUND=0|false|no|off`)⇒ `undefined`(不 stamp ⇒ 引擎缺省 reap)。
54
+ * 返回型刻意是 `true | undefined` —— 服务端 `raw !== true ⇒ 丢弃`,opt-out 只能表达为「不 stamp」。
55
+ */
56
+ export function retainBackgroundFromEnv(env = hostEnv()) {
57
+ return isFalsy(env[RETAIN_BACKGROUND_ENV]) ? undefined : true;
58
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/rewindWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/rewindWireCaps.ts — PURE CC Rewind (E18) field projection (no deps, testable). seamQuery imports it.
6
+ *
7
+ * Maps the CC /rewind MessageSelector modes → the engine's rewind fields (SDK TaskRequest, core 1.166.0;
8
+ * resumeAtMode: core 1.292 [833]):
9
+ * both = { resumeAt, resumeAtMode:"before", rewindFiles:true } fork BEFORE the user message + tree
10
+ * conversation = { resumeAt, resumeAtMode:"before" } fork BEFORE the user message only
11
+ * code = { rewindFilesTo } restore the tree only, NO conversation fork (no setLeafId)
12
+ * The target is a USER-MESSAGE SessionTreeEntry.id (server anchors user-message boundaries). Plus the per-turn
13
+ * CAPTURE stamp (rewindFiles:true on a normal turn) so the engine snapshots the tree (else nothing to restore).
14
+ *
15
+ * resumeAtMode "before"(排他截,core 1.292):CC /rewind 的语义是「回到发这条消息之前」——被选中的
16
+ * user message 回填输入框、从对话里移除;engine 侧对应 setLeafId(entry.parentId)。旧的含目标(inclusive
17
+ * "at")形状会把该 user message 留在引擎历史里 → 模型仍记得(暗号复现 bug)。老 server 未透传该字段时
18
+ * 行为退回含目标(version-safe,无回归——等 server 透传)。
19
+ */
20
+ /** A one-shot rewind RESTORE instruction (or the per-turn capture stamp) projected onto a request. */
21
+ export interface SeamRewindSpec {
22
+ resumeAt?: string;
23
+ /** core 1.292([833]):"before" = 排他截(setLeafId(entry.parentId));缺省 "at" = 含目标。 */
24
+ resumeAtMode?: 'at' | 'before';
25
+ rewindFiles?: boolean;
26
+ rewindFilesTo?: string;
27
+ }
28
+ /** The CC /rewind restore modes that hit the engine (summarize = compact, handled elsewhere). */
29
+ export type RewindMode = 'both' | 'conversation' | 'code';
30
+ /** Build the engine rewind fields for a chosen mode + target user-message entryId. */
31
+ export declare function rewindSpecForMode(mode: RewindMode, targetEntryId: string): SeamRewindSpec;
32
+ /**
33
+ * PURE projection (no state): an armed one-shot RESTORE wins; else the per-turn CAPTURE stamp (when enabled);
34
+ * else nothing. The caller consumes (clears) the arm after a restore.
35
+ */
36
+ export declare function projectRewind(pending: SeamRewindSpec | undefined, captureEnabled: boolean): SeamRewindSpec;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/rewindWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/rewindWireCaps.ts — PURE CC Rewind (E18) field projection (no deps, testable). seamQuery imports it.
6
+ *
7
+ * Maps the CC /rewind MessageSelector modes → the engine's rewind fields (SDK TaskRequest, core 1.166.0;
8
+ * resumeAtMode: core 1.292 [833]):
9
+ * both = { resumeAt, resumeAtMode:"before", rewindFiles:true } fork BEFORE the user message + tree
10
+ * conversation = { resumeAt, resumeAtMode:"before" } fork BEFORE the user message only
11
+ * code = { rewindFilesTo } restore the tree only, NO conversation fork (no setLeafId)
12
+ * The target is a USER-MESSAGE SessionTreeEntry.id (server anchors user-message boundaries). Plus the per-turn
13
+ * CAPTURE stamp (rewindFiles:true on a normal turn) so the engine snapshots the tree (else nothing to restore).
14
+ *
15
+ * resumeAtMode "before"(排他截,core 1.292):CC /rewind 的语义是「回到发这条消息之前」——被选中的
16
+ * user message 回填输入框、从对话里移除;engine 侧对应 setLeafId(entry.parentId)。旧的含目标(inclusive
17
+ * "at")形状会把该 user message 留在引擎历史里 → 模型仍记得(暗号复现 bug)。老 server 未透传该字段时
18
+ * 行为退回含目标(version-safe,无回归——等 server 透传)。
19
+ */
20
+ /** Build the engine rewind fields for a chosen mode + target user-message entryId. */
21
+ export function rewindSpecForMode(mode, targetEntryId) {
22
+ switch (mode) {
23
+ case 'both':
24
+ return { resumeAt: targetEntryId, resumeAtMode: 'before', rewindFiles: true };
25
+ case 'conversation':
26
+ return { resumeAt: targetEntryId, resumeAtMode: 'before' };
27
+ case 'code':
28
+ return { rewindFilesTo: targetEntryId };
29
+ }
30
+ }
31
+ /**
32
+ * PURE projection (no state): an armed one-shot RESTORE wins; else the per-turn CAPTURE stamp (when enabled);
33
+ * else nothing. The caller consumes (clears) the arm after a restore.
34
+ */
35
+ export function projectRewind(pending, captureEnabled) {
36
+ if (pending)
37
+ return { ...pending };
38
+ if (captureEnabled)
39
+ return { rewindFiles: true };
40
+ return {};
41
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/selfOrchestrationWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/selfOrchestrationWireCaps.ts — PURE projection: the shell's "enable the LLM-authored workflow engine"
6
+ * opt-in → the TOP-LEVEL `TaskRequest.selfOrchestration` boolean the SERVICE consumes. Mirrors webSearchWireCaps.ts
7
+ * (the verified wire pattern), but the field rides at the TOP of the body (not under `settings`).
8
+ *
9
+ * The SERVICE reads `body.selfOrchestration` STRICTLY (ai-agent-service task-workflow.ts `selfOrchestrationFromBody`:
10
+ * `body.selfOrchestration !== true` ⇒ dropped) → core `TaskSpec.selfOrchestration`, the gate that MOUNTS core's
11
+ * LLM-authored `run_workflow` engine (the workflow.ts agent/parallel/pipeline/phase/nested orchestrator). The
12
+ * service then gates it on the DEPLOYMENT's `SELF_ORCHESTRATION_ENABLED` (else a harmless no-op, dropped) + the
13
+ * MULTI-TENANT per-principal entitlement RESOLVER (fail-closed: no resolver ⇒ dropped); a single-user worker honors
14
+ * it directly. So the shell's job is ONLY to STAMP the intent when the user opted in — the service owns every gate.
15
+ * We mirror the service's strict `=== true` semantics here: only an explicit truthy env opt-in returns `true`;
16
+ * everything else returns `undefined` (⇒ NO stamp ⇒ the engine never mounts run_workflow ⇒ default behaviour
17
+ * unchanged, exactly like an absent webSearch config).
18
+ *
19
+ * Config source (clay 决定 2026-07-01 §4 — DEFAULT-ON,opt-out):
20
+ * - DEFAULT ON: unset / 其它 ⇒ stamp `selfOrchestration:true`(RunWorkflow 默认可用,体验像 CC 超集)。
21
+ * - ENV opt-OUT: `SEMA_SELF_ORCHESTRATION=0/false/no/off` ⇒ undefined(no stamp)。
22
+ *
23
+ * 前身是 opt-in(parity-safe 保守默认 OFF);clay 拍 §4 后翻转为默认 ON(TOC 单用户本机无 entitlement 顾虑,
24
+ * 引擎侧 SELF_ORCHESTRATION_ENABLED 也在 assembleChildEnv 默认开)。可后续接 settings.json 驱动,这里仍是单一投影点。
25
+ *
26
+ * PURE — no env/file IO of its OWN (callers pass `env`), so it bundles + unit-tests without the heavy settings
27
+ * graph, exactly like webSearchWireCaps. The CALLER (seamQuery / seamQueryEngine) gates the env read to LIVE mode
28
+ * so the mock request shape never changes; the selfOrchestration stamp IS the opt-in (no extra flag needed).
29
+ */
30
+ import { type EnvLike } from './hostEnv.js';
31
+ /** The env key the shell reads. SEMA_-namespaced so it never collides with the worker's OWN deploy
32
+ * `SELF_ORCHESTRATION_ENABLED` env (the service's DEPLOYMENT gate — a different axis the service owns). */
33
+ export declare const SELF_ORCHESTRATION_ENV: "SEMA_SELF_ORCHESTRATION";
34
+ /** ENV source → `true` by DEFAULT (clay 决定 2026-07-01 §4:RunWorkflow 默认可用),unless the user explicitly
35
+ * opts OUT (`SEMA_SELF_ORCHESTRATION=0/false/no/off`) ⇒ `undefined` (no stamp; engine never mounts run_workflow).
36
+ * Still `true | undefined` (never `false`) to mirror the service's strict `=== true` read. A stamped `true` only
37
+ * makes run_workflow AVAILABLE — the engine still owns the final gate (its own `SELF_ORCHESTRATION_ENABLED`, set
38
+ * default-on in TOC assembleChildEnv, + per-principal entitlement; TOC single-user honors it directly). The caller
39
+ * gates this to LIVE mode, so the MOCK request shape is unchanged; only the live default path now stamps true. */
40
+ export declare function selfOrchestrationFromEnv(env?: EnvLike): true | undefined;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/selfOrchestrationWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/selfOrchestrationWireCaps.ts — PURE projection: the shell's "enable the LLM-authored workflow engine"
6
+ * opt-in → the TOP-LEVEL `TaskRequest.selfOrchestration` boolean the SERVICE consumes. Mirrors webSearchWireCaps.ts
7
+ * (the verified wire pattern), but the field rides at the TOP of the body (not under `settings`).
8
+ *
9
+ * The SERVICE reads `body.selfOrchestration` STRICTLY (ai-agent-service task-workflow.ts `selfOrchestrationFromBody`:
10
+ * `body.selfOrchestration !== true` ⇒ dropped) → core `TaskSpec.selfOrchestration`, the gate that MOUNTS core's
11
+ * LLM-authored `run_workflow` engine (the workflow.ts agent/parallel/pipeline/phase/nested orchestrator). The
12
+ * service then gates it on the DEPLOYMENT's `SELF_ORCHESTRATION_ENABLED` (else a harmless no-op, dropped) + the
13
+ * MULTI-TENANT per-principal entitlement RESOLVER (fail-closed: no resolver ⇒ dropped); a single-user worker honors
14
+ * it directly. So the shell's job is ONLY to STAMP the intent when the user opted in — the service owns every gate.
15
+ * We mirror the service's strict `=== true` semantics here: only an explicit truthy env opt-in returns `true`;
16
+ * everything else returns `undefined` (⇒ NO stamp ⇒ the engine never mounts run_workflow ⇒ default behaviour
17
+ * unchanged, exactly like an absent webSearch config).
18
+ *
19
+ * Config source (clay 决定 2026-07-01 §4 — DEFAULT-ON,opt-out):
20
+ * - DEFAULT ON: unset / 其它 ⇒ stamp `selfOrchestration:true`(RunWorkflow 默认可用,体验像 CC 超集)。
21
+ * - ENV opt-OUT: `SEMA_SELF_ORCHESTRATION=0/false/no/off` ⇒ undefined(no stamp)。
22
+ *
23
+ * 前身是 opt-in(parity-safe 保守默认 OFF);clay 拍 §4 后翻转为默认 ON(TOC 单用户本机无 entitlement 顾虑,
24
+ * 引擎侧 SELF_ORCHESTRATION_ENABLED 也在 assembleChildEnv 默认开)。可后续接 settings.json 驱动,这里仍是单一投影点。
25
+ *
26
+ * PURE — no env/file IO of its OWN (callers pass `env`), so it bundles + unit-tests without the heavy settings
27
+ * graph, exactly like webSearchWireCaps. The CALLER (seamQuery / seamQueryEngine) gates the env read to LIVE mode
28
+ * so the mock request shape never changes; the selfOrchestration stamp IS the opt-in (no extra flag needed).
29
+ */
30
+ import { hostEnv } from './hostEnv.js';
31
+ /** The env key the shell reads. SEMA_-namespaced so it never collides with the worker's OWN deploy
32
+ * `SELF_ORCHESTRATION_ENABLED` env (the service's DEPLOYMENT gate — a different axis the service owns). */
33
+ export const SELF_ORCHESTRATION_ENV = 'SEMA_SELF_ORCHESTRATION';
34
+ /** The FALSY spellings that opt OUT (case-insensitive, trimmed). Conservative allowlist — only an explicit
35
+ * `0`/`false`/`no`/`off` disables; everything else (incl. unset/empty) is the DEFAULT-ON path. */
36
+ function isFalsy(v) {
37
+ const s = typeof v === 'string' ? v.trim().toLowerCase() : '';
38
+ return s === '0' || s === 'false' || s === 'no' || s === 'off';
39
+ }
40
+ /** ENV source → `true` by DEFAULT (clay 决定 2026-07-01 §4:RunWorkflow 默认可用),unless the user explicitly
41
+ * opts OUT (`SEMA_SELF_ORCHESTRATION=0/false/no/off`) ⇒ `undefined` (no stamp; engine never mounts run_workflow).
42
+ * Still `true | undefined` (never `false`) to mirror the service's strict `=== true` read. A stamped `true` only
43
+ * makes run_workflow AVAILABLE — the engine still owns the final gate (its own `SELF_ORCHESTRATION_ENABLED`, set
44
+ * default-on in TOC assembleChildEnv, + per-principal entitlement; TOC single-user honors it directly). The caller
45
+ * gates this to LIVE mode, so the MOCK request shape is unchanged; only the live default path now stamps true. */
46
+ export function selfOrchestrationFromEnv(env = hostEnv()) {
47
+ return isFalsy(env[SELF_ORCHESTRATION_ENV]) ? undefined : true;
48
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/sessionModelLatch.ts 逐字搬入(零依赖;🔴 三个模块级 latch 是单实例态)。
3
+ */
4
+ /**
5
+ * sessionModelLatch — 件13 (rev2 2026-07-14, CC 2.1.207 对锚): support module for the /model `s`
6
+ * (use this session only) split, anchored on the 207 mechanism (verified against the 2.1.198 pretty
7
+ * source, semantics identical in 207):
8
+ *
9
+ * wsr(model, get, set, setDefault): // pretty.js:493226 (198 coords)
10
+ * set(a => ({ ...a, mainLoopModel: model, mainLoopModelForSession: null })) // ALWAYS in-memory
11
+ * if (setDefault) RQt(model) // RQt = eo("userSettings",{model}) —
12
+ * // the ONLY settings write (493244)
13
+ * message = `Set model to X${setDefault ? " and saved as your default for new sessions"
14
+ * : " for this session only"}`
15
+ *
16
+ * i.e. in 207 BOTH Enter and `s` write the in-memory mainLoopModel and CLEAR the plan-mode session
17
+ * field; persistence is EXPLICIT (RQt) and only runs on set-as-default. 207 has no auto-persisting
18
+ * model choke point. sema's bundled base DOES auto-persist every mainLoopModel change in
19
+ * state/onChangeAppState.ts, so to reproduce the 207 state shape exactly the session-only commit
20
+ * marks its write with a ONE-SHOT skip-persist flag which the choke consumes: the write then behaves
21
+ * exactly like 207's no-RQt branch (mainLoopModel set in-memory, mainLoopModelForSession null,
22
+ * userSettings untouched → restart reverts to the persisted default).
23
+ *
24
+ * The latch half mirrors "the active model is UNPERSISTED" for NON-React consumers (seamQuery's
25
+ * modelEndpointSwitch runs outside the component tree): a session-only pick must not be promoted to
26
+ * next-boot roster/env state when the engine can route the model by itself. Kept in sync from the
27
+ * onChangeAppState choke (single funnel for every setAppState write): set on a session-only
28
+ * mainLoopModel write OR a plan-mode mainLoopModelForSession write, cleared on a persisted write.
29
+ */
30
+ export declare function setSessionModelLatch(model: string | null): void;
31
+ export declare function getSessionModelLatch(): string | null;
32
+ export declare function markSessionOnlyModelWrite(): void;
33
+ export declare function consumeSessionOnlyModelWrite(): boolean;
34
+ export declare function markUserModelPickThisSession(): void;
35
+ export declare function hasUserModelPickThisSession(): boolean;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/sessionModelLatch.ts 逐字搬入(零依赖;🔴 三个模块级 latch 是单实例态)。
3
+ */
4
+ /**
5
+ * sessionModelLatch — 件13 (rev2 2026-07-14, CC 2.1.207 对锚): support module for the /model `s`
6
+ * (use this session only) split, anchored on the 207 mechanism (verified against the 2.1.198 pretty
7
+ * source, semantics identical in 207):
8
+ *
9
+ * wsr(model, get, set, setDefault): // pretty.js:493226 (198 coords)
10
+ * set(a => ({ ...a, mainLoopModel: model, mainLoopModelForSession: null })) // ALWAYS in-memory
11
+ * if (setDefault) RQt(model) // RQt = eo("userSettings",{model}) —
12
+ * // the ONLY settings write (493244)
13
+ * message = `Set model to X${setDefault ? " and saved as your default for new sessions"
14
+ * : " for this session only"}`
15
+ *
16
+ * i.e. in 207 BOTH Enter and `s` write the in-memory mainLoopModel and CLEAR the plan-mode session
17
+ * field; persistence is EXPLICIT (RQt) and only runs on set-as-default. 207 has no auto-persisting
18
+ * model choke point. sema's bundled base DOES auto-persist every mainLoopModel change in
19
+ * state/onChangeAppState.ts, so to reproduce the 207 state shape exactly the session-only commit
20
+ * marks its write with a ONE-SHOT skip-persist flag which the choke consumes: the write then behaves
21
+ * exactly like 207's no-RQt branch (mainLoopModel set in-memory, mainLoopModelForSession null,
22
+ * userSettings untouched → restart reverts to the persisted default).
23
+ *
24
+ * The latch half mirrors "the active model is UNPERSISTED" for NON-React consumers (seamQuery's
25
+ * modelEndpointSwitch runs outside the component tree): a session-only pick must not be promoted to
26
+ * next-boot roster/env state when the engine can route the model by itself. Kept in sync from the
27
+ * onChangeAppState choke (single funnel for every setAppState write): set on a session-only
28
+ * mainLoopModel write OR a plan-mode mainLoopModelForSession write, cleared on a persisted write.
29
+ */
30
+ /** The model that is currently ACTIVE but UNPERSISTED (session-only pick or plan-mode override). */
31
+ let sessionModelOverride = null;
32
+ export function setSessionModelLatch(model) {
33
+ sessionModelOverride = model;
34
+ }
35
+ export function getSessionModelLatch() {
36
+ return sessionModelOverride;
37
+ }
38
+ /** One-shot mark: the NEXT mainLoopModel change is session-only (207 no-RQt branch) — do not persist. */
39
+ let sessionOnlyWritePending = false;
40
+ export function markSessionOnlyModelWrite() {
41
+ sessionOnlyWritePending = true;
42
+ }
43
+ export function consumeSessionOnlyModelWrite() {
44
+ const v = sessionOnlyWritePending;
45
+ sessionOnlyWritePending = false;
46
+ return v;
47
+ }
48
+ // ── 件3(F1 模型面批,S3 铁律半场):本会话「用户显式选过模型」标记 ─────────────────────
49
+ // boot 目录回读(replEntry 把 /v1/models default 写进 initialState.mainLoopModel)不经过
50
+ // setAppState ⇒ 不置位;任何经 setAppState 的 mainLoopModel 变更(cmd-model Enter、`s` 键、
51
+ // /model <name>、config 面板)都算显式 pick —— 由 onChangeAppState choke(每个 setAppState
52
+ // 写的单一漏斗)置位,散在 UI 调用点零改动。ensureEngineServesModel 据此区分「运行时回声」
53
+ // (boot 回读的目录 default)vs「用户意图」:回声绝不允许持久化写(不 switchRoster 改 env /
54
+ // 不重排 models.json / 不改档位绑定)。注意与上面 latch 的分工:latch=「当前活跃值未持久化」
55
+ // (s 键/plan override),本标记=「本会话内发生过显式选择」——s 键 pick 也置位(它是显式意图,
56
+ // 是否持久化由件13 latch 在 instant 路径拦截,语义不变)。
57
+ let userModelPickThisSession = false;
58
+ export function markUserModelPickThisSession() {
59
+ userModelPickThisSession = true;
60
+ }
61
+ export function hasUserModelPickThisSession() {
62
+ return userModelPickThisSession;
63
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/skillsWireCaps.ts 逐字搬入(只 import SDK)。
3
+ */
4
+ /**
5
+ * src/sema/skillsWireCaps.ts — the PURE projection + server caps for the skills engine wire.
6
+ *
7
+ * Split out from skillsWire.ts so this logic (the part worth pinning) is testable WITHOUT the heavy on-disk
8
+ * loader graph (loadSkillsDir / loadPluginCommands pull analytics/signals/fs). It imports ONLY the `SkillSpec`
9
+ * type (erased at build), so a test can esbuild-bundle THIS file alone — no runtime side effects.
10
+ *
11
+ * The caps mirror the SDK `TaskRequest.skills` server caps (types.ts:82): ≤10 items, name ≤64, description
12
+ * ≤1024, content ≤32768 — a violation is a 400, so we truncate/limit HERE to make the live request well-formed
13
+ * by construction.
14
+ */
15
+ import type { SkillSpec } from '@sema-agent/sdk';
16
+ /** Server caps for `TaskRequest.skills` (SDK types.ts:82 — violations are a 400). */
17
+ export declare const SKILL_CAPS: {
18
+ readonly items: 10;
19
+ readonly name: 64;
20
+ readonly description: 1024;
21
+ readonly content: 32768;
22
+ };
23
+ /** The minimal shape of a loaded skill `Command` this projection reads (a structural subset of CC's Command,
24
+ * cast at the call site so we stay decoupled from the wide Command union). */
25
+ export interface SkillCmdLike {
26
+ type?: string;
27
+ name?: string;
28
+ description?: string;
29
+ whenToUse?: string;
30
+ /** loader origin ('builtin' | 'plugin' | SettingSource…) — read by the skillOverrides four-state
31
+ * resolution when the wire prunes hidden skills (skillsWire.ts). */
32
+ source?: string;
33
+ disableModelInvocation?: boolean;
34
+ /** the skill's own directory (`createSkillCommand` baseDir → command.skillRoot); SKILL.md lives at root/SKILL.md. */
35
+ skillRoot?: string;
36
+ }
37
+ /** Build one capped `SkillSpec`, or null when there is no usable name/body. description folds in `whenToUse`
38
+ * (CC's progressive-disclosure metadata is name+description+whenToUse) so the engine's Skill tool can select
39
+ * it. All three fields are truncated to the server caps. */
40
+ export declare function toCappedSpec(name: string, description: string, whenToUse: string, body: string): SkillSpec | null;
41
+ /**
42
+ * PURE projection: loaded skill commands → `SkillSpec[]` (capped, deduped by name, ≤10 items). `resolveBody`
43
+ * supplies each command's raw SKILL.md body (null ⇒ skip); injecting it keeps this function IO-free and
44
+ * unit-testable. Only MODEL-INVOCABLE prompt skills are sent — UI-only / `disableModelInvocation` commands
45
+ * are excluded (engine injection feeds the model's Skill tool, not the slash menu).
46
+ */
47
+ export declare function skillCommandsToSpecs(cmds: SkillCmdLike[], resolveBody: (cmd: SkillCmdLike) => string | null): SkillSpec[];
@@ -0,0 +1,41 @@
1
+ /** Server caps for `TaskRequest.skills` (SDK types.ts:82 — violations are a 400). */
2
+ export const SKILL_CAPS = { items: 10, name: 64, description: 1024, content: 32768 };
3
+ /** Build one capped `SkillSpec`, or null when there is no usable name/body. description folds in `whenToUse`
4
+ * (CC's progressive-disclosure metadata is name+description+whenToUse) so the engine's Skill tool can select
5
+ * it. All three fields are truncated to the server caps. */
6
+ export function toCappedSpec(name, description, whenToUse, body) {
7
+ const n = name.trim().slice(0, SKILL_CAPS.name);
8
+ const c = body.trim().slice(0, SKILL_CAPS.content);
9
+ if (!n || !c)
10
+ return null;
11
+ const desc = [description.trim(), whenToUse.trim()]
12
+ .filter((s) => s.length > 0)
13
+ .join(' — ')
14
+ .slice(0, SKILL_CAPS.description);
15
+ return { name: n, description: desc, content: c };
16
+ }
17
+ /**
18
+ * PURE projection: loaded skill commands → `SkillSpec[]` (capped, deduped by name, ≤10 items). `resolveBody`
19
+ * supplies each command's raw SKILL.md body (null ⇒ skip); injecting it keeps this function IO-free and
20
+ * unit-testable. Only MODEL-INVOCABLE prompt skills are sent — UI-only / `disableModelInvocation` commands
21
+ * are excluded (engine injection feeds the model's Skill tool, not the slash menu).
22
+ */
23
+ export function skillCommandsToSpecs(cmds, resolveBody) {
24
+ const specs = [];
25
+ const seen = new Set();
26
+ for (const cmd of cmds) {
27
+ if (specs.length >= SKILL_CAPS.items)
28
+ break;
29
+ if (cmd.type !== 'prompt' || cmd.disableModelInvocation)
30
+ continue;
31
+ const body = resolveBody(cmd);
32
+ if (!body)
33
+ continue;
34
+ const spec = toCappedSpec(cmd.name ?? '', cmd.description ?? '', cmd.whenToUse ?? '', body);
35
+ if (!spec || seen.has(spec.name))
36
+ continue;
37
+ seen.add(spec.name);
38
+ specs.push(spec);
39
+ }
40
+ return specs;
41
+ }