@namzu/sdk 62.12.0 → 62.14.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 (248) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/agents/file-definitions.d.ts +7 -0
  3. package/dist/agents/file-definitions.d.ts.map +1 -1
  4. package/dist/agents/file-definitions.js +20 -0
  5. package/dist/agents/file-definitions.js.map +1 -1
  6. package/dist/agents/index.d.ts +1 -1
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +1 -1
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/bridge/acp/server.d.ts +8 -1
  11. package/dist/bridge/acp/server.d.ts.map +1 -1
  12. package/dist/bridge/acp/server.js +12 -0
  13. package/dist/bridge/acp/server.js.map +1 -1
  14. package/dist/connector/index.d.ts +1 -1
  15. package/dist/connector/index.d.ts.map +1 -1
  16. package/dist/connector/index.js +1 -1
  17. package/dist/connector/index.js.map +1 -1
  18. package/dist/connector/mcp/client.d.ts +96 -0
  19. package/dist/connector/mcp/client.d.ts.map +1 -1
  20. package/dist/connector/mcp/client.js +488 -13
  21. package/dist/connector/mcp/client.js.map +1 -1
  22. package/dist/connector/mcp/index.d.ts +1 -0
  23. package/dist/connector/mcp/index.d.ts.map +1 -1
  24. package/dist/connector/mcp/index.js +1 -0
  25. package/dist/connector/mcp/index.js.map +1 -1
  26. package/dist/connector/mcp/tasks.d.ts +40 -0
  27. package/dist/connector/mcp/tasks.d.ts.map +1 -0
  28. package/dist/connector/mcp/tasks.js +55 -0
  29. package/dist/connector/mcp/tasks.js.map +1 -0
  30. package/dist/constants/data-epoch.d.ts +18 -0
  31. package/dist/constants/data-epoch.d.ts.map +1 -0
  32. package/dist/constants/data-epoch.js +18 -0
  33. package/dist/constants/data-epoch.js.map +1 -0
  34. package/dist/constants/index.d.ts +1 -0
  35. package/dist/constants/index.d.ts.map +1 -1
  36. package/dist/constants/index.js +1 -0
  37. package/dist/constants/index.js.map +1 -1
  38. package/dist/constants/mcp/index.d.ts +1 -1
  39. package/dist/constants/mcp/index.d.ts.map +1 -1
  40. package/dist/constants/mcp/index.js +5 -0
  41. package/dist/constants/mcp/index.js.map +1 -1
  42. package/dist/constants/telemetry/index.d.ts +12 -0
  43. package/dist/constants/telemetry/index.d.ts.map +1 -1
  44. package/dist/constants/telemetry/index.js +12 -0
  45. package/dist/constants/telemetry/index.js.map +1 -1
  46. package/dist/eval/bootstrap.d.ts +122 -0
  47. package/dist/eval/bootstrap.d.ts.map +1 -0
  48. package/dist/eval/bootstrap.js +227 -0
  49. package/dist/eval/bootstrap.js.map +1 -0
  50. package/dist/eval/compare.d.ts +109 -0
  51. package/dist/eval/compare.d.ts.map +1 -0
  52. package/dist/eval/compare.js +324 -0
  53. package/dist/eval/compare.js.map +1 -0
  54. package/dist/eval/disclosure.d.ts +70 -0
  55. package/dist/eval/disclosure.d.ts.map +1 -0
  56. package/dist/eval/disclosure.js +64 -0
  57. package/dist/eval/disclosure.js.map +1 -0
  58. package/dist/eval/experiment.d.ts +23 -0
  59. package/dist/eval/experiment.d.ts.map +1 -1
  60. package/dist/eval/experiment.js +87 -15
  61. package/dist/eval/experiment.js.map +1 -1
  62. package/dist/eval/from-turn.d.ts.map +1 -1
  63. package/dist/eval/from-turn.js +5 -0
  64. package/dist/eval/from-turn.js.map +1 -1
  65. package/dist/eval/index.d.ts +13 -2
  66. package/dist/eval/index.d.ts.map +1 -1
  67. package/dist/eval/index.js +6 -1
  68. package/dist/eval/index.js.map +1 -1
  69. package/dist/eval/report-stats.d.ts +77 -0
  70. package/dist/eval/report-stats.d.ts.map +1 -0
  71. package/dist/eval/report-stats.js +184 -0
  72. package/dist/eval/report-stats.js.map +1 -0
  73. package/dist/eval/run-options.d.ts +30 -0
  74. package/dist/eval/run-options.d.ts.map +1 -0
  75. package/dist/eval/run-options.js +20 -0
  76. package/dist/eval/run-options.js.map +1 -0
  77. package/dist/eval/run-summary.d.ts +36 -0
  78. package/dist/eval/run-summary.d.ts.map +1 -0
  79. package/dist/eval/run-summary.js +57 -0
  80. package/dist/eval/run-summary.js.map +1 -0
  81. package/dist/eval/scorers.d.ts +32 -0
  82. package/dist/eval/scorers.d.ts.map +1 -1
  83. package/dist/eval/scorers.js +77 -0
  84. package/dist/eval/scorers.js.map +1 -1
  85. package/dist/eval/types.d.ts +74 -0
  86. package/dist/eval/types.d.ts.map +1 -1
  87. package/dist/manager/session/turn-recorder.d.ts +3 -0
  88. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  89. package/dist/manager/session/turn-recorder.js +18 -2
  90. package/dist/manager/session/turn-recorder.js.map +1 -1
  91. package/dist/plugin/index.d.ts +1 -1
  92. package/dist/plugin/index.d.ts.map +1 -1
  93. package/dist/plugin/index.js +1 -1
  94. package/dist/plugin/index.js.map +1 -1
  95. package/dist/plugin/loader.d.ts +11 -0
  96. package/dist/plugin/loader.d.ts.map +1 -1
  97. package/dist/plugin/loader.js +15 -2
  98. package/dist/plugin/loader.js.map +1 -1
  99. package/dist/public-runtime.d.ts +10 -6
  100. package/dist/public-runtime.d.ts.map +1 -1
  101. package/dist/public-runtime.js +13 -5
  102. package/dist/public-runtime.js.map +1 -1
  103. package/dist/public-types.d.ts +2 -0
  104. package/dist/public-types.d.ts.map +1 -1
  105. package/dist/registry/command/kernel-commands.d.ts +3 -0
  106. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  107. package/dist/registry/command/kernel-commands.js +76 -5
  108. package/dist/registry/command/kernel-commands.js.map +1 -1
  109. package/dist/runtime/query/cancelled-before-start.d.ts.map +1 -1
  110. package/dist/runtime/query/cancelled-before-start.js +2 -0
  111. package/dist/runtime/query/cancelled-before-start.js.map +1 -1
  112. package/dist/runtime/query/index.d.ts.map +1 -1
  113. package/dist/runtime/query/index.js +17 -0
  114. package/dist/runtime/query/index.js.map +1 -1
  115. package/dist/runtime/query/iteration/index.d.ts +18 -1
  116. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  117. package/dist/runtime/query/iteration/index.js +80 -1
  118. package/dist/runtime/query/iteration/index.js.map +1 -1
  119. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  120. package/dist/runtime/query/iteration/phases/tool-review.js +18 -1
  121. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  122. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  123. package/dist/runtime/query/iteration/stream-turn.js +5 -0
  124. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  125. package/dist/runtime/query/result.d.ts.map +1 -1
  126. package/dist/runtime/query/result.js +2 -0
  127. package/dist/runtime/query/result.js.map +1 -1
  128. package/dist/runtime/query/turn/answer-claims.d.ts +52 -0
  129. package/dist/runtime/query/turn/answer-claims.d.ts.map +1 -0
  130. package/dist/runtime/query/turn/answer-claims.js +348 -0
  131. package/dist/runtime/query/turn/answer-claims.js.map +1 -0
  132. package/dist/runtime/query/turn/outcome-ledger.d.ts +7 -0
  133. package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -1
  134. package/dist/runtime/query/turn/outcome-ledger.js +13 -0
  135. package/dist/runtime/query/turn/outcome-ledger.js.map +1 -1
  136. package/dist/skills/index.d.ts +1 -1
  137. package/dist/skills/index.d.ts.map +1 -1
  138. package/dist/skills/index.js +1 -1
  139. package/dist/skills/index.js.map +1 -1
  140. package/dist/skills/loader.d.ts +15 -1
  141. package/dist/skills/loader.d.ts.map +1 -1
  142. package/dist/skills/loader.js +17 -3
  143. package/dist/skills/loader.js.map +1 -1
  144. package/dist/store/goal/index.d.ts +3 -1
  145. package/dist/store/goal/index.d.ts.map +1 -1
  146. package/dist/store/goal/index.js +6 -0
  147. package/dist/store/goal/index.js.map +1 -1
  148. package/dist/telemetry/genai.d.ts +98 -0
  149. package/dist/telemetry/genai.d.ts.map +1 -0
  150. package/dist/telemetry/genai.js +189 -0
  151. package/dist/telemetry/genai.js.map +1 -0
  152. package/dist/telemetry/runtime-accessors.d.ts +19 -0
  153. package/dist/telemetry/runtime-accessors.d.ts.map +1 -1
  154. package/dist/telemetry/runtime-accessors.js +43 -1
  155. package/dist/telemetry/runtime-accessors.js.map +1 -1
  156. package/dist/tools/goal/confirmation.d.ts +154 -0
  157. package/dist/tools/goal/confirmation.d.ts.map +1 -0
  158. package/dist/tools/goal/confirmation.js +290 -0
  159. package/dist/tools/goal/confirmation.js.map +1 -0
  160. package/dist/tools/goal/index.d.ts +4 -1
  161. package/dist/tools/goal/index.d.ts.map +1 -1
  162. package/dist/tools/goal/index.js +27 -2
  163. package/dist/tools/goal/index.js.map +1 -1
  164. package/dist/tools/hand-back/index.d.ts +48 -0
  165. package/dist/tools/hand-back/index.d.ts.map +1 -0
  166. package/dist/tools/hand-back/index.js +129 -0
  167. package/dist/tools/hand-back/index.js.map +1 -0
  168. package/dist/tools/schedules/schedule-tool.d.ts.map +1 -1
  169. package/dist/tools/schedules/schedule-tool.js +15 -1
  170. package/dist/tools/schedules/schedule-tool.js.map +1 -1
  171. package/dist/tools/schedules/types.d.ts +10 -0
  172. package/dist/tools/schedules/types.d.ts.map +1 -1
  173. package/dist/toolsets/manager.d.ts.map +1 -1
  174. package/dist/toolsets/manager.js +12 -0
  175. package/dist/toolsets/manager.js.map +1 -1
  176. package/dist/types/acp/index.d.ts +18 -0
  177. package/dist/types/acp/index.d.ts.map +1 -1
  178. package/dist/types/connector/mcp.d.ts +18 -0
  179. package/dist/types/connector/mcp.d.ts.map +1 -1
  180. package/dist/types/goal/index.d.ts +12 -0
  181. package/dist/types/goal/index.d.ts.map +1 -1
  182. package/dist/types/session/config.d.ts +8 -0
  183. package/dist/types/session/config.d.ts.map +1 -1
  184. package/dist/types/session/hand-back.d.ts +29 -0
  185. package/dist/types/session/hand-back.d.ts.map +1 -0
  186. package/dist/types/session/hand-back.js +11 -0
  187. package/dist/types/session/hand-back.js.map +1 -0
  188. package/dist/types/session/outcome.d.ts +37 -0
  189. package/dist/types/session/outcome.d.ts.map +1 -1
  190. package/dist/types/session/outcome.js.map +1 -1
  191. package/dist/types/session/turn.d.ts +19 -1
  192. package/dist/types/session/turn.d.ts.map +1 -1
  193. package/dist/types/session/turn.js.map +1 -1
  194. package/package.json +1 -1
  195. package/src/agents/file-definitions.ts +21 -0
  196. package/src/agents/index.ts +1 -0
  197. package/src/bridge/acp/server.ts +20 -0
  198. package/src/connector/index.ts +1 -0
  199. package/src/connector/mcp/client.ts +531 -14
  200. package/src/connector/mcp/index.ts +1 -0
  201. package/src/connector/mcp/tasks.ts +81 -0
  202. package/src/constants/data-epoch.ts +17 -0
  203. package/src/constants/index.ts +1 -0
  204. package/src/constants/mcp/index.ts +6 -1
  205. package/src/constants/telemetry/index.ts +13 -0
  206. package/src/eval/bootstrap.ts +314 -0
  207. package/src/eval/compare.ts +473 -0
  208. package/src/eval/disclosure.ts +136 -0
  209. package/src/eval/experiment.ts +127 -15
  210. package/src/eval/from-turn.ts +5 -0
  211. package/src/eval/index.ts +51 -0
  212. package/src/eval/report-stats.ts +257 -0
  213. package/src/eval/run-options.ts +49 -0
  214. package/src/eval/run-summary.ts +104 -0
  215. package/src/eval/scorers.ts +83 -0
  216. package/src/eval/types.ts +76 -0
  217. package/src/manager/session/turn-recorder.ts +23 -2
  218. package/src/plugin/index.ts +7 -1
  219. package/src/plugin/loader.ts +16 -2
  220. package/src/public-runtime.ts +70 -0
  221. package/src/public-types.ts +9 -0
  222. package/src/registry/command/kernel-commands.ts +94 -6
  223. package/src/runtime/query/cancelled-before-start.ts +2 -0
  224. package/src/runtime/query/index.ts +20 -0
  225. package/src/runtime/query/iteration/index.ts +85 -1
  226. package/src/runtime/query/iteration/phases/tool-review.ts +24 -1
  227. package/src/runtime/query/iteration/stream-turn.ts +10 -0
  228. package/src/runtime/query/result.ts +2 -0
  229. package/src/runtime/query/turn/answer-claims.ts +413 -0
  230. package/src/runtime/query/turn/outcome-ledger.ts +12 -0
  231. package/src/skills/index.ts +1 -0
  232. package/src/skills/loader.ts +21 -3
  233. package/src/store/goal/index.ts +11 -0
  234. package/src/telemetry/genai.ts +202 -0
  235. package/src/telemetry/runtime-accessors.ts +40 -1
  236. package/src/tools/goal/confirmation.ts +375 -0
  237. package/src/tools/goal/index.ts +37 -1
  238. package/src/tools/hand-back/index.ts +150 -0
  239. package/src/tools/schedules/schedule-tool.ts +19 -1
  240. package/src/tools/schedules/types.ts +10 -0
  241. package/src/toolsets/manager.ts +22 -0
  242. package/src/types/acp/index.ts +20 -0
  243. package/src/types/connector/mcp.ts +18 -0
  244. package/src/types/goal/index.ts +13 -0
  245. package/src/types/session/config.ts +8 -0
  246. package/src/types/session/hand-back.ts +31 -0
  247. package/src/types/session/outcome.ts +42 -0
  248. package/src/types/session/turn.ts +19 -1
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Compare what an answer claims about checks with what the turn's outcome
3
+ * record holds.
4
+ *
5
+ * A pure function: no model call, no clock, no I/O. The model's words only
6
+ * select which statement to check; the statement the kernel then makes is
7
+ * about the record, so it never claims more than the record establishes.
8
+ *
9
+ * It is built to stay silent. A notice that is wrong teaches people to ignore
10
+ * the line, so every doubt resolves to nothing: a negation, a hedge, a
11
+ * condition, a question, a quotation, code, a report of what someone else
12
+ * said, a statement about the past, a qualified claim ("all but one"), and any
13
+ * language but English. The claim wording and the words that cancel it are data
14
+ * below, each with the reason it is there.
15
+ *
16
+ * It looks at the record of THIS turn only. A turn whose record says nothing
17
+ * changed is never reported: "the tests pass" may be true of an earlier turn.
18
+ */
19
+
20
+ import type {
21
+ AnswerClaimKind,
22
+ AnswerClaimMismatch,
23
+ AnswerClaimRecord,
24
+ TurnOutcome,
25
+ } from '../../../types/session/outcome.js'
26
+ import { auditCommandText } from './completion-audit.js'
27
+
28
+ /** Answers longer than this are read only up to it, so cost stays bounded. */
29
+ export const ANSWER_CLAIM_MAX_CHARS = 100_000
30
+
31
+ /** The longest quotation of the answer a mismatch carries, in characters. */
32
+ export const ANSWER_CLAIM_MAX_QUOTE_CHARS = 160
33
+
34
+ /** Words that may sit between a subject and its verb without changing the claim. */
35
+ const FILLER =
36
+ '(?:\\s+(?:all|now|also|still|again|both|are|were|is|have|has|been|currently|locally|fully))*'
37
+
38
+ /** Verbs that say a check came out well. */
39
+ const PASSED = '(?:pass(?:es|ed|ing)?|green|succeed(?:s|ed)?)\\b'
40
+
41
+ /** Determiners and kinds of test that may precede `tests`. */
42
+ const TEST_WORDS =
43
+ '(?:(?:all(?:\\s+of)?|the|my|our)\\s+)?(?:(?:unit|integration|e2e|existing|new|relevant|related|automated)\\s+)*'
44
+
45
+ interface ClaimPattern {
46
+ readonly kind: AnswerClaimKind
47
+ /** The claim in the kernel's words. */
48
+ readonly said: string
49
+ /** Matches the claim inside one clause. */
50
+ readonly re: RegExp
51
+ /** Why this wording, and why it is safe. */
52
+ readonly why: string
53
+ }
54
+
55
+ /**
56
+ * Plain positive claims only. Each pattern needs the subject, at most a few
57
+ * filler words, and the verb, in that order, so a negation or a modal between
58
+ * them ("did not", "should") breaks the match by itself.
59
+ */
60
+ export const ANSWER_CLAIM_PATTERNS: readonly ClaimPattern[] = [
61
+ {
62
+ kind: 'tests',
63
+ said: 'the tests pass',
64
+ re: new RegExp(`\\b${TEST_WORDS}tests\\b${FILLER}\\s+${PASSED}`, 'i'),
65
+ why: 'The plural "tests" as the subject. The singular is left out: "the test now passes" is usually about the one test just fixed, which says nothing about the run that failed.',
66
+ },
67
+ {
68
+ kind: 'tests',
69
+ said: 'the test suite passes',
70
+ re: new RegExp(
71
+ `\\b(?:test\\s+suite|the\\s+suite|(?:whole|full|entire)\\s+(?:test\\s+)?suite|test\\s+run)\\b${FILLER}\\s+${PASSED}`,
72
+ 'i',
73
+ ),
74
+ why: 'The suite as a whole is the same claim as "the tests pass", also as "the whole suite". A bare "suite" is not enough: it also names a benchmark suite or a suite of rooms.',
75
+ },
76
+ {
77
+ kind: 'tests',
78
+ said: 'no test failures',
79
+ re: /\b(?:no|zero|0)\s+(?:test\s+failures|failing\s+tests|failed\s+tests|test\s+errors)\b/i,
80
+ why: 'The same claim said by counting failures. It is matched whole, so the word "no" in it is not read as a negation.',
81
+ },
82
+ {
83
+ kind: 'tests',
84
+ said: 'no test failures',
85
+ re: /\b(?:no\s+tests?\s+(?:are\s+|is\s+)?(?:fail(?:s|ing|ed)?)|tests?\s+(?:are\s+not|aren['\u2019]t|do\s+not|don['\u2019]t|did\s+not|didn['\u2019]t)\s+(?:fail|failing))\b/i,
86
+ why: 'The same claim said as a negated failure: "no tests are failing", "the tests do not fail". The pattern holds its own negation, and the failure word in it is not read as a failure.',
87
+ },
88
+ {
89
+ kind: 'tests',
90
+ said: 'the tests pass',
91
+ re: /\b(?:ran|run|running)\s+(?:(?:all\s+)?(?:of\s+)?(?:the|my|our)\s+)?(?:\w+\s+){0,2}(?:tests|test\s+suite|suite)\s+and\s+(?:they|it)\s+(?:all\s+)?(?:pass(?:es|ed)?|are\s+(?:green|passing))\b/i,
92
+ why: 'The pronoun form, the commonest plain phrasing: "I ran the tests and they all pass". The run and its result are in one clause, and a negation between them ("they did not pass") breaks the match.',
93
+ },
94
+ {
95
+ kind: 'checks',
96
+ said: 'the checks pass',
97
+ re: /^\W*(?:it['\u2019]?s\s+|it\s+is\s+|everything\s+is\s+)?all\s+green\b/i,
98
+ why: 'The shortest summary: "All green." Anchored to the start of the sentence, so "not all green" never matches.',
99
+ },
100
+ {
101
+ kind: 'build',
102
+ said: 'the build succeeds',
103
+ re: /\b(?:it|everything)\s+compiles\b/i,
104
+ why: 'A bare "it compiles". The pronoun or "everything" is required: "the compiles list" and similar nouns are not claims.',
105
+ },
106
+ {
107
+ kind: 'build',
108
+ said: 'the build succeeds',
109
+ re: new RegExp(
110
+ `\\b(?:build|compilation|compile)\\b${FILLER}\\s+(?:succeed(?:s|ed)?|pass(?:es|ed)?|green|completes?|completed|works|worked|is\\s+clean)\\b`,
111
+ 'i',
112
+ ),
113
+ why: 'The build as the subject with a success verb. "The build" alone, as in "the build script", never matches because the verb must follow.',
114
+ },
115
+ {
116
+ kind: 'build',
117
+ said: 'the build succeeds',
118
+ re: /\b(?:builds?|compiles?)\s+(?:cleanly|successfully|fine)\b/i,
119
+ why: 'The adverb form: "it builds cleanly", "everything compiles successfully".',
120
+ },
121
+ {
122
+ kind: 'lint',
123
+ said: 'lint is clean',
124
+ re: new RegExp(
125
+ `\\blint(?:ing|er)?\\b${FILLER}\\s+(?:is\\s+|are\\s+)?(?:clean|pass(?:es|ed|ing)?|green|succeed(?:s|ed)?|ok)\\b`,
126
+ 'i',
127
+ ),
128
+ why: 'Lint as the subject with a clean or passing verb.',
129
+ },
130
+ {
131
+ kind: 'lint',
132
+ said: 'lint is clean',
133
+ re: /\bno\s+lint(?:ing)?\s+(?:errors?|warnings?|issues?|problems?)\b/i,
134
+ why: 'The same claim by counting: "no lint errors". Matched whole, so its own "no" is not a negation.',
135
+ },
136
+ {
137
+ kind: 'typecheck',
138
+ said: 'type checking passes',
139
+ re: new RegExp(
140
+ `\\b(?:type[\\s-]?check(?:ing|s)?|typecheck|tsc)\\b${FILLER}\\s+(?:is\\s+|are\\s+)?(?:clean|pass(?:es|ed|ing)?|green|succeed(?:s|ed)?|ok)\\b`,
141
+ 'i',
142
+ ),
143
+ why: 'Type checking, the one-word spelling and the compiler named for it, as the subject. "Type checker" is left out: "the type checker passes the argument on" names what a checker does, not a result.',
144
+ },
145
+ {
146
+ kind: 'typecheck',
147
+ said: 'type checking passes',
148
+ re: /\bno\s+type\s+errors\b/i,
149
+ why: 'The same claim by counting: "no type errors".',
150
+ },
151
+ {
152
+ kind: 'checks',
153
+ said: 'the checks pass',
154
+ re: new RegExp(
155
+ `\\b(?:all\\s+(?:of\\s+)?(?:the\\s+)?checks|the\\s+(?:ci\\s+)?checks|ci\\s+checks|everything)\\b${FILLER}\\s+${PASSED}`,
156
+ 'i',
157
+ ),
158
+ why: 'The catch-all a summary reaches for: "all checks pass", "the CI checks pass", "everything is green".',
159
+ },
160
+ {
161
+ kind: 'tests',
162
+ said: 'the tests pass',
163
+ re: /^\W*(?:\*\*)?(?:tests?|test\s+suite)(?:\*\*)?\s*[:\-\u2013]\s*(?:\*\*)?\s*(?:all\s+)?(?:\d+\s+)?(?:pass(?:es|ed|ing)?|green|success(?:ful)?|ok|\u2705|\u2714)/i,
164
+ why: 'The label form of a summary list: "Tests: passing". Anchored to the start of the line, where a label stands.',
165
+ },
166
+ {
167
+ kind: 'build',
168
+ said: 'the build succeeds',
169
+ re: /^\W*(?:\*\*)?build(?:\*\*)?\s*[:\-–]\s*(?:\*\*)?\s*(?:pass(?:es|ed|ing)?|green|success(?:ful)?|ok|clean|✅|✔)/i,
170
+ why: 'The label form: "Build: success".',
171
+ },
172
+ {
173
+ kind: 'lint',
174
+ said: 'lint is clean',
175
+ re: /^\W*(?:\*\*)?lint(?:ing)?(?:\*\*)?\s*[:\-–]\s*(?:\*\*)?\s*(?:pass(?:es|ed|ing)?|green|success(?:ful)?|ok|clean|✅|✔)/i,
176
+ why: 'The label form: "Lint: clean".',
177
+ },
178
+ {
179
+ kind: 'typecheck',
180
+ said: 'type checking passes',
181
+ re: /^\W*(?:\*\*)?(?:type[\s-]?check(?:ing)?|typecheck)(?:\*\*)?\s*[:\-–]\s*(?:\*\*)?\s*(?:pass(?:es|ed|ing)?|green|success(?:ful)?|ok|clean|✅|✔)/i,
182
+ why: 'The label form: "Typecheck: passing".',
183
+ },
184
+ ]
185
+
186
+ /** Words that turn the claim around. Looked for in the few words before it. */
187
+ const NEGATION =
188
+ /\b(?:not|no|never|none|nothing|nobody|zero|scarcely|neither|nor|cannot|unable|without|barely|hardly|n't|isn|aren|doesn|don|didn|wasn|weren|won|couldn|can't|won't)\b|n['’]t\b/i
189
+
190
+ /** Words that make it a wish, a plan, a condition, a request or a guess. Looked for in the few words before it. */
191
+ const HEDGE =
192
+ /\b(?:if|once|when|whenever|unless|until|assuming|provided|whether|in\s+case|so\s+that|ensure|ensures|ensuring|make\s+sure|makes\s+sure|make|makes|get|gets|keep|keeps|verify|verifying|confirm|confirming|check\s+(?:that|whether)|hope|hopes|hoping|hopefully|expect|expects|expecting|expected|want|wants|need|needs|must|should|would|could|might|may|can|will|probably|likely|maybe|perhaps|presumably|supposedly|apparently|ideally|seems?|appears?|think|believe|assume|guess|suppose|imagine|wonder|let|try|please|goal|aim|aims|target)\b|'ll\b/i
193
+
194
+ /** Words that report what someone or something else said. Looked for in the whole clause. */
195
+ const REPORTING =
196
+ /\b(?:said|says|say|saying|claims?|claimed|claiming|reports?|reported|reporting|according\s+to|states?|stated|mentions?|mentioned|noted|told|tells|asked|asks|promises?|promised|advertises?|advertised)\b/i
197
+
198
+ /** Words that put the claim in the past, before this turn's change. Looked for in the whole clause. */
199
+ const EARLIER =
200
+ /\b(?:before|earlier|previously|prior|originally|beforehand|initially|yesterday|formerly|used\s+to|at\s+first|last\s+time|last\s+week|until\s+now|in\s+the\s+past|back\s+then|had\s+been)\b/i
201
+
202
+ /**
203
+ * Words that make the claim partial, a guess or someone else's. Looked for in
204
+ * the few words after it. The last few are the plain negation of other languages
205
+ * ("değil", "nicht", "pas") for a clause that holds an English claim.
206
+ */
207
+ const QUALIFIER =
208
+ /\b(?:except|besides|apart|aside|other\s+than|but\s+for|barring|mostly|partly|partially|mainly|almost|nearly|only|if|unless|provided|once|when|until|assuming|so\s+far\s+as|aside\s+from|on\s+ci|in\s+theory|apparently|supposedly|allegedly|presumably|seemingly|reportedly|hopefully|probably|maybe|perhaps|goal|aim|target|de[g\u011f]il|nicht|nein|pas)\b/i
209
+
210
+ /** A failure named anywhere else in the sentence: the sentence is reporting one, not claiming a pass. */
211
+ const FAILURE =
212
+ /\b(?:fail|fails|failed|failing|failure|failures|error|errors|broken|breaks|red|regress(?:ed|es|ion)?)\b/i
213
+
214
+ /**
215
+ * A word anywhere in the sentence that makes every claim in it a guess, or a
216
+ * claim in another language's negation. It sits at sentence level because a
217
+ * comma often separates it from the claim: "The tests pass, apparently."
218
+ */
219
+ const SENTENCE_HEDGE =
220
+ /\b(?:apparently|supposedly|allegedly|presumably|seemingly|reportedly|hopefully|probably|maybe|perhaps|de[g\u011f]il|nicht|nein)\b/i
221
+
222
+ /** Words a sentence may start with that make everything after it conditional. */
223
+ const CONDITIONAL_START =
224
+ /^\W*(?:if|once|when|whenever|unless|assuming|provided|in\s+case|should|were|had)\b/i
225
+
226
+ /** How many words before and after a claim are read for what cancels it. */
227
+ const WORDS_BEFORE = 6
228
+ const WORDS_AFTER = 6
229
+
230
+ /** What a verb that takes an object starts with: "the type checker passes the argument on" is not a result. */
231
+ const OBJECT_AFTER =
232
+ /^\W*(?:the|a|an|its|their|his|her|them|it|this|that|these|those|each|every|any|some|through|along)\b/i
233
+
234
+ /** A claim worded as a subject ("tests passing is the goal"): the verb is a gerund that a copula follows. */
235
+ const COPULA_AFTER = /^\W*(?:is|are|was|were|means|requires|needs)\b/i
236
+
237
+ /** What is left of an answer once the places a claim does not count are taken out. */
238
+ function readableText(answer: string): string {
239
+ return (
240
+ answer
241
+ .slice(0, ANSWER_CLAIM_MAX_CHARS)
242
+ // A fenced block, or one the answer never closed.
243
+ .replace(/(```|~~~)[\s\S]*?(?:\1|$)/g, ' _ ')
244
+ // A block quotation: its lines are somebody else's words.
245
+ .replace(/^[ \t]*>.*$/gm, ' ')
246
+ // An inline code span names a command or a symbol, not a claim.
247
+ .replace(/`[^`\n]{0,400}`/g, ' _ ')
248
+ // A quotation in either kind of double quotes.
249
+ .replace(/"[^"\n]{0,400}"/g, ' _ ')
250
+ .replace(/“[^”\n]{0,400}”/g, ' _ ')
251
+ )
252
+ }
253
+
254
+ /** Sentences, and within one the clauses a claim and its hedge do not cross. */
255
+ function clausesOf(text: string): { sentenceEndsWithQuestion: boolean; clauses: string[] }[] {
256
+ const out: { sentenceEndsWithQuestion: boolean; clauses: string[] }[] = []
257
+ // A semicolon ends a statement, so a failure named in one half does not cancel a claim in the other.
258
+ for (const raw of text.split(/(?<=[.!?])\s+|\n+|;\s*/)) {
259
+ const sentence = raw.trim()
260
+ if (sentence === '') continue
261
+ if (CONDITIONAL_START.test(sentence) || SENTENCE_HEDGE.test(sentence)) continue
262
+ // What the claim patterns themselves match ("no test failures") is not a failure.
263
+ let rest = sentence
264
+ for (const pattern of ANSWER_CLAIM_PATTERNS) {
265
+ rest = rest.replace(new RegExp(pattern.re.source, 'gi'), ' ')
266
+ }
267
+ // "no errors", "without failures": a count of none is not a failure.
268
+ rest = rest.replace(/\b(?:no|zero|0|without)\s+(?:\w+\s+)?(?:errors?|failures?)\b/gi, ' ')
269
+ if (FAILURE.test(rest)) continue
270
+ out.push({
271
+ sentenceEndsWithQuestion: /\?\W*$/.test(sentence),
272
+ // A comma, a semicolon, a dash, a bracket, or a word that turns the sentence around.
273
+ clauses: sentence.split(
274
+ /[,;()—]|\s-\s|\s–\s|\b(?:but|however|although|though|while|whereas|yet|because|since|so)\b/i,
275
+ ),
276
+ })
277
+ }
278
+ return out
279
+ }
280
+
281
+ function wordsBefore(clause: string, at: number): string {
282
+ return clause.slice(0, at).trim().split(/\s+/).slice(-WORDS_BEFORE).join(' ')
283
+ }
284
+
285
+ function wordsAfter(clause: string, to: number): string {
286
+ return clause.slice(to).trim().split(/\s+/).slice(0, WORDS_AFTER).join(' ')
287
+ }
288
+
289
+ /** The claim in this clause, when it is a plain, present, positive one. */
290
+ function claimIn(clause: string): ClaimPattern | undefined {
291
+ if (REPORTING.test(clause) || EARLIER.test(clause)) return undefined
292
+ for (const pattern of ANSWER_CLAIM_PATTERNS) {
293
+ const found = pattern.re.exec(clause)
294
+ if (!found) continue
295
+ const before = wordsBefore(clause, found.index)
296
+ if (NEGATION.test(before) || HEDGE.test(before)) continue
297
+ const after = wordsAfter(clause, found.index + found[0].length)
298
+ if (QUALIFIER.test(after)) continue
299
+ if (/\bpass(?:es)?$/i.test(found[0]) && OBJECT_AFTER.test(after)) continue
300
+ if (/\bpassing$/i.test(found[0]) && COPULA_AFTER.test(after)) continue
301
+ if (/\d+\s+of\s+\d+/.test(clause)) continue
302
+ return pattern
303
+ }
304
+ return undefined
305
+ }
306
+
307
+ /** The kind of check a command line is, when it says so. */
308
+ const COMMAND_KINDS: readonly { kind: AnswerClaimKind; re: RegExp }[] = [
309
+ {
310
+ kind: 'lint',
311
+ re: /\b(?:lint|eslint|biome|prettier|ruff|flake8|rubocop|clippy|golangci-lint)\b/i,
312
+ },
313
+ { kind: 'typecheck', re: /\b(?:typecheck|tsc|mypy|pyright)\b/i },
314
+ { kind: 'tests', re: /\b(?:test|tests|vitest|jest|pytest|mocha|phpunit|spec)\b/i },
315
+ { kind: 'build', re: /\b(?:build|compile|bundle)\b/i },
316
+ ]
317
+
318
+ /** A type check is a compile, so a claim about either is about both. */
319
+ function sameKind(a: AnswerClaimKind, b: AnswerClaimKind): boolean {
320
+ if (a === b) return true
321
+ return (a === 'build' && b === 'typecheck') || (a === 'typecheck' && b === 'build')
322
+ }
323
+
324
+ /** Every kind a command line is: a chained `pnpm lint && pnpm test` is both. */
325
+ function kindsOfCommand(command: string): AnswerClaimKind[] {
326
+ return COMMAND_KINDS.filter((k) => k.re.test(command)).map((k) => k.kind)
327
+ }
328
+
329
+ /** The failed check the claim is about, or `undefined` when every failed check is plainly another kind. */
330
+ function failedCheckFor(
331
+ record: TurnOutcome,
332
+ claim: AnswerClaimKind,
333
+ ): { command?: string } | undefined {
334
+ const checks = Array.isArray(record.checks) ? record.checks : []
335
+ const failed = checks.filter((c) => c?.result === 'failed')
336
+ if (failed.length === 0) return {}
337
+ const related = failed.filter((c) => {
338
+ const kinds = kindsOfCommand(typeof c.command === 'string' ? c.command : '')
339
+ return claim === 'checks' || kinds.length === 0 || kinds.some((kind) => sameKind(kind, claim))
340
+ })
341
+ if (related.length === 0) return undefined
342
+ const only = related.length === 1 ? related[0]?.command : undefined
343
+ return typeof only === 'string' ? { command: only } : {}
344
+ }
345
+
346
+ /**
347
+ * The one mismatch between a plain claim in `answer` and this turn's `outcome`,
348
+ * or `undefined`.
349
+ *
350
+ * Reports only when the record says a check failed, or says files changed and
351
+ * no check ran or none established a pass. Never when the record says nothing
352
+ * changed, that the checks passed, or that it cannot tell: in those the claim
353
+ * may be true. English only: a claim in another language is not found.
354
+ */
355
+ export function compareAnswerToOutcome(
356
+ answer: string | undefined | null,
357
+ outcome: TurnOutcome | undefined,
358
+ ): AnswerClaimMismatch | undefined {
359
+ // A notice is never worth failing a turn for: input that does not have the
360
+ // expected shape (a hook-supplied record) is read as "nothing to say".
361
+ try {
362
+ return compare(answer, outcome)
363
+ } catch {
364
+ return undefined
365
+ }
366
+ }
367
+
368
+ function compare(
369
+ answer: string | undefined | null,
370
+ outcome: TurnOutcome | undefined,
371
+ ): AnswerClaimMismatch | undefined {
372
+ if (typeof answer !== 'string' || answer === '' || !outcome) return undefined
373
+ const verdict = outcome.verdict
374
+ if (verdict !== 'failing' && verdict !== 'unchecked' && verdict !== 'inconclusive') {
375
+ return undefined
376
+ }
377
+ for (const { sentenceEndsWithQuestion, clauses } of clausesOf(readableText(answer))) {
378
+ if (sentenceEndsWithQuestion) continue
379
+ for (const clause of clauses) {
380
+ const pattern = claimIn(clause)
381
+ if (!pattern) continue
382
+ let record: AnswerClaimRecord
383
+ let recordSays: string
384
+ if (verdict === 'failing') {
385
+ const failed = failedCheckFor(outcome, pattern.kind)
386
+ if (!failed) continue
387
+ record = 'check-failed'
388
+ recordSays = failed.command
389
+ ? `${auditCommandText(failed.command)} failed`
390
+ : 'a check failed'
391
+ } else if (verdict === 'unchecked') {
392
+ record = 'unchecked'
393
+ recordSays = 'files changed and no check ran'
394
+ } else {
395
+ record = 'unreadable'
396
+ recordSays = 'files changed and no check gave a result that could be read'
397
+ }
398
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what this removes
399
+ const folded = clause.replace(/[\u0000-\u001f\u007f-\u009f\s]+/g, ' ').trim()
400
+ const quote =
401
+ folded.length <= ANSWER_CLAIM_MAX_QUOTE_CHARS
402
+ ? folded
403
+ : `${folded.slice(0, ANSWER_CLAIM_MAX_QUOTE_CHARS - 3)}...`
404
+ return { claim: pattern.kind, said: pattern.said, quote, record, recordSays }
405
+ }
406
+ }
407
+ return undefined
408
+ }
409
+
410
+ /** The mismatch as one plain line, for a person: `The answer says the tests pass. The record: ...` */
411
+ export function describeAnswerClaimMismatch(mismatch: AnswerClaimMismatch): string {
412
+ return `The answer says ${mismatch.said}. The record: ${mismatch.recordSays}.`
413
+ }
@@ -539,6 +539,18 @@ export class OutcomeLedger {
539
539
  return this.compute(null)
540
540
  }
541
541
 
542
+ /**
543
+ * The record as it stands now, with the workspace asked once, and without
544
+ * settling: the turn may go on and `finish` still answers once at the end.
545
+ * Used by a host that wants the whole record before it lets a turn end. A
546
+ * record that is not whole is `undefined`, as in `finish`.
547
+ */
548
+ async preview(): Promise<TurnOutcome | undefined> {
549
+ if (this.unrecorded) return undefined
550
+ if (this.settled !== undefined) return this.settled
551
+ return this.compute(this.started ? await this.detect() : null)
552
+ }
553
+
542
554
  private compute(endFingerprint: string | null): TurnOutcome {
543
555
  const workspace = this.started && this.startFingerprint !== null && endFingerprint !== null
544
556
  const moved = workspace && this.startFingerprint !== endFingerprint
@@ -2,6 +2,7 @@ export {
2
2
  SKILL_FRONTMATTER_KEYS,
3
3
  SKILL_FRONTMATTER_TOLERATED_KEYS,
4
4
  loadSkill,
5
+ parseSkillMarkdown,
5
6
  discoverSkills,
6
7
  } from './loader.js'
7
8
  export { SkillCollisionError, SkillRegistry, resolveSkillChain } from './registry.js'
@@ -257,6 +257,24 @@ function estimateTokens(text: string): number {
257
257
  return Math.ceil(text.length / CHARS_PER_TOKEN)
258
258
  }
259
259
 
260
+ /**
261
+ * A `SKILL.md`'s text to its metadata and body, or a thrown reason.
262
+ *
263
+ * The one place the skill rules are applied to text. `loadSkill` reads the
264
+ * file and calls this; a host that checks a skill without loading it (the
265
+ * plugin validator) reads the file its own way and calls the same function,
266
+ * so the two cannot disagree about what a valid skill is.
267
+ *
268
+ * @param dirPath The skill's folder: the name must match its last segment.
269
+ */
270
+ export function parseSkillMarkdown(
271
+ raw: string,
272
+ dirPath: string,
273
+ ): { readonly metadata: SkillMetadata; readonly body: string } {
274
+ const parsed = parseFrontmatter(raw, sourceLabel(dirPath), SKILL_FRONTMATTER_OPTIONS)
275
+ return { metadata: toSkillMetadata(parsed, dirPath), body: parsed.body }
276
+ }
277
+
260
278
  export async function loadSkill(
261
279
  dirPath: string,
262
280
  level: SkillDisclosureLevel = 'metadata',
@@ -264,8 +282,8 @@ export async function loadSkill(
264
282
  ): Promise<SkillLoadResult> {
265
283
  const skillMdPath = join(dirPath, SKILL_FILENAME)
266
284
  const raw = await readFile(skillMdPath, 'utf-8')
267
- const parsed = parseFrontmatter(raw, sourceLabel(dirPath), SKILL_FRONTMATTER_OPTIONS)
268
- const metadata = toSkillMetadata(parsed, dirPath)
285
+ const parsedSkill = parseSkillMarkdown(raw, dirPath)
286
+ const metadata = parsedSkill.metadata
269
287
 
270
288
  const skill: Skill = {
271
289
  metadata,
@@ -273,7 +291,7 @@ export async function loadSkill(
273
291
  }
274
292
 
275
293
  if (level === 'full' || level === 'assets') {
276
- skill.body = parsed.body
294
+ skill.body = parsedSkill.body
277
295
  }
278
296
 
279
297
  const metadataTokens = estimateTokens(`${metadata.name}: ${metadata.description}`)
@@ -3,6 +3,7 @@ import { join } from 'node:path'
3
3
  import { TenantIsolationError } from '../../session/errors.js'
4
4
  import type {
5
5
  GoalBlockReason,
6
+ GoalConfirmedBy,
6
7
  GoalPhase,
7
8
  GoalRef,
8
9
  GoalRoundAuthority,
@@ -29,6 +30,8 @@ export interface CreateSessionGoalParams {
29
30
  readonly sessionId: SessionId
30
31
  readonly objective: string
31
32
  readonly maxGoalRounds?: number
33
+ /** Set only when this goal chooses; absent leaves the choice to the host's setting. */
34
+ readonly confirmedBy?: GoalConfirmedBy
32
35
  }
33
36
 
34
37
  export interface EditSessionGoalParams {
@@ -273,6 +276,13 @@ abstract class SessionGoalStoreBase implements SessionGoalStore {
273
276
  const sessionId = await this.requireSession(params.sessionId, tenantId)
274
277
  const text = objective(params.objective)
275
278
  const maxRounds = maxGoalRounds(params.maxGoalRounds)
279
+ if (
280
+ params.confirmedBy !== undefined &&
281
+ params.confirmedBy !== 'model' &&
282
+ params.confirmedBy !== 'record'
283
+ ) {
284
+ throw new TypeError('goal confirmedBy must be "model" or "record".')
285
+ }
276
286
  return await backendFor(this).transact(sessionId, (existing) => {
277
287
  if (existing) {
278
288
  assertTenant(existing, tenantId)
@@ -290,6 +300,7 @@ abstract class SessionGoalStoreBase implements SessionGoalStore {
290
300
  phase: 'active',
291
301
  maxGoalRounds: maxRounds,
292
302
  roundsAdmitted: 0,
303
+ ...(params.confirmedBy !== undefined ? { confirmedBy: params.confirmedBy } : {}),
293
304
  createdAt: now,
294
305
  updatedAt: now,
295
306
  }