@thincoder/core 0.10.10 → 0.10.12

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 (82) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/advisor/messages.mjs +1 -1
  3. package/advisor/project-context.mjs +2 -2
  4. package/agent/completion.mjs +4 -1
  5. package/agent/family-tools.mjs +3 -3
  6. package/agent/helpers.mjs +13 -11
  7. package/agent/light-round.mjs +1 -4
  8. package/agent/record-results.mjs +6 -1
  9. package/agent/setup-reminders.mjs +1 -1
  10. package/agent/unbooked-pen.mjs +62 -0
  11. package/agent-tools/advisor.mjs +15 -31
  12. package/agent-tools/audit-block.mjs +16 -62
  13. package/agent-tools/batch-gate.mjs +74 -66
  14. package/agent-tools/batch-lifecycle.mjs +66 -9
  15. package/agent-tools/batch-paths.mjs +10 -8
  16. package/agent-tools/context.mjs +1 -1
  17. package/agent-tools/design-slots.mjs +72 -12
  18. package/agent-tools/design-token.mjs +27 -4
  19. package/agent-tools/digest-budget.mjs +2 -0
  20. package/agent-tools/doc-check.mjs +3 -14
  21. package/agent-tools/manifest.mjs +4 -5
  22. package/agent-tools/review-facts.mjs +35 -81
  23. package/agent-tools/settings.mjs +65 -5
  24. package/agent-tools/spawn-gates.mjs +68 -31
  25. package/agent-tools/subagent-async.mjs +6 -3
  26. package/agent-tools/subagent-spawn.mjs +42 -61
  27. package/agent-tools/subagent.mjs +15 -15
  28. package/browser/launch.mjs +14 -3
  29. package/browser/queue.mjs +31 -3
  30. package/browser/session.mjs +4 -0
  31. package/config-defaults.mjs +8 -0
  32. package/config-presets.mjs +1 -1
  33. package/config.mjs +60 -11
  34. package/doc-check/anchors.mjs +10 -9
  35. package/doc-check/index.mjs +27 -23
  36. package/doc-check/paragraph.mjs +119 -0
  37. package/fts-text.mjs +4 -1
  38. package/ledger-cmd.mjs +56 -41
  39. package/ledger-db.mjs +65 -58
  40. package/ledger-tools.mjs +60 -23
  41. package/ledger.mjs +13 -11
  42. package/manifest-discovery.mjs +58 -34
  43. package/manifest.mjs +34 -21
  44. package/model-context.mjs +98 -0
  45. package/model-specs.mjs +121 -50
  46. package/package.json +2 -2
  47. package/permission.mjs +3 -1
  48. package/prompts/advisor-design.md +16 -0
  49. package/prompts/common.md +12 -1
  50. package/prompts/discipline-engineering.md +42 -12
  51. package/prompts/discipline-normal.md +8 -7
  52. package/prompts/persona-coder.md +8 -1
  53. package/prompts/persona-eng-coder.md +8 -0
  54. package/prompts/persona-eng-designer.md +26 -4
  55. package/prompts/persona-engineering.md +37 -38
  56. package/prompts/persona-explore.md +6 -0
  57. package/prompts/persona-normal.md +6 -0
  58. package/prompts/persona-plan.md +6 -0
  59. package/provider/anthropic.mjs +127 -4
  60. package/provider/core.mjs +16 -6
  61. package/provider/google.mjs +8 -8
  62. package/provider/list-models.mjs +37 -5
  63. package/provider/normalize.mjs +2 -2
  64. package/provider/responses.mjs +7 -7
  65. package/provider/sse.mjs +6 -5
  66. package/proxy-transport.mjs +5 -3
  67. package/proxy.mjs +2 -1
  68. package/session-guard.mjs +7 -1
  69. package/session-lifecycle.mjs +23 -8
  70. package/session-segments.mjs +1 -0
  71. package/session-slot-claims.mjs +4 -0
  72. package/session-slot-write.mjs +27 -1
  73. package/session-slots-manifest.mjs +25 -4
  74. package/token-ttl.mjs +78 -4
  75. package/tool-docs/advisor.md +24 -5
  76. package/tool-docs/doc_check.md +23 -16
  77. package/tool-docs/ledger.md +2 -2
  78. package/tool-docs/subagent.md +41 -16
  79. package/tool-docs/wait_for.md +15 -6
  80. package/tools/web-deepseek.mjs +26 -11
  81. package/doc-check/version.mjs +0 -84
  82. package/doc-check/width.mjs +0 -87
@@ -1,36 +1,61 @@
1
1
  ONE tool, NINE actions — pick by what you need:
2
2
 
3
3
  - action:'spawn' (DEFAULT): run a sub-agent in its own isolated context — its reads, searches and tool calls never reach your history; only the final report comes back. Spawn MULTIPLE subagents in the SAME response for parallel work — they run concurrently.
4
- - action:'status': NON-BLOCKING progress query — returns immediately, consumes nothing, never fetches. Pass the spawn id for one child (role, state: running|queued|done, model, elapsed, turn, position, plus a touched-files summary — first 5 + a count) or omit it for a pool overview — see what a running child has changed BEFORE cancelling it. Background advisor reviews share this surface (pass the review id — role:'advisor', reviewType/round/elapsedSec, same shape).
5
- - action:'observe': SEE what a running async subagent is DOING right now (progress vs stuck) — pass the id; returns state, turn, the tool(s) executing right now (omitted when none), and the last N one-line turn summaries, newest first (default 5, via recent). Readonly; running/queued/done all answer — queued returns a placeholder, done gives the activity summary only, NOT the report (it rides the auto channel). Judge stuck vs progressing BEFORE cancelling or steering.
6
- - action:'send': STEER a running async subagent mid-flight — id + message (a direction, e.g. "check X, don't fixate on Y"). The child consumes it at its next turn boundary as an ORDINARY user instruction (non-interrupting; its discipline is unchanged — guidance, not a deviation waiver); returns {id, status:'delivered', queued}. Only RUNNING async children are targetable — sync/queued/settled/unknown error clearly; if the child settles first, its report carries an 'undelivered' note.
7
- - action:'escalate' (飞刀): hand an implementation task to a STRONGER model from your consult models; it gets WRITE access and does the work itself, then returns a post-op report (what changed, why, verification). Escalate EARLY, when the task calls for stronger hands (complex refactors, intractable bugs, intricate algorithms — or beyond your comfortable ability). Not available in engineering mode (implementation goes through eng-coder). Default-async at depth 0: ack {id, role:'escalate', status:'running'} returns immediately; the report arrives automatically with its mutations merged into your bookkeeping (pooled with the other role-domain spawns).
8
- - action:'cancel': STOP ONE background subagent — the id from the spawn return is REQUIRED (a blanket cancel is unsupported; Ctrl+C stops everything). Running aborts immediately ({id, status:'cancelled'}); queued is removed ({id, status:'cancelled', was:'queued'}; later positions shift). Other children and the session keep running. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first, and prefer scoped recovery — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited. Background advisor reviews cancel the same way (pass the review id — no token is issued for a cancelled review).
9
- - action:'consume-design' (engineering mode, parent side): chain-terminal token consumption — after the delivery is verified and the chain closes out, consume this design's token slot. Selectors — pass exactly ONE: designId (a single slot; optional for a single-design session), designIds (an explicit non-empty list), expired:true (every expired slot), or olderThanDays:N (every slot older than N days). A consumed slot mechanically rejects further spawns for the same designId — new work (deviation fixes included) needs a fresh design review and token. Idempotent (unknown/already-consumed = a no-op notice); batch calls report per-slot lines + a `consumed N` count line. Do NOT call while the chain is still open — fix rounds reuse the same slot.
10
- - action:'design-slots' (engineering mode, parent side): READ-ONLY inventory of this session's approved design token slots — one line per slot (designId, live|expired, ageDays, expiresAt, batch, designToken), oldest first, ending with a total/live/expired count line. Nothing is written. Use it to see what still holds a slot before consuming; the expired / olderThanDays selectors act on the same set.
11
- Every row carries its designToken value (expired ones included — status marks them; a malformed record shows designToken=—). Pass designId to filter to a single slot (trimmed exact match; unknown = refused with the held id list; empty string = no filter). Take designId + designToken verbatim onto the eng-coder spawn — the gate checks exact equality.
12
- - action:'panel': DIAGNOSE + fix the subagent panel (the collapsible blocks the user sees; headless/VS Code degrade to a 'no panel' pool view). view (default): the live blocks (key, role, status: running|done|awaitingDigest) exactly as the user sees them — digested-stuck entries carry digested:true. freeze: pass a digested-stuck block key (e.g. 'eng-coder#9' — {action:'panel', freeze:'role#N'}) to reclaim it into the conversation — passes ONLY for awaitingDigest blocks with no live pool entry and no pending report (a still-pending report would break the digestion order — refused).
4
+ - action:'status': NON-BLOCKING progress query — returns immediately, consumes nothing, never fetches. Pass the spawn id for one child (role, state: running|queued|done, model, elapsed, turn, position, plus a touched-files summary — first 5 + a count) or omit it for a pool overview — see what a
5
+ running child has changed BEFORE cancelling it. Background advisor reviews share this surface (pass the review id — role:'advisor', reviewType/round/elapsedSec, same shape).
6
+ - action:'observe': SEE what a running async subagent is DOING right now (progress vs stuck) — pass the id; returns state, turn, the tool(s) executing right now (omitted when none), and the last N one-line turn summaries, newest first (default 5, via recent). Readonly; running/queued/done all answer
7
+ — queued returns a placeholder, done gives the activity summary only, NOT the report (it rides the auto channel). Judge stuck vs progressing BEFORE cancelling or steering.
8
+ - action:'send': STEER a running async subagent mid-flight — id + message (a direction, e.g. "check X, don't fixate on Y"). The child consumes it at its next turn boundary as an ORDINARY user instruction (non-interrupting; its discipline is unchanged — guidance, not a deviation waiver); returns {id,
9
+ status:'delivered', queued}. Only RUNNING async children are targetable — sync/queued/settled/unknown error clearly; if the child settles first, its report carries an 'undelivered' note.
10
+ - action:'escalate' (飞刀): hand an implementation task to a STRONGER model from your consult models; it gets WRITE access and does the work itself, then returns a post-op report (what changed, why, verification). Escalate EARLY, when the task calls for stronger hands (complex refactors, intractable
11
+ bugs, intricate algorithms — or beyond your comfortable ability). Not available in engineering mode (implementation goes through eng-coder). Default-async at depth 0: ack {id, role:'escalate', status:'running'} returns immediately; the report arrives automatically with its mutations merged into
12
+ your bookkeeping (pooled with the other role-domain spawns).
13
+ - action:'cancel': STOP ONE background subagent — the id from the spawn return is REQUIRED (a blanket cancel is unsupported; Ctrl+C stops everything). Running aborts immediately ({id, status:'cancelled'}); queued is removed ({id, status:'cancelled', was:'queued'}; later positions shift). Other
14
+ children and the session keep running. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first, and prefer scoped recovery — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited. Background advisor reviews cancel
15
+ the same way (pass the review id — no token is issued for a cancelled review).
16
+ - action:'consume-design' (engineering mode, parent side): chain-terminal token consumption — after the delivery is verified and the chain closes out, consume this design's token slot. Selectors — pass exactly ONE: designId (a single slot; optional for a single-design session), designIds (an
17
+ explicit non-empty list), expired:true (every expired slot), or olderThanDays:N (every slot older than N days). A consumed slot mechanically rejects further spawns for the same designId — new work (deviation fixes included) needs a fresh design review and token. Idempotent
18
+ (unknown/already-consumed = a no-op notice); batch calls report per-slot lines + a `consumed N` count line. Do NOT call while the chain is still open — fix rounds reuse the same slot.
19
+ - action:'design-slots' (engineering mode, parent side): READ-ONLY inventory of this session's approved design token slots — one line per slot (designId, live|expired, ageDays, expiresAt, batch, designToken), oldest first, ending with a total/live/expired count line. Nothing is written. Use it to
20
+ see what still holds a slot before consuming; the expired / olderThanDays selectors act on the same set.
21
+ Every row carries its designToken value (expired ones included — status marks them; a malformed record shows designToken=—). Pass designId to filter to a single slot (trimmed exact match; unknown = refused with the held id list; empty string = no filter). Take designId + designToken verbatim onto
22
+ the eng-coder spawn — the gate checks exact equality.
23
+ - action:'panel': DIAGNOSE + fix the subagent panel (the collapsible blocks the user sees; headless/VS Code degrade to a 'no panel' pool view). view (default): the live blocks (key, role, status: running|done|awaitingDigest) exactly as the user sees them — digested-stuck entries carry digested:true.
24
+ freeze: pass a digested-stuck block key (e.g. 'eng-coder#9' — {action:'panel', freeze:'role#N'}) to reclaim it into the conversation — passes ONLY for awaitingDigest blocks with no live pool entry and no pending report (a still-pending report would break the digestion order — refused).
13
25
 
14
26
  Why delegate? The child's noise never reaches your window — delegation keeps your context lean and the child focused; parallel children save wall-clock time, and coder/eng-coder children self-review (verify + advisor) before their reports arrive.
15
27
 
16
28
  Available roles (exposed per mode; the role is REQUIRED for action:'spawn' — a missing or misspelled role is refused):
17
29
  - explore — read-only search & analysis (read/search family); no git context — evidence comes from reads and the task book; its report must list what it searched and what it did NOT find. Say quick / medium / thorough (default medium).
18
30
  - plan — read-only planning; NEVER edits; returns a step-by-step plan.
19
- - coder — full implementation; the parent's complete read/write/execute toolset plus verify and advisor self-review. Its report carries a delivery table, one row per requirement (Done / Simplified / Not done — no deferred column). Normal mode: may optionally carry `batchDoc` — binds this spawn's §5 record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file.
20
- - eng-coder — engineering mode only (replaces coder): same toolset plus the design-driven methodology overlay; its delivery protocol (implementation → audit → self-fix → advisor re-review → converged delivery) runs fully inside the child. REQUIRES a valid designToken from a passed advisor(type='design') review — pass the exact value from that review, or copy it verbatim from the action:'design-slots' row (the text after designToken=): missing or mismatched = the spawn is refused, and an expired token is refused with expired named (its reply also echoes a designId — pass it: required to pick between concurrent designs, optional for one; echo it back in the delivery report). REQUIRES a batchDoc — the batch record path, the task book this spawn implements (refused without it or when it does not resolve to a readable file).
21
- - eng-designer — engineering mode only: the SOLE author of the requirements/design documents and of the batch record's design section. Writes no implementation code, does not edit prompts, does not fire reviews, needs NO designToken. Surveys on its own; may only spawn read-only explore children (sync, ≤6 per batch). REQUIRES a batchDoc (same gate as eng-coder).
31
+ - coder — full implementation; the parent's complete read/write/execute toolset plus verify and advisor self-review. Its report carries a delivery table, one row per requirement (Done / Simplified / Not done — no deferred column). Normal mode: may optionally carry `batchDoc` — binds this spawn's §5
32
+ record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file.
33
+ - eng-coder — engineering mode only (replaces coder): same toolset plus the design-driven methodology overlay; its delivery protocol (implementation → audit → self-fix → advisor re-review → converged delivery) runs fully inside the child. REQUIRES a valid designToken from a passed
34
+ advisor(type='design') review — pass the exact value from that review, or copy it verbatim from the action:'design-slots' row (the text after designToken=): missing or mismatched = the spawn is refused, and an expired token is refused with expired named (its reply also echoes a designId — pass it:
35
+ required to pick between concurrent designs, optional for one; echo it back in the delivery report). REQUIRES a batchDoc — the batch record path, the task book this spawn implements (refused without it or when it does not resolve to a readable file).
36
+ - eng-designer — engineering mode only: the SOLE author of the design documents and of the batch record's design section (requirement docs stay with the main agent). Writes no implementation code, does not edit prompts, does not fire reviews, needs NO designToken. Surveys on its own; may only spawn
37
+ read-only explore children (sync, ≤6 per batch). REQUIRES a batchDoc (same gate as eng-coder).
22
38
 
23
- Task book (every spawn role): four core structured parameters — goal (goal & why), knownFacts (the facts you already verified), acceptance (machine-verifiable criteria), reportFormat (delivery-report format) — plus ONE fifth parameter named by role: designPoints (design points & forbidden scope) for eng-coder/eng-designer, scope (scope & forbidden scope — what to read / what not to touch) for explore/plan/coder. All five are required: a missing, blank or non-string value is refused with the parameter named, and one role's fifth name never substitutes for the other's. They are composed into the child's brief in that order. A free-text task is an optional supplement, never the whole brief; round (initial|fix) is mandatory for eng-coder/eng-designer. An eng-coder spawn also carries designToken — the write credential (the exact value from the review, or copied verbatim from the action:'design-slots' row, the text after designToken=): missing or mismatched = the spawn is refused.
39
+ Task book (normal-mode roles: explore / plan / coder): four core structured parameters — goal (goal & why), knownFacts (the facts you already verified), acceptance (machine-verifiable criteria), reportFormat (delivery-report format)
40
+ — plus scope (scope & forbidden scope — what to read / what not to touch).
41
+ All five are required: a missing, blank or non-string value is refused with the parameter named.
42
+ A free-text task is an optional supplement, never the whole brief.
43
+ Engineering roles (eng-coder / eng-designer) take their task book from the batch record: batchDoc (the record path) and round (initial|fix), plus files / dependsOn for scheduling; the child reads §2 of that record itself — no other task text is passed.
44
+ An eng-coder spawn also carries designToken — the write credential (the exact value from the review, or copied verbatim from the action:'design-slots' row, the text after designToken=), and designId when several designs are open: missing or mismatched = the spawn is refused.
24
45
 
25
46
  Pass one parameter per field — contents only (the runtime adds the labels):
26
- subagent(role="eng-coder", round="initial", goal="…", knownFacts="…", designPoints="…", acceptance="…", reportFormat="…", batchDoc="<batch record>", designToken="…")
47
+ subagent(role="eng-coder", round="initial", batchDoc="<batch record>", designToken="…", files=[…])
27
48
  subagent(role="explore", goal="…", knownFacts="…", scope="…", acceptance="…", reportFormat="…")
28
49
 
29
50
  Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/eng-designer/eng-coder — off-mode roles are refused mechanically (enums filtered at assembly; the spawn gate re-checks).
30
51
 
31
- Async spawn: top-level spawns are async by default — never pass async:false at depth 0 (the spawn returns {id, status:"running"} immediately; the report arrives automatically; if your next step needs it, end the turn and let the digest deliver it — do not poll; the explicit async:false is a mechanism parameter, not for model use). Inside subagents (depth>0) spawns are always synchronous. Async spawns are pooled per role domain — 4 concurrent eng-coders plus 4 concurrent other-role spawns by default (agent.poolLimits overrides both); a full domain queues further spawns with a position while the other keeps starting.
52
+ Async spawn: top-level spawns are async by default — never pass async:false at depth 0 (the spawn returns {id, status:"running"} immediately; the report arrives automatically; if your next step needs it, end the turn and let the digest deliver it — do not poll; the explicit async:false is a
53
+ mechanism parameter, not for model use). Inside subagents (depth>0) spawns are always synchronous. Async spawns are pooled per role domain — 4 concurrent eng-coders plus 4 concurrent other-role spawns by default (agent.poolLimits overrides both); a full domain queues further spawns with a position
54
+ while the other keeps starting.
32
55
 
33
- Task scheduling: declare files (the file paths this task will modify) and dependsOn (ids from prior async returns whose outcome this task needs) to let the scheduler order your spawns. Overlapping-file tasks serialize and dependents start in order: a conflicting/dependent spawn queues (position/reason) and auto-starts when the conflict clears / the dependency settles (cancel to drop). A cancelled/failed dependency leaves it queued marked "dependency cancelled" until you decide (AUTO sessions auto-start). Unknown ids error; a consumed id counts as satisfied. Omit both for a plain immediate spawn (no scheduler involvement).
56
+ Task scheduling: declare files (the file paths this task will modify) and dependsOn (ids from prior async returns whose outcome this task needs) to let the scheduler order your spawns. Overlapping-file tasks serialize and dependents start in order: a conflicting/dependent spawn queues
57
+ (position/reason) and auto-starts when the conflict clears / the dependency settles (cancel to drop). A cancelled/failed dependency leaves it queued marked "dependency cancelled" until you decide (AUTO sessions auto-start). Unknown ids error; a consumed id counts as satisfied. Omit both for a plain
58
+ immediate spawn (no scheduler involvement).
34
59
 
35
60
  Writing the prompt:
36
61
  - The child starts with zero context — brief it like a colleague who just walked in: goal, what you already know, the specifics.
@@ -1,15 +1,24 @@
1
- Wait until a condition becomes true — polls the condition every interval_ms and returns as soon as it holds or the timeout_ms ceiling passes. Use it ONLY for genuinely asynchronous waits: an async subagent/consult still settling, a file appearing, a port opening. Synchronous tools (advisor, blocking subagent spawn) return when done — waiting after them is NOT needed and wastes time.
1
+ Wait until a condition becomes true — polls every interval_ms and returns as soon as it holds or timeout_ms passes.
2
+
3
+ **Use it only for states that cannot announce themselves**: a file appearing, a port opening, a background bash or browser task finishing. Nothing delivers those into the session, so a blocking wait is the only way to learn about them.
4
+
5
+ **Never use it to wait for a subagent or a review report.** Those arrive on their own when the session is idle — the act of waiting for one is simply ending the turn.
6
+
7
+ `wait_for "subagent id:N done"` and `wait_for "advisor settled"` park the session for the whole budget, poll hundreds of times, and learn nothing you would not have been handed anyway.
8
+
9
+ Measured in practice: two such waits in one session burned ~4.5 minutes and 210 polls on reports that were already on their way. The conditions stay supported (a background task genuinely may need joining), but reaching for them on a child is the classic misuse.
2
10
 
3
11
  Parameters:
4
12
  - condition (required): a semantic condition expression:
5
- - `advisor settled` — an in-flight advisor review session has finished (async advisor-role children settled)
6
- - `subagent id:N done` — async subagent N (the id your async spawn ack returned) has settled
7
- - `consult done` — every consult_start session has drained
13
+ - `advisor settled` — an in-flight advisor review session has finished — **discouraged**: the report is delivered automatically; end the turn instead
14
+ - `subagent id:N done` — async subagent N (the id your async spawn ack returned) has settled — **discouraged**, same reason
15
+ - `consult done` — every consult_start session has drained — the verdict text is delivered automatically; wait only when a synchronous caller cannot proceed without it
8
16
  - `bash id:N done` — background bash task N (the id from the bash async ack, `bash#N`) has finished — settled, or already left the pool (nothing left to wait on)
9
17
  - `browser id:N done` — a background browser task N (the id from the ack, `browser#N`) has settled — finished, or already left the pool (nothing left to wait on)
10
18
  - `file exists:path` — the file at path exists (path relative to cwd)
11
19
  - `port open:N` — something is listening on 127.0.0.1 port N
12
- - The agent-internal conditions (advisor / subagent / consult / bash) apply to ASYNC sessions only — a synchronous call already completed before it returned and has nothing to wait for. An unknown condition is an explicit error (`wait_for: unsupported condition "..."` — the supported forms are listed above), never a silent wait.
20
+ - The agent-internal conditions (advisor / subagent / consult / bash) apply to ASYNC sessions only — a synchronous call already completed before it returned and has nothing to wait for.
21
+ - An unknown condition is an explicit error (`wait_for: unsupported condition "..."` — the supported forms are listed above), never a silent wait.
13
22
  - interval_ms: poll interval (default 1000, floor 100 — polling never busy-spins)
14
23
  - timeout_ms: overall ceiling (default 30000; config.json `agent.waitForTimeoutMs` overrides the default; hard cap 600000 like the execute tool)
15
24
 
@@ -21,4 +30,4 @@ Returns:
21
30
  Notes:
22
31
  - Read-only and non-destructive — it only observes (agent pools, the filesystem, a local port probe).
23
32
  - Blocking by design — call it ALONE in a turn, not batched with calls that depend on its result.
24
- - Do not use it to wait after synchronous tools (advisor / blocking subagent spawn / verify return only when done).
33
+ - This is the one blocking call an agent usually should not reach for. Before calling it, ask: **will this state announce itself to me anyway?** If yes (a subagent report, a review digest, a timer), end the turn and let it arrive.
@@ -9,8 +9,9 @@
9
9
  *
10
10
  * The tools never fail because of this backend: every export returns a pure value
11
11
  * (`{ status: "ok" | "empty" | "failed" | "unavailable" }`) and never throws.
12
- * 401/402/403/404 trip a per-agent breaker (WeakSet); 408/429/5xx retry once (500 ms);
13
- * everything else falls back to the caller's existing chain.
12
+ * 401/402/403 trip a per-agent breaker for `BREAKER_TTL_MS` (10 min — auto-unseals, no restart);
13
+ * 404 does NOT trip (the target page is missing — not a credential failure, D-WB3); 408/429/5xx
14
+ * retry once (500 ms); everything else falls back to the caller's existing chain.
14
15
  */
15
16
  import { URL } from "node:url"
16
17
  import { truncate, isPrivateHost } from "./shared.mjs"
@@ -27,7 +28,8 @@ const DEEPSEEK_HOST = "api.deepseek.com"
27
28
  const TIMEOUT_MS = 15_000
28
29
  const RETRY_DELAY_MS = 500 // D-WB5: fixed single backoff (no Retry-After parsing)
29
30
  const COST_PER_CALL = 0.00075 // $0.75 / 1000 successful requests — `count` does not change the price
30
- const TRIP_STATUS = new Set([401, 402, 403, 404])
31
+ const TRIP_STATUS = new Set([401, 402, 403]) // 404 = 目标页面的状态,不是凭据失效(D-WB3)
32
+ const BREAKER_TTL_MS = 600_000 // 熔断限时 10 分钟——到期自动放行(改完 key 无需重启会话)
31
33
  const RETRY_STATUS = (() => {
32
34
  const codes = new Set([408, 429])
33
35
  for (let code = 500; code <= 599; code++) codes.add(code)
@@ -35,11 +37,16 @@ const RETRY_STATUS = (() => {
35
37
  })()
36
38
 
37
39
  let _transport = proxyFetch
38
- let _breaker = new WeakSet()
40
+ let _now = Date.now
41
+ let _breaker = new WeakMap() // agent → expiresAtMs(单域——search ∥ fetch 共享,D-WB3)
39
42
 
40
- /** Test seam (CONFIG.md §6.2 ③ — paired): swap the transport; reset also clears the breaker. */
43
+ /** Test seam (CONFIG.md §6.2 ③ — paired): swap the transport ∥ the clock; reset also clears the breaker. */
41
44
  export function _setDeepseekTransportForTest(fn) { _transport = typeof fn === "function" ? fn : proxyFetch }
42
- export function _resetDeepseekTransportForTest() { _transport = proxyFetch; _breaker = new WeakSet() }
45
+ export function _setDeepseekClockForTest(fn) { _now = typeof fn === "function" ? fn : Date.now }
46
+ export function _resetDeepseekTransportForTest() { _transport = proxyFetch; _now = Date.now; _breaker = new WeakMap() }
47
+
48
+ /** 熔断名单只读面(用例断言 404 零命中——D-WB3)。 */
49
+ export const _TRIP_STATUS_FOR_TEST = Object.freeze([...TRIP_STATUS])
43
50
 
44
51
  /** `local` skips the DeepSeek backend; anything else (missing ∥ misspelled ∥ non-string) = `auto` (fail-open). */
45
52
  export function backendMode(cfg, key) {
@@ -66,11 +73,19 @@ function deepseekChannel(cfg) {
66
73
  return null
67
74
  }
68
75
 
69
- const tripped = (agent) => agent !== null && typeof agent === "object" && _breaker.has(agent)
76
+ /** 熔断未到期?到期 ⇒ 删条目并放行(判活读时钟——改完 key 不必重启会话)。 */
77
+ function tripped(agent) {
78
+ if (agent === null || typeof agent !== "object") return false
79
+ const expiresAt = _breaker.get(agent)
80
+ if (typeof expiresAt !== "number") return false
81
+ if (expiresAt > _now()) return true
82
+ _breaker.delete(agent)
83
+ return false
84
+ }
70
85
 
71
- /** Break the backend for this agent identity (single domain — search ∥ fetch share it, D-WB3). */
86
+ /** Break the backend for this agent identity for `BREAKER_TTL_MS` (single domain — search ∥ fetch share it, D-WB3). */
72
87
  function trip(agent) {
73
- if (agent !== null && typeof agent === "object") _breaker.add(agent)
88
+ if (agent !== null && typeof agent === "object") _breaker.set(agent, _now() + BREAKER_TTL_MS)
74
89
  }
75
90
 
76
91
  /** One POST. Returns `{ kind: "ok", data }` | `{ kind: "http", status }` | `{ kind: "net", timeout }` | `{ kind: "json" }`. */
@@ -95,7 +110,7 @@ async function postOnce(url, body, apiKey, proxyUri) {
95
110
  } finally { clearTimeout(timer) }
96
111
  }
97
112
 
98
- /** One logical call: retry once (408/429/5xx), trip on 401/402/403/404, otherwise fall back. */
113
+ /** One logical call: retry once (408/429/5xx), trip on 401/402/403, otherwise fall back. */
99
114
  async function request(url, body, apiKey, proxyUri, tool, agent) {
100
115
  const started = Date.now()
101
116
  let r = await postOnce(url, body, apiKey, proxyUri)
@@ -107,7 +122,7 @@ async function request(url, body, apiKey, proxyUri, tool, agent) {
107
122
  if (r.kind === "http" && TRIP_STATUS.has(r.status) && agent !== null && typeof agent === "object") {
108
123
  trip(agent)
109
124
  // Tripping line only (transient fallbacks stay quiet — ⑤).
110
- logEvent("web:deepseek", { tool, status: r.status, tripped: true, ms: Date.now() - started })
125
+ logEvent("web:deepseek", { tool, status: r.status, tripped: true, ttlMs: BREAKER_TTL_MS, ms: Date.now() - started })
111
126
  }
112
127
  const reason = r.kind === "http" ? `http ${r.status}`
113
128
  : r.kind === "net" ? (r.timeout ? "timeout" : "network")
@@ -1,84 +0,0 @@
1
- /**
2
- * doc-check/version.mjs — 引擎版本一致性的**单源**(纯函数 + 常量;两个消费面共用一个判据)。
3
- *
4
- * 权威规格 = `docs/core/design/DOC-DISCIPLINE.md` §3.20「引擎版本一致」(判据 / 比较 / 裁定 / 保守方向;
5
- * 本档只实现,不重述口径)。消费面两处:工具面 `agent-tools/doc-check.mjs` 的 gate 分支(转口导出、
6
- * 导出面不变)∥ 结构门 `agent-tools/batch-gate.mjs`(batch 两机械点)——两处不得各持一份清单。
7
- *
8
- * 问题形态:`thincoder-core/doc-check/**` 是 ESM,进程启动时加载即缓存;引擎档在启动后被改写 ⇒
9
- * 本会话仍跑旧模块 ⇒ 假阳(旧误报消不掉)/ 假绿(引擎变严时旧版放过)。故 gate 在跑判据前先判版本。
10
- *
11
- * `ENGINE_FILES` 六档 = 判据面:三判据档 + 本档(版本函数单源,自身入清单 = fail-closed)+ 工具面 +
12
- * 描述档。路径按本档 `import.meta.url` 解析到产品核根,**不随 target 变**。
13
- */
14
- import { statSync } from "node:fs"
15
- import { resolve } from "node:path"
16
- import { fileURLToPath } from "node:url"
17
-
18
- /** 产品核根(版本清单基线——本档住 `doc-check/`,上跳一级)。 */
19
- const CORE_ROOT = fileURLToPath(new URL("..", import.meta.url))
20
-
21
- /** 引擎六档 = 版本一致性的判据面(§3.20「引擎版本一致」· 判据)。 */
22
- export const ENGINE_FILES = [
23
- "doc-check/index.mjs",
24
- "doc-check/anchors.mjs",
25
- "doc-check/width.mjs",
26
- "doc-check/version.mjs",
27
- "agent-tools/doc-check.mjs",
28
- "tool-docs/doc_check.md",
29
- ]
30
-
31
- /**
32
- * 引擎版本一致性(纯函数——时钟 ∥ stat 可注 ⇒ 可测,不碰产品档 mtime):
33
- * 逐档 `mtimeMs` 与**本进程启动时刻**(`nowMs − uptimeSec × 1000`)比较——晚于启动 ⇒ 该档可能晚于模块加载 ⇒ stale。
34
- * 不可 `stat` 的档同样入 stale(fail-closed,不静默忽略)。`mtime ≤ 启动` 一定不比缓存新;
35
- * `mtime > 启动` 只是**可能**晚于加载 —— 取 fail-closed 方向(误判代价 = 一次会话重启)。
36
- * @param {{nowMs?:number, uptimeSec?:number, stat?:Function}} [o] 注入缝(缺省 = 真时钟 / 真 `statSync`)
37
- * @returns {{startMs:number, stale:{file:string, mtimeMs:number|null}[]}} `file` = 相对 `thincoder-core/` 的正斜杠路径
38
- */
39
- export function staleEngineFiles({ nowMs = Date.now(), uptimeSec = process.uptime(), stat = statSync } = {}) {
40
- const startMs = nowMs - uptimeSec * 1000
41
- const stale = []
42
- for (const file of ENGINE_FILES) {
43
- let mtimeMs = null
44
- try { mtimeMs = stat(resolve(CORE_ROOT, file)).mtimeMs } catch { stale.push({ file, mtimeMs: null }); continue }
45
- if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs)) { stale.push({ file, mtimeMs: null }); continue }
46
- if (mtimeMs > startMs) stale.push({ file, mtimeMs })
47
- }
48
- return { startMs, stale }
49
- }
50
-
51
- /** stale 列表形(`<文件> mtime <ISO 8601>`,`;` 连列)——gate ERROR 行 ∥ report 注记行共用一处格式。 */
52
- function staleParts(stale) {
53
- const when = (t) => new Date(t).toISOString()
54
- return stale.map((s) => `${s.file} mtime ${s.mtimeMs === null ? "不可读(stat 失败)" : when(s.mtimeMs)}`)
55
- }
56
-
57
- /** 引擎版本 ERROR 报面(固定句形):多档 stale ⇒ 逐档 `<文件> mtime <时间>` 以 `;` 连列——**不产 PASS**。 */
58
- export function gateEngineError({ startMs, stale }) {
59
- const when = (t) => new Date(t).toISOString()
60
- return [
61
- `DOC-CHECK GATE: ERROR(引擎已更新(${staleParts(stale).join(";")} > 进程启动 ${when(startMs)}),请重启会话后复跑)`,
62
- "原因:本会话进程加载的引擎模块早于盘上引擎档——旧引擎读数不可信(假阳:旧误报消不掉 ∥ 假绿:引擎变严时旧版放过)",
63
- "处置:重启会话后复跑 gate(`report` 不裁定、不产 verdict(陈旧时只在首行前加注记))",
64
- ].join("\n")
65
- }
66
-
67
- /** 报告态引擎陈旧注记(纯函数):stale 空 ⇒ `null`;非空 ⇒ 一行注记(逐字单源 = `DOC-DISCIPLINE.md`
68
- * §3.20「gate 裁定 · report 注记」——多档以 `;` 连列,与 `gateEngineError` 同形)。 */
69
- export function reportEngineNote({ startMs, stale } = {}) {
70
- if (!Array.isArray(stale) || stale.length === 0) return null
71
- const when = (t) => new Date(t).toISOString()
72
- return `[doc_check note] 引擎陈旧(${staleParts(stale).join(";")} > 进程启动 ${when(startMs)})——本次读数由会话启动时加载的旧引擎产出,不可作为交付判据;重启会话后复跑。`
73
- }
74
-
75
- /** 报面 + 注记:有注记 ⇒ 注记行 + 原报面;无 ⇒ 原报面逐字返回(三不改:既有行 ∥ verdict ∥ PASS 语义)。 */
76
- export function withEngineNote(report, version) {
77
- const note = reportEngineNote(version ?? {})
78
- return note ? `${note}\n${report}` : report
79
- }
80
-
81
- /** 报面首行取形(唯一机判串 = 首行 `DOC-CHECK GATE:` 前缀——消费面共用一个取法,不各自 split)。 */
82
- export function firstVerdictLine(report) {
83
- return String(report ?? "").split("\n")[0] ?? ""
84
- }
@@ -1,87 +0,0 @@
1
- /**
2
- * doc-check/width.mjs — 文档纪律机检的产品档位(行宽谓词族 + 行宽判据——实现单源)。
3
- *
4
- * 口径单源 = `docs/core/design/DOC-DISCIPLINE.md` §3.18 条目 Q(度量 / 豁免类);
5
- * 产品内建裁定 = 同档 §3.20 条目 S——谓词单源随迁:本仓 `scripts/doc-check-width.mjs` 改述为
6
- * **再出口缝**(薄档 · 零逻辑 · 逐名再出口),盘上不留第二份谓词实现。
7
- * 度量 = `[...line].length`(CJK 计 1——不引显示宽度 / 字体);阈值 = 调用方的内置常量
8
- * (`DOC_CHECK_BUILTINS.lineWidth`)——不读声明面,也不扫目录:**判定集 = 调用方传入的文件清单**。
9
- * 豁免五类 = 表格行 ∥ fenced 块 ∥ 可执行行 ∥ 连续引文 ∥ 区带:**只认结构谓词**,
10
- * 无行级白名单(按行号 / 注释声明的豁免不入判)。
11
- * 边界:本档零门禁 / 零退出码 / 零阻断——报告态由 `agent-tools/doc-check.mjs` 呈现。
12
- */
13
- import { readFileSync } from "node:fs"
14
-
15
- /** 表格行谓词(markdown 表格行结构性不可折行——行宽豁免,与锚检查同源)。 */
16
- export const isTableRow = (l) => /^\s*\|.*\|\s*$/.test(String(l))
17
-
18
- /** 围栏标记(行首可选空白 + 三反引号 ∥ 三波浪号 ⇒ 标记字符;非围栏行 ⇒ null)——**单源**:
19
- * `isFenceLine` 与两处块扫描(本节行宽 ∥ `./anchors.mjs` 锚抽取)同取此式,使「同形围栏行止」只此一解
20
- * (同一模块内同符两义 = 单源可漂的结构口子)。 */
21
- export function fenceMark(l) {
22
- const m = /^\s*(`{3,}|~{3,})/.exec(String(l))
23
- return m ? m[1][0] : null
24
- }
25
-
26
- /** 围栏行谓词(单源 = `fenceMark`)——围栏行自身与块内整行均豁免。 */
27
- export const isFenceLine = (l) => fenceMark(l) !== null
28
-
29
- /** 引文块行谓词(行首可选空白 + `>`)——连续 `>` 行整块豁免(引证须逐字连续、不可折,同表行理据)。 */
30
- export const isQuoteLine = (l) => /^\s*>/.test(String(l))
31
-
32
- /** 区带头谓词(§3.15 谓词面):标题文本去首部编号(`^\d+(?:\.\d+)*[.、]?\s*`)后——等于区带名 ∨ 以
33
- * 「区带名 + (∕(」起(容括注后缀,如 `变更记录(续)`);闭集外一律不纳(fail-closed)。 */
34
- export function isZoneHead(headingText, zones = []) {
35
- const text = String(headingText).replace(/^\d+(?:\.\d+)*[.、]?\s*/, "")
36
- return zones.some((z) => typeof z === "string" && z !== "" && (text === z || text.startsWith(z + "(") || text.startsWith(z + "(")))
37
- }
38
-
39
- /** 行内可执行坐标(命令 / 搜索模式串码段)——整行豁免(证据与命令保字面,与锚判据同源)。 */
40
- export function isExecutableLine(line) {
41
- const l = String(line)
42
- if (/`[^`\n]*&&[^`\n]*`/.test(l)) return true // 命令链(cd … && …)
43
- if (/`[^`\n]*\b(?:cd|node|npm|npx|grep|rg|git)\s[^`\n]*`/.test(l)) return true // 码段内命令词
44
- if (/`[^`\n]*(?:\[\^|\\\.|\(\?<|\(\?:|\{\d)/.test(l)) return true // 码段内搜索模式串片段
45
- return false
46
- }
47
-
48
- /** 行内位置是否在反引号码段内(引述面不判)。 */
49
- export function inCodeSpan(line, idx) {
50
- return (String(line).slice(0, idx).match(/`/g) ?? []).length % 2 === 1
51
- }
52
-
53
- /** 行宽判据(阈值 = 调用方内置常量 `lineWidth`):`[...line].length` > 阈值 ∧ 非表格行 ∧ 非 fenced 块内
54
- * ∧ 非可执行行 ∧ 非连续引文 ∧ 非区带内 ⇔ 命中(口径逐字 = §3.18 条目 Q)。
55
- * 单遍扫描携标题栈(`^(#{1,6})\s` 行入栈——同级 ∥ 更高级出栈):行所属标题链任一为区带头 ⇒ 豁免。
56
- * 围栏未闭合 ⇒ 至档尾整块免(fail-open 方向——§3.18 残余登记 ①);`exemptZones` 缺省 ∥ 空集 ⇒ 零豁免。
57
- * `files`(必填)= **判定集**(调用方传入的档清单——本档不遍历任何目录;空集 ⇒ 零命中)。 */
58
- export function checkDocWidths({ lineWidth, exemptZones = [], files = [] } = {}) {
59
- const zones = (Array.isArray(exemptZones) ? exemptZones : []).filter((z) => typeof z === "string" && z !== "")
60
- const hits = []
61
- for (const f of files) {
62
- let text = ""
63
- try { text = readFileSync(f, "utf8") } catch { continue }
64
- const stack = [] // 标题栈 —— { level, exempt };exempt = 自身 ∥ 祖先命中区带
65
- let fence = null // 围栏标记(` ∥ ~);null = 不在块内
66
- text.split("\n").forEach((l, i) => {
67
- const mark = fenceMark(l)
68
- if (mark !== null) {
69
- if (fence === null) fence = mark
70
- else if (mark === fence) fence = null
71
- return // 围栏行自身免
72
- }
73
- if (fence !== null) return // 块内整块免(未闭合 ⇒ 至档尾免)
74
- const hm = /^(#{1,6})\s+(.*)$/.exec(l)
75
- if (hm) {
76
- while (stack.length && stack[stack.length - 1].level >= hm[1].length) stack.pop()
77
- const self = isZoneHead(hm[2].trim(), zones)
78
- stack.push({ level: hm[1].length, exempt: self || (stack[stack.length - 1]?.exempt ?? false) })
79
- return
80
- }
81
- const inZone = stack[stack.length - 1]?.exempt === true
82
- const len = [...l].length
83
- if (len > lineWidth && !isTableRow(l) && !isExecutableLine(l) && !isQuoteLine(l) && !inZone) hits.push({ file: f, line: i + 1, len })
84
- })
85
- }
86
- return hits
87
- }