@tech-leads-club/harness-toolkit 0.2.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 (300) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/LICENSE +84 -0
  3. package/NOTICE +9 -0
  4. package/README.md +480 -0
  5. package/bin/tlc +10 -0
  6. package/bin/tlc-build +80 -0
  7. package/bin/tlc-cli.ts +1509 -0
  8. package/bin/tlc-exec +10 -0
  9. package/bin/tlc-exec.cmd +4 -0
  10. package/bin/tlc-exec.d.mts +60 -0
  11. package/bin/tlc-exec.mjs +248 -0
  12. package/bin/tlc.cmd +4 -0
  13. package/bin/tlc.mjs +7 -0
  14. package/bin/write-user-hooks.d.mts +40 -0
  15. package/bin/write-user-hooks.mjs +123 -0
  16. package/capabilities/catalog.json +418 -0
  17. package/config.example.json +51 -0
  18. package/dist/compact-before.mjs +7839 -0
  19. package/dist/doctor.mjs +8263 -0
  20. package/dist/help-topic.mjs +82 -0
  21. package/dist/init-project.mjs +993 -0
  22. package/dist/install-runtime.mjs +969 -0
  23. package/dist/lessons-cli.mjs +7258 -0
  24. package/dist/obs-cli.mjs +7025 -0
  25. package/dist/price-lookup.mjs +221 -0
  26. package/dist/prompt-submit.mjs +7860 -0
  27. package/dist/refresh-model-prices.mjs +158 -0
  28. package/dist/response-after.mjs +7879 -0
  29. package/dist/run.mjs +7830 -0
  30. package/dist/session-end.mjs +7937 -0
  31. package/dist/session-start.mjs +8056 -0
  32. package/dist/shim.mjs +52 -0
  33. package/dist/stop.mjs +8494 -0
  34. package/dist/subagent-start.mjs +7861 -0
  35. package/dist/subagent-stop.mjs +7851 -0
  36. package/dist/support.mjs +7045 -0
  37. package/dist/tlc-cli.mjs +8063 -0
  38. package/dist/tool-after.mjs +8113 -0
  39. package/dist/tool-before.mjs +7985 -0
  40. package/dist/tool-failure.mjs +7850 -0
  41. package/dist/uninstall-runtime.mjs +1307 -0
  42. package/docs/architecture.md +270 -0
  43. package/docs/concepts.md +562 -0
  44. package/docs/coverage.md +55 -0
  45. package/docs/decisions/ad-001.md +32 -0
  46. package/docs/decisions/ad-002.md +51 -0
  47. package/docs/decisions/ad-003.md +30 -0
  48. package/docs/decisions/ad-004.md +37 -0
  49. package/docs/decisions/ad-005.md +36 -0
  50. package/docs/decisions/ad-006.md +49 -0
  51. package/docs/decisions/ad-007.md +36 -0
  52. package/docs/decisions/ad-008.md +54 -0
  53. package/docs/decisions/ad-009.md +61 -0
  54. package/docs/decisions/ad-010.md +45 -0
  55. package/docs/decisions/ad-011.md +59 -0
  56. package/docs/decisions/ad-012.md +71 -0
  57. package/docs/decisions/ad-013.md +87 -0
  58. package/docs/decisions/ad-014.md +56 -0
  59. package/docs/decisions/ad-015.md +33 -0
  60. package/docs/decisions/ad-016.md +98 -0
  61. package/docs/decisions/ad-017.md +65 -0
  62. package/docs/decisions/ad-018.md +77 -0
  63. package/docs/decisions/ad-019.md +75 -0
  64. package/docs/decisions/ad-020.md +88 -0
  65. package/docs/decisions/ad-021.md +57 -0
  66. package/docs/decisions/ad-022.md +120 -0
  67. package/docs/decisions/ad-023.md +87 -0
  68. package/docs/decisions/ad-024.md +85 -0
  69. package/docs/decisions/ad-025.md +92 -0
  70. package/docs/decisions/ad-026.md +131 -0
  71. package/docs/decisions/ad-027.md +90 -0
  72. package/docs/decisions/ad-028.md +90 -0
  73. package/docs/decisions/ad-029.md +73 -0
  74. package/docs/decisions/ad-030.md +97 -0
  75. package/docs/decisions/ad-031.md +94 -0
  76. package/docs/decisions/ad-032.md +83 -0
  77. package/docs/decisions/ad-033.md +89 -0
  78. package/docs/decisions/ad-034.md +86 -0
  79. package/docs/decisions/ad-035.md +86 -0
  80. package/docs/decisions/ad-036.md +68 -0
  81. package/docs/decisions/ad-037.md +47 -0
  82. package/docs/decisions/ad-038.md +52 -0
  83. package/docs/decisions/ad-039.md +69 -0
  84. package/docs/decisions/ad-040.md +89 -0
  85. package/docs/decisions/ad-041.md +98 -0
  86. package/docs/decisions/ad-042.md +82 -0
  87. package/docs/decisions/ad-043.md +79 -0
  88. package/docs/decisions/ad-044.md +61 -0
  89. package/docs/decisions/ad-045.md +94 -0
  90. package/docs/decisions/ad-046.md +111 -0
  91. package/docs/decisions/ad-047.md +96 -0
  92. package/docs/decisions/ad-048.md +85 -0
  93. package/docs/decisions/ad-049.md +66 -0
  94. package/docs/decisions/ad-050.md +94 -0
  95. package/docs/decisions/ad-051.md +69 -0
  96. package/docs/decisions/ad-052.md +69 -0
  97. package/docs/decisions/ad-053.md +78 -0
  98. package/docs/decisions/ad-054.md +98 -0
  99. package/docs/decisions/ad-055.md +74 -0
  100. package/docs/decisions/ad-056.md +85 -0
  101. package/docs/decisions/ad-057.md +68 -0
  102. package/docs/decisions/ad-058.md +97 -0
  103. package/docs/decisions/ad-059.md +82 -0
  104. package/docs/decisions/ad-060.md +75 -0
  105. package/docs/decisions/ad-061.md +68 -0
  106. package/docs/decisions/ad-062.md +72 -0
  107. package/docs/decisions/ad-063.md +84 -0
  108. package/docs/decisions/ad-064.md +79 -0
  109. package/docs/decisions/ad-065.md +81 -0
  110. package/docs/decisions/ad-066.md +111 -0
  111. package/docs/decisions/ad-067.md +64 -0
  112. package/docs/decisions/ad-068.md +79 -0
  113. package/docs/decisions/ad-069.md +74 -0
  114. package/docs/decisions/ad-070.md +86 -0
  115. package/docs/decisions/ad-071.md +93 -0
  116. package/docs/decisions/ad-072.md +82 -0
  117. package/docs/decisions/ad-073.md +102 -0
  118. package/docs/decisions/ad-074.md +91 -0
  119. package/docs/decisions/ad-075.md +79 -0
  120. package/docs/decisions/ad-076.md +102 -0
  121. package/docs/decisions/ad-077.md +94 -0
  122. package/docs/decisions/ad-078.md +84 -0
  123. package/docs/decisions/ad-079.md +73 -0
  124. package/docs/decisions/ad-080.md +86 -0
  125. package/docs/decisions/ad-081.md +70 -0
  126. package/docs/decisions/ad-082.md +79 -0
  127. package/docs/decisions/ad-083.md +88 -0
  128. package/docs/decisions/index.md +111 -0
  129. package/docs/diagnose.md +248 -0
  130. package/docs/index.md +52 -0
  131. package/docs/init.md +101 -0
  132. package/docs/lessons.md +294 -0
  133. package/docs/log.md +139 -0
  134. package/docs/measure.md +178 -0
  135. package/docs/providers/claude-code.md +118 -0
  136. package/docs/providers/cursor.md +110 -0
  137. package/docs/providers/index.md +71 -0
  138. package/docs/troubleshooting.md +138 -0
  139. package/model-aliases.json +12 -0
  140. package/model-prices.cursor.json +410 -0
  141. package/model-prices.json +1 -0
  142. package/package.json +57 -0
  143. package/skills/harness-init/SKILL.md +241 -0
  144. package/skills/harness-init/evals/trigger_evals.json +86 -0
  145. package/skills/harness-init/references/capabilities.md +113 -0
  146. package/src/contracts/capabilities.ts +30 -0
  147. package/src/contracts/decision.ts +21 -0
  148. package/src/contracts/effort.ts +15 -0
  149. package/src/contracts/harness-event.ts +87 -0
  150. package/src/contracts/index.ts +6 -0
  151. package/src/contracts/policy-defaults.ts +14 -0
  152. package/src/contracts/wiring.ts +20 -0
  153. package/src/core/attest/attest.service.ts +0 -0
  154. package/src/core/capability/capability.service.ts +62 -0
  155. package/src/core/capability/capability.store.ts +51 -0
  156. package/src/core/capability/capability.types.ts +56 -0
  157. package/src/core/comment-policy/comment-policy.service.ts +307 -0
  158. package/src/core/comment-policy/comment-policy.types.ts +6 -0
  159. package/src/core/comment-policy/comment-resolvability.ts +134 -0
  160. package/src/core/comment-policy/comment-syntax.catalog.ts +310 -0
  161. package/src/core/comment-policy/comment-syntax.store.ts +65 -0
  162. package/src/core/comment-policy/comment-syntax.types.ts +24 -0
  163. package/src/core/core.facade.ts +461 -0
  164. package/src/core/duplication/duplication.service.ts +219 -0
  165. package/src/core/floor/floor.catalog.ts +54 -0
  166. package/src/core/floor/floor.paths.ts +90 -0
  167. package/src/core/floor/floor.policy-surface.ts +311 -0
  168. package/src/core/floor/floor.service.ts +306 -0
  169. package/src/core/floor/floor.tokenize.ts +179 -0
  170. package/src/core/floor/floor.verb.ts +28 -0
  171. package/src/core/floor/index.ts +1 -0
  172. package/src/core/gate/gate.artifact.ts +157 -0
  173. package/src/core/gate/gate.command.ts +130 -0
  174. package/src/core/gate/gate.findings.ts +201 -0
  175. package/src/core/gate/gate.inputs.ts +0 -0
  176. package/src/core/gate/gate.lock.ts +283 -0
  177. package/src/core/gate/gate.service.ts +28 -0
  178. package/src/core/gate/gate.types.ts +67 -0
  179. package/src/core/handoff/handoff.service.ts +43 -0
  180. package/src/core/handoff/handoff.store.ts +59 -0
  181. package/src/core/handoff/handoff.types.ts +89 -0
  182. package/src/core/index.ts +52 -0
  183. package/src/core/integrity/state-seal.ts +81 -0
  184. package/src/core/lesson/lesson.authored.ts +96 -0
  185. package/src/core/lesson/lesson.credit.ts +51 -0
  186. package/src/core/lesson/lesson.garden.ts +283 -0
  187. package/src/core/lesson/lesson.link.ts +93 -0
  188. package/src/core/lesson/lesson.score.ts +61 -0
  189. package/src/core/lesson/lesson.select.ts +209 -0
  190. package/src/core/lesson/lesson.service.ts +110 -0
  191. package/src/core/lesson/lesson.store.ts +317 -0
  192. package/src/core/lesson/lesson.sync.ts +70 -0
  193. package/src/core/lesson/lesson.types.ts +92 -0
  194. package/src/core/lesson/lesson.validity.ts +44 -0
  195. package/src/core/observability/observability.report.ts +326 -0
  196. package/src/core/observability/observability.service.ts +328 -0
  197. package/src/core/observability/observability.store.ts +269 -0
  198. package/src/core/observability/observability.types.ts +193 -0
  199. package/src/core/observability/observability.why.ts +206 -0
  200. package/src/core/observe/observe.service.ts +76 -0
  201. package/src/core/plan/plan.detect.ts +47 -0
  202. package/src/core/plan/plan.service.ts +81 -0
  203. package/src/core/plan/plan.types.ts +25 -0
  204. package/src/core/policy/policy.defaults.ts +103 -0
  205. package/src/core/policy/policy.guard.ts +34 -0
  206. package/src/core/policy/policy.integrity.ts +238 -0
  207. package/src/core/policy/policy.loader.ts +118 -0
  208. package/src/core/policy/policy.operator.ts +82 -0
  209. package/src/core/policy/policy.posture.ts +79 -0
  210. package/src/core/policy/policy.rails.ts +45 -0
  211. package/src/core/policy/policy.types.ts +157 -0
  212. package/src/core/presence/presence.service.ts +111 -0
  213. package/src/core/presence/presence.store.ts +59 -0
  214. package/src/core/presence/presence.types.ts +9 -0
  215. package/src/core/release/release.decisions.ts +147 -0
  216. package/src/core/release/release.seen.ts +38 -0
  217. package/src/core/shell-policy/shell-policy.service.ts +215 -0
  218. package/src/core/shell-policy/shell-policy.stall.ts +57 -0
  219. package/src/core/shell-policy/shell-policy.types.ts +26 -0
  220. package/src/core/ship/ship.ledger.ts +76 -0
  221. package/src/core/ship/ship.service.ts +137 -0
  222. package/src/core/ship/ship.types.ts +19 -0
  223. package/src/core/stagnation/stagnation.resolution.ts +87 -0
  224. package/src/core/stagnation/stagnation.service.ts +20 -0
  225. package/src/core/stagnation/stagnation.store.ts +49 -0
  226. package/src/core/stagnation/stagnation.types.ts +12 -0
  227. package/src/core/subagent-policy/subagent-policy.parent-model.ts +202 -0
  228. package/src/core/subagent-policy/subagent-policy.service.ts +139 -0
  229. package/src/core/subagent-policy/subagent-policy.types.ts +22 -0
  230. package/src/core/supply-chain/supply-chain.catalog.ts +51 -0
  231. package/src/core/supply-chain/supply-chain.service.ts +217 -0
  232. package/src/core/turn/turn.activity.ts +94 -0
  233. package/src/core/turn/turn.autopilot.ts +131 -0
  234. package/src/core/turn/turn.failure-signals.ts +193 -0
  235. package/src/core/turn/turn.loop-counter.ts +71 -0
  236. package/src/core/turn/turn.types.ts +14 -0
  237. package/src/core/untrusted/untrusted.detect.ts +53 -0
  238. package/src/core/untrusted/untrusted.recall.ts +87 -0
  239. package/src/core/untrusted/untrusted.service.ts +129 -0
  240. package/src/core/untrusted/untrusted.store.ts +75 -0
  241. package/src/core/untrusted/untrusted.types.ts +42 -0
  242. package/src/entrypoints/compact-before.ts +14 -0
  243. package/src/entrypoints/prompt-submit.ts +25 -0
  244. package/src/entrypoints/response-after.ts +60 -0
  245. package/src/entrypoints/run.ts +179 -0
  246. package/src/entrypoints/session-end.ts +58 -0
  247. package/src/entrypoints/session-start.ts +172 -0
  248. package/src/entrypoints/shim.ts +46 -0
  249. package/src/entrypoints/stop.ts +876 -0
  250. package/src/entrypoints/subagent-start.ts +16 -0
  251. package/src/entrypoints/subagent-stop.ts +32 -0
  252. package/src/entrypoints/support.ts +182 -0
  253. package/src/entrypoints/tool-after.ts +111 -0
  254. package/src/entrypoints/tool-before.ts +186 -0
  255. package/src/entrypoints/tool-failure.ts +26 -0
  256. package/src/platform/backoff.ts +49 -0
  257. package/src/platform/cli-output.ts +30 -0
  258. package/src/platform/env-scope.ts +22 -0
  259. package/src/platform/fs-atomic.ts +142 -0
  260. package/src/platform/fs-jsonl.ts +24 -0
  261. package/src/platform/git.ts +187 -0
  262. package/src/platform/paths.ts +80 -0
  263. package/src/platform/pricing.ts +229 -0
  264. package/src/platform/process.ts +64 -0
  265. package/src/platform/sanitize.ts +23 -0
  266. package/src/platform/screen.ts +46 -0
  267. package/src/platform/style.ts +103 -0
  268. package/src/providers/claude/claude.capabilities.ts +25 -0
  269. package/src/providers/claude/claude.detect.ts +14 -0
  270. package/src/providers/claude/claude.inbound.ts +252 -0
  271. package/src/providers/claude/claude.lessons-view.ts +33 -0
  272. package/src/providers/claude/claude.outbound.ts +65 -0
  273. package/src/providers/claude/claude.policy-defaults.ts +9 -0
  274. package/src/providers/claude/claude.transcript.ts +62 -0
  275. package/src/providers/claude/claude.wiring.ts +245 -0
  276. package/src/providers/claude/index.ts +17 -0
  277. package/src/providers/cursor/cursor.capabilities.ts +28 -0
  278. package/src/providers/cursor/cursor.detect.ts +14 -0
  279. package/src/providers/cursor/cursor.inbound.ts +203 -0
  280. package/src/providers/cursor/cursor.lessons-view.ts +30 -0
  281. package/src/providers/cursor/cursor.outbound.ts +43 -0
  282. package/src/providers/cursor/cursor.policy-defaults.ts +9 -0
  283. package/src/providers/cursor/cursor.wiring.ts +214 -0
  284. package/src/providers/cursor/index.ts +17 -0
  285. package/src/providers/index.ts +9 -0
  286. package/src/providers/provider.degrade.ts +152 -0
  287. package/src/providers/provider.port.ts +20 -0
  288. package/src/providers/provider.registry.ts +28 -0
  289. package/tools/doctor.ts +631 -0
  290. package/tools/help-topic.ts +74 -0
  291. package/tools/init-project.ts +277 -0
  292. package/tools/install-runtime.ts +164 -0
  293. package/tools/lessons-cli.ts +474 -0
  294. package/tools/obs-cli.ts +175 -0
  295. package/tools/price-lookup.ts +57 -0
  296. package/tools/refresh-model-prices.ts +210 -0
  297. package/tools/test-env.mjs +28 -0
  298. package/tools/test-env.names.d.mts +1 -0
  299. package/tools/test-env.names.mjs +14 -0
  300. package/tools/uninstall-runtime.ts +463 -0
@@ -0,0 +1,67 @@
1
+ export const GATE_SCHEMA = "harness.gate.v1" as const;
2
+
3
+ export type GateFinding = {
4
+ id?: string;
5
+ summary: string;
6
+ detail?: string;
7
+ };
8
+
9
+ export type LastGateArtifact = {
10
+ schema: typeof GATE_SCHEMA;
11
+ gate: string;
12
+ exitCode: number;
13
+ passed: boolean;
14
+ command: string[];
15
+ files: string[];
16
+ durationMs: number;
17
+ ts: string;
18
+ outputTail: string;
19
+ findings: GateFinding[];
20
+ /**
21
+ * The content hash of the command and files this verdict was produced under. Absent on an artifact written
22
+ * before the field existed, which is why the first run after an upgrade always executes
23
+ * ([/decisions/ad-045.md](/decisions/ad-045.md)).
24
+ */
25
+ inputsHash?: string;
26
+ /**
27
+ * Project-scoping environment variables that were set when this gate ran, by name.
28
+ *
29
+ * why: recorded always, as a fact rather than an alarm. A gate failing because the hook's environment points
30
+ * its fixtures at the real repository is indistinguishable, from the follow-up alone, from a gate failing
31
+ * because the code is wrong — and it cost four stop loops of editing code that was not broken. Absent on an
32
+ * artifact written before the field existed ([/decisions/ad-060.md](/decisions/ad-060.md)).
33
+ */
34
+ scopedEnv?: string[];
35
+ };
36
+
37
+ export type AppendFilesMode = "auto" | "always" | "never";
38
+
39
+ export type FailureCategory =
40
+ | "agent-quality"
41
+ | "stagnation"
42
+ | "verification"
43
+ | "ship-evidence"
44
+ | "policy"
45
+ | "config"
46
+ | "budget";
47
+
48
+ export type GateGap = {
49
+ id: string;
50
+ gate: string;
51
+ category: FailureCategory;
52
+ summary: string;
53
+ detail?: string;
54
+ };
55
+
56
+ export type LockBody = {
57
+ provider: string;
58
+ session: string;
59
+ pid: number;
60
+ acquired_at: string;
61
+ /**
62
+ * The machine that wrote the lock. A pid only means something on the host that issued it, so liveness is
63
+ * consulted only when this matches. A body without it — written by an older build — falls back to the age
64
+ * rule, which is the honest answer when liveness cannot be established.
65
+ */
66
+ host?: string;
67
+ };
@@ -0,0 +1,43 @@
1
+ import { divergedMessage, shouldInject, verifySeal } from "../integrity/state-seal.ts";
2
+ import { handoffPath, patchHandoff, readHandoffFile } from "./handoff.store.ts";
3
+ import type { ForeignSlice, HandoffProviderSlice, HandoffShared } from "./handoff.types.ts";
4
+
5
+ export type ResolvedHandoff = HandoffShared & HandoffProviderSlice;
6
+
7
+ /**
8
+ * Whether the handoff is safe to read aloud to the model.
9
+ *
10
+ * why: separate from `readHandoff`, because reading and injecting are different acts. `tlc harness handoff`
11
+ * displaying a diverged file is how an operator investigates it; a turn being *told* what it says is the moment
12
+ * worth withholding ([/decisions/ad-078.md](/decisions/ad-078.md)).
13
+ */
14
+ export function handoffInjectable(root: string): { ok: boolean; note: string | null } {
15
+ const target = handoffPath(root);
16
+ const verdict = verifySeal(target);
17
+ return shouldInject(verdict)
18
+ ? { ok: true, note: null }
19
+ : { ok: false, note: divergedMessage(target, "The handoff") };
20
+ }
21
+
22
+ export function readHandoff(root: string, provider: string): ResolvedHandoff {
23
+ const file = readHandoffFile(root);
24
+ const slice = file.by_provider[provider] ?? { updated_at: file.shared.updated_at };
25
+ return { ...file.shared, ...slice };
26
+ }
27
+
28
+ export function readForeignSlices(root: string, provider: string): ForeignSlice[] {
29
+ const file = readHandoffFile(root);
30
+ const foreign: ForeignSlice[] = [];
31
+ for (const [name, slice] of Object.entries(file.by_provider)) {
32
+ if (name === provider) {
33
+ continue;
34
+ }
35
+ if (slice.next_action === undefined && slice.blockers === undefined) {
36
+ continue;
37
+ }
38
+ foreign.push({ provider: name, next_action: slice.next_action, blockers: slice.blockers });
39
+ }
40
+ return foreign;
41
+ }
42
+
43
+ export { patchHandoff, readHandoffFile };
@@ -0,0 +1,59 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { updateJsonAtomic } from "../../platform/fs-atomic.ts";
4
+ import { projectStateDir } from "../../platform/paths.ts";
5
+ import { seal } from "../integrity/state-seal.ts";
6
+ import {
7
+ defaultHandoffFile,
8
+ type HandoffFile,
9
+ type HandoffProviderSlice,
10
+ type HandoffShared,
11
+ isHandoffFile,
12
+ } from "./handoff.types.ts";
13
+
14
+ export function handoffPath(root: string): string {
15
+ return join(projectStateDir(root), "handoff.json");
16
+ }
17
+
18
+ function handoffLockPath(root: string): string {
19
+ return `${handoffPath(root)}.lock`;
20
+ }
21
+
22
+ export function readHandoffFile(root: string): HandoffFile {
23
+ const path = handoffPath(root);
24
+ if (!existsSync(path)) {
25
+ return defaultHandoffFile();
26
+ }
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
29
+ if (isHandoffFile(parsed)) {
30
+ return parsed;
31
+ }
32
+ } catch {}
33
+ return defaultHandoffFile();
34
+ }
35
+
36
+ export type HandoffPatch = {
37
+ shared?: Partial<HandoffShared>;
38
+ slice?: Partial<HandoffProviderSlice>;
39
+ };
40
+
41
+ export function patchHandoff(root: string, provider: string, patch: HandoffPatch): Promise<HandoffFile> {
42
+ return updateJsonAtomic<HandoffFile>(
43
+ handoffPath(root),
44
+ (current) => {
45
+ const base = current && isHandoffFile(current) ? current : defaultHandoffFile();
46
+ const now = new Date().toISOString();
47
+ const ownSlice = base.by_provider[provider] ?? { updated_at: now };
48
+ return {
49
+ schema: base.schema,
50
+ shared: { ...base.shared, ...patch.shared, updated_at: now },
51
+ by_provider: {
52
+ ...base.by_provider,
53
+ [provider]: { ...ownSlice, ...patch.slice, updated_at: now },
54
+ },
55
+ };
56
+ },
57
+ { lockPath: handoffLockPath(root), afterWrite: seal },
58
+ );
59
+ }
@@ -0,0 +1,89 @@
1
+ import type { FailureCategory, GateGap } from "../gate/gate.types.ts";
2
+ import type { PendingLessonCredit } from "../lesson/lesson.types.ts";
3
+ import type { PlanDeviation } from "../plan/plan.types.ts";
4
+ import type { OperatorMode } from "../policy/policy.types.ts";
5
+
6
+ export type GateResult = "pass" | "fail" | "skipped";
7
+
8
+ export type HandoffShared = {
9
+ mode: OperatorMode;
10
+ project_name?: string;
11
+ git_branch?: string;
12
+ git_sha?: string;
13
+ updated_at: string;
14
+ };
15
+
16
+ export type HandoffProviderSlice = {
17
+ updated_at: string;
18
+ session_key?: string;
19
+ session_narrative?: string;
20
+ completed?: string[];
21
+ in_progress?: string[];
22
+ pending?: string[];
23
+ last_ship_claim_at?: string;
24
+ last_ship_claim_snippet?: string;
25
+ last_ship_claim_kind?: "structured";
26
+ last_changed_files?: string[];
27
+ last_stop_status?: string;
28
+ last_gate_result?: GateResult;
29
+ last_fingerprint?: string;
30
+ fingerprint_hits?: number;
31
+ last_failure_category?: FailureCategory;
32
+ // why: the lessons injected for a gate are graded by that gate's next run, which happens in a later process.
33
+ // The handoff is the only state that survives between the two ([/decisions/ad-039.md](/decisions/ad-039.md)).
34
+ pending_lesson_credit?: PendingLessonCredit;
35
+ previous_gaps?: GateGap[];
36
+ /**
37
+ * why: the revision this turn started at. Every stop-time gate diffs against it rather than against `HEAD`,
38
+ * because a turn that commits moves `HEAD` past its own changes and each gate then reads an empty diff
39
+ * ([/decisions/ad-058.md](/decisions/ad-058.md)).
40
+ */
41
+ turn_base_sha?: string;
42
+ plan_paths?: string[];
43
+ plan_at?: string;
44
+ plan_snippet?: string;
45
+ plan_deviations?: PlanDeviation[];
46
+ next_action?: string;
47
+ blockers?: string;
48
+ machine_state?: Record<string, unknown>;
49
+ };
50
+
51
+ export const HANDOFF_SCHEMA = "harness.handoff.v2" as const;
52
+
53
+ export type HandoffFile = {
54
+ schema: typeof HANDOFF_SCHEMA;
55
+ shared: HandoffShared;
56
+ by_provider: Record<string, HandoffProviderSlice>;
57
+ };
58
+
59
+ export type ForeignSlice = {
60
+ provider: string;
61
+ plan_paths?: string[];
62
+ plan_at?: string;
63
+ plan_snippet?: string;
64
+ plan_deviations?: PlanDeviation[];
65
+ next_action?: string;
66
+ blockers?: string;
67
+ };
68
+
69
+ export function defaultHandoffFile(mode: OperatorMode = "solo"): HandoffFile {
70
+ return {
71
+ schema: HANDOFF_SCHEMA,
72
+ shared: { mode, updated_at: new Date().toISOString() },
73
+ by_provider: {},
74
+ };
75
+ }
76
+
77
+ export function isHandoffFile(value: unknown): value is HandoffFile {
78
+ if (typeof value !== "object" || value === null) {
79
+ return false;
80
+ }
81
+ const candidate = value as Partial<HandoffFile>;
82
+ return (
83
+ candidate.schema === HANDOFF_SCHEMA &&
84
+ typeof candidate.shared === "object" &&
85
+ candidate.shared !== null &&
86
+ typeof candidate.by_provider === "object" &&
87
+ candidate.by_provider !== null
88
+ );
89
+ }
@@ -0,0 +1,52 @@
1
+ export type {
2
+ CapabilityCatalog,
3
+ CatalogCapability,
4
+ RuntimeSeen,
5
+ } from "./capability/capability.types.ts";
6
+ export type { CommentFinding } from "./comment-policy/comment-policy.types.ts";
7
+ export type { CoreFacade } from "./core.facade.ts";
8
+ export { coreFacade } from "./core.facade.ts";
9
+ export type { FloorInput, FloorRule } from "./floor/floor.service.ts";
10
+ export type { FailureCategory, GateFinding, GateGap, LastGateArtifact, LockBody } from "./gate/gate.types.ts";
11
+ export type {
12
+ ForeignSlice,
13
+ GateResult,
14
+ HandoffFile,
15
+ HandoffProviderSlice,
16
+ HandoffShared,
17
+ } from "./handoff/handoff.types.ts";
18
+ export type {
19
+ HarnessLesson,
20
+ LessonEffectiveness,
21
+ LessonLink,
22
+ LessonLinkStatus,
23
+ LessonSource,
24
+ LessonStatus,
25
+ LessonStoreFile,
26
+ LessonTier,
27
+ PendingLessonCredit,
28
+ } from "./lesson/lesson.types.ts";
29
+ export type { ProviderTotals } from "./observability/observability.report.ts";
30
+ export type { SessionRollup } from "./observability/observability.store.ts";
31
+ export type {
32
+ CostPool,
33
+ CostSource,
34
+ ObsEvent,
35
+ ObservabilityConfig,
36
+ ObsKind,
37
+ ObsLevel,
38
+ } from "./observability/observability.types.ts";
39
+ export type {
40
+ LessonsPolicyConfig,
41
+ OperatorMode,
42
+ PartialPolicy,
43
+ Policy,
44
+ ProviderScoped,
45
+ } from "./policy/policy.types.ts";
46
+ export type { PresenceRecord } from "./presence/presence.types.ts";
47
+ export type { ShellEffectClass } from "./shell-policy/shell-policy.types.ts";
48
+ export type { ShipClaim, ShipClaimKind, ShipLedgerEvent, ShipLedgerRow } from "./ship/ship.types.ts";
49
+ export type { FingerprintEntry, FingerprintStore } from "./stagnation/stagnation.types.ts";
50
+ export type { ModelParam, ParentModelSnapshot } from "./subagent-policy/subagent-policy.types.ts";
51
+ export type { AutopilotPlan } from "./turn/turn.autopilot.ts";
52
+ export type { BootResult, LoopCheck, LoopState } from "./turn/turn.types.ts";
@@ -0,0 +1,81 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+
5
+ /**
6
+ * A hash the harness records after every write it makes to a file it later reads aloud to the model.
7
+ *
8
+ * why: the policy baseline answers "did this change since the session started", which is the right question for a
9
+ * file only an operator edits. The handoff changes every turn by design, so the question here is different — did
10
+ * something *other than the harness* change it. One write path per file makes that answerable
11
+ * ([/decisions/ad-078.md](/decisions/ad-078.md)).
12
+ *
13
+ * invariant: this is defence in depth and not the first line. The floor already refuses an agent write to either
14
+ * file, because both sit under the project state directory. What this catches is a write the floor did not see —
15
+ * another process, an MCP server with filesystem access, a route through a tool nothing named.
16
+ */
17
+ export type SealVerdict = "sealed" | "unsealed" | "diverged" | "absent";
18
+
19
+ const SCHEMA = "harness.seal.v1";
20
+
21
+ export function sealPath(target: string): string {
22
+ return join(dirname(target), ".seal", `${basename(target)}.json`);
23
+ }
24
+
25
+ function hashOf(target: string): string | null {
26
+ try {
27
+ return createHash("sha256").update(readFileSync(target)).digest("hex");
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * why: called inside the write lock. A seal recorded after the lock releases races the next writer, and the pair
35
+ * that loses leaves a seal matching neither content.
36
+ */
37
+ export function seal(target: string): void {
38
+ const hash = hashOf(target);
39
+ if (hash === null) {
40
+ return;
41
+ }
42
+ try {
43
+ mkdirSync(dirname(sealPath(target)), { recursive: true });
44
+ writeFileSync(sealPath(target), JSON.stringify({ schema: SCHEMA, hash }));
45
+ } catch {
46
+ // invariant: a seal that cannot be written is not an error the turn should carry. The next verify reads
47
+ // `unsealed` and adopts, which is the same state as a fresh install.
48
+ }
49
+ }
50
+
51
+ export function verifySeal(target: string): SealVerdict {
52
+ if (!existsSync(target)) {
53
+ return "absent";
54
+ }
55
+ let recorded: string | null = null;
56
+ try {
57
+ const parsed = JSON.parse(readFileSync(sealPath(target), "utf8")) as { hash?: unknown };
58
+ recorded = typeof parsed.hash === "string" ? parsed.hash : null;
59
+ } catch {
60
+ recorded = null;
61
+ }
62
+ if (recorded === null) {
63
+ // why: adopted, not refused. Every install that predates sealing has no sidecar, and refusing on absence
64
+ // would break all of them. The cost is stated: deleting the sidecar buys one unverified read.
65
+ return "unsealed";
66
+ }
67
+ return hashOf(target) === recorded ? "sealed" : "diverged";
68
+ }
69
+
70
+ /** why: one place decides what withholding means, so the two call sites cannot disagree about it. */
71
+ export function shouldInject(verdict: SealVerdict): boolean {
72
+ return verdict !== "diverged";
73
+ }
74
+
75
+ export function divergedMessage(target: string, what: string): string {
76
+ return [
77
+ `${what} at ${target} changed without a harness write behind it, so it was not injected into this turn.`,
78
+ "That file is read aloud to the model, so text placed in it reaches every later turn. Read it, and if the",
79
+ "contents are yours, the next harness write reseals it.",
80
+ ].join("\n");
81
+ }
@@ -0,0 +1,96 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { HarnessLesson, LessonLink, LessonTier } from "./lesson.types.ts";
3
+
4
+ /**
5
+ * The lesson store had exactly one producer: gate stagnation. So a lesson learned by *reasoning* — a code review, an
6
+ * incident, an operator's correction, a pattern noticed across several changes — had no way in, while the mechanism
7
+ * that would have carried it (ranked injection with recurrence decay) already existed and ran every session
8
+ * ([/decisions/ad-035.md](/decisions/ad-035.md)).
9
+ *
10
+ * invariant: this knows nothing about where a lesson came from in the world. No document layout, no decision-record
11
+ * convention, no directory. The harness ships the mechanism; a project decides what feeds it. Reading a repository's
12
+ * documentation to harvest lessons would couple every install to one project's filing habits.
13
+ *
14
+ * why: `source: "manual"` was already in `LessonSource` and produced by nothing — the union anticipated this route and
15
+ * it was never built.
16
+ */
17
+ export type AuthoredLessonInput = {
18
+ /** The one thing to do differently. This is what gets injected, so it is written as an instruction. */
19
+ instruction: string;
20
+ /** What went wrong, in the author's words. Empty is allowed — an instruction alone is still useful. */
21
+ avoid?: string;
22
+ prefer?: string;
23
+ preRetryCheck?: string;
24
+ /**
25
+ * The gate this applies to, when it applies to one. Ranking boosts a lesson whose gate matches the failing gate,
26
+ * and session-mode injection includes every lesson regardless — so a lesson about no particular gate still arrives.
27
+ */
28
+ gate?: string;
29
+ /** Words that should pull this lesson up when they appear in the failure text. */
30
+ triggerTokens?: string[];
31
+ /** What makes this lesson true. When any ref breaks, `garden` stops the lesson being injected. */
32
+ refs?: LessonLink[];
33
+ /** ISO bound after which this lesson stops being injected. */
34
+ validTo?: string;
35
+ tier?: Exclude<LessonTier, "core">;
36
+ /** A standing rule: injected before every scored lesson rather than competing with them. */
37
+ pinned?: boolean;
38
+ /**
39
+ * True when the command ran inside an agent session. Recorded rather than refused: an agent that cannot write down
40
+ * what it learned writes nothing down, which is the state this replaces. Marking it is what keeps it auditable.
41
+ */
42
+ inAgentSession?: boolean;
43
+ now?: string;
44
+ };
45
+
46
+ /** why: a stable id from the instruction, so writing the same lesson twice updates rather than duplicates. */
47
+ export function authoredLessonId(instruction: string): string {
48
+ const digest = createHash("sha256").update(instruction.trim().toLowerCase()).digest("hex").slice(0, 12);
49
+ return `manual:${digest}`;
50
+ }
51
+
52
+ export const AUTHORED_GATE = "any";
53
+
54
+ export function buildAuthoredLesson(input: AuthoredLessonInput): HarnessLesson {
55
+ const now = input.now ?? new Date().toISOString();
56
+ const instruction = input.instruction.trim();
57
+ return {
58
+ id: authoredLessonId(instruction),
59
+ scope: "gate-execution",
60
+ // why: `any` rather than a real gate name, so a retry for a specific gate is not falsely boosted by a lesson that
61
+ // was never about it. Session-mode injection carries it anyway.
62
+ failedGate: input.gate?.trim() || AUTHORED_GATE,
63
+ category: input.inAgentSession ? "authored-in-session" : "authored",
64
+ triggerTokens: (input.triggerTokens ?? []).map((token) => token.trim().toLowerCase()).filter(Boolean),
65
+ instruction,
66
+ avoid: input.avoid?.trim() ?? "",
67
+ prefer: input.prefer?.trim() ?? "",
68
+ preRetryCheck: input.preRetryCheck?.trim() ?? "",
69
+ source: "manual",
70
+ tier: input.tier ?? "project",
71
+ // why: `active`, not `candidate`. A candidate exists because the automatic producer is guessing from output; an
72
+ // author is not guessing, and making them wait for a promotion threshold that only recurrence can satisfy would
73
+ // mean an authored lesson never activates.
74
+ status: "active",
75
+ confidence: 0.8,
76
+ hitCount: 1,
77
+ // hazard: this was `0.8`, written as if priority were the 0..1 scale `confidence` uses. Every other producer
78
+ // is 70..100 and `relevanceScore` divides by 200, so an authored lesson contributed 0.004 where a core lesson
79
+ // contributes 0.45 — ranked below everything, always, and never injected under a real char budget.
80
+ priority: 80,
81
+ pinned: input.pinned === true,
82
+ refs: input.refs ?? [],
83
+ ...(input.validTo ? { validTo: input.validTo } : {}),
84
+ // why: an authored lesson has no failing session behind it, so it carries none. Promotion reads distinct
85
+ // sessions and an authored lesson is already active, so it never consults this.
86
+ sessionKeys: [],
87
+ injectedCount: 0,
88
+ gradeableCount: 0,
89
+ helpedCount: 0,
90
+ neutralCount: 0,
91
+ firstSeenAt: now,
92
+ lastSeenAt: now,
93
+ lastAccessedAt: now,
94
+ updatedAt: now,
95
+ };
96
+ }
@@ -0,0 +1,51 @@
1
+ import type { HarnessLesson, LessonEffectiveness } from "./lesson.types.ts";
2
+
3
+ export type LessonVerdict = "helped" | "neutral";
4
+
5
+ export function gradedCount(lesson: HarnessLesson): number {
6
+ return lesson.helpedCount + lesson.neutralCount;
7
+ }
8
+
9
+ // invariant: a rate over zero graded injections is null, not zero. Zero reads as "measured and it never helped";
10
+ // null is the only honest value for "nothing has tested this yet".
11
+ export function helpRate(lesson: HarnessLesson): number | null {
12
+ const graded = gradedCount(lesson);
13
+ return graded === 0 ? null : lesson.helpedCount / graded;
14
+ }
15
+
16
+ /**
17
+ * hazard: this read `injectedCount`, which counts session-start injections. Those are never graded — a lesson
18
+ * whose gate is `any` is not even eligible on a retry — so a healthy pinned standing rule reported `unproven`
19
+ * forever and `doctor` warned about it on every run, in every repository
20
+ * ([/decisions/ad-044.md](/decisions/ad-044.md)).
21
+ */
22
+ export function lessonEffectiveness(lesson: HarnessLesson): LessonEffectiveness {
23
+ const rate = helpRate(lesson);
24
+ if (rate === null) {
25
+ // why: only an injection a gate could have graded can be unproven. Everything else has had no opportunity.
26
+ return lesson.gradeableCount === 0 ? "not-injected" : "unproven";
27
+ }
28
+ return rate > 0 ? "helped" : "neutral";
29
+ }
30
+
31
+ export function creditLesson(lesson: HarnessLesson, verdict: LessonVerdict, now: string): HarnessLesson {
32
+ return {
33
+ ...lesson,
34
+ helpedCount: lesson.helpedCount + (verdict === "helped" ? 1 : 0),
35
+ neutralCount: lesson.neutralCount + (verdict === "neutral" ? 1 : 0),
36
+ updatedAt: now,
37
+ };
38
+ }
39
+
40
+ export function effectivenessLine(lesson: HarnessLesson): string {
41
+ const reading = lessonEffectiveness(lesson);
42
+ if (reading === "not-injected") {
43
+ return lesson.injectedCount === 0
44
+ ? "not-injected"
45
+ : `session-only (injected ${lesson.injectedCount}x, never for a gate)`;
46
+ }
47
+ if (reading === "unproven") {
48
+ return `unproven (injected for a gate ${lesson.gradeableCount}x, graded 0x)`;
49
+ }
50
+ return `${reading} ${lesson.helpedCount}/${gradedCount(lesson)}`;
51
+ }