@thincoder/core 0.9.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/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-facts.mjs — design-review 事实面(纯事实:路径归一 + 评审范围键)。
|
|
3
|
+
*
|
|
4
|
+
* 本档前身 = 第 33 批(2026-09-11)的失败计数护栏档;其计数载体 / 停止谓词 / 两级检查点随
|
|
5
|
+
* 「撤除全部会话级轮次计数器」**整体退场**(2026-09-18 用户裁定;设计权威 =
|
|
6
|
+
* `docs/core/design/ADVISOR-CONVERGENCE.md` §3.1/§3.4 + `ADVISOR-GUARDS.md` §7)。
|
|
7
|
+
* 结算分类迁出为纯函数(输出判据名、零状态)= `thincoder-core/advisor/notice.mjs`。
|
|
8
|
+
*
|
|
9
|
+
* 本档 = 中立模块(**不 import 任何 src/ 模块**——打断潜在环):
|
|
10
|
+
* - `normAbs` 自 advisor-settle.mjs 迁入(原处 re-export——既有 import 面零变;陈旧判定 /
|
|
11
|
+
* 冻结拦截 / 评审范围键同源);
|
|
12
|
+
* - `docSetKey` 自 advisor-async.mjs 迁入(原为私有——零 import 面)。
|
|
13
|
+
*/
|
|
14
|
+
import { join } from "node:path"
|
|
15
|
+
|
|
16
|
+
/** ABS 归一(cwd 相对 → cwd 拼接)——陈旧判定 / 冻结拦截 / 评审范围键同源(§14.14 E-4)。 */
|
|
17
|
+
export function normAbs(p, cwd) {
|
|
18
|
+
const s = String(p)
|
|
19
|
+
return /^[a-zA-Z]:[\\/]/.test(s) || s.startsWith("/") || s.startsWith("\\\\") ? s : join(cwd, s)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Canonical scope key for design reviews — the document multi-set
|
|
23
|
+
* (order-insensitive, ABS-path normalized — launch 与 continuation 的写法差异
|
|
24
|
+
* ("./docs/x.md" vs "docs/x.md"、反斜杠) 不误建新实例).
|
|
25
|
+
* 第 33 批自 advisor-async.mjs 逐字迁入(实例续跑与事实面同锚单源)。 */
|
|
26
|
+
export function docSetKey(documents, cwd) {
|
|
27
|
+
const list = [...new Set((documents ?? [])
|
|
28
|
+
.filter((d) => typeof d === "string" && d.trim())
|
|
29
|
+
.map((d) => normAbs(d, cwd)))]
|
|
30
|
+
return JSON.stringify(list.sort())
|
|
31
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings tool — agent-side configuration adjustment (SETTINGS-TOOL.md, 2026-09-05).
|
|
3
|
+
* 用户裁定:全量 config.json 任意键(点分路径)+ 单工具多动作 list/get/set + 双端同批。
|
|
4
|
+
* 语义:set = 写盘(config.json——磁盘真相最小化:只写被设的键——默认值不固化)+
|
|
5
|
+
* 热应用(ctx.agent.config 内存对象立即更新——回合边界键下回合生效)。
|
|
6
|
+
* 护栏:敏感键(路径段命中词表 apiKey/key/token/secret/password/authorization/auth/cookie/credential,
|
|
7
|
+
* 或段名 headers/env 整族)回显/错误文本永不出现明文
|
|
8
|
+
* (••••(masked)——防密钥泄漏进会话历史/trace);已知键类型校验——非 null 叶子类型表
|
|
9
|
+
* 自动派生自 config.mjs DEFAULTS(不手写防漂移——F-S1.5/N-S1.5);null 默认值键与同族键
|
|
10
|
+
* 走显式形状表(_NULL_LEAF_SHAPES 相等面 / _SIBLING_SHAPES 存在性面——按各自真实消费形态
|
|
11
|
+
* 校验,完备性机械锁 + 一次性警告;SETTINGS-TOOL.md §8);set 侧效走审批门(dispatch 动作级分类)。
|
|
12
|
+
*/
|
|
13
|
+
import { DEFAULTS, configPath, writeConfigAtomic } from "../config.mjs"
|
|
14
|
+
|
|
15
|
+
/** 敏感键段判定(完整点分路径的段级匹配——两句取或:词表段 ∨ 开口键族段;SETTINGS-TOOL.md §2.4) */
|
|
16
|
+
const SENSITIVE_SEGMENT = /(^|[._-])(api[_-]?key|key|token|secret|password|authorization|auth|cookie|credential)($|[._-])/i
|
|
17
|
+
/** 开口键族段判定(headers / env——段名命中 ⇒ 其下全部子键整族遮罩:头名 / 变量名不可枚举) */
|
|
18
|
+
const SENSITIVE_FAMILY = /(^|[._-])(headers|env)($|[._-])/i
|
|
19
|
+
const MASKED = "••••(masked)"
|
|
20
|
+
|
|
21
|
+
function isSensitiveKey(path) {
|
|
22
|
+
return SENSITIVE_SEGMENT.test(path) || SENSITIVE_FAMILY.test(path)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ─── 形状层(第 8 批 2026-09-11——SETTINGS-TOOL.md §8.3;两表结构:相等面 / 存在性面) ─── */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* null 叶子形状表——唯一「逐键手写形状」面(完备性锁的**相等面**:键集 == _nullLeafPaths(DEFAULTS))。
|
|
29
|
+
* 表值 = 形状规格:接受集 = 该键真实消费形态 ∪ `null`(显式清除——消费面均有「未设置」态);
|
|
30
|
+
* 不可消费形态(应用侧读不出写入值)→ 拒绝(违例抛错,磁盘/内存零变化)。
|
|
31
|
+
*/
|
|
32
|
+
const _NULL_LEAF_SHAPES = {
|
|
33
|
+
defaultModel: { kind: "nonEmptyString", providerModel: true, expects: 'non-empty string ("provider:model" composite)' },
|
|
34
|
+
"agent.subagentModel": { kind: "nonEmptyString", expects: "non-empty string" },
|
|
35
|
+
shell: { kind: "nonEmptyString", expects: "non-empty string" },
|
|
36
|
+
"memory.team": { kind: "team", expects: "object { repo: string, name?, dir? }" },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 同族/跨端形状表(**存在性断言面**——不参与完备性锁的集合相等)。W3 同族:`agent.subagentModels`
|
|
40
|
+
* 默认 `{}` ⇒ 派生表零条目 ⇒ 零约束(字符串被静默忽略——回落 subagentModel)。 */
|
|
41
|
+
const _SIBLING_SHAPES = {
|
|
42
|
+
"agent.subagentModels": { kind: "roleMap", expects: "object of role→non-empty string" },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 派生:递归遍历默认值对象——叶子(非对象/数组值)记 `路径 → typeof 默认值`(旧语义逐字保留:
|
|
46
|
+
* null 叶子记 "object",其约束由形状表接管);数组不递归(下标元素无类型约束——全量域)。 */
|
|
47
|
+
function _deriveTypes(obj, prefix = "", out = {}) {
|
|
48
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
49
|
+
const p = prefix ? `${prefix}.${k}` : k
|
|
50
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) _deriveTypes(v, p, out)
|
|
51
|
+
else out[p] = Array.isArray(v) ? "array" : typeof v
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 类型表 = 派生 + 可选 `patch` 覆盖(测试缝——非 null 叶子逐键与派生结果相等;T-S2.15)。 */
|
|
57
|
+
function _buildShapeTable(obj, patch = null) {
|
|
58
|
+
const out = _deriveTypes(obj)
|
|
59
|
+
return patch ? Object.assign(out, patch) : out
|
|
60
|
+
}
|
|
61
|
+
const TYPE_MAP = _buildShapeTable(DEFAULTS)
|
|
62
|
+
|
|
63
|
+
/** null 叶子路径枚举(数组不递归——与派生表同域)。 */
|
|
64
|
+
function _nullLeafPaths(obj, prefix = "", out = []) {
|
|
65
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
66
|
+
const p = prefix ? `${prefix}.${k}` : k
|
|
67
|
+
if (v === null) out.push(p)
|
|
68
|
+
else if (typeof v === "object" && !Array.isArray(v)) _nullLeafPaths(v, p, out)
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 完备性锁(N-S1.5):未声明的 null 叶子 = 形状表漂移——返回键名 + 一次性警告列出键名。 */
|
|
74
|
+
let _shapeWarned = false
|
|
75
|
+
function _checkShapeCompleteness(defaults, warn = console.warn) {
|
|
76
|
+
const declared = new Set(Object.keys(_NULL_LEAF_SHAPES))
|
|
77
|
+
const missing = _nullLeafPaths(defaults).filter((p) => !declared.has(p))
|
|
78
|
+
if (missing.length && !_shapeWarned) {
|
|
79
|
+
_shapeWarned = true
|
|
80
|
+
warn(`[settings] null-default keys missing from _NULL_LEAF_SHAPES: ${missing.join(", ")} — declare their real consumption shape`)
|
|
81
|
+
}
|
|
82
|
+
return missing
|
|
83
|
+
}
|
|
84
|
+
_checkShapeCompleteness(DEFAULTS) // 装载自检——形状表必须覆盖全部 null 叶子(漂移即警告)
|
|
85
|
+
|
|
86
|
+
/** 值位渲染(错误文本——敏感键命中时遮罩:N-S1.2 / D-S2.4——明文零进错误句)。 */
|
|
87
|
+
function _shownValue(path, value) {
|
|
88
|
+
return isSensitiveKey(path) ? MASKED : JSON.stringify(value)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _kindOf(value) {
|
|
92
|
+
return value === null ? "null" : Array.isArray(value) ? "array" : typeof value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _expectsError(path, expects, value) {
|
|
96
|
+
return new Error(`settings set: "${path}" expects ${expects} — got ${_kindOf(value)} (${_shownValue(path, value)})`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 形状校验(形状表命中键——`null` 在 _checkKnownKeyValue 已放行)。 */
|
|
100
|
+
function _checkShape(path, spec, value) {
|
|
101
|
+
if (spec.kind === "team") {
|
|
102
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw _expectsError(path, spec.expects, value)
|
|
103
|
+
if (typeof value.repo !== "string" || !value.repo.trim()) {
|
|
104
|
+
throw new Error(`settings set: "${path}" requires a non-empty "repo" (team layer stays off without it) — got ${_shownValue(path, value)}`)
|
|
105
|
+
}
|
|
106
|
+
for (const k of ["name", "dir"]) {
|
|
107
|
+
if (value[k] !== undefined && (typeof value[k] !== "string" || !value[k].trim())) throw _expectsError(path, spec.expects, value)
|
|
108
|
+
}
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (spec.kind === "roleMap") {
|
|
112
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw _expectsError(path, spec.expects, value)
|
|
113
|
+
for (const v of Object.values(value)) {
|
|
114
|
+
if (typeof v !== "string" || !v.trim()) throw _expectsError(path, spec.expects, value)
|
|
115
|
+
}
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
// nonEmptyString —— defaultModel 另加 provider:model 形态面(与 model-ref.mjs:25-36 同判据;
|
|
119
|
+
// 存在性面不查——D-S2.5)
|
|
120
|
+
if (typeof value !== "string" || !value.trim()) throw _expectsError(path, spec.expects, value)
|
|
121
|
+
if (spec.providerModel) {
|
|
122
|
+
const sep = value.indexOf(":")
|
|
123
|
+
if (sep <= 0 || !value.slice(sep + 1)) throw _expectsError(path, spec.expects, value)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 已知键值校验(set 写盘前;违例抛错——磁盘/内存零变化):
|
|
129
|
+
* ① 形状表命中键(`_NULL_LEAF_SHAPES` / `_SIBLING_SHAPES` **两表都查**——表值 = 形状规格)→ 形状校验
|
|
130
|
+
* (`null` = 显式清除,放行);② 非 null 叶子(派生表值 = `typeof` 串)→ 既有语义逐字保留
|
|
131
|
+
* (`want === "array"` 跳过 / `null` + `want === "object"` 放行);③ 未知键 → 原样通过(全量域)。
|
|
132
|
+
*/
|
|
133
|
+
function _checkKnownKeyValue(path, value) {
|
|
134
|
+
const spec = _NULL_LEAF_SHAPES[path] ?? _SIBLING_SHAPES[path]
|
|
135
|
+
if (spec) {
|
|
136
|
+
if (value === null) return // 显式清除 = 有效动作(消费面均有「未设置」态——D-S2.3)
|
|
137
|
+
return _checkShape(path, spec, value)
|
|
138
|
+
}
|
|
139
|
+
const want = TYPE_MAP[path]
|
|
140
|
+
if (want && want !== "array") {
|
|
141
|
+
const got = value === null ? "null" : typeof value
|
|
142
|
+
if (got !== want && !(value === null && want === "object")) throw _expectsError(path, want, value)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ─── 既有工具面 helpers(第 8 批零改动) ─── */
|
|
147
|
+
|
|
148
|
+
/** 点分路径解析(含数组数字段)——逐段下钻;返回 { ok, value } 或缺失段信息 */
|
|
149
|
+
function resolvePath(obj, path) {
|
|
150
|
+
const segs = String(path).split(".")
|
|
151
|
+
let cur = obj
|
|
152
|
+
for (let i = 0; i < segs.length; i++) {
|
|
153
|
+
if (cur === null || typeof cur !== "object" || !(segs[i] in cur)) {
|
|
154
|
+
return { ok: false, reached: segs.slice(0, i).join("."), missing: segs[i], depth: i }
|
|
155
|
+
}
|
|
156
|
+
cur = cur[segs[i]]
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, value: cur }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 点分路径写入(自动建中间对象——cmd-config 数值项同款);返回旧值 */
|
|
162
|
+
function setKeyPath(obj, path, value) {
|
|
163
|
+
const segs = String(path).split(".")
|
|
164
|
+
let cur = obj
|
|
165
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
166
|
+
if (cur[segs[i]] === null || typeof cur[segs[i]] !== "object") cur[segs[i]] = {}
|
|
167
|
+
cur = cur[segs[i]]
|
|
168
|
+
}
|
|
169
|
+
const last = segs[segs.length - 1]
|
|
170
|
+
const old = cur[last]
|
|
171
|
+
cur[last] = value
|
|
172
|
+
return old
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 递归展平 config 对象(数组下标段形态)→ 排序的 { path, value } 列表 */
|
|
176
|
+
function flatten(obj, prefix = "", out = []) {
|
|
177
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
178
|
+
const p = prefix ? `${prefix}.${k}` : k
|
|
179
|
+
if (v !== null && typeof v === "object") flatten(v, p, out)
|
|
180
|
+
else out.push({ path: p, value: v })
|
|
181
|
+
}
|
|
182
|
+
return out.sort((a, b) => (a.path < b.path ? -1 : 1))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 行格式化:`key = value (type)`——敏感键值遮罩 */
|
|
186
|
+
function formatLine({ path, value }) {
|
|
187
|
+
const shown = isSensitiveKey(path) ? MASKED : value
|
|
188
|
+
return `${path} = ${shown} (${Array.isArray(value) ? "array" : typeof value})`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseValue(raw) {
|
|
192
|
+
const s = String(raw)
|
|
193
|
+
try {
|
|
194
|
+
const v = JSON.parse(s)
|
|
195
|
+
// JSON 顶层标量都接受;解析失败(裸字符串)→ 字符串字面
|
|
196
|
+
if (typeof v === "number" || typeof v === "boolean" || v === null || Array.isArray(v) || typeof v === "object") return v
|
|
197
|
+
// "abc"(带引号传的字符串)——JSON.parse 成功但返回字符串:返回**解析值**(去引号)
|
|
198
|
+
// ——两端统一 ①(2026-09-11 用户裁定;SETTINGS-TOOL.md §9.3)。
|
|
199
|
+
return v
|
|
200
|
+
} catch {
|
|
201
|
+
return s // 裸 abc → 字符串字面
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function settingsTool(opts = {}) {
|
|
206
|
+
const cfgPath = opts.configPath ?? configPath // 测试注入 tmp 文件;默认全局 configPath
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
name: "settings",
|
|
210
|
+
description:
|
|
211
|
+
"Adjust ThinCoder runtime configuration — persisted to config.json AND hot-applied to the live agent config.\n" +
|
|
212
|
+
"Actions: list (all keys + values, flattened) | get <key> | set <key> <value> — dot paths into config.json (agent.maxTurns, traces.enabled, providers.0.model, any nesting).\n" +
|
|
213
|
+
"set persists to disk (only the set key is written — defaults are never baked in) and takes effect in the running session immediately (turn-boundary keys apply next turn); the value survives restarts.\n" +
|
|
214
|
+
"Known keys are type-checked: scalar keys against the built-in defaults (agent.maxTurns a number, traces.enabled a boolean); keys whose default is null against their real consumption shape — defaultModel \"provider:model\", agent.subagentModel / shell non-empty string, memory.team object with a repo — so a value the app would silently drop is refused (null clears the key). Unknown keys under a known section are stored as given. Values parse as JSON first (true/false/numbers/objects/arrays), else stay strings.\n" +
|
|
215
|
+
"SENSITIVE keys (path segment matching apiKey/key/token/secret/password/authorization/auth/cookie/credential, or any key under a headers/env segment) are NEVER echoed in plaintext — list/get/set replies show ••••(masked); setting a sensitive key is allowed and stored, but never echoed back.\n" +
|
|
216
|
+
"list/get are read-only (planMode ok, no approval); set is a side effect (approval gate). The /config TUI command is the human equivalent.",
|
|
217
|
+
parameters: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: {
|
|
220
|
+
action: { type: "string", enum: ["list", "get", "set"], description: "list — full key/value inventory; get <key> — one value; set <key> <value> — persist + hot-apply" },
|
|
221
|
+
key: { type: "string", description: "Dot path into config.json, e.g. agent.maxTurns / traces.enabled / providers.0.model" },
|
|
222
|
+
value: { type: "string", description: "New value for set — parsed as JSON first (true/5/{...}/[...]), otherwise kept as string" },
|
|
223
|
+
},
|
|
224
|
+
required: ["action"],
|
|
225
|
+
},
|
|
226
|
+
readonly: false, // set 是侧效——dispatch 动作级分类放行 list/get
|
|
227
|
+
async execute(args, ctx) {
|
|
228
|
+
const action = args?.action
|
|
229
|
+
if (!["list", "get", "set"].includes(action)) throw new Error(`settings: action must be one of list/get/set — got ${JSON.stringify(action)}`)
|
|
230
|
+
const config = ctx?.agent?.config ?? null
|
|
231
|
+
if (!config) throw new Error("settings: no live agent config (ctx.agent.config missing)")
|
|
232
|
+
if (action === "list") {
|
|
233
|
+
const rows = flatten(config)
|
|
234
|
+
if (rows.length === 0) return "(空配置——无已设置键)"
|
|
235
|
+
return rows.map(formatLine).join("\n")
|
|
236
|
+
}
|
|
237
|
+
if (action === "get") {
|
|
238
|
+
if (!args.key) throw new Error("settings get: key is required (dot path, e.g. agent.maxTurns)")
|
|
239
|
+
const r = resolvePath(config, args.key)
|
|
240
|
+
if (!r.ok) {
|
|
241
|
+
const hint = r.depth === 0 ? "(config 顶层无此键——顶层可用键见 settings list)" : `(父键 ${r.reached || "顶层"} 存在——无 ${r.missing} 子键)`
|
|
242
|
+
throw new Error(`settings get: no such key "${args.key}" ${hint}`)
|
|
243
|
+
}
|
|
244
|
+
return formatLine({ path: args.key, value: r.value })
|
|
245
|
+
}
|
|
246
|
+
// set
|
|
247
|
+
if (!args.key || args.value === undefined) throw new Error("settings set: key and value are required")
|
|
248
|
+
const value = parseValue(args.value)
|
|
249
|
+
// 已知键校验(形状表两表都查 + 派生类型表——SETTINGS-TOOL.md §8.3;未知键 JSON 原样)
|
|
250
|
+
_checkKnownKeyValue(String(args.key), value)
|
|
251
|
+
// 写盘(D-F5b:磁盘真相最小化——writeConfigAtomic 磁盘新鲜读 + 只改被设键 + mtime
|
|
252
|
+
// 门控——默认不固化;冲突/畸形抛错,内存不热应用(零虚假成功))+ 热应用(内存对象)
|
|
253
|
+
const r = await writeConfigAtomic(cfgPath, (disk) => {
|
|
254
|
+
setKeyPath(disk, args.key, value)
|
|
255
|
+
})
|
|
256
|
+
if (!r.ok) throw new Error("config changed on disk concurrently — retry (settings set not applied)")
|
|
257
|
+
setKeyPath(config, args.key, value)
|
|
258
|
+
const shown = isSensitiveKey(String(args.key)) ? MASKED : value
|
|
259
|
+
return `settings set: ${args.key} = ${shown} (${Array.isArray(value) ? "array" : typeof value})${isSensitiveKey(String(args.key)) ? " — stored(值不回显)" : " — persisted + hot-applied(运行中已生效)"}`
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 测试缝导出(`_` 前缀——config.mjs `_setConfigPathForTest` 先例;SETTINGS-TOOL.md §8.3 第 6 条):
|
|
266
|
+
* `_checkShapeCompleteness` = 完备性锁的运行时面(T-S2.14「一次性警告列出键名」的机械断言缝)。
|
|
267
|
+
*/
|
|
268
|
+
export { _buildShapeTable, _nullLeafPaths, _NULL_LEAF_SHAPES, _SIBLING_SHAPES, _checkKnownKeyValue, _checkShapeCompleteness }
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { loadSkills, readSkill } from "../skills.mjs"
|
|
2
|
+
import { escapeXml } from "../agent.mjs"
|
|
3
|
+
|
|
4
|
+
// ─── skill loader 注入缝(#88——「同步 loader 面按核内结构归一」的端侧覆盖面)──────────
|
|
5
|
+
/**
|
|
6
|
+
* skill loader 注入位(**缺省不覆盖** = 核内异步 loader——CLI 语义,零行为变;端装配层
|
|
7
|
+
* 可覆盖为端形态(如核内同步面 `loadSkillsSync` / `readSkillSync` 的绑定)。核内零端名分支
|
|
8
|
+
* (契约 5——本档只认 `loadSkills` / `readSkill` 两函数名)。
|
|
9
|
+
* 契约:两键均可选;给了就用,缺键回核内默认;同步 / 异步返回值均可(内部 await)。
|
|
10
|
+
*/
|
|
11
|
+
let injectedLoader = null
|
|
12
|
+
export function configureSkillLoader(impl) {
|
|
13
|
+
injectedLoader = impl && typeof impl === "object" ? impl : null
|
|
14
|
+
}
|
|
15
|
+
/** 撤销注入(测试与端装配生命周期用——缺省态 = 核内异步 loader)。 */
|
|
16
|
+
export function resetSkillLoader() { injectedLoader = null }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* skill tool: load project skill files on demand (.thincoder/skills/*.md).
|
|
20
|
+
* After loading, skill content is injected into the conversation wrapped in <skill-loaded> for subsequent reference.
|
|
21
|
+
* Use action="list" to see all available skills.
|
|
22
|
+
*/
|
|
23
|
+
export const skillTool = {
|
|
24
|
+
name: "skill",
|
|
25
|
+
description:
|
|
26
|
+
"Load a project skill from .thincoder/skills/. Skills contain reusable instructions, workflows, or reference material. Use this when the user references a skill by name, or when a task matches a known skill's description. Call with action='list' to see available skills; call with action='load' and name=<skill> to activate one. " +
|
|
27
|
+
"Returns the skill list ('list'), the load confirmation ('load' — instructions arrive in the next message), or Error: ... with the available skills.",
|
|
28
|
+
parameters: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
action: { type: "string", enum: ["list", "load"], description: "'list' to see available skills, 'load' to activate one" },
|
|
32
|
+
name: { type: "string", description: "Skill name (for 'load' action)" },
|
|
33
|
+
},
|
|
34
|
+
required: ["action"],
|
|
35
|
+
},
|
|
36
|
+
readonly: true,
|
|
37
|
+
async execute(args, ctx) {
|
|
38
|
+
const loadSkillsFn = typeof injectedLoader?.loadSkills === "function" ? injectedLoader.loadSkills : loadSkills
|
|
39
|
+
const readSkillFn = typeof injectedLoader?.readSkill === "function" ? injectedLoader.readSkill : readSkill
|
|
40
|
+
const skills = await loadSkillsFn(ctx.agent.cwd)
|
|
41
|
+
if (args.action === "list") {
|
|
42
|
+
if (skills.length === 0) return "No project skills found in .thincoder/skills/."
|
|
43
|
+
return skills.map((s) => `- ${s.name}: ${s.description}`).join("\n")
|
|
44
|
+
}
|
|
45
|
+
if (!args.name) return "Error: skill name required for 'load' action."
|
|
46
|
+
// Dedup: skip reloading if history already contains an <skill-loaded> block with the same name
|
|
47
|
+
// (history is the ledger; if it got compacted away we naturally won't find it here — correct behavior)
|
|
48
|
+
if (ctx.agent.history?.some((m) => typeof m.content === "string" && m.content.includes(`<skill-loaded name="${args.name}"`))) {
|
|
49
|
+
return `Skill "${args.name}" is already loaded in this conversation — follow the instructions in the existing <skill-loaded> block above. Do not reload it.`
|
|
50
|
+
}
|
|
51
|
+
const content = await readSkillFn(ctx.agent.cwd, args.name)
|
|
52
|
+
if (!content) {
|
|
53
|
+
const available = skills.map((s) => s.name).join(", ")
|
|
54
|
+
return `Error: skill "${args.name}" not found. Available: ${available || "(none)"}`
|
|
55
|
+
}
|
|
56
|
+
// Inject skill content into history (will appear as the next user message)
|
|
57
|
+
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
58
|
+
ctx.agent._pendingReminders.push(
|
|
59
|
+
`<skill-loaded name="${args.name}" source=".thincoder/skills/${args.name}.md">\n${escapeXml(content)}\n</skill-loaded>\n\nFollow the skill's instructions above for the current task.`
|
|
60
|
+
)
|
|
61
|
+
return `Skill "${args.name}" loaded. Instructions will appear in the next message.`
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawn-gates.mjs — M5 委派与 spawn 门新门禁统一落点(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.3):
|
|
3
|
+
* 全部新增门禁逻辑集中于此——装配点(buildSpawnChild / normalizeFileList)只加 import 调用。
|
|
4
|
+
* 已实现:F2(round 结构化参数 + 任务书五段文本字段校验)· F6(files 声明面拦截谓词)。
|
|
5
|
+
* 裁撤(2026-09-17 主 agent 裁定——不落代码):
|
|
6
|
+
* - F4 修复轮窄带:全量勘察散文 marker 不可机判(误拒风险不可接受)→ 由 F2 五段字段覆盖;
|
|
7
|
+
* - F5 advisor 角色:advisor 不入 spawn 通道(评审走 advisor 工具)——enum 收正为
|
|
8
|
+
* explore/eng-designer/eng-coder。
|
|
9
|
+
* 零依赖:本模块零 import(叶子——纯谓词层,无环、无反向依赖)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* F2 任务书五段文本字段(六强制字段中 round 之外的五段——ENGINEERING-MODE-V2-MODULE-DELEGATION §1.2 F2)。
|
|
14
|
+
* 判据 = 段标题在场(标题行或行内 marker——summaryEngTaskBook 同款手法:marker 正则命中
|
|
15
|
+
* task ∪ context 文本即可);内容够不够由执行者拒收兜底(需求 §1.14 #9 行为面——不做内容校验)。
|
|
16
|
+
* 双语 marker:中文标题(主 agent 派单形)与英文标题(交付报告格式模板形)同收。
|
|
17
|
+
*/
|
|
18
|
+
const TASK_BOOK_FIELDS = [
|
|
19
|
+
{ label: "目标与理由 (goal & why)", markers: [/目标与理由/, /goal\s*&\s*why/i, /goal\s+and\s+why/i] },
|
|
20
|
+
{ label: "已知事实 (known facts)", markers: [/已知事实/, /known\s+facts/i] },
|
|
21
|
+
{ label: "设计要点与禁止范围 (design points & forbidden scope)", markers: [/设计要点/, /design\s+points/i] },
|
|
22
|
+
{ label: "验收标准 (acceptance criteria)", markers: [/验收标准/, /acceptance\s+criteria/i] },
|
|
23
|
+
{ label: "交付报告格式 (delivery-report format)", markers: [/交付报告格式/, /交付报告/, /delivery[- ]report\s+format/i] },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/** F2 round 合法值(初始轮 / 修正轮——「派单必带」= 无默认值)。
|
|
27
|
+
* 单源:本档为唯一字面量处——schema 面(`agent-tools/subagent.mjs`)与谓词面(本档)同引用(D2)。
|
|
28
|
+
*/
|
|
29
|
+
export const ROUND_VALUES = ["initial", "fix"]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 五段任务书最小合法样例(F2 契约的正面样例——跨仓测试夹具共用单源;段落名 = 父侧
|
|
33
|
+
* 任务书五段模板)。文本字段判据 = 在场性(内容质量由执行者拒收兜底——需求 §1.14 #9)。
|
|
34
|
+
*/
|
|
35
|
+
export const ENG_TASK_BOOK_MIN = [
|
|
36
|
+
"目标与理由:实现批准设计(夹具任务书——五段文本字段齐备)。",
|
|
37
|
+
"已知事实:设计已评审通过、token 已签发、批次档在册。",
|
|
38
|
+
"设计要点与禁止范围:仅落设计档受影响文件清单;禁夹带新语义。",
|
|
39
|
+
"验收标准:设计档验收标准逐条可机验、项目验证链全绿。",
|
|
40
|
+
"交付报告格式:交付表 + 文件路径 + 验证命令与结果。",
|
|
41
|
+
].join("\n")
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* F2 任务书六强制字段校验(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2):
|
|
45
|
+
* round 枚举(initial|fix,必带集合 = eng-coder/eng-designer)+ 五段文本字段在场。
|
|
46
|
+
* 缺任一 → 抛错(派单缺陷——spawn 机械拒)。scan 面 = args.task ∪ args.context
|
|
47
|
+
* (context 是任务书背景段的携带参数——字段落在任一面均算在场)。
|
|
48
|
+
*/
|
|
49
|
+
export function validateTaskBookFields(args) {
|
|
50
|
+
const a = args ?? {}
|
|
51
|
+
const missing = []
|
|
52
|
+
if (!ROUND_VALUES.includes(a.round)) {
|
|
53
|
+
missing.push(a.round === undefined || a.round === null
|
|
54
|
+
? `round (${ROUND_VALUES.join("|")}) — the structured round parameter`
|
|
55
|
+
: `round ∈ {${ROUND_VALUES.join(", ")}} (got ${JSON.stringify(a.round)})`)
|
|
56
|
+
}
|
|
57
|
+
const text = [a.context, a.task].filter((s) => typeof s === "string").join("\n")
|
|
58
|
+
for (const f of TASK_BOOK_FIELDS) {
|
|
59
|
+
if (!f.markers.some((m) => m.test(text))) missing.push(f.label)
|
|
60
|
+
}
|
|
61
|
+
if (missing.length === 0) return
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Engineering spawn task book is missing mandatory field(s): ${missing.join(" · ")}. ` +
|
|
64
|
+
`The six-field task book — 目标与理由 (goal & why), round (initial|fix), 已知事实 (known facts), ` +
|
|
65
|
+
`设计要点与禁止范围 (design points & forbidden scope), 验收标准 (acceptance criteria), ` +
|
|
66
|
+
`交付报告格式 (delivery-report format) — is the spawn contract; a missing field is a dispatch ` +
|
|
67
|
+
`defect and the spawn is refused.`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* F6 过程档 basename 族(父侧维护文件——AGENT-LOOP.md §28 R26 F-R26b 同族扩展):
|
|
72
|
+
* CHANGELOG.md。(台账 = 用户数据目录键控库——项目内无文件,不再入族;老台账族 todo.md /
|
|
73
|
+
* todo-archive.md / checklist 族随 M2 SQLite 化 + M7 checklist 废除退役。)
|
|
74
|
+
* 大小写不敏感全名匹配(路径任意层——归一化在调用点 normalizeFileList 已做)。
|
|
75
|
+
*/
|
|
76
|
+
const PROCESS_BASENAMES = new Set(["changelog.md"])
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 工程模式写入面 basename(#33 二道防线——设计 docs/core/design/MANIFEST.md §2.5 / KD-M1-14):
|
|
80
|
+
* PROJECT-MANIFEST.json = M1 项目状态档——写门唯主 agent(`manifest.mjs` `writeManifest` 的
|
|
81
|
+
* `writer` 闸,fail-closed);本常量 = `files` 声明面**第二道防线**(子代理无法把该路径声明为
|
|
82
|
+
* 写域)。字面量(匹配前已大小写归一;**不** import `MANIFEST_REL`——保本模块「叶子·零 import」
|
|
83
|
+
* 性质;单源指针 = `thincoder-core/manifest.mjs`)。**不入** `PROCESS_BASENAMES`:两族理由不同
|
|
84
|
+
* ——过程档 = 父侧对账职责;本档 = 写门唯主 agent。
|
|
85
|
+
*/
|
|
86
|
+
const MANIFEST_BASENAME = "project-manifest.json"
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* F6 files 声明面拦截谓词(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2):
|
|
90
|
+
* scripts/**(工程工具面不入域——内容产物 only)与过程档(父侧职责)→ 收集全部违规
|
|
91
|
+
* 后抛合并错误(fail-closed——错误即工具结果 JSON——无排队残留)。目录声明不在本
|
|
92
|
+
* 谓词(normalizeFileList 循环内先行拒——错误序不变)。调用点 = normalizeFileList
|
|
93
|
+
* (subagent-scheduler.mjs——本谓词在循环后跑——目录错误先于过程档错误)。
|
|
94
|
+
*/
|
|
95
|
+
export function rejectEngineeringFilePaths(files) {
|
|
96
|
+
const violations = []
|
|
97
|
+
for (const f of Array.isArray(files) ? files : []) {
|
|
98
|
+
const segs = String(f).replace(/\\/g, "/").split("/").filter((s) => s)
|
|
99
|
+
const base = (segs[segs.length - 1] ?? "").toLowerCase()
|
|
100
|
+
if (segs.some((s) => s.toLowerCase() === "scripts")) {
|
|
101
|
+
violations.push(`engineering tool path ${f} must not be listed in files — scripts/** is the engineering-tool face, not a content product (files carries source/test/design-doc paths only)`)
|
|
102
|
+
} else if (PROCESS_BASENAMES.has(base)) {
|
|
103
|
+
violations.push(`Parent-side maintained file ${f} must not be listed in files — reconciliation is the parent's duty; use the design-doc path if you need to edit a design doc`)
|
|
104
|
+
} else if (base === MANIFEST_BASENAME) {
|
|
105
|
+
violations.push(`Manifest file ${f} must not be listed in files — PROJECT-MANIFEST.json is the project state manifest and the main agent is its only writer (M1 write gate); this files-domain check is the second line of defense`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (violations.length > 0) throw new Error(violations.join("\n"))
|
|
109
|
+
}
|