@thincoder/core 0.10.7 → 0.10.8

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 (125) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/advisor/compaction.mjs +1 -1
  3. package/advisor/run.mjs +1 -1
  4. package/agent/chat-call.mjs +40 -27
  5. package/agent/child-marks.mjs +1 -1
  6. package/agent/dispatch-gates.mjs +7 -4
  7. package/agent/dispatch-run.mjs +1 -0
  8. package/agent/family-tools.mjs +11 -6
  9. package/agent/helpers.mjs +6 -4
  10. package/agent/image-heal.mjs +65 -0
  11. package/agent/light-round.mjs +4 -4
  12. package/agent/live-beat.mjs +4 -4
  13. package/agent/record-results.mjs +9 -1
  14. package/agent/run-stages.mjs +2 -1
  15. package/agent/setup-reminders.mjs +14 -13
  16. package/agent/setup.mjs +62 -46
  17. package/agent/spawn-child.mjs +1 -1
  18. package/agent/subagent-event.mjs +1 -1
  19. package/agent/suspension.mjs +2 -1
  20. package/agent/timers.mjs +8 -8
  21. package/agent/turn-loop.mjs +1 -1
  22. package/agent-tools/advisor.mjs +1 -1
  23. package/agent-tools/async-discard.mjs +40 -5
  24. package/agent-tools/async-settle.mjs +3 -1
  25. package/agent-tools/audit-block.mjs +1 -1
  26. package/agent-tools/batch-paths.mjs +20 -20
  27. package/agent-tools/checkpoint.mjs +1 -1
  28. package/agent-tools/design-slots.mjs +44 -5
  29. package/agent-tools/doc-check.mjs +37 -0
  30. package/agent-tools/eng.mjs +5 -3
  31. package/agent-tools/manifest.mjs +18 -18
  32. package/agent-tools/parent-channel.mjs +3 -3
  33. package/agent-tools/review-facts.mjs +2 -2
  34. package/agent-tools/settings.mjs +1 -1
  35. package/agent-tools/spawn-gates.mjs +8 -6
  36. package/agent-tools/ssh-session.mjs +439 -0
  37. package/agent-tools/ssh-transport.mjs +311 -0
  38. package/agent-tools/subagent-scheduler.mjs +6 -5
  39. package/agent-tools/subagent.mjs +7 -5
  40. package/agent-tools/task.mjs +1 -1
  41. package/agent-tools/timer.mjs +6 -0
  42. package/agent-tools/zero-write-watch.mjs +2 -2
  43. package/agent-tools.mjs +3 -2
  44. package/attachments.mjs +4 -4
  45. package/browser/cdp.mjs +2 -2
  46. package/browser/launch.mjs +50 -4
  47. package/browser/snapshot.mjs +1 -1
  48. package/config-io.mjs +7 -2
  49. package/config-watch.mjs +2 -2
  50. package/config.mjs +7 -0
  51. package/declaration.mjs +5 -1
  52. package/doc-check/anchors.mjs +100 -0
  53. package/doc-check/index.mjs +61 -0
  54. package/doc-check/width.mjs +131 -0
  55. package/file-refs.mjs +6 -6
  56. package/generate-title.mjs +1 -1
  57. package/hooks.mjs +1 -1
  58. package/i18n.mjs +11 -1
  59. package/ledger-cmd.mjs +4 -5
  60. package/ledger-db.mjs +100 -21
  61. package/ledger-migrate.mjs +3 -3
  62. package/ledger-read.mjs +6 -7
  63. package/ledger-tools.mjs +24 -9
  64. package/ledger.mjs +23 -54
  65. package/log.mjs +17 -3
  66. package/manifest-discovery.mjs +21 -18
  67. package/manifest-schema.mjs +2 -13
  68. package/manifest.mjs +37 -22
  69. package/mcp/transport-stdio.mjs +1 -1
  70. package/mcp.mjs +2 -2
  71. package/memory/core.mjs +1 -1
  72. package/memory/scan.mjs +1 -1
  73. package/memory/sweep.mjs +1 -1
  74. package/model-specs.mjs +7 -7
  75. package/notify-policy.mjs +6 -6
  76. package/package.json +2 -1
  77. package/permission.mjs +14 -0
  78. package/prompts/advisor-design.md +43 -13
  79. package/prompts/advisor-round1.md +25 -5
  80. package/prompts/advisor-round2.md +17 -4
  81. package/prompts/advisor-round3.md +17 -4
  82. package/prompts/common.md +60 -6
  83. package/prompts/consult-base.md +8 -2
  84. package/prompts/discipline-engineering.md +11 -10
  85. package/prompts/discipline-normal.md +22 -14
  86. package/prompts/persona-coder.md +1 -1
  87. package/prompts/persona-eng-designer.md +8 -3
  88. package/prompts/persona-engineering.md +21 -5
  89. package/prompts/persona-normal.md +4 -1
  90. package/provider/errors.mjs +8 -0
  91. package/provider/google.mjs +3 -0
  92. package/provider/normalize.mjs +42 -12
  93. package/provider/responses-request.mjs +1 -1
  94. package/provider/responses.mjs +2 -2
  95. package/provider/wait-status.mjs +1 -1
  96. package/provider-flows.mjs +1 -1
  97. package/queued.mjs +2 -2
  98. package/rules.mjs +4 -4
  99. package/session-gc.mjs +1 -1
  100. package/session-lifecycle.mjs +1 -1
  101. package/session-slot-write.mjs +2 -2
  102. package/session-slots-manifest.mjs +1 -1
  103. package/session.mjs +1 -1
  104. package/shell-candidates.mjs +2 -2
  105. package/shell-identity.mjs +1 -1
  106. package/team.mjs +61 -4
  107. package/think-off.mjs +3 -3
  108. package/token-ttl.mjs +1 -1
  109. package/tool-docs/bash.md +1 -1
  110. package/tool-docs/browser.md +1 -1
  111. package/tool-docs/doc_check.md +31 -0
  112. package/tool-docs/manifest.md +14 -2
  113. package/tool-docs/ssh.md +34 -0
  114. package/tool-docs/subagent.md +2 -0
  115. package/tool-docs/timer.md +2 -1
  116. package/tools/browser.mjs +1 -1
  117. package/tools/execute.mjs +1 -1
  118. package/tools/git-run.mjs +2 -2
  119. package/tools/git.mjs +8 -8
  120. package/tools/index.mjs +3 -0
  121. package/tools/ops.mjs +1 -1
  122. package/tools/process-tree.mjs +2 -2
  123. package/tools/ssh-askpass.mjs +35 -0
  124. package/tools/ssh.mjs +92 -0
  125. package/vision-reader.mjs +4 -4
package/rules.mjs CHANGED
@@ -69,7 +69,7 @@ export function discoverRules(projectRoot) {
69
69
  // ─── A 面:stream 规则装配合并(`.thincoder/rules`——B7 3a 自核装配内联块抽取单源)────────
70
70
 
71
71
  /** 装配合并:`.thincoder/rules/` 文件规则**置前**,config 规则按 `pattern` 去重(文件侧优先;
72
- * 逐字镜像原 CLI `make-agent.mjs:44-48` 语义)。文件规则为空 ⇒ 归一数组原样返回(零拷贝零改)。
72
+ * 逐字镜像原 CLI `make-agent.mjs` 语义)。文件规则为空 ⇒ 归一数组原样返回(零拷贝零改)。
73
73
  * `discover` 可注入(核装配 `DEFAULT_DEPS.discoverRules` 缝——U79 装配序可机验;VSC 端壳同取本件)。 */
74
74
  export function mergeFileRules(configRules, cwd, discover = discoverRules) {
75
75
  const rules = Array.isArray(configRules) ? configRules : []
@@ -79,7 +79,7 @@ export function mergeFileRules(configRules, cwd, discover = discoverRules) {
79
79
  return [...fileRules, ...rules.filter((r) => !filePatterns.has(r.pattern))]
80
80
  }
81
81
 
82
- // ─── B 面:作用域规则读取面(`.cursor/rules`——R10 上提;纯搬自 VSC `src/extension/rules.mjs:28-81`)──
82
+ // ─── B 面:作用域规则读取面(`.cursor/rules`——R10 上提;纯搬自 VSC 端壳)──
83
83
 
84
84
  /**
85
85
  * Load all scoped rules from `.cursor/rules/`.
@@ -171,8 +171,8 @@ export function scopedRulesBlock(agent, cwd) {
171
171
  return `\n\nProject rules (.cursor/rules):\n${rules.always.map((r) => `- ${r.name}: ${r.content}`).join("\n")}`
172
172
  }
173
173
 
174
- /** 文件路径 glob 匹配(简单 `*` / `**` / `?` 通配——无完整 glob 库;纯搬自 VSC
175
- * `src/extension/rules.mjs:37-74`,零语义改)。 */
174
+ /** 文件路径 glob 匹配(简单 `*` / `**` / `?` 通配——无完整 glob 库;纯搬自 VSC 端壳,零语义改)。
175
+ * */
176
176
  export function matchesGlob(filePath, patterns) {
177
177
  if (!patterns || patterns.length === 0) return false
178
178
  // Normalize to forward slashes
package/session-gc.mjs CHANGED
@@ -37,7 +37,7 @@ import { configDir } from "./config.mjs"
37
37
  import { sessionPath, ownerPid } from "./session-slots.mjs"
38
38
  import { probeOwnersAsync, ownerState } from "./process-probe.mjs"
39
39
  import { listStaleCwds, deleteStaleCwd, recycleGroup, sweepStale, trashRootFor } from "./session-stale.mjs"
40
- // sidecar 记录目录后缀常量(单源 = `session-segments.mjs:13`——本档经该常量判后缀形态;
40
+ // sidecar 记录目录后缀常量(单源 = `session-segments.mjs` 的 `RECORD_DIR_SUFFIX`——本档经该常量判后缀形态;
41
41
  // 两处名形匹配式(`classifyResidue` / `sweepOrphanRecordDirs`)按 `{prefix}.{N}` + 后缀内联,不构成第二单源)。
42
42
  import { RECORD_DIR_SUFFIX } from "./session-segments.mjs"
43
43
 
@@ -131,7 +131,7 @@ export function applySession(agent, data, opts = {}) {
131
131
  }
132
132
  // ENG-PLAN-EXCLUSION(FR31 ③ / KD10——恢复点①·CLI `applySession`):工程模式 ⇒ planMode 恒
133
133
  // false(内存位 + 未注入的 plan 提示语)——防「工程纪律 × plan 只读」半状态随槽恢复复活。
134
- // **槽不就地回写**:内存值在下一次 `saveSession` 随 `planMode` 字段自然收正(`session.mjs:128`)。
134
+ // **槽不就地回写**:内存值在下一次 `saveSession` 随 `planMode` 字段自然收正(`session.mjs`)。
135
135
  if (agent.config?.agent?.engineering === true) clearPlanMode(agent)
136
136
  if (data.advisor) {
137
137
  agent.config.advisor = { ...data.advisor }
@@ -108,7 +108,7 @@ export function saveSlotData(cwd, slot, data) {
108
108
  if (data.createdBy === undefined && !existsSync(p)) data.createdBy = sessionEnd()
109
109
  writeSessionFile(p, data)
110
110
  // 写后取 mtime 单次采集(读失败 ⇒ `0` = 不可得):缓存(本档路径键)+ 摘要 `ts` 地板入参
111
- // (#503——保存面写后取 mtime,同族先例 `session-slot-verify.mjs:99`)。
111
+ // (#503——保存面写后取 mtime,同族先例 `session-slot-verify.mjs` 的 `runPass`)。
112
112
  const wroteMtime = (() => { try { return statSync(p).mtimeMs } catch { return 0 } })()
113
113
  slotMtimeCache.set(p, wroteMtime)
114
114
  try {
@@ -214,7 +214,7 @@ export function resolveEffortPatch(level, model) {
214
214
  * `activeModel`(不新增字段;`null` = 缺键语义——`provider` / `model` 无「未设」态);`effort` 经
215
215
  * `resolveEffortPatch` 归一(`model` 取 `patch.model ?? 槽现值 activeModel`),归一 `null` = 未设
216
216
  * (回落配置面 / 渠道默认)——**值置 `null` 不删键**(与 `newSlotData` 规范结构同源)。
217
- * 只写盘:不碰当前内存态(施加面唯一 = `applySession`——§6.21 判据句 4 / 边界表第 5 行)。
217
+ * 只写盘:不碰当前内存态(施加面唯一 = `applySession`——§6.21 判据句 4 / 边界表「写非活动槽」行)。
218
218
  *
219
219
  * **值域面(#441② 明文——核侧有意不校)**:本出口只校**键闭集**,**不校值域**——`provider` /
220
220
  * `model` 传 `null`(或表外字面)按**缺键语义**零改(不报错;`null` 仅对 `effort` 有「未设」
@@ -68,7 +68,7 @@ export function slotDigest(data, mtimeMs) {
68
68
  const isNum = (v) => typeof v === "number" && Number.isFinite(v)
69
69
 
70
70
  /** 摘要 `ts` 打点(**单源** · #503 地板):`max(墙钟, mtimeMs)`;`mtimeMs` 不可得(缺省 / 非数)⇒
71
- * 裸墙钟回溯。沿先例 `session-slot-verify.mjs:99`(回写面地板):**平台文件时间戳可领先
71
+ * 裸墙钟回溯。沿先例 `session-slot-verify.mjs` 的 `runPass`(回写面地板):**平台文件时间戳可领先
72
72
  * `Date.now()` 数 ms**——无地板则保存面刚写的摘要会被 `needsVerify`(`ts < mtime`)判「陈旧」
73
73
  * 一次(有界自纠,非数据面);地板后 `ts ≥ mtime` 恒成立(同一 stat 值即比较对象)。 */
74
74
  export function digestTs(mtimeMs) {
package/session.mjs CHANGED
@@ -183,7 +183,7 @@ export function saveSession(agent, opts = {}) {
183
183
  if (agent._recordStore) saveProjectedSlot(agent, p, fields, contextHistory)
184
184
  else writeSessionFile(p, { ...fields, history: legacyHistory(agent), contextHistory })
185
185
  // 记录我们刚写的 mtime——下次保存跳过解析(缓存 = `{p, mtimeMs}` 按路径命中:#386)+
186
- // 摘要 `ts` 地板入参(#503:保存面**写后取** mtime——同族先例 session-slot-verify.mjs:99)。
186
+ // 摘要 `ts` 地板入参(#503:保存面**写后取** mtime——同族先例 `session-slot-verify.mjs` 的 `runPass`)。
187
187
  let wroteMtimeMs
188
188
  try {
189
189
  wroteMtimeMs = statSync(p).mtimeMs
@@ -1,6 +1,6 @@
1
1
  /**
2
- * shell-candidates.mjs — 平台 shell 候选探测**单源**(B10 S17 收编:桌面 `settings-env.mjs:27-62` ∥
3
- * VSC `src/extension/settings.mjs:72-115` 两造 ≈逐字 ⇒ 候选表 ∕ 探测序 ∕ 超时 ∕ memo 上提本档;
2
+ * shell-candidates.mjs — 平台 shell 候选探测**单源**(B10 S17 收编:桌面 `settings-env.mjs` ∥
3
+ * VSC `src/extension/settings.mjs` 两造 ≈逐字 ⇒ 候选表 ∕ 探测序 ∕ 超时 ∕ memo 上提本档;
4
4
  * 两端薄壳只留 re-export —— 端侧零自持候选表)。
5
5
  *
6
6
  * 面:`shellCandidates()` ⇒ `Promise<{ name, value }[]>`——「System default」恒首 + 平台候选
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * memo(进程生命周期 + 按配置值键控)先例 = `shell-candidates.mjs`(「shell 路径不热变化」同判据);
12
12
  * 配置值变化 ⇒ memo 未命中重探(K2 配置变更刷新 ∥ 裁定①零静态同时成立)。
13
- * 测试缝 `_setComSpecForTest` 先例 = `shell-candidates.mjs:34` `_setShellDetectForTest`。
13
+ * 测试缝 `_setComSpecForTest` 先例 = `shell-candidates.mjs` `_setShellDetectForTest`。
14
14
  */
15
15
 
16
16
  // 识别表(raw 尾段词 → 显示名 + 语法族)。匹配大小写不敏感,尾段词先剥 `.exe` 后缀(U3——
package/team.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * team.mjs — 团队登录机制(TEAM.md §2——三端共用单源:CLI ∥ VSC ∥ 桌面同 import)。
3
3
  *
4
- * 面:登录 ∥ 退出 ∥ 登录态读面 ∥ 活校验(`teamVerify`——只读三值,登录面补全批增)∥ 端标签(自动生成)∥
5
- * 派生 provider 条目(登录写入 / 退出停用)∥ 服务地址归一 ∥ 失败分类。**写面单源** = 本档(`writeConfigAtomic`
4
+ * 面:登录 ∥ 退出 ∥ 轮换(`teamRotate`——team-token-rotation 批增)∥ 登录态读面 ∥ 活校验(`teamVerify`——只读三值,
5
+ * 登录面补全批增)∥ 端标签(自动生成)∥ 派生 provider 条目(登录写入 / 退出停用 / 轮换同步)∥ 服务地址归一 ∥
6
+ * 失败分类。**写面单源** = 本档(`writeConfigAtomic`
6
7
  * 一次 mutate——经 `persistRaw`;端侧零自写盘,三端同源)——`teamVerify` **只读**(零写盘、零状态)。
7
8
  *
8
9
  * 形(TEAM.md §2.1):顶层 `team` 段 `{ server, member{username,name}, label, token }`——`token` 在场 ⇔
@@ -14,9 +15,11 @@
14
15
  * 返回形(B1 批端侧契约——父侧裁定 2026-10-10):
15
16
  * `teamLogin` ⇒ `{ ok:true, notice?:"manual-name-conflict" }` ∥ `{ ok:false, reason }`
16
17
  * `teamLogout` ⇒ `{ ok:true, revokeDelivered:boolean }` ∥ `{ ok:false, reason }`
18
+ * `teamRotate` ⇒ `{ ok:true }` ∥ `{ ok:false, reason }`(子集:`network` ∥ `invalid`(401——旧 token 已失效)∥
19
+ * `write_failed`——TEAM.md §2.7)
17
20
  * `reason` 取值域:`network`(不可达 ∥ DNS/TLS ∥ 超时 ∥ 非 401/429 应答——服务不符合预期面)∥
18
- * `credentials`(401——同措辞)∥ `rate_limited`(429——锁定期)∥ `write_failed`(服务端已应答、本地
19
- * 写盘失败——mtime 冲突 ∥ 畸形档拒写)。`notice` 携**码**不携文(文本归端侧 i18n);
21
+ * `credentials`(401——登录流程同措辞)∥ `invalid`(401——轮换流程:旧 token 已失效)∥ `rate_limited`(429——锁定期)∥
22
+ * `write_failed`(服务端已应答、本地写盘失败——mtime 冲突 ∥ 畸形档拒写)。`notice` 携**码**不携文(文本归端侧 i18n);
20
23
  * `revokeDelivered:false` = 服务端吊销未达(网络失败——本地照清)。
21
24
  */
22
25
  import { hostname } from "node:os"
@@ -104,6 +107,60 @@ export async function teamVerify() {
104
107
  return { state: "unreachable" } // 其余(403 ∥ 404 地址非本服务 ∥ 5xx)⇒ 不判失效
105
108
  }
106
109
 
110
+ /**
111
+ * 轮换(TEAM.md §2.7;三端同构——team-token-rotation 批):`POST <server>/api/client/rotate`(Bearer = `team.token`;
112
+ * 体 `{}`——写端点 JSON 门照常)⇒ 成 ⇒ **一次写盘**:`team.token` = 新明文(替换)+ 派生条目 `apiKey` 同步
113
+ * (条目缺失 ∥ 同名手工 ⇒ 仅落 token——沿登录不覆盖口径);败 ⇒ 零写盘 + 出词。**非 best-effort**:网络败 ⇒
114
+ * 服务端零变更(旧 token 仍有效——重试即复原);401 ⇒ `invalid`(旧 token 已失效——端侧转「已失效」)。
115
+ * 无 token ∥ 无地址 ⇒ `network`(零请求旁路——端侧管理面本应已登录门);服务端语义单源 = CLIENT.md §2 rotate 行。
116
+ */
117
+ export async function teamRotate() {
118
+ const team = loadConfig().team
119
+ const token = typeof team?.token === "string" && team.token !== "" ? team.token : null
120
+ const server = typeof team?.server === "string" && team.server !== "" ? team.server : null
121
+ if (token === null || server === null) return { ok: false, reason: "network" } // 无凭据/无地址 ⇒ 兜底归类(零请求)
122
+ let res
123
+ try {
124
+ res = await fetch(`${server}/api/client/rotate`, {
125
+ method: "POST",
126
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
127
+ body: "{}",
128
+ signal: AbortSignal.timeout(TEAM_FETCH_TIMEOUT_MS),
129
+ })
130
+ } catch {
131
+ return { ok: false, reason: "network" } // 不可达 ∥ DNS ∥ TLS ∥ 超时(服务端零变更——旧 token 仍有效)
132
+ }
133
+ if (!res.ok) {
134
+ await res.text().catch(() => {}) // 错误体零消费面(分类只看状态——零新码)
135
+ if (res.status === 401) return { ok: false, reason: "invalid" } // 旧 token 已失效(端侧转「已失效」)
136
+ return { ok: false, reason: "network" } // 404(地址不是本服务)∥ 5xx 等——服务不符合预期面
137
+ }
138
+ let body = null
139
+ try {
140
+ body = await res.json()
141
+ } catch {
142
+ body = null
143
+ }
144
+ const next = typeof body?.token === "string" && body.token !== "" ? body.token : null
145
+ if (next === null) return { ok: false, reason: "network" } // 200 而形不符(代理 ∥ 非本服务应答)
146
+ try {
147
+ const result = persistRaw((raw) => {
148
+ assertWritableRoot(raw)
149
+ const section = raw.team
150
+ if (section === null || typeof section !== "object" || Array.isArray(section)) throw new Error("team 段缺失——拒写")
151
+ section.token = next // 新明文替换(一次写盘)
152
+ if (Array.isArray(raw.providers)) {
153
+ const entry = raw.providers.find((p) => p !== null && typeof p === "object" && p.name === TEAM_PROVIDER_NAME && p.derived === true)
154
+ if (entry !== undefined) entry.apiKey = next // 派生条目同步;缺失 ∥ 同名手工 ⇒ 仅落 token(零覆盖)
155
+ }
156
+ })
157
+ if (result !== null && result.ok === false) return { ok: false, reason: "write_failed" } // mtime 冲突(不自动合并)
158
+ } catch {
159
+ return { ok: false, reason: "write_failed" } // 畸形档拒写等(writeConfigAtomic fail-closed)
160
+ }
161
+ return { ok: true }
162
+ }
163
+
107
164
  /**
108
165
  * 登录(TEAM.md §2.2;三端同构):请求 `POST <server>/api/client/login` ⇒ 成 ⇒ **一次写盘**(`team` 段 +
109
166
  * 派生条目 upsert);败 ⇒ 分类出词、零写盘。`label` 缺省 ⇒ 自动生成(`defaultTeamLabel()`);显式传入
package/think-off.mjs CHANGED
@@ -17,15 +17,15 @@ export function thinkOffShape(spec) {
17
17
 
18
18
  /** 有效 off 路径(「何时可宣称 OFF」判据本体 = §16.4)——由 `thinkOffShape` 派生(防两处漂移):
19
19
  * `null` 形只有 effort 族的载荷门据它补发 `reasoning_effort:"none"` ⇒ 须枚举含 `"none"`
20
- * (无枚举 / 无 `none` ⇒ 形不发任何字段,§16.4 表第 1 行);`{type:"disabled"}` 形 ⇒ 达载荷层。
21
- * 两形共门 = `thinkAlwaysOn !== true`(服务端强制族:形被拒 / 恒思考 ⇒ 无效,§16.3 / §16.4 表第 2 行)。 */
20
+ * (无枚举 / 无 `none` ⇒ 形不发任何字段,§16.4 表「effort 族」行);`{type:"disabled"}` 形 ⇒ 达载荷层。
21
+ * 两形共门 = `thinkAlwaysOn !== true`(服务端强制族:形被拒 / 恒思考 ⇒ 无效,§16.3 / §16.4 表「服务端强制族」行)。 */
22
22
  export function thinkOffPath(spec) {
23
23
  if (spec?.thinkAlwaysOn === true) return false
24
24
  if (thinkOffShape(spec) === null) return (spec?.reasoningEffortEnum ?? []).includes("none")
25
25
  return true
26
26
  }
27
27
 
28
- /** advisor 推理档写面三态(**写语义单源** —— 由 VSC `settings-panel-write.mjs:149-164` 上提;B10 S11:
28
+ /** advisor 推理档写面三态(**写语义单源** —— 由 VSC `settings-panel-write.mjs` 的 `applyAdvisorEffort` 上提;B10 S11:
29
29
  * 桌面主侧 ∕ VSC 写面两端同引,零副本):`advisor` 对象原地作用于 `{ thinking, reasoningEffort }` 两键 ——
30
30
  * ① `"none"`(关思考)⇒ `thinking = thinkOffShape(spec)`(族别 off 形)+ 删 `reasoningEffort`;
31
31
  * ② 非空档字面 ⇒ `reasoningEffort = 值` + 清 off 形残记(`thinking` 为 `null` 字面 ∕ `{type:"disabled"}`
package/token-ttl.mjs CHANGED
@@ -277,7 +277,7 @@ export function persistEngTokens(agent) {
277
277
  data.updatedAt = Date.now()
278
278
  writeSessionFile(p, data)
279
279
  // 记录我们刚写的 mtime——下次保存/守卫跳过重复解析(saveSession 同款;缓存按路径命中 #386)
280
- // + 摘要 `ts` 地板入参(#503:保存面写后取 mtime——同族先例 session-slot-verify.mjs:99)。
280
+ // + 摘要 `ts` 地板入参(#503:保存面写后取 mtime——同族先例 `session-slot-verify.mjs` 的 `runPass`)。
281
281
  let wroteMtimeMs
282
282
  try {
283
283
  wroteMtimeMs = statSync(p).mtimeMs
package/tool-docs/bash.md CHANGED
@@ -10,7 +10,7 @@ Execute a shell command and return stdout+stderr. Use for running commands, buil
10
10
 
11
11
  Parameters:
12
12
  - command (required): Shell command to execute
13
- - timeout: Timeout in milliseconds (default 120000, max ~300000)
13
+ - timeout: Timeout in milliseconds (default 120000 for synchronous runs; background runs have no default — an explicit timeout still applies; no upper bound)
14
14
  - filter: Optional — a regex; only output lines matching it are returned (case-insensitive). Use instead of hand-writing a pipe into `findstr`/`grep`.
15
15
  - async: Run in the background (default false) — returns an ack at once (bash#<id>, log path); the result arrives as a digest when the process exits. Depth-0 only.
16
16
  {{inject:bash-terminal-face}}
@@ -45,4 +45,4 @@ Notes:
45
45
  - Session assumptions: the session may be closed externally or timed out — never assume it is alive; on `closed externally`, just run the action again (fresh session, login kept).
46
46
  - When not to use it: static page / API → `fetch`; a link needing no interaction → just produce the URL; a step needing a human (2FA, payment) → ask the user to do it in a headed session.
47
47
 
48
- CLI, VS Code and desktop share this description verbatim (single source: core `tool-docs/`).
48
+ CLI, VS Code and desktop share this description verbatim.
@@ -0,0 +1,31 @@
1
+ Run a built-in documentation-discipline check on a project and report its findings.
2
+ Read-only and non-gating: it reports, it never blocks, never sets an exit code, and its output is never a reason to refuse a task or a user instruction.
3
+
4
+ What it checks — exactly two criteria, both driven by the checked project's declarations:
5
+ - Anchors — every documentation pointer (a `*.md` token, optionally carrying directory segments, optionally followed by a section
6
+ indicator `§N` / `§N.M` / `§N.M.K`) must resolve: the file must exist on disk (`档缺` when it does not), and when a section
7
+ indicator is given the target file must carry a heading numbered with it (`节缺` when it does not). Line-number coordinates
8
+ (`file.md:12`) are not section references and are not judged as such. Files inside fenced code blocks and executable lines
9
+ (command / pattern code spans) are skipped whole.
10
+ - Line width — a line whose character count (CJK counts as 1) exceeds the declared threshold, unless it is a table row, inside a
11
+ fenced code block, an executable line, a quote-block line, or inside an exempt zone.
12
+
13
+ It cannot: write anything, gate anything, or turn a finding into a refusal.
14
+
15
+ Arguments — `target` (optional): the project root to check; defaults to the session anchor, relative paths resolve against it.
16
+ A workspace target (several projects below it) is reported with the full candidate list — pass the project root you mean.
17
+
18
+ Routing — use it when you write or revise a project's documents and want the declared discipline checked in one pass. It is not
19
+ a reader: use `doc_search` / `read` to read documents and `grep` to search them; this tool only reports discipline findings.
20
+ Do not wire it as a gate or CI step, and do not treat a finding as a reason to refuse an instruction — a machine check never
21
+ outranks a user instruction.
22
+
23
+ Side effects and permissions — none. The tool is read-only: no writes, no approval prompt, safe to run in parallel with other calls.
24
+
25
+ Parameter source and failure shape — every parameter comes from `<target>/PROJECT-MANIFEST.json` `checkConfig` (`scanDirs` /
26
+ `lineWidth` / `widthExemptZones` / `anchors.exclude`); no repository path is hard-coded, so another project runs it with its own
27
+ declarations. Changing those declarations changes the behavior. A missing or unreadable manifest is surfaced as an error line
28
+ naming the file — the check never fabricates a result.
29
+
30
+ Consistency — the checks and this description are one core implementation: every end that loads the core tool registry gets the
31
+ same semantics.
@@ -1,11 +1,11 @@
1
1
  Read, initialize, or point-edit the project manifest (PROJECT-MANIFEST.json) — the project-level state file (phase / docRoot / checkConfig / codePaths / index / advisor). Attached to the main agent only; subagents can neither see nor write it.
2
2
 
3
3
  Actions:
4
- - read [target] — report the manifest state with a stable anchor `state=ok|missing|invalid|ambiguous|no-project`. The ok state prints the manifest JSON plus missingKeys / unknownKeys / errors rows (only when non-empty). Never throws — a missing manifest is reported, not refused.
4
+ - read [target] — report the manifest state with a stable anchor `state=ok|missing|invalid|workspace|no-project`. The ok state prints the manifest JSON plus missingKeys / unknownKeys / errors rows (only when non-empty). Never throws — a missing manifest is reported, not refused.
5
5
  - init [target] — create the default manifest where the project root resolves (the target itself when no project is discoverable). Refused when a manifest already exists — init never overwrites.
6
6
  - write <key> <value> [target] — point-edit one node: read the current manifest, set the node, write it back through the writer gate. The resulting manifest must pass validation (a fault is refused with the disk byte-for-byte unchanged); a missing manifest is refused — run init first (write never creates one).
7
7
 
8
- target — explicit target directory; defaults to the session anchor, relative paths resolve against it. An ambiguous target (2+ candidate projects) is refused with the full candidate list — the mechanism never picks one.
8
+ target — explicit target directory; defaults to the session anchor, relative paths resolve against it. A workspace target (several projects below it) is reported by `read` and refused by `init` / `write` — with the facts, the full candidate list and a next step: name the project root you mean and pass it as target.
9
9
 
10
10
  key — dot path into the manifest, one of the writable nodes: version / phase / promptsLanding / codePaths, or a sub-key of docRoot / checkConfig / index / advisor (e.g. docRoot.design / index.publicRepos / advisor.docMap). Whole-object writes of those four families are refused, as is any key outside the set (spelling protection).
11
11
 
@@ -14,3 +14,15 @@ value — parsed as JSON first (null / true / 5 / ["a","b"]), falling back to a
14
14
  Refusals throw `manifest <action>: refused — …` and leave the disk unchanged. The written file is picked up by the session's project-state line on the next turn (mtime-gated) — the tool never touches in-memory state.
15
15
 
16
16
  read is read-only (planMode passes, no approval); init / write are side effects (approval gate). Writes go through writeManifest / initManifest with writer "main" — the fail-closed subagent default is never used.
17
+
18
+ Key catalog — the writable nodes: what each is; when to change it; wrong ⇒ consequence.
19
+ - `version` — schema version (a number); change on file migration; wrong ⇒ validation refuses (disk unchanged).
20
+ - `phase` — project stage, `initial-dev` / `production`; change when the stage is crossed; wrong ⇒ validation refuses.
21
+ - `promptsLanding` — declared prompt-landing directory; change when it moves; no failure consequence (no shape check, no in-core reader — a registration face).
22
+ - `codePaths` — path segment names (replaces the default `["src"]`); change when the layout moves; wrong ⇒ no segment is classified (the fallback still runs).
23
+ - `docRoot.requirements` · `docRoot.specs` · `docRoot.design` · `docRoot.modules` · `docRoot.batches` — each doc-layer root path (a string | a multi-root array | `null` = absent); change when a layer moves root; wrong ⇒ validation refuses, or checks / batch landing / review roots point elsewhere.
24
+ - `checkConfig.scanDirs` · `checkConfig.lineWidth` · `checkConfig.widthExemptZones` · `checkConfig.anchors` · `checkConfig.exemptions` — machine-check settings (scan dirs / width / exempt zones / anchors / exemptions); change when the check scope changes; wrong ⇒ checks run on a wrong scope silently (missed scans / wrong width / dead exemptions).
25
+ - `index.codeExtensions` · `index.docExtensions` — extra code / doc extensions (unioned, add-only); change when a file type is missing from the index; wrong ⇒ that type stays out of the index (extras harmless; a missing entry = a miss).
26
+ - `index.excludePaths` — project-root-relative path prefixes (for the index); change to keep a directory out of the index; wrong ⇒ that directory is still indexed, or another is wrongly excluded.
27
+ - `index.publicRepos` — public-repo paths (resolvable from this repo root); change when wiring a public repo into the index or cross-repo refs; wrong ⇒ that repo is unreachable for retrieval (empty origin set / wrong path).
28
+ - `advisor.docMap` · `advisor.standardsDoc` — review pointers (map file / single standards doc; empty = not declared); change when the review injection face changes; wrong ⇒ review injection lands nothing (an unreadable doc ⇒ review with no doc).
@@ -0,0 +1,34 @@
1
+ Open and drive a persistent interactive SSH session (remote login shell) on a host. The session survives across tool calls; password hosts get a credential window — the value never appears in tool args, results, or on disk.
2
+
3
+ **Route to ssh instead of bash:**
4
+ - a one-shot non-interactive command over ssh → `bash` with `ssh host <command>` (cheaper, no session)
5
+ - an interactive shell that must survive several calls (network device CLI, interactive prompts) → ssh
6
+
7
+ Parameters:
8
+ - action (required): connect | send | read | secret_input | close | list
9
+ - host: connect — target host (IP or name)
10
+ - user: connect — login user (optional: your local ssh config decides, else the user recorded for this host locally)
11
+ - port: connect — TCP port (optional, default 22)
12
+ - label: connect — short human label for this session (optional); secret_input — label for the value being sent
13
+ - promptPattern: connect — regex that marks the shell prompt as ready (optional): send/read then settle the moment the prompt appears instead of waiting out the silence window
14
+ - id: send / read / secret_input / close — session id from connect (`ssh#N`)
15
+ - data: send — text to type into the session (a trailing newline is passed as-is; otherwise Enter is sent as `\r`)
16
+ - settle: send — wait for the ready verdict before returning (default true); false returns right after the bytes are written
17
+ - timeoutMs: send / read — wait budget override (default 10000; the wait then returns partial output with a readable line)
18
+ - maxChars: read — return cap override (default 8000; the full output stays in the session log)
19
+
20
+ Output:
21
+ - every result starts with the session status line: `[ssh (ssh#3) 10.0.0.5 — ready · idle 3s]`
22
+ - connect returns the first screen; send returns the round's output plus the ready verdict (silence | prompt matched | timeout); read returns what arrived since the last read
23
+
24
+ Notes:
25
+ - Credential discipline: the password is asked for in a window and reaches ssh only through its child-process environment — never in tool args, never echoed in a result, never written to disk. Any captured output containing that value (or a value given to secret_input) is scrubbed from the returned text and from the session log.
26
+ - secret_input is for device prompts (enable password, sudo, key passphrases): it opens the same kind of window, writes the value plus Enter, and returns only the output — the value is never shown.
27
+ - Sessions: at most 4 live at once (a connect beyond the cap is refused — close one first). A session idle for 600s with no send/read is closed automatically. `close` ends it (stdin EOF → 1.5s grace → the process tree is killed). Ctrl+I (interrupt) does not kill sessions; a user Stop does.
28
+ - Output is capped per return (8000 chars, tail kept); the full stream is always in the session log whose path connect returns.
29
+ - Not for full-screen terminal UIs (vim/top/screen): output is read as a text stream — paged output such as `--More--` is shown as-is, not driven for you.
30
+ - Host keys follow your local ssh config — the tool never rewrites known_hosts. A changed or untrusted key stops the connect with a readable error before any credential is asked for.
31
+ - read / list are read-only (no approval prompt); connect / send / secret_input / close pass the approval gate like any side-effecting tool, and secret_input shows a masked placeholder — the value itself is never in the approval text.
32
+ - Not for file transfer or port forwarding — run `scp` / `sftp` / `ssh -L` through bash.
33
+
34
+ Availability: where the end has no UI to collect a credential (headless / ACP), connecting to a password host fails with a readable error — use keys or an ssh-agent there.
@@ -8,6 +8,8 @@ ONE tool, NINE actions — pick by what you need:
8
8
  - action:'cancel': STOP ONE background subagent — the id from the spawn return is REQUIRED (a blanket cancel is unsupported; Ctrl+C stops everything). Running aborts immediately ({id, status:'cancelled'}); queued is removed ({id, status:'cancelled', was:'queued'}; later positions shift). Other children and the session keep running. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first, and prefer scoped recovery — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited. Background advisor reviews cancel the same way (pass the review id — no token is issued for a cancelled review).
9
9
  - action:'consume-design' (engineering mode, parent side): chain-terminal token consumption — after the delivery is verified and the chain closes out, consume this design's token slot. Selectors — pass exactly ONE: designId (a single slot; optional for a single-design session), designIds (an explicit non-empty list), expired:true (every expired slot), or olderThanDays:N (every slot older than N days). A consumed slot mechanically rejects further spawns for the same designId — new work (deviation fixes included) needs a fresh design review and token. Idempotent (unknown/already-consumed = a no-op notice); batch calls report per-slot lines + a `consumed N` count line. Do NOT call while the chain is still open — fix rounds reuse the same slot.
10
10
  - action:'design-slots' (engineering mode, parent side): READ-ONLY inventory of this session's approved design token slots — one line per slot (designId, live|expired, ageDays, expiresAt, batch), oldest first, ending with a total/live/expired count line. Nothing is written. Use it to see what still holds a slot before consuming; the expired / olderThanDays selectors act on the same set.
11
+ Filtering and recovery: pass designId to filter to a single slot (trimmed exact match; unknown = refused with the held id list; empty string = no filter); add reveal:true (requires designId) to read that slot's full designToken back — the recovery path for a token dropped from your context.
12
+ One slot at a time — never a bulk dump; expired / malformed slots are masked as designToken=—, a non-boolean reveal is refused, and the recovered designId + designToken go verbatim onto the eng-coder spawn (the gate checks exact equality).
11
13
  - action:'panel': DIAGNOSE + fix the subagent panel (the collapsible blocks the user sees; headless/VS Code degrade to a 'no panel' pool view). view (default): the live blocks (key, role, status: running|done|awaitingDigest) exactly as the user sees them — digested-stuck entries carry digested:true. freeze: pass a digested-stuck block key (e.g. 'eng-coder#9' — {action:'panel', freeze:'role#N'}) to reclaim it into the conversation — passes ONLY for awaitingDigest blocks with no live pool entry and no pending report (a still-pending report would break the digestion order — refused).
12
14
 
13
15
  Why delegate? The child's noise never reaches your window — delegation keeps your context lean and the child focused; parallel children save wall-clock time, and coder/eng-coder children self-review (verify + advisor) before their reports arrive.
@@ -8,5 +8,6 @@ Beyond a one-shot countdown, three schedule forms are available, one per call (`
8
8
  All times are local to the host.
9
9
  `cancel` takes the id from the set confirmation and stops that timer; `list` shows every pending timer with its next due time. Repeating timers occupy one pending slot each until cancelled.
10
10
  Returns the set confirmation — the reminder fires at the deadline.
11
- Delivery: while a turn is running it lands at the next step boundary; when the session is idle it is delivered on the spot and starts a turn — the idle wake is available on the CLI / VSC / desktop foregrounds and suspension windows (headless is structurally unsupported); other paths keep the step-boundary behavior.
11
+ Delivery: when the session is idle it is delivered on the spot and starts a turn — the idle wake is available on the CLI / VSC / desktop foregrounds and suspension windows (headless is structurally unsupported).
12
+ A user turn gets no delivery: while a session turn is running, an expired timer waits for the next delivery round (suspension window / idle latch); other paths keep the step-boundary behavior.
12
13
  A pending timer survives across runs; at most 8 timers may be pending at once, and further timer calls are rejected until some expire.
package/tools/browser.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  * `clipboard` 全量;免审 = `navigate` ∥ `type` ∥ `snapshot` ∥ `screenshot` ∥ `wait` ∥ `close` +
7
7
  * `hover` ∥ `wheel` ∥ `insert` ∥ `touch` 手势(swipe / pinch)——同一钩子按参数分类,机制零新增。
8
8
  * 失败回执**返回**(不 throw)——首行 `Error: …` = 模型面失败形(`bash`/`execute` 既例;dispatch 侧按该前缀
9
- * 判失败——`agent/dispatch-run.mjs:77` ∥ `:129` ∥ `:135`)。
9
+ * 判失败——`agent/dispatch-run.mjs` 的 `runPreparedItem`(三处))。
10
10
  * 异步面(§2.11——批 browser-async-fix):`async:true`(depth-0 专项)⇒ `agent-tools/browser-async.mjs`
11
11
  * 起跑(ack 即返——本档薄接线;depth 第二道在此,schema 删参在 `agent/helpers.mjs` `excludeSubagentTools`)。
12
12
  */
package/tools/execute.mjs CHANGED
@@ -61,7 +61,7 @@ function applyFilter(output, filter) {
61
61
  }
62
62
 
63
63
  /** 树杀单源 = `tools/process-tree.mjs`(TOOLS.md §6.14 落位表行 2:自本档抽出以断
64
- * `shared → git-run → execute → shared` 环);**再导出面保留**(`test/tool-seams.test.mjs:26` 消费,
64
+ * `shared → git-run → execute → shared` 环);**再导出面保留**(
65
65
  * `configureProcessTreeKill` 缺省径零改)。 */
66
66
  export { killProcessTree }
67
67
 
package/tools/git-run.mjs CHANGED
@@ -33,7 +33,7 @@ export const GIT_NET_TIMEOUT_MS = 300_000
33
33
  /** 网路面五动作(300s 档——合法耗时可远超本地;依据 = 需求档 §4.7 TTY-DRIVE N3 候选参照值)。 */
34
34
  const GIT_NET_ACTIONS = new Set(["push", "fetch", "pull", "clone", "ls-remote"])
35
35
 
36
- /** 测试态缝(模块级 · 缺省 null = 生产零行为变——先例 `manifest-discovery.mjs:15-20` · `session-gc.mjs:186`)。
36
+ /** 测试态缝(模块级 · 缺省 null = 生产零行为变——先例 `manifest-discovery.mjs` 的 `_setProjectRootForTest` · `session-gc.mjs` 的 `_setSessionGcDelayForTest`)。
37
37
  * 用例 `finally` 复位;不经用户参数面(边界:不新增参数 / 用户选项)。 */
38
38
  let gitTimeoutOverride = null
39
39
  export function _setGitTimeoutForTest(ms) { gitTimeoutOverride = ms }
@@ -100,7 +100,7 @@ export function spawnGit(cwd, args, { timeout, maxBuffer } = {}) {
100
100
  }
101
101
  const treeKill = () => { try { killProcessTree(child) } catch { /* 尽力而为 */ } }
102
102
  // 超时动作序(§6.14):① 直接子 → ② 逾 1.5s 树杀(`killProcessTree`,尽力而为)→ ③ 逾 1.5s 未 `close`
103
- // 亦 settle(kick——孙进程持管道时 `close` 永不触发,`bash.mjs:200-217` 同款)。上界 = timeout + 3s。
103
+ // 亦 settle(kick——孙进程持管道时 `close` 永不触发,`bash.mjs` 的 `runBash` 同款)。上界 = timeout + 3s。
104
104
  // 平台分岔(§6.14 平台注的后果):win32 上 SIGTERM 实为硬终止且**不连带子进程**——父一死,
105
105
  // `taskkill /PID <父> /T` 就够不到孙进程(实测:helper 孤儿存活、管道不放 ⇒ 只能等 kick)⇒
106
106
  // win32 直接走树杀(须在树仍可寻址时执行);POSIX 保留宽限序(组杀在头进程死后仍可达全组)。
package/tools/git.mjs CHANGED
@@ -40,14 +40,14 @@ function resolveBaseDir(cwd, workdir) {
40
40
  * 二者仍可经显式 `workdir` 指落点。 */
41
41
  const NO_REPO_DISCOVERY = new Set(["init", "clone"])
42
42
 
43
- /** 歧义(多态)消息:候选全列(绝对路径、按名排序)+ 指引显式 `workdir`——不猜(§6.13 解析序行 5);
43
+ /** 工作区(多仓)消息:下辖仓全列(绝对路径、按名排序)+ 指引显式 `workdir`(§6.13 解析序行 5);
44
44
  * **两档变体**(#188——按 `matched`):带档级 ⇒ 「.git ∧ PROJECT-MANIFEST.json」;裸仓级 ⇒ 「.git」
45
- * 且注明 none carries …(不书带档句——§6.13 A22)。 */
46
- function ambiguousRepoMessage(candidates, cwd, matched) {
45
+ * 且注明 none carries …(不书带档句——§6.13 A22)。事实行,非错误框定(用户裁定「cwd 下多仓是常态」)。 */
46
+ function workspaceRepoMessage(candidates, cwd, matched) {
47
47
  const head = matched === "git"
48
- ? `Ambiguous git repo at ${cwd}: ${candidates.length} subdirectories carry .git (none carries ${MANIFEST_REL})`
49
- : `Ambiguous git repo at ${cwd}: ${candidates.length} subdirectories carry .git and ${MANIFEST_REL}`
50
- return `${head} — pass workdir to run git in the intended one (no silent pick):\n` + candidates.map((c) => `- ${c}`).join("\n")
48
+ ? `${cwd} is a workspace holding ${candidates.length} repositories — subdirectories carry .git (none carries ${MANIFEST_REL})`
49
+ : `${cwd} is a workspace holding ${candidates.length} repositories — subdirectories carry .git and ${MANIFEST_REL}`
50
+ return `${head} — pass workdir to run git in the intended one:\n` + candidates.map((c) => `- ${c}`).join("\n")
51
51
  }
52
52
 
53
53
  export const gitTool = {
@@ -96,13 +96,13 @@ export const gitTool = {
96
96
  if (args.workdir) ctx = { ...ctx, cwd: resolveBaseDir(ctx.cwd, args.workdir) }
97
97
  // 仓发现(§6.13 · #62):缺省路径(无 workdir)⇒ 单源 = `discoverRepos`(仓梯——#188 含裸仓级)
98
98
  // ——工作区根(非仓)⇒ 唯一带档子仓 / 零档时唯一裸仓:重定向 `ctx.cwd` + 结果首行注记;零 ⇒ 原值
99
- // 落 §6.12 fail-closed(零行为变);多 ⇒ throw(列该级候选 + 指 workdir,不猜——消息按 `matched`
99
+ // 落 §6.12 fail-closed(零行为变);多 ⇒ throw(下辖仓全列 + 指 `workdir`——消息按 `matched`
100
100
  // 两档变体)。判据 = workdir 真值在场(与上行同源——空串与缺省同判);`init` / `clone` 例外
101
101
  // 不做发现。self / none 态 ctx 对象引用透传(不 clone)。
102
102
  let repoNote = ""
103
103
  if (!args.workdir && !NO_REPO_DISCOVERY.has(args.action)) {
104
104
  const discovered = discoverRepos(ctx.cwd)
105
- if (discovered.kind === "ambiguous") throw new Error(ambiguousRepoMessage(discovered.candidates, ctx.cwd, discovered.matched))
105
+ if (discovered.kind === "workspace") throw new Error(workspaceRepoMessage(discovered.candidates, ctx.cwd, discovered.matched))
106
106
  if (discovered.kind === "unique") {
107
107
  ctx = { ...ctx, cwd: discovered.root }
108
108
  repoNote = `(repo: ${discovered.root})`
package/tools/index.mjs CHANGED
@@ -14,6 +14,7 @@ import { executeTool } from "./execute.mjs";
14
14
  import { fileOpsTool, processTool, getCurrentTimeTool, waitForTool } from "./ops.mjs";
15
15
  import { treeTool } from "./tree.mjs";
16
16
  import { browserTool } from "./browser.mjs";
17
+ import { sshTool } from "./ssh.mjs";
17
18
 
18
19
  // Instance-independent built-in table. `read_image` is deliberately NOT here:
19
20
  // its registration is capability-gated per model — see `assembleBuiltinTools`.
@@ -26,6 +27,7 @@ export const builtinTools = [
26
27
  fileOpsTool, processTool, getCurrentTimeTool, waitForTool,
27
28
  treeTool,
28
29
  browserTool,
30
+ sshTool,
29
31
  ];
30
32
 
31
33
  export {
@@ -37,6 +39,7 @@ export {
37
39
  fileOpsTool, processTool, getCurrentTimeTool, waitForTool,
38
40
  treeTool,
39
41
  browserTool,
42
+ sshTool,
40
43
  };
41
44
 
42
45
  // ── Full built-in registry (CORE-UNIFICATION TOOLS #70) ─────────────────────
package/tools/ops.mjs CHANGED
@@ -60,7 +60,7 @@ export const processTool = {
60
60
  id: { type: "number", description: "kill target: background task id — a bash#N (bash async ack) or browser#N (browser async ack): bash ⇒ its process tree is terminated; browser ⇒ a queued task is dropped, a running one is aborted (the browser itself is not killed); either way the entry leaves the pool and no digest will arrive." },
61
61
  },
62
62
  },
63
- // #9:动作分级——list 保持只读分类(dispatch-gates `isSubagentReadonlyAction`:planMode 放行/
63
+ // #9:动作分级——list 保持只读分类(dispatch-gates `isReadonlyAction`:planMode 放行/
64
64
  // 免审批);kill 是破坏性动作(审批门)。`parallel` 保既有调度形(list 恒可批并行)。
65
65
  readonly: false,
66
66
  parallel: true,
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * tools/process-tree.mjs — 平台感知树杀单源(自 `execute.mjs` 抽出——TOOLS.md §6.14 落位表行 2)。
3
- * 抽出理由 = **断环**:`shared → git-run → execute → shared` 会 TDZ(`execute.mjs:29` 在模块求值期
3
+ * 抽出理由 = **断环**:`shared → git-run → execute → shared` 会 TDZ(`execute.mjs` 在模块求值期
4
4
  * 调 `DESC`(`const`))⇒ 树杀须住两不依赖档。行为逐字同(win32 `taskkill /T /F` · POSIX 组杀 + 直杀兜底)。
5
- * `execute.mjs` 保持再导出面(`test/tool-seams.test.mjs:26` 消费)。
5
+ * `execute.mjs` 保持再导出面。
6
6
  */
7
7
  import { execFileSync } from "node:child_process"
8
8
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ssh-askpass.mjs — SSH 口令助手(独立进程入口;设计 = `docs/core/design/TOOLS.md` §6.22 ② · D-TO18)。
3
+ *
4
+ * 调用面 = ssh 经包装档(`<node> <本档> %*`)执行本档:argv[1] = 提示语本体
5
+ * (上游 `readpass.c` `ssh_askpass()`:`execlp(askpass, askpass, msg, NULL)`)。
6
+ * 输出面 = stdout **仅一行**(ssh 读首行作答复;`readpass.c` 取到首个 \r|\n 即止)。
7
+ * 秘密来源 = 环境变量 `TC_SSH_SECRET`(ssh 子进程环境——不入 argv / ps;§6.22 ②「秘密传递」行)。
8
+ * 零日志 / 零落盘 / 零回显——本档只写 stdout,不碰任何持久面。
9
+ *
10
+ * 答复判定(三分支,逐条有据):
11
+ * 1. `SSH_ASKPASS_PROMPT === "confirm"` ⇒ `yes`——上游 `readpass.c` 的 `RP_ASK_PERMISSION`
12
+ * 径(如 `AddKeysToAgent=ask` 的 `ask_permission()`);设计 §6.22 ② 同句。
13
+ * 2. 有秘密且提示语是主机确认句 ⇒ `yes`——**host-key 确认径不设该环境提示**
14
+ * (`sshconnect.c` `confirm()` 走 `read_passphrase(msg, RP_ECHO)`,非 `RP_ASK_PERMISSION`
15
+ * ⇒ 无 hint),故以提示语首行判定(确认句首行 = `The authenticity of host …`)。
16
+ * 3. 有秘密 ⇒ 秘密本体(password ∥ keyboard-interactive 两径同走 `read_passphrase`)。
17
+ * 无秘密 ⇒ `yes`——无秘密的场合 = 预探连(`NumberOfPasswordPrompts=0`),此刻**只可能**
18
+ * 收到确认类提问:password ∥ keyboard-interactive 被该选项短路(`sshconnect2.c`
19
+ * `userauth_passwd` / `userauth_kbdint` 两处同判 `attempt++ >= number_of_password_prompts`)、
20
+ * 私钥口令提示亦被短路(`load_identity_file` 循环 `i <= number_of_password_prompts`)。
21
+ */
22
+ const SECRET_ENV = "TC_SSH_SECRET"
23
+ /** 主机确认句判据(首行;Windows `.cmd` 中介可能截断 argv——首行判据对截断不敏感)。 */
24
+ const HOST_CONFIRM_RE = /authenticity of host|continue connecting/i
25
+
26
+ const prompt = String(process.argv[2] ?? "")
27
+ const hint = String(process.env.SSH_ASKPASS_PROMPT ?? "")
28
+ const secret = process.env[SECRET_ENV]
29
+
30
+ let answer
31
+ if (hint === "confirm" || (secret && HOST_CONFIRM_RE.test(prompt))) answer = "yes"
32
+ else if (secret) answer = secret
33
+ else answer = "yes"
34
+
35
+ process.stdout.write(`${answer}\n`)