@thincoder/core 0.10.6 → 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.
- package/CHANGELOG.md +24 -0
- package/advisor/compaction.mjs +1 -1
- package/advisor/run.mjs +1 -1
- package/agent/chat-call.mjs +40 -27
- package/agent/child-marks.mjs +1 -1
- package/agent/dispatch-gates.mjs +7 -4
- package/agent/dispatch-run.mjs +1 -0
- package/agent/family-tools.mjs +11 -6
- package/agent/helpers.mjs +6 -4
- package/agent/image-heal.mjs +65 -0
- package/agent/light-round.mjs +4 -4
- package/agent/live-beat.mjs +4 -4
- package/agent/record-results.mjs +9 -1
- package/agent/run-stages.mjs +2 -1
- package/agent/setup-reminders.mjs +14 -13
- package/agent/setup.mjs +62 -46
- package/agent/spawn-child.mjs +1 -1
- package/agent/subagent-event.mjs +1 -1
- package/agent/suspension.mjs +2 -1
- package/agent/timers.mjs +8 -8
- package/agent/turn-loop.mjs +1 -1
- package/agent-tools/advisor.mjs +1 -1
- package/agent-tools/async-discard.mjs +40 -5
- package/agent-tools/async-settle.mjs +3 -1
- package/agent-tools/audit-block.mjs +1 -1
- package/agent-tools/batch-paths.mjs +20 -20
- package/agent-tools/checkpoint.mjs +1 -1
- package/agent-tools/design-slots.mjs +44 -5
- package/agent-tools/doc-check.mjs +37 -0
- package/agent-tools/eng.mjs +5 -3
- package/agent-tools/manifest.mjs +25 -18
- package/agent-tools/parent-channel.mjs +3 -3
- package/agent-tools/review-facts.mjs +2 -2
- package/agent-tools/settings.mjs +1 -1
- package/agent-tools/spawn-gates.mjs +8 -6
- package/agent-tools/ssh-session.mjs +439 -0
- package/agent-tools/ssh-transport.mjs +311 -0
- package/agent-tools/subagent-scheduler.mjs +6 -5
- package/agent-tools/subagent.mjs +7 -5
- package/agent-tools/task.mjs +1 -1
- package/agent-tools/timer.mjs +6 -0
- package/agent-tools/zero-write-watch.mjs +2 -2
- package/agent-tools.mjs +3 -2
- package/attachments.mjs +4 -4
- package/browser/cdp.mjs +2 -2
- package/browser/launch.mjs +50 -4
- package/browser/snapshot.mjs +1 -1
- package/config-io.mjs +7 -2
- package/config-watch.mjs +2 -2
- package/config.mjs +7 -0
- package/conventions.mjs +1 -1
- package/declaration.mjs +157 -13
- package/doc-check/anchors.mjs +100 -0
- package/doc-check/index.mjs +61 -0
- package/doc-check/width.mjs +131 -0
- package/file-refs.mjs +6 -6
- package/generate-title.mjs +1 -1
- package/hooks.mjs +1 -1
- package/i18n.mjs +11 -1
- package/ledger-cmd.mjs +4 -5
- package/ledger-db.mjs +100 -21
- package/ledger-migrate.mjs +3 -3
- package/ledger-read.mjs +6 -7
- package/ledger-tools.mjs +24 -9
- package/ledger.mjs +23 -54
- package/log.mjs +17 -3
- package/manifest-discovery.mjs +21 -18
- package/manifest-schema.mjs +2 -13
- package/manifest.mjs +37 -22
- package/mcp/transport-stdio.mjs +1 -1
- package/mcp.mjs +2 -2
- package/memory/code-search.mjs +21 -4
- package/memory/code-sync.mjs +7 -4
- package/memory/core.mjs +1 -1
- package/memory/docs.mjs +23 -6
- package/memory/file-list.mjs +13 -7
- package/memory/file-walk.mjs +9 -4
- package/memory/scan.mjs +1 -1
- package/memory/sweep.mjs +1 -1
- package/memory/sync-tail.mjs +11 -7
- package/model-specs.mjs +7 -7
- package/notify-policy.mjs +6 -6
- package/package.json +2 -1
- package/permission.mjs +14 -0
- package/prompts/advisor-design.md +43 -13
- package/prompts/advisor-round1.md +25 -5
- package/prompts/advisor-round2.md +17 -4
- package/prompts/advisor-round3.md +17 -4
- package/prompts/common.md +60 -6
- package/prompts/consult-base.md +8 -2
- package/prompts/discipline-engineering.md +11 -10
- package/prompts/discipline-normal.md +22 -14
- package/prompts/persona-coder.md +1 -1
- package/prompts/persona-eng-designer.md +8 -3
- package/prompts/persona-engineering.md +21 -5
- package/prompts/persona-normal.md +4 -1
- package/provider/errors.mjs +8 -0
- package/provider/google.mjs +3 -0
- package/provider/normalize.mjs +42 -12
- package/provider/responses-request.mjs +1 -1
- package/provider/responses.mjs +2 -2
- package/provider/wait-status.mjs +1 -1
- package/provider-flows.mjs +1 -1
- package/queued.mjs +2 -2
- package/rules.mjs +4 -4
- package/session-gc.mjs +1 -1
- package/session-lifecycle.mjs +1 -1
- package/session-slot-write.mjs +2 -2
- package/session-slots-manifest.mjs +1 -1
- package/session.mjs +1 -1
- package/shell-candidates.mjs +2 -2
- package/shell-identity.mjs +1 -1
- package/team.mjs +61 -4
- package/think-off.mjs +3 -3
- package/token-ttl.mjs +1 -1
- package/tool-docs/bash.md +1 -1
- package/tool-docs/browser.md +1 -1
- package/tool-docs/doc_check.md +31 -0
- package/tool-docs/manifest.md +14 -2
- package/tool-docs/ssh.md +34 -0
- package/tool-docs/subagent.md +2 -0
- package/tool-docs/timer.md +2 -1
- package/tools/browser.mjs +1 -1
- package/tools/execute.mjs +1 -1
- package/tools/git-run.mjs +2 -2
- package/tools/git.mjs +8 -8
- package/tools/index.mjs +3 -0
- package/tools/ops.mjs +1 -1
- package/tools/process-tree.mjs +2 -2
- package/tools/ssh-askpass.mjs +35 -0
- package/tools/ssh.mjs +92 -0
- package/vision-reader.mjs +4 -4
package/session-lifecycle.mjs
CHANGED
|
@@ -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
|
|
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 }
|
package/session-slot-write.mjs
CHANGED
|
@@ -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
|
|
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 /
|
|
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
|
|
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
|
|
186
|
+
// 摘要 `ts` 地板入参(#503:保存面**写后取** mtime——同族先例 `session-slot-verify.mjs` 的 `runPass`)。
|
|
187
187
|
let wroteMtimeMs
|
|
188
188
|
try {
|
|
189
189
|
wroteMtimeMs = statSync(p).mtimeMs
|
package/shell-candidates.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* shell-candidates.mjs — 平台 shell 候选探测**单源**(B10 S17 收编:桌面 `settings-env.mjs
|
|
3
|
-
* VSC `src/extension/settings.mjs
|
|
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」恒首 + 平台候选
|
package/shell-identity.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* memo(进程生命周期 + 按配置值键控)先例 = `shell-candidates.mjs`(「shell 路径不热变化」同判据);
|
|
12
12
|
* 配置值变化 ⇒ memo 未命中重探(K2 配置变更刷新 ∥ 裁定①零静态同时成立)。
|
|
13
|
-
* 测试缝 `_setComSpecForTest` 先例 = `shell-candidates.mjs
|
|
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 条目(登录写入 /
|
|
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
|
|
19
|
-
*
|
|
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
|
|
21
|
-
* 两形共门 = `thinkAlwaysOn !== true`(服务端强制族:形被拒 / 恒思考 ⇒ 无效,§16.3 / §16.4
|
|
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
|
|
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
|
|
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
|
|
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}}
|
package/tool-docs/browser.md
CHANGED
|
@@ -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
|
|
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.
|
package/tool-docs/manifest.md
CHANGED
|
@@ -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|
|
|
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.
|
|
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).
|
package/tool-docs/ssh.md
ADDED
|
@@ -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.
|
package/tool-docs/subagent.md
CHANGED
|
@@ -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.
|
package/tool-docs/timer.md
CHANGED
|
@@ -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:
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
43
|
+
/** 工作区(多仓)消息:下辖仓全列(绝对路径、按名排序)+ 指引显式 `workdir`(§6.13 解析序行 5);
|
|
44
44
|
* **两档变体**(#188——按 `matched`):带档级 ⇒ 「.git ∧ PROJECT-MANIFEST.json」;裸仓级 ⇒ 「.git」
|
|
45
|
-
* 且注明 none carries …(不书带档句——§6.13 A22
|
|
46
|
-
function
|
|
45
|
+
* 且注明 none carries …(不书带档句——§6.13 A22)。事实行,非错误框定(用户裁定「cwd 下多仓是常态」)。 */
|
|
46
|
+
function workspaceRepoMessage(candidates, cwd, matched) {
|
|
47
47
|
const head = matched === "git"
|
|
48
|
-
?
|
|
49
|
-
:
|
|
50
|
-
return `${head} — pass workdir to run git in the intended one
|
|
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
|
|
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 === "
|
|
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 `
|
|
63
|
+
// #9:动作分级——list 保持只读分类(dispatch-gates `isReadonlyAction`:planMode 放行/
|
|
64
64
|
// 免审批);kill 是破坏性动作(审批门)。`parallel` 保既有调度形(list 恒可批并行)。
|
|
65
65
|
readonly: false,
|
|
66
66
|
parallel: true,
|
package/tools/process-tree.mjs
CHANGED
|
@@ -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
|
|
3
|
+
* 抽出理由 = **断环**:`shared → git-run → execute → shared` 会 TDZ(`execute.mjs` 在模块求值期
|
|
4
4
|
* 调 `DESC`(`const`))⇒ 树杀须住两不依赖档。行为逐字同(win32 `taskkill /T /F` · POSIX 组杀 + 直杀兜底)。
|
|
5
|
-
* `execute.mjs`
|
|
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`)
|
package/tools/ssh.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/ssh.mjs — SSH 会话工具面(薄接线):schema ∥ 参数校验 ∥ `isReadonlyAction` ∥ 回执/错误形
|
|
3
|
+
* (设计 = `docs/core/design/TOOLS.md` §6.22 —— D-TO17/D-TO18 · 台账 #1436;机制单点 =
|
|
4
|
+
* `agent-tools/ssh-session.mjs`;描述档 = `tool-docs/ssh.md`)。
|
|
5
|
+
*
|
|
6
|
+
* 分类钩子(KD-2 ∥ KD-11 同式——动作级,工具级只读位恒 false):免审 = `list` ∥ `read`
|
|
7
|
+
* (只读豁免,NS3 判据);过门 = `connect` ∥ `send` ∥ `secret_input` ∥ `close`——**携密动作仍属
|
|
8
|
+
* 写动作**,门不放松;携密径审批文案 = 掩码占位(`permission.mjs`,值不回显 ⇒ 不入上下文 / 审批记录)。
|
|
9
|
+
*
|
|
10
|
+
* 失败回执**返回**(不 throw)——首行 `Error: …` = 模型面失败形(`bash` / `browser` 既例);
|
|
11
|
+
* 无 UI 考(缺 `ctx.onSecretInput` 回调)⇒ 机制档抛可读错(`question.mjs` 无 UI 抛例同旨)。
|
|
12
|
+
* 会话跨调用存活于池(非本档持有);depth>0 排除(`agent/helpers.mjs` `SUBAGENT_TOOL_EXCLUSIONS`)。
|
|
13
|
+
* 动态 import(W8 契约②:agent-tools 静态链达 node:sqlite——端壳静态闭包须保持零命中)。
|
|
14
|
+
*/
|
|
15
|
+
import { DESC } from "./shared.mjs"
|
|
16
|
+
|
|
17
|
+
export const SSH_ACTIONS = ["connect", "send", "read", "secret_input", "close", "list"]
|
|
18
|
+
|
|
19
|
+
/** 参数校验(缺必填 / 形错 ⇒ `Error: …`——不执行;§6.22 动作表逐动作必填面)。 */
|
|
20
|
+
export function validateArgs(args) {
|
|
21
|
+
const action = args?.action
|
|
22
|
+
if (!SSH_ACTIONS.includes(action)) {
|
|
23
|
+
return `Error: unknown ssh action "${action ?? ""}" — actions: ${SSH_ACTIONS.join(", ")}`
|
|
24
|
+
}
|
|
25
|
+
if (action === "connect") {
|
|
26
|
+
if (typeof args.host !== "string" || args.host.trim() === "") return "Error: connect requires host"
|
|
27
|
+
if (args.port !== undefined && args.port !== null && args.port !== "" && !Number.isFinite(Number(args.port))) {
|
|
28
|
+
return "Error: connect port must be a number"
|
|
29
|
+
}
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
if (action === "list") return null
|
|
33
|
+
if (typeof args.id !== "string" || args.id.trim() === "") {
|
|
34
|
+
return `Error: ${action} requires id (the session id from connect, form "ssh#N")`
|
|
35
|
+
}
|
|
36
|
+
if (action === "send" && typeof args.data !== "string") return "Error: send requires data"
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const sshTool = {
|
|
41
|
+
name: "ssh",
|
|
42
|
+
description: DESC("ssh"),
|
|
43
|
+
parameters: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
action: { type: "string", enum: SSH_ACTIONS, description: "Action to run (one per call): connect | send | read | secret_input | close | list." },
|
|
47
|
+
host: { type: "string", description: "connect: target host (IP or name)." },
|
|
48
|
+
user: { type: "string", description: "connect: login user (optional: your local ssh config decides, else the user recorded for this host locally)." },
|
|
49
|
+
port: { type: "number", description: "connect: TCP port (optional, default 22)." },
|
|
50
|
+
label: { type: "string", description: "connect: short human label for this session (optional); secret_input: label for the value being sent." },
|
|
51
|
+
promptPattern: { type: "string", description: "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." },
|
|
52
|
+
id: { type: "string", description: "send / read / secret_input / close: session id from connect (`ssh#N`)." },
|
|
53
|
+
data: { type: "string", description: "send: text to type into the session (a trailing newline is passed as-is; otherwise Enter is sent as `\\r`)." },
|
|
54
|
+
settle: { type: "boolean", description: "send: wait for the ready verdict before returning (default true); false returns right after the bytes are written." },
|
|
55
|
+
timeoutMs: { type: "number", description: "send / read: wait budget override (default 10000) — on expiry the call returns partial output with a readable line." },
|
|
56
|
+
maxChars: { type: "number", description: "read: return cap override (default 8000; the full output stays in the session log)." },
|
|
57
|
+
},
|
|
58
|
+
required: ["action"],
|
|
59
|
+
},
|
|
60
|
+
// 非工具级只读:动作级分类钩子接管(§6.22 NS3——`list` / `read` 免审);也因此不进只读角色的子代工具表。
|
|
61
|
+
readonly: false,
|
|
62
|
+
isReadonlyAction(args) {
|
|
63
|
+
return args?.action === "list" || args?.action === "read"
|
|
64
|
+
},
|
|
65
|
+
async execute(args, ctx) {
|
|
66
|
+
const invalid = validateArgs(args)
|
|
67
|
+
if (invalid) return invalid
|
|
68
|
+
// 动态 import(W8 契约②——与 bash / browser 异步面同式)。
|
|
69
|
+
const mod = await import("../agent-tools/ssh-session.mjs")
|
|
70
|
+
const parent = ctx?.agent ?? null
|
|
71
|
+
const action = args.action
|
|
72
|
+
let res
|
|
73
|
+
if (action === "connect") {
|
|
74
|
+
res = await mod.connectSsh(parent, ctx, {
|
|
75
|
+
host: args.host, user: args.user ?? null, port: args.port ?? null,
|
|
76
|
+
label: args.label ?? null, promptPattern: args.promptPattern ?? null,
|
|
77
|
+
})
|
|
78
|
+
} else if (action === "send") {
|
|
79
|
+
res = await mod.sendSsh(parent, ctx, { id: args.id, data: args.data, settle: args.settle !== false, timeoutMs: args.timeoutMs })
|
|
80
|
+
} else if (action === "read") {
|
|
81
|
+
res = await mod.readSsh(parent, ctx, { id: args.id, timeoutMs: args.timeoutMs, maxChars: args.maxChars })
|
|
82
|
+
} else if (action === "secret_input") {
|
|
83
|
+
res = await mod.secretInputSsh(parent, ctx, { id: args.id, label: args.label ?? null })
|
|
84
|
+
} else if (action === "close") {
|
|
85
|
+
res = await mod.closeSsh(parent, ctx, { id: args.id })
|
|
86
|
+
} else {
|
|
87
|
+
res = mod.listSsh(parent)
|
|
88
|
+
}
|
|
89
|
+
if (res?.error) return res.error.startsWith("Error:") ? res.error : `Error: ${res.error}`
|
|
90
|
+
return res?.text ?? "(no output)"
|
|
91
|
+
},
|
|
92
|
+
}
|