@brainervirus/workit-core 1.2.0 → 1.3.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/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 +31 -8
- package/src/core/auto-approval.ts +44 -32
- 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 +1102 -209
- package/src/core/external-action.ts +230 -103
- package/src/core/init.ts +25 -115
- package/src/core/legacy-ownership.ts +47 -0
- package/src/core/methods.ts +56 -52
- 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 +16 -3
- package/src/core/task-engine.ts +136 -31
- package/src/core/task-evaluation.ts +3 -5
- 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/workers.ts +17 -0
- package/src/core/workspaces.ts +548 -46
- package/src/core.ts +1 -2
- package/src/core/ports/vcs-token-create-urls.ts +0 -4
|
@@ -13,10 +13,23 @@ 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";
|
|
27
|
+
|
|
28
|
+
const chainStepSchema = z.union([
|
|
29
|
+
z.string().min(1),
|
|
30
|
+
z.object({ branch: z.string().min(1) }).strict(),
|
|
31
|
+
z.object({ pr: z.literal(true) }).strict(),
|
|
32
|
+
]);
|
|
20
33
|
|
|
21
34
|
export type AuthorizedActionInput = {
|
|
22
35
|
core: WorkitCore;
|
|
@@ -39,6 +52,9 @@ export type AuthorizedActionInput = {
|
|
|
39
52
|
};
|
|
40
53
|
|
|
41
54
|
export type ExternalActionResult<T> = Result<T>;
|
|
55
|
+
type WithExternalActionLock = <T>(
|
|
56
|
+
operation: () => Promise<ExternalActionResult<T>>,
|
|
57
|
+
) => Promise<ExternalActionResult<T>>;
|
|
42
58
|
|
|
43
59
|
const externalActionSchema = z.discriminatedUnion("operation", [
|
|
44
60
|
z
|
|
@@ -50,6 +66,7 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
50
66
|
sdd_dir: z.string().optional(),
|
|
51
67
|
target_branch: z.string().optional(),
|
|
52
68
|
stash: z.enum(["yes", "no"]).optional(),
|
|
69
|
+
cwd: z.string().min(1).optional(),
|
|
53
70
|
})
|
|
54
71
|
.strict(),
|
|
55
72
|
})
|
|
@@ -60,8 +77,9 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
60
77
|
payload: z
|
|
61
78
|
.object({
|
|
62
79
|
message: z.string().min(1).optional(),
|
|
63
|
-
plan_steps: z.array(
|
|
80
|
+
plan_steps: z.array(chainStepSchema).min(1).max(32).optional(),
|
|
64
81
|
plan_branch: z.string().min(1).optional(),
|
|
82
|
+
cwd: z.string().min(1).optional(),
|
|
65
83
|
})
|
|
66
84
|
.strict(),
|
|
67
85
|
})
|
|
@@ -69,7 +87,9 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
69
87
|
z
|
|
70
88
|
.object({
|
|
71
89
|
operation: z.literal("git.push"),
|
|
72
|
-
payload: z
|
|
90
|
+
payload: z
|
|
91
|
+
.object({ branch: z.string().min(1).optional(), cwd: z.string().min(1).optional() })
|
|
92
|
+
.strict(),
|
|
73
93
|
})
|
|
74
94
|
.strict(),
|
|
75
95
|
z
|
|
@@ -82,6 +102,7 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
82
102
|
draft: z.boolean().optional(),
|
|
83
103
|
target_branch: z.string().optional(),
|
|
84
104
|
babysit: z.boolean().optional(),
|
|
105
|
+
cwd: z.string().min(1).optional(),
|
|
85
106
|
})
|
|
86
107
|
.strict(),
|
|
87
108
|
})
|
|
@@ -93,10 +114,17 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
93
114
|
.object({
|
|
94
115
|
target_branch: z.string().min(1).optional(),
|
|
95
116
|
source_branch: z.string().min(1).optional(),
|
|
117
|
+
cwd: z.string().min(1).optional(),
|
|
96
118
|
})
|
|
97
119
|
.strict(),
|
|
98
120
|
})
|
|
99
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(),
|
|
100
128
|
z
|
|
101
129
|
.object({
|
|
102
130
|
operation: z.literal("youtrack.update"),
|
|
@@ -173,6 +201,7 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
173
201
|
mode: z.string().optional(),
|
|
174
202
|
specPath: z.string().optional(),
|
|
175
203
|
planPath: z.string().optional(),
|
|
204
|
+
cwd: z.string().min(1).optional(),
|
|
176
205
|
})
|
|
177
206
|
.strict(),
|
|
178
207
|
})
|
|
@@ -199,7 +228,10 @@ export type ExternalActionRunner = <T>(
|
|
|
199
228
|
) => Promise<ExternalActionResult<T>>;
|
|
200
229
|
export type ExternalActionEffect = <T>(
|
|
201
230
|
operation: string,
|
|
202
|
-
effect: (
|
|
231
|
+
effect: (
|
|
232
|
+
step?: string,
|
|
233
|
+
reservation?: ActionReservation,
|
|
234
|
+
) => T | Result<T> | Promise<T | Result<T>>,
|
|
203
235
|
) => Promise<ExternalActionResult<T>>;
|
|
204
236
|
export type ExternalActionBinding = (
|
|
205
237
|
operation: string,
|
|
@@ -244,7 +276,7 @@ export const planReservationLength = (
|
|
|
244
276
|
|
|
245
277
|
/** Compact host-facing help for the fixed optional-action surface. */
|
|
246
278
|
export const externalActionHelp =
|
|
247
|
-
"Fixed actions:
|
|
279
|
+
"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).";
|
|
248
280
|
|
|
249
281
|
export const externalActionRef = (
|
|
250
282
|
host: "opencode" | "pi" | "workit_cli",
|
|
@@ -268,14 +300,7 @@ export const externalActionState = (
|
|
|
268
300
|
return failure("storage_error", "external action state is unavailable");
|
|
269
301
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
270
302
|
const candidates = listed.data.flatMap((task) => {
|
|
271
|
-
if (
|
|
272
|
-
task.status !== "active" ||
|
|
273
|
-
task.workspaceId !== workspace.data!.id ||
|
|
274
|
-
task.intent.provenance.session?.kind !== "host" ||
|
|
275
|
-
task.intent.provenance.session.host !== host ||
|
|
276
|
-
task.intent.provenance.session.handle !== actor
|
|
277
|
-
)
|
|
278
|
-
return [];
|
|
303
|
+
if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
|
|
279
304
|
return task.decisions
|
|
280
305
|
.filter(
|
|
281
306
|
(entry) =>
|
|
@@ -290,8 +315,8 @@ export const externalActionState = (
|
|
|
290
315
|
return failure(
|
|
291
316
|
"permission_denied",
|
|
292
317
|
candidates.length === 0
|
|
293
|
-
? "no approved action is bound to
|
|
294
|
-
: "multiple approved actions match
|
|
318
|
+
? "no approved action is bound to the active task"
|
|
319
|
+
: "multiple approved actions match the active task",
|
|
295
320
|
);
|
|
296
321
|
return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
|
|
297
322
|
};
|
|
@@ -352,13 +377,14 @@ export const priorExternalAction = (
|
|
|
352
377
|
/** Parse a plan-commit authorization descriptor (one approved list per plan). */
|
|
353
378
|
export const planCommitDescriptor = (
|
|
354
379
|
content: string,
|
|
355
|
-
): { steps: ChainStep[]; branch: string; snapshotHead: string | null } | null => {
|
|
380
|
+
): { steps: ChainStep[]; branch: string; snapshotHead: string | null; cwd?: string } | null => {
|
|
356
381
|
try {
|
|
357
382
|
const value = JSON.parse(content) as {
|
|
358
383
|
operation?: unknown;
|
|
359
384
|
payload?: {
|
|
360
385
|
plan_steps?: unknown;
|
|
361
386
|
plan_branch?: unknown;
|
|
387
|
+
cwd?: unknown;
|
|
362
388
|
resolved?: { head?: unknown; branch?: unknown };
|
|
363
389
|
};
|
|
364
390
|
};
|
|
@@ -366,8 +392,15 @@ export const planCommitDescriptor = (
|
|
|
366
392
|
const steps = normalizeChainSteps(value.payload.plan_steps);
|
|
367
393
|
const branch = value.payload.plan_branch;
|
|
368
394
|
if (!steps || typeof branch !== "string" || !branch) return null;
|
|
395
|
+
const cwd = value.payload.cwd;
|
|
396
|
+
if (cwd !== undefined && (typeof cwd !== "string" || !path.isAbsolute(cwd))) return null;
|
|
369
397
|
const head = value.payload.resolved?.head;
|
|
370
|
-
return {
|
|
398
|
+
return {
|
|
399
|
+
steps,
|
|
400
|
+
branch,
|
|
401
|
+
snapshotHead: typeof head === "string" ? head : null,
|
|
402
|
+
...(typeof cwd === "string" ? { cwd } : {}),
|
|
403
|
+
};
|
|
371
404
|
} catch {
|
|
372
405
|
return null;
|
|
373
406
|
}
|
|
@@ -378,17 +411,21 @@ export const planCommitDescriptor = (
|
|
|
378
411
|
* HEAD — history only moved forward. Rebase/reset underneath an approved
|
|
379
412
|
* chain invalidates it instead of executing on rewritten history.
|
|
380
413
|
*/
|
|
381
|
-
export const chainLeaseValid = (
|
|
414
|
+
export const chainLeaseValid = (
|
|
415
|
+
store: TaskStore,
|
|
416
|
+
snapshotHead: string | null,
|
|
417
|
+
targetRoot = store.root,
|
|
418
|
+
): boolean => {
|
|
382
419
|
if (!snapshotHead) return true;
|
|
383
420
|
try {
|
|
384
421
|
const head = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
385
|
-
cwd:
|
|
422
|
+
cwd: targetRoot,
|
|
386
423
|
encoding: "utf8",
|
|
387
424
|
stdio: ["pipe", "pipe", "pipe"],
|
|
388
425
|
}).trim();
|
|
389
426
|
if (head === snapshotHead) return true;
|
|
390
427
|
execFileSync("git", ["merge-base", "--is-ancestor", snapshotHead, head], {
|
|
391
|
-
cwd:
|
|
428
|
+
cwd: targetRoot,
|
|
392
429
|
encoding: "utf8",
|
|
393
430
|
stdio: ["pipe", "pipe", "pipe"],
|
|
394
431
|
});
|
|
@@ -421,6 +458,7 @@ export const approvedPlanCommit = (
|
|
|
421
458
|
host: string,
|
|
422
459
|
actor: string,
|
|
423
460
|
message: string,
|
|
461
|
+
targetRoot = store.root,
|
|
424
462
|
): Result<PlanCommitAuthorization> => {
|
|
425
463
|
if (typeof message !== "string" || !message)
|
|
426
464
|
return failure("invalid_input", "commit message is required");
|
|
@@ -431,12 +469,6 @@ export const approvedPlanCommit = (
|
|
|
431
469
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
432
470
|
const matches = listed.data.flatMap((task) => {
|
|
433
471
|
if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
|
|
434
|
-
if (
|
|
435
|
-
task.intent.provenance.session?.kind !== "host" ||
|
|
436
|
-
task.intent.provenance.session.host !== host ||
|
|
437
|
-
task.intent.provenance.session.handle !== actor
|
|
438
|
-
)
|
|
439
|
-
return [];
|
|
440
472
|
return task.decisions.flatMap((entry) => {
|
|
441
473
|
const decision = entry.data;
|
|
442
474
|
if (
|
|
@@ -449,12 +481,13 @@ export const approvedPlanCommit = (
|
|
|
449
481
|
return [];
|
|
450
482
|
const plan = planCommitDescriptor(decision.binding.approvedContent);
|
|
451
483
|
if (!plan) return [];
|
|
484
|
+
if (!sameDirectoryIdentity(plan.cwd ?? store.root, targetRoot)) return [];
|
|
452
485
|
const completed =
|
|
453
486
|
task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ??
|
|
454
487
|
[];
|
|
455
488
|
const next = plan.steps[completed.length];
|
|
456
489
|
if (!next || next.kind !== "commit" || next.message !== message) return [];
|
|
457
|
-
if (!chainLeaseValid(store, plan.snapshotHead)) return [];
|
|
490
|
+
if (!chainLeaseValid(store, plan.snapshotHead, targetRoot)) return [];
|
|
458
491
|
return [
|
|
459
492
|
{
|
|
460
493
|
task,
|
|
@@ -478,15 +511,10 @@ export const approvedPlanCommit = (
|
|
|
478
511
|
};
|
|
479
512
|
|
|
480
513
|
export type ChainStepQuery =
|
|
481
|
-
| { operation: "git.branch_setup"; target: string }
|
|
482
|
-
| { operation: "hosting.pull_request" };
|
|
514
|
+
| { operation: "git.branch_setup"; target: string; cwd?: string }
|
|
515
|
+
| { operation: "hosting.pull_request"; cwd?: string };
|
|
483
516
|
|
|
484
|
-
/**
|
|
485
|
-
* Match one branch or PR step of a chain authorization: same session-bound
|
|
486
|
-
* search as plan commits, next unconsumed step of the right kind, lease
|
|
487
|
-
* intact. Commit steps stay on the plan path; this covers the steps plans
|
|
488
|
-
* could never express.
|
|
489
|
-
*/
|
|
517
|
+
/** Match the next branch or PR step of an active chain authorization. */
|
|
490
518
|
export const approvedChainStep = (
|
|
491
519
|
store: TaskStore,
|
|
492
520
|
host: string,
|
|
@@ -500,12 +528,6 @@ export const approvedChainStep = (
|
|
|
500
528
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
501
529
|
const matches = listed.data.flatMap((task) => {
|
|
502
530
|
if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
|
|
503
|
-
if (
|
|
504
|
-
task.intent.provenance.session?.kind !== "host" ||
|
|
505
|
-
task.intent.provenance.session.host !== host ||
|
|
506
|
-
task.intent.provenance.session.handle !== actor
|
|
507
|
-
)
|
|
508
|
-
return [];
|
|
509
531
|
return task.decisions.flatMap((entry) => {
|
|
510
532
|
const decision = entry.data;
|
|
511
533
|
if (
|
|
@@ -518,6 +540,8 @@ export const approvedChainStep = (
|
|
|
518
540
|
return [];
|
|
519
541
|
const plan = planCommitDescriptor(decision.binding.approvedContent);
|
|
520
542
|
if (!plan) return [];
|
|
543
|
+
const targetRoot = query.cwd ?? store.root;
|
|
544
|
+
if (!sameDirectoryIdentity(plan.cwd ?? store.root, targetRoot)) return [];
|
|
521
545
|
const completed =
|
|
522
546
|
task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ??
|
|
523
547
|
[];
|
|
@@ -528,7 +552,7 @@ export const approvedChainStep = (
|
|
|
528
552
|
} else if (next.kind !== "pr") {
|
|
529
553
|
return [];
|
|
530
554
|
}
|
|
531
|
-
if (!chainLeaseValid(store, plan.snapshotHead)) return [];
|
|
555
|
+
if (!chainLeaseValid(store, plan.snapshotHead, targetRoot)) return [];
|
|
532
556
|
return [
|
|
533
557
|
{
|
|
534
558
|
task,
|
|
@@ -565,6 +589,17 @@ export const commitMessageFromDescriptor = (operation: string): string | undefin
|
|
|
565
589
|
}
|
|
566
590
|
};
|
|
567
591
|
|
|
592
|
+
const descriptorTargetRoot = (store: TaskStore, operation: string): string | null => {
|
|
593
|
+
try {
|
|
594
|
+
const descriptor = JSON.parse(operation) as { payload?: { cwd?: unknown } };
|
|
595
|
+
const cwd = descriptor.payload?.cwd;
|
|
596
|
+
if (cwd === undefined) return store.root;
|
|
597
|
+
return typeof cwd === "string" && path.isAbsolute(cwd) ? realpathSync(cwd) : null;
|
|
598
|
+
} catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
568
603
|
/** Resolve a chain authorization into the exact binding an adapter needs
|
|
569
604
|
* to run one branch or PR step under the approved chain. Commit steps stay
|
|
570
605
|
* on the plan path; this covers the steps plans could never express. */
|
|
@@ -590,12 +625,18 @@ export const chainStepBinding = (
|
|
|
590
625
|
} catch {
|
|
591
626
|
return null;
|
|
592
627
|
}
|
|
628
|
+
const targetRoot = descriptorTargetRoot(store, operation);
|
|
629
|
+
if (!targetRoot) return null;
|
|
593
630
|
const query: ChainStepQuery | null =
|
|
594
631
|
descriptor.operation === "git.branch_setup" &&
|
|
595
632
|
typeof descriptor.payload?.target_branch === "string"
|
|
596
|
-
? {
|
|
633
|
+
? {
|
|
634
|
+
operation: "git.branch_setup",
|
|
635
|
+
target: descriptor.payload.target_branch as string,
|
|
636
|
+
cwd: targetRoot,
|
|
637
|
+
}
|
|
597
638
|
: descriptor.operation === "hosting.pull_request"
|
|
598
|
-
? { operation: "hosting.pull_request" }
|
|
639
|
+
? { operation: "hosting.pull_request", cwd: targetRoot }
|
|
599
640
|
: null;
|
|
600
641
|
if (!query) return null;
|
|
601
642
|
const chain = approvedChainStep(store, host, actor, query);
|
|
@@ -605,7 +646,7 @@ export const chainStepBinding = (
|
|
|
605
646
|
let branch: string;
|
|
606
647
|
try {
|
|
607
648
|
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
608
|
-
cwd:
|
|
649
|
+
cwd: targetRoot,
|
|
609
650
|
encoding: "utf8",
|
|
610
651
|
stdio: ["pipe", "pipe", "pipe"],
|
|
611
652
|
}).trim();
|
|
@@ -645,12 +686,14 @@ export const planCommitBinding = (
|
|
|
645
686
|
} | null => {
|
|
646
687
|
const message = commitMessageFromDescriptor(operation);
|
|
647
688
|
if (!message) return null;
|
|
648
|
-
const
|
|
689
|
+
const targetRoot = descriptorTargetRoot(store, operation);
|
|
690
|
+
if (!targetRoot) return null;
|
|
691
|
+
const plan = approvedPlanCommit(store, host, actor, message, targetRoot);
|
|
649
692
|
if (!plan.ok) return null;
|
|
650
693
|
let branch: string;
|
|
651
694
|
try {
|
|
652
695
|
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
653
|
-
cwd:
|
|
696
|
+
cwd: targetRoot,
|
|
654
697
|
encoding: "utf8",
|
|
655
698
|
stdio: ["pipe", "pipe", "pipe"],
|
|
656
699
|
}).trim();
|
|
@@ -709,18 +752,26 @@ export const approvedExternalAction = (
|
|
|
709
752
|
return selected;
|
|
710
753
|
};
|
|
711
754
|
|
|
712
|
-
/** Intent identity for
|
|
713
|
-
* effect; head and dirt are execution-time state that re-resolves. */
|
|
755
|
+
/** Intent identity for branch setup includes the exact remote and local base commits. */
|
|
714
756
|
export const branchSetupIntent = (
|
|
715
757
|
descriptor: string,
|
|
716
|
-
): {
|
|
758
|
+
): {
|
|
759
|
+
target: string;
|
|
760
|
+
base: string;
|
|
761
|
+
localBase: string | null;
|
|
762
|
+
remoteBase: string | null;
|
|
763
|
+
targetExists: boolean;
|
|
764
|
+
cwd?: string;
|
|
765
|
+
} | null => {
|
|
717
766
|
try {
|
|
718
767
|
const value = JSON.parse(descriptor) as {
|
|
719
768
|
operation?: unknown;
|
|
720
769
|
payload?: {
|
|
721
770
|
target_branch?: unknown;
|
|
771
|
+
cwd?: unknown;
|
|
722
772
|
resolved?: {
|
|
723
773
|
base_branch?: unknown;
|
|
774
|
+
local_base?: unknown;
|
|
724
775
|
remote_base?: unknown;
|
|
725
776
|
target_exists?: unknown;
|
|
726
777
|
};
|
|
@@ -729,13 +780,20 @@ export const branchSetupIntent = (
|
|
|
729
780
|
if (value.operation !== "git.branch_setup" || !value.payload) return null;
|
|
730
781
|
const { target_branch, resolved } = value.payload;
|
|
731
782
|
if (typeof target_branch !== "string" || !target_branch || !resolved) return null;
|
|
783
|
+
if (
|
|
784
|
+
value.payload.cwd !== undefined &&
|
|
785
|
+
(typeof value.payload.cwd !== "string" || !path.isAbsolute(value.payload.cwd))
|
|
786
|
+
)
|
|
787
|
+
return null;
|
|
732
788
|
if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
|
|
733
789
|
return null;
|
|
734
790
|
return {
|
|
735
791
|
target: target_branch,
|
|
736
792
|
base: resolved.base_branch,
|
|
793
|
+
localBase: typeof resolved.local_base === "string" ? resolved.local_base : null,
|
|
737
794
|
remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
|
|
738
795
|
targetExists: resolved.target_exists,
|
|
796
|
+
...(typeof value.payload.cwd === "string" ? { cwd: value.payload.cwd } : {}),
|
|
739
797
|
};
|
|
740
798
|
} catch {
|
|
741
799
|
return null;
|
|
@@ -751,21 +809,15 @@ export const approvedBranchSetupIntent = (
|
|
|
751
809
|
operation: string,
|
|
752
810
|
) => {
|
|
753
811
|
const current = branchSetupIntent(operation);
|
|
754
|
-
if (!current)
|
|
812
|
+
if (!current)
|
|
813
|
+
return failure("permission_denied", "no approved action is bound to the active task");
|
|
755
814
|
const listed = store.listTasks();
|
|
756
815
|
const workspace = store.readWorkspace();
|
|
757
816
|
if (!listed.ok || !workspace.ok)
|
|
758
817
|
return failure("storage_error", "external action state is unavailable");
|
|
759
818
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
760
819
|
const candidates = listed.data.flatMap((task) => {
|
|
761
|
-
if (
|
|
762
|
-
task.status !== "active" ||
|
|
763
|
-
task.workspaceId !== workspace.data!.id ||
|
|
764
|
-
task.intent.provenance.session?.kind !== "host" ||
|
|
765
|
-
task.intent.provenance.session.host !== host ||
|
|
766
|
-
task.intent.provenance.session.handle !== actor
|
|
767
|
-
)
|
|
768
|
-
return [];
|
|
820
|
+
if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
|
|
769
821
|
return task.decisions
|
|
770
822
|
.filter(
|
|
771
823
|
(entry) =>
|
|
@@ -779,8 +831,10 @@ export const approvedBranchSetupIntent = (
|
|
|
779
831
|
const intent = branchSetupIntent(entry.data.binding.approvedContent);
|
|
780
832
|
return (
|
|
781
833
|
intent !== null &&
|
|
834
|
+
(intent.cwd ?? store.root) === (current.cwd ?? store.root) &&
|
|
782
835
|
intent.target === current.target &&
|
|
783
836
|
intent.base === current.base &&
|
|
837
|
+
intent.localBase === current.localBase &&
|
|
784
838
|
intent.remoteBase === current.remoteBase &&
|
|
785
839
|
intent.targetExists === current.targetExists
|
|
786
840
|
);
|
|
@@ -789,8 +843,8 @@ export const approvedBranchSetupIntent = (
|
|
|
789
843
|
return failure(
|
|
790
844
|
"permission_denied",
|
|
791
845
|
matches.length === 0
|
|
792
|
-
? "no approved action is bound to
|
|
793
|
-
: "multiple approved actions match
|
|
846
|
+
? "no approved action is bound to the active task"
|
|
847
|
+
: "multiple approved actions match the active task",
|
|
794
848
|
);
|
|
795
849
|
return success(workspace.data.revision, null, { ...matches[0], workspace: workspace.data });
|
|
796
850
|
};
|
|
@@ -844,9 +898,11 @@ export const priorResolvedDrift = (
|
|
|
844
898
|
? "base_branch"
|
|
845
899
|
: before.remoteBase !== after.remoteBase
|
|
846
900
|
? "remote_base"
|
|
847
|
-
: before.
|
|
848
|
-
? "
|
|
849
|
-
:
|
|
901
|
+
: before.localBase !== after.localBase
|
|
902
|
+
? "local_base"
|
|
903
|
+
: before.targetExists !== after.targetExists
|
|
904
|
+
? "target_exists"
|
|
905
|
+
: null;
|
|
850
906
|
if (moved === null) return success(null, null, null);
|
|
851
907
|
return failure(
|
|
852
908
|
"invalid_input",
|
|
@@ -864,10 +920,13 @@ export const priorResolvedDrift = (
|
|
|
864
920
|
};
|
|
865
921
|
|
|
866
922
|
export const createAuthorizedExternalActionRunner =
|
|
867
|
-
(core: WorkitCore, bind: ExternalActionBinding): ExternalActionEffect =>
|
|
923
|
+
(core: WorkitCore, bind: ExternalActionBinding, coordinationRoot: string): ExternalActionEffect =>
|
|
868
924
|
async <T>(
|
|
869
925
|
operation: string,
|
|
870
|
-
effect: (
|
|
926
|
+
effect: (
|
|
927
|
+
step?: string,
|
|
928
|
+
reservation?: ActionReservation,
|
|
929
|
+
) => T | Result<T> | Promise<T | Result<T>>,
|
|
871
930
|
) => {
|
|
872
931
|
let steps: string[] = [];
|
|
873
932
|
try {
|
|
@@ -882,9 +941,21 @@ export const createAuthorizedExternalActionRunner =
|
|
|
882
941
|
for (const step of sequence) {
|
|
883
942
|
const binding = bind(operation);
|
|
884
943
|
if ("ok" in binding) return binding as ExternalActionResult<T>;
|
|
944
|
+
const targetRoot = externalActionTargetRoot(operation, coordinationRoot);
|
|
945
|
+
if (targetRoot === null)
|
|
946
|
+
return failure("capability_unavailable", "external action target could not be locked", {
|
|
947
|
+
outcome: "not_started",
|
|
948
|
+
});
|
|
949
|
+
const withActionLock: WithExternalActionLock | undefined = !sameDirectoryIdentity(
|
|
950
|
+
targetRoot,
|
|
951
|
+
coordinationRoot,
|
|
952
|
+
)
|
|
953
|
+
? (run) => new TaskStore(targetRoot).withExternalActionLock(run, true)
|
|
954
|
+
: undefined;
|
|
885
955
|
const result = await runAuthorizedExternalAction(
|
|
886
956
|
{ core, ...(binding as Omit<AuthorizedActionInput, "core">), ...(step ? { step } : {}) },
|
|
887
|
-
() => effect(step),
|
|
957
|
+
(reservation) => effect(step, reservation),
|
|
958
|
+
withActionLock,
|
|
888
959
|
);
|
|
889
960
|
latest = result;
|
|
890
961
|
if (!result.ok) return result;
|
|
@@ -892,6 +963,36 @@ export const createAuthorizedExternalActionRunner =
|
|
|
892
963
|
return latest ?? failure("recovery_required", "external action did not execute");
|
|
893
964
|
};
|
|
894
965
|
|
|
966
|
+
const externalActionTargetRoot = (operation: string, coordinationRoot: string): string | null => {
|
|
967
|
+
let value: unknown;
|
|
968
|
+
try {
|
|
969
|
+
value = JSON.parse(operation);
|
|
970
|
+
} catch {
|
|
971
|
+
return realpathSync(coordinationRoot);
|
|
972
|
+
}
|
|
973
|
+
if (typeof value !== "object" || value === null) return realpathSync(coordinationRoot);
|
|
974
|
+
const descriptor = value as { operation?: unknown; payload?: unknown };
|
|
975
|
+
if (
|
|
976
|
+
typeof descriptor.operation !== "string" ||
|
|
977
|
+
typeof descriptor.payload !== "object" ||
|
|
978
|
+
descriptor.payload === null
|
|
979
|
+
)
|
|
980
|
+
return realpathSync(coordinationRoot);
|
|
981
|
+
if (
|
|
982
|
+
!descriptor.operation.startsWith("git.") &&
|
|
983
|
+
!descriptor.operation.startsWith("hosting.") &&
|
|
984
|
+
descriptor.operation !== "changelog.apply"
|
|
985
|
+
)
|
|
986
|
+
return realpathSync(coordinationRoot);
|
|
987
|
+
const payload = descriptor.payload as Record<string, unknown>;
|
|
988
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : coordinationRoot;
|
|
989
|
+
try {
|
|
990
|
+
return realpathSync(path.resolve(coordinationRoot, cwd));
|
|
991
|
+
} catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
895
996
|
export type NativeExternalActionObservation = {
|
|
896
997
|
actor: string;
|
|
897
998
|
actionRef: Ref;
|
|
@@ -970,7 +1071,8 @@ export const matchesNativeExternalAction = (
|
|
|
970
1071
|
*/
|
|
971
1072
|
export async function runAuthorizedExternalAction<T>(
|
|
972
1073
|
input: AuthorizedActionInput,
|
|
973
|
-
effect: () => T | Result<T> | Promise<T | Result<T>>,
|
|
1074
|
+
effect: (reservation: ActionReservation) => T | Result<T> | Promise<T | Result<T>>,
|
|
1075
|
+
withActionLock?: WithExternalActionLock,
|
|
974
1076
|
): Promise<ExternalActionResult<T>> {
|
|
975
1077
|
if (input.core.isDelegatedCaller())
|
|
976
1078
|
return failure("permission_denied", "delegated workers cannot run external actions");
|
|
@@ -1005,7 +1107,7 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1005
1107
|
expectedRevision: reserved.data.taskRevision,
|
|
1006
1108
|
expectedWorkspaceRevision: reserved.data.workspaceRevision,
|
|
1007
1109
|
};
|
|
1008
|
-
|
|
1110
|
+
const result = input.core.settleAction({
|
|
1009
1111
|
...reserved.data,
|
|
1010
1112
|
taskRevision: revisions.expectedRevision,
|
|
1011
1113
|
workspaceRevision: revisions.expectedWorkspaceRevision,
|
|
@@ -1016,6 +1118,7 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1016
1118
|
workspaceRevision: revisions.expectedWorkspaceRevision,
|
|
1017
1119
|
}),
|
|
1018
1120
|
});
|
|
1121
|
+
return result;
|
|
1019
1122
|
} catch {
|
|
1020
1123
|
return failure("external_outcome_unknown", "external action settlement is unavailable", {
|
|
1021
1124
|
operation: "external_action",
|
|
@@ -1031,43 +1134,67 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1031
1134
|
? settle(outcome, true)
|
|
1032
1135
|
: first;
|
|
1033
1136
|
|
|
1034
|
-
let
|
|
1137
|
+
let effectStarted = false;
|
|
1138
|
+
const runEffect = async (): Promise<ExternalActionResult<T>> => {
|
|
1139
|
+
effectStarted = true;
|
|
1140
|
+
let effectValue: T | Result<T>;
|
|
1141
|
+
try {
|
|
1142
|
+
effectValue = await effect(reserved.data);
|
|
1143
|
+
} catch {
|
|
1144
|
+
const settled = settleWithRefresh("unknown", settle("unknown"));
|
|
1145
|
+
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1146
|
+
return failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
1147
|
+
operation: "external_action",
|
|
1148
|
+
outcome: "unknown",
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
if (
|
|
1152
|
+
typeof effectValue === "object" &&
|
|
1153
|
+
effectValue !== null &&
|
|
1154
|
+
"ok" in effectValue &&
|
|
1155
|
+
typeof (effectValue as { ok?: unknown }).ok === "boolean"
|
|
1156
|
+
) {
|
|
1157
|
+
const contract = effectValue as Result<T>;
|
|
1158
|
+
if (!contract.ok) {
|
|
1159
|
+
const preflight =
|
|
1160
|
+
typeof contract.details === "object" &&
|
|
1161
|
+
contract.details !== null &&
|
|
1162
|
+
(contract.details as Record<string, unknown>).outcome === "not_started";
|
|
1163
|
+
const outcome = input.failureOutcome ?? (preflight ? "not_started" : "unknown");
|
|
1164
|
+
const settled = settleWithRefresh(outcome, settle(outcome));
|
|
1165
|
+
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1166
|
+
return contract;
|
|
1167
|
+
}
|
|
1168
|
+
effectValue = contract.data;
|
|
1169
|
+
}
|
|
1170
|
+
const settled = settleWithRefresh("succeeded", settle("succeeded"));
|
|
1171
|
+
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1172
|
+
return {
|
|
1173
|
+
ok: true,
|
|
1174
|
+
schemaVersion: 1,
|
|
1175
|
+
revision: settled.revision,
|
|
1176
|
+
workspaceRevision: settled.workspaceRevision,
|
|
1177
|
+
data: effectValue as T,
|
|
1178
|
+
};
|
|
1179
|
+
};
|
|
1180
|
+
let result: ExternalActionResult<T>;
|
|
1035
1181
|
try {
|
|
1036
|
-
|
|
1182
|
+
result = withActionLock ? await withActionLock(runEffect) : await runEffect();
|
|
1037
1183
|
} catch {
|
|
1038
|
-
|
|
1184
|
+
if (effectStarted)
|
|
1185
|
+
return failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
1186
|
+
operation: "external_action",
|
|
1187
|
+
outcome: "unknown",
|
|
1188
|
+
});
|
|
1189
|
+
const settled = settleWithRefresh("not_started", settle("not_started"));
|
|
1039
1190
|
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1040
|
-
return failure("
|
|
1041
|
-
|
|
1042
|
-
outcome: "unknown",
|
|
1191
|
+
return failure("writer_conflict", "target checkout was unavailable before the effect", {
|
|
1192
|
+
outcome: "not_started",
|
|
1043
1193
|
});
|
|
1044
1194
|
}
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
"ok" in effectValue &&
|
|
1049
|
-
typeof (effectValue as { ok?: unknown }).ok === "boolean"
|
|
1050
|
-
) {
|
|
1051
|
-
const contract = effectValue as Result<T>;
|
|
1052
|
-
if (!contract.ok) {
|
|
1053
|
-
const preflight =
|
|
1054
|
-
typeof contract.details === "object" &&
|
|
1055
|
-
contract.details !== null &&
|
|
1056
|
-
(contract.details as Record<string, unknown>).outcome === "not_started";
|
|
1057
|
-
const outcome = input.failureOutcome ?? (preflight ? "not_started" : "unknown");
|
|
1058
|
-
const settled = settleWithRefresh(outcome, settle(outcome));
|
|
1059
|
-
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1060
|
-
return contract;
|
|
1061
|
-
}
|
|
1062
|
-
effectValue = contract.data;
|
|
1195
|
+
if (!effectStarted) {
|
|
1196
|
+
const settled = settleWithRefresh("not_started", settle("not_started"));
|
|
1197
|
+
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1063
1198
|
}
|
|
1064
|
-
|
|
1065
|
-
if (!settled.ok) return settled as ExternalActionResult<T>;
|
|
1066
|
-
return {
|
|
1067
|
-
ok: true,
|
|
1068
|
-
schemaVersion: 1,
|
|
1069
|
-
revision: settled.revision,
|
|
1070
|
-
workspaceRevision: settled.workspaceRevision,
|
|
1071
|
-
data: effectValue as T,
|
|
1072
|
-
};
|
|
1199
|
+
return result;
|
|
1073
1200
|
}
|