@brainervirus/workit-core 0.11.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 -3308
  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 -428
  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,541 @@
1
+ import {
2
+ canonicalJson,
3
+ failure,
4
+ success,
5
+ type Id,
6
+ type Ref,
7
+ type Result,
8
+ type Revision,
9
+ type Decision,
10
+ refSchema,
11
+ sha256,
12
+ } from "./task-contract";
13
+ import { WorkitCore } from "./task-engine";
14
+ import { TaskStore } from "./task-store";
15
+ import * as z from "zod";
16
+
17
+ export type AuthorizedActionInput = {
18
+ core: WorkitCore;
19
+ taskId: Id;
20
+ decisionId: Id;
21
+ actionRef: Ref;
22
+ expectedRevision: Revision;
23
+ expectedWorkspaceRevision: Revision;
24
+ binding?: Decision["binding"];
25
+ reserveObservation: unknown;
26
+ settleObservation: (
27
+ outcome: "succeeded" | "not_started" | "unknown",
28
+ revisions?: { taskRevision: Revision; workspaceRevision: Revision },
29
+ ) => unknown;
30
+ refresh?: () => Pick<AuthorizedActionInput, "expectedRevision" | "expectedWorkspaceRevision">;
31
+ step?: string;
32
+ capability?: string;
33
+ available?: boolean;
34
+ failureOutcome?: "not_started" | "unknown";
35
+ };
36
+
37
+ export type ExternalActionResult<T> = Result<T>;
38
+
39
+ const externalActionSchema = z.discriminatedUnion("operation", [
40
+ z
41
+ .object({
42
+ operation: z.literal("git.branch_setup"),
43
+ payload: z
44
+ .object({
45
+ action: z.enum(["setup", "reapply_stash"]).optional(),
46
+ sdd_dir: z.string().optional(),
47
+ target_branch: z.string().optional(),
48
+ stash: z.enum(["yes", "no"]).optional(),
49
+ })
50
+ .strict(),
51
+ })
52
+ .strict(),
53
+ z
54
+ .object({
55
+ operation: z.literal("git.commit"),
56
+ payload: z.object({ message: z.string().min(1) }).strict(),
57
+ })
58
+ .strict(),
59
+ z
60
+ .object({
61
+ operation: z.literal("git.push"),
62
+ payload: z.object({ branch: z.string().min(1).optional() }).strict(),
63
+ })
64
+ .strict(),
65
+ z
66
+ .object({
67
+ operation: z.literal("hosting.pull_request"),
68
+ payload: z
69
+ .object({
70
+ title: z.string().min(1),
71
+ body: z.string().optional(),
72
+ draft: z.boolean().optional(),
73
+ target_branch: z.string().optional(),
74
+ babysit: z.boolean().optional(),
75
+ })
76
+ .strict(),
77
+ })
78
+ .strict(),
79
+ z
80
+ .object({
81
+ operation: z.literal("youtrack.update"),
82
+ payload: z
83
+ .object({
84
+ issueId: z.string().min(1),
85
+ markdown: z.string().min(1),
86
+ minutes: z.number().positive().optional(),
87
+ })
88
+ .strict(),
89
+ })
90
+ .strict(),
91
+ z
92
+ .object({
93
+ operation: z.literal("youtrack.time"),
94
+ payload: z
95
+ .object({
96
+ issueId: z.string().min(1),
97
+ minutes: z.number().positive(),
98
+ text: z.string().optional(),
99
+ dateMs: z.number().finite().optional(),
100
+ })
101
+ .strict(),
102
+ })
103
+ .strict(),
104
+ z
105
+ .object({
106
+ operation: z.literal("youtrack.meeting"),
107
+ payload: z
108
+ .object({
109
+ issueId: z.string().min(1),
110
+ minutes: z.number().positive(),
111
+ text: z.string().min(1),
112
+ })
113
+ .strict(),
114
+ })
115
+ .strict(),
116
+ z
117
+ .object({
118
+ operation: z.literal("changelog.apply"),
119
+ payload: z
120
+ .object({
121
+ entries: z
122
+ .union([
123
+ z.record(z.string(), z.array(z.string())),
124
+ z.array(z.object({ category: z.string().min(1), text: z.string().min(1) }).strict()),
125
+ ])
126
+ .optional(),
127
+ path: z.string().optional(),
128
+ normalize_only: z.boolean().optional(),
129
+ })
130
+ .strict(),
131
+ })
132
+ .strict(),
133
+ z
134
+ .object({
135
+ operation: z.literal("context.read"),
136
+ payload: z
137
+ .object({
138
+ kind: z.enum([
139
+ "git",
140
+ "pr",
141
+ "youtrack",
142
+ "github_issue",
143
+ "gitlab_issue",
144
+ "changelog",
145
+ "release",
146
+ "affected",
147
+ ]),
148
+ range: z.string().optional(),
149
+ issueId: z.string().optional(),
150
+ issueUrl: z.string().optional(),
151
+ issueRef: z.string().optional(),
152
+ mode: z.string().optional(),
153
+ specPath: z.string().optional(),
154
+ planPath: z.string().optional(),
155
+ })
156
+ .strict(),
157
+ })
158
+ .strict(),
159
+ ]);
160
+ export type ExternalActionRequest = z.infer<typeof externalActionSchema>;
161
+
162
+ export type ExternalActionOperation = ExternalActionRequest["operation"];
163
+
164
+ export const externalActionRequest = (value: unknown): Result<ExternalActionRequest> => {
165
+ const parsed = externalActionSchema.safeParse(value);
166
+ return parsed.success
167
+ ? success(null, null, parsed.data)
168
+ : failure("invalid_input", "external action payload is invalid", {
169
+ fields: parsed.error.issues.map((issue) => ({
170
+ path: issue.path.join("."),
171
+ reason: issue.message,
172
+ })),
173
+ });
174
+ };
175
+ export type ExternalActionRunner = <T>(
176
+ input: Omit<AuthorizedActionInput, "core">,
177
+ effect: (step?: string) => T | Result<T> | Promise<T | Result<T>>,
178
+ ) => Promise<ExternalActionResult<T>>;
179
+ export type ExternalActionEffect = <T>(
180
+ operation: string,
181
+ effect: (step?: string) => T | Result<T> | Promise<T | Result<T>>,
182
+ ) => Promise<ExternalActionResult<T>>;
183
+ export type ExternalActionBinding = (
184
+ operation: string,
185
+ ) => Omit<AuthorizedActionInput, "core"> | ExternalActionResult<never>;
186
+
187
+ /**
188
+ * Host adapters use this exact, canonical string as the native approval
189
+ * binding. It deliberately includes the concrete target/payload so an
190
+ * approved sentence cannot authorize a different effect by substring.
191
+ */
192
+ export const externalActionDescriptor = (operation: string, payload: unknown): string =>
193
+ canonicalJson({
194
+ operation,
195
+ payload,
196
+ ...(payload &&
197
+ typeof payload === "object" &&
198
+ !Array.isArray(payload) &&
199
+ "resolved" in payload &&
200
+ payload.resolved &&
201
+ typeof payload.resolved === "object" &&
202
+ Array.isArray((payload.resolved as { steps?: unknown }).steps)
203
+ ? { steps: (payload.resolved as { steps: string[] }).steps }
204
+ : {}),
205
+ });
206
+
207
+ /** Compact host-facing help for the fixed optional-action surface. */
208
+ export const externalActionHelp =
209
+ "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch?,stash?}; git.commit {message}; git.push {branch?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?}; youtrack.update {issueId,markdown,minutes?}; youtrack.time {issueId,minutes,text?,dateMs?}; youtrack.meeting {issueId,minutes,text}; changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,issueId?,issueUrl?,issueRef?,mode?,specPath?,planPath?}. context.read is read-only and needs no approval; all other operations require native approval (CLI uses a TTY; caller-unattested MCP cannot mutate).";
210
+
211
+ export const externalActionRef = (
212
+ host: "opencode" | "pi" | "workit_cli",
213
+ actor: string,
214
+ descriptor: string,
215
+ ): Ref => ({
216
+ kind: "host",
217
+ host,
218
+ handle: `external:${sha256({ actor, descriptor }).slice(0, 32)}`,
219
+ });
220
+
221
+ export const externalActionState = (
222
+ store: TaskStore,
223
+ host: string,
224
+ actor: string,
225
+ operation: string,
226
+ ) => {
227
+ const listed = store.listTasks();
228
+ const workspace = store.readWorkspace();
229
+ if (!listed.ok || !workspace.ok)
230
+ return failure("storage_error", "external action state is unavailable");
231
+ if (!workspace.data) return failure("not_found", "workspace not found");
232
+ const candidates = listed.data.flatMap((task) => {
233
+ if (
234
+ task.status !== "active" ||
235
+ task.workspaceId !== workspace.data!.id ||
236
+ task.intent.provenance.session?.kind !== "host" ||
237
+ task.intent.provenance.session.host !== host ||
238
+ task.intent.provenance.session.handle !== actor
239
+ )
240
+ return [];
241
+ return task.decisions
242
+ .filter(
243
+ (entry) =>
244
+ entry.data.purpose === "action" &&
245
+ entry.data.response === "approved" &&
246
+ entry.data.revoked === null &&
247
+ entry.data.binding.approvedContent === operation,
248
+ )
249
+ .map((entry) => ({ task, entry }));
250
+ });
251
+ if (candidates.length !== 1)
252
+ return failure(
253
+ "permission_denied",
254
+ candidates.length === 0
255
+ ? "no approved action is bound to this session"
256
+ : "multiple approved actions match this session",
257
+ );
258
+ return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
259
+ };
260
+
261
+ /** Find a prior decision for the same user-requested operation/payload even when its resolved target drifted. */
262
+ export const priorExternalAction = (
263
+ store: TaskStore,
264
+ host: string,
265
+ actor: string,
266
+ operation: string,
267
+ payload: unknown,
268
+ ) => {
269
+ const listed = store.listTasks();
270
+ const workspace = store.readWorkspace();
271
+ if (!listed.ok || !workspace.ok)
272
+ return failure("storage_error", "external action state is unavailable");
273
+ if (!workspace.data) return failure("not_found", "workspace not found");
274
+ const candidates = listed.data.flatMap((task) => {
275
+ if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
276
+ return task.decisions
277
+ .filter((entry) => {
278
+ if (
279
+ entry.data.purpose !== "action" ||
280
+ entry.data.response !== "approved" ||
281
+ entry.data.revoked !== null
282
+ )
283
+ return false;
284
+ try {
285
+ const descriptor = JSON.parse(entry.data.binding.approvedContent) as {
286
+ operation?: unknown;
287
+ payload?: Record<string, unknown>;
288
+ };
289
+ if (
290
+ descriptor.operation !== operation ||
291
+ !descriptor.payload ||
292
+ typeof descriptor.payload !== "object"
293
+ )
294
+ return false;
295
+ const storedPayload = { ...descriptor.payload };
296
+ delete storedPayload.resolved;
297
+ return canonicalJson(storedPayload) === canonicalJson(payload);
298
+ } catch {
299
+ return false;
300
+ }
301
+ })
302
+ .map((entry) => ({ task, entry }));
303
+ });
304
+ if (candidates.length !== 1)
305
+ return failure(
306
+ "permission_denied",
307
+ candidates.length === 0
308
+ ? "no prior external action matches this request"
309
+ : "multiple prior external actions match this request",
310
+ );
311
+ return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
312
+ };
313
+
314
+ export const approvedExternalAction = (
315
+ store: TaskStore,
316
+ host: string,
317
+ actor: string,
318
+ operation: string,
319
+ ) => {
320
+ const selected = externalActionState(store, host, actor, operation);
321
+ if (!selected.ok) return selected;
322
+ if (selected.data.entry.data.consumption !== null)
323
+ return failure(
324
+ selected.data.entry.data.consumption.state === "uncertain"
325
+ ? "external_outcome_unknown"
326
+ : "permission_denied",
327
+ selected.data.entry.data.consumption.state === "uncertain"
328
+ ? "previous external action outcome is unknown"
329
+ : "external action was already settled",
330
+ );
331
+ return selected;
332
+ };
333
+
334
+ export const createAuthorizedExternalActionRunner =
335
+ (core: WorkitCore, bind: ExternalActionBinding): ExternalActionEffect =>
336
+ async <T>(
337
+ operation: string,
338
+ effect: (step?: string) => T | Result<T> | Promise<T | Result<T>>,
339
+ ) => {
340
+ let steps: string[] = [];
341
+ try {
342
+ const parsed = JSON.parse(operation) as { steps?: unknown };
343
+ if (Array.isArray(parsed.steps) && parsed.steps.every((step) => typeof step === "string"))
344
+ steps = parsed.steps;
345
+ } catch {
346
+ /* non-descriptor callers remain one-shot */
347
+ }
348
+ const sequence = steps.length ? steps : [undefined];
349
+ let latest: ExternalActionResult<T> | undefined;
350
+ for (const step of sequence) {
351
+ const binding = bind(operation);
352
+ if ("ok" in binding) return binding as ExternalActionResult<T>;
353
+ const result = await runAuthorizedExternalAction(
354
+ { core, ...(binding as Omit<AuthorizedActionInput, "core">), ...(step ? { step } : {}) },
355
+ () => effect(step),
356
+ );
357
+ latest = result;
358
+ if (!result.ok) return result;
359
+ }
360
+ return latest ?? failure("recovery_required", "external action did not execute");
361
+ };
362
+
363
+ export type NativeExternalActionObservation = {
364
+ actor: string;
365
+ actionRef: Ref;
366
+ outcome: "reserve" | "succeeded" | "not_started" | "unknown";
367
+ callId?: string;
368
+ taskRevision?: Revision;
369
+ workspaceRevision?: Revision;
370
+ };
371
+
372
+ export const nativeExternalActionObservation = (
373
+ actor: string,
374
+ actionRef: Ref,
375
+ outcome: NativeExternalActionObservation["outcome"],
376
+ callId = actionRef.kind === "host" ? actionRef.handle : "external-action",
377
+ revisions?: { taskRevision: Revision; workspaceRevision: Revision },
378
+ ): NativeExternalActionObservation =>
379
+ revisions
380
+ ? { actor, actionRef, outcome, callId, ...revisions }
381
+ : { actor, actionRef, outcome, callId };
382
+
383
+ export const readNativeExternalActionObservation = (
384
+ value: unknown,
385
+ ): NativeExternalActionObservation | null => {
386
+ if (typeof value !== "object" || value === null) return null;
387
+ const candidate = value as Record<string, unknown>;
388
+ if (
389
+ typeof candidate.actor !== "string" ||
390
+ !refSchema.safeParse(candidate.actionRef).success ||
391
+ !["reserve", "succeeded", "not_started", "unknown"].includes(String(candidate.outcome)) ||
392
+ (candidate.callId !== undefined && typeof candidate.callId !== "string") ||
393
+ (candidate.taskRevision !== undefined && typeof candidate.taskRevision !== "string") ||
394
+ (candidate.workspaceRevision !== undefined && typeof candidate.workspaceRevision !== "string")
395
+ )
396
+ return null;
397
+ return {
398
+ actor: candidate.actor,
399
+ actionRef: candidate.actionRef as Ref,
400
+ outcome: candidate.outcome as NativeExternalActionObservation["outcome"],
401
+ ...(candidate.callId === undefined ? {} : { callId: candidate.callId }),
402
+ ...(candidate.taskRevision === undefined
403
+ ? {}
404
+ : { taskRevision: candidate.taskRevision as Revision }),
405
+ ...(candidate.workspaceRevision === undefined
406
+ ? {}
407
+ : { workspaceRevision: candidate.workspaceRevision as Revision }),
408
+ };
409
+ };
410
+
411
+ export const matchesNativeExternalAction = (
412
+ value: unknown,
413
+ expected: {
414
+ actor: string;
415
+ actionRef: Ref;
416
+ outcome: NativeExternalActionObservation["outcome"];
417
+ taskRevision?: Revision;
418
+ workspaceRevision?: Revision;
419
+ },
420
+ ): boolean => {
421
+ const observation = readNativeExternalActionObservation(value);
422
+ return (
423
+ observation !== null &&
424
+ observation.actor === expected.actor &&
425
+ observation.outcome === expected.outcome &&
426
+ (expected.taskRevision === undefined || observation.taskRevision === expected.taskRevision) &&
427
+ (expected.workspaceRevision === undefined ||
428
+ observation.workspaceRevision === expected.workspaceRevision) &&
429
+ canonicalJson(observation.actionRef) === canonicalJson(expected.actionRef)
430
+ );
431
+ };
432
+
433
+ /**
434
+ * Run one concrete, already-authorized external effect through the core's
435
+ * one-time reservation and settlement transitions. The adapter supplies the
436
+ * host-observed action evidence; this helper never turns an arbitrary payload
437
+ * into an external effect and never retries an uncertain outcome.
438
+ */
439
+ export async function runAuthorizedExternalAction<T>(
440
+ input: AuthorizedActionInput,
441
+ effect: () => T | Result<T> | Promise<T | Result<T>>,
442
+ ): Promise<ExternalActionResult<T>> {
443
+ if (input.core.isDelegatedCaller())
444
+ return failure("permission_denied", "delegated workers cannot run external actions");
445
+ if (input.available === false)
446
+ return failure(
447
+ "capability_unavailable",
448
+ `${input.capability ?? "external action"} is unavailable`,
449
+ { capability: input.capability ?? "external_action" },
450
+ );
451
+
452
+ const reserved = input.core.reserveAction({
453
+ taskId: input.taskId,
454
+ decisionId: input.decisionId,
455
+ actionRef: input.actionRef,
456
+ expectedRevision: input.expectedRevision,
457
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
458
+ ...(input.binding ? { binding: input.binding } : {}),
459
+ ...(input.step ? { step: input.step } : {}),
460
+ observation: input.reserveObservation,
461
+ });
462
+ if (!reserved.ok) return reserved as ExternalActionResult<T>;
463
+
464
+ const settle = (
465
+ outcome: "succeeded" | "not_started" | "unknown",
466
+ refreshed = false,
467
+ ): ExternalActionResult<unknown> => {
468
+ try {
469
+ const revisions =
470
+ refreshed && input.refresh
471
+ ? input.refresh()
472
+ : {
473
+ expectedRevision: reserved.data.taskRevision,
474
+ expectedWorkspaceRevision: reserved.data.workspaceRevision,
475
+ };
476
+ return input.core.settleAction({
477
+ ...reserved.data,
478
+ taskRevision: revisions.expectedRevision,
479
+ workspaceRevision: revisions.expectedWorkspaceRevision,
480
+ outcome,
481
+ ...(input.step ? { step: input.step } : {}),
482
+ observation: input.settleObservation(outcome, {
483
+ taskRevision: revisions.expectedRevision,
484
+ workspaceRevision: revisions.expectedWorkspaceRevision,
485
+ }),
486
+ });
487
+ } catch {
488
+ return failure("external_outcome_unknown", "external action settlement is unavailable", {
489
+ operation: "external_action",
490
+ outcome: "unknown",
491
+ });
492
+ }
493
+ };
494
+ const settleWithRefresh = (
495
+ outcome: "succeeded" | "not_started" | "unknown",
496
+ first: ExternalActionResult<unknown>,
497
+ ): ExternalActionResult<unknown> =>
498
+ !first.ok && first.code === "revision_conflict" && input.refresh
499
+ ? settle(outcome, true)
500
+ : first;
501
+
502
+ let effectValue: T | Result<T>;
503
+ try {
504
+ effectValue = await effect();
505
+ } catch {
506
+ const settled = settleWithRefresh("unknown", settle("unknown"));
507
+ if (!settled.ok) return settled as ExternalActionResult<T>;
508
+ return failure("external_outcome_unknown", "external action outcome is unknown", {
509
+ operation: "external_action",
510
+ outcome: "unknown",
511
+ });
512
+ }
513
+ if (
514
+ typeof effectValue === "object" &&
515
+ effectValue !== null &&
516
+ "ok" in effectValue &&
517
+ typeof (effectValue as { ok?: unknown }).ok === "boolean"
518
+ ) {
519
+ const contract = effectValue as Result<T>;
520
+ if (!contract.ok) {
521
+ const preflight =
522
+ typeof contract.details === "object" &&
523
+ contract.details !== null &&
524
+ (contract.details as Record<string, unknown>).outcome === "not_started";
525
+ const outcome = input.failureOutcome ?? (preflight ? "not_started" : "unknown");
526
+ const settled = settleWithRefresh(outcome, settle(outcome));
527
+ if (!settled.ok && outcome === "unknown") return settled as ExternalActionResult<T>;
528
+ return contract;
529
+ }
530
+ effectValue = contract.data;
531
+ }
532
+ const settled = settleWithRefresh("succeeded", settle("succeeded"));
533
+ if (!settled.ok) return settled as ExternalActionResult<T>;
534
+ return {
535
+ ok: true,
536
+ schemaVersion: 1,
537
+ revision: settled.revision,
538
+ workspaceRevision: settled.workspaceRevision,
539
+ data: effectValue as T,
540
+ };
541
+ }