@thincoder/core 0.9.5 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +29 -12
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-ref.mjs +96 -3
  119. package/model-specs.mjs +16 -7
  120. package/notify-policy.mjs +51 -0
  121. package/package.json +1 -1
  122. package/peer-claims.mjs +2 -2
  123. package/peer-domains.mjs +7 -11
  124. package/peer-instances.mjs +9 -10
  125. package/process-probe-exec.mjs +187 -0
  126. package/process-probe.mjs +27 -175
  127. package/prompt-overlays.mjs +11 -10
  128. package/prompts/advisor-design.md +2 -2
  129. package/prompts/advisor-round2.md +1 -1
  130. package/prompts/advisor-round3.md +1 -1
  131. package/prompts/common.md +16 -8
  132. package/prompts/discipline-engineering.md +57 -16
  133. package/prompts/discipline-normal.md +6 -3
  134. package/prompts/persona-coder.md +0 -1
  135. package/prompts/persona-eng-coder.md +1 -5
  136. package/prompts/persona-eng-designer.md +2 -3
  137. package/prompts/persona-engineering.md +32 -11
  138. package/prompts/persona-explore.md +0 -1
  139. package/prompts/persona-normal.md +1 -1
  140. package/prompts/persona-plan.md +3 -4
  141. package/prompts/subagent-base.md +14 -0
  142. package/provider/core.mjs +11 -48
  143. package/provider/list-models.mjs +3 -0
  144. package/provider/responses-request.mjs +237 -0
  145. package/provider/responses.mjs +6 -228
  146. package/provider-flows.mjs +248 -0
  147. package/queued.mjs +90 -0
  148. package/rules.mjs +209 -2
  149. package/session-guard.mjs +27 -7
  150. package/session-lifecycle.mjs +108 -27
  151. package/session-rename.mjs +5 -1
  152. package/session-slot-claims.mjs +230 -0
  153. package/session-slot-scan.mjs +299 -0
  154. package/session-slot-verify.mjs +304 -0
  155. package/session-slot-write.mjs +74 -8
  156. package/session-slots-manifest.mjs +163 -241
  157. package/session-slots.mjs +99 -71
  158. package/session.mjs +30 -10
  159. package/shell-candidates.mjs +78 -0
  160. package/text-budget.mjs +2 -2
  161. package/think-off.mjs +49 -0
  162. package/token-ttl.mjs +15 -4
  163. package/tool-args.mjs +73 -0
  164. package/tool-docs/advisor.md +9 -0
  165. package/tool-docs/batch.md +1 -0
  166. package/tool-docs/code_search.md +1 -0
  167. package/tool-docs/consult_start.md +5 -0
  168. package/tool-docs/consult_stop.md +3 -0
  169. package/tool-docs/context.md +7 -0
  170. package/tool-docs/doc_search.md +1 -0
  171. package/tool-docs/edit.md +2 -13
  172. package/tool-docs/eng.md +1 -0
  173. package/tool-docs/execute.md +1 -9
  174. package/tool-docs/git.md +25 -51
  175. package/tool-docs/goal.md +6 -0
  176. package/tool-docs/ledger_add.md +1 -0
  177. package/tool-docs/ledger_close.md +1 -0
  178. package/tool-docs/ledger_count.md +1 -0
  179. package/tool-docs/ledger_query.md +1 -0
  180. package/tool-docs/ledger_update.md +1 -0
  181. package/tool-docs/main_history.md +1 -0
  182. package/tool-docs/memory.md +10 -0
  183. package/tool-docs/notify_parent.md +9 -0
  184. package/tool-docs/peer_instances.md +1 -0
  185. package/tool-docs/plan.md +5 -0
  186. package/tool-docs/process.md +7 -3
  187. package/tool-docs/read_history.md +7 -0
  188. package/tool-docs/recent_changes.md +1 -0
  189. package/tool-docs/repo_outline.md +1 -0
  190. package/tool-docs/settings.md +6 -0
  191. package/tool-docs/skill.md +5 -0
  192. package/tool-docs/subagent.md +31 -0
  193. package/tool-docs/task.md +7 -0
  194. package/tool-docs/timer.md +6 -0
  195. package/tool-docs/verify.md +1 -0
  196. package/tool-docs/wait_for.md +2 -1
  197. package/tools/bash.mjs +28 -4
  198. package/tools/edit-batch.mjs +4 -4
  199. package/tools/edit-diff.mjs +4 -23
  200. package/tools/exec-run.mjs +102 -4
  201. package/tools/execute.mjs +5 -2
  202. package/tools/file.mjs +4 -9
  203. package/tools/git-run.mjs +1 -1
  204. package/tools/git.mjs +0 -24
  205. package/tools/index.mjs +2 -1
  206. package/tools/linter.mjs +2 -1
  207. package/tools/node-child.mjs +21 -0
  208. package/tools/ops.mjs +50 -13
  209. package/tools/patch.mjs +4 -2
  210. package/tools/repomap.mjs +20 -9
  211. package/tools/shared.mjs +8 -6
  212. package/tools/web.mjs +5 -2
  213. package/tools/write-path.mjs +1 -1
  214. package/traces/trace-store.mjs +17 -14
  215. package/vision-reader.mjs +76 -0
  216. package/agent-tools/batch-segment.mjs +0 -18
package/conventions.mjs CHANGED
@@ -1,34 +1,48 @@
1
1
  /**
2
- * conventions.mjs — the single authority for code / doc / temp path classification,
3
- * plus the project convention declaration surface (`.thincoder/conventions.json`).
2
+ * conventions.mjs — the single authority for code / doc / temp / aux path classification,
3
+ * plus the project declaration surface (the three declaration families of the project's
4
+ * `PROJECT-MANIFEST.json`: `codePaths` / `index.*` / `advisor.*`).
4
5
  *
5
- * Why one module: engineering-mode gates and guards (design gate, review-doc gate,
6
+ * Why one module: the write-domain gates and guards (design gate, review-doc gate,
6
7
  * mutation accounting, verify fast path) each carried their own copy of the
7
8
  * "what counts as product code" predicate — anchored `^src/` regexes, `docs/`
8
9
  * prefix checks, component regexes. Each copy drifted, and each hardcoded THIS
9
10
  * repository's layout: a project whose code lives outside `src/` slipped through
10
- * the design gate silently (PORTABILITY FR12 / PO-10). One classifier + one
11
- * declaration file = one truth.
11
+ * the design gate silently (PORTABILITY FR12 / PO-10). One classifier + one declared
12
+ * carrier = one truth. It is a **shared** classification surface: the gates are consumers,
13
+ * not owners (consumer faces: advisor / agent-tools / both ends).
12
14
  *
13
- * Defaults are DATA (`DEFAULT_CODE_PATHS`) — overridable per project through the
14
- * declaration file (§4.1 schema). Missing file → pure defaults (no noise);
15
- * corrupt/unreadable file → defaults + console.warn + a log event (never crash,
16
- * never swallow — PORTABILITY FR10).
15
+ * Declaration (2026-09-27 · `.thincoder/conventions.json` retired — single carrier):
16
+ * the three families live in the project manifest and are projected through `manifest.mjs`
17
+ * → `declaration.mjs` (declaration loading split out of this file 2026-10-01 — this file keeps the
18
+ * classification adjudication; ONE reader / ONE root resolution — `readManifest` + `manifestFilePath`;
19
+ * name / shape / defaults are schema-owned by `docs/core/design/MANIFEST.md` §2.2).
20
+ * Missing manifest → pure defaults (silent); unusable manifest (invalid / read error) →
21
+ * defaults + console.warn + a log event (never crash, never swallow — PORTABILITY FR10/D16).
22
+ * A retired `.thincoder/conventions.json` is never read: while present it yields ONE
23
+ * visible warning per load (existence check only — zero content parsing — KD-M1-34).
17
24
  *
18
- * Classification vocabulary (PORTABILITY design §3.1):
19
- * code — inside a declared code segment (default: the path segment `src`), or
20
- * not a documentation extension; doc — documentation extension outside
21
- * any code segment; temp — tmp-* name or .tmp/.temp extension.
25
+ * Classification vocabulary (PORTABILITY design §3.2):
26
+ * code — inside a declared code segment (default: the path segment `src`), or the
27
+ * fallback; doc — documentation extension outside any code segment and not
28
+ * scratch; temp — tmp-* name or .tmp/.temp extension; aux — a default
29
+ * auxiliary-path segment sequence (test / tests / scripts / .thincoder/tmp —
30
+ * F9) that matched no code segment and neither doc nor temp.
31
+ * Precedence: code segment → temp → doc → aux → fallback code.
32
+ *
33
+ * Segment matching runs on the PROJECT-ROOT-RELATIVE FACE of a path (D17 · 2026-09-28): the
34
+ * machine layout ABOVE the project root never participates. An absolute path outside the
35
+ * root / an unknown root / a `.`-`..` jump segment ⇒ the face is indeterminate: code segments
36
+ * fall back to the whole path (the interception surface does not shrink), aux sequences do
37
+ * NOT match (the former fail-open edge — an ancestor segment exempted a whole subtree).
22
38
  */
23
- import { readFileSync } from "node:fs"
24
- import { join, resolve } from "node:path"
25
- import { logEvent } from "./log.mjs"
26
-
27
- /** Default code-path segments (data, not logic — a project may replace them). */
28
- export const DEFAULT_CODE_PATHS = ["src"]
39
+ import { DEFAULT_DECLARATION } from "./declaration.mjs"
29
40
 
30
- /** Project declaration file, relative to the project root. */
31
- export const CONVENTIONS_REL_PATH = ".thincoder/conventions.json"
41
+ /** Default auxiliary-path segment sequences (data, not logic — F9): the engineering-mode
42
+ * parent gate's default exemption surface — `test` / `tests` / `scripts` at any depth,
43
+ * plus the `.thincoder/tmp` scratch space (two-segment sequence). Not declarable — a
44
+ * project reclaims a sequence by listing it in `codePaths` (code segment wins). */
45
+ export const DEFAULT_AUX_PATHS = ["test", "tests", "scripts", ".thincoder/tmp"]
32
46
 
33
47
  /** Documentation predicate (moved here verbatim from advisor/repos.mjs — one copy). */
34
48
  const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
@@ -46,18 +60,44 @@ function segmentsOf(p) {
46
60
  return String(p ?? "").replace(/\\/g, "/").split("/").filter(Boolean)
47
61
  }
48
62
 
63
+ const lowerSeg = (s) => s.toLowerCase()
64
+ const isJumpSeg = (seg) => seg === "." || seg === ".."
65
+
66
+ /**
67
+ * The segment-matching face of a path — its position INSIDE the project (PORTABILITY design
68
+ * §3.2 · D17「段匹配面 = 项目根相对面」):
69
+ * · absolute form under the root (separators normalized + per-segment case-insensitive
70
+ * comparison) ⇒ the REMAINING segments (an EMPTY list when the path IS the root);
71
+ * · relative form ⇒ its own segments;
72
+ * · anything else — outside the root / root unknown / a `.` / `..` jump segment ⇒
73
+ * `null` = indeterminate.
74
+ */
75
+ function faceOf(p, root) {
76
+ const s = String(p ?? "").replace(/\\/g, "/")
77
+ const segs = segmentsOf(s)
78
+ if (!/^([A-Za-z]:|\/)/.test(s)) return segs.some(isJumpSeg) ? null : segs.map(lowerSeg)
79
+ if (typeof root !== "string" || root === "") return null
80
+ const rootSegs = segmentsOf(root).map(lowerSeg)
81
+ if (segs.length < rootSegs.length) return null
82
+ for (let i = 0; i < rootSegs.length; i++) {
83
+ if (lowerSeg(segs[i]) !== rootSegs[i]) return null
84
+ }
85
+ const rest = segs.slice(rootSegs.length)
86
+ if (rest.length === 0) return []
87
+ return rest.some(isJumpSeg) ? null : rest.map(lowerSeg)
88
+ }
89
+
49
90
  /**
50
- * True when the path contains a declared code segment sequence at any depth.
91
+ * True when `segments` contain any of the given segment sequences at any depth.
51
92
  * Segment matching (not a prefix anchor) is what closes the nested-layout hole:
52
- * `packages/foo/src/x.md` is product code, not a document. Comparison is
53
- * case-insensitive — on case-insensitive filesystems `Src/x.mjs` is the same
54
- * directory, and the gate must not be bypassable by casing.
93
+ * `packages/foo/src/x.md` matches the sequence ["src"] — product code, not a
94
+ * document. Comparison is case-insensitive — on case-insensitive filesystems
95
+ * `Src/x.mjs` is the same directory, and the gate must not be bypassable by casing.
55
96
  */
56
- function hasCodeSegment(p, conv) {
57
- const parts = segmentsOf(p).map((s) => s.toLowerCase())
58
- const wanted = conv?.codePaths ?? DEFAULT_CODE_PATHS
59
- for (const entry of wanted) {
60
- const want = segmentsOf(entry).map((s) => s.toLowerCase())
97
+ function hasSequenceIn(segments, sequences) {
98
+ const parts = segments.map(lowerSeg)
99
+ for (const entry of sequences) {
100
+ const want = segmentsOf(entry).map(lowerSeg)
61
101
  if (want.length === 0) continue
62
102
  for (let i = 0; i + want.length <= parts.length; i++) {
63
103
  if (want.every((seg, j) => parts[i + j] === seg)) return true
@@ -66,15 +106,40 @@ function hasCodeSegment(p, conv) {
66
106
  return false
67
107
  }
68
108
 
69
- /** "code" | "doc" | "temp" — the single classification decision.
109
+ /**
110
+ * Segment matching on the project-root-relative face (D17). An indeterminate face splits the
111
+ * two callers by design: code segments fall back to the WHOLE path (`fallbackAll` — the
112
+ * interception surface does not shrink), while aux sequences do NOT match (the former
113
+ * fail-open edge: an ancestor segment above the root used to exempt a whole subtree).
114
+ */
115
+ function hasSegmentSequence(p, sequences, root, fallbackAll) {
116
+ const face = faceOf(p, root)
117
+ if (face === null) return fallbackAll ? hasSequenceIn(segmentsOf(p), sequences) : false
118
+ return hasSequenceIn(face, sequences)
119
+ }
120
+
121
+ /** True when the path contains a declared code segment sequence (on the face — D17). */
122
+ function hasCodeSegment(p, conv) {
123
+ return hasSegmentSequence(p, conv?.codePaths ?? DEFAULT_DECLARATION.codePaths, conv?.root, true)
124
+ }
125
+
126
+ /** True when the path contains a default auxiliary-path segment sequence (F9 · face-bound). */
127
+ function hasAuxSegment(p, conv) {
128
+ return hasSegmentSequence(p, DEFAULT_AUX_PATHS, conv?.root, false)
129
+ }
130
+
131
+ /** "code" | "doc" | "temp" | "aux" — the single classification decision.
70
132
  * Precedence: code segment first (src/** stays product code even when the name
71
133
  * looks scratch — the pre-existing unconditional-src rule), then temp, then a
72
- * documentation extension, else code (anything not doc/temp is product code). */
134
+ * documentation extension, then an auxiliary-path segment sequence (test / tests
135
+ * / scripts / .thincoder/tmp — F9), else code (anything not doc/temp/aux is product
136
+ * code). Both segment checks run on the project-root-relative face (D17 — `faceOf`). */
73
137
  export function classifyPath(p, conv) {
74
138
  const s = String(p ?? "")
75
139
  if (hasCodeSegment(s, conv)) return "code"
76
140
  if (TEMP_FILE.test(s)) return "temp"
77
141
  if (DOC_FILE.test(s)) return "doc"
142
+ if (hasAuxSegment(s, conv)) return "aux"
78
143
  return "code"
79
144
  }
80
145
 
@@ -90,134 +155,18 @@ export function isDocPath(p, conv) {
90
155
  return DOC_FILE.test(s) && !hasCodeSegment(s, conv)
91
156
  }
92
157
 
93
- // ─────────────────────────────────────────────────────────────────────────────
94
- // Declaration loading (cached per project root — `clearConventionsCache()` is
95
- // the test seam; declaration files change rarely and only at session scope).
96
- // ─────────────────────────────────────────────────────────────────────────────
97
-
98
- function normalizeExtensions(v) {
99
- if (!Array.isArray(v)) return []
100
- const out = []
101
- for (const e of v) {
102
- if (typeof e !== "string") continue
103
- const t = e.trim().toLowerCase()
104
- if (!t) continue
105
- const ext = t.startsWith(".") ? t : `.${t}`
106
- if (!out.includes(ext)) out.push(ext)
107
- }
108
- return out
158
+ /** True when the path is an auxiliary path (`classifyPath === "aux"`) — the engineering-mode
159
+ * parent gate's default exemption surface (test / tests / scripts at any depth ·
160
+ * .thincoder/tmp/** — matched on the project-root-relative face only, D17). Not declarable:
161
+ * a project reclaims one by listing its sequence in `codePaths`, which then wins (code
162
+ * segment first). Production faces read aux through classifyPath / isCodePath; this
163
+ * predicate is the readable form for the test face. */
164
+ export function isAuxPath(p, conv) {
165
+ return classifyPath(p, conv) === "aux"
109
166
  }
110
167
 
111
- /** Declared code paths replace the default (replacement, not union — §4.1). */
112
- function normalizeCodePaths(v) {
113
- if (!Array.isArray(v)) return null
114
- const out = []
115
- for (const e of v) {
116
- if (typeof e !== "string") continue
117
- const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
118
- if (!s || out.includes(s)) continue
119
- out.push(s)
120
- }
121
- return out.length > 0 ? out : null
122
- }
123
-
124
- function normalizeString(v) {
125
- return typeof v === "string" && v.trim() ? v.trim() : ""
126
- }
127
-
128
- /** Per-key type check for recognized keys (present but wrong type). §3.2/§4.1: a
129
- * type error degrades WITH a warning — never silently (the fallback semantics stay
130
- * per-key; only the visibility is added here). */
131
- function typeErrorsOf(raw) {
132
- const isObj = (v) => v !== undefined && v !== null && typeof v === "object" && !Array.isArray(v)
133
- const strArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string")
134
- const errs = []
135
- if (raw.codePaths !== undefined && !strArray(raw.codePaths)) errs.push("codePaths must be an array of strings")
136
- const idx = raw.index
137
- if (idx !== undefined && !isObj(idx)) errs.push("index must be an object")
138
- else if (isObj(idx)) {
139
- for (const k of ["codeExtensions", "docExtensions"]) {
140
- if (idx[k] !== undefined && !strArray(idx[k])) errs.push(`index.${k} must be an array of strings`)
141
- }
142
- }
143
- const adv = raw.advisor
144
- if (adv !== undefined && !isObj(adv)) errs.push("advisor must be an object")
145
- else if (isObj(adv)) {
146
- for (const k of ["docMap", "standardsDoc"]) {
147
- if (adv[k] !== undefined && typeof adv[k] !== "string") errs.push(`advisor.${k} must be a string`)
148
- }
149
- }
150
- return errs
151
- }
152
-
153
- function buildConventions(raw) {
154
- const codePaths = normalizeCodePaths(raw?.codePaths)
155
- const codeExtensions = normalizeExtensions(raw?.index?.codeExtensions)
156
- const docExtensions = normalizeExtensions(raw?.index?.docExtensions)
157
- const docMap = normalizeString(raw?.advisor?.docMap)
158
- const standardsDoc = normalizeString(raw?.advisor?.standardsDoc)
159
- // `declared` = the declaration actually took effect (at least one recognized key
160
- // honored) — the design-gate hint reads it to decide whether to point at the
161
- // declaration file ("declare project conventions … to adjust").
162
- const declared = Boolean(codePaths || codeExtensions.length || docExtensions.length || docMap || standardsDoc)
163
- return Object.freeze({
164
- declared,
165
- codePaths: Object.freeze(codePaths ?? [...DEFAULT_CODE_PATHS]),
166
- index: Object.freeze({
167
- codeExtensions: Object.freeze(codeExtensions),
168
- docExtensions: Object.freeze(docExtensions),
169
- }),
170
- advisor: Object.freeze({ docMap, standardsDoc }),
171
- })
172
- }
173
-
174
- /** Full-default conventions (no declaration) — the fallback every consumer gets. */
175
- export const DEFAULT_CONVENTIONS = buildConventions(null)
176
-
177
- const _cache = new Map()
178
-
179
- /** Drop the per-root cache (test seam — declaration files are read once per root). */
180
- export function clearConventionsCache() {
181
- _cache.clear()
182
- }
183
-
184
- /**
185
- * Load (and cache) the normalized conventions for a project root.
186
- * @param {string} cwd — project root (declaration lives at .thincoder/conventions.json)
187
- * @returns {Readonly<{declared: boolean, codePaths: readonly string[],
188
- * index: {codeExtensions: string[], docExtensions: string[]},
189
- * advisor: {docMap: string, standardsDoc: string}}>}
190
- */
191
- export function loadConventions(cwd) {
192
- const root = resolve(cwd ?? process.cwd())
193
- const hit = _cache.get(root)
194
- if (hit) return hit
195
- let conv = DEFAULT_CONVENTIONS
196
- try {
197
- const text = readFileSync(join(root, CONVENTIONS_REL_PATH), "utf8")
198
- try {
199
- const raw = JSON.parse(text)
200
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("top level must be a JSON object")
201
- conv = buildConventions(raw)
202
- const typeErrs = typeErrorsOf(raw)
203
- if (typeErrs.length > 0) {
204
- // Wrong-typed keys fall back per-key — but the user must SEE that their
205
- // declaration did not take effect (never silently swallowed).
206
- console.warn(`[conventions] ${CONVENTIONS_REL_PATH} has invalid value types (${typeErrs.join("; ")}) — those keys fall back to defaults`)
207
- logEvent("conventions:error", { cwd: root, err: `type errors: ${typeErrs.join("; ").slice(0, 160)}` })
208
- }
209
- } catch (e) {
210
- // Corrupt file / wrong shape → defaults, visible: warn + event (never silent).
211
- console.warn(`[conventions] ${CONVENTIONS_REL_PATH} unreadable (${e?.message ?? e}) — falling back to defaults`)
212
- logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
213
- }
214
- } catch (e) {
215
- if (e?.code !== "ENOENT") {
216
- // File exists but cannot be read (EACCES etc.) — same visible degradation.
217
- console.warn(`[conventions] ${CONVENTIONS_REL_PATH} not readable (${e?.message ?? e}) — falling back to defaults`)
218
- logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
219
- }
220
- }
221
- _cache.set(root, conv)
222
- return conv
223
- }
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+ // 迁出面(2026-10-01 拆分批 · #755 ∥ #786):声明装载面外提 `declaration.mjs`——本档经
170
+ // 转口保名(消费面 / 批内件 import 面零改;`DEFAULT_DECLARATION` 另供本档分类裁判缺省取用)。
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ export { loadProjectDeclaration, DEFAULT_DECLARATION, isExcludedRelPath, declaredPublicRoots, clearDeclarationCache } from "./declaration.mjs"
@@ -0,0 +1,194 @@
1
+ /**
2
+ * declaration.mjs — 声明装载面(PROJECT-MANIFEST.json 三族 `codePaths` / `index.*` / `advisor.*`)
3
+ * · 自 `conventions.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786。
4
+ * 投影单源 = `manifest.mjs`(ONE reader / ONE root resolution——`readManifest` + `manifestFilePath`;
5
+ * name / shape / defaults = schema 面 `docs/core/design/MANIFEST.md` §2.2)。迁出块逐字;
6
+ * 原档 `conventions.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改)。
7
+ */
8
+ import { existsSync } from "node:fs"
9
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
10
+ import { DEFAULT_MANIFEST, manifestFilePath, readManifest } from "./manifest.mjs"
11
+ import { logEvent } from "./log.mjs"
12
+
13
+ /** Retired declaration carrier (project-root relative). Its EXISTENCE is checked, its
14
+ * content is never read — the declaration surface = the manifest three families (KD-M1-34). */
15
+ const RETIRED_REL_PATH = ".thincoder/conventions.json"
16
+
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // Declaration loading — the three families of PROJECT-MANIFEST.json, projected
19
+ // through manifest.mjs (ONE reader / ONE root resolution) and cached per DATA-FILE
20
+ // PATH (`clearDeclarationCache()` is the test seam; declarations change rarely).
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ function normalizeExtensions(v) {
24
+ if (!Array.isArray(v)) return []
25
+ const out = []
26
+ for (const e of v) {
27
+ if (typeof e !== "string") continue
28
+ const t = e.trim().toLowerCase()
29
+ if (!t) continue
30
+ const ext = t.startsWith(".") ? t : `.${t}`
31
+ if (!out.includes(ext)) out.push(ext)
32
+ }
33
+ return out
34
+ }
35
+
36
+ /** Declared code paths replace the default (replacement, not union — §3.1). An EMPTY
37
+ * array is a legal declaration ("no segment-based code face") — only a non-array value
38
+ * falls back to `null` (⇒ caller keeps the default). */
39
+ function normalizeCodePaths(v) {
40
+ if (!Array.isArray(v)) return null
41
+ const out = []
42
+ for (const e of v) {
43
+ if (typeof e !== "string") continue
44
+ const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
45
+ if (!s || out.includes(s)) continue
46
+ out.push(s)
47
+ }
48
+ return out
49
+ }
50
+
51
+ /** 路径列表归一(单源——`index.excludePaths` §6.14 ∥ `index.publicRepos` §6.15 同款):首部 `./` 剥离 · `\`→`/` · 去尾斜杠 · 空 ∕ 非串剔除 · 去重保序。 */
52
+ function normalizePathList(v) {
53
+ if (!Array.isArray(v)) return []
54
+ const out = []
55
+ for (const e of v) {
56
+ if (typeof e !== "string") continue
57
+ const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
58
+ if (!s || out.includes(s)) continue
59
+ out.push(s)
60
+ }
61
+ return out
62
+ }
63
+
64
+ function normalizeString(v) {
65
+ return typeof v === "string" && v.trim() ? v.trim() : ""
66
+ }
67
+
68
+ /** Project the manifest three families into the frozen declaration object.
69
+ * `declared` = VALUE comparison against the defaults — at least one family deviates
70
+ * (a key written with its default value still counts as NOT declared; the design-gate
71
+ * hint reads it to decide whether to point at the manifest, PORTABILITY §3.1).
72
+ * `root` = the project root this declaration belongs to (`dirname(manifestFilePath(cwd))` —
73
+ * the segment-matching face's anchor, D18); NOT a declared value (excluded from `declared`);
74
+ * `null` = unknown root (`DEFAULT_DECLARATION`). */
75
+ function buildDeclaration(m, root = null) {
76
+ const codePaths = normalizeCodePaths(m?.codePaths) ?? [...DEFAULT_MANIFEST.codePaths]
77
+ const codeExtensions = normalizeExtensions(m?.index?.codeExtensions)
78
+ const docExtensions = normalizeExtensions(m?.index?.docExtensions)
79
+ const excludePaths = normalizePathList(m?.index?.excludePaths)
80
+ const publicRepos = normalizePathList(m?.index?.publicRepos)
81
+ const docMap = normalizeString(m?.advisor?.docMap)
82
+ const standardsDoc = normalizeString(m?.advisor?.standardsDoc)
83
+ const defaults = normalizeCodePaths(DEFAULT_MANIFEST.codePaths) ?? []
84
+ const pathsDiffer = codePaths.length !== defaults.length || codePaths.some((p, i) => p !== defaults[i])
85
+ const declared = pathsDiffer || codeExtensions.length > 0 || docExtensions.length > 0 || excludePaths.length > 0 || publicRepos.length > 0 || Boolean(docMap) || Boolean(standardsDoc)
86
+ return Object.freeze({
87
+ declared,
88
+ codePaths: Object.freeze(codePaths),
89
+ index: Object.freeze({
90
+ codeExtensions: Object.freeze(codeExtensions),
91
+ docExtensions: Object.freeze(docExtensions),
92
+ excludePaths: Object.freeze(excludePaths),
93
+ publicRepos: Object.freeze(publicRepos),
94
+ }),
95
+ advisor: Object.freeze({ docMap, standardsDoc }),
96
+ root,
97
+ })
98
+ }
99
+
100
+ /** Full-default declaration (no manifest / unusable manifest) — the fallback every
101
+ * consumer gets; built from `DEFAULT_MANIFEST` so the default values stay single-source. */
102
+ export const DEFAULT_DECLARATION = buildDeclaration(null)
103
+
104
+ /** 排除前缀谓词(单源——§6.14 面①):`rel`(**`base` 相对**——缺省视为根相对)命中声明前缀(恰等
105
+ * ∨ 后随 `/`——`openclaw` 不吞 `openclaw-fork`)⇒ true;缺省 `[]` ⇒ 恒 false(零行为变化)。
106
+ * `base`(调用面 cwd/origin·#700)= `rel` 的相对基:`resolve` + `relative` 换算至 `decl.root` 根面
107
+ * 再比前缀(cwd = 项目根时与既有行为逐字等义);换算越出根面(`..` 头 ∥ 根外绝对形)⇒ **不命中**
108
+ * (保守不排除——沉默洞方向收窄,与 D17 aux 面「indeterminate ⇒ 不匹配」同向);`decl.root` 缺
109
+ * ⇒ `rel` 原样(根起步调用面等义)。 */
110
+ export function isExcludedRelPath(rel, decl, base = null) {
111
+ const list = decl?.index?.excludePaths
112
+ if (!Array.isArray(list) || list.length === 0) return false
113
+ let s = String(rel ?? "").replace(/\\/g, "/")
114
+ const root = decl?.root
115
+ if (base != null && root) {
116
+ const converted = relative(root, resolve(base, s)).replace(/\\/g, "/")
117
+ if (converted === ".." || converted.startsWith("../") || isAbsolute(converted)) return false
118
+ s = converted
119
+ }
120
+ return list.some((p) => p && (s === p || s.startsWith(`${p}/`)))
121
+ }
122
+
123
+ /**
124
+ * 声明公共仓**绝对根集**(§6.15 解析层——检索同步 ∥ 读面 origin 集 ∥ 引用解析三消费面共用的单一定义,
125
+ * 不得二写):值形层逐项 `resolve(decl.root, p)` ⇒ 绝对根集(去重保序);存在性判归消费面(本层不判)。
126
+ * 缺省 `[]` ⇒ `[]`(零行为)。
127
+ * @param {string} [cwd] 会话锚(项目根经 manifest.mjs 解析)
128
+ * @returns {string[]} 绝对路径(去重保序)
129
+ */
130
+ export function declaredPublicRoots(cwd) {
131
+ const decl = loadProjectDeclaration(cwd)
132
+ const list = decl?.index?.publicRepos
133
+ if (!Array.isArray(list) || list.length === 0) return []
134
+ const base = typeof decl.root === "string" && decl.root !== "" ? decl.root : resolve(cwd ?? ".")
135
+ const out = []
136
+ for (const p of list) {
137
+ const abs = resolve(base, p)
138
+ if (!out.includes(abs)) out.push(abs)
139
+ }
140
+ return out
141
+ }
142
+
143
+ const _cache = new Map()
144
+
145
+ /** Drop the per-data-file-path cache (test seam — declarations are read once per path). */
146
+ export function clearDeclarationCache() {
147
+ _cache.clear()
148
+ }
149
+
150
+ /** Retired-carrier warning (existence check only — zero content parsing, zero fallback). */
151
+ function warnRetiredCarrier(root) {
152
+ console.warn(`[declaration] ${RETIRED_REL_PATH} is retired and no longer read — move codePaths / index.*Extensions / advisor.{docMap,standardsDoc} into PROJECT-MANIFEST.json`)
153
+ logEvent("declaration:retired-file", { cwd: root })
154
+ }
155
+
156
+ /**
157
+ * Load (and cache) the normalized project declaration for a project.
158
+ * The data file is located through `manifestFilePath(cwd)` (ONE root resolution — a cwd
159
+ * deeper than the project root still reads the project's manifest) and read through
160
+ * `readManifest` (ONE reader — this module carries no second parse of the manifest).
161
+ * Missing manifest → defaults, silently (the returned `root` is still the project root —
162
+ * `dirname(manifestFilePath(cwd))`, the face's anchor). Unusable manifest (invalid shape /
163
+ * read error) → defaults + `console.warn` + a log event (visible degradation, never a throw).
164
+ * A retired `.thincoder/conventions.json` at the manifest's project root → one warning
165
+ * per cache miss (content zero-parsed, zero effect on the readings — KD-M1-34).
166
+ * @param {string} cwd — project dir / anchor (root resolved by manifest.mjs)
167
+ * @returns {Readonly<{declared: boolean, codePaths: readonly string[],
168
+ * index: {codeExtensions: string[], docExtensions: string[], excludePaths: string[], publicRepos: string[]},
169
+ * advisor: {docMap: string, standardsDoc: string}, root: string|null}>}
170
+ */
171
+ export function loadProjectDeclaration(cwd) {
172
+ const file = manifestFilePath(cwd) // data-file path (single source — manifest.mjs KD-M1-18)
173
+ const hit = _cache.get(file)
174
+ if (hit) return hit
175
+ const root = dirname(file) // project root the manifest belongs to (cwd may be deeper) — the face's anchor
176
+ if (existsSync(join(root, RETIRED_REL_PATH))) warnRetiredCarrier(root)
177
+ let decl = buildDeclaration(null, root) // missing / unusable manifest → the defaults, root still known
178
+ try {
179
+ const r = readManifest(cwd)
180
+ if (r.ok) {
181
+ decl = buildDeclaration(r.manifest, root)
182
+ } else if (r.reason !== "missing") {
183
+ // Usable-file expectation broken (invalid JSON / wrong shape) → defaults, visible.
184
+ console.warn(`[declaration] ${file} is not a usable declaration (${(r.errors ?? []).join("; ") || r.reason}) — falling back to defaults`)
185
+ logEvent("declaration:error", { path: file, err: `invalid: ${(r.errors ?? []).join("; ")}`.slice(0, 200) })
186
+ }
187
+ } catch (e) {
188
+ // Read error (EACCES …) — same visible degradation, never a throw.
189
+ console.warn(`[declaration] ${file} not readable (${e?.message ?? e}) — falling back to defaults`)
190
+ logEvent("declaration:error", { path: file, err: String(e?.message ?? e).slice(0, 200) })
191
+ }
192
+ _cache.set(file, decl)
193
+ return decl
194
+ }
package/embedding.mjs CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { RETRYABLE_STATUS } from "./provider/index.mjs"
9
+ import { resolveEnvRefs } from "./env-ref.mjs"
9
10
  const MAX_RETRIES = 3
10
11
  const BATCH_SIZE = 32 // max texts per request (within SiliconFlow limits)
11
12
 
@@ -16,7 +17,8 @@ export function createEmbedder(config) {
16
17
  if (!config?.model) throw new Error("embedding config: model is required — configure embedding.model in ~/.thincoder/config.json")
17
18
  return {
18
19
  baseURL: config.baseURL.replace(/\/+$/, ""),
19
- apiKey: config.apiKey,
20
+ // #57(provider-config-family 批):值位 `${env:VAR}` 消费侧解析(CONFIG.md §6.3)——embedder 持解析值,请求侧零改。
21
+ apiKey: resolveEnvRefs(config.apiKey),
20
22
  model: config.model,
21
23
  }
22
24
  }
package/env-ref.mjs ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * env-ref.mjs — 值位 `${env:NAME}` 引用的消费侧解析(单源叶档;机制 = `docs/core/design/CONFIG.md` §6.3)。
3
+ *
4
+ * 语义(零 import 纯函数):
5
+ * - 整值与内嵌同解(`sk-${env:K}` 式);同一值可含多引用;非字符串值原样通过。
6
+ * - 变量未设 ∕ 空串 ⇒ 抛错(点名变量;无「未设即缺省」回退——不静默字面透传)。
7
+ * - 畸形引用(`${env:` 起头不构成合法引用)同抛错(点名畸形片段)。
8
+ * - 解析单遍:替换产物不再二次扫描;解析产物永不回显、永不回写(调用方纪律——写盘链按磁盘原文)。
9
+ */
10
+ const REF_PREFIX = "${env:"
11
+ const REF_RE = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g
12
+
13
+ /** 读一个变量(未设 ∕ 空串 ⇒ 抛错,点名变量;错误句零含解析产物 ∕ 前置字面)。 */
14
+ function readVar(name) {
15
+ const v = process.env[name]
16
+ if (v === undefined || v === "") {
17
+ throw new Error(`env reference \${env:${name}}: variable "${name}" ${v === undefined ? "is not set" : "is empty"} — set the variable or replace the reference with a literal value`)
18
+ }
19
+ return v
20
+ }
21
+
22
+ /** 畸形引用抛错(快照自 `${env:` 起截——引用串之前的字面不进错误句)。 */
23
+ function badRef(value, idx) {
24
+ return new Error(`malformed env reference ${JSON.stringify(value.slice(idx, idx + 24))} — expected \${env:NAME} with NAME matching [A-Za-z_][A-Za-z0-9_]*`)
25
+ }
26
+
27
+ /** 字符串值解析:无引用 ⇒ 原串返回;逐引用替换(每次 `${env:` 出现必须构成合法引用)。 */
28
+ export function resolveEnvRefs(value) {
29
+ if (typeof value !== "string" || !value.includes(REF_PREFIX)) return value
30
+ const out = []
31
+ let pos = 0
32
+ while (pos < value.length) {
33
+ const idx = value.indexOf(REF_PREFIX, pos)
34
+ if (idx === -1) { out.push(value.slice(pos)); break }
35
+ REF_RE.lastIndex = idx
36
+ const m = REF_RE.exec(value)
37
+ if (!m || m.index !== idx) throw badRef(value, idx)
38
+ out.push(value.slice(pos, idx), readVar(m[1]))
39
+ pos = idx + m[0].length
40
+ }
41
+ return out.join("")
42
+ }
43
+
44
+ /** 键值对象解析(headers ∕ env 族):无引用 ⇒ 原对象返回(零拷贝);值非字符串原样。 */
45
+ export function resolveEnvRefMap(map) {
46
+ if (map === null || typeof map !== "object" || Array.isArray(map)) return map
47
+ let changed = false
48
+ const out = {}
49
+ for (const [k, v] of Object.entries(map)) {
50
+ const rv = resolveEnvRefs(v)
51
+ if (rv !== v) changed = true
52
+ out[k] = rv
53
+ }
54
+ return changed ? out : map
55
+ }
56
+
57
+ /** providers[] 条目适配:`apiKey` ∥ `headers.*`(无引用 ⇒ 原对象返回——零拷贝零写回)。 */
58
+ export function resolveProviderSecrets(provider) {
59
+ if (provider === null || typeof provider !== "object") return provider
60
+ const apiKey = resolveEnvRefs(provider.apiKey)
61
+ const headers = resolveEnvRefMap(provider.headers)
62
+ if (apiKey === provider.apiKey && headers === provider.headers) return provider
63
+ const out = { ...provider }
64
+ if (apiKey !== provider.apiKey) out.apiKey = apiKey
65
+ if (headers !== provider.headers) out.headers = headers
66
+ return out
67
+ }
68
+
69
+ /** mcp.servers[] 条目适配:`token` ∥ `headers.*` ∥ `env.*`(无引用 ⇒ 原对象返回)。 */
70
+ export function resolveMcpServerSecrets(config) {
71
+ if (config === null || typeof config !== "object") return config
72
+ const token = resolveEnvRefs(config.token)
73
+ const headers = resolveEnvRefMap(config.headers)
74
+ const env = resolveEnvRefMap(config.env)
75
+ if (token === config.token && headers === config.headers && env === config.env) return config
76
+ const out = { ...config }
77
+ if (token !== config.token) out.token = token
78
+ if (headers !== config.headers) out.headers = headers
79
+ if (env !== config.env) out.env = env
80
+ return out
81
+ }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * explore-distill.mjs — End-of-run exploration distillation (AGENT-LOOP §13 +
3
- * CONTEXT-COMPACTION §6.9 H1, 2026-08-23). 2026-09-05 module-split: moved verbatim out of
2
+ * explore-distill.mjs — End-of-run exploration distillation (CONTEXT-COMPACTION.md §6.9 H1, 2026-08-23). 2026-09-05 module-split: moved verbatim out of
4
3
  * context.mjs (524 > 500 hard limit). The main agent's machine line is flooded by inline
5
4
  * step-by-step exploration (read/grep/...). At run end we distill THIS run's exploration
6
5
  * tool-results into one semantic summary note that replaces them in the machine line,
@@ -99,7 +98,7 @@ async function distillExplorations(history, start, provider, signal, agent, dept
99
98
  messages: buildCompressMessages(history, cut, extras?.systemPrompt, EXPLORE_SUMMARY_PROMPT),
100
99
  tools: extras?.tools,
101
100
  signal,
102
- // §18.6 D-TR4:轨迹元数据增补——kind=distill(探索蒸馏面——agent 元数据透出;
101
+ // TRACES.md §6.1 D-TR4:轨迹元数据增补——kind=distill(探索蒸馏面——agent 元数据透出;
103
102
  // depth 经 summarizeRunExplorations 参数透传——agent.mjs 主作用域传入)
104
103
  logCtx: {
105
104
  stage: "distill", child: agent?._logId ?? null, kind: "distill",