@workit-dev/core 0.0.1 → 1.0.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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -43
  3. package/package.json +56 -6
  4. package/schemas/config.json +74 -0
  5. package/schemas/vcs.json +79 -0
  6. package/schemas/workspaces.json +1029 -0
  7. package/schemas/youtrack.json +84 -0
  8. package/scripts/analyze-release-scope.ts +215 -0
  9. package/scripts/bundled-deps.json +131 -0
  10. package/scripts/fixtures/sample-plan.md +15 -0
  11. package/scripts/fixtures/sample-sdd/progress.md +2 -0
  12. package/scripts/install-codex-plugin.sh +28 -0
  13. package/scripts/install-cursor-plugin.sh +146 -0
  14. package/scripts/install-opencode-plugin.sh +79 -0
  15. package/scripts/install-pi-package.sh +56 -0
  16. package/scripts/lib/config-dir.sh +28 -0
  17. package/scripts/publish-changed-packages.ts +395 -0
  18. package/scripts/rewrite-workspace-deps.ts +53 -0
  19. package/scripts/run-cursor-mcp.sh +6 -0
  20. package/scripts/sync-release-manifests.ts +82 -0
  21. package/scripts/sync-runtime.sh +142 -0
  22. package/scripts/validate-cursor-marketplace.ts +171 -0
  23. package/scripts/verify-bundle-sources.ts +134 -0
  24. package/scripts/write-schemas.ts +16 -0
  25. package/skills/workit-architecture/SKILL.md +72 -0
  26. package/skills/workit-architecture/agents/openai.yaml +3 -0
  27. package/skills/workit-architecture/references/instruction-files.md +52 -0
  28. package/skills/workit-architecture/references/vocabulary.md +53 -0
  29. package/skills/workit-bdd/SKILL.md +53 -0
  30. package/skills/workit-continue/SKILL.md +54 -0
  31. package/skills/workit-debug/SKILL.md +45 -0
  32. package/skills/workit-deslop/SKILL.md +58 -0
  33. package/skills/workit-fanout/SKILL.md +71 -0
  34. package/skills/workit-fanout/references/brief.md +141 -0
  35. package/skills/workit-fanout/references/orchestration.md +169 -0
  36. package/skills/workit-implement/SKILL.md +71 -0
  37. package/skills/workit-retro/SKILL.md +72 -0
  38. package/skills/workit-retro/agents/openai.yaml +3 -0
  39. package/skills/workit-retro/references/sources.md +35 -0
  40. package/skills/workit-retro/references/steering.md +39 -0
  41. package/skills/workit-review/SKILL.md +66 -0
  42. package/skills/workit-review/references/impact.md +24 -0
  43. package/skills/workit-shape/SKILL.md +71 -0
  44. package/skills/workit-shape/references/diagrams.md +17 -0
  45. package/skills/workit-shape/references/knowledge.md +61 -0
  46. package/skills/workit-shape/references/mockups.md +15 -0
  47. package/skills/workit-shape/references/slicing.md +44 -0
  48. package/skills/workit-ship/SKILL.md +71 -0
  49. package/skills/workit-ship/references/changelog.md +44 -0
  50. package/skills/workit-ship/references/pr-body.md +32 -0
  51. package/skills/workit-ship/references/release.md +39 -0
  52. package/skills/workit-test-audit/SKILL.md +54 -0
  53. package/skills/workit-verify-app/SKILL.md +68 -0
  54. package/skills/workit-verify-app/references/template.md +51 -0
  55. package/src/autonomy.ts +910 -0
  56. package/src/check-config.ts +613 -0
  57. package/src/checks.ts +534 -0
  58. package/src/ci-waiter.ts +159 -0
  59. package/src/core/boundary.ts +55 -0
  60. package/src/core/branch-policy.ts +112 -0
  61. package/src/core/branch.ts +1192 -0
  62. package/src/core/changelog-gate.ts +349 -0
  63. package/src/core/changelog-merge.ts +214 -0
  64. package/src/core/changelog.ts +392 -0
  65. package/src/core/cleanup.ts +638 -0
  66. package/src/core/cli-locate.ts +173 -0
  67. package/src/core/commit-flavors.ts +65 -0
  68. package/src/core/config-schemas.ts +653 -0
  69. package/src/core/config.ts +351 -0
  70. package/src/core/context-read.ts +144 -0
  71. package/src/core/docs-gate.ts +69 -0
  72. package/src/core/finding-evidence.ts +89 -0
  73. package/src/core/finding-queue.ts +36 -0
  74. package/src/core/git.ts +73 -0
  75. package/src/core/gitignore.ts +52 -0
  76. package/src/core/heavy-eta.ts +170 -0
  77. package/src/core/heavy-lease.ts +290 -0
  78. package/src/core/heavy-rules.ts +373 -0
  79. package/src/core/heavy-scope.ts +131 -0
  80. package/src/core/heavy-words.ts +83 -0
  81. package/src/core/heavy.ts +535 -0
  82. package/src/core/hygiene.ts +149 -0
  83. package/src/core/init.ts +218 -0
  84. package/src/core/live-session.ts +333 -0
  85. package/src/core/logger.ts +310 -0
  86. package/src/core/methods.ts +185 -0
  87. package/src/core/model-fallback.ts +208 -0
  88. package/src/core/node-resolve.mjs +249 -0
  89. package/src/core/operation-input.ts +632 -0
  90. package/src/core/package-root.ts +28 -0
  91. package/src/core/policy/attributes.ts +97 -0
  92. package/src/core/policy/bookkeeping.ts +119 -0
  93. package/src/core/policy/carry.ts +122 -0
  94. package/src/core/policy/check-evidence.ts +115 -0
  95. package/src/core/policy/current-judgment.ts +20 -0
  96. package/src/core/policy/derive.ts +387 -0
  97. package/src/core/policy/judgment.ts +246 -0
  98. package/src/core/policy/lane.ts +562 -0
  99. package/src/core/policy/pr-lane.ts +32 -0
  100. package/src/core/policy/substance.ts +365 -0
  101. package/src/core/policy-resolver.ts +94 -0
  102. package/src/core/pr-create.ts +448 -0
  103. package/src/core/pr-template.ts +162 -0
  104. package/src/core/release-notes.ts +197 -0
  105. package/src/core/release-tracks.ts +779 -0
  106. package/src/core/remotes.ts +436 -0
  107. package/src/core/repo-context.ts +542 -0
  108. package/src/core/repo-paths.ts +29 -0
  109. package/src/core/resources.ts +1299 -0
  110. package/src/core/role-models.ts +603 -0
  111. package/src/core/route-intent.ts +305 -0
  112. package/src/core/rules.ts +65 -0
  113. package/src/core/runtime-identity.ts +63 -0
  114. package/src/core/safe-write.ts +87 -0
  115. package/src/core/scope-glob.ts +56 -0
  116. package/src/core/scripts.ts +6 -0
  117. package/src/core/skill-manifests.ts +173 -0
  118. package/src/core/store-lock.ts +374 -0
  119. package/src/core/support-matrix.ts +24 -0
  120. package/src/core/task-context.ts +247 -0
  121. package/src/core/task-contract.ts +1279 -0
  122. package/src/core/task-delivery.ts +109 -0
  123. package/src/core/task-engine.ts +2248 -0
  124. package/src/core/task-evaluation.ts +1147 -0
  125. package/src/core/task-store.ts +2649 -0
  126. package/src/core/task-view.ts +180 -0
  127. package/src/core/templates.ts +22 -0
  128. package/src/core/tracker-issues.ts +174 -0
  129. package/src/core/vcs-config.ts +540 -0
  130. package/src/core/workers.ts +62 -0
  131. package/src/core/workit-paths.mjs +242 -0
  132. package/src/core/workspace-branch-policy.ts +123 -0
  133. package/src/core/workspaces.ts +1745 -0
  134. package/src/core/youtrack-backend.ts +288 -0
  135. package/src/core/youtrack-tools.ts +260 -0
  136. package/src/core/youtrack-writes.ts +73 -0
  137. package/src/core/youtrack-ytrack.ts +217 -0
  138. package/src/core/youtrack.ts +454 -0
  139. package/src/core/ytrack-skill.ts +297 -0
  140. package/src/core.ts +171 -0
  141. package/src/deslop/lex.ts +271 -0
  142. package/src/deslop/scan.ts +232 -0
  143. package/src/duration.ts +27 -0
  144. package/src/fanout-brief.ts +486 -0
  145. package/src/fanout-check.ts +455 -0
  146. package/src/fanout-landed.ts +370 -0
  147. package/src/fanout-stack.ts +108 -0
  148. package/src/fanout-status.ts +362 -0
  149. package/src/fanout-worktree.ts +893 -0
  150. package/src/fanout.ts +1473 -0
  151. package/src/forge/exec.ts +385 -0
  152. package/src/forge/github.ts +831 -0
  153. package/src/forge/gitlab.ts +906 -0
  154. package/src/forge/issue-forge.ts +209 -0
  155. package/src/forge/issue-link.ts +468 -0
  156. package/src/forge/issue-ops.ts +314 -0
  157. package/src/forge/merge-gate-base-red.ts +72 -0
  158. package/src/forge/pr-body.ts +100 -0
  159. package/src/forge/pr-edit.ts +292 -0
  160. package/src/forge/pr-ops.ts +1109 -0
  161. package/src/forge/pr-status.ts +10 -0
  162. package/src/forge/redact.ts +195 -0
  163. package/src/forge/report.ts +994 -0
  164. package/src/forge/reruns.ts +132 -0
  165. package/src/forge/resolve.ts +509 -0
  166. package/src/forge/types.ts +242 -0
  167. package/src/forge/verify.ts +398 -0
  168. package/src/git/default-base.ts +130 -0
  169. package/src/git/env-redirect.ts +28 -0
  170. package/src/git/facts.ts +122 -0
  171. package/src/git/ops.ts +1823 -0
  172. package/src/git/refs-read.ts +332 -0
  173. package/src/git/release.ts +1020 -0
  174. package/src/git/rev.ts +1175 -0
  175. package/src/git/session-hook.ts +597 -0
  176. package/src/git/stage.ts +63 -0
  177. package/src/hooks/agent-model.ts +149 -0
  178. package/src/hooks/budget-signal.ts +284 -0
  179. package/src/hooks/context.ts +461 -0
  180. package/src/hooks/cwd-normalize.ts +79 -0
  181. package/src/hooks/deadline.ts +228 -0
  182. package/src/hooks/delivery-verbs.ts +182 -0
  183. package/src/hooks/descriptor.ts +124 -0
  184. package/src/hooks/handle.ts +433 -0
  185. package/src/hooks/heavy-gate.ts +99 -0
  186. package/src/hooks/hook-runs.ts +130 -0
  187. package/src/hooks/hosts/claude-code.ts +380 -0
  188. package/src/hooks/hosts/codex.ts +372 -0
  189. package/src/hooks/hosts/cursor.ts +419 -0
  190. package/src/hooks/hosts/fields.ts +98 -0
  191. package/src/hooks/hosts/opencode.ts +79 -0
  192. package/src/hooks/hosts/pi.ts +78 -0
  193. package/src/hooks/index.ts +60 -0
  194. package/src/hooks/model-limit.ts +378 -0
  195. package/src/hooks/model-switch.ts +35 -0
  196. package/src/hooks/policy.ts +29 -0
  197. package/src/hooks/process.ts +48 -0
  198. package/src/hooks/protocol.ts +135 -0
  199. package/src/hooks/raw-git.ts +2289 -0
  200. package/src/hooks/repeat-failure.ts +115 -0
  201. package/src/hooks/run.ts +54 -0
  202. package/src/hooks/session-marker.ts +151 -0
  203. package/src/hooks/shell-words.ts +532 -0
  204. package/src/hooks/skill-nudge.ts +295 -0
  205. package/src/hooks/spawn-gate.ts +72 -0
  206. package/src/hooks/standing-signal.ts +99 -0
  207. package/src/hooks/standing.ts +225 -0
  208. package/src/hooks/steering.ts +74 -0
  209. package/src/hooks/stop.ts +408 -0
  210. package/src/hooks/worktree-confine.ts +415 -0
  211. package/src/hooks/write-gate.ts +1174 -0
  212. package/src/host-session.ts +459 -0
  213. package/src/knowledge.ts +368 -0
  214. package/src/ledger.ts +2913 -0
  215. package/src/stack.ts +2570 -0
  216. package/src/store/event-log.ts +217 -0
  217. package/src/store/notes.ts +29 -0
  218. package/src/store/patch.ts +135 -0
  219. package/src/store/paths.ts +368 -0
  220. package/src/store/reduce.ts +203 -0
  221. package/src/test-audit-mocks.ts +309 -0
  222. package/src/verdict-cover.ts +151 -0
  223. package/src/worktree-deps.ts +142 -0
  224. package/templates/headers.md +3 -0
  225. package/templates/hygiene/.editorconfig +8 -0
  226. package/templates/hygiene/.gitattributes +3 -0
  227. package/templates/hygiene/CHANGELOG.md +14 -0
  228. package/templates/hygiene/CONTRIBUTING.md +3 -0
  229. package/templates/hygiene/LICENSE +21 -0
  230. package/templates/hygiene/README.md +3 -0
  231. package/templates/issue-update.md +5 -0
  232. package/templates/plan-template.md +26 -0
  233. package/templates/spec-template.md +64 -0
  234. package/templates/workit-contract.md +13 -0
@@ -0,0 +1,1279 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as z from "zod";
3
+ import { scopePathCovers } from "./scope-glob";
4
+
5
+ export const SCHEMA_VERSION = 1 as const;
6
+ export const POLICY_VERSION = "1.0.0" as const;
7
+ export const OPERATION_FAMILIES = [
8
+ "task",
9
+ "policy",
10
+ "evidence",
11
+ "finding",
12
+ "decision",
13
+ "worker",
14
+ "state",
15
+ ] as const;
16
+ export type OperationFamily = (typeof OPERATION_FAMILIES)[number];
17
+
18
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
19
+ const digestPattern = /^[0-9a-f]{64}$/;
20
+ const invalidUnicode = (value: string): boolean => {
21
+ for (let i = 0; i < value.length; i += 1) {
22
+ const code = value.charCodeAt(i);
23
+ if (code >= 0xd800 && code <= 0xdbff) {
24
+ const next = value.charCodeAt(i + 1);
25
+ if (Number.isNaN(next) || next < 0xdc00 || next > 0xdfff) return true;
26
+ i += 1;
27
+ } else if (code >= 0xdc00 && code <= 0xdfff) return true;
28
+ }
29
+ return false;
30
+ };
31
+
32
+ const text = z.string().check((ctx) => {
33
+ if (invalidUnicode(ctx.value))
34
+ ctx.issues.push({ code: "custom", input: ctx.value, message: "invalid Unicode", path: [] });
35
+ });
36
+ const id = text.regex(uuidPattern, "expected lowercase UUID");
37
+ /** A full id or an unambiguous lowercase prefix of one (the 8-char short id other surfaces print). */
38
+ export const idRefPattern = /^[0-9a-f][0-9a-f-]{7,35}$/;
39
+ const idRef = text.regex(idRefPattern, "expected a UUID or an id prefix of 8+ characters");
40
+ const digest = text.regex(digestPattern, "expected lowercase SHA-256 digest");
41
+ const revision = text.regex(uuidPattern, "expected UUID revision");
42
+ const validUtc = (value: string): boolean => {
43
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/.exec(value);
44
+ if (!match) return false;
45
+ const [, year, month, day, hour, minute, second] = match;
46
+ const date = new Date(0);
47
+ date.setUTCFullYear(Number(year), Number(month) - 1, Number(day));
48
+ date.setUTCHours(Number(hour), Number(minute), Number(second), 0);
49
+ return (
50
+ date.getUTCFullYear() === Number(year) &&
51
+ date.getUTCMonth() === Number(month) - 1 &&
52
+ date.getUTCDate() === Number(day) &&
53
+ date.getUTCHours() === Number(hour) &&
54
+ date.getUTCMinutes() === Number(minute) &&
55
+ date.getUTCSeconds() === Number(second)
56
+ );
57
+ };
58
+ export const utcSchema = text.check((ctx) => {
59
+ if (!validUtc(ctx.value))
60
+ ctx.issues.push({
61
+ code: "custom",
62
+ input: ctx.value,
63
+ message: "expected UTC timestamp",
64
+ path: [],
65
+ });
66
+ });
67
+ const utc = utcSchema;
68
+ const safeInteger = z.number().int().safe();
69
+ const nonEmpty = text.min(1);
70
+ const pathValue = nonEmpty.check((ctx) => {
71
+ if (ctx.value !== "." && (ctx.value.includes("\\") || ctx.value.split("/").includes(".."))) {
72
+ ctx.issues.push({
73
+ code: "custom",
74
+ input: ctx.value,
75
+ message: "invalid path",
76
+ path: [],
77
+ });
78
+ }
79
+ });
80
+ const nullableDigest = digest.nullable();
81
+ const nullableId = id.nullable();
82
+
83
+ export const hostSchema = z.enum([
84
+ "opencode",
85
+ "cursor",
86
+ "codex_cli",
87
+ "codex_desktop",
88
+ "pi",
89
+ "workit_cli",
90
+ "claude_code",
91
+ ]);
92
+ export type Host = z.infer<typeof hostSchema>;
93
+ export const assuranceSchema = z.enum(["enforced", "agent_guided", "unavailable"]);
94
+ export type Assurance = z.infer<typeof assuranceSchema>;
95
+ export const outcomeSchema = z.enum(["verified", "accepted_limitations", "stopped"]);
96
+ export type Outcome = z.infer<typeof outcomeSchema>;
97
+
98
+ export const scopeSchema = z
99
+ .object({
100
+ description: text,
101
+ paths: z.array(pathValue),
102
+ exclusions: z.array(pathValue),
103
+ })
104
+ .strict();
105
+ export type Scope = z.infer<typeof scopeSchema>;
106
+
107
+ export const scopeCovers = (outer: Scope, inner: Scope): boolean => {
108
+ // Scope paths may carry trailing slashes; normalize so `docs/` covers `docs/x`.
109
+ const strip = (value: string): string => (value.length > 1 ? value.replace(/\/+$/, "") : value);
110
+ const normOuter: Scope = {
111
+ ...outer,
112
+ paths: outer.paths.map(strip),
113
+ exclusions: outer.exclusions.map(strip),
114
+ };
115
+ const innerPaths = (inner.paths.length ? inner.paths : ["."]).map(strip);
116
+ const innerExclusions = inner.exclusions.map(strip);
117
+ const excluded = (path: string, scope: Scope): boolean =>
118
+ scope.exclusions.some(
119
+ (excludedPath) => excludedPath === path || path.startsWith(`${excludedPath}/`),
120
+ );
121
+ const covers = (path: string): boolean =>
122
+ normOuter.paths.some((base) => scopePathCovers(base, path)) && !excluded(path, normOuter);
123
+ const innerExcludes = (path: string): boolean =>
124
+ innerExclusions.some(
125
+ (excludedPath) => excludedPath === path || path.startsWith(`${excludedPath}/`),
126
+ );
127
+ return (
128
+ innerPaths.every(covers) &&
129
+ !normOuter.exclusions.some((excludedPath) =>
130
+ innerPaths.some(
131
+ (base) =>
132
+ (base === "." || excludedPath === base || excludedPath.startsWith(`${base}/`)) &&
133
+ !innerExcludes(excludedPath),
134
+ ),
135
+ )
136
+ );
137
+ };
138
+
139
+ export const refSchema = z.discriminatedUnion("kind", [
140
+ z.object({ kind: z.literal("file"), path: pathValue, digest: nullableDigest }).strict(),
141
+ z
142
+ .object({
143
+ kind: z.literal("record"),
144
+ collection: z.enum(["evidence", "decisions", "findings", "workers"]),
145
+ id,
146
+ })
147
+ .strict(),
148
+ z.object({ kind: z.literal("host"), host: hostSchema, handle: nonEmpty }).strict(),
149
+ z.object({ kind: z.literal("external"), url: text.url() }).strict(),
150
+ ]);
151
+ export type Ref = z.infer<typeof refSchema>;
152
+
153
+ /** Maps one reference during record traversal. Returning null marks the
154
+ * reference unportable: arrays drop it, nullable fields null it, and
155
+ * non-nullable positions propagate the drop to their owner. */
156
+ export type RefMap = (ref: Ref) => Ref | null;
157
+
158
+ type RewriteOutcome = { drop: boolean; value?: unknown };
159
+
160
+ const rewriteUnknown = (value: unknown, map: RefMap): RewriteOutcome => {
161
+ if (Array.isArray(value)) {
162
+ const out: unknown[] = [];
163
+ for (const item of value) {
164
+ const rewritten = rewriteUnknown(item, map);
165
+ if (!rewritten.drop) out.push(rewritten.value);
166
+ }
167
+ return { drop: false, value: out };
168
+ }
169
+ if (typeof value === "object" && value !== null) {
170
+ if (refSchema.safeParse(value).success) {
171
+ const mapped = map(value as Ref);
172
+ return mapped === null ? { drop: true } : { drop: false, value: mapped };
173
+ }
174
+ const out: Record<string, unknown> = {};
175
+ for (const [key, item] of Object.entries(value)) {
176
+ const rewritten = rewriteUnknown(item, map);
177
+ out[key] = rewritten.drop ? null : rewritten.value;
178
+ }
179
+ return { drop: false, value: out };
180
+ }
181
+ return { drop: false, value };
182
+ };
183
+
184
+ const rewriteNode = (value: unknown, schema: z.ZodType, map: RefMap): RewriteOutcome => {
185
+ // Note: this zod version's classic accessors return core types, hence the
186
+ // casts below. Runtime behavior is identical; only the annotations differ.
187
+ if (
188
+ typeof value === "object" &&
189
+ value !== null &&
190
+ !Array.isArray(value) &&
191
+ refSchema.safeParse(value).success
192
+ ) {
193
+ const mapped = map(value as Ref);
194
+ return mapped === null ? { drop: true } : { drop: false, value: mapped };
195
+ }
196
+ let node: z.ZodType = schema;
197
+ while (
198
+ node instanceof z.ZodNullable ||
199
+ node instanceof z.ZodOptional ||
200
+ node instanceof z.ZodDefault
201
+ ) {
202
+ node = node.unwrap() as z.ZodType;
203
+ }
204
+ if (node === refSchema) {
205
+ if (!refSchema.safeParse(value).success) return { drop: false, value };
206
+ const mapped = map(value as Ref);
207
+ return mapped === null ? { drop: true } : { drop: false, value: mapped };
208
+ }
209
+ if (node instanceof z.ZodArray) {
210
+ if (!Array.isArray(value)) return { drop: false, value };
211
+ const out: unknown[] = [];
212
+ for (const item of value) {
213
+ const rewritten = rewriteNode(item, node.element as z.ZodType, map);
214
+ if (!rewritten.drop) out.push(rewritten.value);
215
+ }
216
+ return { drop: false, value: out };
217
+ }
218
+ if (node instanceof z.ZodObject) {
219
+ if (typeof value !== "object" || value === null || Array.isArray(value))
220
+ return { drop: false, value };
221
+ const shape = node.shape as Record<string, z.ZodType>;
222
+ const record = value as Record<string, unknown>;
223
+ const out: Record<string, unknown> = {};
224
+ for (const [key, item] of Object.entries(record)) {
225
+ if (!(key in shape)) {
226
+ const scrubbed = rewriteUnknown(item, map);
227
+ out[key] = scrubbed.drop ? null : scrubbed.value;
228
+ continue;
229
+ }
230
+ const child = shape[key];
231
+ const rewritten = rewriteNode(item, child, map);
232
+ if (rewritten.drop) {
233
+ if (child instanceof z.ZodOptional || child instanceof z.ZodDefault) continue;
234
+ if (child instanceof z.ZodNullable) {
235
+ out[key] = null;
236
+ continue;
237
+ }
238
+ return { drop: true };
239
+ }
240
+ out[key] = rewritten.value;
241
+ }
242
+ return { drop: false, value: out };
243
+ }
244
+ return { drop: false, value };
245
+ };
246
+
247
+ /**
248
+ * Schema-driven reference rewrite over an already-validated record subtree.
249
+ * Every `Ref` anywhere in the value is mapped; unportable references are
250
+ * dropped from arrays, nulled in nullable fields, and propagated upward
251
+ * anywhere else — so a new ref-bearing field can never silently slip
252
+ * through export or import. Unknown (schema-absent) keys are scrubbed the
253
+ * same way. Returns null only when the root itself is an unportable ref.
254
+ */
255
+ export const rewriteRecordRefs = (value: unknown, schema: z.ZodType, map: RefMap): unknown => {
256
+ const rewritten = rewriteNode(value, schema, map);
257
+ return rewritten.drop ? null : rewritten.value;
258
+ };
259
+
260
+ export const provenanceSchema = z
261
+ .object({
262
+ kind: z.enum(["host_observed", "agent_reported", "imported"]),
263
+ host: hostSchema,
264
+ session: refSchema.nullable(),
265
+ workerId: nullableId,
266
+ })
267
+ .strict();
268
+ export type Provenance = z.infer<typeof provenanceSchema>;
269
+ export const entrySchema = <T extends z.ZodType>(data: T) =>
270
+ z.object({ id, recordedAt: utc, provenance: provenanceSchema, data }).strict();
271
+ export type Entry<T> = { id: Id; recordedAt: Utc; provenance: Provenance; data: T };
272
+
273
+ /** Risk tiers an agent judges; aliases (low, medium, …) are mapped on input. */
274
+ export const RISK_TIERS = ["trivial", "normal", "high"] as const;
275
+ export type RiskTier = (typeof RISK_TIERS)[number];
276
+ /**
277
+ * The agent's policy judgment (S17): four flat calls plus an optional note
278
+ * and supporting refs. Everything else is derived deterministically
279
+ * (core/policy/derive.ts).
280
+ */
281
+ export const judgmentSchema = z
282
+ .object({
283
+ riskTier: z.enum(RISK_TIERS),
284
+ behaviorChange: z.boolean(),
285
+ productChoiceOpen: z.boolean(),
286
+ needsPlan: z.boolean(),
287
+ note: text.nullable(),
288
+ refs: z.array(refSchema),
289
+ })
290
+ .strict();
291
+ export type Judgment = z.infer<typeof judgmentSchema>;
292
+ /** A ≤6.x assessment as stored (facts/signals/…): kept verbatim, never written. */
293
+ export const legacyAssessmentSchema = z.looseObject({});
294
+
295
+ export const constraintSchema = z
296
+ .object({
297
+ id: nonEmpty,
298
+ kind: z.enum(["host", "project", "user"]),
299
+ statement: text,
300
+ source: refSchema,
301
+ acceptanceAllowed: z.boolean(),
302
+ requires: z.array(
303
+ z
304
+ .object({
305
+ kind: z.enum(["check", "method", "decision"]),
306
+ scope: scopeSchema,
307
+ refs: z.array(refSchema),
308
+ method: z
309
+ .enum([
310
+ "investigation",
311
+ "challenge",
312
+ "artifacts",
313
+ "testing",
314
+ "verification",
315
+ "review",
316
+ "delegation",
317
+ "continuity",
318
+ "decisions",
319
+ ])
320
+ .nullable(),
321
+ before: z.enum(["dependent_action", "write", "close"]),
322
+ dependentAction: text.nullable(),
323
+ })
324
+ .strict()
325
+ .check((ctx) => {
326
+ const obligation = ctx.value;
327
+ const validMethod =
328
+ (obligation.kind === "method" && obligation.method !== null) ||
329
+ (obligation.kind !== "method" && obligation.method === null);
330
+ if (!validMethod)
331
+ ctx.issues.push({
332
+ code: "custom",
333
+ input: obligation,
334
+ message: "method obligations require a method; checks and decisions do not",
335
+ path: ["method"],
336
+ });
337
+ }),
338
+ ),
339
+ })
340
+ .strict();
341
+ export type Constraint = z.infer<typeof constraintSchema>;
342
+ export const intentSchema = z
343
+ .object({ objective: text, scope: scopeSchema, authorityRefs: z.array(refSchema) })
344
+ .strict();
345
+ export type Intent = z.infer<typeof intentSchema>;
346
+ export const progressSchema = z
347
+ .object({
348
+ summary: text,
349
+ nextAction: text.nullable(),
350
+ blockers: z.array(
351
+ z.object({ reason: text, dependentAction: text, refs: z.array(refSchema) }).strict(),
352
+ ),
353
+ })
354
+ .strict();
355
+ export type Progress = z.infer<typeof progressSchema>;
356
+
357
+ export const dimensionSchema = z.enum([
358
+ "investigation",
359
+ "challenge",
360
+ "artifacts",
361
+ "testing",
362
+ "verification",
363
+ "review",
364
+ "delegation",
365
+ "continuity",
366
+ "decisions",
367
+ ]);
368
+ export type Dimension = z.infer<typeof dimensionSchema>;
369
+
370
+ export const policySchema = z
371
+ .object({
372
+ policyVersion: text.regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/),
373
+ inputDigest: digest,
374
+ requirements: z.array(
375
+ z
376
+ .object({
377
+ id: digest,
378
+ ruleId: nonEmpty,
379
+ dimension: dimensionSchema,
380
+ scope: scopeSchema,
381
+ reason: text,
382
+ satisfaction: text,
383
+ before: z.enum(["dependent_action", "write", "close"]),
384
+ dependentAction: text.nullable(),
385
+ acceptanceAllowed: z.boolean(),
386
+ })
387
+ .strict(),
388
+ ),
389
+ })
390
+ .strict();
391
+ export type Policy = z.infer<typeof policySchema>;
392
+ export const requirementSchema = policySchema.shape.requirements.element;
393
+ export type Requirement = z.infer<typeof requirementSchema>;
394
+ export const policyChangeSchema = z
395
+ .object({
396
+ recordedAt: utc,
397
+ fromInputDigest: nullableDigest,
398
+ toInputDigest: digest,
399
+ added: z.array(digest),
400
+ retired: z.array(digest),
401
+ reason: text,
402
+ })
403
+ .strict();
404
+ export type PolicyChange = z.infer<typeof policyChangeSchema>;
405
+
406
+ export const candidateSchema = z
407
+ .object({
408
+ id: digest,
409
+ scope: scopeSchema,
410
+ completeness: z.enum(["known", "uncertain"]),
411
+ files: z.array(
412
+ z
413
+ .object({
414
+ path: pathValue,
415
+ kind: z.enum(["file", "symlink", "absent"]),
416
+ digest: nullableDigest,
417
+ executable: z.boolean().nullable(),
418
+ })
419
+ .strict()
420
+ .check((ctx) => {
421
+ const file = ctx.value;
422
+ const valid =
423
+ file.kind === "absent"
424
+ ? file.digest === null && file.executable === null
425
+ : file.kind === "file"
426
+ ? file.digest !== null && typeof file.executable === "boolean"
427
+ : file.digest !== null && file.executable === null;
428
+ if (!valid)
429
+ ctx.issues.push({
430
+ code: "custom",
431
+ input: file,
432
+ message: "invalid file metadata",
433
+ path: [],
434
+ });
435
+ }),
436
+ ),
437
+ environment: z
438
+ .array(
439
+ z.object({ name: nonEmpty, value: text.nullable(), refs: z.array(refSchema) }).strict(),
440
+ )
441
+ .check((ctx) => {
442
+ if (new Set(ctx.value.map((value) => value.name)).size !== ctx.value.length)
443
+ ctx.issues.push({
444
+ code: "custom",
445
+ input: ctx.value,
446
+ message: "duplicate environment name",
447
+ path: [],
448
+ });
449
+ }),
450
+ head: text.nullable(),
451
+ })
452
+ .strict()
453
+ .check((ctx) => {
454
+ if (ctx.value.id !== candidateDigest(ctx.value))
455
+ ctx.issues.push({
456
+ code: "custom",
457
+ input: ctx.value,
458
+ message: "candidate identity does not match its content",
459
+ path: ["id"],
460
+ });
461
+ });
462
+ export type Candidate = z.infer<typeof candidateSchema>;
463
+ /**
464
+ * What `workit check` observed (design §2.1 S9, §2.2). Only the CLI's
465
+ * observing path writes it (WorkitCore.observeCheck); the agent-facing
466
+ * `evidence.record` operation rejects it. A task that holds one lists
467
+ * `evidence.*.data.observation` in `critical`, so an older reader fails
468
+ * closed instead of reading the check without it.
469
+ */
470
+ export const checkObservationSchema = z
471
+ .object({
472
+ observer: z.literal("workit_cli"),
473
+ /** The check name (`--name` or `workit check <name>`); null for an unnamed ad-hoc run. */
474
+ name: nonEmpty.nullable(),
475
+ /** The name is configured and argv is exactly its configured command, run from the repo top. */
476
+ configured: z.boolean(),
477
+ argv: z.array(text).min(1),
478
+ shell: z.boolean(),
479
+ /** Working directory relative to the repository top (posix), `.` at the top. */
480
+ cwd: nonEmpty,
481
+ exitCode: safeInteger,
482
+ durationMs: safeInteger.min(0),
483
+ timedOut: z.boolean(),
484
+ head: text.nullable(),
485
+ /** Worktree tree key before the run; evidence is fresh while the tree key is unchanged. */
486
+ tree: text.nullable(),
487
+ dirty: z.boolean().nullable(),
488
+ /** Cheap stat-cached worktree signal before the run (per-turn freshness; see worktreeSignal). */
489
+ signal: text.nullable(),
490
+ /** Tree key after the run; differs from `tree` when the command changed the worktree. */
491
+ treeAfter: text.nullable(),
492
+ /** The command changed the worktree: the evidence is stale (fail safe). */
493
+ modifiedWorktree: z.boolean(),
494
+ base: text.nullable(),
495
+ patchId: text.nullable(),
496
+ /** A minimal environment fingerprint: platform, arch and allowlisted variables. */
497
+ environment: z
498
+ .object({
499
+ platform: nonEmpty,
500
+ arch: nonEmpty,
501
+ vars: z.record(nonEmpty, text.nullable()),
502
+ })
503
+ .strict(),
504
+ logDigest: text.nullable(),
505
+ logRef: text.nullable(),
506
+ logTail: z.array(text).max(40),
507
+ ledgerRowId: text.nullable(),
508
+ /** Set only when a host hook later attests the run (design §2.1 attestation). */
509
+ attestation: z
510
+ .object({ host: nonEmpty, session: text.nullable(), agentId: text.nullable() })
511
+ .strict()
512
+ .nullable(),
513
+ })
514
+ .strict();
515
+ export type CheckObservation = z.infer<typeof checkObservationSchema>;
516
+ /** The record path a task lists in `critical` once it stores a check observation. */
517
+ export const CHECK_OBSERVATION_PATH = "evidence.*.data.observation";
518
+ const evidenceFields = {
519
+ kind: z.enum(["check", "review", "investigation", "artifact"]),
520
+ claim: text,
521
+ requirementIds: z.array(digest),
522
+ beforeCandidateId: nullableDigest.optional(),
523
+ candidateId: nullableDigest.optional(),
524
+ result: z.enum(["passed", "failed", "missing", "skipped"]),
525
+ summary: text,
526
+ refs: z.array(refSchema),
527
+ exitCode: safeInteger.nullable(),
528
+ reviewContext: refSchema.nullable(),
529
+ };
530
+ export const evidenceSchema = z
531
+ .object({ ...evidenceFields, observation: checkObservationSchema.optional() })
532
+ .strict();
533
+ /** Evidence as an agent may submit it: no CLI observation. */
534
+ const reportedEvidenceSchema = z.object(evidenceFields).strict();
535
+ export type Evidence = z.infer<typeof evidenceSchema>;
536
+ export const evidenceEvaluationSchema = z
537
+ .object({
538
+ evidenceId: id,
539
+ status: z.enum(["passed", "failed", "missing", "skipped", "stale"]),
540
+ reason: text,
541
+ })
542
+ .strict();
543
+ export type EvidenceEvaluation = z.infer<typeof evidenceEvaluationSchema>;
544
+ /**
545
+ * A durable decision record (D2, D18): agent-asserted like every other
546
+ * recording. It satisfies `decisions` requirements and accepted limitations;
547
+ * it never authorizes an external effect — autonomy grants and the host's
548
+ * own permission prompt do that. Legacy keys (`approvedContent`, `displayed`,
549
+ * `standing`, `consumption`) are dropped on read (D17).
550
+ */
551
+ export const decisionSchema = z
552
+ .object({
553
+ purpose: z.enum(["design", "action", "limitation", "preference"]),
554
+ binding: z
555
+ .object({
556
+ taskId: id,
557
+ workspaceId: id,
558
+ scope: scopeSchema,
559
+ presented: text,
560
+ contentRefs: z.array(refSchema),
561
+ statedChoice: z.object({ ref: text, text: text }).strict().optional(),
562
+ })
563
+ .strict(),
564
+ digest,
565
+ response: z.enum(["approved", "rejected", "stated"]),
566
+ requirementIds: z.array(digest),
567
+ revoked: z.object({ at: utc, reason: text }).strict().nullable(),
568
+ })
569
+ .strict();
570
+ export type Decision = z.infer<typeof decisionSchema>;
571
+
572
+ export const findingSchema = z
573
+ .object({
574
+ claim: text,
575
+ consequence: text,
576
+ scope: scopeSchema,
577
+ candidateId: nullableDigest,
578
+ refs: z.array(refSchema),
579
+ disposition: z.enum(["open", "fixed", "dismissed", "deferred"]),
580
+ resolution: z
581
+ .object({ reason: text, evidenceIds: z.array(id), decisionIds: z.array(id) })
582
+ .strict()
583
+ .nullable(),
584
+ })
585
+ .strict();
586
+ export type Finding = z.infer<typeof findingSchema>;
587
+ export const assignmentSchema = z
588
+ .object({
589
+ role: z.enum(["investigator", "reviewer", "implementer"]),
590
+ objective: text,
591
+ scope: scopeSchema,
592
+ decisionIds: z.array(id),
593
+ requirementIds: z.array(digest),
594
+ candidateId: nullableDigest.optional(),
595
+ stoppingCondition: text,
596
+ })
597
+ .strict();
598
+ export type Assignment = z.infer<typeof assignmentSchema>;
599
+ export const workerReportSchema = z
600
+ .object({
601
+ outcome: z.enum(["completed", "failed", "cancelled"]),
602
+ summary: text,
603
+ evidenceIds: z.array(id),
604
+ findingIds: z.array(idRef),
605
+ })
606
+ .strict();
607
+ export type WorkerReport = z.infer<typeof workerReportSchema>;
608
+ export const workerSchema = z
609
+ .object({
610
+ assignment: assignmentSchema,
611
+ state: z.enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
612
+ session: refSchema.nullable(),
613
+ coordinator: refSchema.optional(),
614
+ report: workerReportSchema.nullable(),
615
+ })
616
+ .strict();
617
+ export type Worker = z.infer<typeof workerSchema>;
618
+ export const closureSchema = z
619
+ .object({
620
+ outcome: outcomeSchema,
621
+ at: utc,
622
+ summary: text,
623
+ decisionIds: z.array(id),
624
+ evidenceIds: z.array(id),
625
+ })
626
+ .strict();
627
+ export type Closure = z.infer<typeof closureSchema>;
628
+ export const runtimeSchema = z
629
+ .object({
630
+ createdWith: text.nullable(),
631
+ updatedWith: text,
632
+ })
633
+ .strict();
634
+ export type RuntimeInfo = z.infer<typeof runtimeSchema>;
635
+ export const taskRecordSchema = z
636
+ .object({
637
+ schemaVersion: z.literal(1),
638
+ id,
639
+ workspaceId: id,
640
+ revision,
641
+ createdAt: utc,
642
+ updatedAt: utc,
643
+ origin: z.object({ workspaceId: id, taskId: id, exportDigest: digest }).strict().nullable(),
644
+ intent: entrySchema(intentSchema),
645
+ constraints: z.array(constraintSchema),
646
+ status: z.enum(["active", "paused", "closed"]),
647
+ closure: closureSchema.nullable(),
648
+ progress: progressSchema,
649
+ pauseReason: text.nullable().optional(),
650
+ runtime: runtimeSchema.optional(),
651
+ /** ≤6.x assessments, read-only history; judgments replace them. */
652
+ assessments: z.array(entrySchema(legacyAssessmentSchema)),
653
+ /** Listed in `critical` once written, so an older reader fails closed. */
654
+ judgments: z.array(entrySchema(judgmentSchema)).optional(),
655
+ policy: policySchema.nullable(),
656
+ policyChanges: z.array(policyChangeSchema),
657
+ candidates: z.array(candidateSchema),
658
+ evidence: z.array(entrySchema(evidenceSchema)),
659
+ decisions: z.array(entrySchema(decisionSchema)),
660
+ findings: z.array(entrySchema(findingSchema)),
661
+ workers: z.array(entrySchema(workerSchema)),
662
+ /** Paths a reader must understand; see parseStoredRecord. */
663
+ critical: z.array(nonEmpty).optional(),
664
+ })
665
+ .strict();
666
+ export type TaskRecord = z.infer<typeof taskRecordSchema>;
667
+
668
+ type Strip = { path: PropertyKey[]; keys: string[] };
669
+ const stripCount = (strips: Strip[]) => strips.reduce((sum, strip) => sum + strip.keys.length, 0);
670
+ /** Unknown-key issues only, flattened; null when any issue is a real schema
671
+ * violation. For a union, the branch that strips the fewest keys wins, so a
672
+ * key one branch knows is never dropped in favor of a narrower branch. */
673
+ const strippable = (
674
+ issues: readonly z.core.$ZodIssue[],
675
+ prefix: PropertyKey[] = [],
676
+ ): Strip[] | null => {
677
+ const strips: Strip[] = [];
678
+ for (const issue of issues) {
679
+ if (issue.code === "unrecognized_keys") {
680
+ strips.push({ path: [...prefix, ...issue.path], keys: issue.keys });
681
+ continue;
682
+ }
683
+ if (issue.code !== "invalid_union") return null;
684
+ const branch = issue.errors
685
+ .map((errors) => strippable(errors, [...prefix, ...issue.path]))
686
+ .filter((found): found is Strip[] => found !== null && found.length > 0)
687
+ .reduce<Strip[] | null>(
688
+ (best, found) => (best === null || stripCount(found) < stripCount(best) ? found : best),
689
+ null,
690
+ );
691
+ if (!branch) return null;
692
+ strips.push(...branch);
693
+ }
694
+ return strips;
695
+ };
696
+
697
+ /** A dotted record path with array indices as `*`, e.g. `evidence.*.data.observer`. */
698
+ const recordPath = (path: PropertyKey[]): string =>
699
+ path.map((key) => (typeof key === "number" ? "*" : String(key))).join(".");
700
+ const overlaps = (left: string, right: string) =>
701
+ left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
702
+
703
+ export type StoredRecordParse<T> =
704
+ | { success: true; data: T; stripped: string[] }
705
+ | { success: false; error: z.ZodError; critical: string[] };
706
+
707
+ /**
708
+ * Reader tolerance for stored records (D17). A record written by a newer
709
+ * runtime may carry keys this reader does not know; exactly the keys zod
710
+ * reports as unrecognized are dropped and the value is parsed again, and
711
+ * every other violation still fails. Writes keep parsing strictly.
712
+ *
713
+ * The rule for new record fields: a field must be safe for an older reader
714
+ * to ignore (and to lose when that reader rewrites the record), or the writer
715
+ * must list its path in the record's top-level `critical` array. A reader
716
+ * that would strip a critical path fails closed instead (`critical` names the
717
+ * paths), so it neither acts on nor rewrites a record it cannot represent.
718
+ */
719
+ export const parseStoredRecord = <S extends z.ZodType>(
720
+ schema: S,
721
+ value: unknown,
722
+ ): StoredRecordParse<z.output<S>> => {
723
+ let parsed = schema.safeParse(value);
724
+ if (parsed.success) return { success: true, data: parsed.data, stripped: [] };
725
+ const first = { success: false as const, error: parsed.error, critical: [] as string[] };
726
+ const declared =
727
+ typeof value === "object" &&
728
+ value !== null &&
729
+ Array.isArray((value as { critical?: unknown }).critical)
730
+ ? (value as { critical: unknown[] }).critical
731
+ .filter((item): item is string => typeof item === "string")
732
+ // An array index in a declaration means any element, like `*`.
733
+ .map((item) =>
734
+ item
735
+ .split(".")
736
+ .map((key) => (/^\d+$/.test(key) ? "*" : key))
737
+ .join("."),
738
+ )
739
+ : [];
740
+ const stripped: string[] = [];
741
+ let current: unknown = structuredClone(value);
742
+ for (let round = 0; round < 8 && !parsed.success; round++) {
743
+ const strips = strippable(parsed.error.issues);
744
+ if (strips === null || strips.length === 0) return first;
745
+ for (const strip of strips) {
746
+ let target: unknown = current;
747
+ for (const key of strip.path)
748
+ target =
749
+ typeof target === "object" && target !== null
750
+ ? (target as Record<PropertyKey, unknown>)[key]
751
+ : undefined;
752
+ if (typeof target !== "object" || target === null) return first;
753
+ for (const key of strip.keys) {
754
+ stripped.push(recordPath([...strip.path, key]));
755
+ delete (target as Record<string, unknown>)[key];
756
+ }
757
+ }
758
+ parsed = schema.safeParse(current);
759
+ }
760
+ if (!parsed.success) return first;
761
+ const critical = [
762
+ ...new Set(stripped.filter((item) => declared.some((path) => overlaps(item, path)))),
763
+ ];
764
+ if (critical.length > 0) return { ...first, critical };
765
+ return { success: true, data: parsed.data, stripped: [...new Set(stripped)] };
766
+ };
767
+ export const workspaceRecordSchema = z
768
+ .object({
769
+ schemaVersion: z.literal(1),
770
+ id,
771
+ revision,
772
+ root: nonEmpty,
773
+ runtime: runtimeSchema.optional(),
774
+ /** Paths a reader must understand; see parseStoredRecord. */
775
+ critical: z.array(nonEmpty).optional(),
776
+ })
777
+ .strict();
778
+ export type WorkspaceRecord = z.infer<typeof workspaceRecordSchema>;
779
+ export const capabilitySchema = z
780
+ .object({
781
+ name: nonEmpty,
782
+ surface: nonEmpty,
783
+ assurance: assuranceSchema,
784
+ reason: text,
785
+ refs: z.array(refSchema),
786
+ })
787
+ .strict();
788
+ export type Capability = z.infer<typeof capabilitySchema>;
789
+ export const requirementEvaluationSchema = z
790
+ .object({
791
+ requirementId: digest,
792
+ status: z.enum(["satisfied", "unsatisfied", "accepted_limitation", "unavailable"]),
793
+ evidenceIds: z.array(id),
794
+ decisionIds: z.array(id),
795
+ reason: text,
796
+ })
797
+ .strict();
798
+ export type RequirementEvaluation = z.infer<typeof requirementEvaluationSchema>;
799
+ export const taskViewSchema = z
800
+ .object({
801
+ task: taskRecordSchema,
802
+ workspace: workspaceRecordSchema,
803
+ capabilities: z.array(capabilitySchema),
804
+ requirements: z.array(requirementEvaluationSchema),
805
+ evidence: z.array(evidenceEvaluationSchema),
806
+ })
807
+ .strict();
808
+ export type TaskView = z.infer<typeof taskViewSchema>;
809
+ export const taskSummarySchema = z
810
+ .object({
811
+ id,
812
+ revision,
813
+ workspaceRevision: revision,
814
+ createdAt: utc,
815
+ updatedAt: utc,
816
+ runtime: runtimeSchema.nullable(),
817
+ objective: text,
818
+ status: z.enum(["active", "paused", "closed"]),
819
+ closure: closureSchema.nullable(),
820
+ progress: progressSchema,
821
+ policy: policySchema.nullable(),
822
+ requirements: z.array(requirementEvaluationSchema),
823
+ })
824
+ .strict();
825
+ export type TaskSummary = z.infer<typeof taskSummarySchema>;
826
+ /** Bounded `inspect` projection (`view: "summary"`): under 4 KB however long the task history is. */
827
+ export type TaskBrief = {
828
+ id: string;
829
+ revision: string;
830
+ workspaceRevision: string;
831
+ updatedAt: string;
832
+ objective: string;
833
+ status: TaskSummary["status"];
834
+ closure: { outcome: Closure["outcome"]; at: string; summary: string } | null;
835
+ progress: {
836
+ summary: string;
837
+ nextAction: string | null;
838
+ blockers: { reason: string; dependentAction: string }[];
839
+ };
840
+ requirements: {
841
+ total: number;
842
+ unmet: number;
843
+ open: { dimension: Dimension; before: string; status: RequirementEvaluation["status"] }[];
844
+ };
845
+ truncated: boolean;
846
+ };
847
+ export const taskListItemSchema = taskSummarySchema
848
+ .omit({ policy: true, requirements: true })
849
+ .extend({ source: provenanceSchema.pick({ host: true, kind: true }) })
850
+ .strict();
851
+ export type TaskListItem = z.infer<typeof taskListItemSchema>;
852
+ export const exportBundleSchema = z
853
+ .object({
854
+ schemaVersion: z.literal(1),
855
+ exportedAt: utc,
856
+ sourceWorkspaceId: id,
857
+ task: taskRecordSchema,
858
+ digest,
859
+ })
860
+ .strict();
861
+ export type ExportBundle = z.infer<typeof exportBundleSchema>;
862
+
863
+ export const callerSchema = z.object({ host: hostSchema, actor: text }).strict();
864
+ export type Caller = z.infer<typeof callerSchema>;
865
+
866
+ const base = { schemaVersion: z.literal(1) };
867
+ const revisions = {
868
+ expectedRevision: revision.optional(),
869
+ expectedWorkspaceRevision: revision.optional(),
870
+ };
871
+ // Omitted revisions default to the current records inside the engine; explicit
872
+ // values are still CAS-enforced. This keeps single-call flows friction-free
873
+ // without weakening concurrent-write protection.
874
+ const taskId = { taskId: id };
875
+ const operation = <T extends z.ZodRawShape>(shape: T) => z.object({ ...base, ...shape }).strict();
876
+ const taskOperations = {
877
+ start: operation({
878
+ action: z.literal("start"),
879
+ expectedWorkspaceRevision: revision.nullable().optional(),
880
+ intent: intentSchema,
881
+ }),
882
+ list: operation({
883
+ action: z.literal("list"),
884
+ status: z.enum(["open", "closed", "all"]).optional(),
885
+ query: z.string().trim().min(1).max(200).optional(),
886
+ limit: z.number().int().min(1).max(50).optional(),
887
+ }),
888
+ inspect: operation({
889
+ action: z.literal("inspect"),
890
+ ...taskId,
891
+ view: z.enum(["summary", "full"]).optional(),
892
+ }),
893
+ revise: operation({
894
+ action: z.literal("revise"),
895
+ ...taskId,
896
+ ...revisions,
897
+ expectedWorkspaceRevision: revision.optional(),
898
+ intent: intentSchema,
899
+ reason: text,
900
+ }),
901
+ progress: operation({
902
+ action: z.literal("progress"),
903
+ ...taskId,
904
+ expectedRevision: revision.optional(),
905
+ progress: progressSchema,
906
+ }),
907
+ pause: operation({
908
+ action: z.literal("pause"),
909
+ ...taskId,
910
+ ...revisions,
911
+ expectedWorkspaceRevision: revision.optional(),
912
+ reason: text,
913
+ }),
914
+ resume: operation({
915
+ action: z.literal("resume"),
916
+ ...taskId,
917
+ ...revisions,
918
+ expectedWorkspaceRevision: revision.optional(),
919
+ }),
920
+ close: operation({
921
+ action: z.literal("close"),
922
+ ...taskId,
923
+ ...revisions,
924
+ expectedWorkspaceRevision: revision.optional(),
925
+ outcome: outcomeSchema,
926
+ summary: text,
927
+ decisionIds: z.array(id).optional(),
928
+ }),
929
+ };
930
+ /** Judgment values as sent; core/policy/judgment.ts maps aliases and validates. */
931
+ const rawJudgment = z.record(z.string(), z.unknown());
932
+ const policyOperations = {
933
+ assess: operation({
934
+ action: z.literal("assess"),
935
+ ...taskId,
936
+ expectedRevision: revision.optional(),
937
+ judgment: rawJudgment,
938
+ }),
939
+ preview: operation({ action: z.literal("preview"), ...taskId, judgment: rawJudgment }),
940
+ explain: operation({ action: z.literal("explain"), ...taskId }),
941
+ };
942
+ const evidenceOperations = {
943
+ record: operation({
944
+ action: z.literal("record"),
945
+ ...taskId,
946
+ expectedRevision: revision.optional(),
947
+ evidence: reportedEvidenceSchema,
948
+ }),
949
+ };
950
+ const findingOperations = {
951
+ record: operation({
952
+ action: z.literal("record"),
953
+ ...taskId,
954
+ expectedRevision: revision.optional(),
955
+ claim: text,
956
+ consequence: text,
957
+ scope: scopeSchema,
958
+ candidateId: nullableDigest.optional(),
959
+ refs: z.array(refSchema),
960
+ }),
961
+ resolve: operation({
962
+ action: z.literal("resolve"),
963
+ ...taskId,
964
+ expectedRevision: revision.optional(),
965
+ findingId: idRef,
966
+ disposition: z.enum(["open", "fixed", "dismissed", "deferred"]),
967
+ reason: text,
968
+ evidenceIds: z.array(id),
969
+ decisionIds: z.array(id),
970
+ }),
971
+ };
972
+ const decisionOperations = {
973
+ record: operation({
974
+ action: z.literal("record"),
975
+ ...taskId,
976
+ expectedRevision: revision.optional(),
977
+ purpose: decisionSchema.shape.purpose,
978
+ // The engine binds the decision to the task and workspace it records on.
979
+ binding: decisionSchema.shape.binding.extend({
980
+ taskId: id.optional(),
981
+ workspaceId: id.optional(),
982
+ }),
983
+ response: decisionSchema.shape.response,
984
+ requirementIds: z.array(digest),
985
+ }),
986
+ revoke: operation({
987
+ action: z.literal("revoke"),
988
+ ...taskId,
989
+ expectedRevision: revision.optional(),
990
+ decisionId: id,
991
+ reason: text,
992
+ }),
993
+ };
994
+ const workerOperations = {
995
+ assign: operation({
996
+ action: z.literal("assign"),
997
+ ...taskId,
998
+ ...revisions,
999
+ expectedWorkspaceRevision: revision.optional(),
1000
+ assignment: assignmentSchema,
1001
+ }),
1002
+ report: operation({
1003
+ action: z.literal("report"),
1004
+ ...taskId,
1005
+ expectedRevision: revision.optional(),
1006
+ expectedWorkspaceRevision: revision.optional(),
1007
+ workerId: idRef,
1008
+ report: workerReportSchema,
1009
+ }),
1010
+ cancel: operation({
1011
+ action: z.literal("cancel"),
1012
+ ...taskId,
1013
+ ...revisions,
1014
+ expectedWorkspaceRevision: revision.optional(),
1015
+ workerId: idRef,
1016
+ reason: text,
1017
+ }),
1018
+ };
1019
+ const stateOperations = {
1020
+ export: operation({ action: z.literal("export"), ...taskId }),
1021
+ import: operation({
1022
+ action: z.literal("import"),
1023
+ expectedWorkspaceRevision: revision.nullable().optional(),
1024
+ bundle: exportBundleSchema,
1025
+ }),
1026
+ };
1027
+
1028
+ export const operationSchemas = {
1029
+ task: z.discriminatedUnion("action", Object.values(taskOperations) as any),
1030
+ policy: z.discriminatedUnion("action", Object.values(policyOperations) as any),
1031
+ evidence: z.discriminatedUnion("action", Object.values(evidenceOperations) as any),
1032
+ finding: z.discriminatedUnion("action", Object.values(findingOperations) as any),
1033
+ decision: z.discriminatedUnion("action", Object.values(decisionOperations) as any),
1034
+ worker: z.discriminatedUnion("action", Object.values(workerOperations) as any),
1035
+ state: z.discriminatedUnion("action", Object.values(stateOperations) as any),
1036
+ } as const;
1037
+ export type OperationRequest = z.infer<(typeof operationSchemas)[OperationFamily]>;
1038
+ /** Every action of each family, in contract order: the one action table. */
1039
+ export const OPERATION_ACTIONS: Record<OperationFamily, readonly string[]> = {
1040
+ task: Object.keys(taskOperations),
1041
+ policy: Object.keys(policyOperations),
1042
+ evidence: Object.keys(evidenceOperations),
1043
+ finding: Object.keys(findingOperations),
1044
+ decision: Object.keys(decisionOperations),
1045
+ worker: Object.keys(workerOperations),
1046
+ state: Object.keys(stateOperations),
1047
+ };
1048
+
1049
+ /**
1050
+ * Schemas advertised to hosts. Every `taskId` is optional there: an operation
1051
+ * without one applies to the implicit task of the caller's branch (D3); the
1052
+ * engine fills it in before parsing against `operationSchemas`.
1053
+ */
1054
+ const withImplicitTask = (options: Record<string, z.ZodObject>) =>
1055
+ Object.values(options).map((option) =>
1056
+ "taskId" in option.shape ? option.extend({ taskId: id.optional() }) : option,
1057
+ );
1058
+ const advertised = (options: Record<string, z.ZodObject>) =>
1059
+ z.discriminatedUnion("action", withImplicitTask(options) as any);
1060
+ export const advertisedOperationSchemas = {
1061
+ task: advertised(taskOperations),
1062
+ policy: advertised(policyOperations),
1063
+ evidence: advertised(evidenceOperations),
1064
+ finding: advertised(findingOperations),
1065
+ decision: advertised(decisionOperations),
1066
+ worker: advertised(workerOperations),
1067
+ state: advertised(stateOperations),
1068
+ } as const;
1069
+ export type TaskStartRequest = z.infer<typeof taskOperations.start>;
1070
+
1071
+ export type Id = string;
1072
+ export type Revision = string;
1073
+ export type Digest = string;
1074
+ export type Utc = string;
1075
+
1076
+ export type ErrorCode =
1077
+ | "invalid_input"
1078
+ | "unsupported_version"
1079
+ | "not_found"
1080
+ | "invalid_transition"
1081
+ | "revision_conflict"
1082
+ | "needs_input"
1083
+ | "permission_denied"
1084
+ | "capability_unavailable"
1085
+ | "requirements_unsatisfied"
1086
+ /** Retryable: another live Workit call holds the checkout's metadata lock. */
1087
+ | "busy"
1088
+ | "recovery_required"
1089
+ | "storage_error"
1090
+ | "external_outcome_unknown";
1091
+ export type ErrorDetails = {
1092
+ fields?: { path: string; reason: string }[];
1093
+ taskId?: Id;
1094
+ expectedRevision?: Revision;
1095
+ actualRevision?: Revision;
1096
+ expectedWorkspaceRevision?: Revision | null;
1097
+ actualWorkspaceRevision?: Revision | null;
1098
+ requirementIds?: Digest[];
1099
+ capability?: string;
1100
+ path?: string;
1101
+ operation?: string;
1102
+ outcome?: "not_started" | "pending" | "unknown";
1103
+ guidance?: string;
1104
+ /** Structured remedy for unsatisfied requirements: rule, reason, and
1105
+ * satisfaction text instead of opaque requirement hashes alone. */
1106
+ requirements?: {
1107
+ requirementId: string;
1108
+ ruleId: string;
1109
+ reason: string;
1110
+ satisfaction: string;
1111
+ dependentAction: string | null;
1112
+ }[];
1113
+ };
1114
+ export type Result<T> =
1115
+ | {
1116
+ ok: true;
1117
+ schemaVersion: 1;
1118
+ revision: Revision | null;
1119
+ workspaceRevision: Revision | null;
1120
+ data: T;
1121
+ }
1122
+ | { ok: false; schemaVersion: 1; code: ErrorCode; error: string; details: ErrorDetails };
1123
+
1124
+ export const success = <T>(
1125
+ revisionValue: Revision | null,
1126
+ workspaceRevisionValue: Revision | null,
1127
+ data: T,
1128
+ ): Result<T> => ({
1129
+ ok: true,
1130
+ schemaVersion: 1,
1131
+ revision: revisionValue,
1132
+ workspaceRevision: workspaceRevisionValue,
1133
+ data,
1134
+ });
1135
+ export const failure = (
1136
+ code: ErrorCode,
1137
+ error: string,
1138
+ details: ErrorDetails = {},
1139
+ ): Result<never> => ({ ok: false, schemaVersion: 1, code, error, details });
1140
+
1141
+ /**
1142
+ * Resolve a full id or an unambiguous prefix against the ids a task holds.
1143
+ * An exact match always wins; several prefix matches fail listing the candidates.
1144
+ */
1145
+ export function resolveIdRef(kind: string, ref: string, ids: readonly string[]): Result<string> {
1146
+ if (ids.includes(ref)) return success(null, null, ref);
1147
+ const matches = ids.filter((candidate) => candidate.startsWith(ref));
1148
+ if (matches.length === 1) return success(null, null, matches[0]);
1149
+ if (matches.length === 0) return failure("not_found", `${kind} not found`);
1150
+ return failure(
1151
+ "invalid_input",
1152
+ `${kind} prefix ${ref} is ambiguous; candidates: ${matches.join(", ")}`,
1153
+ );
1154
+ }
1155
+
1156
+ export function parseOperation(family: OperationFamily, input: unknown): Result<OperationRequest> {
1157
+ if (
1158
+ typeof input === "object" &&
1159
+ input !== null &&
1160
+ "schemaVersion" in input &&
1161
+ (input as { schemaVersion?: unknown }).schemaVersion !== SCHEMA_VERSION
1162
+ )
1163
+ return failure("unsupported_version", "unsupported schema version", { operation: family });
1164
+ const parsed = operationSchemas[family].safeParse(input);
1165
+ if (parsed.success) return success(null, null, parsed.data);
1166
+ const fields = parsed.error.issues.map((issue) => ({
1167
+ path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
1168
+ reason: issue.message,
1169
+ }));
1170
+ return failure("invalid_input", "operation input is invalid", { fields });
1171
+ }
1172
+
1173
+ type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
1174
+ function canonical(value: unknown, seen: Set<object>): JsonValue {
1175
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
1176
+ if (typeof value === "string" && invalidUnicode(value)) throw new TypeError("invalid Unicode");
1177
+ return value;
1178
+ }
1179
+ if (typeof value === "number") {
1180
+ if (!Number.isSafeInteger(value)) throw new TypeError("number must be a finite safe integer");
1181
+ return value;
1182
+ }
1183
+ if (typeof value !== "object") throw new TypeError("value is not JSON serializable");
1184
+ if (seen.has(value)) throw new TypeError("cyclic value");
1185
+ seen.add(value);
1186
+ let result: JsonValue;
1187
+ if (Array.isArray(value)) {
1188
+ for (let index = 0; index < value.length; index += 1) {
1189
+ if (!(index in value)) throw new TypeError("sparse array");
1190
+ }
1191
+ result = value.map((item) => canonical(item, seen));
1192
+ } else {
1193
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
1194
+ throw new TypeError("value is not a JSON object");
1195
+ if (Reflect.ownKeys(value).some((key) => typeof key !== "string"))
1196
+ throw new TypeError("symbol key is not JSON");
1197
+ if (Object.keys(value).some((key) => invalidUnicode(key)))
1198
+ throw new TypeError("invalid Unicode");
1199
+ result = Object.fromEntries(
1200
+ Object.keys(value)
1201
+ .toSorted()
1202
+ .map((key) => [key, canonical((value as Record<string, unknown>)[key], seen)]),
1203
+ );
1204
+ }
1205
+ seen.delete(value);
1206
+ return result;
1207
+ }
1208
+ export function canonicalJson(value: unknown): string {
1209
+ return JSON.stringify(canonical(value, new Set()));
1210
+ }
1211
+ export function sha256(value: unknown): Digest {
1212
+ if (typeof value === "string" && invalidUnicode(value)) throw new TypeError("invalid Unicode");
1213
+ return createHash("sha256")
1214
+ .update(typeof value === "string" ? value : canonicalJson(value), "utf8")
1215
+ .digest("hex");
1216
+ }
1217
+ export function newId(): Id {
1218
+ return randomUUID().toLowerCase();
1219
+ }
1220
+ export function newRevision(): Revision {
1221
+ return randomUUID().toLowerCase();
1222
+ }
1223
+ const compareCodeUnits = (left: string, right: string): number => {
1224
+ const length = Math.min(left.length, right.length);
1225
+ for (let index = 0; index < length; index += 1) {
1226
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
1227
+ if (difference !== 0) return difference;
1228
+ }
1229
+ return left.length - right.length;
1230
+ };
1231
+ const compareNullableText = (left: string | null, right: string | null): number =>
1232
+ left === right ? 0 : left === null ? -1 : right === null ? 1 : compareCodeUnits(left, right);
1233
+ const compareBooleanNullable = (left: boolean | null, right: boolean | null): number =>
1234
+ left === right ? 0 : left === null ? -1 : right === null ? 1 : Number(left) - Number(right);
1235
+ export function decisionDigest(input: Omit<Decision, "digest"> | Decision): Digest {
1236
+ const { digest: _ignored, revoked: _revoked, ...value } = input as Decision;
1237
+ return sha256(value);
1238
+ }
1239
+ export function candidateDigest(input: Candidate): Digest {
1240
+ const normalizedScope = {
1241
+ description: input.scope.description,
1242
+ paths: [...input.scope.paths].toSorted(compareCodeUnits),
1243
+ exclusions: [...input.scope.exclusions].toSorted(compareCodeUnits),
1244
+ };
1245
+ return sha256({
1246
+ scope: normalizedScope,
1247
+ completeness: input.completeness,
1248
+ files: [...input.files].toSorted(
1249
+ (left, right) =>
1250
+ compareCodeUnits(left.path, right.path) ||
1251
+ compareCodeUnits(left.kind, right.kind) ||
1252
+ compareNullableText(left.digest, right.digest) ||
1253
+ compareBooleanNullable(left.executable, right.executable),
1254
+ ),
1255
+ environment: input.environment
1256
+ .map(({ name, value }) => ({ name, value }))
1257
+ .toSorted(
1258
+ (left, right) =>
1259
+ compareCodeUnits(left.name, right.name) || compareNullableText(left.value, right.value),
1260
+ ),
1261
+ head: input.head,
1262
+ });
1263
+ }
1264
+ export function requirementId(input: {
1265
+ ruleId: string;
1266
+ scope: Scope;
1267
+ satisfaction: string;
1268
+ before: string;
1269
+ dependentAction: string | null;
1270
+ }): Digest {
1271
+ return sha256({
1272
+ ...input,
1273
+ scope: {
1274
+ description: input.scope.description,
1275
+ paths: [...input.scope.paths].toSorted(compareCodeUnits),
1276
+ exclusions: [...input.scope.exclusions].toSorted(compareCodeUnits),
1277
+ },
1278
+ });
1279
+ }