@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.
- package/README.md +5 -3
- package/package.json +8 -3
- package/scripts/analyze-release-scope.ts +4 -1
- package/scripts/doctor-check.ts +3 -1
- package/scripts/install-codex-plugin.sh +28 -0
- package/scripts/install-cursor-plugin.sh +1 -0
- package/scripts/install-opencode-plugin.sh +1 -0
- package/scripts/install-pi-package.sh +56 -0
- package/scripts/rewrite-workspace-deps.ts +22 -5
- package/scripts/sync-release-manifests.ts +8 -5
- package/scripts/sync-runtime.sh +7 -3
- package/scripts/validate-cursor-marketplace.ts +16 -62
- package/skills/workit-babysit/SKILL.md +33 -0
- package/skills/workit-behavioral-tdd/SKILL.md +53 -0
- package/skills/workit-blast-radius/SKILL.md +31 -0
- package/skills/workit-challenge/SKILL.md +62 -0
- package/skills/workit-debug/SKILL.md +61 -0
- package/skills/workit-deslop/SKILL.md +36 -0
- package/skills/workit-diagram/SKILL.md +32 -0
- package/skills/workit-green-run/SKILL.md +29 -0
- package/skills/workit-handoff/SKILL.md +43 -0
- package/skills/workit-implement/SKILL.md +46 -0
- package/skills/workit-mockup/SKILL.md +28 -0
- package/skills/workit-plan/SKILL.md +66 -0
- package/skills/workit-review/SKILL.md +60 -0
- package/skills/workit-steer/SKILL.md +32 -0
- package/src/core/authority.ts +918 -0
- package/src/core/boundary.ts +29 -0
- package/src/core/branch.ts +26 -255
- package/src/core/changelog.ts +95 -18
- package/src/core/commit-flavors.ts +65 -0
- package/src/core/config-conversion.ts +183 -0
- package/src/core/config.ts +42 -0
- package/src/core/cutover.ts +721 -0
- package/src/core/detect-hosts.ts +107 -0
- package/src/core/doctor.ts +428 -18
- package/src/core/external-action-effects.ts +1171 -0
- package/src/core/external-action.ts +541 -0
- package/src/core/init.ts +83 -32
- package/src/core/methods.ts +125 -0
- package/src/core/policy-resolver.ts +523 -0
- package/src/core/pr-create.ts +7 -1
- package/src/core/registration.ts +69 -4
- package/src/core/repo-context.ts +59 -1
- package/src/core/setup.ts +28 -0
- package/src/core/skill-manifests.ts +40 -42
- package/src/core/support-matrix.ts +8 -3
- package/src/core/sync-runtime.ts +15 -3
- package/src/core/task-context.ts +179 -0
- package/src/core/task-contract.ts +1136 -0
- package/src/core/task-engine.ts +2562 -0
- package/src/core/task-evaluation.ts +679 -0
- package/src/core/task-store.ts +1071 -0
- package/src/core/tracker-issues.ts +237 -0
- package/src/core/uninstall.ts +67 -3
- package/src/core/vcs-config.ts +53 -22
- package/src/core/workers.ts +166 -0
- package/src/core/workspaces.ts +3 -1
- package/src/core/youtrack-tools.ts +2 -0
- package/src/core/youtrack.ts +145 -10
- package/src/core.ts +126 -0
- package/templates/execution-contract.md +17 -50
- package/templates/plan-template.md +3 -3
- package/templates/spec-template.md +15 -3
- package/templates/workit-contract.md +12 -0
- package/commands/wk-changelog.md +0 -2
- package/commands/wk-commit.md +0 -2
- package/commands/wk-docs-refresh.md +0 -2
- package/commands/wk-handoff.md +0 -2
- package/commands/wk-implement.md +0 -2
- package/commands/wk-init.md +0 -2
- package/commands/wk-issue-update.md +0 -2
- package/commands/wk-meetings.md +0 -2
- package/commands/wk-pr.md +0 -2
- package/commands/wk-release-notes.md +0 -2
- package/commands/wk-status.md +0 -2
- package/commands/wk-verify.md +0 -2
- package/scripts/update-superpowers.sh +0 -82
- package/scripts/vendor-assets.ts +0 -37
- package/skills/wk-changelog/SKILL.md +0 -15
- package/skills/wk-commit/SKILL.md +0 -16
- package/skills/wk-docs-refresh/SKILL.md +0 -15
- package/skills/wk-handoff/SKILL.md +0 -20
- package/skills/wk-implement/SKILL.md +0 -47
- package/skills/wk-init/SKILL.md +0 -31
- package/skills/wk-issue-update/SKILL.md +0 -27
- package/skills/wk-issue-update/references/youtrack-update-style.md +0 -81
- package/skills/wk-meetings/SKILL.md +0 -17
- package/skills/wk-pr/SKILL.md +0 -27
- package/skills/wk-release-notes/SKILL.md +0 -15
- package/skills/wk-status/SKILL.md +0 -16
- package/skills/wk-verify/SKILL.md +0 -16
- package/src/core/detector.ts +0 -239
- package/src/core/flow-state.ts +0 -3048
- package/src/core/handoff-context.ts +0 -136
- package/src/core/handoff-tools.ts +0 -133
- package/src/core/menu.ts +0 -70
- package/src/core/plan-tasks.ts +0 -43
- package/src/core/reminder.ts +0 -124
- package/src/core/sdd.ts +0 -353
- package/src/state.ts +0 -22
- package/templates/superpowers-doc-contract.md +0 -75
- package/vendor/superpowers/skills/brainstorming/SKILL.md +0 -159
- package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +0 -213
- package/vendor/superpowers/skills/brainstorming/scripts/helper.js +0 -167
- package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +0 -723
- package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +0 -209
- package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +0 -120
- package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/vendor/superpowers/skills/brainstorming/visual-companion.md +0 -291
- package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +0 -185
- package/vendor/superpowers/skills/executing-plans/SKILL.md +0 -70
- package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +0 -241
- package/vendor/superpowers/skills/receiving-code-review/SKILL.md +0 -213
- package/vendor/superpowers/skills/requesting-code-review/SKILL.md +0 -103
- package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +0 -172
- package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +0 -426
- package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +0 -139
- package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +0 -44
- package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +0 -22
- package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +0 -40
- package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +0 -188
- package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +0 -119
- package/vendor/superpowers/skills/systematic-debugging/SKILL.md +0 -296
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +0 -115
- package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +0 -122
- package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +0 -63
- package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +0 -169
- package/vendor/superpowers/skills/systematic-debugging/test-academic.md +0 -14
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +0 -58
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +0 -68
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +0 -69
- package/vendor/superpowers/skills/test-driven-development/SKILL.md +0 -371
- package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +0 -202
- package/vendor/superpowers/skills/using-superpowers/SKILL.md +0 -62
- package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +0 -23
- package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +0 -39
- package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +0 -16
- package/vendor/superpowers/skills/verification-before-completion/SKILL.md +0 -139
- package/vendor/superpowers/skills/writing-plans/SKILL.md +0 -174
- package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
- package/vendor/superpowers/skills/writing-skills/SKILL.md +0 -689
- package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +0 -1150
- package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
- package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +0 -172
- package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +0 -187
- package/vendor/superpowers/skills/writing-skills/render-graphs.js +0 -168
- 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
|
+
}
|