@thincoder/core 0.10.9 → 0.10.11
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 +28 -2
- package/advisor/messages.mjs +4 -5
- package/advisor/project-context.mjs +69 -44
- package/agent/completion.mjs +4 -1
- package/agent/family-tools.mjs +12 -10
- package/agent/helpers.mjs +19 -11
- package/agent/light-round.mjs +1 -4
- package/agent/record-results.mjs +6 -1
- package/agent/run-start.mjs +1 -0
- package/agent/setup-reminders.mjs +1 -1
- package/agent/turn-loop.mjs +17 -13
- package/agent/unbooked-pen.mjs +62 -0
- package/agent-tools/advisor.mjs +15 -31
- package/agent-tools/audit-block.mjs +16 -62
- package/agent-tools/batch-gate.mjs +12 -21
- package/agent-tools/checkpoint.mjs +11 -6
- package/agent-tools/context.mjs +1 -1
- package/agent-tools/design-token.mjs +9 -2
- package/agent-tools/digest-budget.mjs +2 -0
- package/agent-tools/doc-check.mjs +17 -23
- package/agent-tools/manifest.mjs +45 -14
- package/agent-tools/review-facts.mjs +35 -81
- package/agent-tools/settings.mjs +65 -5
- package/agent-tools/spawn-gates.mjs +68 -31
- package/agent-tools/subagent-async.mjs +6 -3
- package/agent-tools/subagent-spawn.mjs +76 -80
- package/agent-tools/subagent.mjs +16 -16
- package/agent-tools.mjs +1 -1
- package/browser/launch.mjs +14 -3
- package/browser/queue.mjs +31 -3
- package/browser/session.mjs +4 -0
- package/config-defaults.mjs +8 -0
- package/config-presets.mjs +1 -1
- package/config.mjs +60 -11
- package/declaration.mjs +5 -12
- package/doc-check/anchors.mjs +53 -60
- package/doc-check/index.mjs +106 -140
- package/doc-check/paragraph.mjs +119 -0
- package/fts-text.mjs +4 -1
- package/i18n.mjs +5 -0
- package/ledger-cmd.mjs +17 -4
- package/ledger-db.mjs +14 -8
- package/ledger.mjs +1 -1
- package/manifest-discovery.mjs +19 -10
- package/manifest-schema.mjs +7 -35
- package/manifest.mjs +21 -9
- package/memory/code-search.mjs +5 -14
- package/memory/docs.mjs +5 -14
- package/memory/read-gate.mjs +26 -0
- package/model-context.mjs +98 -0
- package/model-specs.mjs +110 -37
- package/package.json +2 -2
- package/permission.mjs +3 -1
- package/prompts/advisor-design.md +104 -38
- package/prompts/advisor-round1.md +72 -24
- package/prompts/advisor-round2.md +62 -25
- package/prompts/advisor-round3.md +68 -24
- package/prompts/common.md +214 -58
- package/prompts/consult-base.md +10 -3
- package/prompts/discipline-engineering.md +202 -54
- package/prompts/discipline-normal.md +95 -44
- package/prompts/persona-coder.md +3 -1
- package/prompts/persona-eng-coder.md +29 -9
- package/prompts/persona-eng-designer.md +101 -35
- package/prompts/persona-engineering.md +320 -98
- package/prompts/persona-explore.md +1 -1
- package/prompts/persona-normal.md +16 -8
- package/provider/anthropic.mjs +127 -4
- package/provider/core.mjs +16 -6
- package/provider/google.mjs +8 -8
- package/provider/list-models.mjs +37 -5
- package/provider/normalize.mjs +2 -2
- package/provider/responses.mjs +7 -7
- package/provider/sse.mjs +6 -5
- package/proxy-transport.mjs +5 -3
- package/proxy.mjs +2 -1
- package/session-guard.mjs +7 -1
- package/session-lifecycle.mjs +23 -8
- package/session-segments.mjs +1 -0
- package/session-slot-claims.mjs +4 -0
- package/session-slot-write.mjs +6 -0
- package/session-slots-manifest.mjs +4 -0
- package/token-ttl.mjs +10 -0
- package/tool-docs/advisor.md +24 -5
- package/tool-docs/doc_check.md +44 -25
- package/tool-docs/ledger.md +2 -2
- package/tool-docs/manifest-read.md +8 -0
- package/tool-docs/manifest.md +9 -8
- package/tool-docs/subagent.md +41 -16
- package/tools/web-deepseek.mjs +26 -11
- package/doc-check/version.mjs +0 -84
- package/doc-check/width.mjs +0 -132
package/agent-tools/advisor.mjs
CHANGED
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { runAdvisorReview, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
|
|
11
11
|
import { resolveBatchDocPath } from "./batch.mjs"
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
import { normAbs } from "../agent/write-gate.mjs"
|
|
12
|
+
// 设计评审的文档受理面单源 = review-facts.mjs 的 resolveReviewDocPaths(判据正文 =
|
|
13
|
+
// docs/core/design/ADVISOR-GUARDS.md §12:相对形按会话项目根 / 绝对形照用 / 受理 = 项目内现存 `.md`)。
|
|
14
|
+
import { projectRootView } from "../manifest-discovery.mjs"
|
|
16
15
|
import { resolveReviewDocPaths } from "./review-facts.mjs"
|
|
17
16
|
import {
|
|
18
17
|
generateDesignToken,
|
|
@@ -62,7 +61,7 @@ export const advisorTool = {
|
|
|
62
61
|
documents: {
|
|
63
62
|
type: "array",
|
|
64
63
|
items: { type: "string" },
|
|
65
|
-
description: "Doc paths to review (design / requirements / referenced docs).
|
|
64
|
+
description: "Doc paths to review (design / requirements / referenced docs). One rule: a relative path resolves against the session project root (e.g. docs/core/design/ADVISOR-GUARDS.md); an absolute path is used as-is. Accepted = an existing .md file anywhere inside a project (a repo-root doc such as METHODOLOGY.md counts); anything else is refused with the resolved path, the reason, and the two expected forms. The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
|
|
66
65
|
},
|
|
67
66
|
batchDoc: {
|
|
68
67
|
type: "string",
|
|
@@ -111,36 +110,21 @@ export const advisorTool = {
|
|
|
111
110
|
)
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
//
|
|
115
|
-
// resolveReviewDocPaths (2026-10-04 · #921 · the four-leg single authority in
|
|
116
|
-
// review-facts.mjs): legs ① session-root set / ② owning-project root set keep the
|
|
117
|
-
// legacy behavior verbatim; leg ③ probes candidate project roots for bare
|
|
118
|
-
// project-root-relative forms; leg ④ disambiguates by readable-file uniqueness
|
|
119
|
-
// (fail-closed — criterion=scope-doc-ambiguous when ≥2 candidates are readable).
|
|
113
|
+
// 设计评审:受理面 = 项目内现存 `.md`——相对形按会话项目根解析(越根 ⇒ 拒)· 绝对形照用 · 解析不出 ⇒ 拒并说清期望形;零试探、零歧义归一。
|
|
120
114
|
if (reviewType === "design" && documents) {
|
|
121
|
-
const {
|
|
122
|
-
if (invalid.length > 0
|
|
115
|
+
const { invalid } = resolveReviewDocPaths(documents, agent.cwd)
|
|
116
|
+
if (invalid.length > 0) {
|
|
123
117
|
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
118
|
+
// 拒发文案:既有稳定前缀逐字(行首)+ 逐文档「输入 → 解析后绝对路径 ∥ 拒因」+ 锚态(仅无项目根)+ 期望形两句。
|
|
119
|
+
const lines = invalid.map((v) => `- ${v.doc} → ${v.resolved ?? "(no base)"} ∥ reason=${v.reason}`)
|
|
120
|
+
if (invalid.some((v) => v.reason === "no-project-root")) {
|
|
121
|
+
const view = projectRootView(agent.cwd)
|
|
122
|
+
const where = view.state === "workspace" ? `workspace (${view.candidates.join(", ")})` : "none (no PROJECT-MANIFEST.json here or above)"
|
|
123
|
+
lines.push(`Session anchor is ${where} — no project root, so a relative path has no base to resolve against.`)
|
|
130
124
|
}
|
|
131
|
-
|
|
132
|
-
// 路径 ← 基面 = 会话 cwd」+ 候选项目根提示 cap 5 + 重试指引——纠正「误报因」缺陷)。
|
|
133
|
-
const invalidList = invalid.map((v) => v.doc).join(", ")
|
|
134
|
-
const attemptedList = invalid
|
|
135
|
-
.map((v) => `${v.doc} → ${v.attempted.length > 0 ? v.attempted.join(" | ") : normAbs(v.doc, agent.cwd)}`)
|
|
136
|
-
.join("; ")
|
|
137
|
-
const shown = candidates.slice(0, 5)
|
|
138
|
-
const more = candidates.length - shown.length
|
|
139
|
-
const candidateNote = shown.length > 0
|
|
140
|
-
? `(candidates: ${shown.join(", ")}${more > 0 ? ` +${more} more` : ""}) — pass project-root-relative (e.g. ${shown[0]}/docs/…) or absolute paths`
|
|
141
|
-
: "— pass project-root-relative or absolute paths"
|
|
125
|
+
lines.push("Expected form 1 — project-root-relative: docs/core/design/ADVISOR-GUARDS.md (resolved against the session project root).", "Expected form 2 — absolute: D:\\repo\\docs\\core\\design\\ADVISOR-GUARDS.md (used as-is).")
|
|
142
126
|
return withIdentityLine(
|
|
143
|
-
`Advisor: design review documents must be documentation files (per the project's conventions). Invalid
|
|
127
|
+
`Advisor: design review documents must be documentation files (per the project's conventions). Invalid:\n${lines.join("\n")}`,
|
|
144
128
|
{ type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-not-doc" },
|
|
145
129
|
)
|
|
146
130
|
}
|
|
@@ -3,74 +3,26 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 自 `subagent-spawn.mjs` **逐字搬移**(D23-3 外提:spawn 档越 300 软线 + 块构造成纯函数
|
|
5
5
|
* ⇒ 判据可直测;D23-4:块内文本零改——含块内自指词,整块搬移不改真值)。本档承载
|
|
6
|
-
* spawn 级固定机制性指令的第二块(审计模板 +
|
|
6
|
+
* spawn 级固定机制性指令的第二块(审计模板 + 批档 §2 指向 + touched 快照):
|
|
7
7
|
* 装配侧只收集(`thincoder-core/agent-tools/subagent-spawn.mjs`),下发面 = system 固块
|
|
8
8
|
* (`child._spawnSystemBlock` → 核 `prepareRun` 拼接——`thincoder-core/agent/setup.mjs`)。
|
|
9
|
+
*
|
|
10
|
+
* 2026-10-24(§6.22 KD-SC6):任务书 = 批次档 §2(子代理自读);A2 拼装摘要面(按 marker 抽取
|
|
11
|
+
* 设计档 / 文件清单 / 验收三段)已随 KD-SC6 删除——审计块只注入批档路径行 + §2 指向句(路径取
|
|
12
|
+
* `ctx.agent._batchDoc`)。理由:工程角色 `_engTaskInput` 只剩一行机械文本,摘要失去来源。
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
|
-
/**
|
|
12
|
-
* AGENT-LOOP-SUBAGENT.md §6.7.6 D-TS5 (A2): mechanically summarize the parent spawn task book for the
|
|
13
|
-
* audit spawn — the three audit-relevant elements VERBATIM (design doc paths /
|
|
14
|
-
* affected-file list / acceptance criteria); verbose context/background is
|
|
15
|
-
* dropped (the auditor can read the design docs themselves — they stay
|
|
16
|
-
* available outside this input). Independence preserved: the input is
|
|
17
|
-
* _engTaskInput (mechanically kept by the parent spawn) — never the
|
|
18
|
-
* eng-coder's self-report. Sections are located by header marker, prioritizing
|
|
19
|
-
* header lines (structured task books: "## 文件清单 …") and falling back to
|
|
20
|
-
* inline markers (flat one-line task books); a section runs to the next header
|
|
21
|
-
* of the SAME OR HIGHER level ("## 文件清单" survives a "### 修改" sub-header).
|
|
22
|
-
* Marker not found → the section is reported as missing (never fabricate).
|
|
23
|
-
*/
|
|
24
|
-
export function summarizeEngTaskBook(taskInput) {
|
|
25
|
-
if (!taskInput) return "(unavailable)"
|
|
26
|
-
// 五项拼装形下,`designPoints` 一段同时承载「设计档落点」与「受影响文件清单」——
|
|
27
|
-
// 前两段同取该块是有意的(语义都在其中);代价 = 摘要里该块出现两次,不是抽取失败。
|
|
28
|
-
const SECTIONS = [
|
|
29
|
-
{ name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/, /^Design points/i] },
|
|
30
|
-
{ name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/, /^Design points/i] },
|
|
31
|
-
{ name: "Acceptance criteria", markers: [/Acceptance(?: criteria)?/i, /验收标准/] },
|
|
32
|
-
]
|
|
33
|
-
const lines = taskInput.split("\n")
|
|
34
|
-
const headerLevel = (l) => {
|
|
35
|
-
const m = l.match(/^\s*(#{1,6})\s/)
|
|
36
|
-
return m ? m[1].length : 0
|
|
37
|
-
}
|
|
38
|
-
const headerIdx = lines.map((l, i) => (headerLevel(l) > 0 ? i : -1)).filter((i) => i >= 0)
|
|
39
|
-
const boundsFor = (from, level) => {
|
|
40
|
-
for (const j of headerIdx) {
|
|
41
|
-
if (j > from && (level === 0 || headerLevel(lines[j]) <= level)) return j
|
|
42
|
-
}
|
|
43
|
-
return lines.length
|
|
44
|
-
}
|
|
45
|
-
const out = []
|
|
46
|
-
for (const { name, markers } of SECTIONS) {
|
|
47
|
-
let from = -1
|
|
48
|
-
let level = 0
|
|
49
|
-
for (const i of headerIdx) {
|
|
50
|
-
if (markers.some((m) => m.test(lines[i]))) { from = i; level = headerLevel(lines[i]); break }
|
|
51
|
-
}
|
|
52
|
-
if (from === -1) {
|
|
53
|
-
for (let i = 0; i < lines.length; i++) {
|
|
54
|
-
if (markers.some((m) => m.test(lines[i]))) { from = i; level = 0; break }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (from === -1) { out.push(`${name}: (not found in the parent task book)`); continue }
|
|
58
|
-
const body = lines.slice(from, boundsFor(from, level)).join("\n").trim()
|
|
59
|
-
out.push(body || `${name}: (empty section)`)
|
|
60
|
-
}
|
|
61
|
-
return out.join("\n\n")
|
|
62
|
-
}
|
|
63
|
-
|
|
64
15
|
/**
|
|
65
16
|
* 审计块(spawn 级固定 ⇒ system 固块——§6.26 分类裁定表「审计块」行):纯函数——touched 快照
|
|
66
|
-
* (`ctx.agent._touchedFiles`——机制面证据,非 eng-coder 自述)+
|
|
67
|
-
* (`
|
|
68
|
-
* 报告格式五段模板**逐字**。零副作用(只读 ctx
|
|
69
|
-
* @param {{ agent?: { _touchedFiles?: string[],
|
|
17
|
+
* (`ctx.agent._touchedFiles`——机制面证据,非 eng-coder 自述)+ 批档路径行与 §2 指向句
|
|
18
|
+
* (`ctx.agent._batchDoc`——任务书由审计子代理自读 §2)+ 指令 / 范围 / 零 git 权威 / 预算 /
|
|
19
|
+
* 报告格式五段模板**逐字**。零副作用(只读 ctx)。
|
|
20
|
+
* @param {{ agent?: { _touchedFiles?: string[], _batchDoc?: string } }} ctx
|
|
70
21
|
* @returns {string} 块文本(不含与其它块之间的分隔符——分隔由装配侧 `join("\n\n")` 施加)
|
|
71
22
|
*/
|
|
72
23
|
export function buildAuditBlock(ctx) {
|
|
73
24
|
const touched = (ctx.agent._touchedFiles ?? []).map((f) => `- ${f}`).join("\n") || "- (none yet)"
|
|
25
|
+
const batchDoc = typeof ctx.agent?._batchDoc === "string" ? ctx.agent._batchDoc : "(unavailable)"
|
|
74
26
|
return `[Audit scope — mechanical context, independent of the eng-coder's self-report:]\n` +
|
|
75
27
|
// §6.7.6 D-TS4 A1:审计指令模板(四类偏差 + 范围限制 + 校验清单格式)——审计语义
|
|
76
28
|
// 不再靠模型自悟;范围限制是 §6.7.4 D-AG3 声明(下方 Zero-git scope authority)
|
|
@@ -85,9 +37,11 @@ export function buildAuditBlock(ctx) {
|
|
|
85
37
|
`workspace changes not listed there are unrelated to this delivery and are NOT grounds for an out-of-list finding.\n` +
|
|
86
38
|
`Scope discipline (F-TS6 A1): read ONLY the audited files and the design-doc sections relevant to this delivery — do NOT re-read whole documents.\n` +
|
|
87
39
|
`Every deviation item MUST be fieldized: file:line + design reference (doc path + section/AC id) + severity + evidence (quoted code or doc text).\n` +
|
|
88
|
-
// §6.
|
|
89
|
-
|
|
90
|
-
|
|
40
|
+
// §6.22 KD-SC6:任务书 = 批次档 §2(审计子代理自读)——审计块注入批档路径 + §2 指向句
|
|
41
|
+
// (A2 拼装摘要面已删:工程角色 _engTaskInput = 一行机械文本,摘要无来源)。审计子代理本身是
|
|
42
|
+
// explore(任务书 = 父侧五项)——故指向句限定为「**交付**的任务书 = §2」,不冒称本 child 的唯一通道。
|
|
43
|
+
`Batch record (batchDoc): ${batchDoc}\n` +
|
|
44
|
+
`Delivery task book: §2 of the batch record at ${batchDoc} — read it before judging the delivery; it is the criterion source for this audit.\n` +
|
|
91
45
|
`Files actually touched by the eng-coder (mechanical union — audit these against the file list):\n${touched}\n` +
|
|
92
46
|
// §18.5 D-AG3(2026-09-04):审计零 git 范围权威声明——本审计任务零 git(不注入
|
|
93
47
|
// git 上下文——§6.7.4 全角色零 git);_touchedFiles 为审计范围;工作区未列于
|
|
@@ -95,7 +49,7 @@ export function buildAuditBlock(ctx) {
|
|
|
95
49
|
"Zero-git scope authority: this audit task receives NO git context — nothing is injected. " +
|
|
96
50
|
"The evidence base is the design documents, the current disk state (read/glob/grep), and the _touchedFiles list above. " +
|
|
97
51
|
"Workspace changes NOT listed in _touchedFiles are unrelated to this delivery — they are NOT grounds for an out-of-file-list finding." +
|
|
98
|
-
// §6.7.6 D-A1.2:审计预算句——A1 指令模板 +
|
|
52
|
+
// §6.7.6 D-A1.2:审计预算句——A1 指令模板 + 批档指向块之后、A3 报告模板之前(定序——评审 #7)。
|
|
99
53
|
// 逐字设计锚(D-A1.2 代码块):只读该读的——10 轮机械预算——超时报 PROBLEM 下结论。
|
|
100
54
|
// 前导 \n 与 A3 同款块分隔约定(上一句 Zero-git 句末无换行——不触碰既有句)。
|
|
101
55
|
`\n[Audit budget — mechanical]: read ONLY the touched files listed above and the design-doc sections the parent task book names (affected-files table, acceptance criteria, status line). Do NOT read whole documents. Budget = 10 tool rounds max — if you cannot conclude within it, report PROBLEM (inconclusive) rather than continuing to explore.\n` +
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* `REFUSED`(K ≥ 1)∥ `ERROR` ⇒ 调用面 throw(fail-closed);命中 ⊆ 他批在途集 ⇒ 不阻收口 + 固定行逐档列出。
|
|
15
15
|
*
|
|
16
16
|
* 判据零变:本档不新增判据——只决定「判谁」与「何时强制」;判据与裁定串由产品引擎
|
|
17
|
-
* `doc-check/index.mjs`
|
|
17
|
+
* `doc-check/index.mjs` 产出(判据源唯一)。
|
|
18
18
|
*
|
|
19
|
-
* 两条边界(设计与实测同口径):①
|
|
20
|
-
*
|
|
19
|
+
* 两条边界(设计与实测同口径):① **批档自身不入范围**(载体不是判据对象——判它只会把载体的超段记成本批欠账);
|
|
20
|
+
* ② **本轮提交面不在工具面**——`<批起 sha>` 只有父侧掌握,
|
|
21
21
|
* ② 的写入集只认触达账 + 回执明示「基 sha 未定」,提交面由父侧协议清单兜(不静默)。
|
|
22
22
|
*/
|
|
23
23
|
import { execFileSync } from "node:child_process"
|
|
@@ -25,7 +25,6 @@ import { relative, resolve } from "node:path"
|
|
|
25
25
|
|
|
26
26
|
import { owningProject, readManifest } from "../manifest.mjs"
|
|
27
27
|
import { runDocCheck } from "../doc-check/index.mjs"
|
|
28
|
-
import { firstVerdictLine, gateEngineError, staleEngineFiles } from "../doc-check/version.mjs"
|
|
29
28
|
import { batchProjectRoot } from "./batch-paths.mjs"
|
|
30
29
|
|
|
31
30
|
/** 平台路径键(win32 大小写不敏感——只作集比对,非判据面)。 */
|
|
@@ -61,7 +60,7 @@ function gitSurface(root) {
|
|
|
61
60
|
* (报面单源不重写);行数与命中数不等(引擎报面行序变动)⇒ `aligned: false` + 自造行,由调用面回执明示。 */
|
|
62
61
|
function hitEntries(res, root) {
|
|
63
62
|
const lines = res.report.split("\n").filter((l) => l.startsWith("✗ "))
|
|
64
|
-
const hits = [...res.anchorHits, ...res.
|
|
63
|
+
const hits = [...res.anchorHits, ...res.paragraphHits]
|
|
65
64
|
const aligned = lines.length === hits.length
|
|
66
65
|
const entries = hits.map((h, i) => ({
|
|
67
66
|
file: h.file,
|
|
@@ -93,20 +92,20 @@ const emptyRange = (basis) => ({ count: 0, k: 0, foreign: 0, basis })
|
|
|
93
92
|
const errorState = (first, range = emptyRange("none")) => ({ state: "error", first, report: first, hitLines: [], noteLines: [], range })
|
|
94
93
|
|
|
95
94
|
/**
|
|
96
|
-
*
|
|
97
|
-
* @param {{abs:string, cwd?:string, agent?:object, scope?:'writer'|'close'
|
|
98
|
-
* `abs` = 目标批次档绝对路径(根解析用);`agent._touchedFiles` =
|
|
95
|
+
* 跑一次门(**非抛错**:两因之外的门自身异常收为 `error` 态,由调用面按 `scope` 消费)。
|
|
96
|
+
* @param {{abs:string, cwd?:string, agent?:object, scope?:'writer'|'close'}} o
|
|
97
|
+
* `abs` = 目标批次档绝对路径(根解析用);`agent._touchedFiles` = 触达记账。
|
|
99
98
|
* @returns {{state:'pass'|'refused'|'error'|'not-applicable', first:string|null, report:string,
|
|
100
99
|
* hitLines:string[], noteLines:string[], range:{count:number, k:number, foreign:number, basis:string}}}
|
|
101
100
|
* `first` = 唯一机判串(首行 `DOC-CHECK GATE:`);`not-applicable` ⇒ `first = null` + 固定回执行。
|
|
102
101
|
*/
|
|
103
|
-
export function runBatchGate({ abs, cwd, agent = {}, scope = "writer"
|
|
102
|
+
export function runBatchGate({ abs, cwd, agent = {}, scope = "writer" } = {}) {
|
|
104
103
|
// `scope` 两取值、无第三值(设计「`scope` 语义」)——未知值不静默降级(降级即 ② 变 fail-open)
|
|
105
104
|
if (scope !== "writer" && scope !== "close") {
|
|
106
105
|
return errorState(`DOC-CHECK GATE: ERROR(未知 scope:${JSON.stringify(scope)}——取值只有 "writer" ∥ "close")`)
|
|
107
106
|
}
|
|
108
107
|
try {
|
|
109
|
-
return evaluate({ abs, cwd, agent, scope
|
|
108
|
+
return evaluate({ abs, cwd, agent, scope })
|
|
110
109
|
} catch (e) {
|
|
111
110
|
const first = `DOC-CHECK GATE: ERROR(门自身异常:${e?.message ?? String(e)})`
|
|
112
111
|
return errorState(first)
|
|
@@ -114,7 +113,7 @@ export function runBatchGate({ abs, cwd, agent = {}, scope = "writer", clock } =
|
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
/** 门体(正常路径——异常由上抛给 `runBatchGate` 收口)。 */
|
|
117
|
-
function evaluate({ abs, cwd, agent, scope
|
|
116
|
+
function evaluate({ abs, cwd, agent, scope }) {
|
|
118
117
|
const base = resolve(cwd ?? process.cwd())
|
|
119
118
|
const root = owningProject(resolve(abs)) ?? batchProjectRoot(base)
|
|
120
119
|
const close = scope === "close"
|
|
@@ -123,19 +122,12 @@ function evaluate({ abs, cwd, agent, scope, clock }) {
|
|
|
123
122
|
try { read = readManifest(root) } catch (e) {
|
|
124
123
|
return errorState(`DOC-CHECK GATE: ERROR(manifest 不可读 —— ${e?.message ?? String(e)})`)
|
|
125
124
|
}
|
|
126
|
-
// 适用面(声明面驱动):无 manifest ⇒ 不适用 +
|
|
125
|
+
// 适用面(声明面驱动):无 manifest ⇒ 不适用 + 回执明示;**不是豁免清单**——判据参数 = 引擎内置常量。
|
|
127
126
|
if (!read.ok && read.reason === "missing") {
|
|
128
127
|
return { state: "not-applicable", first: null, report: "[doc-check gate] 未跑(本项目无声明面)", hitLines: [], noteLines: [], range: emptyRange("none") }
|
|
129
128
|
}
|
|
130
|
-
// 引擎版本前置(fail-closed):stale ⇒ 直接 ERROR 串,不进引擎(不产 PASS)。
|
|
131
|
-
const version = staleEngineFiles(clock ?? {})
|
|
132
|
-
if (version.stale.length) {
|
|
133
|
-
const report = gateEngineError(version)
|
|
134
|
-
return { state: "error", first: firstVerdictLine(report), report, hitLines: [], noteLines: [], range: emptyRange("none") }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
129
|
// 范围(判谁):writer = 写者触达账;close = 本批笔域集(触达账)∪ git 面。
|
|
138
|
-
//
|
|
130
|
+
// 批档自身不入范围(载体不是判据对象——判它只会把载体的超段记成本批欠账)。
|
|
139
131
|
const selfKey = pathKey(resolve(abs))
|
|
140
132
|
const touched = docPaths(agent?._touchedFiles, base).filter((p) => pathKey(p) !== selfKey)
|
|
141
133
|
const git = close ? gitSurface(root) : { ok: false, base: root, files: [] }
|
|
@@ -143,7 +135,6 @@ function evaluate({ abs, cwd, agent, scope, clock }) {
|
|
|
143
135
|
const docs = close ? [...new Set([...touched, ...gitDocs])] : touched
|
|
144
136
|
const basis = close ? (git.ok ? "touched+git" : "touched(git 面不可得)") : "touched"
|
|
145
137
|
const res = runDocCheck(root, { mode: "gate", docs, docsSource: close ? "explicit" : "session" })
|
|
146
|
-
if (res.error) return { state: "error", first: firstVerdictLine(res.report), report: res.report, hitLines: [], noteLines: [], range: { count: res.checkFiles.length, k: 0, foreign: 0, basis } }
|
|
147
138
|
|
|
148
139
|
const { aligned, entries } = hitEntries(res, root)
|
|
149
140
|
const ownKeys = new Set(touched.map(pathKey))
|
|
@@ -21,13 +21,16 @@ import { getAsyncPool } from "./async-settle.mjs"
|
|
|
21
21
|
* 段数 / 累计轮次二元文本(§4 留痕口径——两值同读一次):
|
|
22
22
|
* `_continueSegments`(链内段数,首段 1)· `_turnSeq`(链内累计轮次,跨段不重置)。
|
|
23
23
|
* 两值均挂子 agent 本体。
|
|
24
|
-
* #417
|
|
25
|
-
* **只报数**(零阈值 /
|
|
26
|
-
*
|
|
24
|
+
* #417 追加第三元「本段零落盘轮数」,#934 收正后带分母(`_zeroWriteTurns` of `_segmentTurns`):
|
|
25
|
+
* **只报数**(零阈值 / 零自动动作——父侧据此判断「在推进」还是「在原地读」)。分子分母任一缺字段
|
|
26
|
+
* 或非整数的执行面(端侧自有回合环、未跟踪)不渲染该元——缺省不冒充 `0`(「未跟踪」不得读作「零消耗」)。
|
|
27
27
|
*/
|
|
28
28
|
function traceText(child) {
|
|
29
29
|
const base = `segments ${child?._continueSegments ?? 1} · accumulated ${child?._turnSeq ?? 0} turns`
|
|
30
|
-
|
|
30
|
+
const zw = child?._zeroWriteTurns, seg = child?._segmentTurns
|
|
31
|
+
return Number.isInteger(zw) && Number.isInteger(seg)
|
|
32
|
+
? `${base} · ${zw} of this segment's ${seg} rounds produced no file write`
|
|
33
|
+
: base
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/** 留痕片段(§3.1 池条目留痕行):挂池条目摘要**文本**面(status / observe)——桥字段零新增。 */
|
|
@@ -45,7 +48,7 @@ function replyHint(parent, carrier, key) {
|
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* 登记检查点并挂起(异步族 askContinue 的唯一实现——逃逸 ①②③):
|
|
48
|
-
* 登记 `carrier._turnCapRec` + 父侧 ask(载荷 = 段数 / 累计轮次 + 去向一句话)——
|
|
51
|
+
* 登记 `carrier._turnCapRec` + 父侧 ask(载荷 = 段数 / 累计轮次 / 本段零落笔读数 + 职责句 + 去向一句话)——
|
|
49
52
|
* ask 入队即唤醒父侧挂起驱动(`pushChildUpstream`)。
|
|
50
53
|
* @param {object} child 撞帽的子 agent(`_upstream` 携父与来源标签)。
|
|
51
54
|
* @param {object} carrier 兑现载体:池条目(子代 / 飞刀)或会诊 session。
|
|
@@ -83,7 +86,9 @@ export function registerTurnCapCheckpoint(child, carrier, { turn = 0, signal } =
|
|
|
83
86
|
from: child._upstream.label,
|
|
84
87
|
kind: "ask",
|
|
85
88
|
message: `[turn cap] ${traceText(child)} — this child hit its per-segment turn cap and is suspended `
|
|
86
|
-
+ `(history kept, no re-dispatch).
|
|
89
|
+
+ `(history kept, no re-dispatch). `
|
|
90
|
+
+ `Check for a non-writing loop before you grant or stop: inspect (subagent action:'observe'), then steer or cancel. `
|
|
91
|
+
+ replyHint(parent, carrier, key),
|
|
87
92
|
})
|
|
88
93
|
return promise
|
|
89
94
|
}
|
package/agent-tools/context.mjs
CHANGED
|
@@ -92,7 +92,7 @@ function statsReport(agent) {
|
|
|
92
92
|
return [
|
|
93
93
|
`Context ≈${usage.total} tokens (${pct}% of ${usage.window} window); compaction line ${usage.threshold} (${toGo} to the line)`,
|
|
94
94
|
// §6.16.4 字面单源(该行为唯一字面,逐字;预算面同源 requestBudget——不自算第二套):
|
|
95
|
-
// window {window} ({
|
|
95
|
+
// window {window} ({modelContext|provider-override|list|spec|default}) · max_tokens {maxTokens|not sent} ({explicit|spec|absent})
|
|
96
96
|
// · prompt {total} · to compaction line {threshold − total} · to window limit {limit − total}
|
|
97
97
|
// 两距离带符号(负 = 已越);maxTokens 未发写 not sent、来源写 absent(该档守卫不覆盖——§6.17 射程)。
|
|
98
98
|
(() => {
|
|
@@ -97,8 +97,15 @@ const TRUNCATED_ECHO_MARK = "注:回显被截断(uuid 之后缺失或不符
|
|
|
97
97
|
|
|
98
98
|
/** §5.2 verdict gate (#940): the echo alone is NOT the verdict — the receipt must
|
|
99
99
|
* carry a `VERDICT: pass` line and no `VERDICT: changes-required` line (fail-closed). */
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// 裁定行接受本地化等价形(GitHub #32 / #14):评审席用中文回执很常见,
|
|
101
|
+
// 只认英文会让 pass 落成 no-pass-line、凭证永不签发;判据仍只认整行逐字面,
|
|
102
|
+
// 不做「句中含『通过』」这类模糊匹配——那样会把「未通过」也放过。
|
|
103
|
+
const _V_PRE = "^[ \\t]*[*_`]*[ \\t]*"
|
|
104
|
+
const _V_SUF = "[ \\t]*[*_`]*[ \\t]*$"
|
|
105
|
+
const VERDICT_PASS_LINE = new RegExp(
|
|
106
|
+
_V_PRE + "(?:VERDICT:[ \\t]*(?:pass|通过|通过。)|结论[::][ \\t]*(?:通过|pass))" + _V_SUF, "mi")
|
|
107
|
+
const VERDICT_CHANGES_LINE = new RegExp(
|
|
108
|
+
_V_PRE + "(?:VERDICT:[ \\t]*(?:changes-required|不通过|需修改)|结论[::][ \\t]*(?:不通过|需修改|changes-required))" + _V_SUF, "mi")
|
|
102
109
|
export function verdictGateFailure(rawResult) {
|
|
103
110
|
if (typeof rawResult !== "string") return "no-pass-line"
|
|
104
111
|
if (VERDICT_CHANGES_LINE.test(rawResult)) return "changes-required"
|
|
@@ -58,6 +58,8 @@ export function digestBudgetOver(agent, size) {
|
|
|
58
58
|
/** Test seam: 落盘目录重定向(单元测试沙箱——生产从不调用)。 */
|
|
59
59
|
let _digestOffloadDirOverride = null
|
|
60
60
|
export function _setDigestOffloadDirForTest(dir) { _digestOffloadDirOverride = dir }
|
|
61
|
+
/** 缝的读取面(无参):生产恒 undefined ⇒ 消费方取各自默认目录。一条缝覆盖本档与 `offloadToolResult` 两族(TIMEOUT-DIGEST.md §2.2)。 */
|
|
62
|
+
export function _digestOffloadDirForTest() { return _digestOffloadDirOverride ?? undefined }
|
|
61
63
|
|
|
62
64
|
/** 超限条目全量落盘(返回清单行文本;失败 null——调用方回退 inline——结果零丢失)。
|
|
63
65
|
* tag 入文件名(溯源:写入族 + 条目 id)。 */
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-tools/doc-check.mjs — `doc_check` 工具:文档纪律机检(产品内建 · 两种问法)。
|
|
3
3
|
*
|
|
4
|
-
* 设计 = `docs/core/design/DOC-DISCIPLINE.md` §3.20 条目 S(判据 = 锚 ∥
|
|
5
|
-
* 工具形:**无动作枚举**——`mode` = 同一条判据的两种问法(`report` =
|
|
6
|
-
*
|
|
4
|
+
* 设计 = `docs/core/design/DOC-DISCIPLINE.md` §3.20 条目 S(判据 = 锚 ∥ 段判据 · 参数内置常量)。
|
|
5
|
+
* 工具形:**无动作枚举**——`mode` = 同一条判据的两种问法(`report` = 报告态 ∥ `gate` = 交付时点的裁定);
|
|
6
|
+
* **只读**(零副作用 · 免审批 · 可并行);参数 `target` / `mode` / `docs`
|
|
7
7
|
* (`target` 指向多项目工作区 ⇒ 报明候选并要求指名,同 `manifest` 工具面)。
|
|
8
|
-
*
|
|
8
|
+
* 判据参数 = 引擎内置常量 `DOC_CHECK_BUILTINS`——不读 manifest、不硬编码本仓路径、不扫目录。
|
|
9
|
+
* 判定范围 = 调用方传入的档:`report` 面 `docs` 必填(不给 ⇒ 明确报错,零遍历);
|
|
10
|
+
* `gate` 面缺省 = 本会话触达记账里的 `.md`(交付面按轮次收窄的唯一来源)。
|
|
9
11
|
* `report` 非门禁(硬):输出不得被读作拒执行的理由(用户令 > 机检——§2 总则族);
|
|
10
12
|
* `gate` 的强制面住交付协议 + 批次档 §6 前置,工具本体同样不阻断任何动作。
|
|
11
|
-
* gate 前置(fail-closed)= 引擎版本一致:本会话进程持启动时加载的 ESM 引擎,引擎档晚于启动 ⇒ ERROR
|
|
12
|
-
* (判据单源 = `doc-check/version.mjs`——与 `batch-gate.mjs` 共用一份清单,不各持一份)。
|
|
13
13
|
* 描述外置:模型可见文本单点 = `tool-docs/doc_check.md`(`DESC` 单一解析面)。
|
|
14
14
|
* 装配段 = `agent/family-tools.mjs` 三处(depth-0 / eng-coder / eng-designer)——写文档 / 设计档的角色面。
|
|
15
15
|
*/
|
|
16
16
|
import { resolve } from "node:path"
|
|
17
17
|
import { projectRootView } from "../manifest.mjs"
|
|
18
18
|
import { runDocCheck } from "../doc-check/index.mjs"
|
|
19
|
-
import { staleEngineFiles, gateEngineError, withEngineNote } from "../doc-check/version.mjs"
|
|
20
19
|
import { DESC } from "../tools/shared.mjs"
|
|
21
20
|
|
|
22
|
-
// 版本函数单源 = `doc-check/version.mjs`(两消费面:本档 gate 分支 ∥ `batch-gate.mjs` 两机械点)——
|
|
23
|
-
// 本档只转口,导出面不变(原五档清单已随之六档:`version.mjs` 自身入清单)。
|
|
24
|
-
export { staleEngineFiles, gateEngineError }
|
|
25
|
-
|
|
26
21
|
export const docCheckTool = {
|
|
27
22
|
name: "doc_check",
|
|
28
23
|
description: DESC("doc_check"),
|
|
@@ -30,8 +25,8 @@ export const docCheckTool = {
|
|
|
30
25
|
type: "object",
|
|
31
26
|
properties: {
|
|
32
27
|
target: { type: "string", description: "Explicit project root to check — defaults to the session anchor; relative paths resolve against it. A workspace target (several projects below it) is reported with the full candidate list — pass the project root you mean." },
|
|
33
|
-
mode: { type: "string", enum: ["report", "gate"], description: "How to ask the same two-criteria check. \"report\" (default): the
|
|
34
|
-
docs: { type: "array", items: { type: "string" }, description: "
|
|
28
|
+
mode: { type: "string", enum: ["report", "gate"], description: "How to ask the same two-criteria check. \"report\" (default): a non-gating report over the documents you list in docs. \"gate\": the delivery gate — the documents this round touched (docs, or this session's written documents when docs is omitted), whole-file judged, first line a machine-readable verdict (DOC-CHECK GATE: PASS / REFUSED / ERROR)." },
|
|
29
|
+
docs: { type: "array", items: { type: "string" }, description: "The documents to judge, as paths relative to the project root or absolute — each is judged whole; nothing else is read except the targets those documents point at. Required in \"report\" mode (there is no default range: omitting it is refused, not widened). Omitted in \"gate\" mode ⇒ the Markdown documents this session wrote. A path that is not on disk or is not a Markdown document is an error (no document judged) in report mode and a visible skipped line in gate mode." },
|
|
35
30
|
},
|
|
36
31
|
},
|
|
37
32
|
readonly: true,
|
|
@@ -39,13 +34,13 @@ export const docCheckTool = {
|
|
|
39
34
|
const anchor = ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
|
|
40
35
|
const target = args?.target ? resolve(anchor, String(args.target)) : resolve(anchor)
|
|
41
36
|
const mode = args?.mode === undefined ? "report" : String(args.mode)
|
|
42
|
-
if (mode !== "report" && mode !== "gate") return `doc_check: unknown mode "${mode}" — use "report" (
|
|
43
|
-
const explicit = Array.isArray(args?.docs) ? args.docs
|
|
37
|
+
if (mode !== "report" && mode !== "gate") return `doc_check: unknown mode "${mode}" — use "report" (a report over the documents you list) or "gate" (this round's documents).`
|
|
38
|
+
const explicit = Array.isArray(args?.docs) ? args.docs : null
|
|
44
39
|
if (args?.docs !== undefined && !Array.isArray(args.docs)) {
|
|
45
40
|
return `doc_check: \`docs\` must be an array of paths (got ${typeof args.docs}) — a bare string is refused rather than read as a whole-domain request; pass ["<path>"].`
|
|
46
41
|
}
|
|
47
|
-
if (
|
|
48
|
-
return "doc_check: `docs`
|
|
42
|
+
if (explicit && explicit.some((d) => typeof d !== "string")) {
|
|
43
|
+
return "doc_check: every `docs` entry must be a path string — a non-string entry is refused rather than coerced (no document was judged)."
|
|
49
44
|
}
|
|
50
45
|
const view = projectRootView(target)
|
|
51
46
|
if (view.state === "workspace") {
|
|
@@ -61,15 +56,14 @@ export const docCheckTool = {
|
|
|
61
56
|
}
|
|
62
57
|
const root = view.state === "ok" ? view.root : target
|
|
63
58
|
if (mode === "gate") {
|
|
64
|
-
// 引擎版本校验先于引擎(A-DD30 ①):stale 非空 ⇒ 直接 ERROR 串,不进引擎
|
|
65
|
-
const version = staleEngineFiles()
|
|
66
|
-
if (version.stale.length) return gateEngineError(version)
|
|
67
|
-
// 会话候选先取 `.md`(触达记账含代码档——代码档不是文档,不该进范围行);域求交归引擎单源
|
|
68
59
|
const docs = explicit ?? (Array.isArray(ctx?.agent?._touchedFiles) ? ctx.agent._touchedFiles : [])
|
|
69
60
|
.filter((p) => typeof p === "string" && p.toLowerCase().endsWith(".md"))
|
|
70
61
|
return runDocCheck(root, { mode: "gate", docs, docsSource: explicit ? "explicit" : "session" }).report
|
|
71
62
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
63
|
+
// 报告态:清单是唯一范围来源——不给就是拒绝(不是「全域」)。
|
|
64
|
+
if (explicit === null) {
|
|
65
|
+
return "doc_check: `docs` is required in report mode — pass the explicit list of documents to judge (e.g. docs: [\"docs/design/X.md\"]). There is no default range: nothing is scanned unless you name it."
|
|
66
|
+
}
|
|
67
|
+
return runDocCheck(root, { docs: explicit, docsSource: "explicit" }).report
|
|
74
68
|
},
|
|
75
69
|
}
|
package/agent-tools/manifest.mjs
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
* - `write` ⇒ **点改**:读现档 → 设节点 → 落盘(`writeManifest` 内先 `validateManifest`——
|
|
12
12
|
* 编辑结果非法 ⇒ 拒 + 盘零变);**缺档 ⇒ 拒** + 引导 `init`(写不建档)。
|
|
13
13
|
*
|
|
14
|
-
* 写面白名单(判据单源 = `MANIFEST_SCHEMA
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* (传整个对象)⇒ 拒**;白名单外 ⇒ 拒(拼写保护——未知键写盘 = 静默死键,KD-M1-36 问题本体)。
|
|
14
|
+
* 写面白名单(判据单源 = `MANIFEST_SCHEMA`——钉死谓词):**12 个可写节点** = `keys` 中**非** `nestedKeys`
|
|
15
|
+
* 父键者(`version` / `phase` / `codePaths` 整键)+ `nestedKeys` 子键(`docRoot.*` 五子键 / `index.*` 四子键);
|
|
16
|
+
* **两族整键写(传整个对象)⇒ 拒**;白名单外 ⇒ 拒(拼写保护——未知键写盘 = 静默死键,KD-M1-36 问题本体)。
|
|
18
17
|
* `value` 解析 = 同款口径 `settings.mjs` 的 `parseValue`(先 `JSON.parse`,失败取字面串;`"null"` ⇒ `null`
|
|
19
18
|
* ——`docRoot.*` 的「本面无」,KD-M1-37)。
|
|
20
19
|
*
|
|
@@ -31,6 +30,11 @@
|
|
|
31
30
|
* 缓存失效(#1367 面⑦):写档成功(init ∥ write——全部成功路径)⇒ `clearDeclarationCache()`——
|
|
32
31
|
* 「补档即回归」进程内即时(声明 ∥ 范围缓存同清);失败路径不清(盘零变 ⇒ 缓存照旧);
|
|
33
32
|
* 手工改档 ⇒ 重启生效(零文件侦听)。
|
|
33
|
+
*
|
|
34
|
+
* 只读变体(`manifestReadTool`——2026-10-22 · 台账 #1738):子代理面同名 `manifest`,`action` 枚举只剩
|
|
35
|
+
* `read`(schema 级不可达 = 权限面第一道)+ execute 入口机械拒绝(第二道,providers 不强制 schema enum);
|
|
36
|
+
* `readAction` 的 `canInit=false` 让缺档 / 无项目两态的引导行不指 `init`。全量工具 `manifestTool` 零改
|
|
37
|
+
* (`readonly: false` = 动作级分类面,只读性归 `dispatch-gates` 的 `read` 判定)。
|
|
34
38
|
*/
|
|
35
39
|
import { existsSync } from "node:fs"
|
|
36
40
|
import { join, resolve } from "node:path"
|
|
@@ -51,8 +55,8 @@ function resolveTarget(args, ctx) {
|
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/** 写面白名单谓词(**钉死**——设计 §2.10):点分路径恰两形——
|
|
54
|
-
* ① 单段:`keys` ∩ ∉ `nestedKeys`(= `version` / `phase` / `
|
|
55
|
-
* ② 双段:`nestedKeys[父]`
|
|
58
|
+
* ① 单段:`keys` ∩ ∉ `nestedKeys`(= `version` / `phase` / `codePaths` 整键);
|
|
59
|
+
* ② 双段:`nestedKeys[父]` 含子键(两族子键);其余(两族整键写 / 未知键 / 更深路径)⇒ 拒。
|
|
56
60
|
* `Array.isArray` 守卫:原型链名(`__proto__` / `constructor` / `toString` 类)取到非数组 ⇒ 落拒面
|
|
57
61
|
* (不得 TypeError 直出——白名单外一律 `refused` 统一前缀)。 */
|
|
58
62
|
function isWritableKey(key) {
|
|
@@ -90,21 +94,26 @@ function workspaceReason(target, candidates) {
|
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
/** read 动作(五态报明——**不抛**):`state=` 稳定锚 + 各态附行(ok 细节面 / missing 引导 /
|
|
93
|
-
* workspace 下辖项目全列 / invalid 错误行)。
|
|
94
|
-
|
|
97
|
+
* workspace 下辖项目全列 / invalid 错误行)。
|
|
98
|
+
* `canInit`(缺省 `true` ⇒ depth-0 输出逐字零变):`false` 时 `missing` / `no-project` 的引导行不指
|
|
99
|
+
* `init`——子代理面无该动作,指向不可达动作会把模型送进必然失败的调用。 */
|
|
100
|
+
function readAction(target, { canInit = true } = {}) {
|
|
95
101
|
const view = projectView(target)
|
|
96
102
|
const lines = [`state=${view.state}`]
|
|
97
103
|
if (view.path) lines.push(`path=${view.path}`)
|
|
98
104
|
if (view.state === "ok") {
|
|
99
|
-
const m = readManifest(view.root) // 细节面:missingKeys /
|
|
105
|
+
const m = readManifest(view.root) // 细节面:missingKeys / errors(非空才出)
|
|
100
106
|
lines.push(`manifest=${JSON.stringify(m.manifest)}`)
|
|
101
107
|
if (m.missingKeys?.length) lines.push(`missingKeys=${m.missingKeys.join(", ")}`)
|
|
102
|
-
if (m.unknownKeys?.length) lines.push(`unknownKeys=${m.unknownKeys.join(", ")}`)
|
|
103
108
|
if (m.errors?.length) lines.push(`errors=${m.errors.join(";")}`)
|
|
104
109
|
} else if (view.state === "missing") {
|
|
105
|
-
lines.push(
|
|
110
|
+
lines.push(canInit
|
|
111
|
+
? "hint: run manifest action=init to create the default manifest here"
|
|
112
|
+
: "hint: no manifest here — report it; creating one is a main-agent action")
|
|
106
113
|
} else if (view.state === "no-project") {
|
|
107
|
-
lines.push(
|
|
114
|
+
lines.push(canInit
|
|
115
|
+
? "hint: no project at this directory and none in the direct children — manifest action=init can create one at the target"
|
|
116
|
+
: "hint: no project at this directory and none in the direct children — report it; creating one is a main-agent action")
|
|
108
117
|
} else if (view.state === "workspace") {
|
|
109
118
|
lines.push(`projects (${(view.candidates ?? []).length}):`)
|
|
110
119
|
for (const c of view.candidates ?? []) lines.push(`- ${c}`)
|
|
@@ -123,7 +132,7 @@ export const manifestTool = {
|
|
|
123
132
|
properties: {
|
|
124
133
|
action: { type: "string", enum: ["read", "init", "write"], description: "read — current manifest + validation state (five-state anchor, never throws); init — create the default manifest when missing (refused when one already exists); write — point-edit one node (key + value) of an existing manifest" },
|
|
125
134
|
target: { type: "string", description: "Explicit target directory — defaults to the session anchor; relative paths resolve against it. A workspace target (several projects below it) is refused with the full project list — pass the project root you mean." },
|
|
126
|
-
key: { type: "string", description: "(write) Dot path of the node to set — version / phase /
|
|
135
|
+
key: { type: "string", description: "(write) Dot path of the node to set — version / phase / codePaths, or a sub-key of docRoot / index (e.g. docRoot.design / index.publicRepos); whole-object writes of those two families are refused", },
|
|
127
136
|
value: { type: "string", description: "(write) New value — parsed as JSON first (null / true / 5 / [\"a\"]), otherwise kept as a literal string; \"null\" is the explicit no-such-face for docRoot.<key>" },
|
|
128
137
|
},
|
|
129
138
|
required: ["action"],
|
|
@@ -169,7 +178,7 @@ export const manifestTool = {
|
|
|
169
178
|
if (!key) throw new Error("manifest write: key is required (dot path, e.g. docRoot.design)")
|
|
170
179
|
if (args?.value === undefined) throw new Error("manifest write: value is required (JSON-parsed first, otherwise a literal string)")
|
|
171
180
|
if (!isWritableKey(key)) {
|
|
172
|
-
throw refuse("write", `key "${key}" is not writable — writable nodes: version / phase /
|
|
181
|
+
throw refuse("write", `key "${key}" is not writable — writable nodes: version / phase / codePaths + sub-keys of docRoot / index (whole-object writes of the two families are refused)`)
|
|
173
182
|
}
|
|
174
183
|
let current
|
|
175
184
|
try {
|
|
@@ -198,3 +207,25 @@ export const manifestTool = {
|
|
|
198
207
|
return `manifest write: set ${key} = ${JSON.stringify(value)}\npath=${file}`
|
|
199
208
|
},
|
|
200
209
|
}
|
|
210
|
+
|
|
211
|
+
/** 子代理只读变体(同名 `manifest`——action 枚举只剩 `read`):init / write schema 级不可达(权限面第一道);
|
|
212
|
+
* execute 入口再做一次机械拒绝(providers 不强制 schema enum——第二道)。 */
|
|
213
|
+
export const manifestReadTool = {
|
|
214
|
+
name: "manifest",
|
|
215
|
+
description: DESC("manifest-read"),
|
|
216
|
+
parameters: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
action: { type: "string", enum: ["read"], description: "read — current manifest + validation state (five-state anchor, never throws)" },
|
|
220
|
+
target: { type: "string", description: "Explicit target directory — defaults to the session anchor; relative paths resolve against it. A workspace target (several projects below it) is reported with the full project list — pass the project root you mean." },
|
|
221
|
+
},
|
|
222
|
+
required: ["action"],
|
|
223
|
+
},
|
|
224
|
+
readonly: true,
|
|
225
|
+
async execute(args, ctx) {
|
|
226
|
+
if (args?.action !== "read") {
|
|
227
|
+
throw new Error(`manifest read: refused — this read-only variant exposes only action=read (got ${JSON.stringify(args?.action)})`)
|
|
228
|
+
}
|
|
229
|
+
return readAction(resolveTarget(args, ctx), { canInit: false })
|
|
230
|
+
},
|
|
231
|
+
}
|