@thincoder/core 0.10.13 → 0.11.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.
Files changed (164) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/advisor/messages.mjs +4 -2
  3. package/advisor/notice.mjs +1 -1
  4. package/advisor/project-context.mjs +121 -11
  5. package/agent/completion.mjs +13 -1
  6. package/agent/digest-account.mjs +73 -46
  7. package/agent/dispatch-run.mjs +1 -1
  8. package/agent/dispatch.mjs +29 -21
  9. package/agent/family-tools.mjs +9 -5
  10. package/agent/helpers.mjs +1 -94
  11. package/agent/offload.mjs +88 -0
  12. package/agent/record-results.mjs +1 -6
  13. package/agent/run-stages.mjs +69 -48
  14. package/agent/run-start.mjs +8 -8
  15. package/agent/setup.mjs +3 -11
  16. package/agent/suspension.mjs +1 -1
  17. package/agent/turn-loop.mjs +32 -3
  18. package/agent/write-face.mjs +99 -0
  19. package/agent/write-gate.mjs +354 -5
  20. package/agent-tools/advisor.mjs +127 -11
  21. package/agent-tools/async-settle.mjs +2 -2
  22. package/agent-tools/bash-async.mjs +8 -2
  23. package/agent-tools/batch-closeout.mjs +206 -0
  24. package/agent-tools/batch-gate.mjs +287 -73
  25. package/agent-tools/batch-ledger-bind.mjs +183 -0
  26. package/agent-tools/batch-ledger-link.mjs +73 -0
  27. package/agent-tools/batch-lifecycle.mjs +113 -367
  28. package/agent-tools/batch-paths.mjs +45 -40
  29. package/agent-tools/batch-read.mjs +1 -1
  30. package/agent-tools/batch-reopen.mjs +142 -0
  31. package/agent-tools/batch-skeleton.mjs +275 -80
  32. package/agent-tools/batch.mjs +243 -50
  33. package/agent-tools/context.mjs +6 -3
  34. package/agent-tools/design-slots.mjs +24 -24
  35. package/agent-tools/design-token.mjs +96 -15
  36. package/agent-tools/doc-check.mjs +18 -2
  37. package/agent-tools/eng.mjs +1 -1
  38. package/agent-tools/escalate-async.mjs +3 -3
  39. package/agent-tools/manifest.mjs +8 -5
  40. package/agent-tools/read-history.mjs +5 -5
  41. package/agent-tools/review-facts.mjs +31 -13
  42. package/agent-tools/spawn-gates.mjs +10 -14
  43. package/agent-tools/ssh-session.mjs +120 -111
  44. package/agent-tools/ssh-transfer.mjs +210 -0
  45. package/agent-tools/ssh-transport.mjs +277 -46
  46. package/agent-tools/subagent-actions-query.mjs +1 -1
  47. package/agent-tools/subagent-actions.mjs +1 -1
  48. package/agent-tools/subagent-async.mjs +3 -2
  49. package/agent-tools/subagent-scheduler.mjs +43 -9
  50. package/agent-tools/subagent-spawn.mjs +68 -9
  51. package/agent-tools/subagent.mjs +16 -14
  52. package/client-identity.mjs +22 -6
  53. package/config-presets.mjs +5 -3
  54. package/context.mjs +34 -3
  55. package/declaration.mjs +2 -2
  56. package/doc-check/anchors.mjs +128 -28
  57. package/doc-check/index.mjs +68 -15
  58. package/doc-check/line-refs.mjs +112 -0
  59. package/doc-check/paragraph.mjs +18 -5
  60. package/explore-distill.mjs +1 -1
  61. package/i18n.mjs +1 -1
  62. package/index-discover.mjs +4 -1
  63. package/ledger-close-proof.mjs +19 -37
  64. package/ledger-cmd.mjs +71 -195
  65. package/ledger-db.mjs +29 -28
  66. package/ledger-migrate.mjs +4 -4
  67. package/ledger-read.mjs +10 -8
  68. package/ledger-surface.mjs +1 -1
  69. package/ledger-tools.mjs +6 -6
  70. package/ledger-variant-notice.mjs +1 -1
  71. package/ledger-write-ops.mjs +373 -0
  72. package/ledger.mjs +2 -2
  73. package/manifest-discovery.mjs +2 -2
  74. package/manifest-schema.mjs +4 -0
  75. package/memory/memory-tool.mjs +5 -2
  76. package/memory/schema.mjs +5 -26
  77. package/package.json +1 -1
  78. package/path-resolve.mjs +217 -0
  79. package/permission.mjs +4 -0
  80. package/prompt-overlays.mjs +5 -0
  81. package/prompts/advisor-design.md +9 -6
  82. package/prompts/advisor-round1.md +4 -3
  83. package/prompts/advisor-round2.md +6 -5
  84. package/prompts/advisor-round3.md +6 -5
  85. package/prompts/common.md +23 -10
  86. package/prompts/consult-base.md +2 -1
  87. package/prompts/discipline-engineering.md +29 -48
  88. package/prompts/discipline-normal.md +10 -5
  89. package/prompts/persona-eng-coder.md +8 -0
  90. package/prompts/persona-eng-designer.md +22 -12
  91. package/prompts/persona-engineering.md +81 -68
  92. package/prompts/persona-plan.md +2 -1
  93. package/prompts/persona-tester.md +55 -0
  94. package/provider/core.mjs +2 -2
  95. package/provider/google.mjs +15 -5
  96. package/provider/rate.mjs +11 -3
  97. package/provider/sse.mjs +15 -4
  98. package/session-identity.mjs +21 -0
  99. package/session-lifecycle.mjs +9 -4
  100. package/session-slots.mjs +9 -14
  101. package/session.mjs +5 -0
  102. package/skip-dirs.mjs +48 -0
  103. package/team-ledger.mjs +4 -4
  104. package/text-budget.mjs +4 -0
  105. package/token-window.mjs +90 -14
  106. package/tool-docs/advisor.md +2 -2
  107. package/tool-docs/apply_patch.md +2 -0
  108. package/tool-docs/bash.md +4 -0
  109. package/tool-docs/batch.md +85 -1
  110. package/tool-docs/browser.md +3 -1
  111. package/tool-docs/code_search.md +3 -1
  112. package/tool-docs/consult_start.md +6 -2
  113. package/tool-docs/consult_stop.md +3 -1
  114. package/tool-docs/delete.md +3 -0
  115. package/tool-docs/doc_check.md +43 -8
  116. package/tool-docs/doc_search.md +5 -1
  117. package/tool-docs/edit.md +22 -1
  118. package/tool-docs/execute.md +7 -1
  119. package/tool-docs/fetch.md +3 -1
  120. package/tool-docs/file_ops.md +3 -1
  121. package/tool-docs/git.md +9 -1
  122. package/tool-docs/goal.md +2 -0
  123. package/tool-docs/grep.md +1 -1
  124. package/tool-docs/hashline_edit.md +2 -0
  125. package/tool-docs/insert_after.md +3 -1
  126. package/tool-docs/ledger.md +5 -5
  127. package/tool-docs/lint.md +1 -0
  128. package/tool-docs/manifest-read.md +2 -1
  129. package/tool-docs/manifest.md +9 -2
  130. package/tool-docs/memory.md +11 -1
  131. package/tool-docs/notify_parent.md +1 -1
  132. package/tool-docs/plan.md +2 -0
  133. package/tool-docs/question.md +1 -1
  134. package/tool-docs/read_history.md +15 -3
  135. package/tool-docs/read_image.md +3 -1
  136. package/tool-docs/recent_changes.md +3 -1
  137. package/tool-docs/repo_outline.md +3 -1
  138. package/tool-docs/settings.md +10 -2
  139. package/tool-docs/skill.md +2 -0
  140. package/tool-docs/ssh.md +30 -17
  141. package/tool-docs/subagent.md +50 -13
  142. package/tool-docs/task.md +2 -0
  143. package/tool-docs/team_ledger.md +1 -1
  144. package/tool-docs/timer.md +7 -0
  145. package/tool-docs/verify.md +13 -1
  146. package/tool-docs/websearch.md +3 -1
  147. package/tool-docs/write.md +2 -0
  148. package/tools/bash.mjs +12 -2
  149. package/tools/edit-batch.mjs +40 -33
  150. package/tools/edit-diff.mjs +69 -1
  151. package/tools/execute.mjs +10 -4
  152. package/tools/file.mjs +17 -27
  153. package/tools/git.mjs +27 -1
  154. package/tools/ops.mjs +2 -1
  155. package/tools/patch.mjs +12 -3
  156. package/tools/process-doc-mark.mjs +139 -0
  157. package/tools/search.mjs +15 -6
  158. package/tools/shared.mjs +24 -1
  159. package/tools/ssh.mjs +48 -7
  160. package/tools/tree.mjs +4 -1
  161. package/tools/write-path.mjs +64 -3
  162. package/traces/trace-store.mjs +4 -3
  163. package/agent/light-round.mjs +0 -63
  164. package/agent/unbooked-pen.mjs +0 -62
@@ -8,17 +8,27 @@ ONE tool, NINE actions — pick by what you need:
8
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
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
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
11
+ bugs, intricate algorithms — or beyond your comfortable ability). Not available in engineering mode (implementation goes through eng-coder).
12
+
13
+ Default-async at depth 0: ack {id, role:'escalate', status:'running'} returns immediately; the report arrives automatically with its mutations merged into
12
14
  your bookkeeping (pooled with the other role-domain spawns).
13
15
  - 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
16
+ 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.
17
+
18
+ Background advisor reviews cancel
15
19
  the same way (pass the review id — no token is issued for a cancelled review).
16
20
  - 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
21
+ 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.
22
+
23
+ Idempotent
18
24
  (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
25
  - 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
26
  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
27
+ Every row carries its designToken value (expired ones included — status marks them; a malformed record shows designToken=—).
28
+
29
+ Pass designId to filter to a single slot (trimmed exact match; unknown = refused with the held id list; empty string = no filter).
30
+
31
+ Take designId + designToken verbatim onto
22
32
  the eng-coder spawn — the gate checks exact equality.
23
33
  - 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
34
  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).
@@ -31,34 +41,61 @@ Available roles (exposed per mode; the role is REQUIRED for action:'spawn' — a
31
41
  - 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
42
  record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file.
33
43
  - 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).
44
+ 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,
45
+
46
+ and an expired token is refused with expired named (its reply also echoes a designId — pass it:
47
+ required to pick between concurrent designs, optional for one;
48
+
49
+ echo it back in the delivery report). REQUIRES a batchDoc — the batch record path, the task book this spawn implements (refused without it, when it does not resolve to a readable file, or when that record already has an in-flight spawn).
50
+
51
+ One batch record serves one concurrent spawn: wait for the occupant to settle, or cancel it.
36
52
  - 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
53
  read-only explore children (sync, ≤6 per batch). REQUIRES a batchDoc (same gate as eng-coder).
54
+ - tester — BOTH modes: the readings seat. It runs the verification commands the task book puts in `goal` and hands the readings back as they stand —
55
+ no verdict, no fix, no document of any kind.
56
+
57
+ Tool face = the read-only tools plus `execute` / `bash` (dependency installs, compiles and fixtures included), minus `verify` (a verdict tool).
58
+
59
+ Run only, never change is a role contract, not a sandbox — `bash` can write, so a tester that edits anything is treated by the parent as a defect.
60
+
61
+ Task book (non-engineering roles: explore / plan / coder / tester): four core structured parameters — goal (goal & why), knownFacts (the facts you already verified), acceptance (machine-verifiable criteria), reportFormat (delivery-report format)
38
62
 
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
63
  — plus scope (scope & forbidden scope — what to read / what not to touch).
41
64
  All five are required: a missing, blank or non-string value is refused with the parameter named.
42
65
  A free-text task is an optional supplement, never the whole brief.
66
+
43
67
  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.
68
+
44
69
  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.
45
70
 
71
+ A record carries one concurrent spawn at a time: a second spawn on a record still in flight is refused, naming the occupant — sequential hand-offs (design → implementation → fix round) pass, because the previous spawn has settled first.
72
+
46
73
  Pass one parameter per field — contents only (the runtime adds the labels):
47
74
  subagent(role="eng-coder", round="initial", batchDoc="<batch record>", designToken="…", files=[…])
48
75
  subagent(role="explore", goal="…", knownFacts="…", scope="…", acceptance="…", reportFormat="…")
49
76
 
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).
77
+ Mode filtering: normal mode exposes explore/plan/coder/tester (eng-coder and eng-designer are refused); engineering mode exposes explore/eng-designer/eng-coder/tester (plan and coder are refused — plan is a normal-mode role, coder is replaced by eng-coder; tester is dispatchable in both modes).
78
+
79
+ The role enum is filtered per mode at assembly, and the spawn gate refuses out-of-mode roles mechanically.
51
80
 
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
81
+ 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;
82
+
83
+ the explicit async:false is a
84
+ mechanism parameter, not for model use). Inside subagents (depth>0) spawns are always synchronous.
85
+
86
+ 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
87
  while the other keeps starting.
55
88
 
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
89
+ 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.
90
+
91
+ Overlapping-file tasks serialize and dependents start in order: a conflicting/dependent spawn queues
92
+ (position/reason) and auto-starts when the conflict clears / the dependency settles (cancel to drop).
93
+
94
+ 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
95
  immediate spawn (no scheduler involvement).
59
96
 
60
97
  Writing the prompt:
61
98
  - The child starts with zero context — brief it like a colleague who just walked in: goal, what you already know, the specifics.
62
99
  - Put exact paths and commands in — do not make it search for what you already know.
63
- - Do not delegate understanding: resolve the paths/line numbers the task hinges on first.
100
+ - Do not delegate understanding: resolve the paths and the symbols the task hinges on first.
64
101
  - Once it runs, leave that scope to it — don't redo its searches in parallel or finish manually midway.
package/tool-docs/task.md CHANGED
@@ -1,7 +1,9 @@
1
1
  Plan and track a task list for complex multi-step work.
2
2
  Each call replaces the entire list.
3
3
  Keep exactly one item in_progress at a time; mark items done as you complete them; never mark done if tests fail or work is partial.
4
+
4
5
  Statuses: pending | in_progress | done — synonyms (completed/finished/complete, todo/open/waiting, active/running/working, …) are accepted and normalized with a warning.
5
6
  IMPORTANT: title is required and must be a non-empty string — items with empty titles are silently dropped.
7
+
6
8
  For cross-session / project-level tracking, use `/ledger`.
7
9
  Returns the updated task list (or the new item's ID on add).
@@ -5,5 +5,5 @@
5
5
  - count:同 query 参数,出 total。
6
6
  - add:title 必填(≤200)∥ detail(≤4000)∥ owner(缺省 = 自己)——status 恒 待讨论。
7
7
  - update:id 必填;title / detail / owner / status 至少一件;status = 迁移表合法后继(待讨论→待设计 ∥ 待设计→在途 ∥ 在途→待核销 ∥ 待核销→已核销 ∥ 未归档→已废弃);已归档(已核销 / 已废弃)⇒ 拒;仓不可改。
8
- - close:id + status(已核销 ∥ 已废弃)——已核销:勾销(待核销)/ 追认核销(待讨论 ∥ 待设计,须正文非空);已废弃 = 撤回(任意未归档态)。
8
+ - close:id + status(已核销 ∥ 已废弃)——已核销:勾销(只自 待核销;其余现态 400,出路 = 先 PATCH 推进到 待核销);已废弃 = 撤回(任意未归档态)。
9
9
  - 登录态门:未登录 ⇒ 明示「未登录」且零请求;401(token 失效)⇒ 「登录已失效——请重新登录」;不可达 ⇒ 「服务器不可达」(零离线缓存,重试不落旧值)。
@@ -1,13 +1,20 @@
1
1
  Set a timer before you start analyzing code.
2
2
  When the timer fires, a system reminder will be injected suggesting you try running code or adding debug logs.
3
3
  Use this to enforce a thinking budget: you get N seconds to reason, then the timer reminds you to act.
4
+
4
5
  Beyond a one-shot countdown, three schedule forms are available, one per call (`seconds`, `at`, `every` and `daily` are mutually exclusive — passing more than one is a validation error):
6
+
5
7
  `at` takes a local clock time such as "2026-10-13T09:00" and fires once at that moment — a time already past fires immediately.
6
8
  `every` takes a fixed interval in seconds (minimum 60): the first reminder comes N seconds after setting, then every N seconds, until cancelled.
9
+
7
10
  `daily` takes a local "HH:MM" and repeats every day at that time (a time already past today fires tomorrow), until cancelled.
8
11
  All times are local to the host.
12
+
9
13
  `cancel` takes the id from the set confirmation and stops that timer; `list` shows every pending timer with its next due time. Repeating timers occupy one pending slot each until cancelled.
10
14
  Returns the set confirmation — the reminder fires at the deadline.
15
+
11
16
  Delivery: when the session is idle it is delivered on the spot and starts a turn — the idle wake is available on the CLI / VSC / desktop foregrounds and suspension windows (headless is structurally unsupported).
17
+
12
18
  A user turn gets no delivery: while a session turn is running, an expired timer waits for the next delivery round (suspension window / idle latch); other paths keep the step-boundary behavior.
19
+
13
20
  A pending timer survives across runs; at most 8 timers may be pending at once, and further timer calls are rejected until some expire.
@@ -1 +1,13 @@
1
- Generic pre-completion verification gate — language/framework/project agnostic. verify does NOT run any test/verification command for you and does NOT require a test per change. You DECLARE your verification status via the verification argument ({ status: 'passed' | 'failed' | 'skipped', command?, summary?, basis? } — effects per parameter): the declaration is self-reported evidence — verify only mechanically gates on it ('passed' → allowed; 'failed' → blocked; 'skipped' → allowed but needs a reason). 'basis' (optional) is the 'what it stands on' dimension of the declaration — a judgment source, a read file:line coordinate, or an explicit 'unverified' (declaration-only: no format is enforced or machine-checked); a passed / skipped declaration with no basis gets one advisory hint line in the report — never blocking and never changing the gate — while the failed face gets no basis hint (already blocked). Whether/how to verify is described in natural language in the project's AGENTS.md and is decided + executed by you. The report also carries the changed files (git diff), an advisory node --check hint on changed .js/.mjs (not gating), the task list, and a self-review checklist. Call this BEFORE declaring a coding task complete — do not say 'done' until verify passes.
1
+ Generic pre-completion verification gate — language/framework/project agnostic. verify does NOT run any test/verification command for you and does NOT require a test per change.
2
+
3
+ You DECLARE your verification status via the verification argument ({ status: 'passed' | 'failed' | 'skipped', command?, summary?, basis? } — effects per parameter): the declaration is self-reported evidence —
4
+
5
+ verify only mechanically gates on it ('passed' → allowed; 'failed' → blocked; 'skipped' → allowed but needs a reason).
6
+
7
+ 'basis' (optional) is the 'what it stands on' dimension of the declaration — a judgment source, the exact words you read at their path, or an explicit 'unverified' (declaration-only: no format is enforced or machine-checked);
8
+
9
+ a passed / skipped declaration with no basis gets one advisory hint line in the report — never blocking and never changing the gate — while the failed face gets no basis hint (already blocked).
10
+
11
+ Whether/how to verify is described in natural language in the project's AGENTS.md and is decided + executed by you. The report also carries the changed files (git diff), an advisory node --check hint on changed .js/.mjs (not gating), the task list, and a self-review checklist.
12
+
13
+ Call this BEFORE declaring a coding task complete — do not say 'done' until verify passes.
@@ -1,4 +1,6 @@
1
- Search the web (DeepSeek Search when a DeepSeek provider is configured; otherwise Tavily with a key, else Bing). Returns result titles, URLs, and snippets — DeepSeek results also carry a date · authority (0–4) · rerank (0–3) line and a long summary. Use for looking up current information, docs, error messages.
1
+ Search the web (DeepSeek Search when a DeepSeek provider is configured; otherwise Tavily with a key, else Bing). Returns result titles, URLs, and snippets — DeepSeek results also carry a date · authority (0–4) · rerank (0–3) line and a long summary.
2
+
3
+ Use for looking up current information, docs, error messages.
2
4
 
3
5
  Parameters:
4
6
  - query (required): Search query
@@ -9,3 +9,5 @@ Notes:
9
9
  - Returns `Wrote N chars to <path>` + git diff + syntax-check note.
10
10
  - This overwrites the entire file — use `edit` for targeted changes
11
11
  - The file is atomic: it either writes completely or fails
12
+
13
+ **`ledger`** — the in-flight ledger row this call serves: `"#N"` or `"N"`. In engineering mode the parent agent must pass it — a call that does not say which row it serves is refused.
package/tools/bash.mjs CHANGED
@@ -3,12 +3,16 @@ import {
3
3
  sanitizeOutput,
4
4
  truncate,
5
5
  makeDecoder,
6
- BASH_TIMEOUT_MS,
6
+ BASH_TIMEOUT_MS,
7
+ LEDGER_PARAM_DESC,
8
+ PROJECT_PARAM_DESC,
7
9
  } from "./shared.mjs";
8
10
  import { spawn } from "node:child_process";
9
11
  // 树杀单源(台账 #208② · TOOLS.md §6.14 落位表行 2):本地副本删除——改用 `./process-tree.mjs`
10
12
  // (行为逐字同:win32 `taskkill /T /F` · POSIX 组杀 + 直杀兜底)。
11
13
  import { killProcessTree } from "./process-tree.mjs";
14
+ // 过程档标记(TOOLS.md §6.28):输出里行首路径 token 真解析到盘上的过程档行 ⇒ 行首前缀 + 末尾汇总行。
15
+ import { markProcessDocLines } from "./process-doc-mark.mjs";
12
16
  import { resolveShellIdentity } from "../shell-identity.mjs";
13
17
 
14
18
  /** Maximum buffer size per stream (stdout / stderr) before truncation */
@@ -247,6 +251,9 @@ export const bashTool = {
247
251
  timeout: { type: "number", description: `Timeout in ms (default ${BASH_TIMEOUT_MS} for synchronous runs; background runs have no default — an explicit timeout still applies)` },
248
252
  async: { type: "boolean", description: "Run in the background (default false): returns an ack at once (bash#<id>, log path) and the result arrives as a digest when the process exits. Depth-0 only." },
249
253
  filter: { type: "string", description: "Optional: only return output lines matching this regex (case-insensitive; synchronous runs only)" },
254
+ ledger: { type: "string", description: LEDGER_PARAM_DESC },
255
+ // 项目声明面(决策 40):`bash` 没有定位参数 ⇒ 判据点只能靠声明;纯声明,命令仍在会话 cwd 执行。
256
+ project: { type: "string", description: PROJECT_PARAM_DESC },
250
257
  },
251
258
  required: ["command"],
252
259
  },
@@ -288,7 +295,10 @@ export const bashTool = {
288
295
  shell: ctx.agent?.config?.shell ?? null,
289
296
  })
290
297
  const filtered = args.filter ? applyLineFilter(result, args.filter) : result
291
- const body = guard ? `${guard.notice}\n\n${filtered}` : filtered
298
+ // §6.28:顺序写死 = **过滤 → 打标 → 汇总 → 外包装**(filter 才照旧匹配原始行,前缀不吃
299
+ // 过滤命中;`guard.notice` ∥ `hint` 包在标之外)。解析基准 = 子进程 cwd(同步径 = `ctx.cwd`)。
300
+ const marked = markProcessDocLines(filtered, ctx.cwd)
301
+ const body = guard ? `${guard.notice}\n\n${marked}` : marked
292
302
  return hint ? `${hint}\n${body}` : body
293
303
  },
294
304
  }
@@ -3,23 +3,24 @@
3
3
  * (2026-08-31 工具顺手度 §9 ②;2026-09-01 缺陷修复"同文件多条串行累积";
4
4
  * 2026-09-04 EDIT.md §6——条目级判定+应用迁至 edit-diff.mjs——批量调共用。)
5
5
  *
6
- * 语义:同一 path 的多条编辑按序**串行累积应用**——第 n 条基于前 n-1 条已应用后的
7
- * 累积内容做匹配与替换;跨 path 条目互不影响(并行原子语义);任一条失败 →
8
- * 全不写(原子性保留)。每条目独立按判定序(EDIT.md §4——分支 0 单行替换 / 零重叠→替换即删 /
6
+ * 语义:同一 path 的多条编辑**分两类应用**(帧判据单源 = EDIT-HELPERS §7.3 R1–R4)——
7
+ * 行号条目先全部应用(按 startLine 降序,范围重叠则整批拒),内容条目随后按调用序
8
+ * 串行累积;跨 path 条目互不影响(并行原子语义);任一条失败 → 全不写(原子性保留)。
9
+ * 每条目独立按判定序(EDIT.md §4——分支 0 单行替换 / 零重叠→替换即删 /
9
10
  * LCS / 空 new 显式报错;EDIT.md §5(约束——删行形态):省略 new_string = 删行。顶层 path
10
11
  * (args.path)为无自带 path 条目的默认(2026-09-05 用户裁定——条目自带 path 优先——见
11
- * EDIT.md §5 修订注)。
12
+ * EDIT.md §5 修订注)。排顺序与累积走 edit-diff 的两个零 IO 纯函数——本档只留 IO(读 / 写 / 回执)。
12
13
  */
13
14
  import { readFile } from "node:fs/promises"
14
15
  import { resolveInCwd, normalizeEOL, joinWithEol, gitDiffOne, autoSyntaxCheck } from "./shared.mjs"
15
16
  // edit-batch ↔ edit-diff 循环引用:两侧导入的都是函数声明(提升初始化),
16
17
  // 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
17
18
  // 写盘经单一写路径点(§2.13.5)——本档不再直调 fs;回执组装共用 edit-diff 的 composeEditReceipt。
18
- import { writeThroughPath } from "./write-path.mjs"
19
+ import { writeThroughPath, lineAnchorVerdict, lineAnchorRefusal } from "./write-path.mjs"
19
20
  // EDIT.md §6:批量条目判定+应用共用 edit-diff(EDIT.md §4 分支 0 单行替换 + 行级 LCS——零重叠→替换即删);
20
21
  // EDIT.md §5:edits 互斥错误文本随前置校验分支迁出至 edit-diff.mjs。
21
22
  // 回执组装共用 edit-diff 的 composeEditReceipt(单点——缺省回执 = base + 写入点上下文)。
22
- import { assertEditArgsExclusive, assertEditsContainer, assertEditEntries, validateEditEntry, computeEditEntry, splitLines, EMPTY_NEW_STRING_LINE, EDIT_ENTRY_NO_PATH, EDIT_ABORT_PREFIX, editEntryLabel, deleteTarget, composeEditReceipt } from "./edit-diff.mjs"
23
+ import { assertEditArgsExclusive, assertEditsContainer, assertEditEntries, validateEditEntry, hasLineParams, planEditEntries, applyPlannedEntries, splitLines, EMPTY_NEW_STRING_LINE, EDIT_ENTRY_NO_PATH, editEntryLabel, deleteTarget, composeEditReceipt } from "./edit-diff.mjs"
23
24
 
24
25
  /**
25
26
  * Apply the `edits` array form: multi-file atomic replacement. Throws on any
@@ -31,12 +32,13 @@ export async function applyEditBatch(args, ctx) {
31
32
  assertEditArgsExclusive(args)
32
33
  assertEditEntries(args.edits) // #325 条目预扫(非对象条目 ⇒ 含下标成形错误;不读盘)
33
34
  // 原子:先全量检查(所有文件的替换都可执行)——任一失败全不写。
34
- // 2026-09-01 缺陷修复(TOOLS.md §9 ②"同文件多条规则"):同一 path 的多条编辑
35
- // 按序**串行累积应用**——第 n 条基于前 n-1 条已应用后的累积内容做匹配与替换
36
- // (原实现每条都基于盘上原始内容计算、写盘循环后置,同文件后者覆盖前者 →
37
- // 除最后一条外全部静默丢失);跨 path 条目互不影响(并行原子语义不变)。
35
+ // 2026-09-01 缺陷修复(TOOLS.md §9 ②"同文件多条规则"):同一 path 的多条编辑基于同一份
36
+ // 累积内容应用,写盘前不得让后者覆盖前者(原实现每条都基于盘上原始内容计算、写盘循环
37
+ // 后置 → 除最后一条外全部静默丢失);跨 path 条目互不影响(并行原子语义不变)。
38
+ // 累积顺序自 2026-11-02 起 = 帧(行号条目降序先应用、内容条目随后)——见下方判定点。
38
39
  const groups = new Map() // abs → 每文件一条流水线
39
- for (const e of args.edits) {
40
+ for (let i = 0; i < args.edits.length; i++) {
41
+ const e = args.edits[i]
40
42
  // 2026-09-05 用户裁定:顶层 path + edits 并存合法化——`e.path || args.path`(条目优先;
41
43
  // 缺省入口仅在此兜底);两者皆无 → 原错误文本(措辞补顶层选项)。
42
44
  const p = e.path || args.path
@@ -47,39 +49,42 @@ export async function applyEditBatch(args, ctx) {
47
49
  let g = groups.get(abs)
48
50
  if (!g) {
49
51
  const raw = await readFile(abs, "utf8")
50
- g = { abs, path: p, raw, content: normalizeEOL(raw), edits: [] }
52
+ g = { abs, path: p, raw, content: normalizeEOL(raw), edits: [], origIdx: [] }
51
53
  groups.set(abs, g)
52
54
  }
53
55
  g.edits.push(e)
56
+ g.origIdx.push(i) // 条目在 args.edits 里的原始下标——回显按它回位(EDIT-HELPERS §7.3)
54
57
  }
55
- const prepared = [] // 每条目一条回显;同文件内按 args 序,跨文件按首次出现分组序(应用/回显语义均正确)
58
+ // 判定点(§7.3 写死):组循环**之前**用批前快照判一次——命中即抛、零写(下方第二个循环才写盘)。
59
+ // 组内不再逐条判:组内漂移由 R2(降序应用)自己算得出来,逐条再判会用同一份快照判出与 R2 相反的结果。
56
60
  for (const g of groups.values()) {
57
- g.netShift = 0 // 组内行数差累积(合并快照的 shift = 全组净漂移)
58
61
  for (const e of g.edits) {
59
- // 条目级判定+应用(edit-diff——EDIT.md §4 分支 0 单行替换 + 判定序 1/2/3 + 空 new 显式报错 +
60
- // >1000 行报错(diff 形态——D1 行号条目走 applyLineEdit,不经 LCS,无此上限))
61
- const out = computeEditEntry(g.content, e, {
62
- path: g.path,
63
- absPath: g.abs,
64
- abortPrefix: EDIT_ABORT_PREFIX,
65
- })
66
- prepared.push({
67
- g,
68
- editStartLine: out.editStartLine, // 基于累积内容计算——已天然计入前面条目的行偏移,不再累加
69
- lineShift: out.lineShift,
70
- occurrences: out.occurrences,
71
- note: out.note ?? null, // P15.11——空白差异自动落点标记(成功消息追加)
72
- deleted: out.deleted ?? false, // 删行形态(EDIT.md §5)——结果文本用 Deleted 前缀
73
- })
74
- g.content = out.updated // 串行累积:下一条基于本条应用后的内容
75
- g.netShift += out.lineShift
62
+ if (!hasLineParams(e)) continue
63
+ const startLine = e.line ?? e.startLine
64
+ const endLine = e.line ?? e.endLine
65
+ if (lineAnchorVerdict(g.abs, startLine, endLine)) throw new Error(lineAnchorRefusal(g.abs, startLine, endLine))
76
66
  }
77
67
  }
78
- // 全部检查通过——每文件一次写盘(同文件多条:写入串行累积后的最终内容);
68
+ // 回显按调用序(EDIT-HELPERS §7.3):按条目在 `edits` 数组里的**原始下标**回填——跨文件
69
+ // 交错的批同样如此;按文件分组去排会排成 A、A、B,而模型是按位置对号的,顺序一变它会把
70
+ // 结果贴到错的那一笔上。
71
+ const prepared = new Array(args.edits.length)
72
+ for (const g of groups.values()) {
73
+ // 帧单源(§7.3 R1–R4)= edit-diff 的两个零 IO 纯函数:排顺序 + 按计划序累积
74
+ const plan = planEditEntries(g.edits, g.path)
75
+ if (plan.rejection) throw new Error(plan.rejection)
76
+ const applied = applyPlannedEntries(g.content, g.edits, plan.order, { path: g.path })
77
+ g.content = applied.content
78
+ // 合并快照的 shift = 全组净漂移(公式不变;重排后的取值由 AC11 钉住)
79
+ g.netShift = applied.results.reduce((n, r) => n + r.lineShift, 0)
80
+ // results 按组内调用序;g.origIdx[k] = 该结果在 args.edits 里的原始下标
81
+ applied.results.forEach((r, k) => { prepared[g.origIdx[k]] = { g, ...r } })
82
+ }
83
+ // 全部检查通过——每文件一次写盘(同文件多条:写入按帧排好后累积出的最终内容);
79
84
  // recordWrite 每组一条合并快照:startLine = 组内**所有**编辑受影响行的最小值
80
85
  // (#2,2026-09-01 交付评审尾巴——原实现取首条 = 调用序第一条,逆序条目时
81
86
  // 护栏下界过高、受影响区内的 insert_after 被放行),shift = 全组行数差累积
82
- // ——受影响区护栏覆盖组内所有编辑
87
+ // ——受影响区护栏覆盖组内所有编辑(公式不变;重排后的取值由 AC11 钉住)
83
88
  for (const g of groups.values()) {
84
89
  const startLine = Math.min(...prepared.filter((p) => p.g === g).map((p) => p.editStartLine))
85
90
  // 单一写路径点:默认 = writeFile + 记账;端侧注入 ⇒ 编辑器径(§2.13.5)。
@@ -116,6 +121,8 @@ export async function applyEditBatch(args, ctx) {
116
121
  // ---------------------------------------------------------------------------
117
122
  // 2026-09-08 edit 语义升级(EDIT.md §2/§3——D1 按行号改 / D2 模糊匹配,落点定死本模块;
118
123
  // 条目判定接线在 edit-diff.mjs computeEditEntry——单形态/批量/ACP 桥三通道自动继承;
124
+ // 2026-11-02:排顺序(帧 R1–R4)与累积也上提到 edit-diff(planEditEntries /
125
+ // applyPlannedEntries)——两通道共用同一份排法。
119
126
  // 阶段 2 删行形态见 EDIT.md §5(约束)。以下均为纯函数(无 IO)——edit-diff.mjs 调用期导入
120
127
  // (ESM 循环引用安全:两侧仅函数声明,提升初始化——同 file.mjs ↔ edit-diff.mjs 先例)。
121
128
 
@@ -4,6 +4,8 @@
4
4
  * edit 语义(权威 = EDIT.md §2-§5——2026-09-08 语义升级 D1/D2/D3 已并入):两种定位形态(互斥)——
5
5
  * ① 按行号改(D1/F1):line(单行)/ startLine+endLine(1-based 闭区间)→ 直接替换该
6
6
  * 行/行范围为 new_string(无需 old_string——实现落点 edit-batch.mjs applyLineEdit);
7
+ * **先过行号锚护栏**(判据与拒文单源 = write-path.mjs 的 lineAnchorVerdict /
8
+ * lineAnchorRefusal——上一笔写之后目标区落在受影响区内即拒);
7
9
  * 阶段 2 删行形态(EDIT.md §5——约束):省略 new_string = 删除该行/范围(有界意图);
8
10
  * ② 内容定位:old_string = 变化区**当前内容**(单次匹配——匹配档位:逐字 → P15.11 唯一
9
11
  * 空白差异窗口 → D2/F2 模糊匹配(行级 normalize 后 ≥90% 行相等——edit-batch.mjs
@@ -35,7 +37,7 @@ import {
35
37
  // 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
36
38
  // 写盘经单一写路径点(§2.13.5)——本档不再直调 fs。
37
39
  import { appendWriteContext } from "./file.mjs"
38
- import { writeThroughPath, lastWriteOf, isDirty } from "./write-path.mjs"
40
+ import { writeThroughPath, lastWriteOf, isDirty, lineAnchorVerdict, lineAnchorRefusal } from "./write-path.mjs"
39
41
  // D1/D2 落点(EDIT.md §6——edit-batch.mjs):按行号改 + 模糊匹配纯函数。
40
42
  import { applyLineEdit, findFuzzyWindow, FUZZY_MATCH_NOTE } from "./edit-batch.mjs"
41
43
 
@@ -357,6 +359,63 @@ export function computeEditEntry(content, entry, opts = {}) {
357
359
  return { updated, editStartLine, lineShift, occurrences: entry.replace_all ? occurrences : 1, note }
358
360
  }
359
361
 
362
+ /**
363
+ * §7.3 帧(R1–R4)——同档多条 `edits` 的应用顺序:行号条目先全部应用、按 `startLine` 降序
364
+ * (从下往上改,上方行号不被同批自己顶偏);内容条目随后按数组序(位置无关寻址无帧问题)。
365
+ *
366
+ * **零 IO**(只看条目自身的行号):两个通道(本地 `edit-batch` ∥ ACP 桥)调同一份排法,
367
+ * 各自只提供自己的 IO——抄一份组循环就是本机制要根治的形状。
368
+ *
369
+ * @returns {{ order: number[]|null, rejection: string|null }} `order` = 数组下标的应用序
370
+ * (回显仍按调用序取原位);`rejection` 非空 = R3 重叠:整批拒、零写。
371
+ * `path`(可选)= 拒文里点名的目标档(条目自带 path 优先——顶层 path 不在条目上)。
372
+ */
373
+ export function planEditEntries(entries, path = null) {
374
+ const rangeOf = (e) => [e.line ?? e.startLine, e.line ?? e.endLine]
375
+ const lineIdx = []
376
+ const contentIdx = []
377
+ entries.forEach((e, i) => { (hasLineParams(e) ? lineIdx : contentIdx).push(i) })
378
+ // R3:两条行号条目的范围在快照坐标上重叠 ⇒ 先改哪一条决定结果,模型意图在此矛盾
379
+ const byStart = [...lineIdx].sort((a, b) => rangeOf(entries[a])[0] - rangeOf(entries[b])[0])
380
+ for (let i = 1; i < byStart.length; i++) {
381
+ const prev = rangeOf(entries[byStart[i - 1]])
382
+ const cur = rangeOf(entries[byStart[i]])
383
+ if (cur[0] <= prev[1]) {
384
+ const label = entries[byStart[i]].path ?? entries[byStart[i - 1]].path ?? path ?? "file"
385
+ return {
386
+ order: null,
387
+ rejection:
388
+ `${EDIT_ABORT_PREFIX}line-number entries L${prev[0]}-L${prev[1]} and L${cur[0]}-L${cur[1]} (${label}) overlap — ` +
389
+ `two changes to the same region cannot both be anchored on the numbers you read; split them into separate calls`,
390
+ }
391
+ }
392
+ }
393
+ // R2:降序(同档起始行不会相等——相等即重叠,已在 R3 拒掉)
394
+ const desc = [...lineIdx].sort((a, b) => rangeOf(entries[b])[0] - rangeOf(entries[a])[0])
395
+ return { order: [...desc, ...contentIdx], rejection: null }
396
+ }
397
+
398
+ /**
399
+ * 按 `planEditEntries` 排出的应用序逐条 `computeEditEntry` 累积——**零 IO**。
400
+ * `results` 按调用序回到原位(回显顺序不变),元数据形同 `computeEditEntry` 返回。
401
+ */
402
+ export function applyPlannedEntries(content, entries, order, opts = {}) {
403
+ let cur = content
404
+ const results = new Array(entries.length)
405
+ for (const i of order) {
406
+ const out = computeEditEntry(cur, entries[i], { ...opts, abortPrefix: opts.abortPrefix ?? EDIT_ABORT_PREFIX })
407
+ results[i] = {
408
+ editStartLine: out.editStartLine,
409
+ lineShift: out.lineShift,
410
+ occurrences: out.occurrences,
411
+ note: out.note ?? null,
412
+ deleted: out.deleted ?? false,
413
+ }
414
+ cur = out.updated
415
+ }
416
+ return { content: cur, results }
417
+ }
418
+
360
419
  /**
361
420
  * 单形态 edit 执行体(自 file.mjs 整段迁出——模块拆分写优先纪律:逻辑体不变)——
362
421
  * file.mjs 只留工具壳与转发。
@@ -365,6 +424,15 @@ export async function runSingleEdit(args, ctx) {
365
424
  const abs = resolveInCwd(ctx, args.path)
366
425
  const raw = await readFile(abs, "utf8")
367
426
  const content = normalizeEOL(raw)
427
+ // 行号锚护栏(判定点写死:读盘之后、computeEditEntry 之前):行号形才需判——内容形态是
428
+ // 位置无关寻址,没有可漂移的锚。判据与拒文单源 = write-path.mjs。
429
+ // 形态校验先于护栏:缺 endLine / 与 old_string 互斥这类错要报自己的句,不该被拒文顶掉。
430
+ if (hasLineParams(args)) {
431
+ validateEditEntry(args, { rich: true })
432
+ const startLine = args.line ?? args.startLine
433
+ const endLine = args.line ?? args.endLine
434
+ if (lineAnchorVerdict(abs, startLine, endLine)) throw new Error(lineAnchorRefusal(abs, startLine, endLine))
435
+ }
368
436
  const out = computeEditEntry(content, args, { path: args.path, absPath: abs })
369
437
  // 写回按原文行尾(joinWithEol)——normalizeEOL 先行避免 \r\n 污染 split/join
370
438
  // 单一写路径点:默认 = writeFile + 记账;端侧注入 ⇒ 编辑器径(§2.13.5)。
package/tools/execute.mjs CHANGED
@@ -26,9 +26,11 @@
26
26
  */
27
27
  import { spawn } from "node:child_process"
28
28
  import { resolve } from "node:path"
29
- import { DESC } from "./shared.mjs"
29
+ import { DESC, LEDGER_PARAM_DESC, PROJECT_PARAM_DESC } from "./shared.mjs"
30
30
  import { killProcessTree } from "./process-tree.mjs"
31
31
  import { nodeChildEnv } from "./node-child.mjs"
32
+ // 过程档标记(TOOLS.md §6.28):失败径同标(错误文本也是行内容),解析基准 = 子进程 cwd `baseDir`。
33
+ import { markProcessDocLines } from "./process-doc-mark.mjs"
32
34
 
33
35
  const MAX_SCRIPT = 50_000
34
36
  const MAX_OUTPUT = 50_000
@@ -197,6 +199,9 @@ export const executeTool = {
197
199
  maximum: 600000,
198
200
  description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT}, max 600000)`,
199
201
  },
202
+ ledger: { type: "string", description: LEDGER_PARAM_DESC },
203
+ // 项目声明面(决策 40):`workdir` 缺位时才被读(定位参数优先);纯声明,不改执行目录。
204
+ project: { type: "string", description: PROJECT_PARAM_DESC },
200
205
  },
201
206
  required: [],
202
207
  },
@@ -231,8 +236,9 @@ export const executeTool = {
231
236
  }
232
237
 
233
238
  const { text, ok } = await runNode(childArgs, baseDir, timeoutMs, ctx.signal)
234
- // Only filter successful output — never swallow an error report behind a filter.
235
- if (!ok) return text
236
- return args.filter ? applyFilter(text, args.filter) : text
239
+ // §6.28 单一出口:`filter` 只滤成功输出(原口径),打标接在它的后面;解析基准 = `baseDir`
240
+ // (= 子进程 cwd),不是 `ctx.cwd`——`workdir` 在场时只有按子进程 cwd 才解得对上。
241
+ const body = ok && args.filter ? applyFilter(text, args.filter) : text
242
+ return markProcessDocLines(body, baseDir)
237
243
  },
238
244
  }
package/tools/file.mjs CHANGED
@@ -11,12 +11,13 @@ import {
11
11
  joinWithEol,
12
12
  majorityEol,
13
13
  FFFD_WARNING,
14
+ LEDGER_PARAM_DESC,
14
15
  } from "./shared.mjs";
15
16
  import { applyEditBatch } from "./edit-batch.mjs";
16
17
  import { runSingleEdit } from "./edit-diff.mjs";
17
18
  // 单一写路径点(编辑器编辑径注入缝——CORE-UNIFICATION §2.13.5):本档三个写点全走它,
18
19
  // 记账面同源住该档(下面只做再导出——保既有 import 面稳定)。
19
- import { writeThroughPath, clearDirty, clearLastWrite, lastWriteOf, isDirty } from "./write-path.mjs";
20
+ import { writeThroughPath, clearDirty, clearLastWrite, lineAnchorVerdict, lineAnchorRefusal } from "./write-path.mjs";
20
21
  import { specForModel } from "../config.mjs";
21
22
  import { createHash } from "node:crypto";
22
23
  import { mkdir, readFile, stat } from "node:fs/promises";
@@ -192,6 +193,7 @@ export const writeTool = {
192
193
  properties: {
193
194
  path: { type: "string", description: "File path (relative to cwd or absolute)" },
194
195
  content: { type: "string", description: "Full content to write" },
196
+ ledger: { type: "string", description: LEDGER_PARAM_DESC },
195
197
  },
196
198
  required: ["path", "content"],
197
199
  },
@@ -231,7 +233,7 @@ export const editTool = {
231
233
  replace_all: { type: "boolean", description: "Replace all occurrences (default false)" },
232
234
  edits: {
233
235
  type: "array",
234
- description: "Batch form — multiple edits in ONE call, atomic (any failure writes nothing; same-file entries apply serially, each based on the previous result). Use it for multiple changes to the same file AND for independent changes across multiple files — prefer one batched call over N single edits. A top-level path is allowed — it defaults entries without their own path (entry paths win). Mutually exclusive with top-level old_string/new_string/line/startLine/endLine — provide each change's targeting (old_string, or line / startLine+endLine) inside its edits entry; new_string replaces when given, and line-targeted entries may omit it to DELETE the line/range.",
236
+ description: "Batch form — multiple edits in ONE call, atomic (any failure writes nothing; line-number entries apply first, in descending startLine order, and their ranges must not overlap — overlap refuses the whole batch; content entries then apply in array order). Use it for multiple changes to the same file AND for independent changes across multiple files — prefer one batched call over N single edits. A top-level path is allowed — it defaults entries without their own path (entry paths win). Mutually exclusive with top-level old_string/new_string/line/startLine/endLine — provide each change's targeting (old_string, or line / startLine+endLine) inside its edits entry; new_string replaces when given, and line-targeted entries may omit it to DELETE the line/range.",
235
237
  items: {
236
238
  type: "object",
237
239
  properties: {
@@ -245,6 +247,7 @@ export const editTool = {
245
247
  },
246
248
  },
247
249
  },
250
+ ledger: { type: "string", description: LEDGER_PARAM_DESC },
248
251
  },
249
252
  required: [],
250
253
  },
@@ -286,6 +289,7 @@ export const insertAfterTool = {
286
289
  after_line: { type: "number", description: "Line number to insert after (1-based). Takes priority over after_regex." },
287
290
  after_regex: { type: "string", description: "JavaScript regex to find the line to insert after (must match exactly one line)" },
288
291
  content: { type: "string", description: "Text to insert (with leading newline if you need a blank line)" },
292
+ ledger: { type: "string", description: LEDGER_PARAM_DESC },
289
293
  },
290
294
  required: ["path", "content"],
291
295
  },
@@ -296,31 +300,16 @@ export const insertAfterTool = {
296
300
  // Read-before-insert guard: after_line anchors are line numbers, and any
297
301
  // write since the last read made them stale. Refuse instead of silently
298
302
  // inserting at a drifted position (the failure mode that corrupted test
299
- // structure repeatedly). after_regex callers get the same gate — a stale
300
- // target line is just as wrong, and the rule is simpler to reason about.
301
- // 2026-08-31 工具顺手度(用户批准):判定精确化——本 session 写入工具记录受影响区
302
- // (lastWrite),after_line 在未受影响区(<= startLine)→ 行号未漂移 → 允许
303
- // (消掉"我写的文件被当外部修改"的摩擦);受影响区内/write 全文重写 → 拒绝。
304
- const lw = lastWriteOf(abs)
305
- if (lw && args.after_line != null) {
306
- if (lw.type === "write") {
307
- throw new Error(
308
- `${args.path} 刚被 write 全文重写(was modified since your last read)——任何行号都可能漂移,必须重 read。`
309
- )
310
- }
311
- if (args.after_line > lw.startLine) {
312
- throw new Error(
313
- `${args.path} 的 after_line ${args.after_line} 在上次写入(L${lw.startLine})之后——` +
314
- `行号已漂移 ${lw.shift >= 0 ? "+" : ""}${lw.shift},请用新行号或先 read。`
315
- )
316
- }
317
- // after_line <= startLine → 行号未漂移 → 允许
318
- } else if (isDirty(abs)) {
319
- throw new Error(
320
- `${args.path} was modified since your last read — line numbers may be stale.\n` +
321
- `Read the file again (read tool) to refresh line numbers, then retry insert_after.`
322
- )
323
- }
303
+ // structure repeatedly).
304
+ // 2026-11-02:判据与拒文单源 = tools/write-path.mjs 的 lineAnchorVerdict /
305
+ // lineAnchorRefusal(与 edit 行号形同一句、同一六行表——INSERT-AFTER.md §3)。
306
+ // 本工具的边界 = `after_line <= startLine`(插入不读写目标行,比 edit 松一格——
307
+ // 边界按记账类型分,已在判据内);after_regex 没有模型给的行号(定位在工具内按当前
308
+ // 盘面做)⇒ 只判到记账态那一层(dirty 面无快照 ⇒ 第 6 行),与改前同判。
309
+ const afterLine = Number.isInteger(args.after_line) ? args.after_line : null
310
+ const hit = lineAnchorVerdict(abs, afterLine, afterLine)
311
+ if (hit) throw new Error(lineAnchorRefusal(abs, afterLine, afterLine))
312
+
324
313
  const raw = await readFile(abs, "utf8") // original bytes — EOL detection needs the file's real line endings
325
314
  const text = normalizeEOL(raw)
326
315
  const lines = text.split("\n")
@@ -385,6 +374,7 @@ export const hashlineEditTool = {
385
374
  path: { type: "string", description: "File path" },
386
375
  old_hashes: { type: "array", items: { type: "string" }, description: "SHA256 hashes (12 chars) of the lines to replace. Read the file with hashes=true first to obtain these hashes. Single line: pass 1 hash; multiple lines: pass the exact sequence of hashes." },
387
376
  new_content: { type: "string", description: "Replacement text (can span multiple lines)" },
377
+ ledger: { type: "string", description: LEDGER_PARAM_DESC },
388
378
  },
389
379
  required: ["path", "old_hashes", "new_content"],
390
380
  },