@brainervirus/workit-cursor 1.0.8 → 1.0.10
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/.cursor-plugin/plugin.json +1 -1
- package/dist/cursor-session-start.js +388 -195
- package/dist/mcp-server.js +364 -221
- package/dist/workit-hook.js +386 -194
- package/package.json +3 -3
- package/skills/workit-babysit/SKILL.md +3 -1
- package/skills/workit-plan/SKILL.md +14 -2
package/dist/workit-hook.js
CHANGED
|
@@ -3,8 +3,8 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// packages/workit-cursor/hooks/workit-hook.ts
|
|
6
|
-
import { existsSync as
|
|
7
|
-
import
|
|
6
|
+
import { existsSync as existsSync4, realpathSync as realpathSync5 } from "node:fs";
|
|
7
|
+
import path7 from "node:path";
|
|
8
8
|
|
|
9
9
|
// packages/workit-core/src/core/task-contract.ts
|
|
10
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -7573,6 +7573,7 @@ var decisionSchema = object({
|
|
|
7573
7573
|
scope: scopeSchema,
|
|
7574
7574
|
presented: text,
|
|
7575
7575
|
approvedContent: text,
|
|
7576
|
+
displayed: text.optional(),
|
|
7576
7577
|
contentRefs: array(refSchema)
|
|
7577
7578
|
}).strict(),
|
|
7578
7579
|
digest,
|
|
@@ -7611,7 +7612,7 @@ var workerReportSchema = object({
|
|
|
7611
7612
|
}).strict();
|
|
7612
7613
|
var workerSchema = object({
|
|
7613
7614
|
assignment: assignmentSchema,
|
|
7614
|
-
state: _enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
|
|
7615
|
+
state: _enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
|
|
7615
7616
|
session: refSchema.nullable(),
|
|
7616
7617
|
report: workerReportSchema.nullable()
|
|
7617
7618
|
}).strict();
|
|
@@ -7651,6 +7652,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
|
|
|
7651
7652
|
path: ["decisionId"]
|
|
7652
7653
|
});
|
|
7653
7654
|
});
|
|
7655
|
+
var runtimeSchema = object({
|
|
7656
|
+
createdWith: text.nullable(),
|
|
7657
|
+
updatedWith: text
|
|
7658
|
+
}).strict();
|
|
7654
7659
|
var taskRecordSchema = object({
|
|
7655
7660
|
schemaVersion: literal(1),
|
|
7656
7661
|
id,
|
|
@@ -7664,6 +7669,8 @@ var taskRecordSchema = object({
|
|
|
7664
7669
|
status: _enum(["active", "paused", "closed"]),
|
|
7665
7670
|
closure: closureSchema.nullable(),
|
|
7666
7671
|
progress: progressSchema,
|
|
7672
|
+
pauseReason: text.nullable().optional(),
|
|
7673
|
+
runtime: runtimeSchema.optional(),
|
|
7667
7674
|
assessments: array(entrySchema(assessmentSchema)),
|
|
7668
7675
|
policy: policySchema.nullable(),
|
|
7669
7676
|
policyChanges: array(policyChangeSchema),
|
|
@@ -7679,6 +7686,7 @@ var workspaceRecordSchema = object({
|
|
|
7679
7686
|
id,
|
|
7680
7687
|
revision,
|
|
7681
7688
|
root: nonEmpty,
|
|
7689
|
+
runtime: runtimeSchema.optional(),
|
|
7682
7690
|
writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
|
|
7683
7691
|
}).strict();
|
|
7684
7692
|
var capabilitySchema = object({
|
|
@@ -7706,6 +7714,9 @@ var taskSummarySchema = object({
|
|
|
7706
7714
|
id,
|
|
7707
7715
|
revision,
|
|
7708
7716
|
workspaceRevision: revision,
|
|
7717
|
+
createdAt: utc,
|
|
7718
|
+
updatedAt: utc,
|
|
7719
|
+
runtime: runtimeSchema.nullable(),
|
|
7709
7720
|
objective: text,
|
|
7710
7721
|
status: _enum(["active", "paused", "closed"]),
|
|
7711
7722
|
closure: closureSchema.nullable(),
|
|
@@ -7776,7 +7787,7 @@ var taskOperations = {
|
|
|
7776
7787
|
expectedWorkspaceRevision: revision.optional(),
|
|
7777
7788
|
outcome: outcomeSchema,
|
|
7778
7789
|
summary: text,
|
|
7779
|
-
decisionIds: array(id)
|
|
7790
|
+
decisionIds: array(id).optional()
|
|
7780
7791
|
})
|
|
7781
7792
|
};
|
|
7782
7793
|
var policyOperations = {
|
|
@@ -7868,14 +7879,15 @@ var writerOperations = {
|
|
|
7868
7879
|
...taskId,
|
|
7869
7880
|
expectedRevision: revision.optional(),
|
|
7870
7881
|
expectedWorkspaceRevision: revision.optional(),
|
|
7871
|
-
workerId: nullableId.optional()
|
|
7882
|
+
workerId: nullableId.optional(),
|
|
7883
|
+
reason: text.optional()
|
|
7872
7884
|
}),
|
|
7873
7885
|
release: operation({
|
|
7874
7886
|
action: literal("release"),
|
|
7875
7887
|
...taskId,
|
|
7876
7888
|
expectedRevision: revision.optional(),
|
|
7877
7889
|
expectedWorkspaceRevision: revision.optional(),
|
|
7878
|
-
reason: text
|
|
7890
|
+
reason: text.optional()
|
|
7879
7891
|
})
|
|
7880
7892
|
};
|
|
7881
7893
|
var stateOperations = {
|
|
@@ -8026,7 +8038,21 @@ function requirementId(input) {
|
|
|
8026
8038
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
8027
8039
|
import * as fs4 from "node:fs";
|
|
8028
8040
|
import { hostname } from "node:os";
|
|
8029
|
-
import
|
|
8041
|
+
import path4 from "node:path";
|
|
8042
|
+
|
|
8043
|
+
// packages/workit-core/src/core/package-root.ts
|
|
8044
|
+
import { existsSync } from "node:fs";
|
|
8045
|
+
import path from "node:path";
|
|
8046
|
+
import { fileURLToPath } from "node:url";
|
|
8047
|
+
var packageRoot = () => {
|
|
8048
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
8049
|
+
while (true) {
|
|
8050
|
+
const parent = path.dirname(dir);
|
|
8051
|
+
if (existsSync(path.join(dir, "package.json")) || parent === dir)
|
|
8052
|
+
return dir;
|
|
8053
|
+
dir = parent;
|
|
8054
|
+
}
|
|
8055
|
+
};
|
|
8030
8056
|
|
|
8031
8057
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
8032
8058
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -8077,7 +8103,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
8077
8103
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
8078
8104
|
import { randomBytes } from "node:crypto";
|
|
8079
8105
|
import fsSync from "node:fs";
|
|
8080
|
-
import
|
|
8106
|
+
import path2 from "node:path";
|
|
8081
8107
|
|
|
8082
8108
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
8083
8109
|
import fs from "node:fs";
|
|
@@ -8167,13 +8193,13 @@ ${ownershipToken}
|
|
|
8167
8193
|
return { raw, ownershipToken };
|
|
8168
8194
|
}
|
|
8169
8195
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
8170
|
-
const resolved =
|
|
8171
|
-
const lexicalRelative =
|
|
8172
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
8173
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
8196
|
+
const resolved = path2.resolve(lockPath);
|
|
8197
|
+
const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
|
|
8198
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
|
|
8199
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
|
|
8174
8200
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
8175
8201
|
}
|
|
8176
|
-
return relative.split(
|
|
8202
|
+
return relative.split(path2.sep).join(path2.posix.sep);
|
|
8177
8203
|
}
|
|
8178
8204
|
function parseSidecarLockPayload(raw, parser) {
|
|
8179
8205
|
if (parser) {
|
|
@@ -8320,7 +8346,7 @@ function getFsSafeLockConfig() {
|
|
|
8320
8346
|
|
|
8321
8347
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
8322
8348
|
import fs3 from "node:fs";
|
|
8323
|
-
import
|
|
8349
|
+
import path3 from "node:path";
|
|
8324
8350
|
|
|
8325
8351
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
8326
8352
|
function sleepSync(ms) {
|
|
@@ -8411,26 +8437,26 @@ function canonicalLockParentSync(parent) {
|
|
|
8411
8437
|
return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
|
|
8412
8438
|
}
|
|
8413
8439
|
function normalizeTargetPath(targetPath) {
|
|
8414
|
-
const resolved =
|
|
8415
|
-
fs3.mkdirSync(
|
|
8440
|
+
const resolved = path3.resolve(targetPath);
|
|
8441
|
+
fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
8416
8442
|
try {
|
|
8417
|
-
return
|
|
8443
|
+
return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
|
|
8418
8444
|
} catch {
|
|
8419
8445
|
return resolved;
|
|
8420
8446
|
}
|
|
8421
8447
|
}
|
|
8422
8448
|
function boundedLockPath(lockPath, lockRoot) {
|
|
8423
|
-
const resolved =
|
|
8449
|
+
const resolved = path3.resolve(lockPath);
|
|
8424
8450
|
if (!lockRoot)
|
|
8425
8451
|
return resolved;
|
|
8426
8452
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
8427
|
-
const parent =
|
|
8453
|
+
const parent = path3.dirname(resolved);
|
|
8428
8454
|
const parentReal = canonicalLockParentSync(parent);
|
|
8429
|
-
const parentRelative =
|
|
8430
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
8455
|
+
const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
|
|
8456
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
|
|
8431
8457
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
8432
8458
|
}
|
|
8433
|
-
return
|
|
8459
|
+
return path3.join(parentReal, path3.basename(resolved));
|
|
8434
8460
|
}
|
|
8435
8461
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
8436
8462
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -8643,6 +8669,33 @@ var metadataLockSchema = object({
|
|
|
8643
8669
|
nonce: string2().min(1)
|
|
8644
8670
|
}).strict();
|
|
8645
8671
|
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8672
|
+
var cachedRuntimeVersion = null;
|
|
8673
|
+
var runtimeVersion = () => {
|
|
8674
|
+
if (cachedRuntimeVersion === null) {
|
|
8675
|
+
try {
|
|
8676
|
+
const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
|
|
8677
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
8678
|
+
} catch {
|
|
8679
|
+
cachedRuntimeVersion = "0.0.0";
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
return cachedRuntimeVersion;
|
|
8683
|
+
};
|
|
8684
|
+
var stampNew = (record) => ({
|
|
8685
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
8686
|
+
updatedWith: runtimeVersion()
|
|
8687
|
+
});
|
|
8688
|
+
var isNewerVersion = (candidate, current) => {
|
|
8689
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
8690
|
+
const [next, now] = [parts(candidate), parts(current)];
|
|
8691
|
+
for (let index = 0;index < 3; index += 1) {
|
|
8692
|
+
const left = next[index] ?? 0;
|
|
8693
|
+
const right = now[index] ?? 0;
|
|
8694
|
+
if (left !== right)
|
|
8695
|
+
return left > right;
|
|
8696
|
+
}
|
|
8697
|
+
return false;
|
|
8698
|
+
};
|
|
8646
8699
|
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
8647
8700
|
`;
|
|
8648
8701
|
var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
|
|
@@ -8669,12 +8722,12 @@ var sameMetadataLock = (left, right) => {
|
|
|
8669
8722
|
class TaskStore {
|
|
8670
8723
|
root;
|
|
8671
8724
|
constructor(root) {
|
|
8672
|
-
this.root = fs4.existsSync(root) ? fs4.realpathSync(root) :
|
|
8725
|
+
this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
|
|
8673
8726
|
}
|
|
8674
8727
|
readTask(taskId) {
|
|
8675
8728
|
if (!validId(taskId))
|
|
8676
8729
|
return failure("invalid_input", "task ID is invalid", { taskId });
|
|
8677
|
-
const result = this.readRecord(
|
|
8730
|
+
const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
|
|
8678
8731
|
if (!result.exists)
|
|
8679
8732
|
return failure("not_found", "task not found", { taskId });
|
|
8680
8733
|
if (result.result.ok && result.result.data.id !== taskId)
|
|
@@ -8708,7 +8761,7 @@ class TaskStore {
|
|
|
8708
8761
|
});
|
|
8709
8762
|
const tasks = [];
|
|
8710
8763
|
for (const name of names.sort()) {
|
|
8711
|
-
const item = this.readRecord(
|
|
8764
|
+
const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
|
|
8712
8765
|
if (!item.exists)
|
|
8713
8766
|
continue;
|
|
8714
8767
|
if (!item.result.ok)
|
|
@@ -8742,7 +8795,7 @@ class TaskStore {
|
|
|
8742
8795
|
if (!current.ok)
|
|
8743
8796
|
return current;
|
|
8744
8797
|
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
8745
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
8798
|
+
return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
8746
8799
|
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
8747
8800
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
8748
8801
|
});
|
|
@@ -8754,11 +8807,17 @@ class TaskStore {
|
|
|
8754
8807
|
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
8755
8808
|
if (current.data && !previousWorkspaceBytes)
|
|
8756
8809
|
return failure("storage_error", "workspace snapshot disappeared during creation");
|
|
8757
|
-
const workspace = current.data ? {
|
|
8810
|
+
const workspace = current.data ? {
|
|
8811
|
+
...current.data,
|
|
8812
|
+
revision: newRevision(),
|
|
8813
|
+
root: this.root,
|
|
8814
|
+
runtime: stampNew(current.data)
|
|
8815
|
+
} : {
|
|
8758
8816
|
schemaVersion: SCHEMA_VERSION,
|
|
8759
8817
|
id: newId(),
|
|
8760
8818
|
revision: newRevision(),
|
|
8761
8819
|
root: this.root,
|
|
8820
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8762
8821
|
writer: null
|
|
8763
8822
|
};
|
|
8764
8823
|
const timestamp = value.now ?? now();
|
|
@@ -8780,6 +8839,7 @@ class TaskStore {
|
|
|
8780
8839
|
status: "active",
|
|
8781
8840
|
closure: null,
|
|
8782
8841
|
progress: { summary: "", nextAction: null, blockers: [] },
|
|
8842
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8783
8843
|
assessments: [],
|
|
8784
8844
|
policy: null,
|
|
8785
8845
|
policyChanges: [],
|
|
@@ -8810,7 +8870,7 @@ class TaskStore {
|
|
|
8810
8870
|
if (!current.ok)
|
|
8811
8871
|
return current;
|
|
8812
8872
|
if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
|
|
8813
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
8873
|
+
return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
8814
8874
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
8815
8875
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
8816
8876
|
});
|
|
@@ -8818,11 +8878,17 @@ class TaskStore {
|
|
|
8818
8878
|
if (current.data && !previousWorkspaceBytes)
|
|
8819
8879
|
return failure("storage_error", "workspace snapshot disappeared during import");
|
|
8820
8880
|
const timestamp = input.now ?? now();
|
|
8821
|
-
const workspace = current.data ? {
|
|
8881
|
+
const workspace = current.data ? {
|
|
8882
|
+
...current.data,
|
|
8883
|
+
revision: newRevision(),
|
|
8884
|
+
root: this.root,
|
|
8885
|
+
runtime: stampNew(current.data)
|
|
8886
|
+
} : {
|
|
8822
8887
|
schemaVersion: SCHEMA_VERSION,
|
|
8823
8888
|
id: input.workspaceId ?? newId(),
|
|
8824
8889
|
revision: newRevision(),
|
|
8825
8890
|
root: this.root,
|
|
8891
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8826
8892
|
writer: null
|
|
8827
8893
|
};
|
|
8828
8894
|
const task = {
|
|
@@ -8830,7 +8896,11 @@ class TaskStore {
|
|
|
8830
8896
|
workspaceId: workspace.id,
|
|
8831
8897
|
revision: newRevision(),
|
|
8832
8898
|
createdAt: timestamp,
|
|
8833
|
-
updatedAt: timestamp
|
|
8899
|
+
updatedAt: timestamp,
|
|
8900
|
+
runtime: {
|
|
8901
|
+
createdWith: input.task.runtime?.createdWith ?? null,
|
|
8902
|
+
updatedWith: runtimeVersion()
|
|
8903
|
+
}
|
|
8834
8904
|
};
|
|
8835
8905
|
const validTask = taskRecordSchema.safeParse(task);
|
|
8836
8906
|
if (!validTask.success)
|
|
@@ -8879,7 +8949,8 @@ class TaskStore {
|
|
|
8879
8949
|
workspaceId: current.data.workspaceId,
|
|
8880
8950
|
createdAt: current.data.createdAt,
|
|
8881
8951
|
revision: context.revision,
|
|
8882
|
-
updatedAt: context.now
|
|
8952
|
+
updatedAt: context.now,
|
|
8953
|
+
runtime: stampNew(current.data)
|
|
8883
8954
|
};
|
|
8884
8955
|
const valid = taskRecordSchema.safeParse(record);
|
|
8885
8956
|
if (!valid.success)
|
|
@@ -8913,7 +8984,8 @@ class TaskStore {
|
|
|
8913
8984
|
...changed.data,
|
|
8914
8985
|
id: current.data.id,
|
|
8915
8986
|
root: this.root,
|
|
8916
|
-
revision: context.revision
|
|
8987
|
+
revision: context.revision,
|
|
8988
|
+
runtime: stampNew(current.data)
|
|
8917
8989
|
};
|
|
8918
8990
|
const valid = workspaceRecordSchema.safeParse(record);
|
|
8919
8991
|
if (!valid.success)
|
|
@@ -8956,7 +9028,8 @@ class TaskStore {
|
|
|
8956
9028
|
...changedWorkspace.data,
|
|
8957
9029
|
id: workspace.data.id,
|
|
8958
9030
|
root: this.root,
|
|
8959
|
-
revision: workspaceContext.revision
|
|
9031
|
+
revision: workspaceContext.revision,
|
|
9032
|
+
runtime: stampNew(workspace.data)
|
|
8960
9033
|
});
|
|
8961
9034
|
if (!nextWorkspace.success)
|
|
8962
9035
|
return failure("invalid_input", "workspace mutation produced an invalid record");
|
|
@@ -8981,7 +9054,8 @@ class TaskStore {
|
|
|
8981
9054
|
workspaceId: task.data.workspaceId,
|
|
8982
9055
|
createdAt: task.data.createdAt,
|
|
8983
9056
|
revision: taskContext.revision,
|
|
8984
|
-
updatedAt: taskContext.now
|
|
9057
|
+
updatedAt: taskContext.now,
|
|
9058
|
+
runtime: stampNew(task.data)
|
|
8985
9059
|
});
|
|
8986
9060
|
if (!nextTask.success) {
|
|
8987
9061
|
this.markUncertain(nextWorkspace.data);
|
|
@@ -9008,7 +9082,7 @@ class TaskStore {
|
|
|
9008
9082
|
if (match)
|
|
9009
9083
|
candidates.push({
|
|
9010
9084
|
target: match[1],
|
|
9011
|
-
path:
|
|
9085
|
+
path: path4.join(this.recoveryDir, name),
|
|
9012
9086
|
digest: match[3]
|
|
9013
9087
|
});
|
|
9014
9088
|
}
|
|
@@ -9201,7 +9275,8 @@ class TaskStore {
|
|
|
9201
9275
|
const value = {
|
|
9202
9276
|
...workspace,
|
|
9203
9277
|
writer: { ...workspace.writer, state: "uncertain" },
|
|
9204
|
-
revision: newRevision()
|
|
9278
|
+
revision: newRevision(),
|
|
9279
|
+
runtime: stampNew(workspace)
|
|
9205
9280
|
};
|
|
9206
9281
|
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
9207
9282
|
}
|
|
@@ -9279,7 +9354,7 @@ class TaskStore {
|
|
|
9279
9354
|
}
|
|
9280
9355
|
fs4.renameSync(temporary, file);
|
|
9281
9356
|
temporary = undefined;
|
|
9282
|
-
this.fsyncDirectory(
|
|
9357
|
+
this.fsyncDirectory(path4.dirname(file));
|
|
9283
9358
|
return success(null, null, value);
|
|
9284
9359
|
} catch (error) {
|
|
9285
9360
|
return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
|
|
@@ -9293,9 +9368,9 @@ class TaskStore {
|
|
|
9293
9368
|
}
|
|
9294
9369
|
}
|
|
9295
9370
|
saveRecovery(file, bytes) {
|
|
9296
|
-
const target =
|
|
9297
|
-
const id = target === "task" ?
|
|
9298
|
-
const destination =
|
|
9371
|
+
const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
9372
|
+
const id = target === "task" ? path4.basename(file, ".json") : "workspace";
|
|
9373
|
+
const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
9299
9374
|
if (fs4.existsSync(destination)) {
|
|
9300
9375
|
if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
|
|
9301
9376
|
return;
|
|
@@ -9373,7 +9448,7 @@ class TaskStore {
|
|
|
9373
9448
|
for (const name of fs4.readdirSync(this.tasksDir)) {
|
|
9374
9449
|
if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
|
|
9375
9450
|
return null;
|
|
9376
|
-
const parsed = this.parseBytes(fs4.readFileSync(
|
|
9451
|
+
const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
|
|
9377
9452
|
if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
|
|
9378
9453
|
return null;
|
|
9379
9454
|
ids.add(parsed.data.workspaceId);
|
|
@@ -9415,7 +9490,12 @@ class TaskStore {
|
|
|
9415
9490
|
if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
9416
9491
|
return failure("unsupported_version", "unsupported snapshot schema version");
|
|
9417
9492
|
const parsed = schema.safeParse(value);
|
|
9418
|
-
|
|
9493
|
+
if (parsed.success)
|
|
9494
|
+
return success(null, null, parsed.data);
|
|
9495
|
+
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
9496
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
9497
|
+
return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
9498
|
+
return failure("recovery_required", "snapshot does not satisfy its schema");
|
|
9419
9499
|
}
|
|
9420
9500
|
processStart(pid) {
|
|
9421
9501
|
try {
|
|
@@ -9425,43 +9505,98 @@ class TaskStore {
|
|
|
9425
9505
|
}
|
|
9426
9506
|
}
|
|
9427
9507
|
conflict(expected, actual) {
|
|
9428
|
-
return failure("revision_conflict", "snapshot revision does not match", {
|
|
9508
|
+
return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
9429
9509
|
expectedRevision: expected,
|
|
9430
9510
|
actualRevision: actual
|
|
9431
9511
|
});
|
|
9432
9512
|
}
|
|
9433
9513
|
taskPath(taskId) {
|
|
9434
|
-
return
|
|
9514
|
+
return path4.join(this.tasksDir, `${taskId}.json`);
|
|
9435
9515
|
}
|
|
9436
9516
|
get workitDir() {
|
|
9437
|
-
return
|
|
9517
|
+
return path4.join(this.root, ".workit");
|
|
9438
9518
|
}
|
|
9439
9519
|
get tasksDir() {
|
|
9440
|
-
return
|
|
9520
|
+
return path4.join(this.workitDir, "tasks");
|
|
9441
9521
|
}
|
|
9442
9522
|
get recoveryDir() {
|
|
9443
|
-
return
|
|
9523
|
+
return path4.join(this.workitDir, "recovery");
|
|
9444
9524
|
}
|
|
9445
9525
|
get workspacePath() {
|
|
9446
|
-
return
|
|
9526
|
+
return path4.join(this.workitDir, "workspace.json");
|
|
9447
9527
|
}
|
|
9448
9528
|
get lockPath() {
|
|
9449
|
-
return
|
|
9529
|
+
return path4.join(this.workitDir, "metadata.lock");
|
|
9450
9530
|
}
|
|
9451
9531
|
get gitignorePath() {
|
|
9452
|
-
return
|
|
9532
|
+
return path4.join(this.workitDir, ".gitignore");
|
|
9453
9533
|
}
|
|
9454
9534
|
}
|
|
9535
|
+
// packages/workit-core/src/core/workers.ts
|
|
9536
|
+
var same = (left, right) => {
|
|
9537
|
+
try {
|
|
9538
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
9539
|
+
} catch {
|
|
9540
|
+
return false;
|
|
9541
|
+
}
|
|
9542
|
+
};
|
|
9543
|
+
var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
|
|
9544
|
+
var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
|
|
9545
|
+
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
9546
|
+
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
9547
|
+
const code = char.charCodeAt(0);
|
|
9548
|
+
return code < 32 || code === 127;
|
|
9549
|
+
}) && !value.split(/[\\/]/).includes("..");
|
|
9550
|
+
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
9551
|
+
var workerIdOf = (value) => value.workerId ?? null;
|
|
9552
|
+
function assertProductWriteAllowed(input) {
|
|
9553
|
+
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
9554
|
+
return failure("invalid_input", "invalid product write path");
|
|
9555
|
+
if (!input.store)
|
|
9556
|
+
return failure("permission_denied", "write authorization requires core state");
|
|
9557
|
+
const task = input.store.readTask(input.task.id);
|
|
9558
|
+
if (!task.ok)
|
|
9559
|
+
return task;
|
|
9560
|
+
const workspace = input.store.readWorkspace();
|
|
9561
|
+
if (!workspace.ok)
|
|
9562
|
+
return workspace;
|
|
9563
|
+
if (!workspace.data)
|
|
9564
|
+
return failure("not_found", "workspace not found");
|
|
9565
|
+
const workspaceRecord = workspace.data;
|
|
9566
|
+
if (task.data.status !== "active")
|
|
9567
|
+
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
9568
|
+
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
9569
|
+
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
9570
|
+
const writer = workspaceRecord.writer;
|
|
9571
|
+
if (!writer)
|
|
9572
|
+
return failure("permission_denied", "checkout has no writer owner");
|
|
9573
|
+
if (writer.state === "uncertain")
|
|
9574
|
+
return failure("recovery_required", "checkout writer requires recovery", {
|
|
9575
|
+
owner: writer.owner
|
|
9576
|
+
});
|
|
9577
|
+
const caller = callerValue(input.caller);
|
|
9578
|
+
const workerId = workerIdOf(input.caller);
|
|
9579
|
+
const owner = writer.owner;
|
|
9580
|
+
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
9581
|
+
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
9582
|
+
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
9583
|
+
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
9584
|
+
return failure("permission_denied", "only implementers can own product writes");
|
|
9585
|
+
if (workerId !== null && (!worker?.data.session || worker.data.state === "cancelling" || worker.data.state === "unknown" || worker.data.state === "stopped" || !same(worker.data.session, callerSession(input.caller))))
|
|
9586
|
+
return failure("recovery_required", "worker session is not an active writer");
|
|
9587
|
+
return success(null, workspaceRecord.revision, owner);
|
|
9588
|
+
}
|
|
9589
|
+
|
|
9455
9590
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
9456
9591
|
import { createHash as createHash4 } from "node:crypto";
|
|
9457
9592
|
import * as fs6 from "node:fs";
|
|
9458
9593
|
import { spawnSync } from "node:child_process";
|
|
9459
|
-
import
|
|
9594
|
+
import path6 from "node:path";
|
|
9460
9595
|
|
|
9461
9596
|
// packages/workit-core/src/core/authority.ts
|
|
9462
9597
|
import { createHash as createHash3 } from "node:crypto";
|
|
9463
9598
|
import * as fs5 from "node:fs";
|
|
9464
|
-
import
|
|
9599
|
+
import path5 from "node:path";
|
|
9465
9600
|
var verifiedAuthorities = new WeakMap;
|
|
9466
9601
|
var trustedAuthority = (kind, provenance, expected, binding, caller) => {
|
|
9467
9602
|
const token = {};
|
|
@@ -9588,20 +9723,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
9588
9723
|
continue;
|
|
9589
9724
|
if (!reference.digest)
|
|
9590
9725
|
return failure("invalid_input", "document references require a byte digest");
|
|
9591
|
-
const target =
|
|
9592
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
9593
|
-
return failure("invalid_input", "document reference escapes checkout"
|
|
9726
|
+
const target = path5.resolve(root, reference.path);
|
|
9727
|
+
if (target !== root && !target.startsWith(`${root}${path5.sep}`))
|
|
9728
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
9729
|
+
fields: [
|
|
9730
|
+
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
9731
|
+
]
|
|
9732
|
+
});
|
|
9594
9733
|
try {
|
|
9595
|
-
const relative =
|
|
9734
|
+
const relative = path5.relative(root, target);
|
|
9596
9735
|
let current = root;
|
|
9597
|
-
for (const segment of relative.split(
|
|
9736
|
+
for (const segment of relative.split(path5.sep)) {
|
|
9598
9737
|
if (!segment)
|
|
9599
9738
|
continue;
|
|
9600
|
-
current =
|
|
9739
|
+
current = path5.join(current, segment);
|
|
9601
9740
|
const stat = fs5.lstatSync(current);
|
|
9602
9741
|
const real = fs5.realpathSync(current);
|
|
9603
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
9604
|
-
return failure("invalid_input", "document reference escapes checkout"
|
|
9742
|
+
if (real !== root && !real.startsWith(`${root}${path5.sep}`))
|
|
9743
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
9744
|
+
fields: [
|
|
9745
|
+
{
|
|
9746
|
+
path: "contentRefs.path",
|
|
9747
|
+
reason: `${reference.path} resolves outside the checkout`
|
|
9748
|
+
}
|
|
9749
|
+
]
|
|
9750
|
+
});
|
|
9605
9751
|
if (stat.isSymbolicLink())
|
|
9606
9752
|
return failure("invalid_input", "document reference uses a symlink");
|
|
9607
9753
|
if (current === target && !stat.isFile())
|
|
@@ -9948,14 +10094,14 @@ var verifyDecisionContent = verifyContentRefs;
|
|
|
9948
10094
|
|
|
9949
10095
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
9950
10096
|
var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
|
|
9951
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
9952
|
-
var relative = (root, candidate) =>
|
|
10097
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
|
|
10098
|
+
var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
|
|
9953
10099
|
var canonicalPath = (value) => {
|
|
9954
10100
|
if (typeof value !== "string" || !value)
|
|
9955
10101
|
return null;
|
|
9956
10102
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
9957
10103
|
return null;
|
|
9958
|
-
const normalized =
|
|
10104
|
+
const normalized = path6.posix.normalize(value);
|
|
9959
10105
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
9960
10106
|
return null;
|
|
9961
10107
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -10003,14 +10149,14 @@ var gitPaths = (root) => {
|
|
|
10003
10149
|
git: /not a git repository/i.test(stderr) ? false : true
|
|
10004
10150
|
};
|
|
10005
10151
|
}
|
|
10006
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
10152
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
|
|
10007
10153
|
const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
10008
10154
|
if (stagedDeleted.status !== 0) {
|
|
10009
10155
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
10010
10156
|
if (!/not a git repository/i.test(stderr))
|
|
10011
10157
|
return { paths, uncertain: true, git: true };
|
|
10012
10158
|
} else if (stagedDeleted.stdout) {
|
|
10013
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
10159
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
|
|
10014
10160
|
}
|
|
10015
10161
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
10016
10162
|
};
|
|
@@ -10025,7 +10171,7 @@ var walk = (root, directory, output) => {
|
|
|
10025
10171
|
for (const name of names) {
|
|
10026
10172
|
if (name === ".git" || name === ".workit")
|
|
10027
10173
|
continue;
|
|
10028
|
-
const target =
|
|
10174
|
+
const target = path6.join(directory, name);
|
|
10029
10175
|
let stat;
|
|
10030
10176
|
try {
|
|
10031
10177
|
stat = fs6.lstatSync(target);
|
|
@@ -10043,17 +10189,17 @@ var walk = (root, directory, output) => {
|
|
|
10043
10189
|
var scopeRoots = (root, scope) => {
|
|
10044
10190
|
const roots = [];
|
|
10045
10191
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
10046
|
-
const target =
|
|
10192
|
+
const target = path6.resolve(root, item);
|
|
10047
10193
|
if (!inside(root, target))
|
|
10048
|
-
return failure("invalid_input",
|
|
10194
|
+
return failure("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
|
|
10049
10195
|
let ancestor = target;
|
|
10050
10196
|
while (!fs6.existsSync(ancestor) && ancestor !== root)
|
|
10051
|
-
ancestor =
|
|
10197
|
+
ancestor = path6.dirname(ancestor);
|
|
10052
10198
|
try {
|
|
10053
10199
|
if (!inside(root, fs6.realpathSync(ancestor)))
|
|
10054
|
-
return failure("invalid_input",
|
|
10200
|
+
return failure("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
|
|
10055
10201
|
} catch {
|
|
10056
|
-
return failure("invalid_input",
|
|
10202
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
10057
10203
|
}
|
|
10058
10204
|
roots.push(target);
|
|
10059
10205
|
}
|
|
@@ -10068,7 +10214,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
10068
10214
|
}
|
|
10069
10215
|
const normalizedScope = canonicalScope(scope);
|
|
10070
10216
|
if (!normalizedScope)
|
|
10071
|
-
return failure("invalid_input", "candidate scope is invalid");
|
|
10217
|
+
return failure("invalid_input", "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", { fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] });
|
|
10072
10218
|
const roots = scopeRoots(checkout, normalizedScope);
|
|
10073
10219
|
if (!roots.ok)
|
|
10074
10220
|
return roots;
|
|
@@ -10100,7 +10246,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
10100
10246
|
for (const item of [...names].sort()) {
|
|
10101
10247
|
if (!scopeMatches(item, normalizedScope))
|
|
10102
10248
|
continue;
|
|
10103
|
-
const target =
|
|
10249
|
+
const target = path6.join(checkout, item);
|
|
10104
10250
|
try {
|
|
10105
10251
|
const stat = fs6.lstatSync(target);
|
|
10106
10252
|
if (stat.isSymbolicLink()) {
|
|
@@ -10257,7 +10403,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
10257
10403
|
return true;
|
|
10258
10404
|
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
10259
10405
|
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
10260
|
-
const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
|
|
10406
|
+
const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
|
|
10261
10407
|
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
10262
10408
|
});
|
|
10263
10409
|
if (passed.length) {
|
|
@@ -10353,7 +10499,17 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10353
10499
|
const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
|
|
10354
10500
|
if (requestedOutcome !== "stopped" && blocking.length)
|
|
10355
10501
|
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
10356
|
-
requirementIds: blocking.map((item) => item.requirementId)
|
|
10502
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
10503
|
+
requirements: blocking.map((item) => {
|
|
10504
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
10505
|
+
return {
|
|
10506
|
+
requirementId: item.requirementId,
|
|
10507
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
10508
|
+
reason: item.reason,
|
|
10509
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
10510
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
10511
|
+
};
|
|
10512
|
+
})
|
|
10357
10513
|
});
|
|
10358
10514
|
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
10359
10515
|
if (requestedOutcome === "verified" && accepted.length)
|
|
@@ -10380,6 +10536,106 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10380
10536
|
});
|
|
10381
10537
|
}
|
|
10382
10538
|
|
|
10539
|
+
// packages/workit-core/src/core/methods.ts
|
|
10540
|
+
var METHODS = {
|
|
10541
|
+
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
10542
|
+
"workit-behavioral-tdd": { dimensions: ["testing"] },
|
|
10543
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
|
|
10544
|
+
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
10545
|
+
"workit-implement": { dimensions: ["delegation"] },
|
|
10546
|
+
"workit-debug": { ruleIds: ["root-cause-investigation"] },
|
|
10547
|
+
"workit-handoff": { ruleIds: ["durable-handoff"] },
|
|
10548
|
+
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
10549
|
+
};
|
|
10550
|
+
var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
10551
|
+
var assuranceFor = (id, definition, capabilities) => {
|
|
10552
|
+
const keys = new Set([
|
|
10553
|
+
id,
|
|
10554
|
+
...definition.dimensions ?? [],
|
|
10555
|
+
...definition.ruleIds ?? []
|
|
10556
|
+
]);
|
|
10557
|
+
const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
|
|
10558
|
+
if (id === "workit-review" && matches.length === 0)
|
|
10559
|
+
return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
|
|
10560
|
+
if (matches.some((capability) => capability.assurance === "unavailable"))
|
|
10561
|
+
return {
|
|
10562
|
+
assurance: "unavailable",
|
|
10563
|
+
reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
|
|
10564
|
+
};
|
|
10565
|
+
if (matches.some((capability) => capability.assurance === "enforced"))
|
|
10566
|
+
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
10567
|
+
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
10568
|
+
};
|
|
10569
|
+
function selectMethods(policy, capabilities) {
|
|
10570
|
+
return Object.entries(METHODS).flatMap(([id, definition]) => {
|
|
10571
|
+
const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
|
|
10572
|
+
if (requirements.length === 0)
|
|
10573
|
+
return [];
|
|
10574
|
+
const capability = assuranceFor(id, definition, capabilities);
|
|
10575
|
+
const reasons = [
|
|
10576
|
+
...new Set(requirements.map((requirement) => requirement.reason)),
|
|
10577
|
+
...capability.reasons
|
|
10578
|
+
];
|
|
10579
|
+
if (capability.assurance === "unavailable")
|
|
10580
|
+
reasons.unshift("required capability is unavailable");
|
|
10581
|
+
return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
|
|
10582
|
+
});
|
|
10583
|
+
}
|
|
10584
|
+
var invariantBootstrap = () => `
|
|
10585
|
+
Workit keeps one accountable lead and one shared task state. Inspect current task
|
|
10586
|
+
state before acting. An empty task.list means no session yet, not permission to
|
|
10587
|
+
skip Workit; for user-requested product, debug, or behavior work with no active
|
|
10588
|
+
or paused task, run task.start then policy.assess before other product mutations.
|
|
10589
|
+
Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
|
|
10590
|
+
to the current records; explicit values are still concurrency-checked, so never
|
|
10591
|
+
copy revisions between calls. Happy path: task.list, task.start {intent} with no
|
|
10592
|
+
revisions, policy.assess {assessment: {facts, signals, consequences,
|
|
10593
|
+
verification}} where facts are inferred or observed with file refs,
|
|
10594
|
+
writer.acquire {taskId} before product writes, evidence.record {evidence} where
|
|
10595
|
+
check and review kinds auto-bind the current tree and need no digests. Record
|
|
10596
|
+
self-review requirements with kind review and reviewContext matching your own
|
|
10597
|
+
host session; the lead may also record checks. Fresh-context review requires a
|
|
10598
|
+
session distinct from the task creator and every other evidence recorder. Record
|
|
10599
|
+
verification evidence last: later evidence or commits can reopen fixed findings
|
|
10600
|
+
for re-verification, so verify, resolve, then close with nothing in between.
|
|
10601
|
+
decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
10602
|
+
approvedContent} only through a native approval question asked receipt-shaped:
|
|
10603
|
+
header Workit decision: <purpose>, the same label repeated in the question
|
|
10604
|
+
text (some host UIs do not render headers), exactly two options approved and
|
|
10605
|
+
rejected; ask once, never re-ask to mint a receipt. Before asking, present the
|
|
10606
|
+
item in the conversation (durable artifacts: complete digest plus exact path;
|
|
10607
|
+
inline plans: the content) and keep the question to one short scoped sentence;
|
|
10608
|
+
never ask to approve something unseen. An answer with custom text is steering,
|
|
10609
|
+
not approval: adjust, re-present, then ask a new scoped question. After the user
|
|
10610
|
+
approves a spec or plan, execute the remaining tasks continuously with one
|
|
10611
|
+
atomic commit per task and do not ask whether to continue; stop only for a new
|
|
10612
|
+
product decision, a failed safety or verification gate, a conflicting edit, or
|
|
10613
|
+
missing authority. Record the plan's commit list once through the plan-scoped
|
|
10614
|
+
action approval (git.commit with plan_steps and plan_branch) so each listed
|
|
10615
|
+
commit executes without a new question; an unlisted message needs a fresh
|
|
10616
|
+
approval. task.close {outcome,
|
|
10617
|
+
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
10618
|
+
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
10619
|
+
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
10620
|
+
without a bound candidate goes stale; findings close only as fixed with passing
|
|
10621
|
+
verification, dismissed with supporting evidence, or deferred under an approved
|
|
10622
|
+
limitation. Use only the
|
|
10623
|
+
shared operations for task, policy, evidence, finding, decision, worker, writer,
|
|
10624
|
+
and state changes. Authority is bounded by the requested scope, current
|
|
10625
|
+
revision, caller/session provenance, and observed capabilities. Never claim host
|
|
10626
|
+
enforcement or evidence that the host cannot provide. Shell-executed writes are
|
|
10627
|
+
unattested agent-guided work even when a writer is held. Preserve unresolved
|
|
10628
|
+
requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
|
|
10629
|
+
always load on demand. Model-invoked: new instructions, interruptions, or
|
|
10630
|
+
forgotten items mid-task → load workit-steer before responding; about to open
|
|
10631
|
+
a PR or fresh from implementing → load workit-deslop; CI red → load
|
|
10632
|
+
workit-green-run; small-looking change before close or merge → load
|
|
10633
|
+
workit-blast-radius; ambiguous, consequential, or disputed proposal before
|
|
10634
|
+
recommending → load workit-challenge; spec or plan needs a flow diagram →
|
|
10635
|
+
load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
|
|
10636
|
+
act from memory of it.
|
|
10637
|
+
`.trim();
|
|
10638
|
+
|
|
10383
10639
|
// packages/workit-core/src/core/task-context.ts
|
|
10384
10640
|
function reconcileResume(view, observations = []) {
|
|
10385
10641
|
if (observations.length > 0)
|
|
@@ -10389,7 +10645,7 @@ function reconcileResume(view, observations = []) {
|
|
|
10389
10645
|
return candidate;
|
|
10390
10646
|
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
10391
10647
|
const blockers = [...view.task.progress.blockers];
|
|
10392
|
-
if (view.task.workers.some((entry) =>
|
|
10648
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
10393
10649
|
blockers.push({
|
|
10394
10650
|
reason: "worker state requires reconciliation",
|
|
10395
10651
|
dependentAction: "resume",
|
|
@@ -10439,6 +10695,11 @@ function compactTaskContext(view) {
|
|
|
10439
10695
|
digest: entry.data.digest,
|
|
10440
10696
|
references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
|
|
10441
10697
|
}));
|
|
10698
|
+
const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
|
|
10699
|
+
id: method.id,
|
|
10700
|
+
assurance: method.assurance,
|
|
10701
|
+
reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
|
|
10702
|
+
})) : [];
|
|
10442
10703
|
const gaps = Array.from(new Set([
|
|
10443
10704
|
...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
|
|
10444
10705
|
...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
|
|
@@ -10448,15 +10709,18 @@ function compactTaskContext(view) {
|
|
|
10448
10709
|
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
10449
10710
|
status: view.task.status,
|
|
10450
10711
|
decisions,
|
|
10712
|
+
methods,
|
|
10451
10713
|
gaps,
|
|
10452
10714
|
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
|
|
10453
10715
|
};
|
|
10454
10716
|
let encoded = JSON.stringify(context);
|
|
10455
|
-
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.gaps.length)) {
|
|
10717
|
+
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
|
|
10456
10718
|
if (context.gaps.length)
|
|
10457
10719
|
context.gaps.pop();
|
|
10458
|
-
else
|
|
10720
|
+
else if (context.decisions.length)
|
|
10459
10721
|
context.decisions.pop();
|
|
10722
|
+
else
|
|
10723
|
+
context.methods.pop();
|
|
10460
10724
|
encoded = JSON.stringify(context);
|
|
10461
10725
|
}
|
|
10462
10726
|
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
@@ -10472,50 +10736,6 @@ var exportDigest = (bundle) => sha256({
|
|
|
10472
10736
|
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
10473
10737
|
task: bundle.task
|
|
10474
10738
|
});
|
|
10475
|
-
// packages/workit-core/src/core/methods.ts
|
|
10476
|
-
var invariantBootstrap = () => `
|
|
10477
|
-
Workit keeps one accountable lead and one shared task state. Inspect current task
|
|
10478
|
-
state before acting. An empty task.list means no session yet, not permission to
|
|
10479
|
-
skip Workit; for user-requested product, debug, or behavior work with no active
|
|
10480
|
-
or paused task, run task.start then policy.assess before other product mutations.
|
|
10481
|
-
Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
|
|
10482
|
-
to the current records; explicit values are still concurrency-checked, so never
|
|
10483
|
-
copy revisions between calls. Happy path: task.list, task.start {intent} with no
|
|
10484
|
-
revisions, policy.assess {assessment: {facts, signals, consequences,
|
|
10485
|
-
verification}} where facts are inferred or observed with file refs,
|
|
10486
|
-
writer.acquire {taskId} before product writes, evidence.record {evidence} where
|
|
10487
|
-
check and review kinds auto-bind the current tree and need no digests. Record
|
|
10488
|
-
self-review requirements with kind review and reviewContext matching your own
|
|
10489
|
-
host session; the lead may also record checks. Fresh-context review requires a
|
|
10490
|
-
session distinct from the task creator and every other evidence recorder. Record
|
|
10491
|
-
verification evidence last: later evidence or commits can reopen fixed findings
|
|
10492
|
-
for re-verification, so verify, resolve, then close with nothing in between.
|
|
10493
|
-
decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
10494
|
-
approvedContent} only through a native approval question asked receipt-shaped:
|
|
10495
|
-
header Workit decision: <purpose>, the same label repeated in the question
|
|
10496
|
-
text (some host UIs do not render headers), exactly two options approved and
|
|
10497
|
-
rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
|
|
10498
|
-
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
10499
|
-
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
10500
|
-
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
10501
|
-
without a bound candidate goes stale; findings close only as fixed with passing
|
|
10502
|
-
verification, dismissed with supporting evidence, or deferred under an approved
|
|
10503
|
-
limitation. Use only the
|
|
10504
|
-
shared operations for task, policy, evidence, finding, decision, worker, writer,
|
|
10505
|
-
and state changes. Authority is bounded by the requested scope, current
|
|
10506
|
-
revision, caller/session provenance, and observed capabilities. Never claim host
|
|
10507
|
-
enforcement or evidence that the host cannot provide. Shell-executed writes are
|
|
10508
|
-
unattested agent-guided work even when a writer is held. Preserve unresolved
|
|
10509
|
-
requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
|
|
10510
|
-
always load on demand. Model-invoked: new instructions, interruptions, or
|
|
10511
|
-
forgotten items mid-task → load workit-steer before responding; about to open
|
|
10512
|
-
a PR or fresh from implementing → load workit-deslop; CI red → load
|
|
10513
|
-
workit-green-run; small-looking change before close or merge → load
|
|
10514
|
-
workit-blast-radius; ambiguous, consequential, or disputed proposal before
|
|
10515
|
-
recommending → load workit-challenge; spec or plan needs a flow diagram →
|
|
10516
|
-
load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
|
|
10517
|
-
act from memory of it.
|
|
10518
|
-
`.trim();
|
|
10519
10739
|
// packages/workit-core/src/core/external-action.ts
|
|
10520
10740
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
10521
10741
|
object({
|
|
@@ -10529,7 +10749,11 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
10529
10749
|
}).strict(),
|
|
10530
10750
|
object({
|
|
10531
10751
|
operation: literal("git.commit"),
|
|
10532
|
-
payload: object({
|
|
10752
|
+
payload: object({
|
|
10753
|
+
message: string2().min(1).optional(),
|
|
10754
|
+
plan_steps: array(string2().min(1)).optional(),
|
|
10755
|
+
plan_branch: string2().min(1).optional()
|
|
10756
|
+
}).strict()
|
|
10533
10757
|
}).strict(),
|
|
10534
10758
|
object({
|
|
10535
10759
|
operation: literal("git.push"),
|
|
@@ -10764,7 +10988,7 @@ function resolveRequirements(input) {
|
|
|
10764
10988
|
].join(", ")}`,
|
|
10765
10989
|
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
10766
10990
|
before: "dependent_action",
|
|
10767
|
-
dependentAction:
|
|
10991
|
+
dependentAction: null,
|
|
10768
10992
|
acceptanceAllowed: false
|
|
10769
10993
|
}));
|
|
10770
10994
|
if (signals.productChoiceOpen.value === true)
|
|
@@ -10775,7 +10999,7 @@ function resolveRequirements(input) {
|
|
|
10775
10999
|
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
10776
11000
|
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
10777
11001
|
before: "dependent_action",
|
|
10778
|
-
dependentAction:
|
|
11002
|
+
dependentAction: null,
|
|
10779
11003
|
acceptanceAllowed: false
|
|
10780
11004
|
}));
|
|
10781
11005
|
if (signals.behaviorChange.value === true)
|
|
@@ -10933,59 +11157,6 @@ function diffPolicy(previous, next, reason, now) {
|
|
|
10933
11157
|
return parsed.data;
|
|
10934
11158
|
}
|
|
10935
11159
|
|
|
10936
|
-
// packages/workit-core/src/core/workers.ts
|
|
10937
|
-
var same = (left, right) => {
|
|
10938
|
-
try {
|
|
10939
|
-
return canonicalJson(left) === canonicalJson(right);
|
|
10940
|
-
} catch {
|
|
10941
|
-
return false;
|
|
10942
|
-
}
|
|
10943
|
-
};
|
|
10944
|
-
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
10945
|
-
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
10946
|
-
const code = char.charCodeAt(0);
|
|
10947
|
-
return code < 32 || code === 127;
|
|
10948
|
-
}) && !value.split(/[\\/]/).includes("..");
|
|
10949
|
-
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
10950
|
-
var workerIdOf = (value) => value.workerId ?? null;
|
|
10951
|
-
function assertProductWriteAllowed(input) {
|
|
10952
|
-
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
10953
|
-
return failure("invalid_input", "invalid product write path");
|
|
10954
|
-
if (!input.store)
|
|
10955
|
-
return failure("permission_denied", "write authorization requires core state");
|
|
10956
|
-
const task = input.store.readTask(input.task.id);
|
|
10957
|
-
if (!task.ok)
|
|
10958
|
-
return task;
|
|
10959
|
-
const workspace = input.store.readWorkspace();
|
|
10960
|
-
if (!workspace.ok)
|
|
10961
|
-
return workspace;
|
|
10962
|
-
if (!workspace.data)
|
|
10963
|
-
return failure("not_found", "workspace not found");
|
|
10964
|
-
const workspaceRecord = workspace.data;
|
|
10965
|
-
if (task.data.status !== "active")
|
|
10966
|
-
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
10967
|
-
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
10968
|
-
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
10969
|
-
const writer = workspaceRecord.writer;
|
|
10970
|
-
if (!writer)
|
|
10971
|
-
return failure("permission_denied", "checkout has no writer owner");
|
|
10972
|
-
if (writer.state === "uncertain")
|
|
10973
|
-
return failure("recovery_required", "checkout writer requires recovery", {
|
|
10974
|
-
owner: writer.owner
|
|
10975
|
-
});
|
|
10976
|
-
const caller = callerValue(input.caller);
|
|
10977
|
-
const workerId = workerIdOf(input.caller);
|
|
10978
|
-
const owner = writer.owner;
|
|
10979
|
-
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
10980
|
-
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
10981
|
-
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
10982
|
-
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
10983
|
-
return failure("permission_denied", "only implementers can own product writes");
|
|
10984
|
-
if (workerId !== null && (!worker?.data.session || worker.data.state === "cancelling" || worker.data.state === "unknown" || worker.data.state === "stopped" || !same(worker.data.session, callerSession(input.caller))))
|
|
10985
|
-
return failure("recovery_required", "worker session is not an active writer");
|
|
10986
|
-
return success(null, workspaceRecord.revision, owner);
|
|
10987
|
-
}
|
|
10988
|
-
|
|
10989
11160
|
// packages/workit-core/src/core/task-engine.ts
|
|
10990
11161
|
var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
|
|
10991
11162
|
kind,
|
|
@@ -11104,10 +11275,10 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11104
11275
|
const notStarted = input.dispatch === true;
|
|
11105
11276
|
if (notStarted && (input.state !== "stopped" || input.session !== null))
|
|
11106
11277
|
return failure("invalid_input", "a never-dispatched worker stops with no session");
|
|
11107
|
-
if (notStarted &&
|
|
11108
|
-
return failure("invalid_transition", "worker is no longer awaiting dispatch");
|
|
11109
|
-
if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
|
|
11278
|
+
if (notStarted && entry.data.session !== null)
|
|
11110
11279
|
return failure("invalid_transition", "worker already has an observed session");
|
|
11280
|
+
if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
|
|
11281
|
+
return failure("invalid_transition", "worker launch was not claimed");
|
|
11111
11282
|
if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
|
|
11112
11283
|
return failure("invalid_input", "running worker observations require a session");
|
|
11113
11284
|
if (entry.data.session && !sameValue(entry.data.session, input.session))
|
|
@@ -11116,15 +11287,20 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11116
11287
|
return failure("invalid_transition", "worker has not started");
|
|
11117
11288
|
if (input.state === "running" && entry.data.state === "stopped")
|
|
11118
11289
|
return failure("invalid_transition", "stopped worker cannot run again");
|
|
11290
|
+
const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
|
|
11119
11291
|
const nextEntry = {
|
|
11120
11292
|
...entry,
|
|
11121
11293
|
recordedAt: input.now ?? entry.recordedAt,
|
|
11122
11294
|
provenance: authority.provenance,
|
|
11123
|
-
data: {
|
|
11295
|
+
data: {
|
|
11296
|
+
...entry.data,
|
|
11297
|
+
state: terminalCancel ? "cancelling" : input.state,
|
|
11298
|
+
session: input.session
|
|
11299
|
+
}
|
|
11124
11300
|
};
|
|
11125
11301
|
const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
|
|
11126
11302
|
const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
|
|
11127
|
-
if (entry.data.state ===
|
|
11303
|
+
if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
|
|
11128
11304
|
return success(task.data.revision, workspace.data.revision, entry);
|
|
11129
11305
|
const changed = input.store.mutateTaskAndWorkspace({
|
|
11130
11306
|
taskId: task.data.id,
|
|
@@ -11494,7 +11670,7 @@ class WorkitCore {
|
|
|
11494
11670
|
activeWorkerBlocker(task, workspace) {
|
|
11495
11671
|
if (workspace.writer)
|
|
11496
11672
|
return failure("recovery_required", "writer ownership must be released first");
|
|
11497
|
-
if (task.workers.some((entry) =>
|
|
11673
|
+
if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
11498
11674
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
11499
11675
|
return null;
|
|
11500
11676
|
}
|
|
@@ -11731,11 +11907,6 @@ class WorkitCore {
|
|
|
11731
11907
|
if (helper && !helper.ok)
|
|
11732
11908
|
return helper;
|
|
11733
11909
|
const evidence = input.evidence;
|
|
11734
|
-
if (helper?.ok) {
|
|
11735
|
-
const assignment = helper.data.data.assignment;
|
|
11736
|
-
if (evidence.requirementIds.some((id) => !assignment.requirementIds.includes(id)) || assignment.candidateId !== null && evidence.candidateId !== assignment.candidateId && evidence.beforeCandidateId !== assignment.candidateId || !refsWithinScope(evidence.refs, assignment.scope) || !refsWithinScope(evidence.refs, task.data.intent.data.scope))
|
|
11737
|
-
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
11738
|
-
}
|
|
11739
11910
|
if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
|
|
11740
11911
|
return failure("invalid_input", "missing or skipped evidence requires a reason");
|
|
11741
11912
|
if (evidence.kind === "review") {
|
|
@@ -11752,6 +11923,11 @@ class WorkitCore {
|
|
|
11752
11923
|
evidence.beforeCandidateId ??= null;
|
|
11753
11924
|
evidence.candidateId ??= null;
|
|
11754
11925
|
}
|
|
11926
|
+
if (helper?.ok) {
|
|
11927
|
+
const assignment = helper.data.data.assignment;
|
|
11928
|
+
if (evidence.requirementIds.some((id) => !assignment.requirementIds.includes(id)) || assignment.candidateId !== null && evidence.candidateId !== assignment.candidateId && evidence.beforeCandidateId !== assignment.candidateId || !refsWithinScope(evidence.refs, assignment.scope) || !refsWithinScope(evidence.refs, task.data.intent.data.scope))
|
|
11929
|
+
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
11930
|
+
}
|
|
11755
11931
|
const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
|
|
11756
11932
|
for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
|
|
11757
11933
|
if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
|
|
@@ -12258,7 +12434,12 @@ class WorkitCore {
|
|
|
12258
12434
|
return failure("invalid_transition", "worker is not awaiting dispatch");
|
|
12259
12435
|
return success(mutation.revision, null, {
|
|
12260
12436
|
...current,
|
|
12261
|
-
workers: current.workers.map((item) => item.id === input.workerId ? {
|
|
12437
|
+
workers: current.workers.map((item) => item.id === input.workerId ? {
|
|
12438
|
+
...item,
|
|
12439
|
+
recordedAt: mutation.now,
|
|
12440
|
+
provenance: attested.data,
|
|
12441
|
+
data: { ...item.data, state: "dispatching" }
|
|
12442
|
+
} : item)
|
|
12262
12443
|
});
|
|
12263
12444
|
}
|
|
12264
12445
|
});
|
|
@@ -12642,6 +12823,9 @@ class WorkitCore {
|
|
|
12642
12823
|
id: task.id,
|
|
12643
12824
|
revision: task.revision,
|
|
12644
12825
|
workspaceRevision: workspace.data.revision,
|
|
12826
|
+
createdAt: task.createdAt,
|
|
12827
|
+
updatedAt: task.updatedAt,
|
|
12828
|
+
runtime: task.runtime ?? null,
|
|
12645
12829
|
objective: task.intent.data.objective,
|
|
12646
12830
|
status: task.status,
|
|
12647
12831
|
closure: task.closure,
|
|
@@ -12727,7 +12911,7 @@ class WorkitCore {
|
|
|
12727
12911
|
const base = reconcileResume(effectiveView);
|
|
12728
12912
|
if (!base.ok)
|
|
12729
12913
|
return base;
|
|
12730
|
-
const needsReconciliation = view.task.workers.some((entry) =>
|
|
12914
|
+
const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
|
|
12731
12915
|
const blockers = [...base.data.blockers];
|
|
12732
12916
|
if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
|
|
12733
12917
|
blockers.push({
|
|
@@ -12794,7 +12978,8 @@ class WorkitCore {
|
|
|
12794
12978
|
...current,
|
|
12795
12979
|
status,
|
|
12796
12980
|
candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
|
|
12797
|
-
progress:
|
|
12981
|
+
progress: current.progress,
|
|
12982
|
+
pauseReason: status === "paused" ? input.reason ?? null : null
|
|
12798
12983
|
})
|
|
12799
12984
|
});
|
|
12800
12985
|
if (!changed.ok)
|
|
@@ -12830,7 +13015,7 @@ class WorkitCore {
|
|
|
12830
13015
|
return workspace;
|
|
12831
13016
|
if (!workspace.data)
|
|
12832
13017
|
return failure("not_found", "workspace not found");
|
|
12833
|
-
if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state
|
|
13018
|
+
if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
12834
13019
|
return failure("recovery_required", "scope revision requires worker ownership reconciliation");
|
|
12835
13020
|
this.fillRevisions(input, task.data, workspace.data);
|
|
12836
13021
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
@@ -12959,6 +13144,13 @@ var cursorCapabilities = (availability = {}) => {
|
|
|
12959
13144
|
reason: has("subagentStart") ? "Cursor subagentStart enforces explicit reviewer/investigator markers; implementer delegation is unavailable" : "Cursor subagentStart is absent",
|
|
12960
13145
|
refs: [hostRef("subagentStart")]
|
|
12961
13146
|
},
|
|
13147
|
+
{
|
|
13148
|
+
name: "fresh-context-review",
|
|
13149
|
+
surface: "subagentStart",
|
|
13150
|
+
assurance: has("subagentStart") ? "agent_guided" : "unavailable",
|
|
13151
|
+
reason: has("subagentStart") ? "independent review runs as a bounded reviewer subagent; stops lack stable identity, so reviewer exclusivity is evaluated from recorded evidence" : "Cursor subagentStart is unavailable for independent review",
|
|
13152
|
+
refs: [hostRef("subagentStart")]
|
|
13153
|
+
},
|
|
12962
13154
|
{
|
|
12963
13155
|
name: "arbitrary_shell_write",
|
|
12964
13156
|
surface: "unobservable_shell",
|
|
@@ -12992,7 +13184,7 @@ var parseCursorHookInput = (value) => {
|
|
|
12992
13184
|
if (!Array.isArray(roots) || roots.length !== 1 || !roots.every(nonEmpty2))
|
|
12993
13185
|
return { ok: false, error: "exactly one workspace root is required" };
|
|
12994
13186
|
const root = roots[0];
|
|
12995
|
-
if (!
|
|
13187
|
+
if (!path7.isAbsolute(root) || !existsSync4(root))
|
|
12996
13188
|
return { ok: false, error: "workspace root must be an existing absolute path" };
|
|
12997
13189
|
const event = value.hook_event_name;
|
|
12998
13190
|
const conversationId = nonEmpty2(value.conversation_id) ? value.conversation_id : undefined;
|