@namzu/sdk 62.6.0 → 62.7.0

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 (295) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +5 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/examples/SupervisorAgent.js +1 -0
  7. package/dist/agents/examples/SupervisorAgent.js.map +1 -1
  8. package/dist/agents/explore.d.ts.map +1 -1
  9. package/dist/agents/explore.js +2 -1
  10. package/dist/agents/explore.js.map +1 -1
  11. package/dist/compaction/index.d.ts +1 -1
  12. package/dist/compaction/index.d.ts.map +1 -1
  13. package/dist/compaction/index.js +1 -1
  14. package/dist/compaction/index.js.map +1 -1
  15. package/dist/compaction/model-summary.js +2 -2
  16. package/dist/compaction/model-summary.js.map +1 -1
  17. package/dist/compaction/summary.d.ts +16 -1
  18. package/dist/compaction/summary.d.ts.map +1 -1
  19. package/dist/compaction/summary.js +25 -2
  20. package/dist/compaction/summary.js.map +1 -1
  21. package/dist/config/runtime.d.ts +8 -0
  22. package/dist/config/runtime.d.ts.map +1 -1
  23. package/dist/config/runtime.js +15 -0
  24. package/dist/config/runtime.js.map +1 -1
  25. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  26. package/dist/execution/workflow/runtime.js +3 -0
  27. package/dist/execution/workflow/runtime.js.map +1 -1
  28. package/dist/execution/workflow/types.d.ts +8 -0
  29. package/dist/execution/workflow/types.d.ts.map +1 -1
  30. package/dist/manager/agent/child-session.d.ts +12 -0
  31. package/dist/manager/agent/child-session.d.ts.map +1 -1
  32. package/dist/manager/agent/child-session.js +29 -0
  33. package/dist/manager/agent/child-session.js.map +1 -1
  34. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  35. package/dist/manager/agent/lifecycle.js +8 -0
  36. package/dist/manager/agent/lifecycle.js.map +1 -1
  37. package/dist/manager/session/turn-recorder.d.ts +13 -2
  38. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  39. package/dist/manager/session/turn-recorder.js +57 -3
  40. package/dist/manager/session/turn-recorder.js.map +1 -1
  41. package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
  42. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  43. package/dist/prompt/coding-agent-doctrine.js +5 -3
  44. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  45. package/dist/provider/fallback.d.ts.map +1 -1
  46. package/dist/provider/fallback.js +5 -1
  47. package/dist/provider/fallback.js.map +1 -1
  48. package/dist/provider/retry.d.ts +25 -0
  49. package/dist/provider/retry.d.ts.map +1 -1
  50. package/dist/provider/retry.js +28 -0
  51. package/dist/provider/retry.js.map +1 -1
  52. package/dist/public-runtime.d.ts +3 -3
  53. package/dist/public-runtime.d.ts.map +1 -1
  54. package/dist/public-runtime.js +3 -3
  55. package/dist/public-runtime.js.map +1 -1
  56. package/dist/public-tools.d.ts +4 -2
  57. package/dist/public-tools.d.ts.map +1 -1
  58. package/dist/public-tools.js +5 -2
  59. package/dist/public-tools.js.map +1 -1
  60. package/dist/runtime/query/auto-review/classifier.d.ts +7 -1
  61. package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -1
  62. package/dist/runtime/query/auto-review/classifier.js +8 -4
  63. package/dist/runtime/query/auto-review/classifier.js.map +1 -1
  64. package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -1
  65. package/dist/runtime/query/auto-review/prompt.js +2 -0
  66. package/dist/runtime/query/auto-review/prompt.js.map +1 -1
  67. package/dist/runtime/query/auto-review/script-evidence.d.ts +93 -0
  68. package/dist/runtime/query/auto-review/script-evidence.d.ts.map +1 -0
  69. package/dist/runtime/query/auto-review/script-evidence.js +566 -0
  70. package/dist/runtime/query/auto-review/script-evidence.js.map +1 -0
  71. package/dist/runtime/query/checkpoint.d.ts +13 -0
  72. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  73. package/dist/runtime/query/checkpoint.js +37 -0
  74. package/dist/runtime/query/checkpoint.js.map +1 -1
  75. package/dist/runtime/query/executor.d.ts +38 -1
  76. package/dist/runtime/query/executor.d.ts.map +1 -1
  77. package/dist/runtime/query/executor.js +130 -11
  78. package/dist/runtime/query/executor.js.map +1 -1
  79. package/dist/runtime/query/finalize-turn.d.ts +7 -1
  80. package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
  81. package/dist/runtime/query/finalize-turn.js +2 -0
  82. package/dist/runtime/query/finalize-turn.js.map +1 -1
  83. package/dist/runtime/query/index.d.ts +8 -0
  84. package/dist/runtime/query/index.d.ts.map +1 -1
  85. package/dist/runtime/query/index.js +7 -0
  86. package/dist/runtime/query/index.js.map +1 -1
  87. package/dist/runtime/query/iteration/index.d.ts +32 -1
  88. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  89. package/dist/runtime/query/iteration/index.js +158 -10
  90. package/dist/runtime/query/iteration/index.js.map +1 -1
  91. package/dist/runtime/query/iteration/phases/compaction.d.ts +12 -0
  92. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  93. package/dist/runtime/query/iteration/phases/compaction.js +48 -1
  94. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  95. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  96. package/dist/runtime/query/iteration/phases/context.js +5 -0
  97. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  98. package/dist/runtime/query/iteration/stream-turn.js +3 -3
  99. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  100. package/dist/runtime/query/prelude-lease.d.ts.map +1 -1
  101. package/dist/runtime/query/prelude-lease.js +22 -0
  102. package/dist/runtime/query/prelude-lease.js.map +1 -1
  103. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  104. package/dist/runtime/query/prepare-turn.js +13 -4
  105. package/dist/runtime/query/prepare-turn.js.map +1 -1
  106. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  107. package/dist/runtime/query/review-policy.js +29 -0
  108. package/dist/runtime/query/review-policy.js.map +1 -1
  109. package/dist/runtime/query/tooling.d.ts +2 -0
  110. package/dist/runtime/query/tooling.d.ts.map +1 -1
  111. package/dist/runtime/query/tooling.js +1 -0
  112. package/dist/runtime/query/tooling.js.map +1 -1
  113. package/dist/runtime/query/turn/completion-audit.d.ts +27 -1
  114. package/dist/runtime/query/turn/completion-audit.d.ts.map +1 -1
  115. package/dist/runtime/query/turn/completion-audit.js +71 -6
  116. package/dist/runtime/query/turn/completion-audit.js.map +1 -1
  117. package/dist/runtime/query/turn/outcome-ledger.d.ts +113 -0
  118. package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -0
  119. package/dist/runtime/query/turn/outcome-ledger.js +578 -0
  120. package/dist/runtime/query/turn/outcome-ledger.js.map +1 -0
  121. package/dist/sandbox/file-walk.d.ts.map +1 -1
  122. package/dist/sandbox/file-walk.js +34 -17
  123. package/dist/sandbox/file-walk.js.map +1 -1
  124. package/dist/sandbox/glob-segment.d.ts +35 -0
  125. package/dist/sandbox/glob-segment.d.ts.map +1 -0
  126. package/dist/sandbox/glob-segment.js +300 -0
  127. package/dist/sandbox/glob-segment.js.map +1 -0
  128. package/dist/store/session-log/index.d.ts +1 -0
  129. package/dist/store/session-log/index.d.ts.map +1 -1
  130. package/dist/store/session-log/index.js +1 -0
  131. package/dist/store/session-log/index.js.map +1 -1
  132. package/dist/store/session-log/turn-outcome.d.ts +37 -0
  133. package/dist/store/session-log/turn-outcome.d.ts.map +1 -0
  134. package/dist/store/session-log/turn-outcome.js +77 -0
  135. package/dist/store/session-log/turn-outcome.js.map +1 -0
  136. package/dist/tools/builtins/bash.js +1 -1
  137. package/dist/tools/builtins/bash.js.map +1 -1
  138. package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
  139. package/dist/tools/builtins/edit-apply.js +24 -3
  140. package/dist/tools/builtins/edit-apply.js.map +1 -1
  141. package/dist/tools/builtins/edit.d.ts +31 -17
  142. package/dist/tools/builtins/edit.d.ts.map +1 -1
  143. package/dist/tools/builtins/edit.js +92 -27
  144. package/dist/tools/builtins/edit.js.map +1 -1
  145. package/dist/tools/builtins/glob.js +1 -1
  146. package/dist/tools/builtins/glob.js.map +1 -1
  147. package/dist/tools/builtins/grep.js +1 -1
  148. package/dist/tools/builtins/grep.js.map +1 -1
  149. package/dist/tools/builtins/notebook-guard.d.ts +19 -0
  150. package/dist/tools/builtins/notebook-guard.d.ts.map +1 -0
  151. package/dist/tools/builtins/notebook-guard.js +75 -0
  152. package/dist/tools/builtins/notebook-guard.js.map +1 -0
  153. package/dist/tools/builtins/read-render.d.ts.map +1 -1
  154. package/dist/tools/builtins/read-render.js +8 -2
  155. package/dist/tools/builtins/read-render.js.map +1 -1
  156. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  157. package/dist/tools/builtins/write-file.js +14 -1
  158. package/dist/tools/builtins/write-file.js.map +1 -1
  159. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  160. package/dist/tools/coordinator/agent.js +25 -6
  161. package/dist/tools/coordinator/agent.js.map +1 -1
  162. package/dist/tools/coordinator/index.d.ts.map +1 -1
  163. package/dist/tools/coordinator/index.js +6 -1
  164. package/dist/tools/coordinator/index.js.map +1 -1
  165. package/dist/tools/coordinator/outcome-record.d.ts +22 -0
  166. package/dist/tools/coordinator/outcome-record.d.ts.map +1 -0
  167. package/dist/tools/coordinator/outcome-record.js +91 -0
  168. package/dist/tools/coordinator/outcome-record.js.map +1 -0
  169. package/dist/tools/defineTool.d.ts +2 -0
  170. package/dist/tools/defineTool.d.ts.map +1 -1
  171. package/dist/tools/defineTool.js +1 -0
  172. package/dist/tools/defineTool.js.map +1 -1
  173. package/dist/tools/file-read-tracker.d.ts +23 -2
  174. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  175. package/dist/tools/file-read-tracker.js +37 -2
  176. package/dist/tools/file-read-tracker.js.map +1 -1
  177. package/dist/tools/untrusted-envelope.d.ts +6 -0
  178. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  179. package/dist/tools/untrusted-envelope.js +22 -1
  180. package/dist/tools/untrusted-envelope.js.map +1 -1
  181. package/dist/toolsets/manager.d.ts.map +1 -1
  182. package/dist/toolsets/manager.js +3 -4
  183. package/dist/toolsets/manager.js.map +1 -1
  184. package/dist/toolsets/validation-issues.d.ts +27 -0
  185. package/dist/toolsets/validation-issues.d.ts.map +1 -0
  186. package/dist/toolsets/validation-issues.js +83 -0
  187. package/dist/toolsets/validation-issues.js.map +1 -0
  188. package/dist/turn/memory-recall.d.ts +13 -0
  189. package/dist/turn/memory-recall.d.ts.map +1 -1
  190. package/dist/turn/memory-recall.js +18 -0
  191. package/dist/turn/memory-recall.js.map +1 -1
  192. package/dist/types/agent/base.d.ts +19 -0
  193. package/dist/types/agent/base.d.ts.map +1 -1
  194. package/dist/types/agent/query.d.ts +2 -0
  195. package/dist/types/agent/query.d.ts.map +1 -1
  196. package/dist/types/common/index.d.ts +17 -0
  197. package/dist/types/common/index.d.ts.map +1 -1
  198. package/dist/types/common/index.js +8 -1
  199. package/dist/types/common/index.js.map +1 -1
  200. package/dist/types/hitl/index.d.ts +22 -0
  201. package/dist/types/hitl/index.d.ts.map +1 -1
  202. package/dist/types/hitl/index.js.map +1 -1
  203. package/dist/types/session/config.d.ts +20 -0
  204. package/dist/types/session/config.d.ts.map +1 -1
  205. package/dist/types/session/events.d.ts +3 -3
  206. package/dist/types/session/events.d.ts.map +1 -1
  207. package/dist/types/session/index.d.ts +1 -0
  208. package/dist/types/session/index.d.ts.map +1 -1
  209. package/dist/types/session/index.js +1 -0
  210. package/dist/types/session/index.js.map +1 -1
  211. package/dist/types/session/outcome.d.ts +95 -0
  212. package/dist/types/session/outcome.d.ts.map +1 -0
  213. package/dist/types/session/outcome.js +45 -0
  214. package/dist/types/session/outcome.js.map +1 -0
  215. package/dist/types/session/prepare-step.d.ts +2 -0
  216. package/dist/types/session/prepare-step.d.ts.map +1 -1
  217. package/dist/types/session/records.d.ts +167 -2
  218. package/dist/types/session/records.d.ts.map +1 -1
  219. package/dist/types/session/records.js +42 -0
  220. package/dist/types/session/records.js.map +1 -1
  221. package/dist/types/session/turn.d.ts +18 -0
  222. package/dist/types/session/turn.d.ts.map +1 -1
  223. package/dist/types/session/turn.js.map +1 -1
  224. package/dist/types/tool/index.d.ts +30 -0
  225. package/dist/types/tool/index.d.ts.map +1 -1
  226. package/dist/types/tool/index.js.map +1 -1
  227. package/package.json +2 -2
  228. package/src/agents/QueryAgent.ts +5 -0
  229. package/src/agents/examples/SupervisorAgent.ts +1 -0
  230. package/src/agents/explore.ts +2 -1
  231. package/src/compaction/index.ts +1 -0
  232. package/src/compaction/model-summary.ts +2 -2
  233. package/src/compaction/summary.ts +30 -2
  234. package/src/config/runtime.ts +15 -0
  235. package/src/execution/workflow/runtime.ts +4 -0
  236. package/src/execution/workflow/types.ts +9 -0
  237. package/src/manager/agent/child-session.ts +36 -0
  238. package/src/manager/agent/lifecycle.ts +9 -0
  239. package/src/manager/session/turn-recorder.ts +62 -3
  240. package/src/pricing/rates.source.json +1 -1
  241. package/src/prompt/coding-agent-doctrine.ts +5 -3
  242. package/src/provider/fallback.ts +6 -1
  243. package/src/provider/retry.ts +51 -0
  244. package/src/public-runtime.ts +10 -1
  245. package/src/public-tools.ts +6 -1
  246. package/src/runtime/query/auto-review/classifier.ts +21 -5
  247. package/src/runtime/query/auto-review/prompt.ts +2 -0
  248. package/src/runtime/query/auto-review/script-evidence.ts +618 -0
  249. package/src/runtime/query/checkpoint.ts +53 -1
  250. package/src/runtime/query/executor.ts +159 -19
  251. package/src/runtime/query/finalize-turn.ts +8 -1
  252. package/src/runtime/query/index.ts +19 -0
  253. package/src/runtime/query/iteration/index.ts +167 -14
  254. package/src/runtime/query/iteration/phases/compaction.ts +53 -3
  255. package/src/runtime/query/iteration/phases/context.ts +6 -0
  256. package/src/runtime/query/iteration/stream-turn.ts +3 -3
  257. package/src/runtime/query/prelude-lease.ts +19 -0
  258. package/src/runtime/query/prepare-turn.ts +13 -4
  259. package/src/runtime/query/review-policy.ts +33 -0
  260. package/src/runtime/query/tooling.ts +3 -0
  261. package/src/runtime/query/turn/completion-audit.ts +76 -5
  262. package/src/runtime/query/turn/outcome-ledger.ts +633 -0
  263. package/src/sandbox/file-walk.ts +38 -17
  264. package/src/sandbox/glob-segment.ts +309 -0
  265. package/src/store/session-log/index.ts +7 -0
  266. package/src/store/session-log/turn-outcome.ts +84 -0
  267. package/src/tools/builtins/bash.ts +1 -1
  268. package/src/tools/builtins/edit-apply.ts +26 -4
  269. package/src/tools/builtins/edit.ts +106 -41
  270. package/src/tools/builtins/glob.ts +1 -1
  271. package/src/tools/builtins/grep.ts +1 -1
  272. package/src/tools/builtins/notebook-guard.ts +76 -0
  273. package/src/tools/builtins/read-render.ts +12 -2
  274. package/src/tools/builtins/write-file.ts +12 -1
  275. package/src/tools/coordinator/agent.ts +31 -7
  276. package/src/tools/coordinator/index.ts +6 -1
  277. package/src/tools/coordinator/outcome-record.ts +93 -0
  278. package/src/tools/defineTool.ts +3 -0
  279. package/src/tools/file-read-tracker.ts +51 -2
  280. package/src/tools/untrusted-envelope.ts +23 -1
  281. package/src/toolsets/manager.ts +6 -10
  282. package/src/toolsets/validation-issues.ts +97 -0
  283. package/src/turn/memory-recall.ts +28 -0
  284. package/src/types/agent/base.ts +20 -0
  285. package/src/types/agent/query.ts +2 -0
  286. package/src/types/common/index.ts +25 -1
  287. package/src/types/hitl/index.ts +27 -0
  288. package/src/types/session/config.ts +20 -0
  289. package/src/types/session/events.ts +3 -3
  290. package/src/types/session/index.ts +1 -0
  291. package/src/types/session/outcome.ts +139 -0
  292. package/src/types/session/prepare-step.ts +2 -0
  293. package/src/types/session/records.ts +67 -1
  294. package/src/types/session/turn.ts +19 -0
  295. package/src/types/tool/index.ts +30 -0
@@ -0,0 +1,633 @@
1
+ /**
2
+ * The turn's outcome record: what changed in the workspace, what was run to
3
+ * check it, and whether the last word of each check was said about the
4
+ * workspace as the turn left it.
5
+ *
6
+ * It is derived from the turn's own tool calls and, when the host supplies a
7
+ * change detector, from the workspace itself. Never from what the model says.
8
+ * Nothing here blocks or prolongs a turn; the completion audit keeps its own
9
+ * rule. Every uncertainty is reported as uncertainty: a check whose exit status
10
+ * something else may have decided is `masked`, not `passed`; a detector that
11
+ * cannot answer is `null`, never a match.
12
+ */
13
+
14
+ import { isReadOnlyCommandLine } from '../../../authorization/readonly-commands.js'
15
+ import { lexShellCommandLine } from '../../../authorization/shell-lexer.js'
16
+ import {
17
+ OUTCOME_DEFAULT_DETECT_TIMEOUT_MS,
18
+ OUTCOME_MAX_CHECKS,
19
+ OUTCOME_MAX_COMMAND_CHARS,
20
+ OUTCOME_MAX_PATHS,
21
+ OUTCOME_MAX_PATH_CHARS,
22
+ type TurnOutcome,
23
+ type TurnOutcomeCheck,
24
+ type TurnOutcomeCheckResult,
25
+ type TurnOutcomeConfig,
26
+ type TurnOutcomeVerdict,
27
+ } from '../../../types/session/outcome.js'
28
+ import type { ToolFacts } from '../../../types/tool/index.js'
29
+ import { WRITING_TOOLS, isVerificationCommand } from './completion-audit.js'
30
+
31
+ /** A finished tool call as the ledger sees it. */
32
+ export interface OutcomeCall {
33
+ readonly toolName: string
34
+ /** The call's parsed input, when it had one. */
35
+ readonly input?: unknown
36
+ readonly isError?: boolean
37
+ /** A hook stopped it before it ran. */
38
+ readonly skipped?: boolean
39
+ /** The tool declared this call read-only (or it only keeps the task list). */
40
+ readonly readOnly?: boolean
41
+ /** What the call did, as typed fields. Absent when a hook replaced the output. */
42
+ readonly facts?: ToolFacts
43
+ }
44
+
45
+ /** A call about to run: enough to tell whether it could change the workspace. */
46
+ export interface PendingOutcomeCall {
47
+ readonly toolName: string
48
+ readonly input?: unknown
49
+ readonly readOnly?: boolean
50
+ }
51
+
52
+ /** Beyond these the ledger counts but no longer remembers. */
53
+ const MAX_REMEMBERED_PATHS = 1000
54
+ const MAX_REMEMBERED_CHECKS = 200
55
+
56
+ /** Programs that print or search text. A line that only names a check to one of them does not run it. */
57
+ const NOT_A_RUNNER = new Set([
58
+ 'echo',
59
+ 'printf',
60
+ 'cat',
61
+ 'grep',
62
+ 'rg',
63
+ 'sed',
64
+ 'awk',
65
+ 'head',
66
+ 'tail',
67
+ 'less',
68
+ 'more',
69
+ 'man',
70
+ 'which',
71
+ 'type',
72
+ 'command',
73
+ 'ls',
74
+ 'find',
75
+ 'wc',
76
+ 'true',
77
+ 'false',
78
+ 'test',
79
+ '[',
80
+ ':',
81
+ ])
82
+
83
+ /** Package managers whose `install`-like subcommands name a check word without running it. */
84
+ const PACKAGE_MANAGERS = new Set([
85
+ 'npm',
86
+ 'pnpm',
87
+ 'yarn',
88
+ 'bun',
89
+ 'pip',
90
+ 'pip3',
91
+ 'uv',
92
+ 'poetry',
93
+ 'cargo',
94
+ 'composer',
95
+ 'gem',
96
+ ])
97
+ const INSTALLING = new Set([
98
+ 'install',
99
+ 'add',
100
+ 'i',
101
+ 'ci',
102
+ 'remove',
103
+ 'rm',
104
+ 'uninstall',
105
+ 'update',
106
+ 'upgrade',
107
+ ])
108
+ const HELP_OR_VERSION = new Set(['--help', '-h', '--version', '-V', '--list', '--listTests'])
109
+ /** `-v` is verbose to these, a version flag to the rest. */
110
+ const VERBOSE_V = new Set(['pytest', 'py.test', 'mocha', 'vitest', 'jest'])
111
+
112
+ const SNAPSHOT_VERSION = 1
113
+
114
+ interface CheckState {
115
+ key: string
116
+ command: string
117
+ step: number
118
+ result: TurnOutcomeCheckResult
119
+ exitCode?: number
120
+ /** A successful write tool call came after this run. */
121
+ afterWrite: boolean
122
+ /** A call that could change the workspace, and is not a write tool, came after this run. */
123
+ afterUnknown: boolean
124
+ /** The fingerprint taken when it finished; `null` when it could not be taken or was not trustworthy. */
125
+ fingerprint: string | null | undefined
126
+ }
127
+
128
+ /** The part of the ledger a checkpoint keeps, so a resumed turn does not forget what came before it. */
129
+ export interface OutcomeLedgerSnapshot {
130
+ readonly v: typeof SNAPSHOT_VERSION
131
+ readonly started: boolean
132
+ readonly startFingerprint: string | null
133
+ readonly paths: readonly string[]
134
+ readonly writes: number
135
+ /** Absent in a snapshot written before it existed. */
136
+ readonly unseen?: boolean
137
+ /** Absent in a snapshot written before it existed. */
138
+ readonly pathless?: number
139
+ readonly checks: readonly {
140
+ readonly key: string
141
+ readonly command: string
142
+ readonly step: number
143
+ readonly result: TurnOutcomeCheckResult
144
+ readonly exitCode?: number
145
+ readonly afterWrite: boolean
146
+ readonly afterUnknown: boolean
147
+ readonly fingerprint: string | null
148
+ }[]
149
+ }
150
+
151
+ function clip(text: string, max: number): string {
152
+ return text.length > max ? `${text.slice(0, max)}…` : text
153
+ }
154
+
155
+ function normalise(command: string): string {
156
+ return command.trim().replace(/\s+/g, ' ')
157
+ }
158
+
159
+ function commandOf(input: unknown): string | undefined {
160
+ const command = (input as { command?: unknown } | undefined)?.command
161
+ return typeof command === 'string' ? command : undefined
162
+ }
163
+
164
+ function pathOf(call: OutcomeCall): string | undefined {
165
+ if (call.facts?.kind === 'edit' || call.facts?.kind === 'write') {
166
+ if (call.facts.path !== undefined) return call.facts.path
167
+ }
168
+ const input = call.input as Record<string, unknown> | undefined
169
+ for (const key of ['path', 'file_path', 'filePath', 'notebook_path']) {
170
+ const value = input?.[key]
171
+ if (typeof value === 'string' && value !== '') return value
172
+ }
173
+ return undefined
174
+ }
175
+
176
+ /**
177
+ * Whether the line's exit status is its last check's own: every command of the
178
+ * line is a plain command, joined to the others only by `&&`, with nothing
179
+ * before the first, and nothing after the last but a closing `;`. A pipe, a
180
+ * `;` between commands, `||`, a trailing `&`, a group, a nested shell or
181
+ * anything the lexer cannot read to the end lets something else decide the
182
+ * status, and the answer is no. `a && b` exits 0 only when both did.
183
+ *
184
+ * The bash tool does not set `pipefail`, so `pnpm test | tail` exits with
185
+ * `tail`'s status.
186
+ */
187
+ function exitStatusIsOwn(line: string): boolean {
188
+ const lexed = lexShellCommandLine(line)
189
+ if (!lexed.complete || lexed.opaque || lexed.commands.length === 0) return false
190
+ if (lexed.compoundWords.length > 0) return false
191
+ let cursor = 0
192
+ for (const [index, command] of lexed.commands.entries()) {
193
+ if (command.origin !== 'line' || command.depth !== 0) return false
194
+ const at = line.indexOf(command.text, cursor)
195
+ if (at < 0) return false
196
+ const gap = line.slice(cursor, at).trim()
197
+ if (index === 0 ? gap !== '' : gap !== '&&') return false
198
+ cursor = at + command.text.length
199
+ }
200
+ const tail = line.slice(cursor).trim()
201
+ return tail === '' || tail === ';'
202
+ }
203
+
204
+ /**
205
+ * Whether a simple command, as its words, only installs, removes, or asks for
206
+ * help or a version. Such a command names a check word (`lint-staged`,
207
+ * `eslint --version`) and establishes nothing about the work.
208
+ */
209
+ function establishesNothing(values: readonly string[]): boolean {
210
+ const program = (values[0] ?? '').split('/').pop() ?? ''
211
+ const args = values.slice(1)
212
+ if (args.some((word) => HELP_OR_VERSION.has(word))) return true
213
+ if (args.includes('-v') && !VERBOSE_V.has(program)) return true
214
+ if (PACKAGE_MANAGERS.has(program) && args.slice(0, 3).some((word) => INSTALLING.has(word))) {
215
+ return true
216
+ }
217
+ return false
218
+ }
219
+
220
+ function isNamedCheck(text: string, hostIsCheck?: (command: string) => boolean): boolean {
221
+ if (isVerificationCommand(text)) return true
222
+ try {
223
+ return hostIsCheck?.(text) === true
224
+ } catch {
225
+ return false
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Whether the line holds a command that could change the workspace and is not
231
+ * itself a check: `pnpm test && sed -i s/a/b/ f`. A line the lexer cannot read
232
+ * to the end counts, because what it runs is not known.
233
+ */
234
+ function changesBeyondChecks(line: string, hostIsCheck?: (command: string) => boolean): boolean {
235
+ const lexed = lexShellCommandLine(line)
236
+ if (!lexed.complete || lexed.opaque) return true
237
+ for (const command of lexed.commands) {
238
+ const values = command.words.slice(command.assignments).map((word) => word.value)
239
+ const program = (values[0] ?? '').split('/').pop() ?? ''
240
+ const isCheck =
241
+ !NOT_A_RUNNER.has(program) &&
242
+ !establishesNothing(values) &&
243
+ isNamedCheck(command.text, hostIsCheck)
244
+ if (isCheck) continue
245
+ if (isReadOnlyCommandLine(command.text)) continue
246
+ return true
247
+ }
248
+ return false
249
+ }
250
+
251
+ /** Whether some simple command of the line is a recognised check that is not merely named. */
252
+ function runsRecognisedCheck(line: string, hostIsCheck?: (command: string) => boolean): boolean {
253
+ const lexed = lexShellCommandLine(line)
254
+ if (!lexed.complete || lexed.opaque) {
255
+ return !establishesNothing(line.trim().split(/\s+/)) && isNamedCheck(line, hostIsCheck)
256
+ }
257
+ for (const command of lexed.commands) {
258
+ const values = command.words.slice(command.assignments).map((word) => word.value)
259
+ const program = (values[0] ?? '').split('/').pop() ?? ''
260
+ if (NOT_A_RUNNER.has(program)) continue
261
+ if (establishesNothing(values)) continue
262
+ if (isNamedCheck(command.text, hostIsCheck)) return true
263
+ }
264
+ return false
265
+ }
266
+
267
+ export class OutcomeLedger {
268
+ private readonly config: TurnOutcomeConfig
269
+ private started = false
270
+ private startFingerprint: string | null = null
271
+ private readonly paths = new Set<string>()
272
+ private extraPaths = 0
273
+ /** Successful writes whose path could not be read; each is a change that cannot be named. */
274
+ private pathless = 0
275
+ /** A call ran that could change the workspace and is not a write tool: only the detector could say whether it did. */
276
+ private couldHaveChangedUnseen = false
277
+ private writes = 0
278
+ private readonly checks = new Map<string, CheckState>()
279
+ private unrecorded = false
280
+ private settled: TurnOutcome | undefined
281
+
282
+ constructor(config: TurnOutcomeConfig = {}) {
283
+ this.config = config
284
+ }
285
+
286
+ /** False once the record is known to be missing what came before a resume. */
287
+ get recorded(): boolean {
288
+ return !this.unrecorded
289
+ }
290
+
291
+ /**
292
+ * The turn resumed from a checkpoint that carries no ledger. What happened
293
+ * before it is unknown, and a record that began as `unchanged` would be a
294
+ * lie, so the turn reports no outcome at all.
295
+ */
296
+ markUnrecorded(): void {
297
+ this.unrecorded = true
298
+ }
299
+
300
+ /** Take back what a checkpoint kept. A snapshot that does not parse leaves the turn unrecorded. */
301
+ restore(snapshot: unknown): void {
302
+ const parsed = parseSnapshot(snapshot)
303
+ if (parsed === undefined) {
304
+ this.unrecorded = true
305
+ return
306
+ }
307
+ this.started = parsed.started
308
+ this.startFingerprint = parsed.startFingerprint
309
+ for (const path of parsed.paths) this.paths.add(path)
310
+ this.writes = parsed.writes
311
+ this.pathless = parsed.pathless ?? 0
312
+ this.couldHaveChangedUnseen = parsed.unseen === true
313
+ this.checks.clear()
314
+ for (const check of parsed.checks) {
315
+ this.checks.set(check.key, { ...check, fingerprint: check.fingerprint })
316
+ }
317
+ }
318
+
319
+ /** What a checkpoint should keep. */
320
+ snapshot(): OutcomeLedgerSnapshot {
321
+ return {
322
+ v: SNAPSHOT_VERSION,
323
+ started: this.started,
324
+ startFingerprint: this.startFingerprint,
325
+ paths: [...this.paths],
326
+ writes: this.writes,
327
+ unseen: this.couldHaveChangedUnseen,
328
+ pathless: this.pathless,
329
+ checks: [...this.checks.values()].map((check) => ({
330
+ key: check.key,
331
+ command: check.command,
332
+ step: check.step,
333
+ result: check.result,
334
+ ...(check.exitCode !== undefined ? { exitCode: check.exitCode } : {}),
335
+ afterWrite: check.afterWrite,
336
+ afterUnknown: check.afterUnknown,
337
+ fingerprint: check.fingerprint ?? null,
338
+ })),
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Whether the call could change the workspace. Anything not provably
344
+ * read-only counts: a tool that does not say, a shell line that is not on
345
+ * the read-only list.
346
+ */
347
+ private couldChange(call: PendingOutcomeCall): boolean {
348
+ if (call.readOnly === true) return false
349
+ if (call.toolName === 'bash') {
350
+ const command = commandOf(call.input)
351
+ if (command !== undefined && isReadOnlyCommandLine(command)) return false
352
+ }
353
+ return true
354
+ }
355
+
356
+ private async detect(): Promise<string | null> {
357
+ const detect = this.config.detectChanges
358
+ if (detect === undefined) return null
359
+ const deadline = this.config.detectTimeoutMs ?? OUTCOME_DEFAULT_DETECT_TIMEOUT_MS
360
+ let timer: ReturnType<typeof setTimeout> | undefined
361
+ try {
362
+ const asked = Promise.resolve().then(detect)
363
+ // A detector that settles after its deadline must not surface later.
364
+ asked.catch(() => {})
365
+ const late = new Promise<null>((resolve) => {
366
+ timer = setTimeout(() => resolve(null), deadline)
367
+ })
368
+ const answer = await Promise.race([asked, late])
369
+ return typeof answer === 'string' ? answer : null
370
+ } catch {
371
+ return null
372
+ } finally {
373
+ if (timer !== undefined) clearTimeout(timer)
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Called with a step's calls before any of them runs. The first call of the
379
+ * turn that could change the workspace takes the starting fingerprint; a turn
380
+ * of provably read-only calls never asks the detector anything.
381
+ */
382
+ async before(calls: readonly PendingOutcomeCall[]): Promise<void> {
383
+ if (this.started || this.config.detectChanges === undefined) return
384
+ if (!calls.some((call) => this.couldChange(call))) return
385
+ this.started = true
386
+ this.startFingerprint = await this.detect()
387
+ }
388
+
389
+ /** Feed the calls of one finished step, in order. */
390
+ async observe(step: number, calls: readonly OutcomeCall[]): Promise<void> {
391
+ // Checks that finished in this step with nothing after them that could
392
+ // change the workspace: the detector, asked once at the end of the step,
393
+ // answers for exactly the state they left.
394
+ let pending: CheckState[] = []
395
+ for (const call of calls) {
396
+ if (call.skipped === true) continue
397
+ const written = WRITING_TOOLS.has(call.toolName)
398
+ if (written && call.isError !== true) {
399
+ if (call.facts?.kind === 'write' && call.facts.operation === 'unchanged') continue
400
+ this.recordWrite(call)
401
+ this.afterEveryCheck('write')
402
+ for (const check of pending) check.fingerprint = null
403
+ pending = []
404
+ continue
405
+ }
406
+ if (written) continue
407
+ const command = call.toolName === 'bash' ? commandOf(call.input) : undefined
408
+ if (command !== undefined && runsRecognisedCheck(command, this.config.isCheck)) {
409
+ this.afterEveryCheck('other')
410
+ for (const check of pending) check.fingerprint = null
411
+ pending = []
412
+ const state = this.recordCheck(step, command, call)
413
+ // `pnpm test && sed -i ...`: the line changed something after the check.
414
+ if (changesBeyondChecks(command, this.config.isCheck)) {
415
+ state.afterUnknown = true
416
+ this.couldHaveChangedUnseen = true
417
+ }
418
+ pending.push(state)
419
+ continue
420
+ }
421
+ if (this.couldChange(call)) {
422
+ this.couldHaveChangedUnseen = true
423
+ this.afterEveryCheck('other')
424
+ for (const check of pending) check.fingerprint = null
425
+ pending = []
426
+ }
427
+ }
428
+ if (pending.length > 0 && this.started && this.startFingerprint !== null) {
429
+ const fingerprint = await this.detect()
430
+ for (const check of pending) check.fingerprint = fingerprint
431
+ }
432
+ }
433
+
434
+ private afterEveryCheck(kind: 'write' | 'other'): void {
435
+ for (const check of this.checks.values()) {
436
+ if (kind === 'write') check.afterWrite = true
437
+ else check.afterUnknown = true
438
+ }
439
+ }
440
+
441
+ private recordWrite(call: OutcomeCall): void {
442
+ this.writes += 1
443
+ const path = pathOf(call)
444
+ if (path === undefined) {
445
+ this.pathless += 1
446
+ return
447
+ }
448
+ const clipped = clip(path, OUTCOME_MAX_PATH_CHARS)
449
+ if (this.paths.has(clipped)) return
450
+ if (this.paths.size >= MAX_REMEMBERED_PATHS) {
451
+ this.extraPaths += 1
452
+ return
453
+ }
454
+ this.paths.add(clipped)
455
+ }
456
+
457
+ private recordCheck(step: number, command: string, call: OutcomeCall): CheckState {
458
+ const key = normalise(command)
459
+ const { result, exitCode } = this.judge(command, call)
460
+ const state: CheckState = {
461
+ key,
462
+ command: clip(key, OUTCOME_MAX_COMMAND_CHARS),
463
+ step,
464
+ result,
465
+ ...(exitCode !== undefined ? { exitCode } : {}),
466
+ afterWrite: false,
467
+ afterUnknown: false,
468
+ // Not asked for yet: `undefined` until the step's detector call answers.
469
+ fingerprint: undefined,
470
+ }
471
+ // A failure stands until the same command passes. A later run that never
472
+ // settled (background, timed out) says nothing about the failure.
473
+ const earlier = this.checks.get(key)
474
+ if (earlier?.result === 'failed' && state.result !== 'passed') return earlier
475
+ // Latest run of each distinct command, ordered by when it last ran.
476
+ this.checks.delete(key)
477
+ this.checks.set(key, state)
478
+ if (this.checks.size > MAX_REMEMBERED_CHECKS) {
479
+ // A failing check is never the one forgotten.
480
+ for (const [candidate, held] of this.checks) {
481
+ if (held.result !== 'failed' && candidate !== key) {
482
+ this.checks.delete(candidate)
483
+ break
484
+ }
485
+ }
486
+ }
487
+ return state
488
+ }
489
+
490
+ private judge(
491
+ command: string,
492
+ call: OutcomeCall,
493
+ ): { result: TurnOutcomeCheckResult; exitCode?: number } {
494
+ const facts = call.facts
495
+ if (facts?.kind === 'failure') return { result: 'failed' }
496
+ if (facts?.kind !== 'command') {
497
+ // No typed record of the run: a hook replaced the output. An error is
498
+ // still an error; anything else says nothing about the exit status.
499
+ return { result: call.isError === true ? 'failed' : 'unsettled' }
500
+ }
501
+ const exitCode = facts.exitCode
502
+ const withCode = exitCode !== undefined ? { exitCode } : {}
503
+ if (facts.background === true || facts.continued === true || facts.timedOut === true) {
504
+ return { result: 'unsettled', ...withCode }
505
+ }
506
+ if (exitCode === undefined) {
507
+ return { result: call.isError === true ? 'failed' : 'unsettled' }
508
+ }
509
+ if (exitCode === 0) {
510
+ if (call.isError === true) return { result: 'unsettled', exitCode }
511
+ return { result: exitStatusIsOwn(command) ? 'passed' : 'masked', exitCode }
512
+ }
513
+ if (!exitStatusIsOwn(command)) return { result: 'masked', exitCode }
514
+ // A non-zero exit the command's own answer (`grep` found nothing) is not a
515
+ // failure, and not a pass either.
516
+ if (facts.exitNote !== undefined && call.isError !== true) {
517
+ return { result: 'unsettled', exitCode }
518
+ }
519
+ return { result: 'failed', exitCode }
520
+ }
521
+
522
+ /** The outcome, once, when the turn ends. `undefined` when the record is not whole. */
523
+ async finish(): Promise<TurnOutcome | undefined> {
524
+ if (this.unrecorded) return undefined
525
+ if (this.settled !== undefined) return this.settled
526
+ let endFingerprint: string | null = null
527
+ if (this.started) endFingerprint = await this.detect()
528
+ const outcome = this.compute(endFingerprint)
529
+ this.settled = outcome
530
+ return outcome
531
+ }
532
+
533
+ /**
534
+ * The record as it stands, on the calls basis, without asking the detector
535
+ * and without settling: the turn goes on and `finish` still answers once.
536
+ */
537
+ peek(): TurnOutcome | undefined {
538
+ if (this.unrecorded) return undefined
539
+ return this.compute(null)
540
+ }
541
+
542
+ private compute(endFingerprint: string | null): TurnOutcome {
543
+ const workspace = this.started && this.startFingerprint !== null && endFingerprint !== null
544
+ const moved = workspace && this.startFingerprint !== endFingerprint
545
+ // A write tool that succeeded changed something, even a file the detector
546
+ // cannot see (an ignored one). Counting it is the cautious direction.
547
+ const changed = this.writes > 0 || moved
548
+ // Without the workspace, a shell command or another tool that may have
549
+ // changed files cannot be ruled out, so "nothing changed" is not said.
550
+ const unseen = !workspace && this.couldHaveChangedUnseen
551
+
552
+ const all = [...this.checks.values()]
553
+ const freshOf = (check: CheckState): boolean | undefined => {
554
+ if (check.afterWrite) return false
555
+ if (workspace && check.fingerprint !== undefined) {
556
+ return check.fingerprint === null ? undefined : check.fingerprint === endFingerprint
557
+ }
558
+ return check.afterUnknown ? undefined : true
559
+ }
560
+ const rows = all.map((check) => ({ check, fresh: freshOf(check) }))
561
+
562
+ let verdict: TurnOutcomeVerdict
563
+ const passes = rows.filter((row) => row.check.result === 'passed')
564
+ if (rows.some((row) => row.check.result === 'failed')) verdict = 'failing'
565
+ else if (!changed) verdict = unseen ? 'unknown' : 'unchanged'
566
+ else if (rows.length === 0) verdict = 'unchecked'
567
+ else if (passes.some((row) => row.fresh === true)) verdict = 'checked'
568
+ else if (passes.length > 0 && passes.every((row) => row.fresh === false)) verdict = 'stale'
569
+ else verdict = 'inconclusive'
570
+
571
+ // A write tool whose path could not be read still changed something.
572
+ const total = this.paths.size + this.extraPaths + this.pathless
573
+ const kept = rows.slice(-OUTCOME_MAX_CHECKS)
574
+ const checks: TurnOutcomeCheck[] = kept.map(({ check, fresh }) => ({
575
+ command: check.command,
576
+ step: check.step,
577
+ result: check.result,
578
+ ...(check.exitCode !== undefined ? { exitCode: check.exitCode } : {}),
579
+ ...(fresh !== undefined ? { fresh } : {}),
580
+ }))
581
+ return {
582
+ verdict,
583
+ basis: workspace ? 'workspace' : 'calls',
584
+ changes: {
585
+ total,
586
+ paths: [...this.paths].slice(0, OUTCOME_MAX_PATHS),
587
+ outsideKnownTools: moved && this.writes === 0,
588
+ },
589
+ checks,
590
+ ...(rows.length > OUTCOME_MAX_CHECKS ? { checksTotal: rows.length } : {}),
591
+ }
592
+ }
593
+ }
594
+
595
+ function parseSnapshot(value: unknown): OutcomeLedgerSnapshot | undefined {
596
+ if (typeof value !== 'object' || value === null) return undefined
597
+ const snap = value as Record<string, unknown>
598
+ if (snap.v !== SNAPSHOT_VERSION || typeof snap.started !== 'boolean') return undefined
599
+ const start = snap.startFingerprint
600
+ if (start !== null && typeof start !== 'string') return undefined
601
+ if (!Array.isArray(snap.paths) || !snap.paths.every((p) => typeof p === 'string'))
602
+ return undefined
603
+ if (typeof snap.writes !== 'number' || !Number.isSafeInteger(snap.writes) || snap.writes < 0) {
604
+ return undefined
605
+ }
606
+ if (snap.unseen !== undefined && typeof snap.unseen !== 'boolean') return undefined
607
+ if (
608
+ snap.pathless !== undefined &&
609
+ (typeof snap.pathless !== 'number' || !Number.isSafeInteger(snap.pathless) || snap.pathless < 0)
610
+ ) {
611
+ return undefined
612
+ }
613
+ if (!Array.isArray(snap.checks)) return undefined
614
+ const results = new Set<string>(['passed', 'failed', 'masked', 'unsettled'])
615
+ for (const raw of snap.checks) {
616
+ if (typeof raw !== 'object' || raw === null) return undefined
617
+ const c = raw as Record<string, unknown>
618
+ if (
619
+ typeof c.key !== 'string' ||
620
+ typeof c.command !== 'string' ||
621
+ typeof c.step !== 'number' ||
622
+ typeof c.result !== 'string' ||
623
+ !results.has(c.result) ||
624
+ (c.exitCode !== undefined && typeof c.exitCode !== 'number') ||
625
+ typeof c.afterWrite !== 'boolean' ||
626
+ typeof c.afterUnknown !== 'boolean' ||
627
+ (c.fingerprint !== null && typeof c.fingerprint !== 'string')
628
+ ) {
629
+ return undefined
630
+ }
631
+ }
632
+ return snap as unknown as OutcomeLedgerSnapshot
633
+ }