carthagent 1.0.7 → 1.1.0-preview.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/README.md +80 -79
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  4. package/docs/glyph-review/test-isolation-v1.md +26 -0
  5. package/docs/glyph-review/typecheck-base.log +1 -0
  6. package/docs/glyph-review/typecheck-v4.log +1 -0
  7. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  8. package/docs/help-dispatch-review.md +69 -0
  9. package/docs/legacy-cli.md +150 -0
  10. package/docs/live-theme-plugin-api.md +18 -0
  11. package/docs/native-cli.md +102 -0
  12. package/docs/native-compatibility.md +16 -0
  13. package/docs/native-daily-quests.md +132 -0
  14. package/docs/native-glyph-pty.md +59 -0
  15. package/docs/native-manual-testing.md +314 -0
  16. package/docs/native-slash-acceptance-handoff.md +49 -0
  17. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  18. package/docs/native-terminal-performance.md +310 -0
  19. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  20. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  21. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  22. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  23. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  24. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  31. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  32. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  33. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  35. package/docs/native-terminal-semantics.md +89 -0
  36. package/docs/native-upgrade.md +177 -0
  37. package/docs/opencode-composed-source-review.md +89 -0
  38. package/docs/opencode-migration.md +225 -9
  39. package/docs/opencode-owned-runtime.md +241 -0
  40. package/docs/opencode-runtime.md +12 -0
  41. package/docs/opencode-theme-provider-proof/README.md +55 -0
  42. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  43. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  44. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  45. package/docs/owned-host-controls-proof.md +39 -0
  46. package/docs/owned-runtime-release-manifest.md +42 -0
  47. package/docs/owned-visual-clear-proof.md +40 -0
  48. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  49. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  50. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  51. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  52. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  53. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  54. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  57. package/docs/release-notes-native-draft.md +73 -0
  58. package/docs/release-notes-preview.10.md +121 -0
  59. package/docs/release-notes-preview.11.md +76 -0
  60. package/docs/release-notes-preview.12.md +55 -0
  61. package/docs/release-notes-preview.9.md +130 -0
  62. package/evaluate.mjs +28 -1
  63. package/lib/budget.mjs +1 -1
  64. package/lib/codegraph.mjs +1 -1
  65. package/lib/delivery.mjs +3 -2
  66. package/lib/environment.mjs +8 -0
  67. package/lib/guidance.mjs +1 -1
  68. package/lib/opencode/cli-runtime.mjs +1171 -0
  69. package/lib/opencode/cli.mjs +252 -0
  70. package/lib/opencode/credentials.mjs +63 -8
  71. package/lib/opencode/demo.mjs +53 -0
  72. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  73. package/lib/opencode/evaluation.mjs +32 -12
  74. package/lib/opencode/headless.mjs +8 -4
  75. package/lib/opencode/provider-policy.mjs +23 -0
  76. package/lib/opencode/review.mjs +37 -16
  77. package/lib/opencode/runtime-artifact.mjs +335 -0
  78. package/lib/opencode/runtime-github-transport.mjs +121 -0
  79. package/lib/opencode/runtime-release.json +36 -0
  80. package/lib/opencode/runtime.mjs +298 -20
  81. package/lib/opencode/service.mjs +77 -35
  82. package/lib/opencode/session-options.mjs +6 -2
  83. package/lib/opencode/update.mjs +222 -0
  84. package/lib/opencode/workspace.mjs +1 -1
  85. package/lib/planning-advice.mjs +2 -2
  86. package/lib/tui/framing.mjs +1 -80
  87. package/lib/update.mjs +10 -1
  88. package/package.json +33 -7
  89. package/plugins/carthagent/README.md +23 -0
  90. package/plugins/carthagent/src/bootstrap.ts +1 -1
  91. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  92. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  93. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  94. package/plugins/carthagent/src/index.ts +71 -24
  95. package/plugins/carthagent/src/rpc.ts +51 -6
  96. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  97. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  98. package/plugins/carthagent/src/server/daily-source-rpc.ts +197 -0
  99. package/plugins/carthagent/src/server/delivery.ts +217 -106
  100. package/plugins/carthagent/src/server/framing.mjs +80 -0
  101. package/plugins/carthagent/src/server/policy.ts +1 -1
  102. package/plugins/carthagent/src/server/providers.ts +58 -30
  103. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  104. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  105. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  106. package/plugins/carthagent/src/server/ship-guidance.ts +39 -0
  107. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  108. package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
  109. package/plugins/carthagent/src/testing.ts +5 -0
  110. package/plugins/carthagent/src/tui.tsx +492 -0
  111. package/plugins/carthagent/src/ui/client.ts +125 -0
  112. package/plugins/carthagent/src/ui/commands.ts +70 -0
  113. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  114. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  115. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  116. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  117. package/plugins/carthagent/src/ui/daily-quests.tsx +232 -0
  118. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  119. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  120. package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
  121. package/plugins/carthagent/src/ui/native-review.mjs +154 -0
  122. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  123. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  124. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  125. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  126. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  127. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  128. package/plugins/carthagent/src/ui/voyages-navigation.tsx +122 -0
  129. package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
  130. package/plugins/carthagent/src/ui/voyages.tsx +127 -0
  131. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  132. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  133. package/plugins/carthagent/src/voyages-rpc.ts +60 -0
  134. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  135. package/plugins/carthagent/tests/commands.test.ts +107 -0
  136. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  137. package/plugins/carthagent/tests/voyages-navigation.test.tsx +258 -0
  138. package/plugins/carthagent/themes/carthage.json +67 -0
  139. package/plugins/carthagent/tui.ts +26 -1
  140. package/script/ctg/build-owned-runtime.sh +166 -0
  141. package/tests/budget-engine.test.mjs +4 -3
  142. package/tests/cli.test.mjs +52 -96
  143. package/tests/daily-evidence.test.mjs +62 -0
  144. package/tests/delivery.test.mjs +22 -1
  145. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  146. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  147. package/tests/installed-runtime-contract.test.mjs +43 -0
  148. package/tests/native-cli-host-smoke.mjs +457 -0
  149. package/tests/native-daily-editor-focus.test.mjs +213 -0
  150. package/tests/native-daily-editor.test.mjs +43 -0
  151. package/tests/native-daily-quests-layout.test.mjs +53 -0
  152. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  153. package/tests/native-daily-quests-ui.test.mjs +778 -0
  154. package/tests/native-daily-source.test.mjs +426 -0
  155. package/tests/native-demo-ui.test.mjs +48 -0
  156. package/tests/native-evaluation-admission.test.mjs +24 -0
  157. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  158. package/tests/native-fixture-runtime.test.mjs +111 -0
  159. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  160. package/tests/native-glyph-pty.test.mjs +107 -0
  161. package/tests/native-manual-projects.test.mjs +170 -0
  162. package/tests/native-owned-runtime-env.test.mjs +140 -0
  163. package/tests/native-paired-guards.test.mjs +464 -0
  164. package/tests/native-provider-policy.test.mjs +31 -0
  165. package/tests/native-pty-invocation.test.mjs +114 -0
  166. package/tests/native-pty-runtime.test.mjs +45 -0
  167. package/tests/native-resume-session.test.mjs +46 -0
  168. package/tests/native-review-consumer.test.mjs +267 -0
  169. package/tests/native-runtime-identity.test.mjs +214 -0
  170. package/tests/native-ship-parity.test.mjs +526 -0
  171. package/tests/native-ship-plan.test.mjs +75 -0
  172. package/tests/native-ship-rpc.test.mjs +71 -0
  173. package/tests/native-ui-commands.test.mjs +6 -0
  174. package/tests/native-voyages-layout.test.mjs +28 -0
  175. package/tests/native-voyages-navigation.test.mjs +169 -0
  176. package/tests/native-voyages-rpc.test.mjs +395 -0
  177. package/tests/native-voyages-ui.test.mjs +277 -0
  178. package/tests/opencode-atomic-write.test.mjs +55 -0
  179. package/tests/opencode-cli.test.mjs +1312 -0
  180. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  181. package/tests/opencode-providers.test.mjs +245 -18
  182. package/tests/opencode-runtime.test.mjs +546 -8
  183. package/tests/opencode-session-controls.test.mjs +15 -0
  184. package/tests/opencode-ship-rpc.test.mjs +71 -0
  185. package/tests/opencode-update.test.mjs +475 -0
  186. package/tests/opencode-workspace.test.mjs +51 -2
  187. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  188. package/tests/release-workflow.test.mjs +46 -0
  189. package/tests/runtime-artifact.test.mjs +269 -0
  190. package/tests/runtime-development-trust.test.mjs +230 -0
  191. package/tests/runtime-github-transport.test.mjs +132 -0
  192. package/tests/runtime-token-env.test.mjs +23 -0
  193. package/tests/test_native_terminal_probe.py +76 -0
  194. package/tests/test_native_terminal_semantics.py +232 -0
  195. package/tests/update.test.mjs +36 -0
  196. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  197. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  198. package/tests/workspace-live.test.mjs +127 -0
  199. package/tools/benchmark/diagnose-request.mjs +59 -36
  200. package/tools/benchmark/eval-common.mjs +3 -3
  201. package/tools/benchmark/evaluators.test.mjs +4 -4
  202. package/tools/benchmark/guard.test.mjs +35 -2
  203. package/tools/benchmark/guard.ts +20 -7
  204. package/tools/benchmark/planning-startup.test.mjs +30 -34
  205. package/tools/benchmark/runner.mjs +12 -7
  206. package/tools/benchmark/runner.test.mjs +23 -7
  207. package/tools/benchmark/upstream-error.test.mjs +16 -2
  208. package/tools/generate-owned-runtime-release.mjs +217 -0
  209. package/tools/installed-runtime-contract.mjs +16 -0
  210. package/tools/manual-runtime-manifest.mjs +39 -0
  211. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  212. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  213. package/tools/native-fixture.mjs +199 -40
  214. package/tools/native-glyph-helpers.py +127 -0
  215. package/tools/native-glyph-pty.py +144 -0
  216. package/tools/native-manual-projects.mjs +259 -0
  217. package/tools/native-manual.mjs +338 -0
  218. package/tools/native-model-picker-acceptance.py +306 -0
  219. package/tools/native-owned-runtime-env.sh +47 -0
  220. package/tools/native-paired-guards.mjs +282 -0
  221. package/tools/native-paired-manifest.mjs +72 -0
  222. package/tools/native-paired-orchestration.sh +51 -0
  223. package/tools/native-plugin-readiness.mjs +33 -0
  224. package/tools/native-plugin-readiness.test.mjs +53 -0
  225. package/tools/native-pty-invocation.mjs +26 -0
  226. package/tools/native-pty-runtime.mjs +83 -0
  227. package/tools/native-pty-smoke.py +147 -24
  228. package/tools/native-public-repo-shell-result.mjs +13 -0
  229. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  230. package/tools/native-runtime-identity.mjs +201 -0
  231. package/tools/native-slash-acceptance-env.py +38 -0
  232. package/tools/native-terminal-semantics.py +876 -0
  233. package/tools/owned-shell-observer/index.ts +16 -0
  234. package/tools/owned-shell-observer/package.json +1 -0
  235. package/tools/owned-visual-clear-env.py +48 -0
  236. package/tools/owned-visual-clear-model.py +21 -0
  237. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  238. package/tools/prepare-native-manual.mjs +122 -0
  239. package/tools/run-native-terminal-paired.sh +403 -0
  240. package/tools/runtime-token-env.mjs +8 -0
  241. package/tools/seed-native-switch-fixture.mjs +79 -0
  242. package/tools/test-native-controls.mjs +3 -1
  243. package/tools/test-native-daily-quests.mjs +65 -0
  244. package/tools/test-native-default-cli.mjs +106 -0
  245. package/tools/test-native-delivery-host.mjs +100 -18
  246. package/tools/test-native-evaluation.mjs +7 -3
  247. package/tools/test-native-glyph-pty.mjs +94 -0
  248. package/tools/test-native-host.mjs +83 -61
  249. package/tools/test-native-outcomes.mjs +3 -1
  250. package/tools/test-native-package.mjs +206 -38
  251. package/tools/test-native-performance.mjs +26 -0
  252. package/tools/test-native-product.mjs +90 -0
  253. package/tools/test-native-pty.mjs +34 -9
  254. package/tools/test-native-public-repo.mjs +224 -0
  255. package/tools/test-native-refresh-reentry.mjs +106 -0
  256. package/tools/test-native-report.mjs +27 -20
  257. package/tools/test-native-review.mjs +75 -0
  258. package/tools/test-native-runtime.mjs +120 -0
  259. package/tools/test-native-ship-models.mjs +176 -0
  260. package/tools/test-native-ship-walkthrough.mjs +96 -0
  261. package/tools/test-native-slash-commands.mjs +125 -0
  262. package/tools/test-native-slash-commands.py +242 -0
  263. package/tools/test-native-swe-host.mjs +224 -55
  264. package/tools/test-native-tasks.mjs +77 -0
  265. package/tools/test-native-terminal-performance.py +887 -0
  266. package/tools/test-native-terminal-readiness.sh +85 -0
  267. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  268. package/tools/test-native-visual-clear-installed.mjs +48 -0
  269. package/tools/test-owned-host-controls.mjs +238 -0
  270. package/tools/test-owned-opencode-runtime.mjs +35 -0
  271. package/tools/test-owned-visual-clear-env.py +120 -0
  272. package/tools/test-owned-visual-clear.py +373 -0
  273. package/tools/verify-draft-runtime-gate.mjs +138 -0
  274. package/tools/verify-owned-runtime-release.mjs +114 -0
  275. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,197 @@
1
+ import type { Context } from '@opencode/plugin/promise/plugin';
2
+ import type { RpcCallContext } from '@opencode/plugin/promise/rpc';
3
+ import { constants, closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync } from 'node:fs';
4
+ import { isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
5
+ import { checkDigest, evidenceFresh, fingerprint, requiresReview, reviewFresh, validatePlan } from '../../../../lib/delivery.mjs';
6
+ import { DailySource, type DailySourceReport } from '../daily-source-rpc';
7
+
8
+ type RecordValue = Record<string, unknown>;
9
+ type SourceContext = Pick<Context, 'location' | 'session' | 'worktree'>;
10
+ const MAX_FILE = 1024 * 1024;
11
+ const hash = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
12
+ const record = (value: unknown): value is RecordValue => !!value && typeof value === 'object' && !Array.isArray(value);
13
+ const safeID = (value: unknown): value is string => typeof value === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
14
+ const invalid = () => new Error('invalid_source');
15
+ const missing = (error: unknown) => record(error) && error.code === 'ENOENT';
16
+
17
+ /** Match native directory identities through ancestor aliases (macOS /var, /tmp),
18
+ * never a linked directory leaf. Keep this local: importing pull-request-rpc's
19
+ * equivalent helper would also import unrelated process/review integrations. */
20
+ function canonicalDirectory(path: string) {
21
+ const absolute = resolve(path);
22
+ if (!lstatSync(absolute).isDirectory()) throw invalid();
23
+ return realpathSync(absolute);
24
+ }
25
+
26
+ /** Evidence paths use canonical native roots, then reject every linked component. */
27
+ function noLinks(path: string) {
28
+ const absolute = resolve(path);
29
+ let cursor = parse(absolute).root;
30
+ for (const part of relative(cursor, absolute).split(sep).filter(Boolean)) {
31
+ cursor = join(cursor, part);
32
+ if (lstatSync(cursor).isSymbolicLink()) throw invalid();
33
+ }
34
+ }
35
+
36
+ /** No directory/FIFO/device reads, no unbounded reads, and no final-component links. */
37
+ function localJson(path: string, max = MAX_FILE): { value: unknown; raw: string } | null {
38
+ try {
39
+ noLinks(path);
40
+ if (!lstatSync(path).isFile()) throw invalid();
41
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
42
+ try {
43
+ const stat = fstatSync(fd);
44
+ if (!stat.isFile() || stat.size > max) throw invalid();
45
+ const buffer = Buffer.alloc(max + 1);
46
+ let length = 0;
47
+ while (length < buffer.length) {
48
+ const n = readSync(fd, buffer, length, buffer.length - length, null);
49
+ if (!n) break;
50
+ length += n;
51
+ }
52
+ if (length > max) throw invalid();
53
+ const bytes = buffer.subarray(0, length);
54
+ const raw = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
55
+ return { value: JSON.parse(raw), raw };
56
+ } finally { closeSync(fd); }
57
+ } catch (error) { if (missing(error)) return null; throw invalid(); }
58
+ }
59
+
60
+ function validateState(value: unknown, directory: string, runId: string) {
61
+ if (!record(value) || value.runId !== runId || !Number.isSafeInteger(value.revision) || (value.revision as number) < 1 || !record(value.plan) || !Array.isArray(value.plan.checks) || value.plan.checks.length > 20000) throw invalid();
62
+ if (value.status !== undefined && !['implementing', 'verifying', 'verified', 'blocked'].includes(value.status as string)) throw invalid();
63
+ validatePlan(value.plan, directory);
64
+ const checks = value.plan.checks as Array<RecordValue & { id: string }>;
65
+ if (value.evidence !== undefined && !record(value.evidence)) throw invalid();
66
+ const evidence = (value.evidence ?? {}) as Record<string, RecordValue>;
67
+ const ids = new Set(checks.map(check => check.id));
68
+ if (Object.keys(evidence).some(id => !ids.has(id))) throw invalid();
69
+ for (const receipt of Object.values(evidence)) {
70
+ // A real failed native receipt can have a null fingerprint after scope failure.
71
+ if (!record(receipt) || typeof receipt.passed !== 'boolean' || receipt.fingerprint !== undefined && receipt.fingerprint !== null && !hash(receipt.fingerprint)) throw invalid();
72
+ for (const key of ['runId', 'checkDigest', 'outputTail']) if (receipt[key] !== undefined && typeof receipt[key] !== 'string') throw invalid();
73
+ for (const key of ['revision', 'executedRevision', 'code']) if (receipt[key] !== undefined && !Number.isSafeInteger(receipt[key])) throw invalid();
74
+ for (const key of ['timedOut', 'cancelled', 'changedDuringCheck', 'outputLimit']) if (receipt[key] !== undefined && typeof receipt[key] !== 'boolean') throw invalid();
75
+ if (receipt.durationMs !== undefined && (typeof receipt.durationMs !== 'number' || !Number.isFinite(receipt.durationMs) || receipt.durationMs < 0)) throw invalid();
76
+ }
77
+ if (value.reviewEvidence !== undefined && !record(value.reviewEvidence)) throw invalid();
78
+ if (value.handoff !== undefined && !record(value.handoff)) throw invalid();
79
+ if (value.reviewFindings !== undefined && (!Array.isArray(value.reviewFindings) || value.reviewFindings.length > 20000)) throw invalid();
80
+ const findings = (value.reviewFindings ?? []) as RecordValue[];
81
+ const findingIDs = new Set<string>();
82
+ for (const finding of findings) {
83
+ if (!record(finding) || !safeID(finding.id) || findingIDs.has(finding.id) || typeof finding.description !== 'string') throw invalid();
84
+ findingIDs.add(finding.id);
85
+ if (finding.resolution !== undefined && (!record(finding.resolution) || finding.resolution.id !== finding.id || typeof finding.resolution.change !== 'string' || finding.resolution.evidence !== undefined && typeof finding.resolution.evidence !== 'string')) throw invalid();
86
+ }
87
+ return { state: value, checks, evidence, findings };
88
+ }
89
+
90
+ function sourceFor(directory: string, sessionID: string): DailySourceReport | null {
91
+ const pointerPath = join(directory, '.harness', 'active.json');
92
+ const pointer = localJson(pointerPath, 4096);
93
+ if (!pointer) return null;
94
+ if (!record(pointer.value) || pointer.value.version !== 1 || typeof pointer.value.path !== 'string' || isAbsolute(pointer.value.path)) throw invalid();
95
+ // Same exact active-session/run strategy as workspace-rpc.reportFor; never latestReport.
96
+ const parts = pointer.value.path.split(sep);
97
+ if (parts.length !== 4 || parts[0] !== '.harness' || !safeID(parts[1]) || !safeID(parts[2]) || parts[3] !== 'report.json') throw invalid();
98
+ if (parts[1] !== sessionID) return null;
99
+ const path = join(directory, ...parts);
100
+ const file = localJson(path);
101
+ if (!file) return null;
102
+ const { state, checks, evidence, findings } = validateState(file.value, directory, parts[2]);
103
+ const current = fingerprint(directory, ['.']);
104
+ if (!hash(current)) throw invalid();
105
+ let truncated = checks.length > 12 || findings.length > 5;
106
+ // Omit an entire oversized claim instead of presenting a misleading partial sentence.
107
+ // Byte bounds are stricter than the schema's character bounds and preserve UTF-8.
108
+ const text = (value: unknown, limit: number) => {
109
+ if (typeof value !== 'string') return '';
110
+ if (Buffer.byteLength(value, 'utf8') > limit || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\p{Cs}]/u.test(value)) { truncated = true; return ''; }
111
+ return value;
112
+ };
113
+ const isFresh = (check: RecordValue & { id: string }) => {
114
+ const receipt = evidence[check.id];
115
+ return evidenceFresh(check, receipt, current, state) && receipt?.code === 0 && !receipt.timedOut && !receipt.cancelled && !receipt.changedDuringCheck && !receipt.outputLimit;
116
+ };
117
+ const isCurrentFailure = (check: RecordValue & { id: string }) => {
118
+ const receipt = evidence[check.id];
119
+ // A failed receipt is not evidence of failure at the current source revision
120
+ // unless both its identity and actual execution outcome are bound to it.
121
+ return receipt?.passed === false && receipt.runId === state.runId && receipt.revision === state.revision &&
122
+ receipt.checkDigest === checkDigest(check) && receipt.fingerprint === current && !receipt.changedDuringCheck &&
123
+ (Number.isSafeInteger(receipt.code) && receipt.code !== 0 || receipt.timedOut === true || receipt.cancelled === true || receipt.outputLimit === true);
124
+ };
125
+ const stale = checks.some(check => {
126
+ const receipt = evidence[check.id];
127
+ return receipt && (receipt.runId !== state.runId || receipt.revision !== state.revision || receipt.checkDigest !== checkDigest(check) || receipt.fingerprint !== current || receipt.passed && !isFresh(check));
128
+ }) || requiresReview(state) && !reviewFresh(state, current) || record(state.handoff) && state.handoff.fingerprint !== current;
129
+ const result: DailySourceReport = {
130
+ runId: parts[2], revision: current, planRevision: state.revision as number,
131
+ freshness: stale ? 'stale' : 'current', goal: text((state.plan as RecordValue).goal, 1024),
132
+ // Native delivery_finish stores review/launch/limitations, not a change summary.
133
+ // Do not manufacture a change claim from those fields or from the goal.
134
+ change: '',
135
+ checks: checks.slice(0, 12).map(check => {
136
+ const receipt = evidence[check.id];
137
+ const fresh = Boolean(isFresh(check));
138
+ const failed = isCurrentFailure(check);
139
+ // Omit unbound failure output rather than making old/scope-error text look
140
+ // like an execution result for the current revision in the outbound DTO.
141
+ const output = receipt?.passed === false && !failed ? undefined : receipt?.outputTail;
142
+ return { id: check.id, before: 'not_run', after: fresh ? 'passed' : failed ? 'failed' : 'not_run', revision: current, result: text(output, 1024) };
143
+ }),
144
+ findings: findings.slice(0, 5).map(finding => {
145
+ const resolution = record(finding.resolution) ? finding.resolution : {};
146
+ return { id: finding.id as string, issue: text(finding.description, 768), evidence: text(resolution.evidence, 1536), resolution: text(resolution.change, 1024) };
147
+ }),
148
+ truncated: false,
149
+ };
150
+ result.truncated = truncated;
151
+ // Reject a concurrent active-run/report replacement rather than mixing snapshots.
152
+ if (localJson(pointerPath, 4096)?.raw !== pointer.raw || localJson(path)?.raw !== file.raw) throw invalid();
153
+ if (Buffer.byteLength(JSON.stringify(result), 'utf8') > 64 * 1024) throw invalid();
154
+ return result;
155
+ }
156
+
157
+ /** No storage writes, transcript/capture/log imports, credential lookup, or cloud client. */
158
+ export function createDailySourceRpcHandlers(ctx: SourceContext) {
159
+ return {
160
+ report: async (input: unknown, call: RpcCallContext<typeof DailySource.methods.report>) => {
161
+ call.signal.throwIfAborted();
162
+ if (!record(input) || !safeID(input.sessionID) || Object.keys(input).some(key => key !== 'sessionID')) return call.error('dailySourceError', 'invalid_request', { code: 'invalid_request' });
163
+ try {
164
+ const root = canonicalDirectory(ctx.location.project.canonical);
165
+ const location = canonicalDirectory(ctx.location.directory);
166
+ const session = await ctx.session.get({ sessionID: input.sessionID }, { signal: call.signal });
167
+ if (session.id !== input.sessionID || session.projectID !== ctx.location.project.id || !isAbsolute(session.location.directory)) throw new Error('not_owned');
168
+ const directory = canonicalDirectory(session.location.directory);
169
+ // Ownership mirrors workspace-rpc. Durable task records alone cannot authorize
170
+ // a forged worktree: also require the native project's authoritative inventory.
171
+ const journal = localJson(join(root, '.harness', 'opencode-tasks.json'))?.value;
172
+ if (journal !== undefined && (!record(journal) || journal.version !== 1 || !Array.isArray(journal.tasks) || !Array.isArray(journal.operations))) throw invalid();
173
+ const tasks = record(journal) ? journal.tasks as unknown[] : [];
174
+ const operations = record(journal) ? journal.operations as unknown[] : [];
175
+ const owned = [...tasks, ...operations].find(item => record(item) && item.sessionID === session.id && typeof item.worktree === 'string');
176
+ const expected = record(owned) && typeof owned.worktree === 'string' ? canonicalDirectory(owned.worktree) : location;
177
+ if (directory !== expected) throw new Error('not_owned');
178
+ if (directory !== root && directory !== location) {
179
+ const native = await ctx.worktree.list({ projectID: ctx.location.project.id }, { signal: call.signal });
180
+ if (!native.some(entry => canonicalDirectory(entry.directory) === directory)) throw new Error('not_owned');
181
+ }
182
+ noLinks(directory);
183
+ call.signal.throwIfAborted();
184
+ return { source: sourceFor(directory, session.id) };
185
+ } catch (error) {
186
+ if (call.signal.aborted) throw error;
187
+ const code = error instanceof Error && error.message === 'not_owned' ? 'not_owned' : 'invalid_source';
188
+ return call.error('dailySourceError', code, { code });
189
+ }
190
+ },
191
+ };
192
+ }
193
+
194
+ /** Parent-owned composition chooses when to register this local-only boundary. */
195
+ export async function registerDailySourceRpc(ctx: Context) {
196
+ return ctx.rpc.register(DailySource, createDailySourceRpcHandlers(ctx));
197
+ }