@brainervirus/workit-core 0.10.0 → 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 (150) hide show
  1. package/README.md +5 -3
  2. package/package.json +8 -3
  3. package/scripts/analyze-release-scope.ts +4 -1
  4. package/scripts/doctor-check.ts +3 -1
  5. package/scripts/install-codex-plugin.sh +28 -0
  6. package/scripts/install-cursor-plugin.sh +1 -0
  7. package/scripts/install-opencode-plugin.sh +1 -0
  8. package/scripts/install-pi-package.sh +56 -0
  9. package/scripts/rewrite-workspace-deps.ts +22 -5
  10. package/scripts/sync-release-manifests.ts +8 -5
  11. package/scripts/sync-runtime.sh +7 -3
  12. package/scripts/validate-cursor-marketplace.ts +16 -62
  13. package/skills/workit-babysit/SKILL.md +33 -0
  14. package/skills/workit-behavioral-tdd/SKILL.md +53 -0
  15. package/skills/workit-blast-radius/SKILL.md +31 -0
  16. package/skills/workit-challenge/SKILL.md +62 -0
  17. package/skills/workit-debug/SKILL.md +61 -0
  18. package/skills/workit-deslop/SKILL.md +36 -0
  19. package/skills/workit-diagram/SKILL.md +32 -0
  20. package/skills/workit-green-run/SKILL.md +29 -0
  21. package/skills/workit-handoff/SKILL.md +43 -0
  22. package/skills/workit-implement/SKILL.md +46 -0
  23. package/skills/workit-mockup/SKILL.md +28 -0
  24. package/skills/workit-plan/SKILL.md +66 -0
  25. package/skills/workit-review/SKILL.md +60 -0
  26. package/skills/workit-steer/SKILL.md +32 -0
  27. package/src/core/authority.ts +918 -0
  28. package/src/core/boundary.ts +29 -0
  29. package/src/core/branch.ts +26 -255
  30. package/src/core/changelog.ts +95 -18
  31. package/src/core/commit-flavors.ts +65 -0
  32. package/src/core/config-conversion.ts +183 -0
  33. package/src/core/config.ts +42 -0
  34. package/src/core/cutover.ts +721 -0
  35. package/src/core/detect-hosts.ts +107 -0
  36. package/src/core/doctor.ts +428 -18
  37. package/src/core/external-action-effects.ts +1171 -0
  38. package/src/core/external-action.ts +541 -0
  39. package/src/core/init.ts +83 -32
  40. package/src/core/methods.ts +125 -0
  41. package/src/core/policy-resolver.ts +523 -0
  42. package/src/core/pr-create.ts +7 -1
  43. package/src/core/registration.ts +69 -4
  44. package/src/core/repo-context.ts +59 -1
  45. package/src/core/setup.ts +28 -0
  46. package/src/core/skill-manifests.ts +40 -42
  47. package/src/core/support-matrix.ts +8 -3
  48. package/src/core/sync-runtime.ts +15 -3
  49. package/src/core/task-context.ts +179 -0
  50. package/src/core/task-contract.ts +1136 -0
  51. package/src/core/task-engine.ts +2562 -0
  52. package/src/core/task-evaluation.ts +679 -0
  53. package/src/core/task-store.ts +1071 -0
  54. package/src/core/tracker-issues.ts +237 -0
  55. package/src/core/uninstall.ts +67 -3
  56. package/src/core/vcs-config.ts +53 -22
  57. package/src/core/workers.ts +166 -0
  58. package/src/core/workspaces.ts +3 -1
  59. package/src/core/youtrack-tools.ts +2 -0
  60. package/src/core/youtrack.ts +145 -10
  61. package/src/core.ts +126 -0
  62. package/templates/execution-contract.md +17 -50
  63. package/templates/plan-template.md +3 -3
  64. package/templates/spec-template.md +15 -3
  65. package/templates/workit-contract.md +12 -0
  66. package/commands/wk-changelog.md +0 -2
  67. package/commands/wk-commit.md +0 -2
  68. package/commands/wk-docs-refresh.md +0 -2
  69. package/commands/wk-handoff.md +0 -2
  70. package/commands/wk-implement.md +0 -2
  71. package/commands/wk-init.md +0 -2
  72. package/commands/wk-issue-update.md +0 -2
  73. package/commands/wk-meetings.md +0 -2
  74. package/commands/wk-pr.md +0 -2
  75. package/commands/wk-release-notes.md +0 -2
  76. package/commands/wk-status.md +0 -2
  77. package/commands/wk-verify.md +0 -2
  78. package/scripts/update-superpowers.sh +0 -82
  79. package/scripts/vendor-assets.ts +0 -37
  80. package/skills/wk-changelog/SKILL.md +0 -15
  81. package/skills/wk-commit/SKILL.md +0 -16
  82. package/skills/wk-docs-refresh/SKILL.md +0 -15
  83. package/skills/wk-handoff/SKILL.md +0 -20
  84. package/skills/wk-implement/SKILL.md +0 -47
  85. package/skills/wk-init/SKILL.md +0 -31
  86. package/skills/wk-issue-update/SKILL.md +0 -27
  87. package/skills/wk-issue-update/references/youtrack-update-style.md +0 -81
  88. package/skills/wk-meetings/SKILL.md +0 -17
  89. package/skills/wk-pr/SKILL.md +0 -27
  90. package/skills/wk-release-notes/SKILL.md +0 -15
  91. package/skills/wk-status/SKILL.md +0 -16
  92. package/skills/wk-verify/SKILL.md +0 -16
  93. package/src/core/detector.ts +0 -239
  94. package/src/core/flow-state.ts +0 -3048
  95. package/src/core/handoff-context.ts +0 -136
  96. package/src/core/handoff-tools.ts +0 -133
  97. package/src/core/menu.ts +0 -70
  98. package/src/core/plan-tasks.ts +0 -43
  99. package/src/core/reminder.ts +0 -124
  100. package/src/core/sdd.ts +0 -353
  101. package/src/state.ts +0 -22
  102. package/templates/superpowers-doc-contract.md +0 -75
  103. package/vendor/superpowers/skills/brainstorming/SKILL.md +0 -159
  104. package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +0 -213
  105. package/vendor/superpowers/skills/brainstorming/scripts/helper.js +0 -167
  106. package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +0 -723
  107. package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +0 -209
  108. package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +0 -120
  109. package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
  110. package/vendor/superpowers/skills/brainstorming/visual-companion.md +0 -291
  111. package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +0 -185
  112. package/vendor/superpowers/skills/executing-plans/SKILL.md +0 -70
  113. package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +0 -241
  114. package/vendor/superpowers/skills/receiving-code-review/SKILL.md +0 -213
  115. package/vendor/superpowers/skills/requesting-code-review/SKILL.md +0 -103
  116. package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +0 -172
  117. package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +0 -426
  118. package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +0 -139
  119. package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +0 -44
  120. package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +0 -22
  121. package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +0 -40
  122. package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +0 -188
  123. package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +0 -119
  124. package/vendor/superpowers/skills/systematic-debugging/SKILL.md +0 -296
  125. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
  126. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +0 -115
  127. package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +0 -122
  128. package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +0 -63
  129. package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +0 -169
  130. package/vendor/superpowers/skills/systematic-debugging/test-academic.md +0 -14
  131. package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +0 -58
  132. package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +0 -68
  133. package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +0 -69
  134. package/vendor/superpowers/skills/test-driven-development/SKILL.md +0 -371
  135. package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +0 -299
  136. package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +0 -202
  137. package/vendor/superpowers/skills/using-superpowers/SKILL.md +0 -62
  138. package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +0 -23
  139. package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +0 -39
  140. package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +0 -16
  141. package/vendor/superpowers/skills/verification-before-completion/SKILL.md +0 -139
  142. package/vendor/superpowers/skills/writing-plans/SKILL.md +0 -174
  143. package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
  144. package/vendor/superpowers/skills/writing-skills/SKILL.md +0 -689
  145. package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +0 -1150
  146. package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
  147. package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +0 -172
  148. package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +0 -187
  149. package/vendor/superpowers/skills/writing-skills/render-graphs.js +0 -168
  150. package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +0 -384
@@ -0,0 +1,918 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ canonicalJson,
6
+ decisionDigest,
7
+ failure,
8
+ provenanceSchema,
9
+ refSchema,
10
+ scopeCovers,
11
+ success,
12
+ type Decision,
13
+ type Entry,
14
+ type Caller,
15
+ type Digest,
16
+ type Id,
17
+ type Provenance,
18
+ type Ref,
19
+ type Result,
20
+ type Revision,
21
+ type TaskRecord,
22
+ type Utc,
23
+ } from "./task-contract";
24
+ import { TaskStore } from "./task-store";
25
+
26
+ export type NativeAuthorityContext = {
27
+ now: Utc;
28
+ };
29
+
30
+ export type NativeDecisionVerification = {
31
+ observation: unknown;
32
+ expected: {
33
+ taskId: Id;
34
+ workspaceId: Id;
35
+ purpose: Decision["purpose"];
36
+ response: Decision["response"];
37
+ binding: Decision["binding"];
38
+ bindingBytes: string;
39
+ digest: Digest;
40
+ requirementIds: Digest[];
41
+ };
42
+ caller: Caller;
43
+ };
44
+
45
+ export type NativeActionVerification = {
46
+ observation: unknown;
47
+ expected: {
48
+ taskId: Id;
49
+ workspaceId: Id;
50
+ decisionId: Id;
51
+ actionRef: Ref;
52
+ taskRevision: Revision;
53
+ workspaceRevision: Revision;
54
+ outcome: "reserve" | "succeeded" | "not_started" | "unknown";
55
+ decision: Pick<Decision, "purpose" | "response" | "binding" | "digest" | "requirementIds">;
56
+ };
57
+ caller: Caller;
58
+ };
59
+
60
+ export type NativeReconciliationVerification = {
61
+ observation: unknown;
62
+ expected: {
63
+ taskId: Id;
64
+ workspaceId: Id;
65
+ decisionId: Id;
66
+ actionRef: Ref;
67
+ taskRevision: Revision;
68
+ workspaceRevision: Revision;
69
+ outcome: "succeeded" | "not_started";
70
+ evidenceDigest: Digest;
71
+ step?: string;
72
+ decision: Pick<Decision, "purpose" | "response" | "binding" | "digest" | "requirementIds">;
73
+ };
74
+ caller: Caller;
75
+ };
76
+
77
+ export type NativeAuthorityVerifier = {
78
+ verifyDecision: (input: NativeDecisionVerification) => Result<Provenance>;
79
+ verifyAction: (input: NativeActionVerification) => Result<Provenance>;
80
+ verifyReconciliation?: (input: NativeReconciliationVerification) => Result<Provenance>;
81
+ };
82
+
83
+ export type VerifiedNativeAuthority = object;
84
+
85
+ type AuthorityRecord = {
86
+ kind: "decision" | "action" | "reconciliation";
87
+ provenance: Provenance;
88
+ taskId: Id;
89
+ workspaceId: Id;
90
+ decisionId?: Id;
91
+ purpose?: Decision["purpose"];
92
+ response?: Decision["response"];
93
+ binding?: Decision["binding"];
94
+ bindingBytes?: string;
95
+ digest?: Digest;
96
+ requirementIds?: Digest[];
97
+ actionRef?: Ref;
98
+ taskRevision?: Revision;
99
+ workspaceRevision?: Revision;
100
+ outcome?: "reserve" | "succeeded" | "not_started" | "unknown";
101
+ evidenceDigest?: Digest;
102
+ step?: string;
103
+ owner: object;
104
+ store: TaskStore;
105
+ root: string;
106
+ caller: Caller;
107
+ };
108
+
109
+ type AuthorityBinding = { owner: object; store: TaskStore; root: string };
110
+
111
+ const verifiedAuthorities = new WeakMap<object, AuthorityRecord>();
112
+
113
+ const trustedAuthority = (
114
+ kind: AuthorityRecord["kind"],
115
+ provenance: Provenance,
116
+ expected: Omit<AuthorityRecord, "kind" | "provenance" | "owner" | "store" | "root" | "caller">,
117
+ binding: AuthorityBinding,
118
+ caller: Caller,
119
+ ): VerifiedNativeAuthority => {
120
+ const token = {};
121
+ verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
122
+ return token;
123
+ };
124
+
125
+ export type ReserveActionInput = {
126
+ store: TaskStore;
127
+ taskId: Id;
128
+ decisionId: Id;
129
+ actionRef: Ref;
130
+ expectedRevision: Revision;
131
+ expectedWorkspaceRevision: Revision;
132
+ binding?: Decision["binding"];
133
+ step?: string;
134
+ steps?: string[];
135
+ authority: VerifiedNativeAuthority;
136
+ authorityOwner: object;
137
+ authorityCaller: Caller;
138
+ native?: NativeAuthorityContext;
139
+ };
140
+
141
+ export type ActionReservation = {
142
+ taskId: Id;
143
+ decisionId: Id;
144
+ actionRef: Ref;
145
+ taskRevision: Revision;
146
+ workspaceRevision: Revision;
147
+ completedSteps: string[];
148
+ remainingSteps: string[];
149
+ };
150
+
151
+ export type SettleActionInput = {
152
+ store: TaskStore;
153
+ taskId: Id;
154
+ decisionId: Id;
155
+ actionRef: Ref;
156
+ taskRevision: Revision;
157
+ workspaceRevision: Revision;
158
+ outcome: "succeeded" | "not_started" | "unknown";
159
+ step?: string;
160
+ authority: VerifiedNativeAuthority;
161
+ authorityOwner: object;
162
+ authorityCaller: Caller;
163
+ native?: NativeAuthorityContext;
164
+ };
165
+
166
+ export type ReconcileActionInput = {
167
+ store: TaskStore;
168
+ taskId: Id;
169
+ decisionId: Id;
170
+ actionRef: Ref;
171
+ taskRevision: Revision;
172
+ workspaceRevision: Revision;
173
+ outcome: "succeeded" | "not_started";
174
+ evidenceDigest: Digest;
175
+ step?: string;
176
+ authority: VerifiedNativeAuthority;
177
+ authorityOwner: object;
178
+ authorityCaller: Caller;
179
+ native?: NativeAuthorityContext;
180
+ };
181
+
182
+ type Workflow = { steps: string[]; completed: string[] };
183
+
184
+ const scopeEqual = (
185
+ left: Decision["binding"]["scope"],
186
+ right: Decision["binding"]["scope"],
187
+ ): boolean => canonicalJson(left) === canonicalJson(right);
188
+
189
+ const parseSteps = (content: string): string[] | null => {
190
+ let value: unknown;
191
+ try {
192
+ value = JSON.parse(content) as unknown;
193
+ } catch {
194
+ return [];
195
+ }
196
+ if (typeof value !== "object" || value === null || !("steps" in value)) return [];
197
+ const steps = (value as { steps?: unknown }).steps;
198
+ return Array.isArray(steps) && steps.every((step) => typeof step === "string") ? steps : null;
199
+ };
200
+
201
+ const decisionMatches = (
202
+ entry: Entry<Decision>,
203
+ purpose: Decision["purpose"],
204
+ binding: Decision["binding"],
205
+ ): boolean => {
206
+ const decision = entry.data;
207
+ return (
208
+ binding.taskId === decision.binding.taskId &&
209
+ binding.workspaceId === decision.binding.workspaceId &&
210
+ decision.purpose === purpose &&
211
+ decision.response === "approved" &&
212
+ decision.revoked === null &&
213
+ (purpose !== "action" || decision.consumption === null) &&
214
+ decision.digest === decisionDigest(decision) &&
215
+ scopeEqual(decision.binding.scope, binding.scope) &&
216
+ canonicalJson(decision.binding) === canonicalJson(binding)
217
+ );
218
+ };
219
+
220
+ const sameRef = (left: Ref, right: Ref): boolean => canonicalJson(left) === canonicalJson(right);
221
+
222
+ const validProvenance = (value: unknown, caller: Caller): value is Provenance => {
223
+ if (!provenanceSchema.safeParse(value).success) return false;
224
+ const provenance = value as Provenance;
225
+ return (
226
+ provenance.kind === "host_observed" &&
227
+ provenance.host === caller.host &&
228
+ provenance.session?.kind === "host" &&
229
+ provenance.session.host === caller.host &&
230
+ provenance.session.handle === caller.actor &&
231
+ provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host)
232
+ );
233
+ };
234
+
235
+ export const verifyNativeDecision = (
236
+ verifier: NativeAuthorityVerifier | undefined,
237
+ input: NativeDecisionVerification,
238
+ binding: AuthorityBinding,
239
+ ): Result<VerifiedNativeAuthority> => {
240
+ if (!verifier) return failure("permission_denied", "native authority verifier is unavailable");
241
+ const result = verifier.verifyDecision(input);
242
+ if (!result.ok || !validProvenance(result.data, input.caller))
243
+ return failure("permission_denied", "native decision observation was not attested");
244
+ return success(
245
+ null,
246
+ null,
247
+ trustedAuthority(
248
+ "decision",
249
+ result.data,
250
+ {
251
+ taskId: input.expected.taskId,
252
+ workspaceId: input.expected.workspaceId,
253
+ purpose: input.expected.purpose,
254
+ response: input.expected.response,
255
+ binding: input.expected.binding,
256
+ bindingBytes: input.expected.bindingBytes,
257
+ digest: input.expected.digest,
258
+ requirementIds: [...input.expected.requirementIds],
259
+ },
260
+ binding,
261
+ input.caller,
262
+ ),
263
+ );
264
+ };
265
+
266
+ export const verifyNativeAction = (
267
+ verifier: NativeAuthorityVerifier | undefined,
268
+ input: NativeActionVerification,
269
+ binding: AuthorityBinding,
270
+ ): Result<VerifiedNativeAuthority> => {
271
+ if (!verifier) return failure("permission_denied", "native authority verifier is unavailable");
272
+ const result = verifier.verifyAction(input);
273
+ if (!result.ok || !validProvenance(result.data, input.caller))
274
+ return failure("permission_denied", "native action observation was not attested");
275
+ return success(
276
+ null,
277
+ null,
278
+ trustedAuthority(
279
+ "action",
280
+ result.data,
281
+ {
282
+ taskId: input.expected.taskId,
283
+ workspaceId: input.expected.workspaceId,
284
+ decisionId: input.expected.decisionId,
285
+ actionRef: input.expected.actionRef,
286
+ taskRevision: input.expected.taskRevision,
287
+ workspaceRevision: input.expected.workspaceRevision,
288
+ outcome: input.expected.outcome,
289
+ purpose: input.expected.decision.purpose,
290
+ response: input.expected.decision.response,
291
+ binding: input.expected.decision.binding,
292
+ digest: input.expected.decision.digest,
293
+ requirementIds: [...input.expected.decision.requirementIds],
294
+ },
295
+ binding,
296
+ input.caller,
297
+ ),
298
+ );
299
+ };
300
+
301
+ export const verifyNativeReconciliation = (
302
+ verifier: NativeAuthorityVerifier | undefined,
303
+ input: NativeReconciliationVerification,
304
+ binding: AuthorityBinding,
305
+ ): Result<VerifiedNativeAuthority> => {
306
+ if (!verifier?.verifyReconciliation)
307
+ return failure("permission_denied", "native reconciliation authority is unavailable");
308
+ const result = verifier.verifyReconciliation(input);
309
+ if (!result.ok || !validProvenance(result.data, input.caller))
310
+ return failure("permission_denied", "native reconciliation observation was not attested");
311
+ return success(
312
+ null,
313
+ null,
314
+ trustedAuthority(
315
+ "reconciliation",
316
+ result.data,
317
+ {
318
+ taskId: input.expected.taskId,
319
+ workspaceId: input.expected.workspaceId,
320
+ decisionId: input.expected.decisionId,
321
+ actionRef: input.expected.actionRef,
322
+ taskRevision: input.expected.taskRevision,
323
+ workspaceRevision: input.expected.workspaceRevision,
324
+ outcome: input.expected.outcome,
325
+ evidenceDigest: input.expected.evidenceDigest,
326
+ step: input.expected.step,
327
+ purpose: input.expected.decision.purpose,
328
+ response: input.expected.decision.response,
329
+ binding: input.expected.decision.binding,
330
+ digest: input.expected.decision.digest,
331
+ requirementIds: [...input.expected.decision.requirementIds],
332
+ },
333
+ binding,
334
+ input.caller,
335
+ ),
336
+ );
337
+ };
338
+
339
+ const takeAuthority = (
340
+ authority: VerifiedNativeAuthority,
341
+ binding: AuthorityBinding,
342
+ caller: Caller,
343
+ ): AuthorityRecord | null => {
344
+ const record = verifiedAuthorities.get(authority);
345
+ verifiedAuthorities.delete(authority);
346
+ if (!record) return null;
347
+ if (
348
+ record.owner !== binding.owner ||
349
+ record.store !== binding.store ||
350
+ record.root !== binding.root ||
351
+ record.root !== record.store.root ||
352
+ canonicalJson(record.caller) !== canonicalJson(caller)
353
+ )
354
+ return null;
355
+ return record;
356
+ };
357
+
358
+ export const retireNativeAuthority = (authority: VerifiedNativeAuthority): Provenance | null => {
359
+ const record = verifiedAuthorities.get(authority);
360
+ verifiedAuthorities.delete(authority);
361
+ return record?.provenance ?? null;
362
+ };
363
+
364
+ const authorityMatches = (
365
+ authority: AuthorityRecord,
366
+ expected: {
367
+ taskId: Id;
368
+ workspaceId: Id;
369
+ decisionId: Id;
370
+ actionRef: Ref;
371
+ taskRevision: Revision;
372
+ workspaceRevision: Revision;
373
+ outcome: AuthorityRecord["outcome"];
374
+ },
375
+ ): boolean =>
376
+ authority.kind === "action" &&
377
+ authority.taskId === expected.taskId &&
378
+ authority.workspaceId === expected.workspaceId &&
379
+ authority.decisionId === expected.decisionId &&
380
+ sameRef(authority.actionRef!, expected.actionRef) &&
381
+ authority.taskRevision === expected.taskRevision &&
382
+ authority.workspaceRevision === expected.workspaceRevision &&
383
+ authority.outcome === expected.outcome;
384
+
385
+ const authorityDecisionMatches = (authority: AuthorityRecord, decision: Decision): boolean =>
386
+ authority.purpose === decision.purpose &&
387
+ authority.response === decision.response &&
388
+ authority.digest === decision.digest &&
389
+ canonicalJson(authority.binding) === canonicalJson(decision.binding) &&
390
+ canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds);
391
+
392
+ const provenanceMatches = (left: Provenance, right: Provenance): boolean =>
393
+ left.kind === "host_observed" &&
394
+ right.kind === "host_observed" &&
395
+ left.host === right.host &&
396
+ canonicalJson(left.session) === canonicalJson(right.session);
397
+
398
+ /** Return only still-valid, approved decisions for the exact requested binding. */
399
+ export function applicableDecision(
400
+ task: TaskRecord,
401
+ purpose: Decision["purpose"],
402
+ binding: Decision["binding"],
403
+ checkoutRoot?: string,
404
+ ): Decision[] {
405
+ if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId) return [];
406
+ return task.decisions
407
+ .filter(
408
+ (entry) =>
409
+ entry.provenance.kind !== "imported" &&
410
+ decisionMatches(entry, purpose, binding) &&
411
+ (checkoutRoot
412
+ ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok
413
+ : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file")),
414
+ )
415
+ .map((entry) => entry.data);
416
+ }
417
+
418
+ const digestBytes = (bytes: Buffer) => createHash("sha256").update(bytes).digest("hex");
419
+ export const verifyDecisionContentAtRoot = (
420
+ checkoutRoot: string,
421
+ binding: Decision["binding"],
422
+ ): Result<null> => {
423
+ let root: string;
424
+ try {
425
+ root = fs.realpathSync(checkoutRoot);
426
+ } catch {
427
+ return failure("permission_denied", "checkout root is unavailable");
428
+ }
429
+ for (const reference of binding.contentRefs) {
430
+ if (reference.kind !== "file") continue;
431
+ if (!reference.digest)
432
+ return failure("invalid_input", "document references require a byte digest");
433
+ const target = path.resolve(root, reference.path);
434
+ if (target !== root && !target.startsWith(`${root}${path.sep}`))
435
+ return failure("invalid_input", "document reference escapes checkout");
436
+ try {
437
+ const relative = path.relative(root, target);
438
+ let current = root;
439
+ for (const segment of relative.split(path.sep)) {
440
+ if (!segment) continue;
441
+ current = path.join(current, segment);
442
+ const stat = fs.lstatSync(current);
443
+ const real = fs.realpathSync(current);
444
+ if (real !== root && !real.startsWith(`${root}${path.sep}`))
445
+ return failure("invalid_input", "document reference escapes checkout");
446
+ if (stat.isSymbolicLink())
447
+ return failure("invalid_input", "document reference uses a symlink");
448
+ if (current === target && !stat.isFile())
449
+ return failure("invalid_input", "document reference is not a regular file");
450
+ if (current !== target && !stat.isDirectory())
451
+ return failure("invalid_input", "document reference ancestor is not a directory");
452
+ }
453
+ if (digestBytes(fs.readFileSync(target)) !== reference.digest)
454
+ return failure("permission_denied", "approved document bytes have changed");
455
+ } catch {
456
+ return failure("invalid_input", "approved document is unavailable");
457
+ }
458
+ }
459
+ return success(null, null, null);
460
+ };
461
+
462
+ const verifyContentRefs = (store: TaskStore, binding: Decision["binding"]): Result<null> =>
463
+ verifyDecisionContentAtRoot(store.root, binding);
464
+
465
+ export const storedDecisionApplicable = (
466
+ store: TaskStore,
467
+ task: TaskRecord,
468
+ purpose: Decision["purpose"],
469
+ binding: Decision["binding"],
470
+ ): Entry<Decision>[] =>
471
+ task.decisions.filter(
472
+ (entry) =>
473
+ entry.provenance.kind !== "imported" &&
474
+ decisionMatches(entry, purpose, binding) &&
475
+ verifyDecisionContentAtRoot(store.root, entry.data.binding).ok,
476
+ );
477
+
478
+ const validateAction = (
479
+ store: TaskStore,
480
+ task: TaskRecord,
481
+ workspaceId: Id,
482
+ input: ReserveActionInput,
483
+ authority: AuthorityRecord,
484
+ ): Result<{ entry: Entry<Decision>; workflow: Workflow }> => {
485
+ if (task.status !== "active")
486
+ return failure("invalid_transition", "only active tasks can authorize actions");
487
+ if (!refSchema.safeParse(input.actionRef).success)
488
+ return failure("invalid_input", "action reference is invalid");
489
+ const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
490
+ if (!entry) return failure("not_found", "decision not found");
491
+ const decision = entry.data;
492
+ if (decision.purpose !== "action")
493
+ return failure("permission_denied", "decision is not an action approval");
494
+ if (
495
+ !authorityMatches(authority, {
496
+ taskId: task.id,
497
+ workspaceId,
498
+ decisionId: input.decisionId,
499
+ actionRef: input.actionRef,
500
+ taskRevision: input.expectedRevision,
501
+ workspaceRevision: input.expectedWorkspaceRevision,
502
+ outcome: "reserve",
503
+ })
504
+ )
505
+ return failure("permission_denied", "native reservation authority is not bound");
506
+ if (!authorityDecisionMatches(authority, decision))
507
+ return failure("permission_denied", "native reservation authority does not match decision");
508
+ if (decision.response !== "approved")
509
+ return failure("permission_denied", "decision was rejected");
510
+ if (
511
+ entry.provenance.kind !== "host_observed" ||
512
+ entry.provenance.receipts.length === 0 ||
513
+ !provenanceMatches(entry.provenance, authority.provenance)
514
+ )
515
+ return failure("permission_denied", "action approval lacks native receipt assurance");
516
+ if (decision.revoked) return failure("permission_denied", "decision is revoked");
517
+ if (decision.digest !== decisionDigest(decision))
518
+ return failure("permission_denied", "decision binding is invalid");
519
+ if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
520
+ return failure("permission_denied", "decision task or workspace binding is invalid");
521
+ if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
522
+ return failure("permission_denied", "action binding does not match the approved decision");
523
+ const content = verifyContentRefs(store, decision.binding);
524
+ if (!content.ok) return content as Result<never>;
525
+ const requestedSteps = parseSteps(decision.binding.approvedContent);
526
+ if (requestedSteps === null)
527
+ return failure("invalid_input", "bounded action steps must be a string array");
528
+ if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps))
529
+ return failure("permission_denied", "bounded action steps do not match approved content");
530
+ if (
531
+ requestedSteps.some((step) => !step || typeof step !== "string") ||
532
+ new Set(requestedSteps).size !== requestedSteps.length
533
+ )
534
+ return failure("invalid_input", "bounded action steps must be unique and non-empty");
535
+ const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
536
+ const workflow = stored
537
+ ? { steps: stored.steps, completed: stored.completedSteps }
538
+ : { steps: requestedSteps, completed: [] };
539
+ if (
540
+ workflow.steps.length &&
541
+ requestedSteps.length &&
542
+ canonicalJson(workflow.steps) !== canonicalJson(requestedSteps)
543
+ )
544
+ return failure("permission_denied", "bounded action scope changed");
545
+ return success(null, null, { entry, workflow });
546
+ };
547
+
548
+ export function reserveAction(input: ReserveActionInput): Result<ActionReservation> {
549
+ if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
550
+ return failure("invalid_input", "action reservation preconditions are required");
551
+ const authority = takeAuthority(
552
+ input.authority,
553
+ {
554
+ owner: input.authorityOwner,
555
+ store: input.store,
556
+ root: input.store.root,
557
+ },
558
+ input.authorityCaller,
559
+ );
560
+ if (!authority)
561
+ return failure("permission_denied", "native reservation authority is not verified");
562
+ const changed = input.store.mutateTask(
563
+ input.taskId,
564
+ input.expectedRevision,
565
+ (current, mutation) => {
566
+ const workspace = input.store.readWorkspace();
567
+ if (!workspace.ok) return workspace as Result<never>;
568
+ if (!workspace.data) return failure("not_found", "workspace not found");
569
+ if (workspace.data.revision !== input.expectedWorkspaceRevision)
570
+ return failure("revision_conflict", "workspace revision does not match", {
571
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
572
+ actualWorkspaceRevision: workspace.data.revision,
573
+ });
574
+ const valid = validateAction(input.store, current, workspace.data.id, input, authority);
575
+ if (!valid.ok) return valid as Result<never>;
576
+ const { entry, workflow } = valid.data;
577
+ const existing = entry.data.consumption;
578
+ if (existing?.state === "uncertain")
579
+ return failure("external_outcome_unknown", "previous action outcome is unknown", {
580
+ operation: "reserve_action",
581
+ outcome: "unknown",
582
+ });
583
+ if (existing?.state === "consumed")
584
+ return failure("permission_denied", "action approval is consumed");
585
+ if (existing?.state === "reserved" && workflow.completed.length === 0)
586
+ return failure("permission_denied", "action approval is already reserved");
587
+ const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
588
+ if (
589
+ workflow.steps.length &&
590
+ (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length])
591
+ )
592
+ return failure(
593
+ "permission_denied",
594
+ "action step is outside the approved remaining workflow",
595
+ );
596
+ if (requestedStep && workflow.completed.includes(requestedStep))
597
+ return failure("permission_denied", "action step was already completed");
598
+ const nextDecision = {
599
+ ...entry.data,
600
+ consumption: { state: "reserved" as const, at: mutation.now, actionRef: input.actionRef },
601
+ };
602
+ const actionProgress = workflow.steps.length
603
+ ? [
604
+ ...(current.actionProgress ?? []).filter(
605
+ (progress) => progress.decisionId !== input.decisionId,
606
+ ),
607
+ {
608
+ decisionId: input.decisionId,
609
+ steps: [...workflow.steps],
610
+ completedSteps: [...workflow.completed],
611
+ },
612
+ ]
613
+ : (current.actionProgress ?? []);
614
+ return success(mutation.revision, null, {
615
+ ...current,
616
+ actionProgress,
617
+ decisions: current.decisions.map((candidate) =>
618
+ candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate,
619
+ ),
620
+ });
621
+ },
622
+ input.native?.now,
623
+ );
624
+ if (!changed.ok) return changed as Result<never>;
625
+ const workspace = input.store.readWorkspace();
626
+ if (!workspace.ok || !workspace.data)
627
+ return failure("recovery_required", "workspace disappeared after reservation");
628
+ const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
629
+ if (!entry) return failure("recovery_required", "reserved decision disappeared");
630
+ const workflow = changed.data.actionProgress?.find(
631
+ (progress) => progress.decisionId === input.decisionId,
632
+ ) ?? {
633
+ steps: [],
634
+ completedSteps: [],
635
+ };
636
+ return success(changed.data.revision, workspace.data.revision, {
637
+ taskId: input.taskId,
638
+ decisionId: input.decisionId,
639
+ actionRef: input.actionRef,
640
+ taskRevision: changed.data.revision,
641
+ workspaceRevision: workspace.data.revision,
642
+ completedSteps: [...workflow.completedSteps],
643
+ remainingSteps: workflow.steps.slice(workflow.completedSteps.length),
644
+ });
645
+ }
646
+
647
+ export function settleAction(input: SettleActionInput): Result<Entry<Decision>> {
648
+ if (!input.store || !input.taskRevision || !input.workspaceRevision)
649
+ return failure("invalid_input", "action settlement preconditions are required");
650
+ const authority = takeAuthority(
651
+ input.authority,
652
+ {
653
+ owner: input.authorityOwner,
654
+ store: input.store,
655
+ root: input.store.root,
656
+ },
657
+ input.authorityCaller,
658
+ );
659
+ if (!authority)
660
+ return failure("permission_denied", "native settlement authority is not verified");
661
+ if (!refSchema.safeParse(input.actionRef).success)
662
+ return failure("invalid_input", "action reference is invalid");
663
+ if (
664
+ !authorityMatches(authority, {
665
+ taskId: input.taskId,
666
+ workspaceId: authority.workspaceId,
667
+ decisionId: input.decisionId,
668
+ actionRef: input.actionRef,
669
+ taskRevision: input.taskRevision,
670
+ workspaceRevision: input.workspaceRevision,
671
+ outcome: input.outcome,
672
+ })
673
+ )
674
+ return failure("permission_denied", "native settlement authority is not verified");
675
+ let outcomeResult: Result<Entry<Decision>> | null = null;
676
+ const changed = input.store.mutateTask(
677
+ input.taskId,
678
+ input.taskRevision,
679
+ (current, mutation) => {
680
+ const workspace = input.store.readWorkspace();
681
+ if (!workspace.ok) return workspace as Result<never>;
682
+ if (!workspace.data) return failure("not_found", "workspace not found");
683
+ if (
684
+ !authorityMatches(authority, {
685
+ taskId: input.taskId,
686
+ workspaceId: workspace.data.id,
687
+ decisionId: input.decisionId,
688
+ actionRef: input.actionRef,
689
+ taskRevision: input.taskRevision,
690
+ workspaceRevision: input.workspaceRevision,
691
+ outcome: input.outcome,
692
+ })
693
+ )
694
+ return failure("permission_denied", "native settlement authority does not match context");
695
+ if (workspace.data.revision !== input.workspaceRevision)
696
+ return failure("revision_conflict", "workspace revision does not match", {
697
+ expectedWorkspaceRevision: input.workspaceRevision,
698
+ actualWorkspaceRevision: workspace.data.revision,
699
+ });
700
+ const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
701
+ if (!entry) return failure("not_found", "decision not found");
702
+ if (!authorityDecisionMatches(authority, entry.data))
703
+ return failure("permission_denied", "native settlement authority does not match decision");
704
+ if (!provenanceMatches(entry.provenance, authority.provenance))
705
+ return failure("permission_denied", "native settlement caller does not match approval");
706
+ if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
707
+ return failure("permission_denied", "decision binding is invalid");
708
+ if (entry.data.consumption?.state === "uncertain")
709
+ return failure("external_outcome_unknown", "previous action outcome is unknown", {
710
+ operation: "settle_action",
711
+ outcome: "unknown",
712
+ });
713
+ if (entry.data.consumption?.state !== "reserved")
714
+ return failure("permission_denied", "action is not reserved");
715
+ if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
716
+ return failure("permission_denied", "settlement reference does not match reservation");
717
+ const stored = current.actionProgress?.find(
718
+ (progress) => progress.decisionId === input.decisionId,
719
+ );
720
+ const workflow = stored
721
+ ? { steps: stored.steps, completed: stored.completedSteps }
722
+ : { steps: [], completed: [] };
723
+ if (
724
+ entry.data.binding.approvedContent &&
725
+ !stored &&
726
+ (parseSteps(entry.data.binding.approvedContent) ?? []).length
727
+ )
728
+ return failure("recovery_required", "bounded action progress is missing");
729
+ const currentStep = input.step ?? workflow.steps[workflow.completed.length];
730
+ if (
731
+ workflow.steps.length &&
732
+ (!currentStep || currentStep !== workflow.steps[workflow.completed.length])
733
+ )
734
+ return failure("permission_denied", "settlement step is outside the approved workflow");
735
+ if (input.outcome === "unknown") {
736
+ outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
737
+ operation: "settle_action",
738
+ outcome: "unknown",
739
+ });
740
+ const uncertain = {
741
+ ...entry.data,
742
+ consumption: { ...entry.data.consumption, state: "uncertain" as const, at: mutation.now },
743
+ };
744
+ return success(mutation.revision, null, {
745
+ ...current,
746
+ decisions: current.decisions.map((candidate) =>
747
+ candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate,
748
+ ),
749
+ });
750
+ }
751
+ if (input.outcome === "not_started") {
752
+ const released = { ...entry.data, consumption: null };
753
+ return success(mutation.revision, null, {
754
+ ...current,
755
+ decisions: current.decisions.map((candidate) =>
756
+ candidate.id === input.decisionId ? { ...candidate, data: released } : candidate,
757
+ ),
758
+ });
759
+ }
760
+ if (currentStep) workflow.completed.push(currentStep);
761
+ const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
762
+ const settled = {
763
+ ...entry.data,
764
+ consumption: complete
765
+ ? { ...entry.data.consumption, state: "consumed" as const, at: mutation.now }
766
+ : null,
767
+ };
768
+ const actionProgress = workflow.steps.length
769
+ ? [
770
+ ...(current.actionProgress ?? []).filter(
771
+ (progress) => progress.decisionId !== input.decisionId,
772
+ ),
773
+ {
774
+ decisionId: input.decisionId,
775
+ steps: [...workflow.steps],
776
+ completedSteps: [...workflow.completed],
777
+ },
778
+ ]
779
+ : (current.actionProgress ?? []);
780
+ const next = {
781
+ ...current,
782
+ actionProgress,
783
+ decisions: current.decisions.map((candidate) =>
784
+ candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate,
785
+ ),
786
+ };
787
+ if (!complete) {
788
+ outcomeResult = success(mutation.revision, input.workspaceRevision, {
789
+ ...entry,
790
+ data: settled,
791
+ });
792
+ }
793
+ return success(mutation.revision, null, next);
794
+ },
795
+ input.native?.now,
796
+ );
797
+ if (!changed.ok) return changed as Result<never>;
798
+ const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
799
+ if (!entry) return failure("recovery_required", "settled decision disappeared");
800
+ if (outcomeResult) return outcomeResult;
801
+ return success(changed.data.revision, input.workspaceRevision, entry);
802
+ }
803
+
804
+ /** Apply one opaque, host-attested provider-read outcome to an uncertain action. */
805
+ export function reconcileAction(input: ReconcileActionInput): Result<Entry<Decision>> {
806
+ if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
807
+ return failure("invalid_input", "action reconciliation preconditions are required");
808
+ const authority = takeAuthority(
809
+ input.authority,
810
+ { owner: input.authorityOwner, store: input.store, root: input.store.root },
811
+ input.authorityCaller,
812
+ );
813
+ if (!authority || authority.kind !== "reconciliation")
814
+ return failure("permission_denied", "native reconciliation authority is not verified");
815
+ if (
816
+ authority.taskId !== input.taskId ||
817
+ authority.decisionId !== input.decisionId ||
818
+ !sameRef(authority.actionRef!, input.actionRef) ||
819
+ authority.taskRevision !== input.taskRevision ||
820
+ authority.workspaceRevision !== input.workspaceRevision ||
821
+ authority.outcome !== input.outcome ||
822
+ authority.evidenceDigest !== input.evidenceDigest ||
823
+ authority.step !== input.step
824
+ )
825
+ return failure("permission_denied", "native reconciliation authority is not bound");
826
+ const changed = input.store.mutateTask(
827
+ input.taskId,
828
+ input.taskRevision,
829
+ (current, mutation) => {
830
+ const workspace = input.store.readWorkspace();
831
+ if (!workspace.ok) return workspace as Result<never>;
832
+ if (!workspace.data) return failure("not_found", "workspace not found");
833
+ if (authority.workspaceId !== workspace.data.id)
834
+ return failure("permission_denied", "native reconciliation workspace does not match");
835
+ if (workspace.data.revision !== input.workspaceRevision)
836
+ return failure("revision_conflict", "workspace revision does not match");
837
+ const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
838
+ if (!entry || entry.data.purpose !== "action")
839
+ return failure("not_found", "decision not found");
840
+ if (
841
+ !authorityDecisionMatches(authority, entry.data) ||
842
+ !provenanceMatches(entry.provenance, authority.provenance)
843
+ )
844
+ return failure("permission_denied", "native reconciliation caller does not match approval");
845
+ if (
846
+ entry.data.response !== "approved" ||
847
+ entry.data.revoked !== null ||
848
+ entry.data.digest !== decisionDigest(entry.data) ||
849
+ entry.data.binding.taskId !== current.id ||
850
+ entry.data.binding.workspaceId !== current.workspaceId ||
851
+ entry.data.binding.workspaceId !== workspace.data.id
852
+ )
853
+ return failure("permission_denied", "reconciliation decision binding is invalid");
854
+ if (entry.data.consumption?.state !== "uncertain")
855
+ return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
856
+ if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
857
+ return failure("permission_denied", "reconciliation reference does not match reservation");
858
+ const stored = current.actionProgress?.find(
859
+ (progress) => progress.decisionId === input.decisionId,
860
+ );
861
+ const workflow = stored
862
+ ? { steps: [...stored.steps], completed: [...stored.completedSteps] }
863
+ : { steps: [], completed: [] };
864
+ const currentStep = input.step ?? workflow.steps[workflow.completed.length];
865
+ if (
866
+ workflow.steps.length &&
867
+ (!currentStep || currentStep !== workflow.steps[workflow.completed.length])
868
+ )
869
+ return failure("permission_denied", "reconciliation step is outside the approved workflow");
870
+ if (input.outcome === "not_started") {
871
+ return success(mutation.revision, null, {
872
+ ...current,
873
+ decisions: current.decisions.map((candidate) =>
874
+ candidate.id === input.decisionId
875
+ ? { ...candidate, data: { ...candidate.data, consumption: null } }
876
+ : candidate,
877
+ ),
878
+ });
879
+ }
880
+ if (currentStep) workflow.completed.push(currentStep);
881
+ const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
882
+ const nextDecision = {
883
+ ...entry.data,
884
+ consumption: complete
885
+ ? { ...entry.data.consumption, state: "consumed" as const, at: mutation.now }
886
+ : null,
887
+ };
888
+ const actionProgress = workflow.steps.length
889
+ ? [
890
+ ...(current.actionProgress ?? []).filter(
891
+ (progress) => progress.decisionId !== input.decisionId,
892
+ ),
893
+ {
894
+ decisionId: input.decisionId,
895
+ steps: workflow.steps,
896
+ completedSteps: workflow.completed,
897
+ },
898
+ ]
899
+ : (current.actionProgress ?? []);
900
+ return success(mutation.revision, null, {
901
+ ...current,
902
+ actionProgress,
903
+ decisions: current.decisions.map((candidate) =>
904
+ candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate,
905
+ ),
906
+ });
907
+ },
908
+ input.native?.now,
909
+ );
910
+ if (!changed.ok) return changed as Result<never>;
911
+ const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
912
+ return entry
913
+ ? success(changed.data.revision, input.workspaceRevision, entry)
914
+ : failure("recovery_required", "reconciled decision disappeared");
915
+ }
916
+
917
+ export const bindingCovers = scopeCovers;
918
+ export const verifyDecisionContent = verifyContentRefs;