@brainervirus/workit-core 1.0.9 → 1.0.11
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-plan/SKILL.md +14 -2
- package/src/core/authority.ts +21 -2
- package/src/core/branch.ts +45 -14
- package/src/core/external-action-effects.ts +205 -9
- package/src/core/external-action.ts +338 -11
- package/src/core/methods.ts +13 -2
- package/src/core/policy-resolver.ts +2 -2
- package/src/core/route-intent.ts +28 -0
- package/src/core/task-context.ts +2 -5
- package/src/core/task-contract.ts +58 -3
- package/src/core/task-engine.ts +60 -45
- package/src/core/task-evaluation.ts +36 -7
- package/src/core/task-store.ts +97 -17
- package/src/core/workers.ts +6 -0
- package/src/core.ts +11 -2
package/src/core/task-engine.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from "./task-context";
|
|
59
59
|
import {
|
|
60
60
|
assertProductWriteAllowed,
|
|
61
|
+
isUncertainWorker,
|
|
61
62
|
type CallerContext,
|
|
62
63
|
type NativeWorkerObservation,
|
|
63
64
|
type NativeWorkerVerification,
|
|
@@ -360,11 +361,16 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
|
|
|
360
361
|
return failure("invalid_transition", "worker has not started");
|
|
361
362
|
if (input.state === "running" && entry.data.state === "stopped")
|
|
362
363
|
return failure("invalid_transition", "stopped worker cannot run again");
|
|
364
|
+
const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
|
|
363
365
|
const nextEntry = {
|
|
364
366
|
...entry,
|
|
365
367
|
recordedAt: input.now ?? entry.recordedAt,
|
|
366
368
|
provenance: authority.provenance,
|
|
367
|
-
data: {
|
|
369
|
+
data: {
|
|
370
|
+
...entry.data,
|
|
371
|
+
state: terminalCancel ? ("cancelling" as const) : input.state,
|
|
372
|
+
session: input.session,
|
|
373
|
+
},
|
|
368
374
|
} satisfies Entry<Worker>;
|
|
369
375
|
const shouldClear =
|
|
370
376
|
input.state === "stopped" &&
|
|
@@ -381,7 +387,7 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
|
|
|
381
387
|
// revision bump per event livelocks worker sessions — each call would
|
|
382
388
|
// invalidate the revision the previous call returned.
|
|
383
389
|
if (
|
|
384
|
-
entry.data.state ===
|
|
390
|
+
entry.data.state === nextEntry.data.state &&
|
|
385
391
|
sameValue(entry.data.session, input.session) &&
|
|
386
392
|
!shouldClear &&
|
|
387
393
|
!shouldUncertain
|
|
@@ -823,11 +829,7 @@ export class WorkitCore {
|
|
|
823
829
|
) {
|
|
824
830
|
if (workspace.writer)
|
|
825
831
|
return failure("recovery_required", "writer ownership must be released first");
|
|
826
|
-
if (
|
|
827
|
-
task.workers.some((entry) =>
|
|
828
|
-
["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state),
|
|
829
|
-
)
|
|
830
|
-
)
|
|
832
|
+
if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
831
833
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
832
834
|
return null;
|
|
833
835
|
}
|
|
@@ -1063,18 +1065,6 @@ export class WorkitCore {
|
|
|
1063
1065
|
(this.context.workerId ?? null) === null ? null : this.helperEntry(task.data, true, true);
|
|
1064
1066
|
if (helper && !helper.ok) return helper as Result<never>;
|
|
1065
1067
|
const evidence = input.evidence as Evidence;
|
|
1066
|
-
if (helper?.ok) {
|
|
1067
|
-
const assignment = helper.data.data.assignment;
|
|
1068
|
-
if (
|
|
1069
|
-
evidence.requirementIds.some((id: string) => !assignment.requirementIds.includes(id)) ||
|
|
1070
|
-
(assignment.candidateId !== null &&
|
|
1071
|
-
evidence.candidateId !== assignment.candidateId &&
|
|
1072
|
-
evidence.beforeCandidateId !== assignment.candidateId) ||
|
|
1073
|
-
!refsWithinScope(evidence.refs, assignment.scope) ||
|
|
1074
|
-
!refsWithinScope(evidence.refs, task.data.intent.data.scope)
|
|
1075
|
-
)
|
|
1076
|
-
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
1077
|
-
}
|
|
1078
1068
|
if (
|
|
1079
1069
|
(evidence.result === "missing" || evidence.result === "skipped") &&
|
|
1080
1070
|
!evidence.summary.trim()
|
|
@@ -1100,6 +1090,18 @@ export class WorkitCore {
|
|
|
1100
1090
|
evidence.beforeCandidateId ??= null;
|
|
1101
1091
|
evidence.candidateId ??= null;
|
|
1102
1092
|
}
|
|
1093
|
+
if (helper?.ok) {
|
|
1094
|
+
const assignment = helper.data.data.assignment;
|
|
1095
|
+
if (
|
|
1096
|
+
evidence.requirementIds.some((id: string) => !assignment.requirementIds.includes(id)) ||
|
|
1097
|
+
(assignment.candidateId !== null &&
|
|
1098
|
+
evidence.candidateId !== assignment.candidateId &&
|
|
1099
|
+
evidence.beforeCandidateId !== assignment.candidateId) ||
|
|
1100
|
+
!refsWithinScope(evidence.refs, assignment.scope) ||
|
|
1101
|
+
!refsWithinScope(evidence.refs, task.data.intent.data.scope)
|
|
1102
|
+
)
|
|
1103
|
+
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
1104
|
+
}
|
|
1103
1105
|
const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
|
|
1104
1106
|
for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
|
|
1105
1107
|
if (
|
|
@@ -1187,7 +1189,15 @@ export class WorkitCore {
|
|
|
1187
1189
|
"permission_denied",
|
|
1188
1190
|
"native observation is only valid for recording decisions",
|
|
1189
1191
|
);
|
|
1190
|
-
|
|
1192
|
+
// Stated choices carry user-settled decisions without a native receipt.
|
|
1193
|
+
// They never authorize mutating actions; native verification is skipped
|
|
1194
|
+
// and provenance stays agent-reported.
|
|
1195
|
+
const stated = input.action === "record" && input.response === "stated";
|
|
1196
|
+
if (stated && input.purpose === "action")
|
|
1197
|
+
return failure("permission_denied", "stated choices cannot authorize mutating actions");
|
|
1198
|
+
if (stated && !input.binding?.statedChoice)
|
|
1199
|
+
return failure("invalid_input", "stated choices require binding.statedChoice");
|
|
1200
|
+
if (nativeRequired && nativeObservation === undefined && !stated)
|
|
1191
1201
|
return failure("permission_denied", "native decision observation is required");
|
|
1192
1202
|
const task = this.store.readTask(input.taskId);
|
|
1193
1203
|
if (!task.ok) return task as Result<never>;
|
|
@@ -1218,26 +1228,27 @@ export class WorkitCore {
|
|
|
1218
1228
|
consumption: null,
|
|
1219
1229
|
} satisfies Omit<Decision, "digest">;
|
|
1220
1230
|
const data: Decision = { ...base, digest: decisionDigest(base) };
|
|
1221
|
-
const native =
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1231
|
+
const native =
|
|
1232
|
+
nativeRequired && !stated
|
|
1233
|
+
? verifyNativeDecision(
|
|
1234
|
+
this.context.nativeAuthority,
|
|
1235
|
+
{
|
|
1236
|
+
observation: nativeObservation,
|
|
1237
|
+
expected: {
|
|
1238
|
+
taskId: task.data.id,
|
|
1239
|
+
workspaceId: workspace.data.id,
|
|
1240
|
+
purpose: input.purpose,
|
|
1241
|
+
response: input.response,
|
|
1242
|
+
binding: input.binding,
|
|
1243
|
+
bindingBytes: canonicalJson(input.binding),
|
|
1244
|
+
digest: data.digest,
|
|
1245
|
+
requirementIds: [...input.requirementIds],
|
|
1246
|
+
},
|
|
1247
|
+
caller: this.context.caller,
|
|
1235
1248
|
},
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
)
|
|
1240
|
-
: null;
|
|
1249
|
+
{ owner: this.authorityOwner, store: this.store, root: this.store.root },
|
|
1250
|
+
)
|
|
1251
|
+
: null;
|
|
1241
1252
|
if (native && !native.ok) return native as Result<never>;
|
|
1242
1253
|
const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
|
|
1243
1254
|
if (native?.ok && !nativeProvenance)
|
|
@@ -2242,6 +2253,9 @@ export class WorkitCore {
|
|
|
2242
2253
|
id: task.id,
|
|
2243
2254
|
revision: task.revision,
|
|
2244
2255
|
workspaceRevision: workspace.data.revision,
|
|
2256
|
+
createdAt: task.createdAt,
|
|
2257
|
+
updatedAt: task.updatedAt,
|
|
2258
|
+
runtime: task.runtime ?? null,
|
|
2245
2259
|
objective: task.intent.data.objective,
|
|
2246
2260
|
status: task.status,
|
|
2247
2261
|
closure: task.closure,
|
|
@@ -2344,9 +2358,7 @@ export class WorkitCore {
|
|
|
2344
2358
|
const base = reconcileResumeContext(effectiveView);
|
|
2345
2359
|
if (!base.ok) return base;
|
|
2346
2360
|
const needsReconciliation = view.task.workers.some((entry) =>
|
|
2347
|
-
|
|
2348
|
-
observedStates.get(entry.id) ?? entry.data.state,
|
|
2349
|
-
),
|
|
2361
|
+
isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state),
|
|
2350
2362
|
);
|
|
2351
2363
|
const blockers = [...base.data.blockers];
|
|
2352
2364
|
if (
|
|
@@ -2438,8 +2450,8 @@ export class WorkitCore {
|
|
|
2438
2450
|
!current.candidates.some((candidate) => candidate.id === resumeCandidate!.id)
|
|
2439
2451
|
? [...current.candidates, resumeCandidate]
|
|
2440
2452
|
: current.candidates,
|
|
2441
|
-
progress:
|
|
2442
|
-
|
|
2453
|
+
progress: current.progress,
|
|
2454
|
+
pauseReason: status === "paused" ? (input.reason ?? null) : null,
|
|
2443
2455
|
}),
|
|
2444
2456
|
});
|
|
2445
2457
|
if (!changed.ok) return changed as Result<never>;
|
|
@@ -2475,7 +2487,10 @@ export class WorkitCore {
|
|
|
2475
2487
|
const workspace = this.store.readWorkspace();
|
|
2476
2488
|
if (!workspace.ok) return workspace as Result<never>;
|
|
2477
2489
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
2478
|
-
if (
|
|
2490
|
+
if (
|
|
2491
|
+
workspace.data.writer ||
|
|
2492
|
+
task.data.workers.some((entry) => isUncertainWorker(entry.data.state))
|
|
2493
|
+
)
|
|
2479
2494
|
return failure(
|
|
2480
2495
|
"recovery_required",
|
|
2481
2496
|
"scope revision requires worker ownership reconciliation",
|
|
@@ -154,14 +154,23 @@ const scopeRoots = (root: string, scope: Scope): Result<string[]> => {
|
|
|
154
154
|
const roots: string[] = [];
|
|
155
155
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
156
156
|
const target = path.resolve(root, item);
|
|
157
|
-
if (!inside(root, target))
|
|
157
|
+
if (!inside(root, target))
|
|
158
|
+
return failure(
|
|
159
|
+
"invalid_input",
|
|
160
|
+
`candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`,
|
|
161
|
+
{ fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] },
|
|
162
|
+
);
|
|
158
163
|
let ancestor = target;
|
|
159
164
|
while (!fs.existsSync(ancestor) && ancestor !== root) ancestor = path.dirname(ancestor);
|
|
160
165
|
try {
|
|
161
166
|
if (!inside(root, fs.realpathSync(ancestor)))
|
|
162
|
-
return failure(
|
|
167
|
+
return failure(
|
|
168
|
+
"invalid_input",
|
|
169
|
+
`candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`,
|
|
170
|
+
{ fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] },
|
|
171
|
+
);
|
|
163
172
|
} catch {
|
|
164
|
-
return failure("invalid_input",
|
|
173
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
165
174
|
}
|
|
166
175
|
roots.push(target);
|
|
167
176
|
}
|
|
@@ -180,7 +189,12 @@ export function captureCandidate(
|
|
|
180
189
|
return failure("invalid_input", "candidate root does not exist");
|
|
181
190
|
}
|
|
182
191
|
const normalizedScope = canonicalScope(scope);
|
|
183
|
-
if (!normalizedScope)
|
|
192
|
+
if (!normalizedScope)
|
|
193
|
+
return failure(
|
|
194
|
+
"invalid_input",
|
|
195
|
+
"candidate scope is invalid: paths must be checkout-relative — to work in another repository, keep this task here and coordinate a separate linked task in that checkout",
|
|
196
|
+
{ fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] },
|
|
197
|
+
);
|
|
184
198
|
const roots = scopeRoots(checkout, normalizedScope);
|
|
185
199
|
if (!roots.ok) return roots;
|
|
186
200
|
const git = gitPaths(checkout);
|
|
@@ -399,7 +413,7 @@ const applicableRequirementDecision = (
|
|
|
399
413
|
({ data, provenance }) =>
|
|
400
414
|
provenance.kind !== "imported" &&
|
|
401
415
|
data.purpose !== "limitation" &&
|
|
402
|
-
data.response === "approved" &&
|
|
416
|
+
(data.response === "approved" || data.response === "stated") &&
|
|
403
417
|
data.revoked === null &&
|
|
404
418
|
data.binding.taskId === task.id &&
|
|
405
419
|
data.binding.workspaceId === workspace.id &&
|
|
@@ -461,7 +475,10 @@ export function evaluateRequirements(
|
|
|
461
475
|
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
462
476
|
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
463
477
|
const sameEvidenceSession = task.evidence.some(
|
|
464
|
-
(other) =>
|
|
478
|
+
(other) =>
|
|
479
|
+
other.id !== entry.id &&
|
|
480
|
+
other.data.kind === "review" &&
|
|
481
|
+
sameSession(reviewSession, other.provenance.session),
|
|
465
482
|
);
|
|
466
483
|
return (
|
|
467
484
|
entry.data.kind === "review" &&
|
|
@@ -536,7 +553,7 @@ export function evaluateRequirements(
|
|
|
536
553
|
status: "satisfied" as const,
|
|
537
554
|
evidenceIds: [],
|
|
538
555
|
decisionIds: decisions.map((decision) => decision.id),
|
|
539
|
-
reason: "an applicable approved decision satisfies the requirement",
|
|
556
|
+
reason: "an applicable approved or stated decision satisfies the requirement",
|
|
540
557
|
};
|
|
541
558
|
const limitations = requirement.acceptanceAllowed
|
|
542
559
|
? applicableDecision(task, workspace, requirement, checkoutRoot)
|
|
@@ -641,6 +658,18 @@ export function evaluateClosure(
|
|
|
641
658
|
if (requestedOutcome !== "stopped" && blocking.length)
|
|
642
659
|
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
643
660
|
requirementIds: blocking.map((item) => item.requirementId),
|
|
661
|
+
requirements: blocking.map((item) => {
|
|
662
|
+
const requirement = view.task.policy?.requirements.find(
|
|
663
|
+
(candidate) => candidate.id === item.requirementId,
|
|
664
|
+
);
|
|
665
|
+
return {
|
|
666
|
+
requirementId: item.requirementId,
|
|
667
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
668
|
+
reason: item.reason,
|
|
669
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
670
|
+
dependentAction: requirement?.dependentAction ?? null,
|
|
671
|
+
};
|
|
672
|
+
}),
|
|
644
673
|
});
|
|
645
674
|
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
646
675
|
if (requestedOutcome === "verified" && accepted.length)
|
package/src/core/task-store.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import * as z from "zod";
|
|
6
|
+
import { packageRoot } from "./package-root";
|
|
6
7
|
import {
|
|
7
8
|
acquireFileLockSync,
|
|
8
9
|
type FileLockSyncHandle,
|
|
@@ -109,6 +110,43 @@ export type RecoveryCandidate = {
|
|
|
109
110
|
};
|
|
110
111
|
|
|
111
112
|
const now = (): Utc => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
113
|
+
|
|
114
|
+
let cachedRuntimeVersion: string | null = null;
|
|
115
|
+
/** Version of the package that hosts this runtime; recorded with state so a
|
|
116
|
+
* bug report can identify the writer version without reading raw .workit. */
|
|
117
|
+
export const runtimeVersion = (): string => {
|
|
118
|
+
if (cachedRuntimeVersion === null) {
|
|
119
|
+
try {
|
|
120
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot(), "package.json"), "utf8")) as {
|
|
121
|
+
version?: unknown;
|
|
122
|
+
};
|
|
123
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
124
|
+
} catch {
|
|
125
|
+
cachedRuntimeVersion = "0.0.0";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return cachedRuntimeVersion;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const stampNew = (record: { runtime?: { createdWith: string | null } }) => ({
|
|
132
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
133
|
+
updatedWith: runtimeVersion(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const isNewerVersion = (candidate: string, current: string): boolean => {
|
|
137
|
+
const parts = (value: string) =>
|
|
138
|
+
value
|
|
139
|
+
.split("-")[0]
|
|
140
|
+
.split(".")
|
|
141
|
+
.map((item) => Number.parseInt(item, 10) || 0);
|
|
142
|
+
const [next, now] = [parts(candidate), parts(current)];
|
|
143
|
+
for (let index = 0; index < 3; index += 1) {
|
|
144
|
+
const left = next[index] ?? 0;
|
|
145
|
+
const right = now[index] ?? 0;
|
|
146
|
+
if (left !== right) return left > right;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
};
|
|
112
150
|
const jsonBytes = (value: unknown): string => `${canonicalJson(value)}\n`;
|
|
113
151
|
const digestBytes = (value: string | Buffer): string =>
|
|
114
152
|
createHash("sha256").update(value).digest("hex");
|
|
@@ -221,10 +259,14 @@ export class TaskStore {
|
|
|
221
259
|
? value.expectedWorkspaceRevision !== current.data.revision
|
|
222
260
|
: value.expectedWorkspaceRevision !== null
|
|
223
261
|
) {
|
|
224
|
-
return failure(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
262
|
+
return failure(
|
|
263
|
+
"revision_conflict",
|
|
264
|
+
"workspace revision does not match; omit expectedWorkspaceRevision to use the current record",
|
|
265
|
+
{
|
|
266
|
+
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
267
|
+
actualWorkspaceRevision: current.data?.revision ?? null,
|
|
268
|
+
},
|
|
269
|
+
);
|
|
228
270
|
}
|
|
229
271
|
if (!value.provenance) return failure("invalid_input", "provenance is required");
|
|
230
272
|
if (
|
|
@@ -236,12 +278,18 @@ export class TaskStore {
|
|
|
236
278
|
if (current.data && !previousWorkspaceBytes)
|
|
237
279
|
return failure("storage_error", "workspace snapshot disappeared during creation");
|
|
238
280
|
const workspace: WorkspaceRecord = current.data
|
|
239
|
-
? {
|
|
281
|
+
? {
|
|
282
|
+
...current.data,
|
|
283
|
+
revision: newRevision(),
|
|
284
|
+
root: this.root,
|
|
285
|
+
runtime: stampNew(current.data),
|
|
286
|
+
}
|
|
240
287
|
: {
|
|
241
288
|
schemaVersion: SCHEMA_VERSION,
|
|
242
289
|
id: newId(),
|
|
243
290
|
revision: newRevision(),
|
|
244
291
|
root: this.root,
|
|
292
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
245
293
|
writer: null,
|
|
246
294
|
};
|
|
247
295
|
const timestamp = value.now ?? now();
|
|
@@ -263,6 +311,7 @@ export class TaskStore {
|
|
|
263
311
|
status: "active",
|
|
264
312
|
closure: null,
|
|
265
313
|
progress: { summary: "", nextAction: null, blockers: [] },
|
|
314
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
266
315
|
assessments: [],
|
|
267
316
|
policy: null,
|
|
268
317
|
policyChanges: [],
|
|
@@ -312,21 +361,31 @@ export class TaskStore {
|
|
|
312
361
|
? input.expectedWorkspaceRevision !== current.data.revision
|
|
313
362
|
: input.expectedWorkspaceRevision !== null
|
|
314
363
|
)
|
|
315
|
-
return failure(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
364
|
+
return failure(
|
|
365
|
+
"revision_conflict",
|
|
366
|
+
"workspace revision does not match; omit expectedWorkspaceRevision to use the current record",
|
|
367
|
+
{
|
|
368
|
+
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
369
|
+
actualWorkspaceRevision: current.data?.revision ?? null,
|
|
370
|
+
},
|
|
371
|
+
);
|
|
319
372
|
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
320
373
|
if (current.data && !previousWorkspaceBytes)
|
|
321
374
|
return failure("storage_error", "workspace snapshot disappeared during import");
|
|
322
375
|
const timestamp = input.now ?? now();
|
|
323
376
|
const workspace: WorkspaceRecord = current.data
|
|
324
|
-
? {
|
|
377
|
+
? {
|
|
378
|
+
...current.data,
|
|
379
|
+
revision: newRevision(),
|
|
380
|
+
root: this.root,
|
|
381
|
+
runtime: stampNew(current.data),
|
|
382
|
+
}
|
|
325
383
|
: {
|
|
326
384
|
schemaVersion: SCHEMA_VERSION,
|
|
327
385
|
id: input.workspaceId ?? newId(),
|
|
328
386
|
revision: newRevision(),
|
|
329
387
|
root: this.root,
|
|
388
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
330
389
|
writer: null,
|
|
331
390
|
};
|
|
332
391
|
const task = {
|
|
@@ -335,6 +394,10 @@ export class TaskStore {
|
|
|
335
394
|
revision: newRevision(),
|
|
336
395
|
createdAt: timestamp,
|
|
337
396
|
updatedAt: timestamp,
|
|
397
|
+
runtime: {
|
|
398
|
+
createdWith: input.task.runtime?.createdWith ?? null,
|
|
399
|
+
updatedWith: runtimeVersion(),
|
|
400
|
+
},
|
|
338
401
|
};
|
|
339
402
|
const validTask = taskRecordSchema.safeParse(task);
|
|
340
403
|
if (!validTask.success)
|
|
@@ -402,6 +465,7 @@ export class TaskStore {
|
|
|
402
465
|
createdAt: current.data.createdAt,
|
|
403
466
|
revision: context.revision,
|
|
404
467
|
updatedAt: context.now,
|
|
468
|
+
runtime: stampNew(current.data),
|
|
405
469
|
};
|
|
406
470
|
const valid = taskRecordSchema.safeParse(record);
|
|
407
471
|
if (!valid.success)
|
|
@@ -433,6 +497,7 @@ export class TaskStore {
|
|
|
433
497
|
id: current.data.id,
|
|
434
498
|
root: this.root,
|
|
435
499
|
revision: context.revision,
|
|
500
|
+
runtime: stampNew(current.data),
|
|
436
501
|
};
|
|
437
502
|
const valid = workspaceRecordSchema.safeParse(record);
|
|
438
503
|
if (!valid.success)
|
|
@@ -472,6 +537,7 @@ export class TaskStore {
|
|
|
472
537
|
id: workspace.data.id,
|
|
473
538
|
root: this.root,
|
|
474
539
|
revision: workspaceContext.revision,
|
|
540
|
+
runtime: stampNew(workspace.data),
|
|
475
541
|
});
|
|
476
542
|
if (!nextWorkspace.success)
|
|
477
543
|
return failure("invalid_input", "workspace mutation produced an invalid record");
|
|
@@ -508,6 +574,7 @@ export class TaskStore {
|
|
|
508
574
|
createdAt: task.data.createdAt,
|
|
509
575
|
revision: taskContext.revision,
|
|
510
576
|
updatedAt: taskContext.now,
|
|
577
|
+
runtime: stampNew(task.data),
|
|
511
578
|
});
|
|
512
579
|
if (!nextTask.success) {
|
|
513
580
|
this.markUncertain(nextWorkspace.data);
|
|
@@ -785,6 +852,7 @@ export class TaskStore {
|
|
|
785
852
|
...workspace,
|
|
786
853
|
writer: { ...workspace.writer, state: "uncertain" as const },
|
|
787
854
|
revision: newRevision(),
|
|
855
|
+
runtime: stampNew(workspace),
|
|
788
856
|
};
|
|
789
857
|
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
790
858
|
}
|
|
@@ -1027,9 +1095,17 @@ export class TaskStore {
|
|
|
1027
1095
|
if (isObject(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
1028
1096
|
return failure("unsupported_version", "unsupported snapshot schema version");
|
|
1029
1097
|
const parsed = schema.safeParse(value);
|
|
1030
|
-
return parsed.
|
|
1031
|
-
|
|
1032
|
-
|
|
1098
|
+
if (parsed.success) return success(null, null, parsed.data);
|
|
1099
|
+
const writerVersion =
|
|
1100
|
+
isObject(value) && isObject((value as { runtime?: unknown }).runtime)
|
|
1101
|
+
? (value as { runtime: { updatedWith?: unknown } }).runtime.updatedWith
|
|
1102
|
+
: null;
|
|
1103
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
1104
|
+
return failure(
|
|
1105
|
+
"recovery_required",
|
|
1106
|
+
`snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`,
|
|
1107
|
+
);
|
|
1108
|
+
return failure("recovery_required", "snapshot does not satisfy its schema");
|
|
1033
1109
|
}
|
|
1034
1110
|
|
|
1035
1111
|
private processStart(pid: number): string | null {
|
|
@@ -1041,10 +1117,14 @@ export class TaskStore {
|
|
|
1041
1117
|
}
|
|
1042
1118
|
|
|
1043
1119
|
private conflict(expected: Revision, actual: Revision): Result<never> {
|
|
1044
|
-
return failure(
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1120
|
+
return failure(
|
|
1121
|
+
"revision_conflict",
|
|
1122
|
+
"snapshot revision does not match; omit expectedRevision to use the current record",
|
|
1123
|
+
{
|
|
1124
|
+
expectedRevision: expected,
|
|
1125
|
+
actualRevision: actual,
|
|
1126
|
+
},
|
|
1127
|
+
);
|
|
1048
1128
|
}
|
|
1049
1129
|
|
|
1050
1130
|
private taskPath(taskId: Id) {
|
package/src/core/workers.ts
CHANGED
|
@@ -93,6 +93,12 @@ const same = (left: unknown, right: unknown): boolean => {
|
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
/** The one shared definition of active or uncertain worker state used by
|
|
97
|
+
* close, revise, resume, and replacement-launch checks. */
|
|
98
|
+
export const UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"] as const;
|
|
99
|
+
export const isUncertainWorker = (state: string): boolean =>
|
|
100
|
+
(UNCERTAIN_WORKER_STATES as readonly string[]).includes(state);
|
|
101
|
+
|
|
96
102
|
const callerSession = (caller: CallerContext): HostSession =>
|
|
97
103
|
caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
98
104
|
|
package/src/core.ts
CHANGED
|
@@ -22,6 +22,8 @@ export {
|
|
|
22
22
|
decisionDigest,
|
|
23
23
|
candidateDigest,
|
|
24
24
|
requirementId,
|
|
25
|
+
BINDING_QUESTION_BUDGET,
|
|
26
|
+
workitBindingQuestionIssue,
|
|
25
27
|
} from "./core/task-contract";
|
|
26
28
|
export type {
|
|
27
29
|
OperationFamily,
|
|
@@ -55,7 +57,7 @@ export type {
|
|
|
55
57
|
ExportBundle,
|
|
56
58
|
Result as ContractResult,
|
|
57
59
|
} from "./core/task-contract";
|
|
58
|
-
export { TaskStore } from "./core/task-store";
|
|
60
|
+
export { TaskStore, runtimeVersion } from "./core/task-store";
|
|
59
61
|
export type { MetadataLock, ProcessEvidence, RecoveryInput } from "./core/task-store";
|
|
60
62
|
export { compactTaskContext, reconcileResume } from "./core/task-context";
|
|
61
63
|
export type {
|
|
@@ -66,7 +68,7 @@ export type {
|
|
|
66
68
|
} from "./core/task-context";
|
|
67
69
|
export { METHODS, invariantBootstrap, selectMethods } from "./core/methods";
|
|
68
70
|
export type { MethodId, SelectedMethod } from "./core/methods";
|
|
69
|
-
export { shellRouteIntent } from "./core/route-intent";
|
|
71
|
+
export { hasLiveWorkitTask, shellRouteIntent, shouldDenyShellRoute } from "./core/route-intent";
|
|
70
72
|
export type { ShellRouteIntent } from "./core/route-intent";
|
|
71
73
|
export { applicableDecision, reserveAction, settleAction, reconcileAction } from "./core/authority";
|
|
72
74
|
export {
|
|
@@ -84,9 +86,16 @@ export type {
|
|
|
84
86
|
ExternalActionOperation,
|
|
85
87
|
} from "./core/external-action";
|
|
86
88
|
export {
|
|
89
|
+
approvedBranchSetupIntent,
|
|
87
90
|
approvedExternalAction,
|
|
91
|
+
approvedPlanCommit,
|
|
92
|
+
branchSetupIntent,
|
|
93
|
+
planCommitDescriptor,
|
|
94
|
+
planCommitBinding,
|
|
95
|
+
commitMessageFromDescriptor,
|
|
88
96
|
externalActionState,
|
|
89
97
|
priorExternalAction,
|
|
98
|
+
priorResolvedDrift,
|
|
90
99
|
externalActionRef,
|
|
91
100
|
externalActionDescriptor,
|
|
92
101
|
externalActionHelp,
|