@brainervirus/workit-core 1.2.1 → 1.3.1
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/package.json +1 -1
- package/skills/workit-babysit/SKILL.md +20 -26
- package/skills/workit-behavioral-tdd/SKILL.md +0 -5
- package/skills/workit-blast-radius/SKILL.md +0 -5
- package/skills/workit-challenge/SKILL.md +30 -47
- package/skills/workit-debug/SKILL.md +5 -27
- package/skills/workit-deslop/SKILL.md +0 -5
- package/skills/workit-diagram/SKILL.md +0 -5
- package/skills/workit-green-run/SKILL.md +0 -5
- package/skills/workit-handoff/SKILL.md +2 -8
- package/skills/workit-implement/SKILL.md +14 -20
- package/skills/workit-mockup/SKILL.md +0 -5
- package/skills/workit-plan/SKILL.md +30 -63
- package/skills/workit-review/SKILL.md +0 -5
- package/skills/workit-steer/SKILL.md +27 -27
- package/src/core/authority.ts +6 -1
- package/src/core/auto-approval.ts +34 -14
- package/src/core/branch.ts +293 -42
- package/src/core/config-conversion.ts +4 -4
- package/src/core/config-guard.ts +1 -7
- package/src/core/config.ts +1 -1
- package/src/core/cutover.ts +1207 -140
- package/src/core/doctor.ts +155 -82
- package/src/core/external-action-effects.ts +886 -172
- package/src/core/external-action.ts +222 -64
- package/src/core/init.ts +25 -115
- package/src/core/legacy-ownership.ts +47 -0
- package/src/core/methods.ts +65 -52
- package/src/core/policy-resolver.ts +13 -5
- package/src/core/ports/pr-create.ts +23 -15
- package/src/core/pr-create.ts +374 -84
- package/src/core/registration.ts +0 -15
- package/src/core/repo-tools.ts +0 -1
- package/src/core/route-intent.ts +34 -82
- package/src/core/safe-write.ts +66 -1
- package/src/core/setup-state.ts +7 -0
- package/src/core/setup.ts +175 -82
- package/src/core/task-contract.ts +66 -6
- package/src/core/task-engine.ts +47 -2
- package/src/core/task-store.ts +159 -1
- package/src/core/tracker-issues.ts +55 -118
- package/src/core/vcs-config.ts +178 -187
- package/src/core/workspaces.ts +554 -46
- package/src/core.ts +3 -2
- package/src/core/ports/vcs-token-create-urls.ts +0 -4
|
@@ -13,10 +13,17 @@ import {
|
|
|
13
13
|
sha256,
|
|
14
14
|
} from "./task-contract";
|
|
15
15
|
import { WorkitCore } from "./task-engine";
|
|
16
|
-
import { TaskStore } from "./task-store";
|
|
17
|
-
import {
|
|
16
|
+
import { sameDirectoryIdentity, TaskStore } from "./task-store";
|
|
17
|
+
import {
|
|
18
|
+
chainStepKey,
|
|
19
|
+
normalizeChainSteps,
|
|
20
|
+
type ActionReservation,
|
|
21
|
+
type ChainStep,
|
|
22
|
+
} from "./authority";
|
|
18
23
|
import * as z from "zod";
|
|
19
24
|
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { realpathSync } from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
20
27
|
|
|
21
28
|
const chainStepSchema = z.union([
|
|
22
29
|
z.string().min(1),
|
|
@@ -45,8 +52,11 @@ export type AuthorizedActionInput = {
|
|
|
45
52
|
};
|
|
46
53
|
|
|
47
54
|
export type ExternalActionResult<T> = Result<T>;
|
|
55
|
+
type WithExternalActionLock = <T>(
|
|
56
|
+
operation: () => Promise<ExternalActionResult<T>>,
|
|
57
|
+
) => Promise<ExternalActionResult<T>>;
|
|
48
58
|
|
|
49
|
-
const externalActionSchema = z.discriminatedUnion("operation", [
|
|
59
|
+
export const externalActionSchema = z.discriminatedUnion("operation", [
|
|
50
60
|
z
|
|
51
61
|
.object({
|
|
52
62
|
operation: z.literal("git.branch_setup"),
|
|
@@ -56,6 +66,7 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
56
66
|
sdd_dir: z.string().optional(),
|
|
57
67
|
target_branch: z.string().optional(),
|
|
58
68
|
stash: z.enum(["yes", "no"]).optional(),
|
|
69
|
+
cwd: z.string().min(1).optional(),
|
|
59
70
|
})
|
|
60
71
|
.strict(),
|
|
61
72
|
})
|
|
@@ -68,6 +79,7 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
68
79
|
message: z.string().min(1).optional(),
|
|
69
80
|
plan_steps: z.array(chainStepSchema).min(1).max(32).optional(),
|
|
70
81
|
plan_branch: z.string().min(1).optional(),
|
|
82
|
+
cwd: z.string().min(1).optional(),
|
|
71
83
|
})
|
|
72
84
|
.strict(),
|
|
73
85
|
})
|
|
@@ -75,7 +87,9 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
75
87
|
z
|
|
76
88
|
.object({
|
|
77
89
|
operation: z.literal("git.push"),
|
|
78
|
-
payload: z
|
|
90
|
+
payload: z
|
|
91
|
+
.object({ branch: z.string().min(1).optional(), cwd: z.string().min(1).optional() })
|
|
92
|
+
.strict(),
|
|
79
93
|
})
|
|
80
94
|
.strict(),
|
|
81
95
|
z
|
|
@@ -88,6 +102,7 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
88
102
|
draft: z.boolean().optional(),
|
|
89
103
|
target_branch: z.string().optional(),
|
|
90
104
|
babysit: z.boolean().optional(),
|
|
105
|
+
cwd: z.string().min(1).optional(),
|
|
91
106
|
})
|
|
92
107
|
.strict(),
|
|
93
108
|
})
|
|
@@ -99,10 +114,17 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
99
114
|
.object({
|
|
100
115
|
target_branch: z.string().min(1).optional(),
|
|
101
116
|
source_branch: z.string().min(1).optional(),
|
|
117
|
+
cwd: z.string().min(1).optional(),
|
|
102
118
|
})
|
|
103
119
|
.strict(),
|
|
104
120
|
})
|
|
105
121
|
.strict(),
|
|
122
|
+
z
|
|
123
|
+
.object({
|
|
124
|
+
operation: z.literal("hosting.delete_branch"),
|
|
125
|
+
payload: z.object({ branch: z.string().min(1), cwd: z.string().min(1).optional() }).strict(),
|
|
126
|
+
})
|
|
127
|
+
.strict(),
|
|
106
128
|
z
|
|
107
129
|
.object({
|
|
108
130
|
operation: z.literal("youtrack.update"),
|
|
@@ -179,6 +201,7 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
179
201
|
mode: z.string().optional(),
|
|
180
202
|
specPath: z.string().optional(),
|
|
181
203
|
planPath: z.string().optional(),
|
|
204
|
+
cwd: z.string().min(1).optional(),
|
|
182
205
|
})
|
|
183
206
|
.strict(),
|
|
184
207
|
})
|
|
@@ -186,6 +209,13 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
186
209
|
]);
|
|
187
210
|
export type ExternalActionRequest = z.infer<typeof externalActionSchema>;
|
|
188
211
|
|
|
212
|
+
/** The complete external-action contract for hosts that register native tools. */
|
|
213
|
+
export const externalActionJsonSchema = (): z.core.JSONSchema.BaseSchema =>
|
|
214
|
+
({
|
|
215
|
+
type: "object",
|
|
216
|
+
...z.toJSONSchema(externalActionSchema, { target: "draft-2020-12" }),
|
|
217
|
+
}) as z.core.JSONSchema.BaseSchema;
|
|
218
|
+
|
|
189
219
|
export type ExternalActionOperation = ExternalActionRequest["operation"];
|
|
190
220
|
|
|
191
221
|
export const externalActionRequest = (value: unknown): Result<ExternalActionRequest> => {
|
|
@@ -205,7 +235,10 @@ export type ExternalActionRunner = <T>(
|
|
|
205
235
|
) => Promise<ExternalActionResult<T>>;
|
|
206
236
|
export type ExternalActionEffect = <T>(
|
|
207
237
|
operation: string,
|
|
208
|
-
effect: (
|
|
238
|
+
effect: (
|
|
239
|
+
step?: string,
|
|
240
|
+
reservation?: ActionReservation,
|
|
241
|
+
) => T | Result<T> | Promise<T | Result<T>>,
|
|
209
242
|
) => Promise<ExternalActionResult<T>>;
|
|
210
243
|
export type ExternalActionBinding = (
|
|
211
244
|
operation: string,
|
|
@@ -250,7 +283,7 @@ export const planReservationLength = (
|
|
|
250
283
|
|
|
251
284
|
/** Compact host-facing help for the fixed optional-action surface. */
|
|
252
285
|
export const externalActionHelp =
|
|
253
|
-
"Fixed actions:
|
|
286
|
+
"Fixed actions: Git/hosting operations accept cwd? for an action-time target repository: git.branch_setup {target_branch,action?,sdd_dir?,stash?,cwd?}; git.commit {message?,plan_steps?,plan_branch?,cwd?}; git.push {branch?,cwd?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?,cwd?} creates only after pre-verifying the approved source SHA and post-verifies the provider PR head before reporting success (the residual non-atomic source-SHA race is accepted); babysit:true opts into PR-ready follow-up and does not authorize merge or release; omission means no babysitting; hosting.merge {target_branch?,source_branch?,cwd?}; hosting.delete_branch {branch,cwd?} deletes only an exact merged PR head. YouTrack: update {issueId,markdown,minutes?}, time {issueId,minutes,text?,dateMs?}, meeting {issueId,minutes,text}. changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,cwd?,issueId?,issueUrl?,issueRef?}. context.read is read-only; all other actions require host-native approval (CLI needs TTY; caller-unattested MCP cannot mutate).";
|
|
254
287
|
|
|
255
288
|
export const externalActionRef = (
|
|
256
289
|
host: "opencode" | "pi" | "workit_cli",
|
|
@@ -351,13 +384,14 @@ export const priorExternalAction = (
|
|
|
351
384
|
/** Parse a plan-commit authorization descriptor (one approved list per plan). */
|
|
352
385
|
export const planCommitDescriptor = (
|
|
353
386
|
content: string,
|
|
354
|
-
): { steps: ChainStep[]; branch: string; snapshotHead: string | null } | null => {
|
|
387
|
+
): { steps: ChainStep[]; branch: string; snapshotHead: string | null; cwd?: string } | null => {
|
|
355
388
|
try {
|
|
356
389
|
const value = JSON.parse(content) as {
|
|
357
390
|
operation?: unknown;
|
|
358
391
|
payload?: {
|
|
359
392
|
plan_steps?: unknown;
|
|
360
393
|
plan_branch?: unknown;
|
|
394
|
+
cwd?: unknown;
|
|
361
395
|
resolved?: { head?: unknown; branch?: unknown };
|
|
362
396
|
};
|
|
363
397
|
};
|
|
@@ -365,8 +399,15 @@ export const planCommitDescriptor = (
|
|
|
365
399
|
const steps = normalizeChainSteps(value.payload.plan_steps);
|
|
366
400
|
const branch = value.payload.plan_branch;
|
|
367
401
|
if (!steps || typeof branch !== "string" || !branch) return null;
|
|
402
|
+
const cwd = value.payload.cwd;
|
|
403
|
+
if (cwd !== undefined && (typeof cwd !== "string" || !path.isAbsolute(cwd))) return null;
|
|
368
404
|
const head = value.payload.resolved?.head;
|
|
369
|
-
return {
|
|
405
|
+
return {
|
|
406
|
+
steps,
|
|
407
|
+
branch,
|
|
408
|
+
snapshotHead: typeof head === "string" ? head : null,
|
|
409
|
+
...(typeof cwd === "string" ? { cwd } : {}),
|
|
410
|
+
};
|
|
370
411
|
} catch {
|
|
371
412
|
return null;
|
|
372
413
|
}
|
|
@@ -377,17 +418,21 @@ export const planCommitDescriptor = (
|
|
|
377
418
|
* HEAD — history only moved forward. Rebase/reset underneath an approved
|
|
378
419
|
* chain invalidates it instead of executing on rewritten history.
|
|
379
420
|
*/
|
|
380
|
-
export const chainLeaseValid = (
|
|
421
|
+
export const chainLeaseValid = (
|
|
422
|
+
store: TaskStore,
|
|
423
|
+
snapshotHead: string | null,
|
|
424
|
+
targetRoot = store.root,
|
|
425
|
+
): boolean => {
|
|
381
426
|
if (!snapshotHead) return true;
|
|
382
427
|
try {
|
|
383
428
|
const head = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
384
|
-
cwd:
|
|
429
|
+
cwd: targetRoot,
|
|
385
430
|
encoding: "utf8",
|
|
386
431
|
stdio: ["pipe", "pipe", "pipe"],
|
|
387
432
|
}).trim();
|
|
388
433
|
if (head === snapshotHead) return true;
|
|
389
434
|
execFileSync("git", ["merge-base", "--is-ancestor", snapshotHead, head], {
|
|
390
|
-
cwd:
|
|
435
|
+
cwd: targetRoot,
|
|
391
436
|
encoding: "utf8",
|
|
392
437
|
stdio: ["pipe", "pipe", "pipe"],
|
|
393
438
|
});
|
|
@@ -420,6 +465,7 @@ export const approvedPlanCommit = (
|
|
|
420
465
|
host: string,
|
|
421
466
|
actor: string,
|
|
422
467
|
message: string,
|
|
468
|
+
targetRoot = store.root,
|
|
423
469
|
): Result<PlanCommitAuthorization> => {
|
|
424
470
|
if (typeof message !== "string" || !message)
|
|
425
471
|
return failure("invalid_input", "commit message is required");
|
|
@@ -442,12 +488,13 @@ export const approvedPlanCommit = (
|
|
|
442
488
|
return [];
|
|
443
489
|
const plan = planCommitDescriptor(decision.binding.approvedContent);
|
|
444
490
|
if (!plan) return [];
|
|
491
|
+
if (!sameDirectoryIdentity(plan.cwd ?? store.root, targetRoot)) return [];
|
|
445
492
|
const completed =
|
|
446
493
|
task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ??
|
|
447
494
|
[];
|
|
448
495
|
const next = plan.steps[completed.length];
|
|
449
496
|
if (!next || next.kind !== "commit" || next.message !== message) return [];
|
|
450
|
-
if (!chainLeaseValid(store, plan.snapshotHead)) return [];
|
|
497
|
+
if (!chainLeaseValid(store, plan.snapshotHead, targetRoot)) return [];
|
|
451
498
|
return [
|
|
452
499
|
{
|
|
453
500
|
task,
|
|
@@ -471,8 +518,8 @@ export const approvedPlanCommit = (
|
|
|
471
518
|
};
|
|
472
519
|
|
|
473
520
|
export type ChainStepQuery =
|
|
474
|
-
| { operation: "git.branch_setup"; target: string }
|
|
475
|
-
| { operation: "hosting.pull_request" };
|
|
521
|
+
| { operation: "git.branch_setup"; target: string; cwd?: string }
|
|
522
|
+
| { operation: "hosting.pull_request"; cwd?: string };
|
|
476
523
|
|
|
477
524
|
/** Match the next branch or PR step of an active chain authorization. */
|
|
478
525
|
export const approvedChainStep = (
|
|
@@ -500,6 +547,8 @@ export const approvedChainStep = (
|
|
|
500
547
|
return [];
|
|
501
548
|
const plan = planCommitDescriptor(decision.binding.approvedContent);
|
|
502
549
|
if (!plan) return [];
|
|
550
|
+
const targetRoot = query.cwd ?? store.root;
|
|
551
|
+
if (!sameDirectoryIdentity(plan.cwd ?? store.root, targetRoot)) return [];
|
|
503
552
|
const completed =
|
|
504
553
|
task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ??
|
|
505
554
|
[];
|
|
@@ -510,7 +559,7 @@ export const approvedChainStep = (
|
|
|
510
559
|
} else if (next.kind !== "pr") {
|
|
511
560
|
return [];
|
|
512
561
|
}
|
|
513
|
-
if (!chainLeaseValid(store, plan.snapshotHead)) return [];
|
|
562
|
+
if (!chainLeaseValid(store, plan.snapshotHead, targetRoot)) return [];
|
|
514
563
|
return [
|
|
515
564
|
{
|
|
516
565
|
task,
|
|
@@ -547,6 +596,17 @@ export const commitMessageFromDescriptor = (operation: string): string | undefin
|
|
|
547
596
|
}
|
|
548
597
|
};
|
|
549
598
|
|
|
599
|
+
const descriptorTargetRoot = (store: TaskStore, operation: string): string | null => {
|
|
600
|
+
try {
|
|
601
|
+
const descriptor = JSON.parse(operation) as { payload?: { cwd?: unknown } };
|
|
602
|
+
const cwd = descriptor.payload?.cwd;
|
|
603
|
+
if (cwd === undefined) return store.root;
|
|
604
|
+
return typeof cwd === "string" && path.isAbsolute(cwd) ? realpathSync(cwd) : null;
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
550
610
|
/** Resolve a chain authorization into the exact binding an adapter needs
|
|
551
611
|
* to run one branch or PR step under the approved chain. Commit steps stay
|
|
552
612
|
* on the plan path; this covers the steps plans could never express. */
|
|
@@ -572,12 +632,18 @@ export const chainStepBinding = (
|
|
|
572
632
|
} catch {
|
|
573
633
|
return null;
|
|
574
634
|
}
|
|
635
|
+
const targetRoot = descriptorTargetRoot(store, operation);
|
|
636
|
+
if (!targetRoot) return null;
|
|
575
637
|
const query: ChainStepQuery | null =
|
|
576
638
|
descriptor.operation === "git.branch_setup" &&
|
|
577
639
|
typeof descriptor.payload?.target_branch === "string"
|
|
578
|
-
? {
|
|
640
|
+
? {
|
|
641
|
+
operation: "git.branch_setup",
|
|
642
|
+
target: descriptor.payload.target_branch as string,
|
|
643
|
+
cwd: targetRoot,
|
|
644
|
+
}
|
|
579
645
|
: descriptor.operation === "hosting.pull_request"
|
|
580
|
-
? { operation: "hosting.pull_request" }
|
|
646
|
+
? { operation: "hosting.pull_request", cwd: targetRoot }
|
|
581
647
|
: null;
|
|
582
648
|
if (!query) return null;
|
|
583
649
|
const chain = approvedChainStep(store, host, actor, query);
|
|
@@ -587,7 +653,7 @@ export const chainStepBinding = (
|
|
|
587
653
|
let branch: string;
|
|
588
654
|
try {
|
|
589
655
|
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
590
|
-
cwd:
|
|
656
|
+
cwd: targetRoot,
|
|
591
657
|
encoding: "utf8",
|
|
592
658
|
stdio: ["pipe", "pipe", "pipe"],
|
|
593
659
|
}).trim();
|
|
@@ -627,12 +693,14 @@ export const planCommitBinding = (
|
|
|
627
693
|
} | null => {
|
|
628
694
|
const message = commitMessageFromDescriptor(operation);
|
|
629
695
|
if (!message) return null;
|
|
630
|
-
const
|
|
696
|
+
const targetRoot = descriptorTargetRoot(store, operation);
|
|
697
|
+
if (!targetRoot) return null;
|
|
698
|
+
const plan = approvedPlanCommit(store, host, actor, message, targetRoot);
|
|
631
699
|
if (!plan.ok) return null;
|
|
632
700
|
let branch: string;
|
|
633
701
|
try {
|
|
634
702
|
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
635
|
-
cwd:
|
|
703
|
+
cwd: targetRoot,
|
|
636
704
|
encoding: "utf8",
|
|
637
705
|
stdio: ["pipe", "pipe", "pipe"],
|
|
638
706
|
}).trim();
|
|
@@ -691,18 +759,26 @@ export const approvedExternalAction = (
|
|
|
691
759
|
return selected;
|
|
692
760
|
};
|
|
693
761
|
|
|
694
|
-
/** Intent identity for
|
|
695
|
-
* effect; head and dirt are execution-time state that re-resolves. */
|
|
762
|
+
/** Intent identity for branch setup includes the exact remote and local base commits. */
|
|
696
763
|
export const branchSetupIntent = (
|
|
697
764
|
descriptor: string,
|
|
698
|
-
): {
|
|
765
|
+
): {
|
|
766
|
+
target: string;
|
|
767
|
+
base: string;
|
|
768
|
+
localBase: string | null;
|
|
769
|
+
remoteBase: string | null;
|
|
770
|
+
targetExists: boolean;
|
|
771
|
+
cwd?: string;
|
|
772
|
+
} | null => {
|
|
699
773
|
try {
|
|
700
774
|
const value = JSON.parse(descriptor) as {
|
|
701
775
|
operation?: unknown;
|
|
702
776
|
payload?: {
|
|
703
777
|
target_branch?: unknown;
|
|
778
|
+
cwd?: unknown;
|
|
704
779
|
resolved?: {
|
|
705
780
|
base_branch?: unknown;
|
|
781
|
+
local_base?: unknown;
|
|
706
782
|
remote_base?: unknown;
|
|
707
783
|
target_exists?: unknown;
|
|
708
784
|
};
|
|
@@ -711,13 +787,20 @@ export const branchSetupIntent = (
|
|
|
711
787
|
if (value.operation !== "git.branch_setup" || !value.payload) return null;
|
|
712
788
|
const { target_branch, resolved } = value.payload;
|
|
713
789
|
if (typeof target_branch !== "string" || !target_branch || !resolved) return null;
|
|
790
|
+
if (
|
|
791
|
+
value.payload.cwd !== undefined &&
|
|
792
|
+
(typeof value.payload.cwd !== "string" || !path.isAbsolute(value.payload.cwd))
|
|
793
|
+
)
|
|
794
|
+
return null;
|
|
714
795
|
if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
|
|
715
796
|
return null;
|
|
716
797
|
return {
|
|
717
798
|
target: target_branch,
|
|
718
799
|
base: resolved.base_branch,
|
|
800
|
+
localBase: typeof resolved.local_base === "string" ? resolved.local_base : null,
|
|
719
801
|
remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
|
|
720
802
|
targetExists: resolved.target_exists,
|
|
803
|
+
...(typeof value.payload.cwd === "string" ? { cwd: value.payload.cwd } : {}),
|
|
721
804
|
};
|
|
722
805
|
} catch {
|
|
723
806
|
return null;
|
|
@@ -755,8 +838,10 @@ export const approvedBranchSetupIntent = (
|
|
|
755
838
|
const intent = branchSetupIntent(entry.data.binding.approvedContent);
|
|
756
839
|
return (
|
|
757
840
|
intent !== null &&
|
|
841
|
+
(intent.cwd ?? store.root) === (current.cwd ?? store.root) &&
|
|
758
842
|
intent.target === current.target &&
|
|
759
843
|
intent.base === current.base &&
|
|
844
|
+
intent.localBase === current.localBase &&
|
|
760
845
|
intent.remoteBase === current.remoteBase &&
|
|
761
846
|
intent.targetExists === current.targetExists
|
|
762
847
|
);
|
|
@@ -820,9 +905,11 @@ export const priorResolvedDrift = (
|
|
|
820
905
|
? "base_branch"
|
|
821
906
|
: before.remoteBase !== after.remoteBase
|
|
822
907
|
? "remote_base"
|
|
823
|
-
: before.
|
|
824
|
-
? "
|
|
825
|
-
:
|
|
908
|
+
: before.localBase !== after.localBase
|
|
909
|
+
? "local_base"
|
|
910
|
+
: before.targetExists !== after.targetExists
|
|
911
|
+
? "target_exists"
|
|
912
|
+
: null;
|
|
826
913
|
if (moved === null) return success(null, null, null);
|
|
827
914
|
return failure(
|
|
828
915
|
"invalid_input",
|
|
@@ -840,10 +927,13 @@ export const priorResolvedDrift = (
|
|
|
840
927
|
};
|
|
841
928
|
|
|
842
929
|
export const createAuthorizedExternalActionRunner =
|
|
843
|
-
(core: WorkitCore, bind: ExternalActionBinding): ExternalActionEffect =>
|
|
930
|
+
(core: WorkitCore, bind: ExternalActionBinding, coordinationRoot: string): ExternalActionEffect =>
|
|
844
931
|
async <T>(
|
|
845
932
|
operation: string,
|
|
846
|
-
effect: (
|
|
933
|
+
effect: (
|
|
934
|
+
step?: string,
|
|
935
|
+
reservation?: ActionReservation,
|
|
936
|
+
) => T | Result<T> | Promise<T | Result<T>>,
|
|
847
937
|
) => {
|
|
848
938
|
let steps: string[] = [];
|
|
849
939
|
try {
|
|
@@ -858,9 +948,21 @@ export const createAuthorizedExternalActionRunner =
|
|
|
858
948
|
for (const step of sequence) {
|
|
859
949
|
const binding = bind(operation);
|
|
860
950
|
if ("ok" in binding) return binding as ExternalActionResult<T>;
|
|
951
|
+
const targetRoot = externalActionTargetRoot(operation, coordinationRoot);
|
|
952
|
+
if (targetRoot === null)
|
|
953
|
+
return failure("capability_unavailable", "external action target could not be locked", {
|
|
954
|
+
outcome: "not_started",
|
|
955
|
+
});
|
|
956
|
+
const withActionLock: WithExternalActionLock | undefined = !sameDirectoryIdentity(
|
|
957
|
+
targetRoot,
|
|
958
|
+
coordinationRoot,
|
|
959
|
+
)
|
|
960
|
+
? (run) => new TaskStore(targetRoot).withExternalActionLock(run, true)
|
|
961
|
+
: undefined;
|
|
861
962
|
const result = await runAuthorizedExternalAction(
|
|
862
963
|
{ core, ...(binding as Omit<AuthorizedActionInput, "core">), ...(step ? { step } : {}) },
|
|
863
|
-
() => effect(step),
|
|
964
|
+
(reservation) => effect(step, reservation),
|
|
965
|
+
withActionLock,
|
|
864
966
|
);
|
|
865
967
|
latest = result;
|
|
866
968
|
if (!result.ok) return result;
|
|
@@ -868,6 +970,36 @@ export const createAuthorizedExternalActionRunner =
|
|
|
868
970
|
return latest ?? failure("recovery_required", "external action did not execute");
|
|
869
971
|
};
|
|
870
972
|
|
|
973
|
+
const externalActionTargetRoot = (operation: string, coordinationRoot: string): string | null => {
|
|
974
|
+
let value: unknown;
|
|
975
|
+
try {
|
|
976
|
+
value = JSON.parse(operation);
|
|
977
|
+
} catch {
|
|
978
|
+
return realpathSync(coordinationRoot);
|
|
979
|
+
}
|
|
980
|
+
if (typeof value !== "object" || value === null) return realpathSync(coordinationRoot);
|
|
981
|
+
const descriptor = value as { operation?: unknown; payload?: unknown };
|
|
982
|
+
if (
|
|
983
|
+
typeof descriptor.operation !== "string" ||
|
|
984
|
+
typeof descriptor.payload !== "object" ||
|
|
985
|
+
descriptor.payload === null
|
|
986
|
+
)
|
|
987
|
+
return realpathSync(coordinationRoot);
|
|
988
|
+
if (
|
|
989
|
+
!descriptor.operation.startsWith("git.") &&
|
|
990
|
+
!descriptor.operation.startsWith("hosting.") &&
|
|
991
|
+
descriptor.operation !== "changelog.apply"
|
|
992
|
+
)
|
|
993
|
+
return realpathSync(coordinationRoot);
|
|
994
|
+
const payload = descriptor.payload as Record<string, unknown>;
|
|
995
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : coordinationRoot;
|
|
996
|
+
try {
|
|
997
|
+
return realpathSync(path.resolve(coordinationRoot, cwd));
|
|
998
|
+
} catch {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
871
1003
|
export type NativeExternalActionObservation = {
|
|
872
1004
|
actor: string;
|
|
873
1005
|
actionRef: Ref;
|
|
@@ -946,7 +1078,8 @@ export const matchesNativeExternalAction = (
|
|
|
946
1078
|
*/
|
|
947
1079
|
export async function runAuthorizedExternalAction<T>(
|
|
948
1080
|
input: AuthorizedActionInput,
|
|
949
|
-
effect: () => T | Result<T> | Promise<T | Result<T>>,
|
|
1081
|
+
effect: (reservation: ActionReservation) => T | Result<T> | Promise<T | Result<T>>,
|
|
1082
|
+
withActionLock?: WithExternalActionLock,
|
|
950
1083
|
): Promise<ExternalActionResult<T>> {
|
|
951
1084
|
if (input.core.isDelegatedCaller())
|
|
952
1085
|
return failure("permission_denied", "delegated workers cannot run external actions");
|
|
@@ -981,7 +1114,7 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
981
1114
|
expectedRevision: reserved.data.taskRevision,
|
|
982
1115
|
expectedWorkspaceRevision: reserved.data.workspaceRevision,
|
|
983
1116
|
};
|
|
984
|
-
|
|
1117
|
+
const result = input.core.settleAction({
|
|
985
1118
|
...reserved.data,
|
|
986
1119
|
taskRevision: revisions.expectedRevision,
|
|
987
1120
|
workspaceRevision: revisions.expectedWorkspaceRevision,
|
|
@@ -992,6 +1125,7 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
992
1125
|
workspaceRevision: revisions.expectedWorkspaceRevision,
|
|
993
1126
|
}),
|
|
994
1127
|
});
|
|
1128
|
+
return result;
|
|
995
1129
|
} catch {
|
|
996
1130
|
return failure("external_outcome_unknown", "external action settlement is unavailable", {
|
|
997
1131
|
operation: "external_action",
|
|
@@ -1007,43 +1141,67 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1007
1141
|
? settle(outcome, true)
|
|
1008
1142
|
: first;
|
|
1009
1143
|
|
|
1010
|
-
let
|
|
1144
|
+
let effectStarted = false;
|
|
1145
|
+
const runEffect = async (): Promise<ExternalActionResult<T>> => {
|
|
1146
|
+
effectStarted = true;
|
|
1147
|
+
let effectValue: T | Result<T>;
|
|
1148
|
+
try {
|
|
1149
|
+
effectValue = await effect(reserved.data);
|
|
1150
|
+
} catch {
|
|
1151
|
+
const settled = settleWithRefresh("unknown", settle("unknown"));
|
|
1152
|
+
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1153
|
+
return failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
1154
|
+
operation: "external_action",
|
|
1155
|
+
outcome: "unknown",
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
if (
|
|
1159
|
+
typeof effectValue === "object" &&
|
|
1160
|
+
effectValue !== null &&
|
|
1161
|
+
"ok" in effectValue &&
|
|
1162
|
+
typeof (effectValue as { ok?: unknown }).ok === "boolean"
|
|
1163
|
+
) {
|
|
1164
|
+
const contract = effectValue as Result<T>;
|
|
1165
|
+
if (!contract.ok) {
|
|
1166
|
+
const preflight =
|
|
1167
|
+
typeof contract.details === "object" &&
|
|
1168
|
+
contract.details !== null &&
|
|
1169
|
+
(contract.details as Record<string, unknown>).outcome === "not_started";
|
|
1170
|
+
const outcome = input.failureOutcome ?? (preflight ? "not_started" : "unknown");
|
|
1171
|
+
const settled = settleWithRefresh(outcome, settle(outcome));
|
|
1172
|
+
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1173
|
+
return contract;
|
|
1174
|
+
}
|
|
1175
|
+
effectValue = contract.data;
|
|
1176
|
+
}
|
|
1177
|
+
const settled = settleWithRefresh("succeeded", settle("succeeded"));
|
|
1178
|
+
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1179
|
+
return {
|
|
1180
|
+
ok: true,
|
|
1181
|
+
schemaVersion: 1,
|
|
1182
|
+
revision: settled.revision,
|
|
1183
|
+
workspaceRevision: settled.workspaceRevision,
|
|
1184
|
+
data: effectValue as T,
|
|
1185
|
+
};
|
|
1186
|
+
};
|
|
1187
|
+
let result: ExternalActionResult<T>;
|
|
1011
1188
|
try {
|
|
1012
|
-
|
|
1189
|
+
result = withActionLock ? await withActionLock(runEffect) : await runEffect();
|
|
1013
1190
|
} catch {
|
|
1014
|
-
|
|
1191
|
+
if (effectStarted)
|
|
1192
|
+
return failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
1193
|
+
operation: "external_action",
|
|
1194
|
+
outcome: "unknown",
|
|
1195
|
+
});
|
|
1196
|
+
const settled = settleWithRefresh("not_started", settle("not_started"));
|
|
1015
1197
|
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1016
|
-
return failure("
|
|
1017
|
-
|
|
1018
|
-
outcome: "unknown",
|
|
1198
|
+
return failure("writer_conflict", "target checkout was unavailable before the effect", {
|
|
1199
|
+
outcome: "not_started",
|
|
1019
1200
|
});
|
|
1020
1201
|
}
|
|
1021
|
-
if (
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
"ok" in effectValue &&
|
|
1025
|
-
typeof (effectValue as { ok?: unknown }).ok === "boolean"
|
|
1026
|
-
) {
|
|
1027
|
-
const contract = effectValue as Result<T>;
|
|
1028
|
-
if (!contract.ok) {
|
|
1029
|
-
const preflight =
|
|
1030
|
-
typeof contract.details === "object" &&
|
|
1031
|
-
contract.details !== null &&
|
|
1032
|
-
(contract.details as Record<string, unknown>).outcome === "not_started";
|
|
1033
|
-
const outcome = input.failureOutcome ?? (preflight ? "not_started" : "unknown");
|
|
1034
|
-
const settled = settleWithRefresh(outcome, settle(outcome));
|
|
1035
|
-
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1036
|
-
return contract;
|
|
1037
|
-
}
|
|
1038
|
-
effectValue = contract.data;
|
|
1202
|
+
if (!effectStarted) {
|
|
1203
|
+
const settled = settleWithRefresh("not_started", settle("not_started"));
|
|
1204
|
+
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1039
1205
|
}
|
|
1040
|
-
|
|
1041
|
-
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1042
|
-
return {
|
|
1043
|
-
ok: true,
|
|
1044
|
-
schemaVersion: 1,
|
|
1045
|
-
revision: settled.revision,
|
|
1046
|
-
workspaceRevision: settled.workspaceRevision,
|
|
1047
|
-
data: effectValue as T,
|
|
1048
|
-
};
|
|
1206
|
+
return result;
|
|
1049
1207
|
}
|