@thincoder/core 0.10.9 → 0.10.10
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/advisor/messages.mjs +4 -5
- package/advisor/project-context.mjs +69 -44
- package/agent/family-tools.mjs +9 -7
- package/agent/helpers.mjs +6 -0
- package/agent/run-start.mjs +1 -0
- package/agent/turn-loop.mjs +17 -13
- package/agent-tools/batch-gate.mjs +4 -5
- package/agent-tools/checkpoint.mjs +11 -6
- package/agent-tools/doc-check.mjs +18 -13
- package/agent-tools/manifest.mjs +44 -12
- package/agent-tools/subagent-spawn.mjs +34 -19
- package/agent-tools/subagent.mjs +1 -1
- package/agent-tools.mjs +1 -1
- package/declaration.mjs +5 -12
- package/doc-check/anchors.mjs +51 -59
- package/doc-check/index.mjs +96 -134
- package/doc-check/width.mjs +9 -54
- package/i18n.mjs +5 -0
- package/manifest-schema.mjs +7 -35
- package/manifest.mjs +4 -4
- package/memory/code-search.mjs +5 -14
- package/memory/docs.mjs +5 -14
- package/memory/read-gate.mjs +26 -0
- package/package.json +1 -1
- package/prompts/advisor-design.md +89 -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 +212 -57
- package/prompts/consult-base.md +10 -3
- package/prompts/discipline-engineering.md +180 -53
- package/prompts/discipline-normal.md +91 -40
- package/prompts/persona-coder.md +3 -1
- package/prompts/persona-eng-coder.md +29 -9
- package/prompts/persona-eng-designer.md +87 -33
- package/prompts/persona-engineering.md +322 -89
- package/prompts/persona-explore.md +1 -1
- package/prompts/persona-normal.md +16 -8
- package/tool-docs/doc_check.md +27 -15
- package/tool-docs/manifest-read.md +8 -0
- package/tool-docs/manifest.md +9 -8
- package/tool-docs/subagent.md +2 -2
package/advisor/messages.mjs
CHANGED
|
@@ -155,15 +155,14 @@ function buildAdvisorUserMessageInner(agent, prior, reviewType, designToken = nu
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
// Engineering mode: the project
|
|
159
|
-
//
|
|
158
|
+
// Engineering mode: the project standards section — the declaration face is gone with the
|
|
159
|
+
// manifest `advisor` key, so this is always an explicit degradation (PO-2).
|
|
160
160
|
if (agent.config?.agent?.engineering) {
|
|
161
161
|
injectProjectStandards(agent, parts, guideRoot)
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// Document map:
|
|
165
|
-
//
|
|
166
|
-
// (never the old silent skip).
|
|
164
|
+
// Document map: derived from the manifest `docRoot` (declared roots' README, then their common
|
|
165
|
+
// ancestor); neither → explicit degradation sentence (never a silent skip).
|
|
167
166
|
injectDocumentMap(agent, parts, guideRoot ?? agent.cwd)
|
|
168
167
|
|
|
169
168
|
parts.push("## Instructions")
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* absent — in a project with a different layout the reviewer still scored the
|
|
13
13
|
* dimension it could no longer check (silent failure, the worst class: invisible
|
|
14
14
|
* and unfixable by the user). Missing pieces now say so, and the reviewer is told
|
|
15
|
-
* to state the limitation in its findings.
|
|
16
|
-
*
|
|
15
|
+
* to state the limitation in its findings. The document map is derived from the
|
|
16
|
+
* manifest's `docRoot` (the one place that says where the docs live); the standards
|
|
17
|
+
* face has no derivable convention name and is therefore an explicit degradation.
|
|
17
18
|
*/
|
|
18
19
|
import { readFileSync, existsSync } from "node:fs"
|
|
19
20
|
import { resolve, join, relative, dirname, sep } from "node:path"
|
|
20
21
|
import { providerSpec } from "../config.mjs"
|
|
21
|
-
import {
|
|
22
|
+
import { DEFAULT_MANIFEST, docRootPaths, readManifest } from "../manifest.mjs"
|
|
22
23
|
|
|
23
24
|
/** Project guide (AGENTS.md) injection budget — decision 2026-08-08:
|
|
24
25
|
* NO fixed truncation; long-context models (1M+) get up to 5% of their context
|
|
@@ -32,8 +33,8 @@ const PROJECT_GUIDE_FRACTION = 0.05 // 5% of the reviewer model's context window
|
|
|
32
33
|
export const NO_GUIDE_NOTICE = "(No AGENTS.md found — neither at the working directory root nor in any review-scope subdirectory. Judge the user's requirements from the conversation background, and say so explicitly if the requirements are unclear.)"
|
|
33
34
|
|
|
34
35
|
/** Explicit degradation sentences (PORTABILITY design §4.4 — verbatim). */
|
|
35
|
-
export const NO_DOC_MAP_NOTICE = "(No document map found under the
|
|
36
|
-
export const NO_STANDARDS_NOTICE = "(No project standards document
|
|
36
|
+
export const NO_DOC_MAP_NOTICE = "(No document map found — no README.md under the declared document roots and none at their common ancestor — the Document ownership criterion is degraded: check placement against the Project Guide where present, and state the limitation in your findings.)"
|
|
37
|
+
export const NO_STANDARDS_NOTICE = "(No project standards document — nothing in the project declares one — judge methodology compliance from the Project Guide (when present) and the review criteria above; state the limitation in your findings.)"
|
|
37
38
|
export const NO_GIT_NOTICE = "(No git repository detected — change-set context is unavailable; read the review-scope files directly.)"
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -126,25 +127,66 @@ export function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
|
126
127
|
return root // guide injected — requirement-fit criteria apply (truthy root)
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/** `docRoot` probe order: the face a reviewer needs first (design) → down to the batch records. */
|
|
131
|
+
const DOC_ROOT_ORDER = ["design", "requirements", "specs", "modules", "batches"]
|
|
132
|
+
|
|
133
|
+
/** Declared document roots as ABSOLUTE paths, in declaration order (`null` / missing face ⇒ no root). */
|
|
134
|
+
function declaredDocRoots(base) {
|
|
135
|
+
const read = readManifest(base)
|
|
136
|
+
const docRoot = (read.ok && read.manifest ? read.manifest : DEFAULT_MANIFEST).docRoot ?? {}
|
|
137
|
+
const out = []
|
|
138
|
+
for (const key of DOC_ROOT_ORDER) {
|
|
139
|
+
for (const p of docRootPaths(docRoot[key], base)) if (!out.includes(p)) out.push(p)
|
|
140
|
+
}
|
|
141
|
+
return out
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Common ancestor of every root (segment-wise — `docs/design` and `docs/designX` are not siblings).
|
|
145
|
+
* A lone drive root is no map home ⇒ null. */
|
|
146
|
+
function commonAncestor(paths) {
|
|
147
|
+
if (paths.length === 0) return null
|
|
148
|
+
const split = (p) => p.split(/[\\/]+/)
|
|
149
|
+
let acc = split(paths[0])
|
|
150
|
+
for (const p of paths.slice(1)) {
|
|
151
|
+
const segs = split(p)
|
|
152
|
+
let i = 0
|
|
153
|
+
while (i < acc.length && i < segs.length && acc[i] === segs[i]) i++
|
|
154
|
+
acc = acc.slice(0, i)
|
|
155
|
+
}
|
|
156
|
+
if (acc.length === 0) return null
|
|
157
|
+
const joined = acc.join(sep)
|
|
158
|
+
return /^[A-Za-z]:$/.test(joined) ? null : joined
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
/**
|
|
130
|
-
*
|
|
131
|
-
* (
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
162
|
+
* Discover the document map from the manifest's `docRoot` — a pure fs probe, no index generation
|
|
163
|
+
* (the map is a hand-maintained register; a generated one would be a second index, and a second
|
|
164
|
+
* index is exactly the duplicate pointer the retired declaration face used to create).
|
|
165
|
+
* Ladder: ① `README.md` under each declared doc root (declaration order) ② `README.md` at the common
|
|
166
|
+
* ancestor of every declared root. Neither ⇒ null (caller injects `NO_DOC_MAP_NOTICE` — no silent skip).
|
|
167
|
+
* @param {string} base project root (or any dir inside it — `docRoot` resolves against the project root)
|
|
168
|
+
* @returns {string|null} absolute path of the map file, or null when degraded
|
|
169
|
+
*/
|
|
170
|
+
export function findDocumentMap(base) {
|
|
171
|
+
const roots = declaredDocRoots(base)
|
|
172
|
+
for (const r of roots) {
|
|
173
|
+
const p = join(r, "README.md")
|
|
174
|
+
if (existsSync(p)) return p
|
|
175
|
+
}
|
|
176
|
+
const ancestor = commonAncestor(roots)
|
|
177
|
+
if (!ancestor) return null
|
|
178
|
+
const p = join(ancestor, "README.md")
|
|
179
|
+
return existsSync(p) ? p : null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Inject the project document map (PO-1), derived from `docRoot`. Degrades visibly: the reviewer
|
|
184
|
+
* must know the Document-ownership criterion is unchecked — never a silent skip.
|
|
135
185
|
* @returns {string|null} the injected map path, or null when degraded
|
|
136
186
|
*/
|
|
137
187
|
export function injectDocumentMap(agent, parts, root) {
|
|
138
188
|
const base = root ?? agent.cwd
|
|
139
|
-
const
|
|
140
|
-
let mapPath = null
|
|
141
|
-
if (conv.advisor.docMap) {
|
|
142
|
-
const declared = resolve(base, conv.advisor.docMap)
|
|
143
|
-
if (existsSync(declared)) mapPath = declared
|
|
144
|
-
} else {
|
|
145
|
-
mapPath = [resolve(base, "docs", "README.md"), resolve(base, "docs", "design", "README.md")]
|
|
146
|
-
.find((p) => existsSync(p)) ?? null
|
|
147
|
-
}
|
|
189
|
+
const mapPath = findDocumentMap(base)
|
|
148
190
|
parts.push("## Document Map")
|
|
149
191
|
let text = null
|
|
150
192
|
if (mapPath) {
|
|
@@ -166,32 +208,15 @@ export function injectDocumentMap(agent, parts, root) {
|
|
|
166
208
|
}
|
|
167
209
|
|
|
168
210
|
/**
|
|
169
|
-
* Inject the project standards
|
|
170
|
-
*
|
|
171
|
-
* `METHODOLOGY.md` probe
|
|
172
|
-
*
|
|
173
|
-
* @returns {
|
|
211
|
+
* Inject the project standards section (PO-2). The declaration face is gone with the manifest
|
|
212
|
+
* `advisor` key, and no standards file name is derivable from what the project does declare (the old
|
|
213
|
+
* `METHODOLOGY.md` probe was deleted exactly because most projects have none). The section stays so the
|
|
214
|
+
* reviewer is TOLD the dimension is unchecked — visible degradation, not a silent skip.
|
|
215
|
+
* @returns {null} always degraded
|
|
174
216
|
*/
|
|
175
|
-
export function injectProjectStandards(
|
|
176
|
-
const base = root ?? agent.cwd
|
|
177
|
-
const conv = loadProjectDeclaration(agent.cwd)
|
|
178
|
-
const declared = conv.advisor.standardsDoc ? resolve(base, conv.advisor.standardsDoc) : null
|
|
217
|
+
export function injectProjectStandards(_agent, parts, _root) {
|
|
179
218
|
parts.push("## Project Standards")
|
|
180
|
-
|
|
181
|
-
if (declared && existsSync(declared)) {
|
|
182
|
-
try {
|
|
183
|
-
text = readFileSync(declared, "utf8")
|
|
184
|
-
} catch (e) {
|
|
185
|
-
console.warn(`[advisor] project standards unreadable at ${declared}: ${e.message}`)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (text === null) {
|
|
189
|
-
parts.push(NO_STANDARDS_NOTICE) // degraded — visible by contract
|
|
190
|
-
parts.push("")
|
|
191
|
-
return null
|
|
192
|
-
}
|
|
193
|
-
parts.push("The project declares these standards for this review. Judge the design/changes against them:")
|
|
194
|
-
parts.push(text)
|
|
219
|
+
parts.push(NO_STANDARDS_NOTICE) // degraded — visible by contract
|
|
195
220
|
parts.push("")
|
|
196
|
-
return
|
|
221
|
+
return null
|
|
197
222
|
}
|
package/agent/family-tools.mjs
CHANGED
|
@@ -26,7 +26,7 @@ export async function assembleFamilyTools({
|
|
|
26
26
|
decorate = null, // object 端差面:{ subagent?, consultStart?, consultStop?, settings? }
|
|
27
27
|
} = {}) {
|
|
28
28
|
// CORE-UNIFICATION TOOLS #83:consult 家族随统一登记册自 `../agent-tools.mjs` 取用(单一来源)
|
|
29
|
-
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, contextTool, batchTool, manifestTool, consultStartTool, consultStopTool, parentChannelTool, docCheckTool } = await import("../agent-tools.mjs")
|
|
29
|
+
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, contextTool, batchTool, manifestTool, manifestReadTool, consultStartTool, consultStopTool, parentChannelTool, docCheckTool } = await import("../agent-tools.mjs")
|
|
30
30
|
// 台账统一入口(2026-10-05 统一入口批——台账 #923):全量变体 = depth-0 段 ∥ 只读变体 = 全部
|
|
31
31
|
// depth>0 段。动态 import——ledger 链静态达 node:sqlite(W8 契约②);两变体同名单对象、schema 窄化。
|
|
32
32
|
const { ledgerTool, ledgerReadTool } = await import("../ledger.mjs")
|
|
@@ -149,7 +149,7 @@ export async function assembleFamilyTools({
|
|
|
149
149
|
// 团队台账全量变体(五 action——同律:子代理段只挂读变体;登录态门在工具内)
|
|
150
150
|
teamLedgerTool,
|
|
151
151
|
// manifest 工具(`MANIFEST.md` §2.10 · 2026-10-08 · 台账 #1098):主 agent 读写出口——
|
|
152
|
-
//
|
|
152
|
+
// **两模式同挂**(需求未设模式门);全量变体仅 depth-0——子代理各段挂只读变体(见下 `:187-193`)。
|
|
153
153
|
manifestTool,
|
|
154
154
|
// 文档纪律机检(`DOC-DISCIPLINE.md` §3.20 条目 S):只读报告态——depth-0 主 agent 段;
|
|
155
155
|
// 写文档 / 设计档的角色段(eng-coder / eng-designer)另挂(见下两分支)。
|
|
@@ -182,13 +182,15 @@ export async function assembleFamilyTools({
|
|
|
182
182
|
// consult 段不入(其角色语义 = 父发起的一次性会诊,父在其 settle 前不期望中途对话)。
|
|
183
183
|
// 台账只读变体(query / count)——全部 depth>0 角色段(含 consult ∕ 兜底;写三 schema 级不可达);
|
|
184
184
|
// 团队台账只读变体(team-ledger 批)同律。
|
|
185
|
-
|
|
185
|
+
// manifest 只读变体(MANIFEST.md §2.10.1 · 2026-10-22 · 台账 #1738):同名 `manifest`、action 枚举只剩 `read`;
|
|
186
|
+
// 与上两条只读先例同律挂在全部 depth>0 角色段(全量变体仅 depth-0——两变体互斥装配,同名不塌缩)。
|
|
187
|
+
: engChildRole === "eng-coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool, advisorTool, verifyTool, batchTool(batchDoc), docCheckTool,
|
|
186
188
|
...(engChildSubagent ? [engChildSubagent] : [])]
|
|
187
|
-
: engChildRole === "eng-designer" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, batchTool(batchDoc), docCheckTool,
|
|
189
|
+
: engChildRole === "eng-designer" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool, batchTool(batchDoc), docCheckTool,
|
|
188
190
|
...(engChildSubagent ? [engChildSubagent] : [])]
|
|
189
|
-
: role === "coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, verifyTool, advisorTool, ...(batchDoc ? [batchTool(batchDoc)] : [])]
|
|
190
|
-
: role === "consult" ? [recentChangesTool, ledgerReadTool, teamLedgerReadTool]
|
|
191
|
-
: [parentChannelTool, ledgerReadTool, teamLedgerReadTool]
|
|
191
|
+
: role === "coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool, verifyTool, advisorTool, ...(batchDoc ? [batchTool(batchDoc)] : [])]
|
|
192
|
+
: role === "consult" ? [recentChangesTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool]
|
|
193
|
+
: [parentChannelTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool]
|
|
192
194
|
|
|
193
195
|
// 固定段(装配序 = agent.tools → 固定段 → 家族段 → extraTools):timer 全模式全深度;
|
|
194
196
|
// plan 随模式位——ENG-PLAN-EXCLUSION(FR31 ① · KD8 卸载而非「注册 + 报错」· KD11 全深度两端);
|
package/agent/helpers.mjs
CHANGED
|
@@ -275,6 +275,12 @@ export function turnFrame(seq, turn, maxTurns) {
|
|
|
275
275
|
return { turn: seq, maxTurns: seq - turn - 1 + maxTurns }
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
/** #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32.2 ① 判据式):上轮结账至今写序是否推进;非整数(未跟踪面 / NaN)⇒ true 记「有落笔」不判(不用 `?? 0` 兜底——未跟踪面当零落笔正是本收正要消除的误报方向)。 */
|
|
279
|
+
export function wroteSince(agent, mark) {
|
|
280
|
+
const seq = agent._mutationSeq
|
|
281
|
+
return Number.isInteger(seq) ? seq !== mark : true
|
|
282
|
+
}
|
|
283
|
+
|
|
278
284
|
/** Repair malformed conversation history: remove orphan tool messages and fill missing tool results */
|
|
279
285
|
export function repairHistory(history) {
|
|
280
286
|
const out = []
|
package/agent/run-start.mjs
CHANGED
|
@@ -105,6 +105,7 @@ export async function beginRun(agent, input, callbacks, {
|
|
|
105
105
|
// #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值常量 / 零自动动作):段起点复位(首段 /
|
|
106
106
|
// 续跑新段——与 `_continueSegments` 同点);段内累加,采集点在回合环(与 `_turnSeq` 同源面)。
|
|
107
107
|
agent._zeroWriteTurns = 0
|
|
108
|
+
agent._segmentTurns = 0 // #934 B 面读数分母(§6.32.4):同点复位——同段同源(分子 ≤ 分母)
|
|
108
109
|
if (!resume) {
|
|
109
110
|
// 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):链内累计编号
|
|
110
111
|
// 只在链起点复位——续跑(resume:true)不重置、不回退(编号帧公式见 helpers.mjs
|
package/agent/turn-loop.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 TURN-CAP-CONTINUE.md §4)
|
|
19
19
|
ContinueError,
|
|
20
20
|
FILE_MUTATORS, toolTouchPaths,
|
|
21
|
+
wroteSince, // #934(§6.32.2 ① 判据式):写序是否推进——轮顶 ∥ 撞帽收尾两处判据点共用
|
|
21
22
|
} from "./helpers.mjs"
|
|
22
23
|
|
|
23
24
|
export async function runTurnLoop(agent, {
|
|
@@ -57,9 +58,10 @@ export async function runTurnLoop(agent, {
|
|
|
57
58
|
// §6.31.5 落痕写点门(#939):载体吸收经 `carrierField` 单点(§6.31.4 同式);动态 import = 零新增静态边(沿上方两条同款)。
|
|
58
59
|
const { carrierField } = await import("../agent-tools/async-settle.mjs")
|
|
59
60
|
|
|
60
|
-
// #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 /
|
|
61
|
-
//
|
|
62
|
-
|
|
61
|
+
// #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 / 零自动动作)∥ #934(零落笔看门狗):计数基线——
|
|
62
|
+
// 判据物 = 写序(每次写事件 +1,不去重);`_touchedFiles` 是去重路径集,同档重写长度不变 ⇒ 旧判据在
|
|
63
|
+
// 此读成零落笔(本收正要消除的误报方向)。环内逐轮结账、撞帽收尾轮在环外单结(同一谓词)。
|
|
64
|
+
agent._mutSeqMark = agent._mutationSeq
|
|
63
65
|
|
|
64
66
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
65
67
|
// #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环头检查点——中止恒以
|
|
@@ -73,21 +75,23 @@ export async function runTurnLoop(agent, {
|
|
|
73
75
|
const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
|
|
74
76
|
agent._currentTurn = frame.turn
|
|
75
77
|
agent._maxTurns = frame.maxTurns
|
|
76
|
-
// #
|
|
77
|
-
|
|
78
|
-
// #
|
|
79
|
-
//
|
|
78
|
+
// #934 B 面读数分母(§6.32.4):本段轮数逐轮 +1——段起点复位见 run-start.mjs(与零落笔计数同点)。
|
|
79
|
+
agent._segmentTurns = (agent._segmentTurns ?? 0) + 1
|
|
80
|
+
// #417(撞帽载荷「本段零落盘轮数」——采集点与 `_turnSeq` 同源面):上一轮至今写序未推进
|
|
81
|
+
// ⇒ 上一轮零落笔,计数 +1;写序标每轮前移(撞帽收尾轮在环外结账——见下方 ContinueError 前)。
|
|
82
|
+
// #934(零落笔看门狗——§6.32.2 ① 单点采集):streak 与越阈判定同点——上轮零落笔 ⇒ +1 并判越阈;
|
|
83
|
+
// 有落笔 ⇒ streak 归零 + 清闩(写后重臂——每连续零落笔段恰一条)。#417 计数与 streak 同源同谓词。
|
|
80
84
|
if (turn > 0) {
|
|
81
|
-
if (agent
|
|
85
|
+
if (!wroteSince(agent, agent._mutSeqMark)) {
|
|
82
86
|
agent._zeroWriteTurns += 1
|
|
83
87
|
agent._zeroWriteStreak += 1
|
|
84
|
-
maybeZeroWriteAlert(agent) // #934
|
|
88
|
+
maybeZeroWriteAlert(agent) // #934:连续零落笔越阈 ⇒ 自动上行提醒(只推——§6.32)
|
|
85
89
|
} else {
|
|
86
90
|
agent._zeroWriteStreak = 0
|
|
87
91
|
agent._zeroWriteAlerted = false // 写后重臂
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
|
-
agent.
|
|
94
|
+
agent._mutSeqMark = agent._mutationSeq
|
|
91
95
|
// D2 (AGENT-LOOP-SUBAGENT.md §6.7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
|
|
92
96
|
// single emit point covering all three spawn tools; phase=llm (tool/done progress rides
|
|
93
97
|
// the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
|
|
@@ -271,9 +275,9 @@ export async function runTurnLoop(agent, {
|
|
|
271
275
|
injectPostTurn(agent, results, recentCallSigs, callbacks, turn, { suspDriven })
|
|
272
276
|
}
|
|
273
277
|
|
|
274
|
-
// #417
|
|
275
|
-
//
|
|
276
|
-
if (maxTurns > 0 && agent
|
|
278
|
+
// #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)单结——与环内同一谓词(不另作 alert ∥ streak
|
|
279
|
+
// 判定,链尾无下一轮可推报);轮顶计数只覆盖到倒数第二轮(maxTurns ≤ 0 时环未跑,不虚计)。
|
|
280
|
+
if (maxTurns > 0 && !wroteSince(agent, agent._mutSeqMark)) agent._zeroWriteTurns += 1
|
|
277
281
|
// #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环尾检查点——中止恒以
|
|
278
282
|
// AbortError 收束(不落 ContinueError——用户 Stop 优先于继续提示;消费契约 = 外层按 reason 判定)。
|
|
279
283
|
if (signal?.aborted) throw abortError(signal, "agent", "turn-tail")
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* 判据零变:本档不新增判据——只决定「判谁」与「何时强制」;判据与裁定串由产品引擎
|
|
17
17
|
* `doc-check/index.mjs` 产出(判据源唯一)。引擎版本前置同源 = `doc-check/version.mjs`。
|
|
18
18
|
*
|
|
19
|
-
* 两条边界(设计与实测同口径):①
|
|
20
|
-
*
|
|
19
|
+
* 两条边界(设计与实测同口径):① **批档自身不入范围**(载体不是判据对象——判它只会把载体的行宽记成本批欠账);
|
|
20
|
+
* ② **本轮提交面不在工具面**——`<批起 sha>` 只有父侧掌握,
|
|
21
21
|
* ② 的写入集只认触达账 + 回执明示「基 sha 未定」,提交面由父侧协议清单兜(不静默)。
|
|
22
22
|
*/
|
|
23
23
|
import { execFileSync } from "node:child_process"
|
|
@@ -123,7 +123,7 @@ function evaluate({ abs, cwd, agent, scope, clock }) {
|
|
|
123
123
|
try { read = readManifest(root) } catch (e) {
|
|
124
124
|
return errorState(`DOC-CHECK GATE: ERROR(manifest 不可读 —— ${e?.message ?? String(e)})`)
|
|
125
125
|
}
|
|
126
|
-
// 适用面(声明面驱动):无 manifest ⇒ 不适用 +
|
|
126
|
+
// 适用面(声明面驱动):无 manifest ⇒ 不适用 + 回执明示;**不是豁免清单**——判据参数 = 引擎内置常量。
|
|
127
127
|
if (!read.ok && read.reason === "missing") {
|
|
128
128
|
return { state: "not-applicable", first: null, report: "[doc-check gate] 未跑(本项目无声明面)", hitLines: [], noteLines: [], range: emptyRange("none") }
|
|
129
129
|
}
|
|
@@ -135,7 +135,7 @@ function evaluate({ abs, cwd, agent, scope, clock }) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// 范围(判谁):writer = 写者触达账;close = 本批笔域集(触达账)∪ git 面。
|
|
138
|
-
//
|
|
138
|
+
// 批档自身不入范围(载体不是判据对象——判它只会把载体的行宽记成本批欠账)。
|
|
139
139
|
const selfKey = pathKey(resolve(abs))
|
|
140
140
|
const touched = docPaths(agent?._touchedFiles, base).filter((p) => pathKey(p) !== selfKey)
|
|
141
141
|
const git = close ? gitSurface(root) : { ok: false, base: root, files: [] }
|
|
@@ -143,7 +143,6 @@ function evaluate({ abs, cwd, agent, scope, clock }) {
|
|
|
143
143
|
const docs = close ? [...new Set([...touched, ...gitDocs])] : touched
|
|
144
144
|
const basis = close ? (git.ok ? "touched+git" : "touched(git 面不可得)") : "touched"
|
|
145
145
|
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
146
|
|
|
148
147
|
const { aligned, entries } = hitEntries(res, root)
|
|
149
148
|
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
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
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
13
|
* gate 前置(fail-closed)= 引擎版本一致:本会话进程持启动时加载的 ESM 引擎,引擎档晚于启动 ⇒ ERROR
|
|
@@ -30,8 +32,8 @@ export const docCheckTool = {
|
|
|
30
32
|
type: "object",
|
|
31
33
|
properties: {
|
|
32
34
|
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: "
|
|
35
|
+
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)." },
|
|
36
|
+
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
37
|
},
|
|
36
38
|
},
|
|
37
39
|
readonly: true,
|
|
@@ -39,13 +41,13 @@ export const docCheckTool = {
|
|
|
39
41
|
const anchor = ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
|
|
40
42
|
const target = args?.target ? resolve(anchor, String(args.target)) : resolve(anchor)
|
|
41
43
|
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
|
|
44
|
+
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).`
|
|
45
|
+
const explicit = Array.isArray(args?.docs) ? args.docs : null
|
|
44
46
|
if (args?.docs !== undefined && !Array.isArray(args.docs)) {
|
|
45
47
|
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
48
|
}
|
|
47
|
-
if (
|
|
48
|
-
return "doc_check: `docs`
|
|
49
|
+
if (explicit && explicit.some((d) => typeof d !== "string")) {
|
|
50
|
+
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
51
|
}
|
|
50
52
|
const view = projectRootView(target)
|
|
51
53
|
if (view.state === "workspace") {
|
|
@@ -64,12 +66,15 @@ export const docCheckTool = {
|
|
|
64
66
|
// 引擎版本校验先于引擎(A-DD30 ①):stale 非空 ⇒ 直接 ERROR 串,不进引擎
|
|
65
67
|
const version = staleEngineFiles()
|
|
66
68
|
if (version.stale.length) return gateEngineError(version)
|
|
67
|
-
// 会话候选先取 `.md
|
|
69
|
+
// 会话候选先取 `.md`(触达记账含代码档——代码档不是文档,不该进范围行);可判集归引擎单源
|
|
68
70
|
const docs = explicit ?? (Array.isArray(ctx?.agent?._touchedFiles) ? ctx.agent._touchedFiles : [])
|
|
69
71
|
.filter((p) => typeof p === "string" && p.toLowerCase().endsWith(".md"))
|
|
70
72
|
return runDocCheck(root, { mode: "gate", docs, docsSource: explicit ? "explicit" : "session" }).report
|
|
71
73
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
+
// 报告态:清单是唯一范围来源——不给就是拒绝(不是「全域」)。引擎陈旧 ⇒ 首行前一行注记(A-DD30 ①)。
|
|
75
|
+
if (explicit === null) {
|
|
76
|
+
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."
|
|
77
|
+
}
|
|
78
|
+
return withEngineNote(runDocCheck(root, { docs: explicit, docsSource: "explicit" }).report, staleEngineFiles())
|
|
74
79
|
},
|
|
75
80
|
}
|
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,8 +94,10 @@ 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}`)
|
|
@@ -102,9 +108,13 @@ function readAction(target) {
|
|
|
102
108
|
if (m.unknownKeys?.length) lines.push(`unknownKeys=${m.unknownKeys.join(", ")}`)
|
|
103
109
|
if (m.errors?.length) lines.push(`errors=${m.errors.join(";")}`)
|
|
104
110
|
} else if (view.state === "missing") {
|
|
105
|
-
lines.push(
|
|
111
|
+
lines.push(canInit
|
|
112
|
+
? "hint: run manifest action=init to create the default manifest here"
|
|
113
|
+
: "hint: no manifest here — report it; creating one is a main-agent action")
|
|
106
114
|
} else if (view.state === "no-project") {
|
|
107
|
-
lines.push(
|
|
115
|
+
lines.push(canInit
|
|
116
|
+
? "hint: no project on the ancestor chain and none below — manifest action=init can create one at the target"
|
|
117
|
+
: "hint: no project on the ancestor chain and none below — report it; creating one is a main-agent action")
|
|
108
118
|
} else if (view.state === "workspace") {
|
|
109
119
|
lines.push(`projects (${(view.candidates ?? []).length}):`)
|
|
110
120
|
for (const c of view.candidates ?? []) lines.push(`- ${c}`)
|
|
@@ -123,7 +133,7 @@ export const manifestTool = {
|
|
|
123
133
|
properties: {
|
|
124
134
|
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
135
|
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 /
|
|
136
|
+
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
137
|
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
138
|
},
|
|
129
139
|
required: ["action"],
|
|
@@ -169,7 +179,7 @@ export const manifestTool = {
|
|
|
169
179
|
if (!key) throw new Error("manifest write: key is required (dot path, e.g. docRoot.design)")
|
|
170
180
|
if (args?.value === undefined) throw new Error("manifest write: value is required (JSON-parsed first, otherwise a literal string)")
|
|
171
181
|
if (!isWritableKey(key)) {
|
|
172
|
-
throw refuse("write", `key "${key}" is not writable — writable nodes: version / phase /
|
|
182
|
+
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
183
|
}
|
|
174
184
|
let current
|
|
175
185
|
try {
|
|
@@ -198,3 +208,25 @@ export const manifestTool = {
|
|
|
198
208
|
return `manifest write: set ${key} = ${JSON.stringify(value)}\npath=${file}`
|
|
199
209
|
},
|
|
200
210
|
}
|
|
211
|
+
|
|
212
|
+
/** 子代理只读变体(同名 `manifest`——action 枚举只剩 `read`):init / write schema 级不可达(权限面第一道);
|
|
213
|
+
* execute 入口再做一次机械拒绝(providers 不强制 schema enum——第二道)。 */
|
|
214
|
+
export const manifestReadTool = {
|
|
215
|
+
name: "manifest",
|
|
216
|
+
description: DESC("manifest-read"),
|
|
217
|
+
parameters: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: {
|
|
220
|
+
action: { type: "string", enum: ["read"], description: "read — current manifest + validation state (five-state anchor, never throws)" },
|
|
221
|
+
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." },
|
|
222
|
+
},
|
|
223
|
+
required: ["action"],
|
|
224
|
+
},
|
|
225
|
+
readonly: true,
|
|
226
|
+
async execute(args, ctx) {
|
|
227
|
+
if (args?.action !== "read") {
|
|
228
|
+
throw new Error(`manifest read: refused — this read-only variant exposes only action=read (got ${JSON.stringify(args?.action)})`)
|
|
229
|
+
}
|
|
230
|
+
return readAction(resolveTarget(args, ctx), { canInit: false })
|
|
231
|
+
},
|
|
232
|
+
}
|