@thincoder/core 0.10.7 → 0.10.9

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 (149) hide show
  1. package/CHANGELOG.md +21 -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 +13 -8
  9. package/agent/helpers.mjs +12 -10
  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/post-turn.mjs +34 -0
  14. package/agent/record-results.mjs +9 -1
  15. package/agent/run-stages.mjs +45 -10
  16. package/agent/setup-reminders.mjs +14 -13
  17. package/agent/setup.mjs +72 -48
  18. package/agent/spawn-child.mjs +1 -1
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +2 -1
  21. package/agent/timers.mjs +8 -8
  22. package/agent/turn-loop.mjs +7 -1
  23. package/agent-tools/advisor.mjs +1 -1
  24. package/agent-tools/async-discard.mjs +40 -5
  25. package/agent-tools/async-settle.mjs +3 -1
  26. package/agent-tools/audit-block.mjs +5 -3
  27. package/agent-tools/batch-gate.mjs +198 -0
  28. package/agent-tools/batch-lifecycle.mjs +216 -36
  29. package/agent-tools/batch-paths.mjs +20 -20
  30. package/agent-tools/batch-skeleton.mjs +87 -0
  31. package/agent-tools/batch.mjs +11 -3
  32. package/agent-tools/checkpoint.mjs +1 -1
  33. package/agent-tools/context.mjs +28 -5
  34. package/agent-tools/design-slots.mjs +20 -7
  35. package/agent-tools/doc-check.mjs +75 -0
  36. package/agent-tools/eng.mjs +5 -3
  37. package/agent-tools/manifest.mjs +18 -18
  38. package/agent-tools/parent-channel.mjs +3 -3
  39. package/agent-tools/review-facts.mjs +2 -2
  40. package/agent-tools/settings.mjs +1 -1
  41. package/agent-tools/spawn-gates.mjs +108 -47
  42. package/agent-tools/ssh-session.mjs +486 -0
  43. package/agent-tools/ssh-transport.mjs +324 -0
  44. package/agent-tools/ssh-vault.mjs +291 -0
  45. package/agent-tools/subagent-scheduler.mjs +6 -5
  46. package/agent-tools/subagent-spawn.mjs +18 -11
  47. package/agent-tools/subagent.mjs +17 -9
  48. package/agent-tools/task.mjs +1 -1
  49. package/agent-tools/timer.mjs +8 -2
  50. package/agent-tools/zero-write-watch.mjs +2 -2
  51. package/agent-tools.mjs +3 -2
  52. package/attachments.mjs +4 -4
  53. package/browser/cdp.mjs +2 -2
  54. package/browser/launch.mjs +50 -4
  55. package/browser/snapshot.mjs +1 -1
  56. package/config-defaults.mjs +92 -0
  57. package/config-io.mjs +7 -2
  58. package/config-normalize.mjs +109 -0
  59. package/config-watch.mjs +2 -2
  60. package/config.mjs +88 -190
  61. package/declaration.mjs +5 -1
  62. package/doc-check/anchors.mjs +142 -0
  63. package/doc-check/index.mjs +210 -0
  64. package/doc-check/version.mjs +84 -0
  65. package/doc-check/width.mjs +132 -0
  66. package/file-refs.mjs +6 -6
  67. package/generate-title.mjs +1 -1
  68. package/hooks.mjs +1 -1
  69. package/i18n.mjs +13 -1
  70. package/ledger-cmd.mjs +314 -62
  71. package/ledger-db.mjs +122 -31
  72. package/ledger-migrate.mjs +18 -12
  73. package/ledger-read.mjs +6 -7
  74. package/ledger-tools.mjs +138 -77
  75. package/ledger.mjs +34 -62
  76. package/log.mjs +17 -3
  77. package/manifest-discovery.mjs +21 -18
  78. package/manifest-schema.mjs +17 -14
  79. package/manifest.mjs +37 -22
  80. package/mcp/transport-stdio.mjs +1 -1
  81. package/mcp.mjs +2 -2
  82. package/memory/core.mjs +1 -1
  83. package/memory/scan.mjs +1 -1
  84. package/memory/sweep.mjs +1 -1
  85. package/model-specs.mjs +7 -7
  86. package/notify-policy.mjs +6 -6
  87. package/package.json +2 -1
  88. package/permission.mjs +21 -1
  89. package/prompts/advisor-design.md +53 -23
  90. package/prompts/advisor-round1.md +34 -14
  91. package/prompts/advisor-round2.md +32 -19
  92. package/prompts/advisor-round3.md +34 -21
  93. package/prompts/common.md +148 -79
  94. package/prompts/consult-base.md +14 -8
  95. package/prompts/discipline-engineering.md +70 -66
  96. package/prompts/discipline-normal.md +86 -77
  97. package/prompts/persona-coder.md +6 -6
  98. package/prompts/persona-eng-coder.md +19 -18
  99. package/prompts/persona-eng-designer.md +40 -31
  100. package/prompts/persona-engineering.md +124 -92
  101. package/prompts/persona-explore.md +2 -2
  102. package/prompts/persona-normal.md +20 -17
  103. package/prompts/persona-plan.md +7 -7
  104. package/prompts/subagent-base.md +4 -4
  105. package/provider/anthropic.mjs +4 -2
  106. package/provider/core.mjs +4 -2
  107. package/provider/errors.mjs +8 -0
  108. package/provider/google.mjs +8 -1
  109. package/provider/normalize.mjs +42 -12
  110. package/provider/responses-request.mjs +4 -3
  111. package/provider/responses.mjs +2 -2
  112. package/provider/wait-status.mjs +1 -1
  113. package/provider-flows.mjs +1 -1
  114. package/queued.mjs +2 -2
  115. package/rules.mjs +4 -4
  116. package/session-gc.mjs +1 -1
  117. package/session-lifecycle.mjs +7 -7
  118. package/session-slot-write.mjs +2 -2
  119. package/session-slots-manifest.mjs +1 -1
  120. package/session.mjs +1 -1
  121. package/shell-candidates.mjs +2 -2
  122. package/shell-identity.mjs +1 -1
  123. package/team.mjs +63 -5
  124. package/think-off.mjs +3 -3
  125. package/token-ttl.mjs +1 -1
  126. package/token-window.mjs +1 -1
  127. package/tool-docs/bash.md +1 -1
  128. package/tool-docs/batch.md +1 -1
  129. package/tool-docs/browser.md +1 -1
  130. package/tool-docs/doc_check.md +54 -0
  131. package/tool-docs/fetch.md +3 -2
  132. package/tool-docs/ledger.md +7 -5
  133. package/tool-docs/manifest.md +14 -2
  134. package/tool-docs/ssh.md +51 -0
  135. package/tool-docs/subagent.md +6 -9
  136. package/tool-docs/timer.md +2 -1
  137. package/tool-docs/websearch.md +3 -1
  138. package/tools/browser.mjs +1 -1
  139. package/tools/execute.mjs +1 -1
  140. package/tools/git-run.mjs +2 -2
  141. package/tools/git.mjs +8 -8
  142. package/tools/index.mjs +3 -0
  143. package/tools/ops.mjs +1 -1
  144. package/tools/process-tree.mjs +2 -2
  145. package/tools/ssh-askpass.mjs +35 -0
  146. package/tools/ssh.mjs +105 -0
  147. package/tools/web-deepseek.mjs +172 -0
  148. package/tools/web.mjs +29 -4
  149. package/vision-reader.mjs +4 -4
@@ -1 +1 @@
1
- Batch-record lifecycle tool (一段一作者 — six append-only sections, one author each). actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic + source, plus optional ledger / board that fill the record's 台账 header line at creation) · append (add text to the END of YOUR section — existing lines are never rewritten or deleted) · status (update YOUR section's `**状态行**:` line — the value must carry exactly ONE legal keyword of your section's STATUS_WORDS entry, decorated only by surrounding symbols / a trailing ISO date / whitespace; prose goes to the separate `note` field, which lands as a parenthetical) · close (depth-0 only — freeze the record: §1 → 「已收口 <date>」, every further write is refused). Identity fixes what you may write (段白名单): main agent (depth 0) — create/close; append/status domains are mode-dependent: engineering — append §1/§4/§6, status §1; normal — append §1/§2/§4/§5/§6, status §1/§2/§5 (the main agent holds the designer seat and records direct implementation); eng-designer — §2; eng-coder — §5; a normal-mode coder bound via batchDoc — §5; design review (review binding) — §3. A write outside your own section is refused. Skeleton placeholders (dead literals — `#<编号>` / `<板块>` and the older `<BATCH-ID>` / `<讨论来源>`) in the record header's structural lines block append/status until filled: create with ledger / board fills them at creation; if omitted, the main agent fills the header by file edit right after create (citing these literals in a section's prose is fine — only the header is checked); `<§N 模板占位:…>` template lines are legal and never blocked. Target record: sub-agents and design reviews have NO path parameter in practice — the record arrives via the spawn binding / the review instance key (passing path is refused); depth-0 MAY pass path (若传则须可读), omitting it picks the unique in-flight record (0 or ≥2 in flight ⇒ refuse — pass path). A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). Credential values are stripped mechanically before writing (never write a token or designId value). Failures are hard and visible (no silent fallback): if a write is refused or fails, say so in your report — “§× 未写入”.
1
+ Batch-record lifecycle tool (一段一作者 — six append-only sections, one author each). actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic + source, plus optional ledger / board that fill the record's 台账 header line at creation) · append (add text to the END of YOUR section — existing lines are never rewritten or deleted) · status (update YOUR section's `**状态行**:` line — the value must carry exactly ONE legal keyword of your section's STATUS_WORDS entry, decorated only by surrounding symbols / a trailing ISO date / whitespace; prose goes to the separate `note` field, which lands as a parenthetical). Writing §1 已收口 closes the record and runs the closeout-chain gate: §2 / §3 / §4 / §6 must each carry content, or §1 declares it inapplicable with `> 不适用:§N —— <reason>` (§2 / §3 / §4 only; §5 is exempt; §6 never). Reopening a frozen record = §1 → 进行中 with `note` = the reason: the tool rewrites the §1 status line and appends a 「重开记录」 line — no other existing line is touched. · close (depth-0 only — freeze the record: §1 → 「已收口 <date>」, every further write is refused except the reopen conjunction; runs the same closeout-chain gate as status — an incomplete chain is refused, naming the missing sections). Closing a record — the close action, or status writing §1 已收口 — also writes the ledger: this record's unsettled rows (task_book → this record, status 待讨论 / 待设计 / 在途, trigger ≠ 认账不排期 — a policy call leaves its row untouched, and a row already 条件 is skipped with zero writes) get trigger → 条件, with the resume sentence 「原批 <record> 已收口,须另起批次接手」 landing in resume_when (a non-empty resume_when keeps its value — the sentence is appended to evidence instead); each changed / skipped row is named in a `[ledger reanchor]` receipt block (a ledger write failure — busy store / no derivable project root — is named there too and never blocks the freeze). Identity fixes what you may write (段白名单): main agent (depth 0) — create/close; append/status domains are mode-dependent: engineering — append §1/§4/§6, status §1; normal — append §1/§2/§4/§5/§6, status §1/§2/§5 (the main agent holds the designer seat and records direct implementation); eng-designer — §2; eng-coder — §5; a normal-mode coder bound via batchDoc — §5; design review (review binding) — §3. A write outside your own section is refused. Skeleton placeholders (dead literals — `#<编号>` / `<板块>` and the older `<BATCH-ID>` / `<讨论来源>`) in the record header's structural lines block append/status until filled: create with ledger / board fills them at creation; if omitted, the main agent fills the header by file edit right after create (citing these literals in a section's prose is fine — only the header is checked); `<§N 模板占位:…>` template lines are legal and never blocked. Target record: sub-agents and design reviews have NO path parameter in practice — the record arrives via the spawn binding / the review instance key (passing path is refused); depth-0 MAY pass path (若传则须可读), omitting it picks the unique in-flight record (0 or ≥2 in flight ⇒ refuse — pass path). A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). Credential values are stripped mechanically before writing (never write a token or designId value). Document-check gate: appending §2 / §5 as a sub-agent and closing a record (or setting §1 to 已收口) run the doc-check gate before the write — REFUSED ⇒ the write is refused and the record's bytes stay unchanged (do not close a record after a refused append); ERROR ⇒ a closeout is refused while an append still lands with an ERROR receipt line; no PROJECT-MANIFEST.json ⇒ the gate does not run and the receipt says so. Failures are hard and visible (no silent fallback): if a write is refused or fails, say so in your report — “§× 未写入”.
@@ -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,54 @@
1
+ Run a built-in documentation-discipline check on a project and report its findings.
2
+ Read-only. In the default `report` mode it is 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
+ When the engine files are newer than this session's start time (or unreadable), `report` prefixes its output with one
4
+ `[doc_check note]` line naming the stale files — that reading came from an engine loaded at startup and is not usable as
5
+ delivery evidence. Nothing else changes: `report` still produces no verdict and no existing line is altered (no stale files ⇒
6
+ the report is byte-identical).
7
+
8
+ What it checks — exactly two criteria, both driven by the checked project's declarations:
9
+ - Anchors — every documentation pointer (a `*.md` token, optionally carrying directory segments, optionally followed by a section
10
+ indicator `§N` / `§N.M` / `§N.M.K`) must resolve: the file must exist on disk (`档缺` when it does not), and when a section
11
+ indicator is given the target file must carry a heading numbered with it (`节缺` when it does not). Line-number coordinates
12
+ (`<doc>:12`) are not section references and are not judged as such. Files inside fenced code blocks and executable lines
13
+ (command / pattern code spans) are skipped whole.
14
+ - Line width — a line whose character count (CJK counts as 1) exceeds the declared threshold, unless it is a table row, inside a
15
+ fenced code block, an executable line, a quote-block line, or inside an exempt zone.
16
+
17
+ It cannot — in `report` mode — write anything, gate anything, or turn a finding into a refusal.
18
+
19
+ Arguments — `target` (optional): the project root to check; defaults to the session anchor, relative paths resolve against it.
20
+ A workspace target (several projects below it) is reported with the full candidate list — pass the project root you mean.
21
+ `mode` (optional) — `report` (default: the whole declared domain) or `gate` (the delivery gate below).
22
+ `docs` (optional, gate only) — the documents this round touched, as paths relative to the project root or absolute; omitted in
23
+ gate mode ⇒ the documents this session wrote. Passing `docs` with `mode: "report"` is refused rather than silently ignored.
24
+
25
+ Delivery gate — `mode: "gate"`, for the end of a design or development round. Every listed doc is judged **whole**: a
26
+ pre-existing dangling pointer or overlong line anywhere in it counts, not only this round's edited lines. The first line is the
27
+ machine-readable verdict — `DOC-CHECK GATE: PASS` / `DOC-CHECK GATE: REFUSED(命中 K 条)` / `DOC-CHECK GATE: ERROR(reason)` —
28
+ and consumers judge by that prefix; the hits are fixed in the same round and are never booked to the ledger. The gate refuses
29
+ **one delivery**, not a user instruction: an explicit user go-ahead is followed, with the hits written into the delivery.
30
+ The gate narrows the judged set, not the parse baseline: bare-name pointers still resolve across the whole declared domain, so
31
+ judging one doc never turns a pointer into a false `档缺`.
32
+ Before running the criteria the gate checks engine-version consistency: the six engine files (`doc-check/index.mjs` /
33
+ `doc-check/anchors.mjs` / `doc-check/width.mjs` / `doc-check/version.mjs` / `agent-tools/doc-check.mjs` / this tool description) each have their mtime
34
+ compared with this process's start time — newer, or unreadable, ⇒ `DOC-CHECK GATE: ERROR(引擎已更新…)` instead of a PASS,
35
+ because a session keeps the engine modules it loaded at startup. `ERROR` is fail-closed (no PASS means not passed): restart the
36
+ session and re-run; a missing / unreadable manifest ⇒ fix the declaration surface; a multi-project workspace ⇒ name the project
37
+ root, per the stated cause.
38
+
39
+ Do not wire it as a gate or CI step in `report` mode, and do not treat a finding as a reason to refuse an instruction — a
40
+ machine check never outranks a user instruction.
41
+
42
+ Routing — use it when you write or revise a project's documents and want the declared discipline checked in one pass. It is not
43
+ a reader: use `doc_search` / `read` to read documents and `grep` to search them; this tool only reports discipline findings.
44
+
45
+ Side effects and permissions — none. The tool is read-only: no writes, no approval prompt, safe to run in parallel with other calls.
46
+
47
+ Parameter source and failure shape — every parameter comes from `<target>/PROJECT-MANIFEST.json` `checkConfig` (`scanDirs` /
48
+ `lineWidth` / `widthExemptZones` / `anchors.exclude` / `anchors.roots`); no repository path is hard-coded, so another project runs it with its own
49
+ declarations. Changing those declarations changes the behavior. A missing or unreadable manifest is surfaced as an error line
50
+ naming the file — the check never fabricates a result; in gate mode it yields the `ERROR` first line instead of a PASS.
51
+ `anchors.roots` (optional string array of project-root-relative directories) widens anchor resolution only — never the line-width scope.
52
+
53
+ Consistency — the checks and this description are one core implementation: every end that loads the core tool registry gets the
54
+ same semantics.
@@ -1,4 +1,4 @@
1
- Fetch a URL and return its content as text. HTML pages are stripped to readable text. Use after websearch to read full documents.
1
+ Fetch a URL and return its content. With a DeepSeek provider configured the URL is fetched through DeepSeek's servers and returned as Markdown ([deepseek] title + content); otherwise it is fetched locally and HTML is stripped to readable text. Private/metadata URLs are never sent. Use after websearch to read full documents.
2
2
 
3
3
  Parameters:
4
4
  - url (required): http/https URL
@@ -8,5 +8,6 @@ Notes:
8
8
  - Follows redirects automatically
9
9
  - Timeout: 15 seconds
10
10
  - HTML pages are converted to plain text (scripts, styles, navigation stripped)
11
- - Non-HTML responses are returned as-is (truncated at ~50000 chars)
11
+ - Non-HTML responses are returned as-is (truncated at ~200000 chars)
12
12
  - Proxy support: set `"proxy": {"uri": "http://host:port", "web": true}` in config.json or `HTTPS_PROXY` env var
13
+ - fetch.backend: "local" disables the DeepSeek backend.
@@ -1,6 +1,8 @@
1
1
  台账统一入口——action = add / update / close / query / count。query / count 只读全角色;add / update / close 写仅主 agent。
2
- - add:新增条目入待讨论。kind = requirement(需求池)/ tech_todo(技术待办);trigger = 归批 / 条件 / 认账不排期(可空)。
3
- - update:按六态允许表迁移:待讨论→待设计 / 待设计→在途 / 在途→待核销 / 待核销→已核销 / 任意态→已废弃;表外拒。status 缺省 = 仅更新字段;executor 缺省 = 进入待设计 / 在途自动写本会话,出在途 / 撤回清空。evidence 传入 = 追加(旧值非空 ⇒ 拼接 `旧|新`——「|」零空格;旧空直写);`evidenceReplace: true` 与 evidence 同传 = 整段替换;旗独传(无新文)∥ 新值全空白 ⇒ 拒。
4
- - close:勾销(待核销 → 已核销);追认核销(待讨论 / 待设计 → 已核销,须有 evidence——可随本调用传入(一跳核销),判结果值:缺 / 全空白拒);在途 → 已核销 不可跳;撤回(任意态 → 已废弃)。软删除(写 closed_at,行保留)。evidence 传入 = 追加(旧值非空 ⇒ 拼接 `旧|新`;旧空直写);`evidenceReplace: true` 同传 = 整段替换;旗独传 / 新值全空白 ⇒ 拒;缺省 = 判行值。
5
- - query:SQLite 行集,等值过滤(status / kind / board / trigger)缺省 = 全部行;行集逐行携 aged(未决四态 ∧ 行龄 > 30 天)。六态 = 待讨论 / 待设计 / 在途 / 待核销 / 已核销 / 已废弃。
6
- - count:未决四态(待讨论 / 待设计 / 在途 / 待核销)COUNT(*)。
2
+ - add:新增条目入待讨论。kind = requirement(需求池)/ tech_todo(技术待办);trigger = 归批 / 条件 / 认账不排期(可空);resume_when = 复启条件句(可空;值非空——空串 / 全空白拒;句内可带锚 `@YYYY-MM-DD`(机判到期)或 `@事件件名`(机器不判))。task_book 写门(add ∥ update 同判,判本笔写入面):结果行 在途 / 待核销 ∥ trigger = 归批 ⇒ task_book 必填(缺 / 空串 / 全空白拒);本笔新写 task_book(非空值)∥ 本笔新写 trigger = 归批 ⇒ 该指针的文件部分(首个 `§` 前子串)须可解析且指出的档在盘,否则拒。本笔不动指针 ⇒ 零判零阻——旧指针所指档已被清理不阻 status / evidence / 其它字段更新(「档在不在盘」是读数 taskBookOnDisk,不是门)。
3
+ - update:按六态允许表迁移:待讨论→待设计 / 待设计→在途 / 在途→待核销 / 待核销→已核销 / 任意态→已废弃;表外拒。id ∥ batch 恰携其一。status 缺省 = 仅更新字段;executor 缺省 = 进入待设计 / 在途自动写本会话,出在途 / 撤回清空;resume_when 同 update 可空字段(缺省 = 不变)。evidence 传入 = 追加(旧值非空 ⇒ 拼接 `旧|新`——「|」零空格;旧空直写);`evidenceReplace: true` 与 evidence 同传 = 整段替换;旗独传(无新文)∥ 新值全空白 ⇒ 拒。
4
+ - update --batch:批级批量迁移(batch = 批档指针,与该档 task_book 同址的行一次处理)——携 status ⇒ 逐行按迁移表重判;不携 status ⇒ 纯字段补丁(status 零写)。回执 { batch, status, applied, skipped };被跳过的行逐行点名 { id, from, why }(why = 逐行拒因原文),其余行照落;事务级失败整批回滚。batch 值须可解析且批档在盘,否则拒。
5
+ - close:勾销(待核销 → 已核销);追认核销(待讨论 / 待设计 → 已核销,须有 evidence——可随本调用传入(一跳核销),判结果值:缺 / 全空白拒);在途 → 已核销 不可跳;撤回(任意态 → 已废弃)。软删除(写 closed_at,行保留)。id ∥ batch 恰携其一。evidence 传入 = 追加(旧值非空 ⇒ 拼接 `旧|新`;旧空直写);`evidenceReplace: true` 同传 = 整段替换;旗独传 / 新值全空白 ⇒ 拒;缺省 = 判行值。
6
+ - close --batch:批级批量结算——只销已到 待核销 的行(勾销);待讨论 / 待设计 逐行点名跳过(追认核销不入批量面,转单行 close),在途 逐行点名跳过(不可跳);已核销 / 已废弃 行静默不计。status 可不给(缺省 = 已核销),给了只能是 已核销。
7
+ - query:SQLite 行集,等值过滤(status / kind / board / trigger / task_book)缺省 = 全部行;行集逐行携 aged(未决四态 ∧ 行龄 > 30 天)/ evidenceOversize(evidence 超软上限 300 字——读数非拒写)/ taskBookOnDisk(task_book 文件部分解析成立 ∧ 该档在盘 ⇒ true;缺指针 / 不可解析 / 不在盘 ⇒ false——读数非门,不拦任何写入)/ overdue(出口态字串 ∥ null:resume_due / dangling / unlabeled / aged,首中优先)。六态 = 待讨论 / 待设计 / 在途 / 待核销 / 已核销 / 已废弃。
8
+ - count:回报 { count, overdue, evidenceOversize }——count = 未决四态(待讨论 / 待设计 / 在途 / 待核销)行数;overdue = 其中命中出口态的行数;evidenceOversize = 其中 evidence 超限行数。task_book 等值过滤。
@@ -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,51 @@
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 or results (an explicit opt-in can store it as ciphertext on this machine).
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 | credentials | forget
9
+ - host: connect / forget — 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 / forget — 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
+ - identityFile: connect — private-key path to offer as `-i` (`~` expands); remembered for this host and reused when omitted (optional)
15
+ - id: send / read / secret_input / close — session id from connect (`ssh#N`)
16
+ - data: send — text to type into the session (a trailing newline is passed as-is; otherwise Enter is sent as `\r`)
17
+ - settle: send — wait for the ready verdict before returning (default true); false returns right after the bytes are written
18
+ - timeoutMs: send / read — wait budget override (default 10000; the wait then returns partial output with a readable line)
19
+ - maxChars: read — return cap override (default 8000; the full output stays in the session log)
20
+
21
+ Output:
22
+ - every result starts with the session status line: `[ssh (ssh#3) 10.0.0.5 — ready · idle 3s]`
23
+ - 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
24
+
25
+ Notes:
26
+ - Key-first: keys, your ssh config and ssh-agent are always tried before any saved credential — a connect succeeds without a window
27
+ when a key works, and the credential store is not read at all in that case. To make a password host prompt-free, install a key once:
28
+ `ssh-keygen -t ed25519` (if you have none), then `ssh-copy-id user@host` (or append `id_ed25519.pub` to the host's
29
+ `~/.ssh/authorized_keys`), then reconnect.
30
+ - Credential discipline: the password is asked for in a window and reaches ssh only through its child-process environment — never in tool
31
+ args, never echoed in a result, never stored in plaintext. Any captured output containing that value (or a value given to secret_input)
32
+ is scrubbed from the returned text and from the session log.
33
+ - Saving a credential is opt-in: the credential window carries a `save` checkbox (off by default). When checked and authentication succeeds,
34
+ the password is stored encrypted with your OS user account (Windows DPAPI) in a per-user file outside the repository; the store holds only
35
+ ciphertext. A saved credential is reused silently — the next connect skips the window, and only after key
36
+ authentication fails. The residual risk is honest: another process running as the same logged-in user can decrypt it; this protects the
37
+ file at rest, not a compromised account. Saving is Windows-only for now; on other platforms the checkbox does not appear and the window
38
+ keeps asking.
39
+ - Fail-soft: an unreadable/undecryptable entry, a damaged store or a missing backend falls back to the credential window (the connection is
40
+ not broken; undecryptable entries are kept). If the server rejects a saved credential it is deleted and the window is shown. A failed save
41
+ never affects the connection.
42
+ - 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.
43
+ - 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.
44
+ - Output is capped per return (8000 chars, tail kept); the full stream is always in the session log whose path connect returns.
45
+ - 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.
46
+ - 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.
47
+ - read / list / credentials are read-only (no approval prompt); connect / send / secret_input / close / forget 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.
48
+ - credentials lists saved entries (host / user / saved / last used — never a value); forget deletes the entry for a host (and optional port) and is idempotent — the next connect asks in the window again.
49
+ - Not for file transfer or port forwarding — run `scp` / `sftp` / `ssh -L` through bash.
50
+
51
+ 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.
@@ -7,7 +7,8 @@ ONE tool, NINE actions — pick by what you need:
7
7
  - action:'escalate' (飞刀): hand an implementation task to a STRONGER model from your consult models; it gets WRITE access and does the work itself, then returns a post-op report (what changed, why, verification). Escalate EARLY, when the task calls for stronger hands (complex refactors, intractable bugs, intricate algorithms — or beyond your comfortable ability). Not available in engineering mode (implementation goes through eng-coder). Default-async at depth 0: ack {id, role:'escalate', status:'running'} returns immediately; the report arrives automatically with its mutations merged into your bookkeeping (pooled with the other role-domain spawns).
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
- - 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.
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, designToken), 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
+ Every row carries its designToken value (expired ones included — status marks them; a malformed record shows designToken=—). Pass designId to filter to a single slot (trimmed exact match; unknown = refused with the held id list; empty string = no filter). Take designId + designToken verbatim onto the eng-coder spawn — the gate checks exact equality.
11
12
  - 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
13
 
13
14
  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.
@@ -19,15 +20,11 @@ Available roles (exposed per mode; the role is REQUIRED for action:'spawn' — a
19
20
  - eng-coder — engineering mode only (replaces coder): same toolset plus the design-driven methodology overlay; its delivery protocol (implementation → audit → self-fix → advisor re-review → converged delivery) runs fully inside the child. REQUIRES a valid designToken from a passed advisor(type='design') review (its reply also echoes a designId — pass it: required to pick between concurrent designs, optional for one; echo it back in the delivery report). REQUIRES a batchDoc — the batch record path, the task book this spawn implements (refused without it or when it does not resolve to a readable file).
20
21
  - eng-designer — engineering mode only: the SOLE author of the requirements/design documents and of the batch record's design section. Writes no implementation code, does not edit prompts, does not fire reviews, needs NO designToken. Surveys on its own; may only spawn read-only explore children (sync, ≤6 per batch). REQUIRES a batchDoc (same gate as eng-coder).
21
22
 
22
- Task book (eng-coder / eng-designer spawns — six mandatory fields): 目标与理由 (goal & why), round (initial|fix — the structured parameter), 已知事实 (known facts), 设计要点与禁止范围 (design points & forbidden scope), 验收标准 (acceptance criteria), 交付报告格式 (delivery-report format). Keep the labels (Chinese or the English equivalents) — the gate matches these markers; a missing field is a dispatch defect and the spawn is refused.
23
+ Task book (every spawn role): four core structured parameters — goal (goal & why), knownFacts (the facts you already verified), acceptance (machine-verifiable criteria), reportFormat (delivery-report format) — plus ONE fifth parameter named by role: designPoints (design points & forbidden scope) for eng-coder/eng-designer, scope (scope & forbidden scope — what to read / what not to touch) for explore/plan/coder. All five are required: a missing, blank or non-string value is refused with the parameter named, and one role's fifth name never substitutes for the other's. They are composed into the child's brief in that order. A free-text task is an optional supplement, never the whole brief; round (initial|fix) is mandatory for eng-coder/eng-designer.
23
24
 
24
- Copy this skeleton verbatim into every task book — fill the blanks, do not relabel the six lines:
25
- **目标与理由**:…
26
- **round**:initial | fix(此字段=结构化参数——spawn 时同时传 `round`)
27
- **已知事实**:…
28
- **设计要点与禁止范围**:…
29
- **验收标准**:…
30
- **交付报告格式**:…
25
+ Pass one parameter per field — contents only (the runtime adds the labels):
26
+ subagent(role="eng-coder", round="initial", goal="…", knownFacts="…", designPoints="…", acceptance="…", reportFormat="…", batchDoc="<batch record>", designToken="…")
27
+ subagent(role="explore", goal="…", knownFacts="…", scope="…", acceptance="…", reportFormat="…")
31
28
 
32
29
  Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/eng-designer/eng-coder — off-mode roles are refused mechanically (enums filtered at assembly; the spawn gate re-checks).
33
30
 
@@ -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.
@@ -1,4 +1,4 @@
1
- Search the web via Bing. Returns result titles, URLs, and snippets. Use for looking up current information, docs, error messages.
1
+ Search the web (DeepSeek Search when a DeepSeek provider is configured; otherwise Tavily with a key, else Bing). Returns result titles, URLs, and snippets — DeepSeek results also carry a date · authority (0–4) · rerank (0–3) line and a long summary. Use for looking up current information, docs, error messages.
2
2
 
3
3
  Parameters:
4
4
  - query (required): Search query
@@ -14,3 +14,5 @@ Notes:
14
14
  - Follow up with `fetch` to read full pages from the results
15
15
  - **Weak engine warning**: Bing's index is noisy for technical queries — if a first websearch returns irrelevant/townhall-grade results, DO NOT retry the same query. Configure a search MCP tool (e.g. `glm-websearch` via the MCP config) for technical lookups; websearch is the fallback.
16
16
  - Proxy: NOT auto-applied from config.json (2026-08-31 ruling). Pass `proxy: "http://host:port"` explicitly when the target needs one; omit for domestic targets.
17
+ - Backend priority: DeepSeek (auto — a providers[] entry with baseURL host api.deepseek.com and a key) → Tavily → Bing. websearch.backend: "local" disables the DeepSeek backend.
18
+ - DeepSeek query operators: site:, lang:, after:/before:, and quoted phrases pass through as-is; limit maps to count (1–20).
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`)
package/tools/ssh.mjs ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * tools/ssh.mjs — SSH 会话工具面(薄接线):schema ∥ 参数校验 ∥ `isReadonlyAction` ∥ 回执/错误形
3
+ * (设计 = `docs/core/design/TOOLS.md` §6.22 —— D-TO17/D-TO18 · 台账 #1436;保存面 D-TO19/D-TO20 · 台账 #1602;
4
+ * 机制单点 = `agent-tools/ssh-session.mjs`(保存件 = `agent-tools/ssh-vault.mjs`);描述档 = `tool-docs/ssh.md`)。
5
+ *
6
+ * 分类钩子(KD-2 ∥ KD-11 同式——动作级,工具级只读位恒 false):免审 = `list` ∥ `read` ∥ `credentials`
7
+ * (只读豁免,NS3 判据);过门 = `connect` ∥ `send` ∥ `secret_input` ∥ `close` ∥ `forget`——**携密动作仍属
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", "credentials", "forget"]
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" || action === "credentials") return null
33
+ if (action === "forget") {
34
+ if (typeof args.host !== "string" || args.host.trim() === "") return "Error: forget requires host"
35
+ if (args.port !== undefined && args.port !== null && args.port !== "" && !Number.isFinite(Number(args.port))) {
36
+ return "Error: forget port must be a number"
37
+ }
38
+ return null
39
+ }
40
+ if (typeof args.id !== "string" || args.id.trim() === "") {
41
+ return `Error: ${action} requires id (the session id from connect, form "ssh#N")`
42
+ }
43
+ if (action === "send" && typeof args.data !== "string") return "Error: send requires data"
44
+ return null
45
+ }
46
+
47
+ export const sshTool = {
48
+ name: "ssh",
49
+ description: DESC("ssh"),
50
+ parameters: {
51
+ type: "object",
52
+ properties: {
53
+ action: { type: "string", enum: SSH_ACTIONS, description: "Action to run (one per call): connect | send | read | secret_input | close | list | credentials | forget." },
54
+ host: { type: "string", description: "connect / forget: target host (IP or name)." },
55
+ user: { type: "string", description: "connect: login user (optional: your local ssh config decides, else the user recorded for this host locally)." },
56
+ port: { type: "number", description: "connect / forget: TCP port (optional, default 22)." },
57
+ label: { type: "string", description: "connect: short human label for this session (optional); secret_input: label for the value being sent." },
58
+ 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." },
59
+ id: { type: "string", description: "send / read / secret_input / close: session id from connect (`ssh#N`)." },
60
+ data: { type: "string", description: "send: text to type into the session (a trailing newline is passed as-is; otherwise Enter is sent as `\\r`)." },
61
+ settle: { type: "boolean", description: "send: wait for the ready verdict before returning (default true); false returns right after the bytes are written." },
62
+ timeoutMs: { type: "number", description: "send / read: wait budget override (default 10000) — on expiry the call returns partial output with a readable line." },
63
+ maxChars: { type: "number", description: "read: return cap override (default 8000; the full output stays in the session log)." },
64
+ identityFile: { type: "string", description: "connect: private-key path to offer (`-i`, `~` expands); remembered for this host and reused when omitted (optional)." },
65
+ },
66
+ required: ["action"],
67
+ },
68
+ // 非工具级只读:动作级分类钩子接管(§6.22 NS3——`list` / `read` / `credentials` 免审);也因此不进只读角色的子代工具表。
69
+ readonly: false,
70
+ isReadonlyAction(args) {
71
+ return args?.action === "list" || args?.action === "read" || args?.action === "credentials"
72
+ },
73
+ async execute(args, ctx) {
74
+ const invalid = validateArgs(args)
75
+ if (invalid) return invalid
76
+ // 动态 import(W8 契约②——与 bash / browser 异步面同式)。
77
+ const mod = await import("../agent-tools/ssh-session.mjs")
78
+ const parent = ctx?.agent ?? null
79
+ const action = args.action
80
+ let res
81
+ if (action === "connect") {
82
+ res = await mod.connectSsh(parent, ctx, {
83
+ host: args.host, user: args.user ?? null, port: args.port ?? null,
84
+ label: args.label ?? null, promptPattern: args.promptPattern ?? null,
85
+ identityFile: args.identityFile ?? null,
86
+ })
87
+ } else if (action === "send") {
88
+ res = await mod.sendSsh(parent, ctx, { id: args.id, data: args.data, settle: args.settle !== false, timeoutMs: args.timeoutMs })
89
+ } else if (action === "read") {
90
+ res = await mod.readSsh(parent, ctx, { id: args.id, timeoutMs: args.timeoutMs, maxChars: args.maxChars })
91
+ } else if (action === "secret_input") {
92
+ res = await mod.secretInputSsh(parent, ctx, { id: args.id, label: args.label ?? null })
93
+ } else if (action === "close") {
94
+ res = await mod.closeSsh(parent, ctx, { id: args.id })
95
+ } else if (action === "credentials") {
96
+ res = mod.credentialsSsh()
97
+ } else if (action === "forget") {
98
+ res = mod.forgetSsh({ host: args.host, port: args.port ?? null })
99
+ } else {
100
+ res = mod.listSsh(parent)
101
+ }
102
+ if (res?.error) return res.error.startsWith("Error:") ? res.error : `Error: ${res.error}`
103
+ return res?.text ?? "(no output)"
104
+ },
105
+ }