@thincoder/core 0.10.13 → 0.11.1
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 +29 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +69 -48
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +3 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +32 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -367
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +275 -80
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/client-identity.mjs +22 -6
- package/config-presets.mjs +5 -3
- package/context.mjs +34 -3
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +68 -15
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +18 -5
- package/explore-distill.mjs +1 -1
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +19 -37
- package/ledger-cmd.mjs +71 -195
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +6 -6
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -68
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/core.mjs +2 -2
- package/provider/google.mjs +15 -5
- package/provider/rate.mjs +11 -3
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +9 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +90 -14
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- package/agent/unbooked-pen.mjs +0 -62
package/tools/git.mjs
CHANGED
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
DESC,
|
|
3
3
|
truncate,
|
|
4
4
|
runGit,
|
|
5
|
-
gitFailureMessage
|
|
5
|
+
gitFailureMessage,
|
|
6
|
+
LEDGER_PARAM_DESC_GIT,
|
|
7
|
+
PROJECT_PARAM_DESC,
|
|
6
8
|
} from "./shared.mjs";
|
|
7
9
|
import { spawnGit } from "./git-run.mjs";
|
|
8
10
|
import { resolve } from "node:path";
|
|
@@ -86,10 +88,34 @@ export const gitTool = {
|
|
|
86
88
|
create: { type: "boolean", description: "(switch) create the branch then switch (-c)" },
|
|
87
89
|
dest: { type: "string", description: "(mv) destination path (file or directory)" },
|
|
88
90
|
worktreeAction: { type: "string", enum: ["list", "add", "remove"], description: "(worktree) list / add (path, ref) / remove (path)" },
|
|
91
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC_GIT },
|
|
92
|
+
// 项目声明面(决策 40):`path` ∥ `workdir` 缺位时才被读(定位参数优先);纯声明,不改执行目录。
|
|
93
|
+
project: { type: "string", description: PROJECT_PARAM_DESC },
|
|
89
94
|
},
|
|
90
95
|
required: ["action"],
|
|
91
96
|
},
|
|
92
97
|
readonly: false,
|
|
98
|
+
/**
|
|
99
|
+
* 动作级只读分类(决策 37 · 2026-11-03 笔-账绑定批——**本钩子是单源**)。
|
|
100
|
+
* 消费面两处:`thincoder-core/agent/dispatch-gates.mjs` 的 `readonlyActionOf(tool, "git", args)`
|
|
101
|
+
* (planMode 门 ∥ 权限短路 ∥ 写门的 `git` 写型/只读判定)——三面(CLI ∥ desktop ∥ vscode)同答案。
|
|
102
|
+
* 名单与 `thincoder-vscode/src/tools/index.mjs` 原端装饰体逐条同形(该覆写随本钩子同笔删除):
|
|
103
|
+
* 六项平铺 + 六条子动作分支。`checkpoint` 的 `list` ∥ `cat` 判只读——`checkpoint list` 有 F6 清理副作用
|
|
104
|
+
* (`lazyClearIfCommitted` 可能删除整个 checkpoint 目录),但清的是 commit 后已失去意义的过期快照 ⇒
|
|
105
|
+
* 保留只读分类免审批(`docs/core/design/CHECKPOINT.md` F6/D3 设计意图)。
|
|
106
|
+
* 缺此钩子的面回落到核名面谓词(无 `git` 分支 ⇒ false ⇒ 写型 ⇒ 要号),fail-closed,不出洞。
|
|
107
|
+
*/
|
|
108
|
+
isReadonlyAction(args) {
|
|
109
|
+
if (!args || typeof args.action !== "string") return false
|
|
110
|
+
if (["diff", "status", "log", "show", "ls-remote", "blame"].includes(args.action)) return true
|
|
111
|
+
if (args.action === "checkpoint") return ["list", "cat"].includes(args.checkpointAction)
|
|
112
|
+
if (args.action === "tag") return args.tagAction === "list"
|
|
113
|
+
if (args.action === "branch") return args.branchAction === "list"
|
|
114
|
+
if (args.action === "stash") return args.stashAction === "list"
|
|
115
|
+
if (args.action === "remote") return args.remoteAction === "list"
|
|
116
|
+
if (args.action === "worktree") return args.worktreeAction === "list"
|
|
117
|
+
return false // add/rm/commit/push/fetch/pull/reset/revert/merge/cherry-pick/checkout/restore + tag/branch/stash 写操作
|
|
118
|
+
},
|
|
93
119
|
async execute(args, ctx) {
|
|
94
120
|
// workdir: run git in a subdirectory (monorepo / multi-repo). Shadow ctx.cwd so
|
|
95
121
|
// every action + snapshotBefore + checkpoint resolves against the workdir.
|
package/tools/ops.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* wait_for 条件族 +`bash id:N done`。list ∥ 既有条件面零变。批 browser-async-fix(#1045):kill id
|
|
8
8
|
* 路由先 bg 后 browser(同一 id 命名空间)∥ wait_for 条件族 +`browser id:N done`。
|
|
9
9
|
*/
|
|
10
|
-
import { DESC, resolveInCwd, truncate } from "./shared.mjs"
|
|
10
|
+
import { DESC, resolveInCwd, truncate, LEDGER_PARAM_DESC } from "./shared.mjs"
|
|
11
11
|
// 单一写路径点(编辑器编辑径注入缝——CORE-UNIFICATION §2.13.5):路径操作也经它落盘
|
|
12
12
|
// (默认径 = 既有 cp / rename / EXDEV 回退语义,零行为变)。
|
|
13
13
|
import { writeThroughPath } from "./write-path.mjs"
|
|
@@ -26,6 +26,7 @@ export const fileOpsTool = {
|
|
|
26
26
|
action: { type: "string", enum: ["move", "copy", "rename"], description: "move | copy | rename" },
|
|
27
27
|
source: { type: "string", description: "Source path, relative to cwd or absolute" },
|
|
28
28
|
dest: { type: "string", description: "Destination path" },
|
|
29
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
29
30
|
},
|
|
30
31
|
required: ["action", "source", "dest"],
|
|
31
32
|
},
|
package/tools/patch.mjs
CHANGED
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
resolveInCwd,
|
|
5
5
|
normalizeEOL,
|
|
6
6
|
detectFileEol,
|
|
7
|
-
majorityEol
|
|
7
|
+
majorityEol,
|
|
8
|
+
LEDGER_PARAM_DESC
|
|
8
9
|
} from "./shared.mjs";
|
|
9
|
-
import { writeThroughPath, TMP_SUFFIX, markDirty } from "./write-path.mjs";
|
|
10
|
+
import { writeThroughPath, TMP_SUFFIX, markDirty, clearLastWrite } from "./write-path.mjs";
|
|
10
11
|
import { spawnGit } from "./git-run.mjs";
|
|
11
12
|
import { mkdir, readFile, lstat, unlink } from "node:fs/promises";
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
@@ -175,6 +176,7 @@ export const applyPatchTool = {
|
|
|
175
176
|
type: "object",
|
|
176
177
|
properties: {
|
|
177
178
|
patch: { type: "string", description: "Unified diff. May span multiple files (multiple --- / +++ header pairs — including creating MULTIPLE new files via --- /dev/null); --- / +++ headers per file, @@ -old,count +new,count @@ hunks (a bare @@ header is also accepted — coordinate-less hunks are located by their anchor lines: context lines plus the removed (-) lines, matched as a contiguous sequence — a unique match applies; a zero/one-context hunk is accepted only when it removes (-) at least one line and that anchor sequence is unique, while anchor-free pure-+ (insert) hunks need at least 2 context lines). The +++ b/<path> pair may be omitted for existing files — a lone --- a/<path> (or --- b/<path>) header followed directly by hunks applies to that path (new files still need --- /dev/null + +++ b/<path>)." },
|
|
179
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
178
180
|
},
|
|
179
181
|
required: ["patch"],
|
|
180
182
|
},
|
|
@@ -234,7 +236,13 @@ export const applyPatchTool = {
|
|
|
234
236
|
const summary = planned.map((p) => ` ${p.isNew ? "created " : "modified"} ${p.path}`).join("\n")
|
|
235
237
|
// Mark every touched file dirty — insert_after must not run on stale line numbers.
|
|
236
238
|
// 整批记账(dirty)= apply_patch 的批级语义(两段式全成功后一次落账——与改前逐字同)。
|
|
237
|
-
|
|
239
|
+
// 2026-11-02:落 dirty 同时清写入快照——apply_patch 不记快照,留着旧 `startLine` 就是
|
|
240
|
+
// 陈旧的锚:行号锚护栏第 2 行(dirty ∧ 快照在场)会拒,而第 4 行会拿旧值判「目标区在之前
|
|
241
|
+
// ⇒ 放行」——两种结果都不该由一笔没更新快照的写来决定(EDIT-HELPERS §7.1)。
|
|
242
|
+
for (const p of planned) {
|
|
243
|
+
markDirty(p.abs)
|
|
244
|
+
clearLastWrite(p.abs)
|
|
245
|
+
}
|
|
238
246
|
const syntaxChecks = await Promise.all(planned.map(async (p) => {
|
|
239
247
|
const r = await autoSyntaxCheck(p.abs)
|
|
240
248
|
return r ? `${p.path}:${r.replace("Syntax: ", "")}` : ""
|
|
@@ -256,6 +264,7 @@ export const deleteTool = {
|
|
|
256
264
|
properties: {
|
|
257
265
|
path: { type: "string", description: "File or empty-directory path (relative to cwd or absolute)" },
|
|
258
266
|
force: { type: "boolean", description: "Allow deleting git-tracked files (default false)" },
|
|
267
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
259
268
|
},
|
|
260
269
|
required: ["path"],
|
|
261
270
|
},
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process-doc-mark.mjs — 工具输出里的过程档标记(单源小档;机制 = `docs/core/design/TOOLS.md` §6.28)。
|
|
3
|
+
*
|
|
4
|
+
* 依赖面说清楚:本档**不**是零依赖叶档——谓词都来自判据单源 `../declaration.mjs`,而它的静态闭包
|
|
5
|
+
* 经 `memory/file-walk.mjs` 达 `memory/schema.mjs`(`node:sqlite`)。因此端壳里静态引本档 = 把那条链拉进静态闭包;
|
|
6
|
+
* 当前端装配面本已静态可达同一链(`setup-tooltable.mjs` → 核 `agent-tools/verify.mjs` → `conventions.mjs`),
|
|
7
|
+
* 零新增风险,但别把它当零依赖档随手引。
|
|
8
|
+
*
|
|
9
|
+
* 一句话:输出里**判定得出**来源档落在索引排除集(batches 段 ∥ `_archive` 前缀)的行,
|
|
10
|
+
* 行首加 `[process doc: not a design basis]`(原行整体后随);其余行一个字节不动。**不是门**——
|
|
11
|
+
* 不拦、不筛、不改命中集,只加可读信息。
|
|
12
|
+
*
|
|
13
|
+
* 两套判据、一份文案、一个谓词、一个 helper:
|
|
14
|
+
* - `grep` 面按命中行的**档**判(消费面直调 `isProcessDocPath`——grep 自己已有 `h.file`,本档不参与拼装);
|
|
15
|
+
* - 命令面(`bash` 同步 ∥ `bash` 后台摘要 ∥ `execute` ∥ VSC 端 `bash`)的输出是任意文本,只能按
|
|
16
|
+
* **行首路径 token 真解析到盘上**判(`markProcessDocLines`)——解析不到 = 判不了 = 不动输出。
|
|
17
|
+
*
|
|
18
|
+
* 判据单源 = `../declaration.mjs` 的 `isExcludedRelPath`(面① 前缀 ∪ 面⑥ 段序列):本档零新排除名单、
|
|
19
|
+
* 零新谓词。第三参必须传 `decl.root`——`isExcludedRelPath` 内 `resolve(base, s)` 对绝对参恒等,
|
|
20
|
+
* 再由 `relative(root, …)` 换成根相对面,面① 前缀才判得住(只被面① 命中的 `_archive` 靠这一点)。
|
|
21
|
+
*
|
|
22
|
+
* 零 console 噪声:本档不调用 `console`。判据只走 `declarationForTarget` → `loadProjectDeclaration`,
|
|
23
|
+
* 后者按档缓存,只在声明档不可用 / 读不出时 warn(既有降级面,非本档新增)。
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync } from "node:fs"
|
|
26
|
+
import { isAbsolute, resolve } from "node:path"
|
|
27
|
+
import { declarationForTarget, isExcludedRelPath } from "../declaration.mjs"
|
|
28
|
+
|
|
29
|
+
/** 逐字文案(33 字符 · 全 ASCII · 方括号闭合):工具输出面全英文,同一行混中英破坏「一词一物」。 */
|
|
30
|
+
export const PROCESS_DOC_MARK = "[process doc: not a design basis]"
|
|
31
|
+
|
|
32
|
+
/** 末尾汇总行(仅命令面):N = 本次输出被判为过程档的行数;N = 0 不加(`grep` 面不加此形)。 */
|
|
33
|
+
export const processDocSummary = (n) =>
|
|
34
|
+
`[process doc: ${n} of this output's lines come from process docs — not a design basis]`
|
|
35
|
+
|
|
36
|
+
/** 行首 token 的字符帽(步骤 ①):超过即成不了路径,该行跳过不解析。 */
|
|
37
|
+
const MAX_TOKEN_CHARS = 260
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 档判据(一次调用内按档记忆:`Map<absoluteFile, boolean>`)。
|
|
41
|
+
* @param {string} absFile 档的绝对路径(grep 面 = 命中行的 `h.file`;命令面 = 盘上解析出的绝对路径)
|
|
42
|
+
* @param {Map<string, boolean>} [cache] 记忆表(缺省自建——不传就没有跳调用的记忆)
|
|
43
|
+
* @returns {boolean} 所属档在索引排除集内 ⇒ true;出辖(祖先链无 manifest)⇒ false(无档类可判)
|
|
44
|
+
*/
|
|
45
|
+
export function isProcessDocPath(absFile, cache) {
|
|
46
|
+
const key = String(absFile ?? "")
|
|
47
|
+
if (!key) return false
|
|
48
|
+
const memo = cache instanceof Map ? cache : new Map() // 缺省自建:一次调用内按档记忆
|
|
49
|
+
if (memo.has(key)) return memo.get(key)
|
|
50
|
+
const decl = declarationForTarget(key) // null = 出辖(场景:会话锚在无 manifest 的目录里跑检索)
|
|
51
|
+
const marked = decl ? isExcludedRelPath(key, decl, decl.root) : false
|
|
52
|
+
memo.set(key, marked)
|
|
53
|
+
return marked
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 剥两侧成对引号(步骤 ①)——行首字段被引起来的输出很常见。 */
|
|
57
|
+
function stripPairedQuotes(s) {
|
|
58
|
+
if (s.length < 2) return s
|
|
59
|
+
const first = s[0]
|
|
60
|
+
const last = s[s.length - 1]
|
|
61
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) return s.slice(1, -1)
|
|
62
|
+
return s
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 行首路径 token(步骤 ①–③)。判不了 ⇒ null(调用方原样保留该行)。
|
|
67
|
+
* @param {string} line 输出行
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
function headPathToken(line) {
|
|
71
|
+
const head = /^\S+/.exec(String(line ?? "").trimStart())
|
|
72
|
+
if (!head) return null // 空行 / 纯空白行
|
|
73
|
+
const raw = stripPairedQuotes(head[0])
|
|
74
|
+
if (!raw || raw.length > MAX_TOKEN_CHARS) return null
|
|
75
|
+
// ② 剥尾:内层 `grep` ∥ `rg` ∥ `findstr` 的 `path:12: text` 形与尾随冒号一并剥掉
|
|
76
|
+
const token = raw.replace(/[:\-]\d+[:\-]?$/, "").replace(/:+$/, "")
|
|
77
|
+
if (!token) return null
|
|
78
|
+
// ③ 预筛:不含 `/` `\` `.` 的字段不做盘上解析(`[stdout]:` ∥ `(exit code 0)` ∥ 纯数字过不了这一关)
|
|
79
|
+
if (!/[/\\.]/.test(token)) return null
|
|
80
|
+
return token
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 盘上解析(步骤 ④):驱动绝对形 → MSYS 形 → 相对基准 `base`,取**第一个在盘上存在**者。
|
|
85
|
+
* 三支都不存在 ⇒ null(= 判不了)。UNC(`\\server\share\…`)走驱动绝对形一支。
|
|
86
|
+
* @param {string} token 行进字段(已剥尾)
|
|
87
|
+
* @param {string} [base] 解析基准:`bash` 同步 = `ctx.cwd` ∥ 后台径 = entry 记下的 `cwd` ∥ `execute` = `baseDir`
|
|
88
|
+
* @returns {string|null}
|
|
89
|
+
*/
|
|
90
|
+
function resolveOnDisk(token, base) {
|
|
91
|
+
const candidates = []
|
|
92
|
+
if (/^[A-Za-z]:[\\/]/.test(token) || token.startsWith("\\\\")) {
|
|
93
|
+
candidates.push(token) // 驱动绝对形 ∥ UNC
|
|
94
|
+
} else {
|
|
95
|
+
// MSYS 形(`/c/teamcode/…`)在 win32 上 `isAbsolute` 也为真,会落成当前盘的 `/c/…`
|
|
96
|
+
// ⇒ 必须显式把首段 `^/([A-Za-z])/` 映射为 `X:/`,否则这一支恒不命中。
|
|
97
|
+
const msys = /^\/([A-Za-z])(\/.*)?$/.exec(token)
|
|
98
|
+
if (msys) candidates.push(`${msys[1].toUpperCase()}:${msys[2] ?? "/"}`)
|
|
99
|
+
else if (isAbsolute(token)) candidates.push(token) // POSIX 绝对形(`resolve` 对绝对参恒等,同一支)
|
|
100
|
+
}
|
|
101
|
+
if (base) candidates.push(resolve(String(base), token))
|
|
102
|
+
for (const c of candidates) if (existsSync(c)) return c
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 命令面打标(§6.28「命令输出面」六步,`bash` ∥ `execute` ∥ VSC 端 `bash` 共用):
|
|
108
|
+
* 逐行取行首 token → 真解析到盘上 → 判归属 → 命中者行首加前缀(行其余部分原样)。
|
|
109
|
+
* N > 0 ⇒ 正文之后追加一行汇总;N = 0 ⇒ **原样返回入参**(一个字节不动,无空汇总行)。
|
|
110
|
+
*
|
|
111
|
+
* 纪律:不许按「看起来像路径」打标——解析不到盘上就不标(误标比不标更坏)。
|
|
112
|
+
*
|
|
113
|
+
* @param {string} text 输出文本
|
|
114
|
+
* @param {string} [base] 解析基准(见 `resolveOnDisk`)
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function markProcessDocLines(text, base) {
|
|
118
|
+
const src = String(text ?? "")
|
|
119
|
+
if (!src) return src
|
|
120
|
+
const fileCache = new Map() // 档 → boolean
|
|
121
|
+
const tokenCache = new Map() // token → boolean(一次调用内按 token 记忆;不设解析上限)
|
|
122
|
+
let n = 0
|
|
123
|
+
const out = src.split("\n").map((line) => {
|
|
124
|
+
const token = headPathToken(line)
|
|
125
|
+
if (!token) return line
|
|
126
|
+
let marked = tokenCache.get(token)
|
|
127
|
+
if (marked === undefined) {
|
|
128
|
+
const abs = resolveOnDisk(token, base)
|
|
129
|
+
marked = abs ? isProcessDocPath(abs, fileCache) : false
|
|
130
|
+
tokenCache.set(token, marked)
|
|
131
|
+
}
|
|
132
|
+
if (!marked) return line
|
|
133
|
+
n += 1
|
|
134
|
+
return `${PROCESS_DOC_MARK} ${line}`
|
|
135
|
+
})
|
|
136
|
+
if (n === 0) return src
|
|
137
|
+
const body = out.join("\n")
|
|
138
|
+
return `${body}${body.endsWith("\n") ? "" : "\n"}${processDocSummary(n)}`
|
|
139
|
+
}
|
package/tools/search.mjs
CHANGED
|
@@ -6,8 +6,12 @@ import {
|
|
|
6
6
|
splitGlobPatterns,
|
|
7
7
|
compileGlobMatchers,
|
|
8
8
|
normalizeEOL,
|
|
9
|
-
IGNORED_DIRS,
|
|
10
9
|
} from "./shared.mjs";
|
|
10
|
+
// 排除名单单源(零依赖叶档 `../skip-dirs.mjs`——2026-10-27 四份名单合一):旧名 `IGNORED_DIRS`
|
|
11
|
+
// 退场,本档三处直接引 `SKIP_DIRS`(一词一物)。
|
|
12
|
+
import { SKIP_DIRS } from "../skip-dirs.mjs";
|
|
13
|
+
// 过程档标记(TOOLS.md §6.28):命中行的**档**在索引排除集内 ⇒ 行首逐字前缀。只加可读信息,不改命中集。
|
|
14
|
+
import { PROCESS_DOC_MARK, isProcessDocPath } from "./process-doc-mark.mjs";
|
|
11
15
|
import { readFile, readdir, stat, lstat } from "node:fs/promises";
|
|
12
16
|
import { join } from "node:path";
|
|
13
17
|
|
|
@@ -60,7 +64,7 @@ export const globTool = {
|
|
|
60
64
|
},
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
/** Recursively traverse files, yield relative paths (skip
|
|
67
|
+
/** Recursively traverse files, yield relative paths (skip SKIP_DIRS) */
|
|
64
68
|
async function* walkFiles(dir, rel = "") {
|
|
65
69
|
let entries
|
|
66
70
|
try {
|
|
@@ -71,7 +75,7 @@ async function* walkFiles(dir, rel = "") {
|
|
|
71
75
|
for (const e of entries) {
|
|
72
76
|
// Skip ignored dirs AND symbolic links (symlinks to directories would cause infinite loops)
|
|
73
77
|
if (e.isSymbolicLink()) continue
|
|
74
|
-
if (e.isDirectory() &&
|
|
78
|
+
if (e.isDirectory() && SKIP_DIRS.has(e.name)) continue
|
|
75
79
|
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
76
80
|
if (e.isDirectory()) {
|
|
77
81
|
yield* walkFiles(join(dir, e.name), relPath)
|
|
@@ -169,7 +173,7 @@ export const grepTool = {
|
|
|
169
173
|
return
|
|
170
174
|
}
|
|
171
175
|
for (const e of entries) {
|
|
172
|
-
if (e.isDirectory() &&
|
|
176
|
+
if (e.isDirectory() && SKIP_DIRS.has(e.name)) continue
|
|
173
177
|
await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
|
|
174
178
|
}
|
|
175
179
|
}
|
|
@@ -177,9 +181,14 @@ export const grepTool = {
|
|
|
177
181
|
await walk(base, "")
|
|
178
182
|
if (hits.length === 0) return "(no matches)"
|
|
179
183
|
|
|
184
|
+
// §6.28:一次调用一个档记忆表;命中行与上下文行都在原有文案前镀前缀(判据是**档**不是行——
|
|
185
|
+
// 不标则同一档的上下文行读起来像普通档的内容)。打标在 `truncate` 之前,不新增截断点。
|
|
186
|
+
const markCache = new Map()
|
|
187
|
+
const mark = (f) => (isProcessDocPath(f, markCache) ? `${PROCESS_DOC_MARK} ` : "")
|
|
188
|
+
|
|
180
189
|
// No context: keep original path:line: content format
|
|
181
190
|
if (!wantCtx) {
|
|
182
|
-
return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
|
|
191
|
+
return truncate(hits.map((h) => `${mark(h.file)}${h.file}:${h.line}: ${h.text}`).join("\n"))
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
// With context: matching lines use ':', context lines use '-' (like ripgrep); overlapping ranges in same file are merged and de-duplicated
|
|
@@ -197,7 +206,7 @@ export const grepTool = {
|
|
|
197
206
|
}
|
|
198
207
|
for (const l of [...lineSet].sort((a, b) => a - b)) {
|
|
199
208
|
const sep = matchedLines.has(l) ? ":" : "-"
|
|
200
|
-
out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
|
|
209
|
+
out.push(`${mark(file)}${file}${sep}${l}${sep} ${lines[l - 1]}`)
|
|
201
210
|
}
|
|
202
211
|
}
|
|
203
212
|
return truncate(out.join("\n"))
|
package/tools/shared.mjs
CHANGED
|
@@ -21,10 +21,33 @@ export const DESC = (name) => loadToolDoc(name)
|
|
|
21
21
|
export const MAX_READ_LINES = 2000
|
|
22
22
|
export const MAX_OUTPUT_CHARS = 200_000
|
|
23
23
|
|
|
24
|
+
// ─── 写门 `ledger` 参数(2026-11-03 笔-账绑定批 · 设计档 §2.3「工具 schema 面」)───────────────
|
|
25
|
+
// 描述单源:十二把入口工具的 `parameters.properties.ledger` 共用下面这一条(文面逐字 = 设计档 §2.3)。
|
|
26
|
+
// 为什么放这里:同一个参数在十二处各写一遍 = 同一机制十二份描述,措辞一旦漂移,模型读到的规则就有十二版;
|
|
27
|
+
// 而 `git` ∥ `manifest` ∥ `memory` 的差异只是各接一句「本工具的哪一径要号」——差异走后缀,不复制全文。
|
|
28
|
+
// 为什么不进 `required`(决策 35):`required` 是静态字面,而这十二把工具在两模式、子代理、出辖目标下共用;
|
|
29
|
+
// 静态必填会让普通模式读到一条不成立的强制(写锁仅工程模式),也让子代理读到一条没人核的要求。
|
|
30
|
+
// 机械强制在门(缺号即拒),schema 只负责让模型知道有这个参数;拒文因此是唯一教学面(决策 38)。
|
|
31
|
+
export const LEDGER_PARAM_DESC = '本笔调用服务的在途台账号,`"#N"` 或 `"N"`;工程模式下父侧不带 = 拒'
|
|
32
|
+
/** `git`:写型 action 要号、只读 action 不要(决策 37——枚举可判,不猜命令字面)。 */
|
|
33
|
+
export const LEDGER_PARAM_DESC_GIT = LEDGER_PARAM_DESC
|
|
34
|
+
+ ';只读 action(`diff` ∥ `status` ∥ `log` ∥ `show` ∥ `ls-remote` ∥ `blame` ∥ 各 `*Action:"list"`)不需传号'
|
|
35
|
+
/** `manifest`:只有 `init` ∥ `write` 写项目内档(决策 39)。 */
|
|
36
|
+
export const LEDGER_PARAM_DESC_MANIFEST = LEDGER_PARAM_DESC + ';仅 `init` ∥ `write` 要号,`read` 不要'
|
|
37
|
+
/** `memory`:只有 `layer:"project"` 写项目内 `.thincoder/memory/**`(决策 39;`put` 缺省 `personal`)。 */
|
|
38
|
+
export const LEDGER_PARAM_DESC_MEMORY = LEDGER_PARAM_DESC
|
|
39
|
+
+ ';仅落 project 层的写型 action 要号:`layer:"project"`,或省略 `layer`、单条删除且 id 以 `project:` 开头(此时层由前缀定;`personal` ∥ `team` 落用户数据目录,不要号)'
|
|
40
|
+
/** 写门 `project` 参数(2026-11-03 项目声明面批 · 设计档 §2.3「工具 schema 面」/ 决策 40):项目根路径(带
|
|
41
|
+
* `PROJECT-MANIFEST.json` 的那个目录)—— 台账按项目根算键,所以「传项目」就是把这个根传进来,零映射表。
|
|
42
|
+
* 五把可能缺定位参数的工具(`bash` ∥ `memory` ∥ `execute` ∥ `git` ∥ `manifest`)共用这一条(文面逐字 = 设计档
|
|
43
|
+
* §2.3);写盘七件不加——目标路径恒在。定位参数在场时以它为准,`project` 只补位;纯声明,不改执行目录。 */
|
|
44
|
+
export const PROJECT_PARAM_DESC = '本笔调用服务的项目根——带 `PROJECT-MANIFEST.json` 的那个目录(绝对路径,或相对会话 cwd);工程模式下父侧 `bash` ∥ `memory` 必带,`execute` ∥ `git` ∥ `manifest` 在定位参数缺位时必带。纯声明:不改执行目录(`bash` 仍在会话 cwd 执行,`workdir` 才是改执行目录的那个)。'
|
|
45
|
+
|
|
24
46
|
const ENCODING_DETECT_MAX_TRIM = 3
|
|
25
47
|
const SYNTAX_CHECK_TIMEOUT = 10000
|
|
26
48
|
export const BASH_TIMEOUT_MS = 120_000
|
|
27
|
-
|
|
49
|
+
// `IGNORED_DIRS` 退场(2026-10-27 四份名单合一):单源 = `../skip-dirs.mjs` 的 `SKIP_DIRS`,
|
|
50
|
+
// 消费面 `search.mjs` 直接引它(一词一物——同一集合不再两个名字)。
|
|
28
51
|
|
|
29
52
|
/** SSRF guard: check if a hostname is private/internal. Shared by web.mjs and codemode.mjs.
|
|
30
53
|
* Returns TRUE for private hosts — callers block them. */
|
package/tools/ssh.mjs
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
* 会话跨调用存活于池(非本档持有);depth>0 排除(`agent/helpers.mjs` `SUBAGENT_TOOL_EXCLUSIONS`)。
|
|
13
13
|
* 动态 import(W8 契约②:agent-tools 静态链达 node:sqlite——端壳静态闭包须保持零命中)。
|
|
14
14
|
*/
|
|
15
|
-
import { DESC } from "./shared.mjs"
|
|
15
|
+
import { DESC, LEDGER_PARAM_DESC } from "./shared.mjs"
|
|
16
|
+
// `~` 展开单源(`touchedPaths` 钩子与传输件同一件叶子——`config.mjs` 已 import 同一处)。
|
|
17
|
+
import { expandHome } from "../expand-home.mjs"
|
|
16
18
|
|
|
17
|
-
export const SSH_ACTIONS = ["connect", "send", "read", "secret_input", "close", "list", "credentials", "forget"]
|
|
19
|
+
export const SSH_ACTIONS = ["connect", "send", "read", "secret_input", "close", "list", "credentials", "forget", "put", "get"]
|
|
18
20
|
|
|
19
21
|
/** 参数校验(缺必填 / 形错 ⇒ `Error: …`——不执行;§6.22 动作表逐动作必填面)。 */
|
|
20
22
|
export function validateArgs(args) {
|
|
@@ -30,6 +32,19 @@ export function validateArgs(args) {
|
|
|
30
32
|
return null
|
|
31
33
|
}
|
|
32
34
|
if (action === "list" || action === "credentials") return null
|
|
35
|
+
if (action === "put" || action === "get") {
|
|
36
|
+
// 两个传输动作 = 单文件(目录参数不收——递归归边界句);主机 / 凭据全取会话记录值(S10)。
|
|
37
|
+
if (typeof args.localPath !== "string" || args.localPath.trim() === "") return `Error: ${action} requires localPath`
|
|
38
|
+
if (typeof args.remotePath !== "string" || args.remotePath.trim() === "") return `Error: ${action} requires remotePath`
|
|
39
|
+
if (args.protocol !== undefined && args.protocol !== null && args.protocol !== "" && args.protocol !== "scp" && args.protocol !== "sftp") {
|
|
40
|
+
return `Error: ${action} protocol must be "scp" (classic, the default) or "sftp"`
|
|
41
|
+
}
|
|
42
|
+
// S10「`put` 参数面没有 `host` / `user` / `port`」:传了就拒——静默忽略会让模型以为换了目标主机。
|
|
43
|
+
const stray = ["host", "user", "port"].filter((k) => args[k] !== undefined && args[k] !== null && args[k] !== "")
|
|
44
|
+
if (stray.length) {
|
|
45
|
+
return `Error: ${action} takes the host / user / port from the session — remove ${stray.join(" / ")} (the session already names the target host)`
|
|
46
|
+
}
|
|
47
|
+
}
|
|
33
48
|
if (action === "forget") {
|
|
34
49
|
if (typeof args.host !== "string" || args.host.trim() === "") return "Error: forget requires host"
|
|
35
50
|
if (args.port !== undefined && args.port !== null && args.port !== "" && !Number.isFinite(Number(args.port))) {
|
|
@@ -50,18 +65,24 @@ export const sshTool = {
|
|
|
50
65
|
parameters: {
|
|
51
66
|
type: "object",
|
|
52
67
|
properties: {
|
|
53
|
-
action: { type: "string", enum: SSH_ACTIONS, description: "Action to run (one per call): connect | send | read | secret_input | close | list | credentials | forget." },
|
|
68
|
+
action: { type: "string", enum: SSH_ACTIONS, description: "Action to run (one per call): connect | send | read | secret_input | close | list | credentials | forget | put | get." },
|
|
54
69
|
host: { type: "string", description: "connect / forget: target host (IP or name)." },
|
|
55
70
|
user: { type: "string", description: "connect: login user (optional: your local ssh config decides, else the user recorded for this host locally)." },
|
|
56
71
|
port: { type: "number", description: "connect / forget: TCP port (optional, default 22)." },
|
|
57
72
|
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
|
|
59
|
-
id: { type: "string", description: "send / read / secret_input / close: session id from connect (`ssh#N`)." },
|
|
73
|
+
promptPattern: { type: "string", description: "connect: regex that marks the shell prompt as ready (optional; the default recognises lines ending in # > $ %): without a hit, send/read on a slow device wait out the full timeout instead of returning early." },
|
|
74
|
+
id: { type: "string", description: "send / read / secret_input / close / put / get: session id from connect (`ssh#N`)." },
|
|
60
75
|
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
76
|
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
|
|
63
|
-
maxChars: { type: "number", description: "read: return
|
|
77
|
+
timeoutMs: { type: "number", description: "send / read: wait budget override (default 60000) — a return happens the moment the device is waiting for input (prompt, `--More--`, a y/n or password prompt); on expiry the call returns partial output plus a line naming what was not seen. put / get: transfer budget override (default 600000) — on expiry the process tree is killed." },
|
|
78
|
+
maxChars: { type: "number", description: "read: return-window override (default 8000) — head 60% + a note + tail 40%; the omitted middle is kept in the session, so the next read collects it." },
|
|
64
79
|
identityFile: { type: "string", description: "connect: private-key path to offer (`-i`, `~` expands); remembered for this host and reused when omitted (optional)." },
|
|
80
|
+
localPath: { type: "string", description: "put / get: local file path (`~` expands; a file, never a directory)." },
|
|
81
|
+
remotePath: { type: "string", description: "put / get: path of the file on the session host (`/flash/img.bin`)." },
|
|
82
|
+
overwrite: { type: "boolean", description: "get only: replace an existing local file (default false — an existing target is refused, never overwritten silently). put follows scp's own semantics: a same-named remote file is replaced." },
|
|
83
|
+
protocol: { type: "string", enum: ["scp", "sftp"], description: "put / get: `scp` = classic SCP protocol, the default and what network devices speak; `sftp` = the SFTP subsystem, for hosts that only offer that. No automatic fallback between them." },
|
|
84
|
+
// 写门接面(⑬ ④):`get` 往项目内落盘 ⇒ 与 `write` 同一道门——那一道门读的就是本参数。
|
|
85
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC + ";仅 `get` 要号,`put` 写的是远端主机,不要" },
|
|
65
86
|
},
|
|
66
87
|
required: ["action"],
|
|
67
88
|
},
|
|
@@ -70,6 +91,19 @@ export const sshTool = {
|
|
|
70
91
|
isReadonlyAction(args) {
|
|
71
92
|
return args?.action === "list" || args?.action === "read" || args?.action === "credentials"
|
|
72
93
|
},
|
|
94
|
+
// 写门接面(§6.22 ⑬ ④ · D-TO26):只有 `get` 往本地落盘;其余动作不写项目内档。
|
|
95
|
+
// 钩子契约 = `agent/helpers.mjs` 的 `toolTouchPaths`(`const hook = tool?.touchedPaths`)——
|
|
96
|
+
// 光有这个钩子不够:门还要把 `ssh` 收进入口集并按 action 判要号,三处缺一不可。
|
|
97
|
+
// `~` 在这里就展开成绝对形:钩子拿不到 `ctx`(契约形 = `touchedPaths(args)`,`toolTouchPaths` 以 `hook(a)` 调用),
|
|
98
|
+
// 而各消费面都在其后按会话 cwd 归一(`resolve(agent.cwd, p)`;`agent.cwd` 即工具 ctx 的 `cwd`,见
|
|
99
|
+
// `agent/dispatch-run.mjs` 的 `toolCtx`)⇒ 原样回 `~/x` 会被拼成 `<项目>/~/x` 这个虚构路径,工程模式下
|
|
100
|
+
// `get` 到 `~`(本工具最常见的备份落点)因此被无故要号、判据点也不是真实落点。回 `expandHome` 的绝对形后,
|
|
101
|
+
// 消费面的 `resolve(agent.cwd, …)` 对它恒等 ⇒ 判据点 = 真实落点,与传输件起跑点同一件叶子(D2)。
|
|
102
|
+
// 空 ∥ 非字符串 ⇒ `[]`(不虚报——缺参由 execute 的 `validateArgs` 出可读句)。
|
|
103
|
+
touchedPaths(args) {
|
|
104
|
+
const localPath = String(args?.action ?? "") === "get" ? args?.localPath : null
|
|
105
|
+
return typeof localPath === "string" && localPath.trim() !== "" ? [expandHome(localPath)] : []
|
|
106
|
+
},
|
|
73
107
|
async execute(args, ctx) {
|
|
74
108
|
const invalid = validateArgs(args)
|
|
75
109
|
if (invalid) return invalid
|
|
@@ -96,6 +130,13 @@ export const sshTool = {
|
|
|
96
130
|
res = mod.credentialsSsh()
|
|
97
131
|
} else if (action === "forget") {
|
|
98
132
|
res = mod.forgetSsh({ host: args.host, port: args.port ?? null })
|
|
133
|
+
} else if (action === "put" || action === "get") {
|
|
134
|
+
// 传输面自成一件(非交互原语:无剥离 / 无就绪判据 / 无遮蔽——塞进会话件会把两套语义混在一档)。
|
|
135
|
+
const xfer = await import("../agent-tools/ssh-transfer.mjs")
|
|
136
|
+
res = await xfer.transferSsh(parent, ctx, {
|
|
137
|
+
action, id: args.id, localPath: args.localPath, remotePath: args.remotePath,
|
|
138
|
+
overwrite: args.overwrite === true, protocol: args.protocol ?? null, timeoutMs: args.timeoutMs,
|
|
139
|
+
})
|
|
99
140
|
} else {
|
|
100
141
|
res = mod.listSsh(parent)
|
|
101
142
|
}
|
package/tools/tree.mjs
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
import { DESC, truncate, resolveInCwd } from "./shared.mjs"
|
|
8
8
|
import { readdir, stat } from "node:fs/promises"
|
|
9
9
|
import { join, basename, extname } from "node:path"
|
|
10
|
+
// 排除名单单源(零依赖叶档 `skip-dirs.mjs`——2026-10-27 四份名单合一):本档不再持本地副本。
|
|
11
|
+
// 两项旧值随之退场:`turbo`(无点,笔误——另三份都是 `.turbo`)、`bin`(`thincoder-cli/bin/**`
|
|
12
|
+
// 是 CLI 入口、产品代码,不该从任何面上被剪掉)。
|
|
13
|
+
import { SKIP_DIRS } from "../skip-dirs.mjs"
|
|
10
14
|
|
|
11
15
|
const MAX_ENTRIES = 200
|
|
12
16
|
const DEFAULT_DEPTH = 3
|
|
13
|
-
const SKIP_DIRS = new Set(["node_modules", "bin", "obj", "dist", "build", "coverage", "turbo", ".git", ".thincoder", ".vs", ".venv", "__pycache__", ".idea"])
|
|
14
17
|
const BINARY_EXTS = new Set([".exe", ".dll", ".png", ".jpg", ".jpeg", ".gif", ".pdf", ".docx", ".xlsx", ".pptx", ".zip", ".7z", ".mp3", ".mp4", ".woff", ".woff2", ".ico"])
|
|
15
18
|
|
|
16
19
|
// ─── cwd 归一注入缝(#56——「cwd 归一开关按端注入」,形态参 §2.13.5 注入缝)─────────
|
package/tools/write-path.mjs
CHANGED
|
@@ -34,9 +34,9 @@ export function markDirty(abs) { dirtyPaths.add(abs) }
|
|
|
34
34
|
export function clearDirty(abs) { dirtyPaths.delete(abs) }
|
|
35
35
|
export function isDirty(abs) { return dirtyPaths.has(abs) }
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// write 全文重写 →
|
|
37
|
+
// 写入工具记录受影响行范围(受影响区 = 从 startLine 起、净漂移 shift 行)——行号锚护栏
|
|
38
|
+
// 的数据源:目标区整体在受影响区之前 → 行号未漂移 → 允许;区内或其下 → 拒;
|
|
39
|
+
// write 全文重写 → 全文件受影响,任何行号都拒。判据本体 = 下方 `lineAnchorVerdict`。
|
|
40
40
|
const lastWrites = new Map() // abs → { type: 'write'|'edit'|'insert', startLine, shift }
|
|
41
41
|
export function recordWrite(abs, write) {
|
|
42
42
|
lastWrites.set(abs, write)
|
|
@@ -74,6 +74,67 @@ export function resetWritePath() { injected = null }
|
|
|
74
74
|
export const dirtyRefusalMessage = (abs) =>
|
|
75
75
|
`File has unsaved changes in the editor: ${abs}. Save or discard before allowing automated edits.`
|
|
76
76
|
|
|
77
|
+
// ────────────────────────────────────────
|
|
78
|
+
// 行号锚护栏(edit 行号形 ∥ insert_after 共用的唯一判据)
|
|
79
|
+
// ────────────────────────────────────────
|
|
80
|
+
/**
|
|
81
|
+
* 判据(六行按序判):放行 ⇒ `null`;拒绝 ⇒ 命中行的实况
|
|
82
|
+
* `{ row, type?, startLine?, shift? }`(row = §7.1 的行号 1–6)。
|
|
83
|
+
*
|
|
84
|
+
* @param {string} abs 目标绝对路径(记账键)
|
|
85
|
+
* @param {number|null} startLine 目标区首行;`null` = 调用方没有行号目标(正则定位形)
|
|
86
|
+
* @param {number|null} endLine 目标区末行(单行形与 startLine 同值)
|
|
87
|
+
*/
|
|
88
|
+
export function lineAnchorVerdict(abs, startLine, endLine) {
|
|
89
|
+
const lw = lastWrites.get(abs)
|
|
90
|
+
const dirty = dirtyPaths.has(abs)
|
|
91
|
+
if (!lw) return dirty ? { row: 6 } : null // 行 1 放行 / 行 6 拒
|
|
92
|
+
if (dirty) return { row: 2, type: lw.type, startLine: lw.startLine, shift: lw.shift }
|
|
93
|
+
// 行 3–5 判的是「模型给的行号」,没有行号目标就无从判——只判到记账态这一层
|
|
94
|
+
if (startLine == null) return null
|
|
95
|
+
if (lw.type === "write") return { row: 3, type: "write" }
|
|
96
|
+
// 「整体在之前」的边界按记账类型分:替换要读也写目标行(`<`),插入只落在两行之间(`<=`)
|
|
97
|
+
const last = endLine ?? startLine
|
|
98
|
+
const untouched = lw.type === "insert" ? last <= lw.startLine : last < lw.startLine
|
|
99
|
+
if (untouched) return null // 行 4 放行
|
|
100
|
+
return { row: 5, type: lw.type, startLine: lw.startLine, shift: lw.shift }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 第 1 / 3 / 5 行的首句之后共用的三要素(两条下一步 + 第二条路的成立条件)。 */
|
|
104
|
+
const LINE_ANCHOR_NEXT_STEPS =
|
|
105
|
+
`Editing by the old numbers would land on the wrong lines and still report success.\n` +
|
|
106
|
+
`Two ways on:\n` +
|
|
107
|
+
` 1. read this file again and retry with the new line numbers;\n` +
|
|
108
|
+
` 2. address the region without line numbers — pass its current content as old_string, or use hashline_edit,\n` +
|
|
109
|
+
` which needs old_hashes from a read with hashes=true and is only valid while that block's content is unchanged.`
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 拒文(单点构造——edit 行号形与 insert_after 同句;四要素固定:路径与漂移事实 / 重 read /
|
|
113
|
+
* 改走位置无关寻址 / 那条路的成立条件)。事实取自 `lineAnchorVerdict`,不另算一份。
|
|
114
|
+
*
|
|
115
|
+
* 第 2 行(脏 ∧ 陈旧快照)与第 6 行(无快照但脏)报不出「上次写入点 + 漂移量」——首句改成
|
|
116
|
+
* 说清这两个数不可得,其余三要素不变(照填一个不可信的数字等于把它重新递给模型)。
|
|
117
|
+
*/
|
|
118
|
+
export function lineAnchorRefusal(abs, startLine, endLine) {
|
|
119
|
+
const hit = lineAnchorVerdict(abs, startLine, endLine)
|
|
120
|
+
if (!hit) {
|
|
121
|
+
throw new Error("lineAnchorRefusal: the call passes the line-anchor criterion — no refusal to build")
|
|
122
|
+
}
|
|
123
|
+
const range = startLine == null ? null : endLine == null || startLine === endLine ? `L${startLine}` : `L${startLine}-L${endLine}`
|
|
124
|
+
const head = hit.row === 2
|
|
125
|
+
? `${abs} — these line numbers are stale, and the drift cannot be measured: a write in this session changed\n` +
|
|
126
|
+
`this file after the recorded write point, so neither that line nor an offset from it can be trusted.`
|
|
127
|
+
: hit.row === 6
|
|
128
|
+
? `${abs} — this file was modified since your last read; line numbers may be stale, and there is no recorded\n` +
|
|
129
|
+
`write point to check them against.`
|
|
130
|
+
: hit.row === 3
|
|
131
|
+
? `${abs} — these line numbers are stale: this file was rewritten in full by a write since your last read,\n` +
|
|
132
|
+
`so no line number from that read can still be trusted.`
|
|
133
|
+
: `${abs} — these line numbers are stale: the last write to this file was at L${hit.startLine}\n` +
|
|
134
|
+
`(line numbers are now offset by ${hit.shift >= 0 ? "+" : ""}${hit.shift}), and the target range ${range} falls inside or below it.`
|
|
135
|
+
return `${head}\n${LINE_ANCHOR_NEXT_STEPS}`
|
|
136
|
+
}
|
|
137
|
+
|
|
77
138
|
/**
|
|
78
139
|
* 门禁单点(src 面与 dest 面共用——每面各问一次 `openDoc`):
|
|
79
140
|
* fail-closed(给了 `applyEdit` 却没给 `isDirty` ⇒ 脏态不可知 ⇒ 抛配置错,不静默退化为
|
package/traces/trace-store.mjs
CHANGED
|
@@ -222,11 +222,12 @@ export function recordChatTrace(provider, opts = {}, result = null, error = null
|
|
|
222
222
|
const logCtx = opts.logCtx ?? {}
|
|
223
223
|
if (!tracesEnabled(logCtx)) return
|
|
224
224
|
// cwd/session 经 logCtx 增补(调用点传 agent.cwd / agent._sessionStart——
|
|
225
|
-
// D-TR3 的 cwdHash/命名与 D-TR1 的 session
|
|
226
|
-
//
|
|
225
|
+
// D-TR3 的 cwdHash/命名与 D-TR1 的 session 字段所需);logCtx 未携 cwd(无 agent 作用域
|
|
226
|
+
// 的调用点)⇒ 该值缺失即失败,不回落进程 cwd(换界会静默写错会话目录)。
|
|
227
|
+
// 该类点开关状态:distill 已含
|
|
227
228
|
// traces 开关(D-TR6 fix round1);auto-think 已闭环(D-TS12——chat 调用点
|
|
228
229
|
// logCtx 全字段补传:traces/session/cwd/role/depth/kind——无残留点)。
|
|
229
|
-
const cwd = logCtx.cwd
|
|
230
|
+
const cwd = logCtx.cwd
|
|
230
231
|
// D-TR3(fix round1):分日按本地时区(_traceHooks.now——T-TR12 注入点);
|
|
231
232
|
// ts 与 dateStr 同源(同一时刻)——记录时间戳与目录日不撕裂。
|
|
232
233
|
const now = _traceHooks.now()
|
package/agent/light-round.mjs
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* agent/light-round.mjs — 轻通道代码笔通路谓词(F-LC5 · 设计 = `docs/core/design/LIGHT-CHANNEL.md` §2.8)。
|
|
3
|
-
*
|
|
4
|
-
* `lightRoundOpen(agent)`:闸读判据——**两件双在盘**才算轻通道轮在盘:
|
|
5
|
-
* ① 轮次台账行:`status = 在途` ∧ `title` 携「收尾链待跑」(轮型标记 = 闸凭据)∧ `task_book` 非空;
|
|
6
|
-
* ② 该指针指向的轮次批档:`§1` 状态行 = 「进行中」(判定器单源 = `readBatchStatusLine`)。
|
|
7
|
-
* 查找面(纯向下)= 会话锚 + 其直接子目录一层(候选枚举单源 = `ledgerChildren`——与族发现同级判据
|
|
8
|
-
* 同源;不递归 ∥ 不取同胞):逐候选查行 ∥ 以各候选为基解析指针(台账写门同式)。歧义候选
|
|
9
|
-
* (`projectRootView` 判 `workspace`——如工作区锚自身)⇒ 跳过该候选、照查其余(既定观察态,非读错);
|
|
10
|
-
* 候选读错 ∥ 不可判 ⇒ 停本候选(总判不因他候选读错而失——开路凭据仍是「两件校验全过」)。
|
|
11
|
-
* 语义:开门动作 ≡ 开轮两件落盘(「启动即挂账」本体——无独立于账的开门面);单件不放行
|
|
12
|
-
* (轮档独在 ∥ 行独在 ∥ 无标记 ∥ 行非在途 ∥ 指针失据 ∥ 档已收口 ⇒ 皆 false);收口即自闭
|
|
13
|
-
* (信号生命周期 = 账生命周期——零撤销动作 ∥ 零跨轮残留)。
|
|
14
|
-
* 读错 ∥ 不可判 ⇒ `false`(fail-closed——照拦;被拦时文案不因读错而变)。
|
|
15
|
-
* 只读:台账 = `ledgerQuery` **动态 import**(`ledger-db.mjs` 静态 import node:sqlite ⇒
|
|
16
|
-
* 消费侧静态 import 破 W8 契约②);批档 = 一次 `readFileSync`。
|
|
17
|
-
*/
|
|
18
|
-
import { readFileSync } from "node:fs"
|
|
19
|
-
import { resolve } from "node:path"
|
|
20
|
-
import { projectRootView, resolveProjectRoot } from "../manifest.mjs"
|
|
21
|
-
import { resolveDeclaredRef } from "../declaration.mjs"
|
|
22
|
-
import { readBatchStatusLine } from "../agent-tools/batch-skeleton.mjs"
|
|
23
|
-
|
|
24
|
-
/** 轻通道轮在盘谓词(异步 · 只读 · fail-closed)。契约见头注。
|
|
25
|
-
* @param {object} agent — 会话 agent(读 `cwd`)
|
|
26
|
-
* @returns {Promise<boolean>} true = 放行(在盘轮次);读错 ∥ 不可判 ⇒ false(照拦)
|
|
27
|
-
*/
|
|
28
|
-
export async function lightRoundOpen(agent) {
|
|
29
|
-
try {
|
|
30
|
-
const cwd = agent?.cwd
|
|
31
|
-
if (typeof cwd !== "string" || cwd.trim() === "") return false
|
|
32
|
-
// W8 契约②:node:sqlite 链只经动态 import 进入(勿升静态)——候选枚举与逐候选查行同取本档公共面。
|
|
33
|
-
const { ledgerChildren, ledgerQuery, ROUND_MARKER } = await import("../ledger.mjs")
|
|
34
|
-
const anchor = resolve(cwd)
|
|
35
|
-
// 候选面 = 锚自身 + 直接子目录一层(纯向下;子项枚举单源 = `ledgerChildren`)。
|
|
36
|
-
const candidates = [anchor, ...ledgerChildren(anchor).map((p) => p.root)]
|
|
37
|
-
for (const cand of candidates) {
|
|
38
|
-
// 工作区候选(下辖 ≥2 项目——`projectRootView`)⇒ 跳过、照查其余(工作区锚不充当项目 = AC-M2-19 同判;
|
|
39
|
-
// 该态判据 = 根推导单源「已注册」的库臂排除(键控库在盘 ≠ 可作项目)——既定观察态,非读错)。
|
|
40
|
-
if (projectRootView(cand).state === "workspace") continue
|
|
41
|
-
let rows
|
|
42
|
-
try { rows = ledgerQuery({ cwd: cand, status: "在途" }) } catch { continue } // 候选读错 ⇒ 停本候选
|
|
43
|
-
if (!Array.isArray(rows) || rows.length === 0) continue
|
|
44
|
-
// 指针基准 = 台账写门同式(`ledger-cmd.mjs` assertTaskBookGate 同一子表达式)——以各候选为基。
|
|
45
|
-
const base = resolveProjectRoot(cand) ?? resolve(cand ?? ".")
|
|
46
|
-
for (const row of rows) {
|
|
47
|
-
if (!String(row?.title ?? "").includes(ROUND_MARKER)) continue // 无标记 ⇒ 非轻通道轮(全链批行)
|
|
48
|
-
const taskBook = row?.task_book
|
|
49
|
-
if (typeof taskBook !== "string" || taskBook.trim() === "") continue // 缺指针 ⇒ 单件不放行
|
|
50
|
-
const part = taskBook.split("§")[0].trim() // 台账写门同式:首个 § 前子串
|
|
51
|
-
if (part === "") continue
|
|
52
|
-
const hit = resolveDeclaredRef(base, part)
|
|
53
|
-
if (!hit.ok) continue // 指针失据 ⇒ 不放行
|
|
54
|
-
let src
|
|
55
|
-
try { src = readFileSync(hit.abs, "utf8") } catch { continue } // 档读不到 ⇒ 不放行
|
|
56
|
-
if (readBatchStatusLine(src) === "open") return true // 两件双在盘 ⇒ 放行
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return false
|
|
60
|
-
} catch {
|
|
61
|
-
return false // 读错 ∥ 不可判 ⇒ fail-closed(照拦)
|
|
62
|
-
}
|
|
63
|
-
}
|